UNPKG

40.8 kBJavaScriptView Raw
1/*
2 * ISC License (ISC)
3 * Copyright (c) 2018 aeternity developers
4 *
5 * Permission to use, copy, modify, and/or distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14 * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 * PERFORMANCE OF THIS SOFTWARE.
16 */
17/* eslint-disable no-unused-expressions */
18
19import { describe, it, before, after, beforeEach, afterEach } from 'mocha'
20import * as sinon from 'sinon'
21import BigNumber from 'bignumber.js'
22import { getSdk, BaseAe, networkId } from './'
23import { generateKeyPair, encodeBase64Check } from '../../es/utils/crypto'
24import { unpackTx, buildTx, buildTxHash } from '../../es/tx/builder'
25import { decode } from '../../es/tx/builder/helpers'
26import Channel from '../../es/channel'
27import MemoryAccount from '../../es/account/memory'
28
29const wsUrl = process.env.TEST_WS_URL || 'ws://localhost:3014/channel'
30
31const identityContract = `
32contract Identity =
33 entrypoint main(x : int) : int = x
34`
35
36function waitForChannel (channel) {
37 return new Promise(resolve =>
38 channel.on('statusChanged', (status) => {
39 if (status === 'open') {
40 resolve()
41 }
42 })
43 )
44}
45
46describe('Channel', function () {
47 let initiator
48 let responder
49 let initiatorCh
50 let responderCh
51 let responderShouldRejectUpdate
52 let existingChannelId
53 let offchainTx
54 let contractAddress
55 let contractEncodeCall
56 let callerNonce
57 let majorVersion
58 let minorVersion
59 const initiatorSign = sinon.spy((tag, tx) => initiator.signTransaction(tx))
60 const responderSign = sinon.spy((tag, tx) => {
61 if (typeof responderShouldRejectUpdate === 'number') {
62 return responderShouldRejectUpdate
63 }
64 if (responderShouldRejectUpdate) {
65 return null
66 }
67 return responder.signTransaction(tx)
68 })
69 const sharedParams = {
70 url: wsUrl,
71 pushAmount: 3,
72 initiatorAmount: BigNumber('100e18'),
73 responderAmount: BigNumber('100e18'),
74 channelReserve: 0,
75 ttl: 10000,
76 host: 'localhost',
77 port: 3001,
78 lockPeriod: 1,
79 statePassword: 'correct horse battery staple',
80 debug: false
81 }
82
83 before(async function () {
84 initiator = await getSdk()
85 responder = await BaseAe({ nativeMode: true, networkId, accounts: [] })
86 await responder.addAccount(MemoryAccount({ keypair: generateKeyPair() }), { select: true })
87 sharedParams.initiatorId = await initiator.address()
88 sharedParams.responderId = await responder.address()
89 await initiator.spend(BigNumber('500e18').toString(), await responder.address())
90 const version = initiator.getNodeInfo().version.split(/[.-]/).map(i => parseInt(i, 10))
91 majorVersion = version[0]
92 minorVersion = version[1]
93 })
94
95 after(() => {
96 initiatorCh.disconnect()
97 responderCh.disconnect()
98 })
99
100 beforeEach(() => {
101 responderShouldRejectUpdate = false
102 })
103
104 afterEach(() => {
105 initiatorSign.resetHistory()
106 responderSign.resetHistory()
107 })
108
109 it('can open a channel', async () => {
110 initiatorCh = await Channel({
111 ...sharedParams,
112 role: 'initiator',
113 sign: initiatorSign
114 })
115 responderCh = await Channel({
116 ...sharedParams,
117 role: 'responder',
118 sign: responderSign
119 })
120 await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)])
121 initiatorCh.round().should.equal(1)
122 responderCh.round().should.equal(1)
123 sinon.assert.calledOnce(initiatorSign)
124 sinon.assert.calledWithExactly(initiatorSign, sinon.match('initiator_sign'), sinon.match.string)
125 sinon.assert.calledOnce(responderSign)
126 sinon.assert.calledWithExactly(responderSign, sinon.match('responder_sign'), sinon.match.string)
127 const expectedTxParams = {
128 initiator: await initiator.address(),
129 responder: await responder.address(),
130 initiatorAmount: sharedParams.initiatorAmount.toString(),
131 responderAmount: sharedParams.responderAmount.toString(),
132 channelReserve: sharedParams.channelReserve.toString(),
133 lockPeriod: sharedParams.lockPeriod.toString()
134 }
135 const { txType: initiatorTxType, tx: initiatorTx } = unpackTx(initiatorSign.firstCall.args[1])
136 const { txType: responderTxType, tx: responderTx } = unpackTx(responderSign.firstCall.args[1])
137 initiatorTxType.should.equal('channelCreate')
138 initiatorTx.should.eql({ ...initiatorTx, ...expectedTxParams })
139 responderTxType.should.equal('channelCreate')
140 responderTx.should.eql({ ...responderTx, ...expectedTxParams })
141 })
142
143 it('can post update and accept', async () => {
144 responderShouldRejectUpdate = false
145 const roundBefore = initiatorCh.round()
146 const sign = sinon.spy(initiator.signTransaction.bind(initiator))
147 const amount = 1
148 const result = await initiatorCh.update(
149 await initiator.address(),
150 await responder.address(),
151 amount,
152 sign
153 )
154 initiatorCh.round().should.equal(roundBefore + 1)
155 result.accepted.should.equal(true)
156 result.signedTx.should.be.a('string')
157 sinon.assert.notCalled(initiatorSign)
158 sinon.assert.calledOnce(responderSign)
159 sinon.assert.calledWithExactly(
160 responderSign,
161 sinon.match('update_ack'),
162 sinon.match.string,
163 sinon.match({
164 updates: sinon.match([{
165 amount: sinon.match(amount),
166 from: sinon.match(await initiator.address()),
167 to: sinon.match(await responder.address()),
168 op: sinon.match('OffChainTransfer')
169 }])
170 })
171 )
172 sinon.assert.calledOnce(sign)
173 sinon.assert.calledWithExactly(
174 sign,
175 sinon.match.string,
176 sinon.match({
177 updates: sinon.match([{
178 amount: sinon.match(amount),
179 from: sinon.match(await initiator.address()),
180 to: sinon.match(await responder.address()),
181 op: sinon.match('OffChainTransfer')
182 }])
183 })
184 )
185 const { txType } = unpackTx(sign.firstCall.args[0])
186 txType.should.equal('channelOffChain')
187 sign.firstCall.args[1].should.eql({
188 updates: [
189 {
190 amount,
191 from: await initiator.address(),
192 to: await responder.address(),
193 op: 'OffChainTransfer'
194 }
195 ]
196 })
197 })
198
199 it('can post update and reject', async () => {
200 responderShouldRejectUpdate = true
201 const sign = sinon.spy(initiator.signTransaction.bind(initiator))
202 const amount = 1
203 const roundBefore = initiatorCh.round()
204 const result = await initiatorCh.update(
205 await responder.address(),
206 await initiator.address(),
207 amount,
208 sign
209 )
210 result.accepted.should.equal(false)
211 initiatorCh.round().should.equal(roundBefore)
212 sinon.assert.notCalled(initiatorSign)
213 sinon.assert.calledOnce(responderSign)
214 sinon.assert.calledWithExactly(
215 responderSign,
216 sinon.match('update_ack'),
217 sinon.match.string,
218 sinon.match({
219 updates: sinon.match([{
220 amount: sinon.match(amount),
221 from: sinon.match(await responder.address()),
222 to: sinon.match(await initiator.address()),
223 op: sinon.match('OffChainTransfer')
224 }])
225 })
226 )
227 sinon.assert.calledOnce(sign)
228 sinon.assert.calledWithExactly(
229 sign,
230 sinon.match.string,
231 sinon.match({
232 updates: sinon.match([{
233 amount: sinon.match(amount),
234 from: sinon.match(await responder.address()),
235 to: sinon.match(await initiator.address()),
236 op: sinon.match('OffChainTransfer')
237 }])
238 })
239 )
240 const { txType } = unpackTx(sign.firstCall.args[0])
241 txType.should.equal('channelOffChain')
242 sign.firstCall.args[1].should.eql({
243 updates: [
244 {
245 amount,
246 from: await responder.address(),
247 to: await initiator.address(),
248 op: 'OffChainTransfer'
249 }
250 ]
251 })
252 })
253
254 it('can abort update sign request', async () => {
255 const errorCode = 12345
256 const result = await initiatorCh.update(
257 await initiator.address(),
258 await responder.address(),
259 100,
260 () => errorCode
261 )
262 result.should.eql({ accepted: false })
263 })
264
265 it('can abort update with custom error code', async () => {
266 responderShouldRejectUpdate = 1234
267 const result = await initiatorCh.update(
268 await initiator.address(),
269 await responder.address(),
270 100,
271 tx => initiator.signTransaction(tx)
272 )
273 result.should.eql({
274 accepted: false,
275 errorCode: responderShouldRejectUpdate,
276 errorMessage: 'user-defined'
277 })
278 })
279
280 it('can post bignumber update and accept', async () => {
281 responderShouldRejectUpdate = false
282 const sign = sinon.spy(initiator.signTransaction.bind(initiator))
283 const amount = BigNumber('10e18')
284 const result = await initiatorCh.update(
285 await initiator.address(),
286 await responder.address(),
287 amount,
288 sign
289 )
290 result.accepted.should.equal(true)
291 result.signedTx.should.be.a('string')
292 sinon.assert.notCalled(initiatorSign)
293 sinon.assert.calledOnce(responderSign)
294 sinon.assert.calledWithExactly(
295 responderSign,
296 sinon.match('update_ack'),
297 sinon.match.string,
298 sinon.match({
299 updates: sinon.match([{
300 amount: sinon.match(amount.toString()),
301 from: sinon.match(await initiator.address()),
302 to: sinon.match(await responder.address()),
303 op: sinon.match('OffChainTransfer')
304 }])
305 })
306 )
307 sinon.assert.calledOnce(sign)
308 sinon.assert.calledWithExactly(
309 sign,
310 sinon.match.string,
311 sinon.match({
312 updates: sinon.match([{
313 amount: sinon.match(amount.toString()),
314 from: sinon.match(await initiator.address()),
315 to: sinon.match(await responder.address()),
316 op: sinon.match('OffChainTransfer')
317 }])
318 })
319 )
320 const { txType } = unpackTx(sign.firstCall.args[0])
321 txType.should.equal('channelOffChain')
322 sign.firstCall.args[1].should.eql({
323 updates: [
324 {
325 amount: amount.toString(),
326 from: await initiator.address(),
327 to: await responder.address(),
328 op: 'OffChainTransfer'
329 }
330 ]
331 })
332 })
333
334 it('can post update with metadata', async () => {
335 responderShouldRejectUpdate = true
336 const meta = 'meta 1'
337 const sign = sinon.spy(initiator.signTransaction.bind(initiator))
338 await initiatorCh.update(
339 await initiator.address(),
340 await responder.address(),
341 100,
342 sign,
343 [meta]
344 )
345 sign.firstCall.args[1].updates.should.eql([
346 sign.firstCall.args[1].updates[0],
347 { data: meta, op: 'OffChainMeta' }
348 ])
349 responderSign.firstCall.args[2].updates.should.eql([
350 responderSign.firstCall.args[2].updates[0],
351 { data: meta, op: 'OffChainMeta' }
352 ])
353 })
354
355 it('can get proof of inclusion', async () => {
356 const initiatorAddr = await initiator.address()
357 const responderAddr = await responder.address()
358 const params = { accounts: [initiatorAddr, responderAddr] }
359 const initiatorPoi = await initiatorCh.poi(params)
360 const responderPoi = await responderCh.poi(params)
361 initiatorPoi.should.be.a('string')
362 responderPoi.should.be.a('string')
363 const unpackedInitiatorPoi = unpackTx(decode(initiatorPoi, 'pi'), true)
364 const unpackedResponderPoi = unpackTx(decode(responderPoi, 'pi'), true)
365 buildTx(unpackedInitiatorPoi.tx, unpackedInitiatorPoi.txType, { prefix: 'pi' }).tx.should.equal(initiatorPoi)
366 buildTx(unpackedResponderPoi.tx, unpackedResponderPoi.txType, { prefix: 'pi' }).tx.should.equal(responderPoi)
367 })
368
369 it('can get balances', async () => {
370 const initiatorAddr = await initiator.address()
371 const responderAddr = await responder.address()
372 const addresses = [initiatorAddr, responderAddr]
373 const initiatorBalances = await initiatorCh.balances(addresses)
374 const responderBalances = await responderCh.balances(addresses)
375 initiatorBalances.should.be.an('object')
376 responderBalances.should.be.an('object')
377 initiatorBalances[initiatorAddr].should.be.a('string')
378 initiatorBalances[responderAddr].should.be.a('string')
379 responderBalances[initiatorAddr].should.be.a('string')
380 responderBalances[responderAddr].should.be.a('string')
381 })
382
383 it('can send a message', async () => {
384 const sender = await initiator.address()
385 const recipient = await responder.address()
386 const info = 'hello world'
387 initiatorCh.sendMessage(info, recipient)
388 const message = await new Promise(resolve => responderCh.on('message', resolve))
389 message.should.eql({
390 channel_id: initiatorCh.id(),
391 from: sender,
392 to: recipient,
393 info
394 })
395 })
396
397 it('can request a withdraw and accept', async () => {
398 const sign = sinon.spy(initiator.signTransaction.bind(initiator))
399 const amount = BigNumber('2e18')
400 const onOnChainTx = sinon.spy()
401 const onOwnWithdrawLocked = sinon.spy()
402 const onWithdrawLocked = sinon.spy()
403 responderShouldRejectUpdate = false
404 const roundBefore = initiatorCh.round()
405 const result = await initiatorCh.withdraw(
406 amount,
407 sign,
408 { onOnChainTx, onOwnWithdrawLocked, onWithdrawLocked }
409 )
410 result.should.eql({ accepted: true, signedTx: (await initiatorCh.state()).signedTx })
411 initiatorCh.round().should.equal(roundBefore + 1)
412 sinon.assert.called(onOnChainTx)
413 sinon.assert.calledWithExactly(onOnChainTx, sinon.match.string)
414 sinon.assert.calledOnce(onOwnWithdrawLocked)
415 sinon.assert.calledOnce(onWithdrawLocked)
416 sinon.assert.notCalled(initiatorSign)
417 sinon.assert.calledOnce(responderSign)
418 sinon.assert.calledWithExactly(
419 responderSign,
420 sinon.match('withdraw_ack'),
421 sinon.match.string,
422 sinon.match({
423 updates: [{
424 amount: amount.toString(),
425 op: 'OffChainWithdrawal',
426 to: await initiator.address()
427 }]
428 })
429 )
430 sinon.assert.calledOnce(sign)
431 sinon.assert.calledWithExactly(
432 sign,
433 sinon.match.string,
434 sinon.match({
435 updates: [{
436 amount: amount.toString(),
437 op: 'OffChainWithdrawal',
438 to: await initiator.address()
439 }]
440 })
441 )
442 const { txType, tx } = unpackTx(sign.firstCall.args[0])
443 txType.should.equal('channelWithdraw')
444 tx.should.eql({
445 ...tx,
446 toId: await initiator.address(),
447 amount: amount.toString()
448 })
449 })
450
451 it('can request a withdraw and reject', async () => {
452 const sign = sinon.spy(initiator.signTransaction.bind(initiator))
453 const amount = BigNumber('2e18')
454 const onOnChainTx = sinon.spy()
455 const onOwnWithdrawLocked = sinon.spy()
456 const onWithdrawLocked = sinon.spy()
457 responderShouldRejectUpdate = true
458 const roundBefore = initiatorCh.round()
459 const result = await initiatorCh.withdraw(
460 amount,
461 sign,
462 { onOnChainTx, onOwnWithdrawLocked, onWithdrawLocked }
463 )
464 initiatorCh.round().should.equal(roundBefore)
465 result.should.eql({ ...result, accepted: false })
466 sinon.assert.notCalled(onOnChainTx)
467 sinon.assert.notCalled(onOwnWithdrawLocked)
468 sinon.assert.notCalled(onWithdrawLocked)
469 sinon.assert.notCalled(initiatorSign)
470 sinon.assert.calledOnce(responderSign)
471 sinon.assert.calledWithExactly(
472 responderSign,
473 sinon.match('withdraw_ack'),
474 sinon.match.string,
475 sinon.match({
476 updates: [{
477 amount: amount.toString(),
478 op: 'OffChainWithdrawal',
479 to: await initiator.address()
480 }]
481 })
482 )
483 sinon.assert.calledOnce(sign)
484 sinon.assert.calledWithExactly(
485 sign,
486 sinon.match.string,
487 sinon.match({
488 updates: [{
489 amount: amount.toString(),
490 op: 'OffChainWithdrawal',
491 to: await initiator.address()
492 }]
493 })
494 )
495 const { txType, tx } = unpackTx(sign.firstCall.args[0])
496 txType.should.equal('channelWithdraw')
497 tx.should.eql({
498 ...tx,
499 toId: await initiator.address(),
500 amount: amount.toString()
501 })
502 })
503
504 it('can abort withdraw sign request', async () => {
505 const errorCode = 12345
506 const result = await initiatorCh.withdraw(
507 100,
508 () => errorCode
509 )
510 result.should.eql({ accepted: false })
511 })
512
513 it('can abort withdraw with custom error code', async () => {
514 responderShouldRejectUpdate = 12345
515 const result = await initiatorCh.withdraw(
516 100,
517 tx => initiator.signTransaction(tx)
518 )
519 result.should.eql({
520 accepted: false,
521 errorCode: responderShouldRejectUpdate,
522 errorMessage: 'user-defined'
523 })
524 })
525
526 it('can request a deposit and accept', async () => {
527 const sign = sinon.spy(initiator.signTransaction.bind(initiator))
528 const amount = BigNumber('2e18')
529 const onOnChainTx = sinon.spy()
530 const onOwnDepositLocked = sinon.spy()
531 const onDepositLocked = sinon.spy()
532 responderShouldRejectUpdate = false
533 const roundBefore = initiatorCh.round()
534 const result = await initiatorCh.deposit(
535 amount,
536 sign,
537 { onOnChainTx, onOwnDepositLocked, onDepositLocked }
538 )
539 result.should.eql({ accepted: true, signedTx: (await initiatorCh.state()).signedTx })
540 initiatorCh.round().should.equal(roundBefore + 1)
541 sinon.assert.called(onOnChainTx)
542 sinon.assert.calledWithExactly(onOnChainTx, sinon.match.string)
543 sinon.assert.calledOnce(onOwnDepositLocked)
544 sinon.assert.calledOnce(onDepositLocked)
545 sinon.assert.notCalled(initiatorSign)
546 sinon.assert.calledOnce(responderSign)
547 sinon.assert.calledWithExactly(
548 responderSign,
549 sinon.match('deposit_ack'),
550 sinon.match.string,
551 sinon.match({
552 updates: sinon.match([{
553 amount: amount.toString(),
554 op: 'OffChainDeposit',
555 from: await initiator.address()
556 }])
557 })
558 )
559 sinon.assert.calledOnce(sign)
560 sinon.assert.calledWithExactly(
561 sign,
562 sinon.match.string,
563 sinon.match({
564 updates: sinon.match([{
565 amount: amount.toString(),
566 op: 'OffChainDeposit',
567 from: await initiator.address()
568 }])
569 })
570 )
571 const { txType, tx } = unpackTx(sign.firstCall.args[0])
572 txType.should.equal('channelDeposit')
573 tx.should.eql({
574 ...tx,
575 fromId: await initiator.address(),
576 amount: amount.toString()
577 })
578 })
579
580 it('can request a deposit and reject', async () => {
581 const sign = sinon.spy(initiator.signTransaction.bind(initiator))
582 const amount = BigNumber('2e18')
583 const onOnChainTx = sinon.spy()
584 const onOwnDepositLocked = sinon.spy()
585 const onDepositLocked = sinon.spy()
586 responderShouldRejectUpdate = true
587 const roundBefore = initiatorCh.round()
588 const result = await initiatorCh.deposit(
589 amount,
590 sign,
591 { onOnChainTx, onOwnDepositLocked, onDepositLocked }
592 )
593 initiatorCh.round().should.equal(roundBefore)
594 result.should.eql({ ...result, accepted: false })
595 sinon.assert.notCalled(onOnChainTx)
596 sinon.assert.notCalled(onOwnDepositLocked)
597 sinon.assert.notCalled(onDepositLocked)
598 sinon.assert.notCalled(initiatorSign)
599 sinon.assert.calledOnce(responderSign)
600 sinon.assert.calledWithExactly(
601 responderSign,
602 sinon.match('deposit_ack'),
603 sinon.match.string,
604 sinon.match({
605 updates: [{
606 amount: amount.toString(),
607 op: 'OffChainDeposit',
608 from: await initiator.address()
609 }]
610 })
611 )
612 const { txType, tx } = unpackTx(sign.firstCall.args[0])
613 txType.should.equal('channelDeposit')
614 tx.should.eql({
615 ...tx,
616 fromId: await initiator.address(),
617 amount: amount.toString()
618 })
619 })
620
621 it('can abort deposit sign request', async () => {
622 const errorCode = 12345
623 const result = await initiatorCh.deposit(
624 100,
625 () => errorCode
626 )
627 result.should.eql({ accepted: false })
628 })
629
630 it('can abort deposit with custom error code', async () => {
631 responderShouldRejectUpdate = 12345
632 const result = await initiatorCh.deposit(
633 100,
634 tx => initiator.signTransaction(tx)
635 )
636 result.should.eql({
637 accepted: false,
638 errorCode: responderShouldRejectUpdate,
639 errorMessage: 'user-defined'
640 })
641 })
642
643 it('can close a channel', async () => {
644 const sign = sinon.spy(initiator.signTransaction.bind(initiator))
645 const result = await initiatorCh.shutdown(sign)
646 result.should.be.a('string')
647 sinon.assert.notCalled(initiatorSign)
648 sinon.assert.calledOnce(responderSign)
649 sinon.assert.calledWithExactly(
650 responderSign,
651 sinon.match('shutdown_sign_ack'),
652 sinon.match.string,
653 sinon.match.any
654 )
655 sinon.assert.calledOnce(sign)
656 sinon.assert.calledWithExactly(sign, sinon.match.string)
657 const { txType, tx } = unpackTx(sign.firstCall.args[0])
658 txType.should.equal('channelCloseMutual')
659 tx.should.eql({
660 ...tx,
661 fromId: await initiator.address()
662 // TODO: check `initiatorAmountFinal` and `responderAmountFinal`
663 })
664 })
665
666 it('can leave a channel', async () => {
667 initiatorCh.disconnect()
668 responderCh.disconnect()
669 initiatorCh = await Channel({
670 ...sharedParams,
671 role: 'initiator',
672 sign: initiatorSign
673 })
674 responderCh = await Channel({
675 ...sharedParams,
676 role: 'responder',
677 sign: responderSign
678 })
679 await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)])
680 initiatorCh.round() // existingChannelRound
681 const result = await initiatorCh.leave()
682 result.channelId.should.be.a('string')
683 result.signedTx.should.be.a('string')
684 existingChannelId = result.channelId
685 offchainTx = result.signedTx
686 })
687
688 it('can reestablish a channel', async () => {
689 const existingChannelIdKey =
690 majorVersion > 5 || (majorVersion === 5 && minorVersion >= 2)
691 ? 'existingFsmId'
692 : 'existingChannelId'
693 initiatorCh = await Channel({
694 ...sharedParams,
695 role: 'initiator',
696 port: 3002,
697 [existingChannelIdKey]: existingChannelId,
698 offchainTx,
699 sign: initiatorSign
700 })
701 await waitForChannel(initiatorCh)
702 // TODO: why node doesn't return signed_tx when channel is reestablished?
703 // initiatorCh.round().should.equal(existingChannelRound)
704 sinon.assert.notCalled(initiatorSign)
705 sinon.assert.notCalled(responderSign)
706 })
707
708 it('can solo close a channel', async () => {
709 initiatorCh.disconnect()
710 responderCh.disconnect()
711 initiatorCh = await Channel({
712 ...sharedParams,
713 role: 'initiator',
714 port: 3003,
715 sign: initiatorSign
716 })
717 responderCh = await Channel({
718 ...sharedParams,
719 role: 'responder',
720 port: 3003,
721 sign: responderSign
722 })
723 await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)])
724
725 const initiatorAddr = await initiator.address()
726 const responderAddr = await responder.address()
727 const { signedTx } = await initiatorCh.update(
728 await initiator.address(),
729 await responder.address(),
730 BigNumber('3e18'),
731 tx => initiator.signTransaction(tx)
732 )
733 const poi = await initiatorCh.poi({
734 accounts: [initiatorAddr, responderAddr]
735 })
736 const balances = await initiatorCh.balances([initiatorAddr, responderAddr])
737 const initiatorBalanceBeforeClose = await initiator.balance(initiatorAddr)
738 const responderBalanceBeforeClose = await responder.balance(responderAddr)
739 const closeSoloTx = await initiator.channelCloseSoloTx({
740 channelId: await initiatorCh.id(),
741 fromId: initiatorAddr,
742 poi,
743 payload: signedTx
744 })
745 const closeSoloTxFee = unpackTx(closeSoloTx).tx.fee
746 await initiator.sendTransaction(await initiator.signTransaction(closeSoloTx), { waitMined: true })
747 const settleTx = await initiator.channelSettleTx({
748 channelId: await initiatorCh.id(),
749 fromId: initiatorAddr,
750 initiatorAmountFinal: balances[initiatorAddr],
751 responderAmountFinal: balances[responderAddr]
752 })
753 const settleTxFee = unpackTx(settleTx).tx.fee
754 await initiator.sendTransaction(await initiator.signTransaction(settleTx), { waitMined: true })
755 const initiatorBalanceAfterClose = await initiator.balance(initiatorAddr)
756 const responderBalanceAfterClose = await responder.balance(responderAddr)
757 new BigNumber(initiatorBalanceAfterClose).minus(initiatorBalanceBeforeClose).plus(closeSoloTxFee).plus(settleTxFee).isEqualTo(
758 new BigNumber(balances[initiatorAddr])
759 ).should.be.equal(true)
760 new BigNumber(responderBalanceAfterClose).minus(responderBalanceBeforeClose).isEqualTo(
761 new BigNumber(balances[responderAddr])
762 ).should.be.equal(true)
763 })
764
765 it('can dispute via slash tx', async () => {
766 const initiatorAddr = await initiator.address()
767 const responderAddr = await responder.address()
768 initiatorCh.disconnect()
769 responderCh.disconnect()
770 initiatorCh = await Channel({
771 ...sharedParams,
772 lockPeriod: 5,
773 role: 'initiator',
774 sign: initiatorSign,
775 port: 3004
776 })
777 responderCh = await Channel({
778 ...sharedParams,
779 lockPeriod: 5,
780 role: 'responder',
781 sign: responderSign,
782 port: 3004
783 })
784 await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)])
785 const initiatorBalanceBeforeClose = await initiator.balance(initiatorAddr)
786 const responderBalanceBeforeClose = await responder.balance(responderAddr)
787 const oldUpdate = await initiatorCh.update(initiatorAddr, responderAddr, 100, (tx) => initiator.signTransaction(tx))
788 const oldPoi = await initiatorCh.poi({
789 accounts: [initiatorAddr, responderAddr]
790 })
791 const recentUpdate = await initiatorCh.update(initiatorAddr, responderAddr, 100, (tx) => initiator.signTransaction(tx))
792 const recentPoi = await responderCh.poi({
793 accounts: [initiatorAddr, responderAddr]
794 })
795 const recentBalances = await responderCh.balances([initiatorAddr, responderAddr])
796 const closeSoloTx = await initiator.channelCloseSoloTx({
797 channelId: initiatorCh.id(),
798 fromId: initiatorAddr,
799 poi: oldPoi,
800 payload: oldUpdate.signedTx
801 })
802 const closeSoloTxFee = unpackTx(closeSoloTx).tx.fee
803 await initiator.sendTransaction(await initiator.signTransaction(closeSoloTx), { waitMined: true })
804 const slashTx = await responder.channelSlashTx({
805 channelId: responderCh.id(),
806 fromId: responderAddr,
807 poi: recentPoi,
808 payload: recentUpdate.signedTx
809 })
810 const slashTxFee = unpackTx(slashTx).tx.fee
811 await responder.sendTransaction(await responder.signTransaction(slashTx), { waitMined: true })
812 const settleTx = await responder.channelSettleTx({
813 channelId: responderCh.id(),
814 fromId: responderAddr,
815 initiatorAmountFinal: recentBalances[initiatorAddr],
816 responderAmountFinal: recentBalances[responderAddr]
817 })
818 const settleTxFee = unpackTx(settleTx).tx.fee
819 await responder.sendTransaction(await responder.signTransaction(settleTx), { waitMined: true })
820 const initiatorBalanceAfterClose = await initiator.balance(initiatorAddr)
821 const responderBalanceAfterClose = await responder.balance(responderAddr)
822 new BigNumber(initiatorBalanceAfterClose).minus(initiatorBalanceBeforeClose).plus(closeSoloTxFee).isEqualTo(
823 new BigNumber(recentBalances[initiatorAddr])
824 ).should.be.equal(true)
825 new BigNumber(responderBalanceAfterClose).minus(responderBalanceBeforeClose).plus(slashTxFee).plus(settleTxFee).isEqualTo(
826 new BigNumber(recentBalances[responderAddr])
827 ).should.be.equal(true)
828 })
829
830 it('can create a contract and accept', async () => {
831 initiatorCh.disconnect()
832 responderCh.disconnect()
833 initiatorCh = await Channel({
834 ...sharedParams,
835 role: 'initiator',
836 port: 3005,
837 sign: initiatorSign
838 })
839 responderCh = await Channel({
840 ...sharedParams,
841 role: 'responder',
842 port: 3005,
843 sign: responderSign
844 })
845 await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)])
846 const code = await initiator.compileContractAPI(identityContract, { backend: 'aevm' })
847 const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', [], { backend: 'aevm' })
848 const roundBefore = initiatorCh.round()
849 const result = await initiatorCh.createContract({
850 code,
851 callData,
852 deposit: 1000,
853 vmVersion: 6,
854 abiVersion: 1
855 }, async (tx) => initiator.signTransaction(tx))
856 result.should.eql({ accepted: true, address: result.address, signedTx: (await initiatorCh.state()).signedTx })
857 initiatorCh.round().should.equal(roundBefore + 1)
858 contractAddress = result.address
859 contractEncodeCall = (method, args) => initiator.contractEncodeCallDataAPI(identityContract, method, args, { backend: 'aevm' })
860 })
861
862 it('can create a contract and reject', async () => {
863 responderShouldRejectUpdate = true
864 const code = await initiator.compileContractAPI(identityContract, { backend: 'aevm' })
865 const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', [], { backend: 'aevm' })
866 const roundBefore = initiatorCh.round()
867 const result = await initiatorCh.createContract({
868 code,
869 callData,
870 deposit: BigNumber('10e18'),
871 vmVersion: 4,
872 abiVersion: 1
873 }, async (tx) => initiator.signTransaction(tx))
874 initiatorCh.round().should.equal(roundBefore)
875 result.should.eql({ ...result, accepted: false })
876 })
877
878 it('can abort contract sign request', async () => {
879 const errorCode = 12345
880 const code = await initiator.compileContractAPI(identityContract, { backend: 'aevm' })
881 const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', [], { backend: 'aevm' })
882 const result = await initiatorCh.createContract({
883 code,
884 callData,
885 deposit: BigNumber('10e18'),
886 vmVersion: 4,
887 abiVersion: 1
888 }, () => errorCode)
889 result.should.eql({ accepted: false })
890 })
891
892 it('can abort contract with custom error code', async () => {
893 responderShouldRejectUpdate = 12345
894 const code = await initiator.compileContractAPI(identityContract, { backend: 'aevm' })
895 const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', [], { backend: 'aevm' })
896 const result = await initiatorCh.createContract({
897 code,
898 callData,
899 deposit: BigNumber('10e18'),
900 vmVersion: 4,
901 abiVersion: 1
902 }, async (tx) => initiator.signTransaction(tx))
903 result.should.eql({
904 accepted: false,
905 errorCode: responderShouldRejectUpdate,
906 errorMessage: 'user-defined'
907 })
908 })
909
910 it('can call a contract and accept', async () => {
911 const roundBefore = initiatorCh.round()
912 const result = await initiatorCh.callContract({
913 amount: 0,
914 callData: await contractEncodeCall('main', ['42']),
915 contract: contractAddress,
916 abiVersion: 1
917 }, async (tx) => initiator.signTransaction(tx))
918 result.should.eql({ accepted: true, signedTx: (await initiatorCh.state()).signedTx })
919 initiatorCh.round().should.equal(roundBefore + 1)
920 callerNonce = initiatorCh.round()
921 })
922
923 it('can call a force progress', async () => {
924 const forceTx = await initiatorCh.forceProgress({
925 amount: 0,
926 callData: await contractEncodeCall('main', ['42']),
927 contract: contractAddress,
928 abiVersion: 1
929 }, async (tx) => initiator.signTransaction(tx))
930 console.log('after done')
931 const hash = buildTxHash(forceTx.tx)
932 const txInfo = await initiator.tx(hash)
933 console.log(txInfo)
934 })
935
936 it('can call a contract and reject', async () => {
937 responderShouldRejectUpdate = true
938 const roundBefore = initiatorCh.round()
939 const result = await initiatorCh.callContract({
940 amount: 0,
941 callData: await contractEncodeCall('main', ['42']),
942 contract: contractAddress,
943 abiVersion: 1
944 }, async (tx) => initiator.signTransaction(tx))
945 initiatorCh.round().should.equal(roundBefore)
946 result.should.eql({ ...result, accepted: false })
947 })
948
949 it('can abort contract call sign request', async () => {
950 const errorCode = 12345
951 const result = await initiatorCh.callContract({
952 amount: 0,
953 callData: await contractEncodeCall('main', ['42']),
954 contract: contractAddress,
955 abiVersion: 1
956 }, () => errorCode)
957 result.should.eql({ accepted: false })
958 })
959
960 it('can abort contract call with custom error code', async () => {
961 responderShouldRejectUpdate = 12345
962 const result = await initiatorCh.callContract({
963 amount: 0,
964 callData: await contractEncodeCall('main', ['42']),
965 contract: contractAddress,
966 abiVersion: 1
967 }, async (tx) => initiator.signTransaction(tx))
968 result.should.eql({
969 accepted: false,
970 errorCode: responderShouldRejectUpdate,
971 errorMessage: 'user-defined'
972 })
973 })
974
975 it('can get contract call', async () => {
976 const result = await initiatorCh.getContractCall({
977 caller: await initiator.address(),
978 contract: contractAddress,
979 round: callerNonce
980 })
981 result.should.eql({
982 callerId: await initiator.address(),
983 callerNonce,
984 contractId: contractAddress,
985 gasPrice: result.gasPrice,
986 gasUsed: result.gasUsed,
987 height: result.height,
988 log: result.log,
989 returnType: 'ok',
990 returnValue: result.returnValue
991 })
992 const value = await initiator.contractDecodeDataAPI('int', result.returnValue)
993 value.should.eql({ type: 'word', value: 42 })
994 })
995
996 it('can call a contract using dry-run', async () => {
997 const result = await initiatorCh.callContractStatic({
998 amount: 0,
999 callData: await contractEncodeCall('main', ['42']),
1000 contract: contractAddress,
1001 abiVersion: 1
1002 })
1003 result.should.eql({
1004 callerId: await initiator.address(),
1005 callerNonce: result.callerNonce,
1006 contractId: contractAddress,
1007 gasPrice: result.gasPrice,
1008 gasUsed: result.gasUsed,
1009 height: result.height,
1010 log: result.log,
1011 returnType: 'ok',
1012 returnValue: result.returnValue
1013 })
1014 const value = await initiator.contractDecodeDataAPI('int', result.returnValue)
1015 value.should.eql({ type: 'word', value: 42 })
1016 })
1017
1018 it('can clean contract calls', async () => {
1019 await initiatorCh.cleanContractCalls()
1020 initiatorCh.getContractCall({
1021 caller: await initiator.address(),
1022 contract: contractAddress,
1023 round: callerNonce
1024 }).should.eventually.be.rejected
1025 })
1026
1027 it('can get contract state', async () => {
1028 const result = await initiatorCh.getContractState(contractAddress)
1029 result.should.eql({
1030 contract: {
1031 abiVersion: 1,
1032 active: true,
1033 deposit: 1000,
1034 id: contractAddress,
1035 ownerId: await initiator.address(),
1036 referrerIds: [],
1037 vmVersion: 6
1038 },
1039 contractState: result.contractState
1040 })
1041 // TODO: contractState deserialization
1042 })
1043 // TODO fix this
1044 it.skip('can post snapshot solo transaction', async () => {
1045 const snapshotSoloTx = await initiator.channelSnapshotSoloTx({
1046 channelId: initiatorCh.id(),
1047 fromId: await initiator.address(),
1048 payload: (await initiatorCh.state()).signedTx
1049 })
1050 await initiator.sendTransaction(await initiator.signTransaction(snapshotSoloTx), { waitMined: true })
1051 })
1052
1053 it('can reconnect', async () => {
1054 initiatorCh.disconnect()
1055 responderCh.disconnect()
1056 initiatorCh = await Channel({
1057 ...sharedParams,
1058 role: 'initiator',
1059 port: 3006,
1060 sign: initiatorSign
1061 })
1062 responderCh = await Channel({
1063 ...sharedParams,
1064 role: 'responder',
1065 port: 3006,
1066 sign: responderSign
1067 })
1068 await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)])
1069 const result = await initiatorCh.update(
1070 await initiator.address(),
1071 await responder.address(),
1072 100,
1073 tx => initiator.signTransaction(tx)
1074 )
1075 result.accepted.should.be.true
1076 const channelId = await initiatorCh.id()
1077 const round = initiatorCh.round()
1078 let ch
1079 if (majorVersion > 5 || (majorVersion === 5 && minorVersion >= 2)) {
1080 const fsmId = initiatorCh.fsmId()
1081 initiatorCh.disconnect()
1082 ch = await Channel({
1083 url: sharedParams.url,
1084 host: sharedParams.host,
1085 port: 3006,
1086 role: 'initiator',
1087 existingChannelId: channelId,
1088 existingFsmId: fsmId
1089 })
1090 await waitForChannel(ch)
1091 ch.fsmId().should.equal(fsmId)
1092 } else {
1093 initiatorCh.disconnect()
1094 ch = await Channel.reconnect({
1095 ...sharedParams,
1096 role: 'initiator',
1097 port: 3006,
1098 sign: initiatorSign
1099 }, {
1100 channelId,
1101 round,
1102 role: 'initiator',
1103 pubkey: await initiator.address()
1104 })
1105 await waitForChannel(ch)
1106 }
1107 // TODO: why node doesn't return signed_tx when channel is reestablished?
1108 // await new Promise((resolve) => {
1109 // const checkRound = () => {
1110 // ch.round().should.equal(round)
1111 // // TODO: enable line below
1112 // // ch.off('stateChanged', checkRound)
1113 // resolve()
1114 // }
1115 // ch.on('stateChanged', checkRound)
1116 // })
1117 ch.state().should.eventually.be.fulfilled
1118 await new Promise(resolve => setTimeout(resolve, 10 * 1000))
1119 })
1120
1121 it('can post backchannel update', async () => {
1122 async function appendSignature (target, source) {
1123 const { txType, tx: { signatures, encodedTx: { rlpEncoded } } } = unpackTx(target)
1124 const tx = buildTx({
1125 signatures: signatures.concat(unpackTx(source).tx.signatures),
1126 encodedTx: rlpEncoded
1127 }, txType)
1128 return `tx_${encodeBase64Check(tx.rlpEncoded)}`
1129 }
1130
1131 initiatorCh.disconnect()
1132 responderCh.disconnect()
1133 initiatorCh = await Channel({
1134 ...sharedParams,
1135 role: 'initiator',
1136 port: 3007,
1137 sign: initiatorSign
1138 })
1139 responderCh = await Channel({
1140 ...sharedParams,
1141 role: 'responder',
1142 port: 3007,
1143 sign: responderSign
1144 })
1145 await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)])
1146 initiatorCh.disconnect()
1147 const { accepted } = await responderCh.update(
1148 await initiator.address(),
1149 await responder.address(),
1150 100,
1151 tx => responder.signTransaction(tx)
1152 )
1153 accepted.should.be.false
1154 const result = await responderCh.update(
1155 await initiator.address(),
1156 await responder.address(),
1157 100,
1158 async (tx) => appendSignature(
1159 await responder.signTransaction(tx),
1160 await initiator.signTransaction(tx)
1161 )
1162 )
1163 result.accepted.should.equal(true)
1164 result.signedTx.should.be.a('string')
1165 initiatorCh.disconnect()
1166 initiatorCh.disconnect()
1167 })
1168
1169 describe('throws errors', function () {
1170 before(async function () {
1171 initiatorCh.disconnect()
1172 responderCh.disconnect()
1173 initiatorCh = await Channel({
1174 ...sharedParams,
1175 role: 'initiator',
1176 port: 3008,
1177 sign: initiatorSign
1178 })
1179 responderCh = await Channel({
1180 ...sharedParams,
1181 role: 'responder',
1182 port: 3008,
1183 sign: responderSign
1184 })
1185 await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)])
1186 })
1187
1188 after(() => {
1189 initiatorCh.disconnect()
1190 responderCh.disconnect()
1191 })
1192
1193 async function update ({ from, to, amount, sign }) {
1194 return initiatorCh.update(
1195 from || await initiator.address(),
1196 to || await responder.address(),
1197 amount || 1,
1198 sign || initiator.signTransaction
1199 )
1200 }
1201
1202 it('when posting an update with negative amount', async () => {
1203 return update({ amount: -10 }).should.eventually.be.rejectedWith('Amount cannot be negative')
1204 })
1205
1206 it('when posting an update with insufficient balance', async () => {
1207 return update({ amount: BigNumber('999e18') }).should.eventually.be.rejectedWith('Insufficient balance')
1208 })
1209
1210 it('when posting an update with incorrect address', async () => {
1211 return update({ from: 'ak_123' }).should.eventually.be.rejectedWith('Rejected')
1212 })
1213 })
1214})