channel_htlc.aes
1 2 @compiler >= 6 3 4 // Adapted from JellySwap's JellyHTLC for State Channel markets 5 include "List.aes" 6 include "String.aes" 7 8 contract ChannelHTLC = 9 10 record state = { sends : map(hash, send), 11 receives : map(hash, recv), 12 client : address, 13 hub : address, 14 minimum_fee : int, 15 default_timeout : int } 16 17 datatype status = INVALID | ACTIVE | REFUNDED | COLLECTED 18 19 record recv = { 20 amount : int, 21 hash_lock : hash, 22 status : status, 23 sender : address, 24 timeout : int } 25 26 record send = { 27 amount : int, 28 fee : int, 29 hash_lock : hash, 30 status : status, 31 receiver : address, 32 timeout : int } 33 34 entrypoint init( 35 client' : address, 36 minimum_fee' : int, 37 default_timeout' : int ) : state = 38 39 { client = client', 40 hub = Call.caller, 41 minimum_fee = minimum_fee', 42 default_timeout = default_timeout', 43 sends = {}, 44 receives = {} 45 } 46 47 // Called by the client in order to initiate a new send. 48 // Before doing this, the receiver must have provided a hashed secret 49 // (passed as `hash_lock`) 50 // Note that timeout' is the relative timeout 51 payable stateful entrypoint new_send( 52 amount' : int, 53 receiver' : address, 54 fee' : int, 55 timeout' : int, 56 hash_lock' : hash ) : int = 57 58 require( Call.caller == state.client, "Not client" ) 59 require( fee' >= state.minimum_fee, "Fee too low" ) 60 require( amount' > 0, "Invalid amount") 61 require( Call.value == (amount' + fee'), "Wrong value (should be amount + fee)" ) 62 require( timeout' >= 0, "Invalid timeout") 63 64 // Do not include fee in id calculation 65 let id : hash = generate_id(state.client, receiver', amount', hash_lock') 66 67 let abs_timeout : int = abs_timeout(timeout', state.default_timeout, Chain.block_height) 68 69 require(!send_exists(id), "Send entry already exists") 70 71 let _send : send = { 72 amount = amount', 73 fee = fee', 74 hash_lock = hash_lock', 75 status = ACTIVE, 76 receiver = receiver', 77 timeout = abs_timeout } 78 79 put(state{ sends[id] = _send }) 80 abs_timeout 81 82 // Called by the hub on the receiver channel in order to forward 83 // the send request. The `hash_lock` is the one provided by the receiver earlier 84 // There is no fee here, since the fee is paid by the sender and collected by 85 // the hub. 86 // 87 // Note that timeout' here is the absolute timeout 88 payable stateful entrypoint new_receive( 89 amount' : int, 90 sender' : address, 91 timeout' : int, 92 hash_lock' : hash ) : hash = 93 require( Call.caller == state.hub, "Not hub" ) 94 require( amount' > 0, "Invalid amount" ) 95 require( Call.value == amount', "Wrong value (should be amount)") 96 97 let id : hash = generate_id(sender', state.client, amount', hash_lock') 98 99 require(!receive_exists(id), "Receive entry already exists") 100 101 let _recv : recv = { 102 amount = amount', 103 hash_lock = hash_lock', 104 status = ACTIVE, 105 sender = sender', 106 timeout = timeout' } 107 put(state{receives[id] = _recv}) 108 id 109 110 // Called by the hub to collect the collateral + the fee 111 // To be safe, the withdraw should be performed before 112 payable stateful entrypoint collect( 113 receiver' : address, 114 amount' : int, 115 hash_lock' : hash, 116 secret' : hash ) : unit = 117 require( Call.caller == state.hub, "Unauthorized") 118 let id : hash = generate_id(state.client, receiver', amount', hash_lock') 119 let _send : send = state.sends[id] 120 collectable(_send, secret') 121 Chain.spend(state.hub, _send.amount + _send.fee) 122 put(state{sends[id].status = COLLECTED}) 123 124 payable stateful entrypoint receive( 125 sender' : address, 126 amount' : int, 127 hash_lock' : hash, 128 secret' : hash ) : unit = 129 require( Call.caller == state.client, "Unauthorized") 130 let id : hash = generate_id(sender', state.client, amount', hash_lock') 131 let _recv : recv = state.receives[id] 132 receivable(_recv, secret') 133 Chain.spend(state.client, _recv.amount) 134 put(state{ receives[id].status = COLLECTED }) 135 136 // Called by client in order to get a refund. This will only be allowed 137 // under certain circumstances. 138 payable stateful entrypoint refund( 139 receiver' : address, 140 amount' : int, 141 hash_lock' : hash ) : unit = 142 require(Call.caller == state.client, "Unauthorized") 143 let id : hash = generate_id(Call.caller, receiver', amount', hash_lock') 144 let _send : send = state.sends[id] 145 refundable(_send) 146 147 Chain.spend(state.client, _send.amount) 148 Chain.spend(state.hub, _send.fee) 149 150 put(state{ sends[id].status = REFUNDED }) 151 152 // Called by the hub in order to get a refund on the collateral. 153 // This will only be allowed after Timeout + 3 blocks 154 payable stateful entrypoint refund_receive( 155 id' : hash ) : unit = 156 let _recv: recv = state.receives[id'] 157 refundable_receive(_recv) 158 Chain.spend(state.hub, _recv.amount) 159 put(state{ receives[id'].status = REFUNDED }) 160 161 entrypoint get_send_status(id : hash) : status = 162 let _send : send = state.sends[id] 163 _send.status 164 165 entrypoint get_many_send_status(ids : list(hash)) : list(status) = 166 List.map((id) => get_send_status(id), ids) 167 168 entrypoint generate_id(sender : address, receiver : address, 169 amount : int, hash_lock : hash) : hash = 170 let packed_string : string = 171 cc([ 172 Address.to_str(sender), 173 Address.to_str(receiver), 174 Int.to_str(amount), 175 Bytes.to_str(hash_lock)]) 176 177 Crypto.sha256(packed_string) 178 179 entrypoint get_send(id : hash) : send = 180 require(send_exists(id), "SEND_NOT_FOUND") 181 state.sends[id] 182 183 function abs_timeout(timeout' : int, default' : int, height' : int) : int = 184 if (timeout' == 0) 185 height' + default' 186 else 187 height' + timeout' 188 189 function collectable(_send : send, secret : hash) = 190 require(is_active(_send.status), "NOT_ACTIVE") 191 require(_send.hash_lock == Crypto.sha256(secret), "INVALID_SECRET") 192 require(Chain.block_height < _send.timeout + 1, "COLLECT_TIMEOUT") 193 194 function receivable(_recv : recv, secret : hash) = 195 require(is_active(_recv.status), "NOT_ACTIVE") 196 require(_recv.hash_lock == Crypto.sha256(secret), "INVALID_SECRET") 197 require(Chain.block_height < _recv.timeout, "RECEIVE_TIMEOUT") 198 199 function refundable(_send: send) = 200 require(is_active(_send.status), "NOT_ACTIVE") 201 require(state.client == Call.caller, "UNAUTHORIZED") 202 require(Chain.block_height >= _send.timeout + 3, "NOT_YET_REFUNDABLE") 203 204 function refundable_receive(_recv: recv) = 205 require(is_active(_recv.status), "NOT_ACTIVE") 206 require(state.hub == Call.caller, "UNAUTHORIZED") 207 require(Chain.block_height >= _recv.timeout + 3, "NOT_YET_REFUNDABLE") 208 209 function is_active(x : status) : bool = 210 x == ACTIVE 211 212 function send_exists(id : hash) : bool = 213 Map.member(id, state.sends) 214 215 function receive_exists(id : hash) : bool = 216 Map.member(id, state.receives) 217 218 function cc(s :: ss : list(string)) : string = 219 List.foldl(String.concat, s, ss) 220 221 function concat(ss : list(string)) : string = 222 cc(List.intersperse(",", ss))