/ test / contracts / channel_htlc.aes
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))