@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.
- package/package.json +4 -4
- package/src/externalservices/moonpay.ts +2 -1
- package/src/externalservices/simplex.ts +0 -30
- package/src/lib/chain/btc/index.ts +12 -7
- package/src/lib/chain/eth/index.ts +5 -1
- package/src/lib/chain/index.ts +4 -0
- package/src/lib/chain/sol/index.ts +51 -3
- package/src/lib/chain/xrp/index.ts +7 -2
- package/src/lib/common/defaults.ts +6 -0
- package/src/lib/common/utils.ts +52 -1
- 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/middleware/authTssRequest.ts +1 -1
- package/src/lib/routes/services.ts +1 -14
- package/src/lib/routes/transactions.ts +5 -5
- package/src/lib/routes/wallets.ts +2 -2
- package/src/lib/server.ts +11 -4
- package/src/types/chain.d.ts +1 -0
- 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 +229 -0
- package/test/chain/xrp.test.ts +51 -0
- package/test/integration/externalservices/moonpay.test.ts +29 -0
- package/test/integration/externalservices/simplex.test.ts +0 -18
- package/test/integration/server.test.ts +193 -0
- package/test/integration/tss.test.ts +67 -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/simplex.d.ts +0 -1
- package/ts_build/src/externalservices/simplex.d.ts.map +1 -1
- package/ts_build/src/externalservices/simplex.js +0 -25
- package/ts_build/src/externalservices/simplex.js.map +1 -1
- package/ts_build/src/lib/chain/btc/index.d.ts +2 -1
- package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/btc/index.js +3 -0
- package/ts_build/src/lib/chain/btc/index.js.map +1 -1
- package/ts_build/src/lib/chain/eth/index.d.ts +1 -0
- package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/eth/index.js +3 -0
- package/ts_build/src/lib/chain/eth/index.js.map +1 -1
- package/ts_build/src/lib/chain/index.d.ts +1 -0
- package/ts_build/src/lib/chain/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/index.js +3 -0
- package/ts_build/src/lib/chain/index.js.map +1 -1
- package/ts_build/src/lib/chain/sol/index.d.ts +1 -0
- package/ts_build/src/lib/chain/sol/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/sol/index.js +5 -0
- package/ts_build/src/lib/chain/sol/index.js.map +1 -1
- package/ts_build/src/lib/chain/xrp/index.d.ts +1 -0
- package/ts_build/src/lib/chain/xrp/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/xrp/index.js +4 -0
- package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
- package/ts_build/src/lib/common/index.d.ts +2 -0
- package/ts_build/src/lib/common/index.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.d.ts +4 -0
- package/ts_build/src/lib/common/utils.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.js +33 -0
- package/ts_build/src/lib/common/utils.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
- package/ts_build/src/lib/routes/services.d.ts.map +1 -1
- package/ts_build/src/lib/routes/services.js +1 -13
- package/ts_build/src/lib/routes/services.js.map +1 -1
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +1 -1
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/test/chain/sol.test.d.ts +2 -0
- package/ts_build/test/chain/sol.test.d.ts.map +1 -0
- package/ts_build/test/chain/sol.test.js +78 -0
- package/ts_build/test/chain/sol.test.js.map +1 -0
- package/ts_build/test/chain/xrp.test.js +28 -0
- package/ts_build/test/chain/xrp.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/simplex.test.js +0 -15
- package/ts_build/test/integration/externalservices/simplex.test.js.map +1 -1
- package/ts_build/test/integration/server.test.js +80 -0
- package/ts_build/test/integration/server.test.js.map +1 -1
- package/ts_build/test/integration/tss.test.js +57 -0
- 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) <
|
|
3036
|
-
return next(new Error(`Insufficient token allowance. Allowed: ${BigInt(allowance)}, Want: ${
|
|
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
|
-
//
|
|
3124
|
-
if (
|
|
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
|
}
|
package/src/types/chain.d.ts
CHANGED
|
@@ -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) =>
|
|
16
|
+
export type CheckNumberFormatFn = (numberFormat: string | undefined, res: express.Response) => boolean;
|
package/test/chain/btc.test.ts
CHANGED
|
@@ -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
|
|
package/test/chain/eth.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/test/chain/xrp.test.ts
CHANGED
|
@@ -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
|
});
|