@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.
Files changed (81) hide show
  1. package/package.json +4 -4
  2. package/src/externalservices/moonpay.ts +8 -3
  3. package/src/lib/chain/btc/index.ts +7 -7
  4. package/src/lib/chain/eth/index.ts +1 -1
  5. package/src/lib/chain/sol/index.ts +45 -3
  6. package/src/lib/chain/xrp/index.ts +2 -2
  7. package/src/lib/common/defaults.ts +6 -0
  8. package/src/lib/errors/errordefinitions.ts +5 -0
  9. package/src/lib/model/txproposal.ts +39 -11
  10. package/src/lib/routes/context.ts +3 -1
  11. package/src/lib/routes/helpers/error.ts +2 -2
  12. package/src/lib/routes/transactions.ts +5 -5
  13. package/src/lib/routes/wallets.ts +2 -2
  14. package/src/lib/server.ts +5 -2
  15. package/src/types/expressapp.d.ts +1 -1
  16. package/test/chain/btc.test.ts +32 -0
  17. package/test/chain/eth.test.ts +43 -0
  18. package/test/chain/sol.test.ts +181 -0
  19. package/test/chain/xrp.test.ts +21 -0
  20. package/test/integration/externalservices/moonpay.test.ts +79 -0
  21. package/test/integration/server.test.ts +99 -0
  22. package/test/model/txproposal.test.ts +118 -0
  23. package/test/numberformat-routes.test.ts +88 -0
  24. package/ts_build/package.json +3 -3
  25. package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
  26. package/ts_build/src/externalservices/moonpay.js +8 -3
  27. package/ts_build/src/externalservices/moonpay.js.map +1 -1
  28. package/ts_build/src/lib/chain/btc/index.js +7 -7
  29. package/ts_build/src/lib/chain/btc/index.js.map +1 -1
  30. package/ts_build/src/lib/chain/eth/index.js +1 -1
  31. package/ts_build/src/lib/chain/eth/index.js.map +1 -1
  32. package/ts_build/src/lib/chain/sol/index.d.ts +2 -0
  33. package/ts_build/src/lib/chain/sol/index.d.ts.map +1 -1
  34. package/ts_build/src/lib/chain/sol/index.js +36 -3
  35. package/ts_build/src/lib/chain/sol/index.js.map +1 -1
  36. package/ts_build/src/lib/chain/xrp/index.js +2 -2
  37. package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
  38. package/ts_build/src/lib/common/defaults.d.ts +3 -0
  39. package/ts_build/src/lib/common/defaults.d.ts.map +1 -1
  40. package/ts_build/src/lib/common/defaults.js +3 -0
  41. package/ts_build/src/lib/common/defaults.js.map +1 -1
  42. package/ts_build/src/lib/common/index.d.ts +3 -0
  43. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  44. package/ts_build/src/lib/errors/errordefinitions.d.ts +2 -0
  45. package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
  46. package/ts_build/src/lib/errors/errordefinitions.js +2 -0
  47. package/ts_build/src/lib/errors/errordefinitions.js.map +1 -1
  48. package/ts_build/src/lib/model/txproposal.d.ts +3 -2
  49. package/ts_build/src/lib/model/txproposal.d.ts.map +1 -1
  50. package/ts_build/src/lib/model/txproposal.js +33 -9
  51. package/ts_build/src/lib/model/txproposal.js.map +1 -1
  52. package/ts_build/src/lib/routes/context.d.ts.map +1 -1
  53. package/ts_build/src/lib/routes/context.js +3 -1
  54. package/ts_build/src/lib/routes/context.js.map +1 -1
  55. package/ts_build/src/lib/routes/helpers/error.js +2 -2
  56. package/ts_build/src/lib/routes/helpers/error.js.map +1 -1
  57. package/ts_build/src/lib/routes/transactions.js +10 -5
  58. package/ts_build/src/lib/routes/transactions.js.map +1 -1
  59. package/ts_build/src/lib/routes/wallets.js +4 -2
  60. package/ts_build/src/lib/routes/wallets.js.map +1 -1
  61. package/ts_build/src/lib/server.d.ts.map +1 -1
  62. package/ts_build/src/lib/server.js +6 -2
  63. package/ts_build/src/lib/server.js.map +1 -1
  64. package/ts_build/test/chain/btc.test.js +26 -0
  65. package/ts_build/test/chain/btc.test.js.map +1 -1
  66. package/ts_build/test/chain/eth.test.js +40 -0
  67. package/ts_build/test/chain/eth.test.js.map +1 -1
  68. package/ts_build/test/chain/sol.test.js +156 -0
  69. package/ts_build/test/chain/sol.test.js.map +1 -1
  70. package/ts_build/test/chain/xrp.test.js +18 -0
  71. package/ts_build/test/chain/xrp.test.js.map +1 -1
  72. package/ts_build/test/integration/externalservices/moonpay.test.js +67 -0
  73. package/ts_build/test/integration/externalservices/moonpay.test.js.map +1 -1
  74. package/ts_build/test/integration/server.test.js +87 -0
  75. package/ts_build/test/integration/server.test.js.map +1 -1
  76. package/ts_build/test/model/txproposal.test.js +101 -0
  77. package/ts_build/test/model/txproposal.test.js.map +1 -1
  78. package/ts_build/test/numberformat-routes.test.d.ts +2 -0
  79. package/ts_build/test/numberformat-routes.test.d.ts.map +1 -0
  80. package/ts_build/test/numberformat-routes.test.js +116 -0
  81. 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.12",
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.12",
46
- "@bitpay-labs/crypto-wallet-core": "^11.10.12",
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": "d1992fd744501a1b69b8cb97f07b0896df7223fd"
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
- return resolve(data.body ? data.body : data);
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
- return resolve(data.body ?? data);
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
- return resolve(data.body ? data.body : data);
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
- o.amount = parseInt(o.amount);
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: o.amount
410
+ satoshis: amount
411
411
  })
412
412
  );
413
413
  } else {
414
- t.to(o.toAddress, o.amount);
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.getTotalAmount();
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
- const fee = feePerKb * numSignatures;
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.getTotalAmount()) {
253
+ if (totalAmount - minRentException < txp.getTotalAmountBigInt()) {
237
254
  return cb(Errors.INSUFFICIENT_FUNDS);
238
- } else if (availableAmount < txp.getTotalAmount()) {
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.getTotalAmount()) {
226
+ if (totalAmount - minXrpBalance < txp.getTotalAmountBigInt()) {
227
227
  return cb(Errors.INSUFFICIENT_FUNDS);
228
- } else if (availableAmount < txp.getTotalAmount()) {
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(((this as TxProposal<number>).outputs || []).reduce((total, o) => total += BigInt(o.amount), 0n));
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 txp = numberFormat ? TxProposal.formatNumbers(this, numberFormat as 'number') : this as TxProposal<number>;
530
- const tx = ChainService.getBitcoreTx(txp);
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
- tx,
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 = tx.uncheckedSerialize();
544
- this.txid = tx.id;
555
+ this.raw = clientTx.uncheckedSerialize();
556
+ this.txid = clientTx.id;
545
557
  if (this.multiTx) {
546
- this.txids = tx?.txids && tx.txids() || [tx.id];
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
- convertFn = parseInt;
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
- convertFn = (n) => typeof n === 'string' && n.startsWith('0x') ? BigInt(n).toString() : n.toString();
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
- return returnError(Errors.INVALID_NUMBER_FORMAT, res, null);
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.url + ' ' + JSON.stringify(err));
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.url + ' :' + code + ':' + m);
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) < 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
 
@@ -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
+