|
3 | 3 | */ |
4 | 4 | import sinon from 'sinon'; |
5 | 5 | import 'should'; |
6 | | -import { Wallet } from '../../../../src'; |
| 6 | +import { Wallet, BaseCoin } from '../../../../src'; |
| 7 | +import { OfcToken } from '../../../../src/coins'; |
7 | 8 |
|
8 | | -describe('Wallet - OFC signTransaction', function () { |
| 9 | +const TEST_TOKEN_CONFIG = { |
| 10 | + coin: 'ofcusdt', |
| 11 | + decimalPlaces: 6, |
| 12 | + name: 'OFCUSDT', |
| 13 | + type: 'ofcusdt', |
| 14 | + backingCoin: 'usdt', |
| 15 | + isFiat: false, |
| 16 | +}; |
| 17 | + |
| 18 | +describe('Wallet - OFC', function () { |
9 | 19 | let wallet: Wallet; |
10 | 20 | let mockBitGo: any; |
11 | | - let mockBaseCoin: any; |
12 | | - let mockWalletData: any; |
| 21 | + let ofcToken: OfcToken; |
| 22 | + let signMessageStub: sinon.SinonStub; |
| 23 | + const signatureBytes = Buffer.from('aabbccdd', 'hex'); |
| 24 | + const payload = '{"amount":"100","from":"alice","to":"bob"}'; |
| 25 | + const prv = 'test-prv'; |
| 26 | + // bg-<32 hex chars> is valid for OfcToken without needing to resolve the backing coin |
| 27 | + const recipients = [{ address: 'bg-aabbccddeeff00112233445566778899', amount: '100' }]; |
| 28 | + const userPub = |
| 29 | + 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC51xERxh8vDMjc3PiGpQ3VeQcHBiGxbsrRXbCKQZZxBtDMrmBRzxRMkBJGsC9bsYb5tggVF3jGfEE'; |
| 30 | + |
| 31 | + const walletData = { |
| 32 | + id: 'test-wallet-id', |
| 33 | + coin: 'ofcusdt', |
| 34 | + keys: ['user-key', 'backup-key', 'bitgo-key'], |
| 35 | + type: 'trading', |
| 36 | + multisigType: 'onchain', |
| 37 | + enterprise: 'ent-id', |
| 38 | + }; |
| 39 | + |
| 40 | + /** |
| 41 | + * Creates a fluent request mock that handles .query().send().result() and .send().result() |
| 42 | + * chains, resolving with the provided value. |
| 43 | + */ |
| 44 | + function makeRequestChain(resolved: unknown): any { |
| 45 | + const chain: any = {}; |
| 46 | + chain.query = sinon.stub().returns(chain); |
| 47 | + chain.send = sinon.stub().returns(chain); |
| 48 | + chain.result = sinon.stub().resolves(resolved); |
| 49 | + return chain; |
| 50 | + } |
13 | 51 |
|
14 | 52 | beforeEach(function () { |
| 53 | + signMessageStub = sinon.stub(BaseCoin.prototype, 'signMessage'); |
| 54 | + signMessageStub.withArgs({ prv }, payload).resolves(signatureBytes); |
| 55 | + sinon.stub(BaseCoin.prototype, 'keychains').returns({ |
| 56 | + getKeysForSigning: sinon.stub().resolves([{ id: walletData.keys[0], pub: userPub }]), |
| 57 | + } as any); |
15 | 58 | mockBitGo = { |
16 | | - url: sinon.stub().returns('https://test.bitgo.com'), |
| 59 | + url: sinon.stub().returns('https://test.bitgo.com/'), |
17 | 60 | post: sinon.stub(), |
18 | | - get: sinon.stub(), |
19 | 61 | setRequestTracer: sinon.stub(), |
20 | 62 | }; |
21 | | - |
22 | | - mockBaseCoin = { |
23 | | - getFamily: sinon.stub().returns('ofc'), |
24 | | - url: sinon.stub().returns('https://test.bitgo.com/wallet'), |
25 | | - keychains: sinon.stub(), |
26 | | - supportsTss: sinon.stub().returns(false), |
27 | | - getMPCAlgorithm: sinon.stub(), |
28 | | - presignTransaction: sinon.stub().resolvesArg(0), |
29 | | - keyIdsForSigning: sinon.stub().returns([0]), |
30 | | - signTransaction: sinon.stub().resolves({ halfSigned: { payload: 'test', signature: 'aabbcc' } }), |
31 | | - }; |
32 | | - |
33 | | - mockWalletData = { |
34 | | - id: 'test-wallet-id', |
35 | | - coin: 'ofcusdt', |
36 | | - keys: ['user-key', 'backup-key', 'bitgo-key'], |
37 | | - multisigType: 'onchain', |
38 | | - enterprise: 'ent-id', |
39 | | - }; |
40 | | - |
41 | | - wallet = new Wallet(mockBitGo, mockBaseCoin, mockWalletData); |
| 63 | + ofcToken = new OfcToken(mockBitGo, TEST_TOKEN_CONFIG); |
| 64 | + wallet = new Wallet(mockBitGo, ofcToken, walletData); |
42 | 65 | }); |
43 | 66 |
|
44 | 67 | afterEach(function () { |
45 | 68 | sinon.restore(); |
46 | 69 | }); |
47 | 70 |
|
48 | | - it('should pass prv and paylaod to baseCoin.signTransaction', async function () { |
49 | | - const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any; |
50 | | - const prv = 'test-prv'; |
| 71 | + describe('signTransaction', function () { |
| 72 | + describe('with prv (local signing routed through wallet)', function () { |
| 73 | + it('should call baseCoin.signMessage with the prv and payload', async function () { |
| 74 | + const result = await wallet.signTransaction({ txPrebuild: { payload } as any, prv }); |
| 75 | + |
| 76 | + signMessageStub.calledOnceWith({ prv }, payload).should.be.true(); |
| 77 | + mockBitGo.post.called.should.be.false(); |
| 78 | + result.should.deepEqual({ halfSigned: { payload, signature: signatureBytes.toString('hex') } }); |
| 79 | + }); |
| 80 | + }); |
| 81 | + |
| 82 | + describe('with walletPassphrase and keychain', function () { |
| 83 | + const walletPassphrase = 'test-passphrase'; |
| 84 | + const encryptedPrv = 'encrypted-prv-blob'; |
51 | 85 |
|
52 | | - await wallet.signTransaction({ txPrebuild, prv }); |
| 86 | + beforeEach(function () { |
| 87 | + // getUserPrvAsync decrypts the keychain via the compiled decryptKeychainPrivateKey, |
| 88 | + // which calls bitgo.decrypt synchronously |
| 89 | + mockBitGo.decrypt = sinon.stub().returns(prv); |
| 90 | + }); |
53 | 91 |
|
54 | | - mockBaseCoin.signTransaction.calledOnce.should.be.true(); |
55 | | - mockBaseCoin.signTransaction.calledWith({ txPrebuild, prv }); |
56 | | - const callArgs = mockBaseCoin.signTransaction.getCall(0).args[0]; |
57 | | - callArgs.prv.should.equal(prv); |
| 92 | + it('should decrypt the keychain with the passphrase and sign via baseCoin.signMessage', async function () { |
| 93 | + const result = await wallet.signTransaction({ |
| 94 | + txPrebuild: { payload } as any, |
| 95 | + walletPassphrase, |
| 96 | + keychain: { pub: userPub, encryptedPrv } as any, |
| 97 | + }); |
| 98 | + |
| 99 | + signMessageStub.calledOnceWith({ prv }, payload).should.be.true(); |
| 100 | + mockBitGo.post.called.should.be.false(); |
| 101 | + result.should.deepEqual({ halfSigned: { payload, signature: signatureBytes.toString('hex') } }); |
| 102 | + }); |
| 103 | + }); |
58 | 104 | }); |
59 | 105 |
|
60 | | - it('should return the result from baseCoin.signTransaction', async function () { |
61 | | - const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any; |
62 | | - const prv = 'test-prv'; |
| 106 | + describe('prebuildAndSignTransaction', function () { |
| 107 | + const buildUrl = () => ofcToken.url('/wallet/' + walletData.id + '/tx/build'); |
| 108 | + const signUrl = () => ofcToken.url('/wallet/' + walletData.id + '/tx/sign'); |
| 109 | + |
| 110 | + beforeEach(function () { |
| 111 | + mockBitGo.post.withArgs(buildUrl()).returns(makeRequestChain({ payload })); |
| 112 | + }); |
| 113 | + |
| 114 | + it('should call the prebuild API then sign locally with the prv', async function () { |
| 115 | + const result = await wallet.prebuildAndSignTransaction({ prv }); |
| 116 | + |
| 117 | + mockBitGo.post.calledWith(buildUrl()).should.be.true(); |
| 118 | + signMessageStub.calledOnceWith({ prv }, payload).should.be.true(); |
| 119 | + result.should.deepEqual({ halfSigned: { payload, signature: signatureBytes.toString('hex') } }); |
| 120 | + }); |
| 121 | + |
| 122 | + it('should not call the remote sign endpoint when prv is provided', async function () { |
| 123 | + await wallet.prebuildAndSignTransaction({ prv }); |
| 124 | + |
| 125 | + mockBitGo.post.calledWith(signUrl()).should.be.false(); |
| 126 | + }); |
| 127 | + }); |
| 128 | + |
| 129 | + describe('sendMany', function () { |
| 130 | + const buildUrl = () => ofcToken.url('/wallet/' + walletData.id + '/tx/build'); |
| 131 | + const sendUrl = () => ofcToken.url('/wallet/' + walletData.id + '/tx/send'); |
| 132 | + |
| 133 | + beforeEach(function () { |
| 134 | + mockBitGo.post.withArgs(buildUrl()).returns(makeRequestChain({ payload })); |
| 135 | + mockBitGo.post.withArgs(sendUrl()).returns(makeRequestChain({ txid: 'test-txid', status: 'signed' })); |
| 136 | + }); |
63 | 137 |
|
64 | | - const result = await wallet.signTransaction({ txPrebuild, prv }); |
| 138 | + it('should prebuild, sign locally, and submit to the send endpoint', async function () { |
| 139 | + const result = await wallet.sendMany({ recipients, prv }); |
65 | 140 |
|
66 | | - result.should.deepEqual({ halfSigned: { payload: 'test', signature: 'aabbcc' } }); |
| 141 | + mockBitGo.post.calledWith(buildUrl()).should.be.true(); |
| 142 | + signMessageStub.calledOnceWith({ prv }, payload).should.be.true(); |
| 143 | + mockBitGo.post.calledWith(sendUrl()).should.be.true(); |
| 144 | + result.should.deepEqual({ txid: 'test-txid', status: 'signed' }); |
| 145 | + }); |
67 | 146 | }); |
68 | 147 | }); |
0 commit comments