@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/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@bitpay-labs/bitcore-wallet-service",
|
|
3
3
|
"description": "A service for Mutisig HD Bitcoin Wallets",
|
|
4
4
|
"author": "BitPay Inc",
|
|
5
|
-
"version": "11.10.
|
|
5
|
+
"version": "11.10.14",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "ts_build/src/index.js",
|
|
8
8
|
"types": "./ts_build/src/index.d.ts",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@bitpay-labs/bitcore-logging": "^11.10.9",
|
|
45
|
-
"@bitpay-labs/bitcore-tss": "^11.10.
|
|
46
|
-
"@bitpay-labs/crypto-wallet-core": "^11.10.
|
|
45
|
+
"@bitpay-labs/bitcore-tss": "^11.10.14",
|
|
46
|
+
"@bitpay-labs/crypto-wallet-core": "^11.10.14",
|
|
47
47
|
"@sendgrid/mail": "6.5.4",
|
|
48
48
|
"abi-decoder": "2.4.0",
|
|
49
49
|
"async": "0.9.2",
|
|
@@ -107,5 +107,5 @@
|
|
|
107
107
|
"email": "kjoseph@bitpay.com"
|
|
108
108
|
}
|
|
109
109
|
],
|
|
110
|
-
"gitHead": "
|
|
110
|
+
"gitHead": "4a71ba9f78b8104f3c0377997cb380fcf6cbcfb3"
|
|
111
111
|
}
|
|
@@ -447,7 +447,8 @@ export class MoonpayService {
|
|
|
447
447
|
if (err) {
|
|
448
448
|
return reject(err.body ? err.body : err);
|
|
449
449
|
} else {
|
|
450
|
-
|
|
450
|
+
const success = data.statusCode >= 200 && data.statusCode < 300;
|
|
451
|
+
return resolve(data.body ? data.body : { success });
|
|
451
452
|
}
|
|
452
453
|
}
|
|
453
454
|
);
|
|
@@ -526,7 +527,10 @@ export class MoonpayService {
|
|
|
526
527
|
if (err) {
|
|
527
528
|
return reject(err.body ?? err);
|
|
528
529
|
} else {
|
|
529
|
-
|
|
530
|
+
if (!data.body) {
|
|
531
|
+
return reject(new ClientError('Moonpay did not return a session'));
|
|
532
|
+
}
|
|
533
|
+
return resolve(data.body);
|
|
530
534
|
}
|
|
531
535
|
}
|
|
532
536
|
);
|
|
@@ -560,7 +564,8 @@ export class MoonpayService {
|
|
|
560
564
|
if (err) {
|
|
561
565
|
return reject(err.body ? err.body : err);
|
|
562
566
|
} else {
|
|
563
|
-
|
|
567
|
+
const success = data.statusCode >= 200 && data.statusCode < 300;
|
|
568
|
+
return resolve(data.body ? data.body : { success });
|
|
564
569
|
}
|
|
565
570
|
}
|
|
566
571
|
);
|
|
@@ -327,8 +327,8 @@ export class BtcChain implements IChain {
|
|
|
327
327
|
|
|
328
328
|
// if TX is ready? no estimation is needed.
|
|
329
329
|
if (txp.inputs.length && !txp.changeAddress && txp.outputs.length) {
|
|
330
|
-
const totalInputs = txp.inputs.reduce((sum, x) => sum + x.satoshis, 0);
|
|
331
|
-
const totalOutputs = txp.outputs.reduce((sum, x) => sum + x.amount, 0);
|
|
330
|
+
const totalInputs = txp.inputs.reduce((sum, x) => sum + Number(x.satoshis), 0);
|
|
331
|
+
const totalOutputs = txp.outputs.reduce((sum, x) => sum + Number(x.amount), 0);
|
|
332
332
|
if (totalInputs && totalOutputs) {
|
|
333
333
|
fee = totalInputs - totalOutputs;
|
|
334
334
|
}
|
|
@@ -401,24 +401,24 @@ export class BtcChain implements IChain {
|
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
for (const o of txp.outputs || []) {
|
|
404
|
-
|
|
404
|
+
const amount = parseInt(o.amount);
|
|
405
405
|
$.checkState(o.script || o.toAddress, 'Failed state: Output should have either toAddress or script specified at <getBitcoreTx()>');
|
|
406
406
|
if (o.script) {
|
|
407
407
|
t.addOutput(
|
|
408
408
|
new this.bitcoreLib.Transaction.Output({
|
|
409
409
|
script: o.script,
|
|
410
|
-
satoshis:
|
|
410
|
+
satoshis: amount
|
|
411
411
|
})
|
|
412
412
|
);
|
|
413
413
|
} else {
|
|
414
|
-
t.to(o.toAddress,
|
|
414
|
+
t.to(o.toAddress, amount);
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
t.fee(parseInt(txp.fee));
|
|
419
419
|
|
|
420
420
|
if (txp.instantAcceptanceEscrow && txp.escrowAddress) {
|
|
421
|
-
t.escrow(txp.escrowAddress.address, txp.instantAcceptanceEscrow + txp.fee);
|
|
421
|
+
t.escrow(txp.escrowAddress.address, Number(txp.instantAcceptanceEscrow) + Number(txp.fee));
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
if (txp.enableRBF) t.enableRBF();
|
|
@@ -568,7 +568,7 @@ export class BtcChain implements IChain {
|
|
|
568
568
|
conservativeEstimation: opts.payProUrl ? true : false,
|
|
569
569
|
instantAcceptanceEscrow: opts.instantAcceptanceEscrow
|
|
570
570
|
};
|
|
571
|
-
const escrowAmount = opts.instantAcceptanceEscrow || 0;
|
|
571
|
+
const escrowAmount = Number(opts.instantAcceptanceEscrow) || 0;
|
|
572
572
|
const txpAmount = txp.getTotalAmount() + escrowAmount;
|
|
573
573
|
const baseTxpSize = this.getEstimatedSize(txp, feeOpts);
|
|
574
574
|
const baseTxpFee = (baseTxpSize * txp.feePerKb) / 1000;
|
|
@@ -468,7 +468,7 @@ export class EthChain implements IChain {
|
|
|
468
468
|
const txpTotalAmount =
|
|
469
469
|
(opts.multisigContractAddress || opts.tokenAddress) && txp.payProUrl
|
|
470
470
|
? getInvoiceValue(txp)
|
|
471
|
-
: txp.
|
|
471
|
+
: txp.getTotalAmountBigInt();
|
|
472
472
|
|
|
473
473
|
if (totalAmount < txpTotalAmount) {
|
|
474
474
|
return cb(Errors.INSUFFICIENT_FUNDS);
|
|
@@ -4,6 +4,7 @@ import { IChain } from '../../../types/chain';
|
|
|
4
4
|
import { WalletWithOpts } from '../../blockchainexplorers/v8';
|
|
5
5
|
import { Common } from '../../common';
|
|
6
6
|
import { Defaults } from '../../common/defaults';
|
|
7
|
+
import { ClientError } from '../../errors/clienterror';
|
|
7
8
|
import { Errors } from '../../errors/errordefinitions';
|
|
8
9
|
import logger from '../../logger';
|
|
9
10
|
import { IWallet, TxProposal, Wallet } from '../../model';
|
|
@@ -95,11 +96,27 @@ export class SolChain implements IChain {
|
|
|
95
96
|
return new Promise(resolve => {
|
|
96
97
|
const numSignatures = opts.numSignatures || 1;
|
|
97
98
|
const feePerKb = Defaults.SOL_BASE_FEE; // Fee per signature in lamports
|
|
98
|
-
|
|
99
|
+
let fee = feePerKb * numSignatures;
|
|
100
|
+
if (opts.priorityFee) {
|
|
101
|
+
fee += this.getPriorityFee(opts);
|
|
102
|
+
}
|
|
99
103
|
return resolve({ fee, feePerKb });
|
|
100
104
|
});
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Lamports charged on top of the signature fee when a priority fee is set:
|
|
109
|
+
* ceil(micro-lamports per compute unit * compute unit limit / 1e6).
|
|
110
|
+
* Without computeUnits the runtime reserves a fixed limit per instruction, so count the
|
|
111
|
+
* instructions CWC builds for a transfer: one per recipient, plus one for a memo.
|
|
112
|
+
*/
|
|
113
|
+
private getPriorityFee({ priorityFee, computeUnits, outputs = [], memo }) {
|
|
114
|
+
const microLamportsPerUnit = Math.max(Defaults.SOL_MIN_PRIORITY_FEE, priorityFee);
|
|
115
|
+
const instructions = Math.max(outputs.length, 1) + (memo ? 1 : 0);
|
|
116
|
+
const units = computeUnits || Math.min(Defaults.SOL_MAX_COMPUTE_UNITS, Defaults.SOL_COMPUTE_UNITS_PER_INSTRUCTION * instructions);
|
|
117
|
+
return Math.ceil(microLamportsPerUnit * units / 1e6);
|
|
118
|
+
}
|
|
119
|
+
|
|
103
120
|
isPrePublishRawBound(txp) {
|
|
104
121
|
return Common.Utils.isPrePublishRawBound(this, txp);
|
|
105
122
|
}
|
|
@@ -233,16 +250,41 @@ export class SolChain implements IChain {
|
|
|
233
250
|
const { totalAmount, availableAmount } = balance;
|
|
234
251
|
// calculate how much space is needed to find rent amount
|
|
235
252
|
const minRentException = opts.tokenAddress ? 0 : Defaults.MIN_SOL_BALANCE;
|
|
236
|
-
if (totalAmount - minRentException < txp.
|
|
253
|
+
if (totalAmount - minRentException < txp.getTotalAmountBigInt()) {
|
|
237
254
|
return cb(Errors.INSUFFICIENT_FUNDS);
|
|
238
|
-
} else if (availableAmount < txp.
|
|
255
|
+
} else if (availableAmount < txp.getTotalAmountBigInt()) {
|
|
239
256
|
return cb(Errors.LOCKED_FUNDS);
|
|
257
|
+
} else if (opts.tokenAddress) {
|
|
258
|
+
// SPL fees are paid in SOL. Fetch the native balance without opts.wallet,
|
|
259
|
+
// since getWalletBalance leaves tokenAddress set on that object.
|
|
260
|
+
server.getBalance({}, (err, solBalance) => {
|
|
261
|
+
if (err) return cb(err);
|
|
262
|
+
if (solBalance.totalAmount < txp.fee) {
|
|
263
|
+
return cb(this.getFeeError('INSUFFICIENT_SOL_FEE', txp));
|
|
264
|
+
} else if (solBalance.availableAmount < txp.fee) {
|
|
265
|
+
return cb(this.getFeeError('LOCKED_SOL_FEE', txp));
|
|
266
|
+
} else {
|
|
267
|
+
return cb(this.checkTx(txp));
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
} else if (availableAmount - txp.fee < txp.getTotalAmountBigInt()) {
|
|
271
|
+
return cb(this.getFeeError('INSUFFICIENT_FUNDS_FOR_FEE', txp));
|
|
240
272
|
} else {
|
|
241
273
|
return cb(this.checkTx(txp));
|
|
242
274
|
}
|
|
243
275
|
});
|
|
244
276
|
}
|
|
245
277
|
|
|
278
|
+
private getFeeError(code: 'INSUFFICIENT_SOL_FEE' | 'LOCKED_SOL_FEE' | 'INSUFFICIENT_FUNDS_FOR_FEE', txp: TxProposal) {
|
|
279
|
+
return new ClientError(
|
|
280
|
+
Errors.codes[code],
|
|
281
|
+
`${Errors[code].message}. RequiredFee: ${txp.fee}`,
|
|
282
|
+
{
|
|
283
|
+
requiredFee: txp.fee
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
246
288
|
validateAddress(wallet, inaddr, opts) {
|
|
247
289
|
const chain = this.chain.toLowerCase();
|
|
248
290
|
const isValidTo = Validation.validateAddress(chain, wallet.network, inaddr);
|
|
@@ -223,9 +223,9 @@ export class XrpChain implements IChain {
|
|
|
223
223
|
if (err) return cb(err);
|
|
224
224
|
const { totalAmount, availableAmount } = balance;
|
|
225
225
|
const minXrpBalance = Defaults.MIN_XRP_BALANCE;
|
|
226
|
-
if (totalAmount - minXrpBalance < txp.
|
|
226
|
+
if (totalAmount - minXrpBalance < txp.getTotalAmountBigInt()) {
|
|
227
227
|
return cb(Errors.INSUFFICIENT_FUNDS);
|
|
228
|
-
} else if (availableAmount < txp.
|
|
228
|
+
} else if (availableAmount < txp.getTotalAmountBigInt()) {
|
|
229
229
|
return cb(Errors.LOCKED_FUNDS);
|
|
230
230
|
} else {
|
|
231
231
|
return cb(this.checkTx(txp));
|
|
@@ -414,6 +414,12 @@ export const Defaults = {
|
|
|
414
414
|
|
|
415
415
|
SOL_BASE_FEE: 5000,
|
|
416
416
|
|
|
417
|
+
// SOL prioritization fee: CWC raises any priority fee to at least this many micro-lamports per compute unit
|
|
418
|
+
SOL_MIN_PRIORITY_FEE: 1000,
|
|
419
|
+
// Compute unit limit the runtime reserves per instruction when no limit is set, and the per-tx maximum
|
|
420
|
+
SOL_COMPUTE_UNITS_PER_INSTRUCTION: 200000,
|
|
421
|
+
SOL_MAX_COMPUTE_UNITS: 1400000,
|
|
422
|
+
|
|
417
423
|
// Time to get the latest push notification subscriptions. In ms.
|
|
418
424
|
PUSH_NOTIFICATION_SUBS_TIME: 10 * 60 * 1000, // 10 min.
|
|
419
425
|
|
|
@@ -34,6 +34,9 @@ interface Errors<T> {
|
|
|
34
34
|
// Optimisim Errors
|
|
35
35
|
INSUFFICIENT_OP_FEE: T;
|
|
36
36
|
LOCKED_OP_FEE: T;
|
|
37
|
+
// Solana Errors
|
|
38
|
+
INSUFFICIENT_SOL_FEE: T;
|
|
39
|
+
LOCKED_SOL_FEE: T;
|
|
37
40
|
HISTORY_LIMIT_EXCEEDED: T;
|
|
38
41
|
MAIN_ADDRESS_GAP_REACHED: T;
|
|
39
42
|
MULTI_TX_UNSUPPORTED: T;
|
|
@@ -105,6 +108,8 @@ const errors: Errors<string> = {
|
|
|
105
108
|
LOCKED_BASE_FEE: 'Your linked BASE wallet does not have enough ETH for fee',
|
|
106
109
|
INSUFFICIENT_OP_FEE: 'Your linked OP wallet does not have enough ETH for fee',
|
|
107
110
|
LOCKED_OP_FEE: 'Your linked OP wallet does not have enough ETH for fee',
|
|
111
|
+
INSUFFICIENT_SOL_FEE: 'Your linked SOL wallet does not have enough SOL for fee',
|
|
112
|
+
LOCKED_SOL_FEE: 'Your linked SOL wallet does not have enough SOL for fee',
|
|
108
113
|
INVALID_ADDRESS: 'Invalid address',
|
|
109
114
|
INVALID_CHANGE_ADDRESS: 'Invalid change address',
|
|
110
115
|
KEY_IN_COPAYER: 'Key already registered',
|
|
@@ -478,7 +478,11 @@ export class TxProposal<NumberType = number> implements ITxProposal<NumberType>
|
|
|
478
478
|
* @return {Number} total amount of all outputs excluding change output
|
|
479
479
|
*/
|
|
480
480
|
getTotalAmount() {
|
|
481
|
-
return Number(
|
|
481
|
+
return Number(this.getTotalAmountBigInt());
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
getTotalAmountBigInt() {
|
|
485
|
+
return ((this as TxProposal<number>).outputs || []).reduce((total, o) => total += BigInt(o.amount), 0n);
|
|
482
486
|
}
|
|
483
487
|
|
|
484
488
|
/**
|
|
@@ -526,11 +530,19 @@ export class TxProposal<NumberType = number> implements ITxProposal<NumberType>
|
|
|
526
530
|
sign(copayerId, signatures, xpub, numberFormat?: NumberFormat) {
|
|
527
531
|
try {
|
|
528
532
|
// numberFormat as 'number' is to sidestep TS errors, but is not necessarily true.
|
|
529
|
-
const
|
|
530
|
-
const
|
|
533
|
+
const clientTxp = numberFormat ? TxProposal.formatNumbers(this, numberFormat as 'number') : this as TxProposal<number>;
|
|
534
|
+
const clientTx = ChainService.getBitcoreTx(clientTxp);
|
|
535
|
+
if (numberFormat) {
|
|
536
|
+
// Signatures only bind the bytes the copayer signed, and every later rebuild starts from
|
|
537
|
+
// the stored values, so a format that rebuilds other bytes signs a different transaction.
|
|
538
|
+
const storedTx = ChainService.getBitcoreTx(this as TxProposal<number>);
|
|
539
|
+
if (JSON.stringify(clientTx.uncheckedSerialize()) !== JSON.stringify(storedTx.uncheckedSerialize())) {
|
|
540
|
+
throw new Error(`numberFormat ${numberFormat} rebuilds a different tx for proposal ${this.id}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
531
543
|
ChainService.addSignaturesToBitcoreTx(
|
|
532
544
|
this.chain,
|
|
533
|
-
|
|
545
|
+
clientTx,
|
|
534
546
|
this.inputs,
|
|
535
547
|
this.inputPaths,
|
|
536
548
|
signatures,
|
|
@@ -540,10 +552,10 @@ export class TxProposal<NumberType = number> implements ITxProposal<NumberType>
|
|
|
540
552
|
this.addAction(copayerId, 'accept', null, signatures, xpub);
|
|
541
553
|
|
|
542
554
|
if (this.status == 'accepted') {
|
|
543
|
-
this.raw =
|
|
544
|
-
this.txid =
|
|
555
|
+
this.raw = clientTx.uncheckedSerialize();
|
|
556
|
+
this.txid = clientTx.id;
|
|
545
557
|
if (this.multiTx) {
|
|
546
|
-
this.txids =
|
|
558
|
+
this.txids = clientTx?.txids && clientTx.txids() || [clientTx.id];
|
|
547
559
|
}
|
|
548
560
|
}
|
|
549
561
|
|
|
@@ -604,19 +616,35 @@ export class TxProposal<NumberType = number> implements ITxProposal<NumberType>
|
|
|
604
616
|
static formatNumbers<T>(txp: TxProposal<T>, numberFormat: 'string'): TxProposal<string>;
|
|
605
617
|
static formatNumbers<T>(txp: TxProposal<T>, numberFormat: 'hex'): TxProposal<string>;
|
|
606
618
|
static formatNumbers<T>(txp: TxProposal<T>, numberFormat: 'bigint'): TxProposal<bigint>;
|
|
607
|
-
static formatNumbers<T>(txp: TxProposal<T>, numberFormat: 'number'): TxProposal<number>;
|
|
619
|
+
static formatNumbers<T>(txp: TxProposal<T>, numberFormat: 'number'): TxProposal<number | T>;
|
|
608
620
|
static formatNumbers<T>(txp: ITxProposal<T>, numberFormat: 'string'): ITxProposal<string>;
|
|
609
621
|
static formatNumbers<T>(txp: ITxProposal<T>, numberFormat: 'hex'): ITxProposal<string>;
|
|
610
622
|
static formatNumbers<T>(txp: ITxProposal<T>, numberFormat: 'bigint'): ITxProposal<bigint>;
|
|
611
|
-
static formatNumbers<T>(txp: ITxProposal<T>, numberFormat: 'number'): ITxProposal<number>;
|
|
623
|
+
static formatNumbers<T>(txp: ITxProposal<T>, numberFormat: 'number'): ITxProposal<number | T>;
|
|
612
624
|
static formatNumbers<T>(txp: TxProposal<T> | ITxProposal<T>, numberFormat: NumberFormat = 'number') {
|
|
613
625
|
let convertFn;
|
|
614
626
|
switch (numberFormat) {
|
|
615
627
|
case 'number':
|
|
616
|
-
|
|
628
|
+
// Clients rebuild the raw tx from these values to check the proposal signature, so a value
|
|
629
|
+
// parseInt cannot round-trip must be returned as stored rather than rounded.
|
|
630
|
+
convertFn = (n) => {
|
|
631
|
+
const parsed = parseInt(n);
|
|
632
|
+
const value = typeof n === 'string' ? n.trim() : n;
|
|
633
|
+
try {
|
|
634
|
+
return BigInt(parsed) === BigInt(value) ? parsed : n;
|
|
635
|
+
} catch {
|
|
636
|
+
return Number.isSafeInteger(parsed) && !/e/i.test(String(value)) ? parsed : n;
|
|
637
|
+
}
|
|
638
|
+
};
|
|
617
639
|
break;
|
|
618
640
|
case 'string':
|
|
619
|
-
|
|
641
|
+
// toString() renders large integers in exponential notation, which no tx builder can parse.
|
|
642
|
+
convertFn = (n) => {
|
|
643
|
+
if (typeof n === 'string') return n.startsWith('0x') ? BigInt(n).toString() : n;
|
|
644
|
+
return typeof n === 'number' && Number.isInteger(n)
|
|
645
|
+
? n.toLocaleString('fullwide', { useGrouping: false, maximumFractionDigits: 0 })
|
|
646
|
+
: n.toString();
|
|
647
|
+
};
|
|
620
648
|
break;
|
|
621
649
|
case 'hex':
|
|
622
650
|
convertFn = (n) => CWCUtils.toHex(n);
|
|
@@ -143,8 +143,10 @@ export function createRouteHelpers(returnError: Types.ReturnErrorFn): RouteHelpe
|
|
|
143
143
|
const checkNumberFormat = (numberFormat: string | undefined, res: express.Response) => {
|
|
144
144
|
const validFormats = ['hex', 'string', 'number']; // bigint cannot be serialized to JSON, so it's not supported in the API response
|
|
145
145
|
if (numberFormat && !validFormats.includes(numberFormat)) {
|
|
146
|
-
|
|
146
|
+
returnError(Errors.INVALID_NUMBER_FORMAT, res, null);
|
|
147
|
+
return false;
|
|
147
148
|
}
|
|
149
|
+
return true;
|
|
148
150
|
};
|
|
149
151
|
|
|
150
152
|
return {
|
|
@@ -18,7 +18,7 @@ export class ApiErrorHelper {
|
|
|
18
18
|
}
|
|
19
19
|
if (err instanceof ClientError) {
|
|
20
20
|
const status = err.code == 'NOT_AUTHORIZED' ? 401 : 400;
|
|
21
|
-
if (!this.disableLogs) logger.info('Client Err: ' + status + ' ' + req
|
|
21
|
+
if (!this.disableLogs) logger.info('Client Err: ' + status + ' ' + req?.url + ' ' + JSON.stringify(err));
|
|
22
22
|
|
|
23
23
|
const clientError: { code: string; message: string; messageData?: object } = {
|
|
24
24
|
code: err.code,
|
|
@@ -39,7 +39,7 @@ export class ApiErrorHelper {
|
|
|
39
39
|
|
|
40
40
|
const m = message || err.toString();
|
|
41
41
|
|
|
42
|
-
if (!this.disableLogs) logger.error(req
|
|
42
|
+
if (!this.disableLogs) logger.error(req?.url + ' :' + code + ':' + m);
|
|
43
43
|
|
|
44
44
|
res
|
|
45
45
|
.status(code || 500)
|
|
@@ -22,7 +22,7 @@ export function registerTransactionRoutes(router: express.Router, context: Route
|
|
|
22
22
|
|
|
23
23
|
router.get('/v2/txproposals/', (req, res) => {
|
|
24
24
|
getServerWithAuth(req, res, server => {
|
|
25
|
-
checkNumberFormat(req.query.numberFormat, res);
|
|
25
|
+
if (!checkNumberFormat(req.query.numberFormat, res)) return;
|
|
26
26
|
|
|
27
27
|
const opts = {
|
|
28
28
|
numberFormat: req.query.numberFormat,
|
|
@@ -52,7 +52,7 @@ export function registerTransactionRoutes(router: express.Router, context: Route
|
|
|
52
52
|
|
|
53
53
|
router.post('/v3/txproposals/', (req, res) => {
|
|
54
54
|
getServerWithAuth(req, res, server => {
|
|
55
|
-
checkNumberFormat(req.query.numberFormat, res);
|
|
55
|
+
if (!checkNumberFormat(req.query.numberFormat, res)) return;
|
|
56
56
|
req.body.txpVersion = 3;
|
|
57
57
|
req.body.numberFormat = req.query.numberFormat;
|
|
58
58
|
server.createTx(req.body, (err, txp) => {
|
|
@@ -76,7 +76,7 @@ export function registerTransactionRoutes(router: express.Router, context: Route
|
|
|
76
76
|
|
|
77
77
|
router.post('/v2/txproposals/:id/signatures/', (req, res) => {
|
|
78
78
|
getServerWithAuth(req, res, server => {
|
|
79
|
-
checkNumberFormat(req.query.numberFormat, res);
|
|
79
|
+
if (!checkNumberFormat(req.query.numberFormat, res)) return;
|
|
80
80
|
req.body.txProposalId = req.params['id'];
|
|
81
81
|
req.body.maxTxpVersion = 3;
|
|
82
82
|
req.body.supportBchSchnorr = true;
|
|
@@ -91,7 +91,7 @@ export function registerTransactionRoutes(router: express.Router, context: Route
|
|
|
91
91
|
|
|
92
92
|
router.post('/v1/txproposals/:id/prepare/', (req, res) => {
|
|
93
93
|
getServerWithAuth(req, res, server => {
|
|
94
|
-
checkNumberFormat(req.query.numberFormat, res);
|
|
94
|
+
if (!checkNumberFormat(req.query.numberFormat, res)) return;
|
|
95
95
|
req.body.txProposalId = req.params['id'];
|
|
96
96
|
req.body.numberFormat = req.query.numberFormat;
|
|
97
97
|
server.prepareTx(req.body, (err, txp) => {
|
|
@@ -116,7 +116,7 @@ export function registerTransactionRoutes(router: express.Router, context: Route
|
|
|
116
116
|
|
|
117
117
|
router.post('/v2/txproposals/:id/publish/', (req, res) => {
|
|
118
118
|
getServerWithAuth(req, res, server => {
|
|
119
|
-
checkNumberFormat(req.query.numberFormat, res);
|
|
119
|
+
if (!checkNumberFormat(req.query.numberFormat, res)) return;
|
|
120
120
|
req.body.txProposalId = req.params['id'];
|
|
121
121
|
req.body.numberFormat = req.query.numberFormat;
|
|
122
122
|
server.publishTx(req.body, (err, txp) => {
|
|
@@ -97,7 +97,7 @@ export function registerWalletRoutes(router: express.Router, context: RouteConte
|
|
|
97
97
|
|
|
98
98
|
router.get('/v3/wallets/', (req, res) => {
|
|
99
99
|
getServerWithAuth(req, res, server => {
|
|
100
|
-
checkNumberFormat(req.query.numberFormat, res);
|
|
100
|
+
if (!checkNumberFormat(req.query.numberFormat, res)) return;
|
|
101
101
|
const opts = {
|
|
102
102
|
includeExtendedInfo: false,
|
|
103
103
|
twoStep: false,
|
|
@@ -125,7 +125,7 @@ export function registerWalletRoutes(router: express.Router, context: RouteConte
|
|
|
125
125
|
const silentFailure = req.query.silentFailure == '1';
|
|
126
126
|
const includeServerMessages = req.query.serverMessageArray == '1';
|
|
127
127
|
const numberFormat = req.query.numberFormat;
|
|
128
|
-
checkNumberFormat(numberFormat, res);
|
|
128
|
+
if (!checkNumberFormat(numberFormat, res)) return;
|
|
129
129
|
|
|
130
130
|
const buildOpts = (request, copayerId) => {
|
|
131
131
|
const getParam = (param, returnArray = false) => {
|
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
|
|
|
@@ -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
|
+
|