@bitpay-labs/bitcore-wallet-service 11.10.11 → 11.10.13

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.
Files changed (82) hide show
  1. package/package.json +4 -4
  2. package/src/externalservices/moonpay.ts +2 -1
  3. package/src/externalservices/simplex.ts +0 -30
  4. package/src/lib/chain/btc/index.ts +12 -7
  5. package/src/lib/chain/eth/index.ts +5 -1
  6. package/src/lib/chain/index.ts +4 -0
  7. package/src/lib/chain/sol/index.ts +51 -3
  8. package/src/lib/chain/xrp/index.ts +7 -2
  9. package/src/lib/common/defaults.ts +6 -0
  10. package/src/lib/common/utils.ts +52 -1
  11. package/src/lib/errors/errordefinitions.ts +5 -0
  12. package/src/lib/model/txproposal.ts +39 -11
  13. package/src/lib/routes/context.ts +3 -1
  14. package/src/lib/routes/helpers/error.ts +2 -2
  15. package/src/lib/routes/middleware/authTssRequest.ts +1 -1
  16. package/src/lib/routes/services.ts +1 -14
  17. package/src/lib/routes/transactions.ts +5 -5
  18. package/src/lib/routes/wallets.ts +2 -2
  19. package/src/lib/server.ts +11 -4
  20. package/src/types/chain.d.ts +1 -0
  21. package/src/types/expressapp.d.ts +1 -1
  22. package/test/chain/btc.test.ts +32 -0
  23. package/test/chain/eth.test.ts +43 -0
  24. package/test/chain/sol.test.ts +229 -0
  25. package/test/chain/xrp.test.ts +51 -0
  26. package/test/integration/externalservices/moonpay.test.ts +29 -0
  27. package/test/integration/externalservices/simplex.test.ts +0 -18
  28. package/test/integration/server.test.ts +193 -0
  29. package/test/integration/tss.test.ts +67 -0
  30. package/test/model/txproposal.test.ts +118 -0
  31. package/test/numberformat-routes.test.ts +88 -0
  32. package/ts_build/package.json +3 -3
  33. package/ts_build/src/externalservices/simplex.d.ts +0 -1
  34. package/ts_build/src/externalservices/simplex.d.ts.map +1 -1
  35. package/ts_build/src/externalservices/simplex.js +0 -25
  36. package/ts_build/src/externalservices/simplex.js.map +1 -1
  37. package/ts_build/src/lib/chain/btc/index.d.ts +2 -1
  38. package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
  39. package/ts_build/src/lib/chain/btc/index.js +3 -0
  40. package/ts_build/src/lib/chain/btc/index.js.map +1 -1
  41. package/ts_build/src/lib/chain/eth/index.d.ts +1 -0
  42. package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
  43. package/ts_build/src/lib/chain/eth/index.js +3 -0
  44. package/ts_build/src/lib/chain/eth/index.js.map +1 -1
  45. package/ts_build/src/lib/chain/index.d.ts +1 -0
  46. package/ts_build/src/lib/chain/index.d.ts.map +1 -1
  47. package/ts_build/src/lib/chain/index.js +3 -0
  48. package/ts_build/src/lib/chain/index.js.map +1 -1
  49. package/ts_build/src/lib/chain/sol/index.d.ts +1 -0
  50. package/ts_build/src/lib/chain/sol/index.d.ts.map +1 -1
  51. package/ts_build/src/lib/chain/sol/index.js +5 -0
  52. package/ts_build/src/lib/chain/sol/index.js.map +1 -1
  53. package/ts_build/src/lib/chain/xrp/index.d.ts +1 -0
  54. package/ts_build/src/lib/chain/xrp/index.d.ts.map +1 -1
  55. package/ts_build/src/lib/chain/xrp/index.js +4 -0
  56. package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
  57. package/ts_build/src/lib/common/index.d.ts +2 -0
  58. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  59. package/ts_build/src/lib/common/utils.d.ts +4 -0
  60. package/ts_build/src/lib/common/utils.d.ts.map +1 -1
  61. package/ts_build/src/lib/common/utils.js +33 -0
  62. package/ts_build/src/lib/common/utils.js.map +1 -1
  63. package/ts_build/src/lib/routes/middleware/authTssRequest.js +1 -1
  64. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  65. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  66. package/ts_build/src/lib/routes/services.js +1 -13
  67. package/ts_build/src/lib/routes/services.js.map +1 -1
  68. package/ts_build/src/lib/server.d.ts.map +1 -1
  69. package/ts_build/src/lib/server.js +1 -1
  70. package/ts_build/src/lib/server.js.map +1 -1
  71. package/ts_build/test/chain/sol.test.d.ts +2 -0
  72. package/ts_build/test/chain/sol.test.d.ts.map +1 -0
  73. package/ts_build/test/chain/sol.test.js +78 -0
  74. package/ts_build/test/chain/sol.test.js.map +1 -0
  75. package/ts_build/test/chain/xrp.test.js +28 -0
  76. package/ts_build/test/chain/xrp.test.js.map +1 -1
  77. package/ts_build/test/integration/externalservices/simplex.test.js +0 -15
  78. package/ts_build/test/integration/externalservices/simplex.test.js.map +1 -1
  79. package/ts_build/test/integration/server.test.js +80 -0
  80. package/ts_build/test/integration/server.test.js.map +1 -1
  81. package/ts_build/test/integration/tss.test.js +57 -0
  82. package/ts_build/test/integration/tss.test.js.map +1 -1
package/src/lib/server.ts CHANGED
@@ -3032,8 +3032,8 @@ export class WalletService implements IWalletService {
3032
3032
  spenderAddress: txp.multiSendContractAddress
3033
3033
  }, (err, allowance) => {
3034
3034
  if (err) { return next(err); }
3035
- if (BigInt(allowance) < BigInt(txp.getTotalAmount())) {
3036
- return next(new Error(`Insufficient token allowance. Allowed: ${BigInt(allowance)}, Want: ${BigInt(txp.getTotalAmount())}`));
3035
+ if (BigInt(allowance) < txp.getTotalAmountBigInt()) {
3036
+ return next(new Error(`Insufficient token allowance. Allowed: ${BigInt(allowance)}, Want: ${txp.getTotalAmountBigInt()}`));
3037
3037
  }
3038
3038
  return next();
3039
3039
  });
@@ -3108,6 +3108,9 @@ export class WalletService implements IWalletService {
3108
3108
  if (err) return cb(err);
3109
3109
  if (!txp) return cb(Errors.TX_NOT_FOUND);
3110
3110
  if (!txp.isTemporary() && !txp.hasMutableTxData()) return cb(null, txp);
3111
+ // Broadcasted proposals should not be published again. Only temporary or pending proposals can be published.
3112
+ if (txp.isBroadcasted()) return cb(Errors.TX_ALREADY_BROADCASTED);
3113
+ if (!txp.isTemporary() && txp.status !== 'pending') return cb(Errors.TX_NOT_PENDING);
3111
3114
 
3112
3115
  const copayer = wallet.getCopayer(this.copayerId);
3113
3116
 
@@ -3120,8 +3123,12 @@ export class WalletService implements IWalletService {
3120
3123
 
3121
3124
  let signingKey = this._getSigningKey(raw, opts.proposalSignature, copayer.requestPubKeys);
3122
3125
  if (!signingKey) {
3123
- // If the txp has been published previously, we will verify the signature against the previously published raw tx
3124
- if (txp.hasMutableTxData() && txp.prePublishRaw) {
3126
+ // The signature didn't match the current (refreshed) raw tx. Fall back to the previously-published
3127
+ // raw tx -- but only if it is bound to THIS proposal (differs from the current tx solely in the
3128
+ // field BWS mutates at publish: blockhash/nonce). Without that check a tampered stored proposal
3129
+ // could reuse an old, still-valid proposalSignature over prePublishRaw. Mirrors the client
3130
+ // Verifier.checkPrePublishRaw guard.
3131
+ if (txp.hasMutableTxData() && txp.prePublishRaw && ChainService.isPrePublishRawBound(txp)) {
3125
3132
  raw = txp.prePublishRaw;
3126
3133
  signingKey = this._getSigningKey(raw, opts.proposalSignature, copayer.requestPubKeys);
3127
3134
  }
@@ -33,6 +33,7 @@ export interface IChain {
33
33
  checkScriptOutput(output: { script: string; amount: number });
34
34
  getFee(server: WalletService, wallet: IWallet, opts: { fee: number; feePerKb: number; signatures?: number } & any);
35
35
  getBitcoreTx(txp: TxProposal, opts: { signed: boolean });
36
+ isPrePublishRawBound(txp: TxProposal): boolean;
36
37
  convertFeePerKb(p: number, feePerKb: number);
37
38
  checkTx(server: WalletService, txp: ITxProposal);
38
39
  checkTxUTXOs(server: WalletService, txp: ITxProposal, opts: { noCashAddr: boolean } & any, cb);
@@ -13,4 +13,4 @@ export type ServerCallback = (server: WalletService, err?: Error) => void;
13
13
  export type GetServerWithAuthFn = (req: express.Request, res: express.Response, opts?: ServerOpts | ServerCallback, cb?: ServerCallback) => Promise<WalletService | void>;
14
14
  export type GetServerWithMultiAuthFn = (req: express.Request, res: express.Response, opts?: ServerOpts) => Array<Promise<WalletService | void>>;
15
15
  export type CreateWalletLimiterFn = (req: express.Request, res: express.Response, next: express.NextFunction) => void;
16
- export type CheckNumberFormatFn = (numberFormat: string | undefined, res: express.Response) => void;
16
+ export type CheckNumberFormatFn = (numberFormat: string | undefined, res: express.Response) => boolean;
@@ -15,6 +15,26 @@ const { Constants } = Common;
15
15
  const segWitToAddress = 'BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4'; // 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq';
16
16
 
17
17
  describe('Chain BTC', function() {
18
+ describe('#getEstimatedFee', function() {
19
+ const btcChain = ChainService.get('btc') as BtcChain;
20
+ const readyTxp = (satoshis, amounts) => ({
21
+ inputs: satoshis.map(s => ({ satoshis: s })),
22
+ outputs: amounts.map(amount => ({ amount })),
23
+ changeAddress: null,
24
+ feePerKb: 10000
25
+ });
26
+ const feeOf = txp => btcChain.getEstimatedFee(txp as any, { conservativeEstimation: false });
27
+
28
+ it('should derive the same fee from string and number amounts', function() {
29
+ const fromNumbers = feeOf(readyTxp([2000, 1000], [1000, 1000]));
30
+
31
+ fromNumbers.should.equal(1000);
32
+ feeOf(readyTxp([2000, 1000], ['1000', '1000'])).should.equal(fromNumbers);
33
+ feeOf(readyTxp(['2000', '1000'], [1000, 1000])).should.equal(fromNumbers);
34
+ feeOf(readyTxp(['2000', '1000'], ['1000', '1000'])).should.equal(fromNumbers);
35
+ });
36
+ });
37
+
18
38
  describe('#getBitcoreTx', function() {
19
39
  it('should create a valid bitcore TX', function() {
20
40
  const txp = TxProposal.fromObj(aTXP()) as TxProposal;
@@ -22,6 +42,18 @@ describe('Chain BTC', function() {
22
42
  should.exist(t);
23
43
  });
24
44
 
45
+ it('should build the same tx from string and number amounts without mutating the proposal', function() {
46
+ const asStrings: any = aTXP();
47
+ asStrings.outputs = asStrings.outputs.map(o => ({ ...o, amount: String(o.amount) }));
48
+ const fromStrings = TxProposal.fromObj(asStrings) as TxProposal;
49
+ const fromNumbers = TxProposal.fromObj(aTXP()) as TxProposal;
50
+
51
+ const built = ChainService.getBitcoreTx(fromStrings);
52
+
53
+ built.uncheckedSerialize().should.equal(ChainService.getBitcoreTx(fromNumbers).uncheckedSerialize());
54
+ fromStrings.outputs[0].amount.should.equal(String(aTXP().outputs[0].amount));
55
+ });
56
+
25
57
  it('should order outputs as specified by outputOrder', function() {
26
58
  const txp = TxProposal.fromObj(aTXP()) as TxProposal;
27
59
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as chai from 'chai';
4
4
  import { ChainService } from '../../src/lib/chain';
5
+ import { TxProposal } from '../../src/lib/model/txproposal';
5
6
 
6
7
  const should = chai.should();
7
8
 
@@ -17,5 +18,47 @@ describe('Chain ETH', function() {
17
18
  ChainService.addressFromStorageTransform('eth', 'dfg', x);
18
19
  x.address.should.equal('0x01');
19
20
  });
21
+
22
+ describe('#selectTxInputs', function() {
23
+ it('should reject a total that a number rounds down to the balance', function(done) {
24
+ const server = {
25
+ getBalance: (_opts, cb) => cb(null, { totalAmount: 9007199254740992, availableAmount: 9007199254740992 })
26
+ };
27
+ const txp = TxProposal.fromObj({
28
+ ...aTXP(),
29
+ outputs: [
30
+ { ...aTXP().outputs[0], amount: '9007199254740992' },
31
+ { ...aTXP().outputs[0], amount: '1' }
32
+ ]
33
+ } as any);
34
+
35
+ ChainService.get('eth').selectTxInputs(server as any, txp, {} as any, {}, err => {
36
+ should.exist(err);
37
+ err.code.should.equal('INSUFFICIENT_FUNDS');
38
+ done();
39
+ });
40
+ });
41
+ });
20
42
  });
21
43
 
44
+ const aTXP = function() {
45
+ return {
46
+ version: 3,
47
+ createdOn: 1423146231,
48
+ id: '75c34f49-1ed6-255f-e9fd-0c71ae75ed1e',
49
+ walletId: '1',
50
+ creatorId: '1',
51
+ coin: 'eth',
52
+ chain: 'eth',
53
+ network: 'livenet',
54
+ from: '0x9858EfFD232B4033E47d90003D41EC34EcaEda94',
55
+ nonce: 5,
56
+ gasPrice: 25000000000,
57
+ gasLimit: 60000,
58
+ fee: 1500000000000000,
59
+ outputs: [{ toAddress: '0xc27eD3DF0DE776246cdAD5a052A9982473FceaB8', amount: 1000, gasLimit: 60000 }],
60
+ status: 'pending' as const,
61
+ actions: []
62
+ };
63
+ };
64
+
@@ -0,0 +1,229 @@
1
+ 'use strict';
2
+
3
+ import * as chai from 'chai';
4
+ import sinon from 'sinon';
5
+ import { ChainService } from '../../src/lib/chain';
6
+ import { Defaults } from '../../src/lib/common/defaults';
7
+ import { ClientError } from '../../src/lib/errors/clienterror';
8
+ import { TxProposal } from '../../src/lib/model/txproposal';
9
+
10
+ const should = chai.should();
11
+
12
+ describe('Chain SOL', function() {
13
+ describe('#getWalletBalance', function() {
14
+ const cases = [
15
+ { name: 'pending amounts and fees', amount: 1000000000, txps: [{ amount: '2000000000', fee: '5000' }], reserve: 1002240, locked: 2001007240, available: -1001007240 },
16
+ { name: 'dust below rent', amount: 10000, txps: [], reserve: 1002240, locked: 1002240, available: -992240 },
17
+ { name: 'an empty account with a pending proposal', amount: 0, txps: [{ amount: '2000000000', fee: '5000' }], reserve: 0, locked: 2000005000, available: -2000005000 },
18
+ { name: 'an SPL balance', amount: 1000000, txps: [{ tokenAddress: 'mint', amount: '500000', fee: '5000' }], tokenAddress: 'mint', reserve: 0, locked: 500000, available: 500000 }
19
+ ];
20
+
21
+ for (const c of cases) {
22
+ it('should expose the reserve separately for ' + c.name, function(done) {
23
+ const server = {
24
+ walletId: '1',
25
+ _getBlockchainExplorer: () => ({
26
+ getBalance: (_wallet, cb) => cb(null, { balance: c.amount, confirmed: c.amount }),
27
+ getRentMinimum: (_space, cb) => cb(null, Defaults.MIN_SOL_BALANCE)
28
+ }),
29
+ getPendingTxs: (_opts, cb) => cb(null, c.txps),
30
+ storage: { fetchAddresses: (_walletId, cb) => cb(null, []) }
31
+ };
32
+ const wallet = { chain: 'sol', network: 'livenet' };
33
+
34
+ ChainService.get('sol').getWalletBalance(server as any, wallet as any, { tokenAddress: (c as { tokenAddress?: string }).tokenAddress }, (err, balance) => {
35
+ should.not.exist(err);
36
+ balance.should.deep.equal({
37
+ totalAmount: c.amount,
38
+ totalConfirmedAmount: c.amount,
39
+ lockedAmount: c.locked,
40
+ lockedConfirmedAmount: c.locked,
41
+ reserve: c.reserve,
42
+ availableAmount: c.available,
43
+ availableConfirmedAmount: c.available,
44
+ byAddress: []
45
+ });
46
+ done();
47
+ });
48
+ });
49
+ }
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
+ });
229
+ });
@@ -4,12 +4,43 @@ 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
 
10
11
  const xpub = 'tpubDD7tYYerLNNm65Ez7pRjxQ2NpRHDoyRWoLWudnQ8agXjr7qs9BsPsRXk8Z6spPPJodnaY158YqeCKT5oXuZvbuNLfm1R4kXGJ2vPd9pUxDT';
11
12
 
12
13
  describe('Chain XRP', function() {
14
+ describe('#getWalletBalance', function() {
15
+ it('should expose the reserve separately from pending amounts and fees', function(done) {
16
+ const server = {
17
+ walletId: '1',
18
+ _getBlockchainExplorer: () => ({
19
+ getBalance: (_wallet, cb) => cb(null, { balance: 2000000, confirmed: 2000000 }),
20
+ getReserve: cb => cb(null, 1000000)
21
+ }),
22
+ getPendingTxs: (_opts, cb) => cb(null, [{ amount: '3000000', fee: '12' }]),
23
+ storage: { fetchAddresses: (_walletId, cb) => cb(null, []) }
24
+ };
25
+ const wallet = { chain: 'xrp', network: 'livenet' };
26
+
27
+ ChainService.get('xrp').getWalletBalance(server as any, wallet as any, {}, (err, balance) => {
28
+ should.not.exist(err);
29
+ balance.should.deep.equal({
30
+ totalAmount: 2000000,
31
+ totalConfirmedAmount: 2000000,
32
+ lockedAmount: 4000012,
33
+ lockedConfirmedAmount: 4000012,
34
+ reserve: 1000000,
35
+ availableAmount: -2000012,
36
+ availableConfirmedAmount: -2000012,
37
+ byAddress: []
38
+ });
39
+ done();
40
+ });
41
+ });
42
+ });
43
+
13
44
  describe('#getWalletSendMaxInfo', function() {
14
45
  it('should resolve fee level before calculating sendMax amount', function(done) {
15
46
  const server = {
@@ -31,6 +62,26 @@ describe('Chain XRP', function() {
31
62
  });
32
63
  });
33
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
+
34
85
  describe('#getBitcoreTx', function() {
35
86
  it('should create a valid bitcore TX', function() {
36
87
  const txp = TxProposal.fromObj(aTXP()) as TxProposal;
@@ -723,6 +723,35 @@ describe('Moonpay integration', () => {
723
723
  await server.externalServices.moonpay.moonpayRevokeActiveSession(req);
724
724
  });
725
725
 
726
+ it('should return the response body if there is one', async () => {
727
+ const data = await server.externalServices.moonpay.moonpayRevokeActiveSession(req);
728
+ data.should.equal('data');
729
+ });
730
+
731
+ it('should return success true without leaking the response if there is no body', async () => {
732
+ const fakeRequest2 = {
733
+ delete: (_url, opts, _cb) => {
734
+ return _cb(null, { statusCode: 204, body: undefined, headers: {}, request: { headers: opts.headers } });
735
+ },
736
+ };
737
+
738
+ server.externalServices.moonpay.request = fakeRequest2;
739
+ const data = await server.externalServices.moonpay.moonpayRevokeActiveSession(req);
740
+ data.should.deep.equal({ success: true });
741
+ });
742
+
743
+ it('should return success false if there is no body and status is not 2xx', async () => {
744
+ const fakeRequest2 = {
745
+ delete: (_url, opts, _cb) => {
746
+ return _cb(null, { statusCode: 404, body: '', headers: {}, request: { headers: opts.headers } });
747
+ },
748
+ };
749
+
750
+ server.externalServices.moonpay.request = fakeRequest2;
751
+ const data = await server.externalServices.moonpay.moonpayRevokeActiveSession(req);
752
+ data.should.deep.equal({ success: false });
753
+ });
754
+
726
755
  it('should return error if delete returns error', async () => {
727
756
  const fakeRequest2 = {
728
757
  delete: (_url, _opts, _cb) => { return _cb(new Error('Error'), null); },
@@ -503,22 +503,4 @@ describe('Simplex integration', () => {
503
503
  }
504
504
  });
505
505
  });
506
-
507
- describe('#simplexGetEvents', () => {
508
- beforeEach(() => {
509
- req = {
510
- env: 'production'
511
- };
512
-
513
- fakeRequest = {
514
- get: (_url, _opts, _cb) => { return _cb(null, { body: {} }); },
515
- };
516
- server.externalServices.simplex.request = fakeRequest;
517
- });
518
-
519
- it('should work properly if req is OK', async () => {
520
- const data = await server.externalServices.simplex.simplexGetEvents(req);
521
- should.exist(data);
522
- });
523
- });
524
506
  });