@bitpay-labs/bitcore-wallet-service 11.10.12 → 11.10.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/externalservices/moonpay.ts +8 -3
- package/src/lib/chain/btc/index.ts +7 -7
- package/src/lib/chain/eth/index.ts +1 -1
- package/src/lib/chain/sol/index.ts +45 -3
- package/src/lib/chain/xrp/index.ts +2 -2
- package/src/lib/common/defaults.ts +6 -0
- package/src/lib/errors/errordefinitions.ts +5 -0
- package/src/lib/model/txproposal.ts +39 -11
- package/src/lib/routes/context.ts +3 -1
- package/src/lib/routes/helpers/error.ts +2 -2
- package/src/lib/routes/transactions.ts +5 -5
- package/src/lib/routes/wallets.ts +2 -2
- package/src/lib/server.ts +5 -2
- package/src/types/expressapp.d.ts +1 -1
- package/test/chain/btc.test.ts +32 -0
- package/test/chain/eth.test.ts +43 -0
- package/test/chain/sol.test.ts +181 -0
- package/test/chain/xrp.test.ts +21 -0
- package/test/integration/externalservices/moonpay.test.ts +79 -0
- package/test/integration/server.test.ts +99 -0
- package/test/model/txproposal.test.ts +118 -0
- package/test/numberformat-routes.test.ts +88 -0
- package/ts_build/package.json +3 -3
- package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
- package/ts_build/src/externalservices/moonpay.js +8 -3
- package/ts_build/src/externalservices/moonpay.js.map +1 -1
- package/ts_build/src/lib/chain/btc/index.js +7 -7
- package/ts_build/src/lib/chain/btc/index.js.map +1 -1
- package/ts_build/src/lib/chain/eth/index.js +1 -1
- package/ts_build/src/lib/chain/eth/index.js.map +1 -1
- package/ts_build/src/lib/chain/sol/index.d.ts +2 -0
- package/ts_build/src/lib/chain/sol/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/sol/index.js +36 -3
- package/ts_build/src/lib/chain/sol/index.js.map +1 -1
- package/ts_build/src/lib/chain/xrp/index.js +2 -2
- package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
- package/ts_build/src/lib/common/defaults.d.ts +3 -0
- package/ts_build/src/lib/common/defaults.d.ts.map +1 -1
- package/ts_build/src/lib/common/defaults.js +3 -0
- package/ts_build/src/lib/common/defaults.js.map +1 -1
- package/ts_build/src/lib/common/index.d.ts +3 -0
- package/ts_build/src/lib/common/index.d.ts.map +1 -1
- package/ts_build/src/lib/errors/errordefinitions.d.ts +2 -0
- package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
- package/ts_build/src/lib/errors/errordefinitions.js +2 -0
- package/ts_build/src/lib/errors/errordefinitions.js.map +1 -1
- package/ts_build/src/lib/model/txproposal.d.ts +3 -2
- package/ts_build/src/lib/model/txproposal.d.ts.map +1 -1
- package/ts_build/src/lib/model/txproposal.js +33 -9
- package/ts_build/src/lib/model/txproposal.js.map +1 -1
- package/ts_build/src/lib/routes/context.d.ts.map +1 -1
- package/ts_build/src/lib/routes/context.js +3 -1
- package/ts_build/src/lib/routes/context.js.map +1 -1
- package/ts_build/src/lib/routes/helpers/error.js +2 -2
- package/ts_build/src/lib/routes/helpers/error.js.map +1 -1
- package/ts_build/src/lib/routes/transactions.js +10 -5
- package/ts_build/src/lib/routes/transactions.js.map +1 -1
- package/ts_build/src/lib/routes/wallets.js +4 -2
- package/ts_build/src/lib/routes/wallets.js.map +1 -1
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +6 -2
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/test/chain/btc.test.js +26 -0
- package/ts_build/test/chain/btc.test.js.map +1 -1
- package/ts_build/test/chain/eth.test.js +40 -0
- package/ts_build/test/chain/eth.test.js.map +1 -1
- package/ts_build/test/chain/sol.test.js +156 -0
- package/ts_build/test/chain/sol.test.js.map +1 -1
- package/ts_build/test/chain/xrp.test.js +18 -0
- package/ts_build/test/chain/xrp.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/moonpay.test.js +67 -0
- package/ts_build/test/integration/externalservices/moonpay.test.js.map +1 -1
- package/ts_build/test/integration/server.test.js +87 -0
- package/ts_build/test/integration/server.test.js.map +1 -1
- package/ts_build/test/model/txproposal.test.js +101 -0
- package/ts_build/test/model/txproposal.test.js.map +1 -1
- package/ts_build/test/numberformat-routes.test.d.ts +2 -0
- package/ts_build/test/numberformat-routes.test.d.ts.map +1 -0
- package/ts_build/test/numberformat-routes.test.js +116 -0
- package/ts_build/test/numberformat-routes.test.js.map +1 -0
package/test/chain/sol.test.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
import * as chai from 'chai';
|
|
4
|
+
import sinon from 'sinon';
|
|
4
5
|
import { ChainService } from '../../src/lib/chain';
|
|
5
6
|
import { Defaults } from '../../src/lib/common/defaults';
|
|
7
|
+
import { ClientError } from '../../src/lib/errors/clienterror';
|
|
8
|
+
import { TxProposal } from '../../src/lib/model/txproposal';
|
|
6
9
|
|
|
7
10
|
const should = chai.should();
|
|
8
11
|
|
|
@@ -45,4 +48,182 @@ describe('Chain SOL', function() {
|
|
|
45
48
|
});
|
|
46
49
|
}
|
|
47
50
|
});
|
|
51
|
+
|
|
52
|
+
describe('#getFee', function() {
|
|
53
|
+
const sol = ChainService.get('sol');
|
|
54
|
+
const getFee = async opts => (await sol.getFee(null, null, opts)).fee;
|
|
55
|
+
const outputs = n => Array.from({ length: n }, () => ({ toAddress: 'addr', amount: 1 }));
|
|
56
|
+
|
|
57
|
+
it('should be the signature fee when no priority fee is set', async function() {
|
|
58
|
+
(await getFee({ outputs: outputs(1) })).should.equal(5000);
|
|
59
|
+
(await getFee({ outputs: outputs(1), numSignatures: 2 })).should.equal(10000);
|
|
60
|
+
(await getFee({ outputs: outputs(1), computeUnits: 300000 })).should.equal(5000);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should add the priority fee for the given compute unit limit', async function() {
|
|
64
|
+
// 50,000 micro-lamports/CU * 300,000 CU = 15,000 lamports
|
|
65
|
+
(await getFee({ outputs: outputs(1), priorityFee: 50000, computeUnits: 300000 })).should.equal(5000 + 15000);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should round the priority fee up to a whole lamport', async function() {
|
|
69
|
+
// 1,001 micro-lamports/CU * 1,000 CU = 1,001,000 micro-lamports = 1.001 lamports
|
|
70
|
+
(await getFee({ outputs: outputs(1), priorityFee: 1001, computeUnits: 1000 })).should.equal(5000 + 2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should raise the priority fee to the minimum CWC applies', async function() {
|
|
74
|
+
// 1 micro-lamport/CU is raised to 1,000; 1,000 * 200,000 CU = 200 lamports
|
|
75
|
+
(await getFee({ outputs: outputs(1), priorityFee: 1, computeUnits: 200000 })).should.equal(5000 + 200);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should use the default compute unit limit per instruction without computeUnits', async function() {
|
|
79
|
+
// 200,000 CU per transfer instruction
|
|
80
|
+
(await getFee({ outputs: outputs(1), priorityFee: 10000 })).should.equal(5000 + 2000);
|
|
81
|
+
(await getFee({ outputs: outputs(3), priorityFee: 10000 })).should.equal(5000 + 6000);
|
|
82
|
+
// the memo is one more instruction
|
|
83
|
+
(await getFee({ outputs: outputs(1), memo: 'refund', priorityFee: 10000 })).should.equal(5000 + 4000);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should cap the default compute unit limit at the per-transaction maximum', async function() {
|
|
87
|
+
// 12 transfers would be 2.4M CU, capped at 1.4M
|
|
88
|
+
(await getFee({ outputs: outputs(12), priorityFee: 10000 })).should.equal(5000 + 14000);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('#selectTxInputs', function() {
|
|
93
|
+
const sol = ChainService.get('sol');
|
|
94
|
+
const wallet = { chain: 'sol', network: 'livenet' };
|
|
95
|
+
const fee = 5000;
|
|
96
|
+
let checkTxCalled;
|
|
97
|
+
|
|
98
|
+
beforeEach(function() {
|
|
99
|
+
checkTxCalled = false;
|
|
100
|
+
sinon.stub(sol, 'checkTx').callsFake(() => {
|
|
101
|
+
checkTxCalled = true;
|
|
102
|
+
return null;
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(function() {
|
|
107
|
+
sinon.restore();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should reject a total that a number rounds down to the balance', function(done) {
|
|
111
|
+
const balance = 9007199254740992 + Defaults.MIN_SOL_BALANCE;
|
|
112
|
+
const server = {
|
|
113
|
+
getBalance: (_opts, cb) => cb(null, { totalAmount: balance, availableAmount: balance })
|
|
114
|
+
};
|
|
115
|
+
const txp = TxProposal.fromObj({
|
|
116
|
+
version: 3,
|
|
117
|
+
id: '75c34f49-1ed6-255f-e9fd-0c71ae75ed1e',
|
|
118
|
+
walletId: '1',
|
|
119
|
+
creatorId: '1',
|
|
120
|
+
coin: 'sol',
|
|
121
|
+
chain: 'sol',
|
|
122
|
+
network: 'livenet',
|
|
123
|
+
from: '8WyoNvKsmfdG6zrbzNBVN8DETyLra3ond61saU9C52YR',
|
|
124
|
+
fee: 5000,
|
|
125
|
+
status: 'pending',
|
|
126
|
+
actions: [],
|
|
127
|
+
outputs: [
|
|
128
|
+
{ toAddress: 'F7FknkRckx4yvA3Gexnx1H3nwPxndMxVt58BwAzEQhcY', amount: '9007199254740992' },
|
|
129
|
+
{ toAddress: 'F7FknkRckx4yvA3Gexnx1H3nwPxndMxVt58BwAzEQhcY', amount: '1' }
|
|
130
|
+
]
|
|
131
|
+
} as any);
|
|
132
|
+
|
|
133
|
+
ChainService.get('sol').selectTxInputs(server as any, txp, {} as any, {}, err => {
|
|
134
|
+
should.exist(err);
|
|
135
|
+
err.code.should.equal('INSUFFICIENT_FUNDS');
|
|
136
|
+
done();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Token balance is returned when tokenAddress is passed, native SOL balance otherwise
|
|
141
|
+
const makeServer = ({ native, token }: { native: any; token?: any }) => ({
|
|
142
|
+
getBalance: (opts, cb) => cb(null, opts.tokenAddress ? token : native)
|
|
143
|
+
});
|
|
144
|
+
const balance = (total, available) => ({ totalAmount: total, availableAmount: available });
|
|
145
|
+
const makeTxp = amount => ({ fee, getTotalAmount: () => amount, getTotalAmountBigInt: () => BigInt(amount) });
|
|
146
|
+
|
|
147
|
+
describe('SPL token', function() {
|
|
148
|
+
const opts = { tokenAddress: 'mint' };
|
|
149
|
+
|
|
150
|
+
it('should fail with INSUFFICIENT_SOL_FEE if the wallet has no SOL for the fee', function(done) {
|
|
151
|
+
const server = makeServer({ token: balance(1e6, 1e6), native: balance(0, 0) });
|
|
152
|
+
sol.selectTxInputs(server as any, makeTxp(5e5) as any, wallet as any, opts, err => {
|
|
153
|
+
should.exist(err);
|
|
154
|
+
err.should.be.instanceof(ClientError);
|
|
155
|
+
err.code.should.equal('INSUFFICIENT_SOL_FEE');
|
|
156
|
+
err.message.should.equal('Your linked SOL wallet does not have enough SOL for fee. RequiredFee: 5000');
|
|
157
|
+
err.messageData.should.deep.equal({ requiredFee: fee });
|
|
158
|
+
checkTxCalled.should.equal(false);
|
|
159
|
+
done();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should fail with LOCKED_SOL_FEE if the SOL for the fee is locked', function(done) {
|
|
164
|
+
const server = makeServer({ token: balance(1e6, 1e6), native: balance(1e7, 1000) });
|
|
165
|
+
sol.selectTxInputs(server as any, makeTxp(5e5) as any, wallet as any, opts, err => {
|
|
166
|
+
should.exist(err);
|
|
167
|
+
err.should.be.instanceof(ClientError);
|
|
168
|
+
err.code.should.equal('LOCKED_SOL_FEE');
|
|
169
|
+
err.messageData.should.deep.equal({ requiredFee: fee });
|
|
170
|
+
checkTxCalled.should.equal(false);
|
|
171
|
+
done();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should still fail with INSUFFICIENT_FUNDS when the token balance is short', function(done) {
|
|
176
|
+
const server = makeServer({ token: balance(1e5, 1e5), native: balance(1e9, 1e9) });
|
|
177
|
+
sol.selectTxInputs(server as any, makeTxp(5e5) as any, wallet as any, opts, err => {
|
|
178
|
+
should.exist(err);
|
|
179
|
+
err.code.should.equal('INSUFFICIENT_FUNDS');
|
|
180
|
+
done();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should pass when both the token and the SOL fee are covered', function(done) {
|
|
185
|
+
const server = makeServer({ token: balance(1e6, 1e6), native: balance(1e9, 1e9) });
|
|
186
|
+
sol.selectTxInputs(server as any, makeTxp(5e5) as any, wallet as any, opts, err => {
|
|
187
|
+
should.not.exist(err);
|
|
188
|
+
checkTxCalled.should.equal(true);
|
|
189
|
+
done();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('native SOL', function() {
|
|
195
|
+
it('should fail with INSUFFICIENT_FUNDS_FOR_FEE if the amount fits but the fee does not', function(done) {
|
|
196
|
+
const amount = 1e9;
|
|
197
|
+
const server = makeServer({ native: balance(amount + Defaults.MIN_SOL_BALANCE, amount) });
|
|
198
|
+
sol.selectTxInputs(server as any, makeTxp(amount) as any, wallet as any, {}, err => {
|
|
199
|
+
should.exist(err);
|
|
200
|
+
err.should.be.instanceof(ClientError);
|
|
201
|
+
err.code.should.equal('INSUFFICIENT_FUNDS_FOR_FEE');
|
|
202
|
+
err.message.should.equal('Insufficient funds for fee. RequiredFee: 5000');
|
|
203
|
+
err.messageData.should.deep.equal({ requiredFee: fee });
|
|
204
|
+
checkTxCalled.should.equal(false);
|
|
205
|
+
done();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should still fail with INSUFFICIENT_FUNDS when the amount does not fit', function(done) {
|
|
210
|
+
const server = makeServer({ native: balance(1e9, 1e9) });
|
|
211
|
+
sol.selectTxInputs(server as any, makeTxp(2e9) as any, wallet as any, {}, err => {
|
|
212
|
+
should.exist(err);
|
|
213
|
+
err.code.should.equal('INSUFFICIENT_FUNDS');
|
|
214
|
+
done();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should pass when the amount and fee are covered', function(done) {
|
|
219
|
+
const amount = 1e9;
|
|
220
|
+
const server = makeServer({ native: balance(2e9, amount + fee) });
|
|
221
|
+
sol.selectTxInputs(server as any, makeTxp(amount) as any, wallet as any, {}, err => {
|
|
222
|
+
should.not.exist(err);
|
|
223
|
+
checkTxCalled.should.equal(true);
|
|
224
|
+
done();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
48
229
|
});
|
package/test/chain/xrp.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as chai from 'chai';
|
|
|
4
4
|
import 'chai/register-should';
|
|
5
5
|
import { ChainService } from '../../src/lib/chain';
|
|
6
6
|
import { ITxProposal, TxProposal } from '../../src/lib/model/txproposal';
|
|
7
|
+
import { Defaults } from '../../src/lib/common/defaults';
|
|
7
8
|
|
|
8
9
|
const should = chai.should();
|
|
9
10
|
|
|
@@ -61,6 +62,26 @@ describe('Chain XRP', function() {
|
|
|
61
62
|
});
|
|
62
63
|
});
|
|
63
64
|
|
|
65
|
+
describe('#selectTxInputs', function() {
|
|
66
|
+
const balanceOf = amount => ({
|
|
67
|
+
getBalance: (_opts, cb) => cb(null, { totalAmount: amount, availableAmount: amount })
|
|
68
|
+
});
|
|
69
|
+
const txpOf = amounts => TxProposal.fromObj({
|
|
70
|
+
...aTXP(),
|
|
71
|
+
outputs: amounts.map(amount => ({ ...aTXP().outputs[0], amount }))
|
|
72
|
+
} as any);
|
|
73
|
+
|
|
74
|
+
it('should reject a total that a number rounds down to the balance', function(done) {
|
|
75
|
+
const balance = 9007199254740992 + Defaults.MIN_XRP_BALANCE;
|
|
76
|
+
|
|
77
|
+
ChainService.get('xrp').selectTxInputs(balanceOf(balance) as any, txpOf(['9007199254740992', '1']), {} as any, {}, err => {
|
|
78
|
+
should.exist(err);
|
|
79
|
+
err.code.should.equal('INSUFFICIENT_FUNDS');
|
|
80
|
+
done();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
64
85
|
describe('#getBitcoreTx', function() {
|
|
65
86
|
it('should create a valid bitcore TX', function() {
|
|
66
87
|
const txp = TxProposal.fromObj(aTXP()) as TxProposal;
|
|
@@ -573,6 +573,35 @@ describe('Moonpay integration', () => {
|
|
|
573
573
|
should.exist(data);
|
|
574
574
|
});
|
|
575
575
|
|
|
576
|
+
it('should return the response body if there is one', async () => {
|
|
577
|
+
const data = await server.externalServices.moonpay.moonpayCancelSellTransaction(req);
|
|
578
|
+
data.should.equal('data');
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('should return success true without leaking the response if there is no body', async () => {
|
|
582
|
+
const fakeRequest2 = {
|
|
583
|
+
delete: (_url, opts, _cb) => {
|
|
584
|
+
return _cb(null, { statusCode: 204, body: undefined, headers: {}, request: { headers: opts.headers } });
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
server.externalServices.moonpay.request = fakeRequest2;
|
|
589
|
+
const data = await server.externalServices.moonpay.moonpayCancelSellTransaction(req);
|
|
590
|
+
data.should.deep.equal({ success: true });
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('should return success false if there is no body and status is not 2xx', async () => {
|
|
594
|
+
const fakeRequest2 = {
|
|
595
|
+
delete: (_url, opts, _cb) => {
|
|
596
|
+
return _cb(null, { statusCode: 404, body: '', headers: {}, request: { headers: opts.headers } });
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
server.externalServices.moonpay.request = fakeRequest2;
|
|
601
|
+
const data = await server.externalServices.moonpay.moonpayCancelSellTransaction(req);
|
|
602
|
+
data.should.deep.equal({ success: false });
|
|
603
|
+
});
|
|
604
|
+
|
|
576
605
|
it('should return error if delete returns error', async () => {
|
|
577
606
|
const fakeRequest2 = {
|
|
578
607
|
delete: (_url, _opts, _cb) => { return _cb(new Error('Error'), null); },
|
|
@@ -641,6 +670,27 @@ describe('Moonpay integration', () => {
|
|
|
641
670
|
should.exist(data);
|
|
642
671
|
});
|
|
643
672
|
|
|
673
|
+
it('should return the response body if there is one', async () => {
|
|
674
|
+
const data = await server.externalServices.moonpay.moonpayCreateSession(req);
|
|
675
|
+
data.should.equal('data');
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('should throw without leaking the response if there is no session body', async () => {
|
|
679
|
+
const fakeRequest2 = {
|
|
680
|
+
post: (_url, opts, _cb) => {
|
|
681
|
+
return _cb(null, { statusCode: 200, body: undefined, headers: {}, request: { headers: opts.headers } });
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
server.externalServices.moonpay.request = fakeRequest2;
|
|
686
|
+
try {
|
|
687
|
+
await server.externalServices.moonpay.moonpayCreateSession(req);
|
|
688
|
+
should.fail('should have thrown');
|
|
689
|
+
} catch (err) {
|
|
690
|
+
err.message.should.equal('Moonpay did not return a session');
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
644
694
|
it('should return error if post returns error', async () => {
|
|
645
695
|
const fakeRequest2 = {
|
|
646
696
|
post: (_url, _opts, _cb) => { return _cb(new Error('Error'), null); },
|
|
@@ -723,6 +773,35 @@ describe('Moonpay integration', () => {
|
|
|
723
773
|
await server.externalServices.moonpay.moonpayRevokeActiveSession(req);
|
|
724
774
|
});
|
|
725
775
|
|
|
776
|
+
it('should return the response body if there is one', async () => {
|
|
777
|
+
const data = await server.externalServices.moonpay.moonpayRevokeActiveSession(req);
|
|
778
|
+
data.should.equal('data');
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('should return success true without leaking the response if there is no body', async () => {
|
|
782
|
+
const fakeRequest2 = {
|
|
783
|
+
delete: (_url, opts, _cb) => {
|
|
784
|
+
return _cb(null, { statusCode: 204, body: undefined, headers: {}, request: { headers: opts.headers } });
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
server.externalServices.moonpay.request = fakeRequest2;
|
|
789
|
+
const data = await server.externalServices.moonpay.moonpayRevokeActiveSession(req);
|
|
790
|
+
data.should.deep.equal({ success: true });
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('should return success false if there is no body and status is not 2xx', async () => {
|
|
794
|
+
const fakeRequest2 = {
|
|
795
|
+
delete: (_url, opts, _cb) => {
|
|
796
|
+
return _cb(null, { statusCode: 404, body: '', headers: {}, request: { headers: opts.headers } });
|
|
797
|
+
},
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
server.externalServices.moonpay.request = fakeRequest2;
|
|
801
|
+
const data = await server.externalServices.moonpay.moonpayRevokeActiveSession(req);
|
|
802
|
+
data.should.deep.equal({ success: false });
|
|
803
|
+
});
|
|
804
|
+
|
|
726
805
|
it('should return error if delete returns error', async () => {
|
|
727
806
|
const fakeRequest2 = {
|
|
728
807
|
delete: (_url, _opts, _cb) => { return _cb(new Error('Error'), null); },
|
|
@@ -3935,6 +3935,45 @@ describe('Wallet service', function() {
|
|
|
3935
3935
|
});
|
|
3936
3936
|
});
|
|
3937
3937
|
|
|
3938
|
+
describe('#createTx token allowance', function() {
|
|
3939
|
+
let server: WalletService;
|
|
3940
|
+
let wallet: Model.Wallet;
|
|
3941
|
+
|
|
3942
|
+
beforeEach(async function() {
|
|
3943
|
+
({ server, wallet } = await helpers.createAndJoinWallet(1, 1, { coin: 'eth' }));
|
|
3944
|
+
await util.promisify(server.createAddress).call(server, {});
|
|
3945
|
+
});
|
|
3946
|
+
|
|
3947
|
+
it('should reject a total that a number rounds down to the allowance', async function() {
|
|
3948
|
+
const be = helpers.getBlockchainExplorer();
|
|
3949
|
+
be.getBalance = sinon.stub().callsArgWith(1, null, { unconfirmed: 0, confirmed: '1' + '0'.repeat(20), balance: '1' + '0'.repeat(20) });
|
|
3950
|
+
be.getTokenAllowance = sinon.stub().callsArgWith(1, null, '9007199254740992');
|
|
3951
|
+
|
|
3952
|
+
const opts = {
|
|
3953
|
+
outputs: [
|
|
3954
|
+
{ toAddress: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A', amount: '9007199254740992' },
|
|
3955
|
+
{ toAddress: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A', amount: '1' }
|
|
3956
|
+
],
|
|
3957
|
+
from: '0xE299d49C2cf9BfaFb7C6E861E80bb8c83f961622',
|
|
3958
|
+
tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
|
|
3959
|
+
multiSendContractAddress: '0x30e5bfa1e83c9c6f0a4c4bd4a1f0e0d7d0c4d3a2',
|
|
3960
|
+
feePerKb: 25000000000,
|
|
3961
|
+
nonce: 5
|
|
3962
|
+
};
|
|
3963
|
+
|
|
3964
|
+
let error;
|
|
3965
|
+
try {
|
|
3966
|
+
await util.promisify(server.createTx).call(server, opts);
|
|
3967
|
+
} catch (e) {
|
|
3968
|
+
error = e;
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
should.exist(error);
|
|
3972
|
+
error.message.should.contain('Insufficient token allowance');
|
|
3973
|
+
error.message.should.contain('Want: 9007199254740993');
|
|
3974
|
+
});
|
|
3975
|
+
});
|
|
3976
|
+
|
|
3938
3977
|
const testSet = [
|
|
3939
3978
|
{
|
|
3940
3979
|
coin: 'btc',
|
|
@@ -8390,6 +8429,66 @@ describe('Wallet service', function() {
|
|
|
8390
8429
|
txp.nonce.should.equal('5');
|
|
8391
8430
|
});
|
|
8392
8431
|
|
|
8432
|
+
describe('republishing a txp with mutable tx data', function() {
|
|
8433
|
+
let txp, publishOpts;
|
|
8434
|
+
|
|
8435
|
+
beforeEach(async function() {
|
|
8436
|
+
txp = await util.promisify(server.createTx).call(server, {
|
|
8437
|
+
outputs: [{ toAddress: ETH_ADDR, amount: 8000 }],
|
|
8438
|
+
feePerKb: 123e2,
|
|
8439
|
+
from: fromAddr,
|
|
8440
|
+
deferNonce: true
|
|
8441
|
+
});
|
|
8442
|
+
publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0);
|
|
8443
|
+
await util.promisify(server.publishTx).call(server, publishOpts);
|
|
8444
|
+
});
|
|
8445
|
+
|
|
8446
|
+
const setStatus = async status => {
|
|
8447
|
+
const stored = await util.promisify(server.storage.fetchTx).call(server.storage, wallet.id, txp.id);
|
|
8448
|
+
stored.status = status;
|
|
8449
|
+
await util.promisify(server.storage.storeTx).call(server.storage, wallet.id, stored);
|
|
8450
|
+
};
|
|
8451
|
+
|
|
8452
|
+
const republishError = async () => {
|
|
8453
|
+
try {
|
|
8454
|
+
await util.promisify(server.publishTx).call(server, publishOpts);
|
|
8455
|
+
} catch (err) {
|
|
8456
|
+
return err;
|
|
8457
|
+
}
|
|
8458
|
+
};
|
|
8459
|
+
|
|
8460
|
+
it('should republish a pending txp', async function() {
|
|
8461
|
+
should.not.exist(await republishError());
|
|
8462
|
+
const stored = await util.promisify(server.storage.fetchTx).call(server.storage, wallet.id, txp.id);
|
|
8463
|
+
stored.status.should.equal('pending');
|
|
8464
|
+
});
|
|
8465
|
+
|
|
8466
|
+
it('should not republish a broadcasted txp back to pending', async function() {
|
|
8467
|
+
await setStatus('broadcasted');
|
|
8468
|
+
const err = await republishError();
|
|
8469
|
+
should.exist(err);
|
|
8470
|
+
err.code.should.equal('TX_ALREADY_BROADCASTED');
|
|
8471
|
+
const stored = await util.promisify(server.storage.fetchTx).call(server.storage, wallet.id, txp.id);
|
|
8472
|
+
stored.status.should.equal('broadcasted');
|
|
8473
|
+
});
|
|
8474
|
+
|
|
8475
|
+
it('should not republish an accepted txp back to pending', async function() {
|
|
8476
|
+
await setStatus('accepted');
|
|
8477
|
+
const err = await republishError();
|
|
8478
|
+
should.exist(err);
|
|
8479
|
+
err.code.should.equal('TX_NOT_PENDING');
|
|
8480
|
+
const stored = await util.promisify(server.storage.fetchTx).call(server.storage, wallet.id, txp.id);
|
|
8481
|
+
stored.status.should.equal('accepted');
|
|
8482
|
+
});
|
|
8483
|
+
|
|
8484
|
+
it('should not republish a rejected txp back to pending', async function() {
|
|
8485
|
+
await setStatus('rejected');
|
|
8486
|
+
const err = await republishError();
|
|
8487
|
+
should.exist(err);
|
|
8488
|
+
err.code.should.equal('TX_NOT_PENDING');
|
|
8489
|
+
});
|
|
8490
|
+
});
|
|
8491
|
+
|
|
8393
8492
|
it('should publish a deferred-nonce txp and save prePublishRaw', async function() {
|
|
8394
8493
|
const txOpts = {
|
|
8395
8494
|
outputs: [{ toAddress: ETH_ADDR, amount: 8000 }],
|
|
@@ -53,6 +53,28 @@ describe('TxProposal', function() {
|
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
describe('#fromObj', function() {
|
|
57
|
+
it('should keep the stored output amount type untouched', function() {
|
|
58
|
+
const stored: any = { ...aTXP(), outputs: [{ ...aTXP().outputs[0], amount: 70000000000000010 }] };
|
|
59
|
+
|
|
60
|
+
const txp = TxProposal.fromObj(stored);
|
|
61
|
+
|
|
62
|
+
txp.outputs[0].amount.should.equal(70000000000000010);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('#getTotalAmountBigInt', function() {
|
|
67
|
+
it('should keep a total that a number cannot represent', function() {
|
|
68
|
+
const txp = TxProposal.fromObj({
|
|
69
|
+
...aTXP(),
|
|
70
|
+
outputs: [{ ...aTXP().outputs[0], amount: '9007199254740992' }, { ...aTXP().outputs[1], amount: '1' }]
|
|
71
|
+
} as any);
|
|
72
|
+
|
|
73
|
+
txp.getTotalAmountBigInt().should.equal(9007199254740993n);
|
|
74
|
+
txp.getTotalAmount().should.equal(9007199254740992);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
56
78
|
describe('#getTotalAmount', function() {
|
|
57
79
|
it('should compute total amount', function() {
|
|
58
80
|
const x = TxProposal.fromObj(aTXP());
|
|
@@ -81,6 +103,21 @@ describe('TxProposal', function() {
|
|
|
81
103
|
});
|
|
82
104
|
});
|
|
83
105
|
|
|
106
|
+
describe('#sign with a number format', function() {
|
|
107
|
+
it('should keep the signatures in the persisted raw tx', function() {
|
|
108
|
+
const plain = TxProposal.fromObj(aTXP());
|
|
109
|
+
plain.sign('1', theSignatures, theXPub);
|
|
110
|
+
plain.sign('2', theSignatures, theXPub);
|
|
111
|
+
|
|
112
|
+
const formatted = TxProposal.fromObj(aTXP());
|
|
113
|
+
formatted.sign('1', theSignatures, theXPub, 'number');
|
|
114
|
+
formatted.sign('2', theSignatures, theXPub, 'number');
|
|
115
|
+
|
|
116
|
+
formatted.raw.should.deep.equal(plain.raw);
|
|
117
|
+
formatted.txid.should.equal(plain.txid);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
84
121
|
describe('#reject', function() {
|
|
85
122
|
it('should reject 2-2', function() {
|
|
86
123
|
const txp = TxProposal.fromObj(aTXP());
|
|
@@ -142,6 +179,13 @@ describe('TxProposal', function() {
|
|
|
142
179
|
txp.outputs[0].amount.should.be.a('number');
|
|
143
180
|
});
|
|
144
181
|
|
|
182
|
+
it('should keep a base-unit amount that a number cannot represent exactly', function() {
|
|
183
|
+
const amount = '70000000000000010';
|
|
184
|
+
const t = TxProposal.fromObj({ ...aTXP(), amount, outputs: [{ ...aTXP().outputs[0], amount }] } as any);
|
|
185
|
+
const txp = TxProposal.formatNumbers(t, 'number');
|
|
186
|
+
txp.outputs[0].amount.should.equal(amount);
|
|
187
|
+
});
|
|
188
|
+
|
|
145
189
|
it('should format string numbers as hex', function() {
|
|
146
190
|
const t1 = TxProposal.fromObj(aTXP());
|
|
147
191
|
const t2 = TxProposal.formatNumbers(t1, 'string');
|
|
@@ -156,6 +200,80 @@ describe('TxProposal', function() {
|
|
|
156
200
|
txp.outputs[0].amount.should.be.a('string');
|
|
157
201
|
CWCUtils.isHexString(txp.outputs[0].amount).should.equal(true);
|
|
158
202
|
});
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
describe('legacy format contracts', function() {
|
|
206
|
+
const cases: Array<[string, any, any]> = [
|
|
207
|
+
['safe integer boundary', '9007199254740991', 9007199254740991],
|
|
208
|
+
['first representable unsafe integer', '9007199254740992', 9007199254740992],
|
|
209
|
+
['first unrepresentable integer', '9007199254740993', '9007199254740993'],
|
|
210
|
+
['representable amount', '70000000000000008', Number('70000000000000008')],
|
|
211
|
+
['inexact amount', '70000000000000010', '70000000000000010'],
|
|
212
|
+
['representable hex', '0x20000000000000', 9007199254740992],
|
|
213
|
+
['unrepresentable hex', '0x20000000000001', '0x20000000000001'],
|
|
214
|
+
['exponential string', '1e+21', '1e+21'],
|
|
215
|
+
['exponential number', 1e21, 1e21],
|
|
216
|
+
['overflow string', '9'.repeat(400), '9'.repeat(400)],
|
|
217
|
+
['empty string', '', ''],
|
|
218
|
+
['nonnumeric string', 'abc', 'abc'],
|
|
219
|
+
['null', null, null],
|
|
220
|
+
['undefined', undefined, undefined],
|
|
221
|
+
['NaN', NaN, NaN],
|
|
222
|
+
['Infinity', Infinity, Infinity],
|
|
223
|
+
['small bigint', 5n, 5],
|
|
224
|
+
['inexact bigint', 9007199254740993n, 9007199254740993n],
|
|
225
|
+
['positive sign', '+5', 5],
|
|
226
|
+
['negative zero', '-0', -0],
|
|
227
|
+
['leading zeros', '007', 7],
|
|
228
|
+
['whitespace', ' 42 ', 42],
|
|
229
|
+
['fractional prefix', '1.5', 1],
|
|
230
|
+
['malformed prefix', '12xyz', 12],
|
|
231
|
+
['SOL nonce prefix', '2' + 'A'.repeat(43), 2]
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
for (const [label, value, expected] of cases) {
|
|
235
|
+
it(`should leave the legacy number behavior unchanged for ${label}`, function() {
|
|
236
|
+
const source: any = TxProposal.fromObj(aTXP());
|
|
237
|
+
source.amount = value;
|
|
238
|
+
source.outputs[0].amount = value;
|
|
239
|
+
const formatted = TxProposal.formatNumbers(source, 'number');
|
|
240
|
+
|
|
241
|
+
chai.expect(Object.is(formatted.amount, expected)).to.equal(true);
|
|
242
|
+
chai.expect(Object.is(formatted.outputs[0].amount, expected)).to.equal(true);
|
|
243
|
+
chai.expect(Object.is(source.outputs[0].amount, value)).to.equal(true);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const format of ['string', 'hex', 'bigint'] as const) {
|
|
248
|
+
it(`should leave the legacy ${format} behavior unchanged`, function() {
|
|
249
|
+
const source: any = TxProposal.fromObj(aTXP());
|
|
250
|
+
source.amount = 42;
|
|
251
|
+
source.outputs[0].amount = '70000000000000010';
|
|
252
|
+
const formatted = TxProposal.formatNumbers(source, format as 'string') as any;
|
|
253
|
+
const expected = {
|
|
254
|
+
string: ['42', '70000000000000010'],
|
|
255
|
+
hex: ['0x2a', '0xf8b0a10e47000a'],
|
|
256
|
+
bigint: [42n, 70000000000000010n]
|
|
257
|
+
}[format];
|
|
258
|
+
|
|
259
|
+
formatted.amount.should.equal(expected[0]);
|
|
260
|
+
formatted.outputs[0].amount.should.equal(expected[1]);
|
|
261
|
+
source.amount.should.equal(42);
|
|
262
|
+
source.outputs[0].amount.should.equal('70000000000000010');
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
it('should expand large integers and keep existing hex strings in string mode', function() {
|
|
267
|
+
const source: any = TxProposal.fromObj(aTXP());
|
|
268
|
+
source.amount = 1e21;
|
|
269
|
+
source.outputs[0].amount = '0x2a';
|
|
270
|
+
const formatted = TxProposal.formatNumbers(source, 'string');
|
|
271
|
+
|
|
272
|
+
formatted.amount.should.equal('1000000000000000000000');
|
|
273
|
+
BigInt(formatted.amount).should.equal(1000000000000000000000n);
|
|
274
|
+
formatted.outputs[0].amount.should.equal('0x2a');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
159
277
|
});
|
|
160
278
|
});
|
|
161
279
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import * as chai from 'chai';
|
|
4
|
+
import 'chai/register-should';
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import sinon from 'sinon';
|
|
7
|
+
import { Errors } from '../src/lib/errors/errordefinitions';
|
|
8
|
+
import { createRouteHelpers } from '../src/lib/routes/context';
|
|
9
|
+
import { ApiErrorHelper } from '../src/lib/routes/helpers/error';
|
|
10
|
+
import { registerTransactionRoutes } from '../src/lib/routes/transactions';
|
|
11
|
+
import { registerWalletRoutes } from '../src/lib/routes/wallets';
|
|
12
|
+
|
|
13
|
+
const should = chai.should();
|
|
14
|
+
|
|
15
|
+
describe('Transaction number format routes', function() {
|
|
16
|
+
const routes = [
|
|
17
|
+
['get', '/v2/txproposals/', 'getPendingTxs'],
|
|
18
|
+
['post', '/v3/txproposals/', 'createTx'],
|
|
19
|
+
['post', '/v2/txproposals/:id/signatures/', 'signTx'],
|
|
20
|
+
['post', '/v1/txproposals/:id/prepare/', 'prepareTx'],
|
|
21
|
+
['post', '/v2/txproposals/:id/publish/', 'publishTx'],
|
|
22
|
+
['get', '/v3/wallets/', 'getStatus'],
|
|
23
|
+
['get', '/v1/wallets/all/', 'getStatus']
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function setup() {
|
|
27
|
+
const router = express.Router();
|
|
28
|
+
const error = new ApiErrorHelper();
|
|
29
|
+
const returnError = error.returnError.bind(error);
|
|
30
|
+
const helpers = createRouteHelpers(returnError);
|
|
31
|
+
const server: any = { copayerId: 'copayer', walletId: 'wallet' };
|
|
32
|
+
for (const method of new Set(routes.map(route => route[2]))) {
|
|
33
|
+
server[method] = sinon.stub().callsArgWith(1, null, { outputs: [{ amount: '70000000000000010' }] });
|
|
34
|
+
}
|
|
35
|
+
const context = {
|
|
36
|
+
...helpers,
|
|
37
|
+
returnError,
|
|
38
|
+
getServerWithAuth: sinon.stub().callsFake((_req, _res, cb) => cb(server)),
|
|
39
|
+
getServerWithMultiAuth: sinon.stub().returns([Promise.resolve(server)]),
|
|
40
|
+
createWalletLimiter: sinon.stub().callsArg(2)
|
|
41
|
+
};
|
|
42
|
+
registerTransactionRoutes(router, context);
|
|
43
|
+
registerWalletRoutes(router, context);
|
|
44
|
+
const response: any = {
|
|
45
|
+
status: sinon.stub().returnsThis(),
|
|
46
|
+
json: sinon.stub().returnsThis(),
|
|
47
|
+
end: sinon.stub().returnsThis()
|
|
48
|
+
};
|
|
49
|
+
return { router, server, response, context };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [method, path, serverMethod] of routes) {
|
|
53
|
+
for (const format of ['number', 'string', 'hex', undefined]) {
|
|
54
|
+
it(`should accept ${format ?? 'unspecified'} on ${method.toUpperCase()} ${path}`, async function() {
|
|
55
|
+
const { router, server, response } = setup();
|
|
56
|
+
const route = router.stack.find(layer => layer.route?.path === path && layer.route.methods[method]);
|
|
57
|
+
const request: any = { query: { numberFormat: format }, body: {}, params: { id: 'proposal' }, headers: {} };
|
|
58
|
+
|
|
59
|
+
await route.route.stack[0].handle(request, response);
|
|
60
|
+
|
|
61
|
+
server[serverMethod].callCount.should.equal(1);
|
|
62
|
+
chai.expect(server[serverMethod].firstCall.args[0].numberFormat).to.equal(format);
|
|
63
|
+
response.status.called.should.equal(false);
|
|
64
|
+
response.json.callCount.should.equal(1);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const format of ['invalid', 'bigint', 'raw']) {
|
|
69
|
+
it(`should return 400 without executing ${method.toUpperCase()} ${path} for ${format}`, async function() {
|
|
70
|
+
const { router, server, response, context } = setup();
|
|
71
|
+
const route = router.stack.find(layer => layer.route?.path === path && layer.route.methods[method]);
|
|
72
|
+
const request: any = { query: { numberFormat: format }, body: {}, params: { id: 'proposal' }, headers: {} };
|
|
73
|
+
|
|
74
|
+
await route.route.stack[0].handle(request, response);
|
|
75
|
+
|
|
76
|
+
response.status.calledOnceWithExactly(400).should.equal(true);
|
|
77
|
+
response.json.callCount.should.equal(1);
|
|
78
|
+
response.json.firstCall.args[0].code.should.equal('INVALID_NUMBER_FORMAT');
|
|
79
|
+
server[serverMethod].called.should.equal(false);
|
|
80
|
+
if (path === '/v1/wallets/all/') context.getServerWithMultiAuth.called.should.equal(false);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
it('should list the supported formats in the validation error', function() {
|
|
86
|
+
Errors.INVALID_NUMBER_FORMAT.message.should.include('number, string, hex');
|
|
87
|
+
});
|
|
88
|
+
});
|