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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/package.json +4 -4
  2. package/src/externalservices/moonpay.ts +2 -1
  3. package/src/externalservices/simplex.ts +0 -30
  4. package/src/lib/chain/btc/index.ts +12 -7
  5. package/src/lib/chain/eth/index.ts +5 -1
  6. package/src/lib/chain/index.ts +4 -0
  7. package/src/lib/chain/sol/index.ts +51 -3
  8. package/src/lib/chain/xrp/index.ts +7 -2
  9. package/src/lib/common/defaults.ts +6 -0
  10. package/src/lib/common/utils.ts +52 -1
  11. package/src/lib/errors/errordefinitions.ts +5 -0
  12. package/src/lib/model/txproposal.ts +39 -11
  13. package/src/lib/routes/context.ts +3 -1
  14. package/src/lib/routes/helpers/error.ts +2 -2
  15. package/src/lib/routes/middleware/authTssRequest.ts +1 -1
  16. package/src/lib/routes/services.ts +1 -14
  17. package/src/lib/routes/transactions.ts +5 -5
  18. package/src/lib/routes/wallets.ts +2 -2
  19. package/src/lib/server.ts +11 -4
  20. package/src/types/chain.d.ts +1 -0
  21. package/src/types/expressapp.d.ts +1 -1
  22. package/test/chain/btc.test.ts +32 -0
  23. package/test/chain/eth.test.ts +43 -0
  24. package/test/chain/sol.test.ts +229 -0
  25. package/test/chain/xrp.test.ts +51 -0
  26. package/test/integration/externalservices/moonpay.test.ts +29 -0
  27. package/test/integration/externalservices/simplex.test.ts +0 -18
  28. package/test/integration/server.test.ts +193 -0
  29. package/test/integration/tss.test.ts +67 -0
  30. package/test/model/txproposal.test.ts +118 -0
  31. package/test/numberformat-routes.test.ts +88 -0
  32. package/ts_build/package.json +3 -3
  33. package/ts_build/src/externalservices/simplex.d.ts +0 -1
  34. package/ts_build/src/externalservices/simplex.d.ts.map +1 -1
  35. package/ts_build/src/externalservices/simplex.js +0 -25
  36. package/ts_build/src/externalservices/simplex.js.map +1 -1
  37. package/ts_build/src/lib/chain/btc/index.d.ts +2 -1
  38. package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
  39. package/ts_build/src/lib/chain/btc/index.js +3 -0
  40. package/ts_build/src/lib/chain/btc/index.js.map +1 -1
  41. package/ts_build/src/lib/chain/eth/index.d.ts +1 -0
  42. package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
  43. package/ts_build/src/lib/chain/eth/index.js +3 -0
  44. package/ts_build/src/lib/chain/eth/index.js.map +1 -1
  45. package/ts_build/src/lib/chain/index.d.ts +1 -0
  46. package/ts_build/src/lib/chain/index.d.ts.map +1 -1
  47. package/ts_build/src/lib/chain/index.js +3 -0
  48. package/ts_build/src/lib/chain/index.js.map +1 -1
  49. package/ts_build/src/lib/chain/sol/index.d.ts +1 -0
  50. package/ts_build/src/lib/chain/sol/index.d.ts.map +1 -1
  51. package/ts_build/src/lib/chain/sol/index.js +5 -0
  52. package/ts_build/src/lib/chain/sol/index.js.map +1 -1
  53. package/ts_build/src/lib/chain/xrp/index.d.ts +1 -0
  54. package/ts_build/src/lib/chain/xrp/index.d.ts.map +1 -1
  55. package/ts_build/src/lib/chain/xrp/index.js +4 -0
  56. package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
  57. package/ts_build/src/lib/common/index.d.ts +2 -0
  58. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  59. package/ts_build/src/lib/common/utils.d.ts +4 -0
  60. package/ts_build/src/lib/common/utils.d.ts.map +1 -1
  61. package/ts_build/src/lib/common/utils.js +33 -0
  62. package/ts_build/src/lib/common/utils.js.map +1 -1
  63. package/ts_build/src/lib/routes/middleware/authTssRequest.js +1 -1
  64. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  65. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  66. package/ts_build/src/lib/routes/services.js +1 -13
  67. package/ts_build/src/lib/routes/services.js.map +1 -1
  68. package/ts_build/src/lib/server.d.ts.map +1 -1
  69. package/ts_build/src/lib/server.js +1 -1
  70. package/ts_build/src/lib/server.js.map +1 -1
  71. package/ts_build/test/chain/sol.test.d.ts +2 -0
  72. package/ts_build/test/chain/sol.test.d.ts.map +1 -0
  73. package/ts_build/test/chain/sol.test.js +78 -0
  74. package/ts_build/test/chain/sol.test.js.map +1 -0
  75. package/ts_build/test/chain/xrp.test.js +28 -0
  76. package/ts_build/test/chain/xrp.test.js.map +1 -1
  77. package/ts_build/test/integration/externalservices/simplex.test.js +0 -15
  78. package/ts_build/test/integration/externalservices/simplex.test.js.map +1 -1
  79. package/ts_build/test/integration/server.test.js +80 -0
  80. package/ts_build/test/integration/server.test.js.map +1 -1
  81. package/ts_build/test/integration/tss.test.js +57 -0
  82. package/ts_build/test/integration/tss.test.js.map +1 -1
package/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.11",
5
+ "version": "11.10.13",
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.11",
46
- "@bitpay-labs/crypto-wallet-core": "^11.10.11",
45
+ "@bitpay-labs/bitcore-tss": "^11.10.13",
46
+ "@bitpay-labs/crypto-wallet-core": "^11.10.13",
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": "10bcfcf7cc6953e2af65d707b35d3e65e1254583"
110
+ "gitHead": "c51c900f0009f1237ef30dca2c35922381864491"
111
111
  }
@@ -560,7 +560,8 @@ export class MoonpayService {
560
560
  if (err) {
561
561
  return reject(err.body ? err.body : err);
562
562
  } else {
563
- return resolve(data.body ? data.body : data);
563
+ const success = data.statusCode >= 200 && data.statusCode < 300;
564
+ return resolve(data.body ? data.body : { success });
564
565
  }
565
566
  }
566
567
  );
@@ -260,34 +260,4 @@ export class SimplexService {
260
260
  );
261
261
  });
262
262
  }
263
-
264
- simplexGetEvents(req): Promise<any> {
265
- return new Promise((resolve, reject) => {
266
- if (!config.simplex) return reject(new Error('Simplex missing credentials'));
267
- if (!req.env || (req.env != 'sandbox' && req.env != 'production'))
268
- return reject(new Error("Simplex's request wrong environment"));
269
-
270
- const API = config.simplex[req.env].api;
271
- const API_KEY = config.simplex[req.env].apiKey;
272
- const headers = {
273
- 'Content-Type': 'application/json',
274
- Authorization: 'ApiKey ' + API_KEY
275
- };
276
-
277
- this.request.get(
278
- API + '/wallet/merchant/v2/events',
279
- {
280
- headers,
281
- json: true
282
- },
283
- (err, data) => {
284
- if (err) {
285
- return reject(err.body ? err.body : null);
286
- } else {
287
- return resolve(data.body ? data.body : null);
288
- }
289
- }
290
- );
291
- });
292
- }
293
263
  }
@@ -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
  }
@@ -349,6 +349,11 @@ export class BtcChain implements IChain {
349
349
  });
350
350
  }
351
351
 
352
+ isPrePublishRawBound(_txp: TxProposal) {
353
+ // UTXO chains never mutate tx data at publish, so they never legitimately carry prePublishRaw.
354
+ return false;
355
+ }
356
+
352
357
  getBitcoreTx(txp, opts = { signed: true }) {
353
358
  const t = new this.bitcoreLib.Transaction();
354
359
 
@@ -396,24 +401,24 @@ export class BtcChain implements IChain {
396
401
  }
397
402
 
398
403
  for (const o of txp.outputs || []) {
399
- o.amount = parseInt(o.amount);
404
+ const amount = parseInt(o.amount);
400
405
  $.checkState(o.script || o.toAddress, 'Failed state: Output should have either toAddress or script specified at <getBitcoreTx()>');
401
406
  if (o.script) {
402
407
  t.addOutput(
403
408
  new this.bitcoreLib.Transaction.Output({
404
409
  script: o.script,
405
- satoshis: o.amount
410
+ satoshis: amount
406
411
  })
407
412
  );
408
413
  } else {
409
- t.to(o.toAddress, o.amount);
414
+ t.to(o.toAddress, amount);
410
415
  }
411
416
  }
412
417
 
413
418
  t.fee(parseInt(txp.fee));
414
419
 
415
420
  if (txp.instantAcceptanceEscrow && txp.escrowAddress) {
416
- t.escrow(txp.escrowAddress.address, txp.instantAcceptanceEscrow + txp.fee);
421
+ t.escrow(txp.escrowAddress.address, Number(txp.instantAcceptanceEscrow) + Number(txp.fee));
417
422
  }
418
423
 
419
424
  if (txp.enableRBF) t.enableRBF();
@@ -563,7 +568,7 @@ export class BtcChain implements IChain {
563
568
  conservativeEstimation: opts.payProUrl ? true : false,
564
569
  instantAcceptanceEscrow: opts.instantAcceptanceEscrow
565
570
  };
566
- const escrowAmount = opts.instantAcceptanceEscrow || 0;
571
+ const escrowAmount = Number(opts.instantAcceptanceEscrow) || 0;
567
572
  const txpAmount = txp.getTotalAmount() + escrowAmount;
568
573
  const baseTxpSize = this.getEstimatedSize(txp, feeOpts);
569
574
  const baseTxpFee = (baseTxpSize * txp.feePerKb) / 1000;
@@ -287,6 +287,10 @@ export class EthChain implements IChain {
287
287
  });
288
288
  }
289
289
 
290
+ isPrePublishRawBound(txp: TxProposal) {
291
+ return Common.Utils.isPrePublishRawBound(this, txp);
292
+ }
293
+
290
294
  getBitcoreTx(txp: TxProposal, opts = { signed: true }) {
291
295
  const {
292
296
  data,
@@ -464,7 +468,7 @@ export class EthChain implements IChain {
464
468
  const txpTotalAmount =
465
469
  (opts.multisigContractAddress || opts.tokenAddress) && txp.payProUrl
466
470
  ? getInvoiceValue(txp)
467
- : txp.getTotalAmount();
471
+ : txp.getTotalAmountBigInt();
468
472
 
469
473
  if (totalAmount < txpTotalAmount) {
470
474
  return cb(Errors.INSUFFICIENT_FUNDS);
@@ -98,6 +98,10 @@ class ChainProxy {
98
98
  return this.get(txp.chain).getBitcoreTx(txp, { signed: opts.signed });
99
99
  }
100
100
 
101
+ isPrePublishRawBound(txp: TxProposal<any>) {
102
+ return this.get(txp.chain).isPrePublishRawBound(txp);
103
+ }
104
+
101
105
  convertFeePerKb(chain, p, feePerKb) {
102
106
  return this.get(chain).convertFeePerKb(p, feePerKb);
103
107
  }
@@ -2,7 +2,9 @@ import { Transactions, Utils, Validation } from '@bitpay-labs/crypto-wallet-core
2
2
  import _ from 'lodash';
3
3
  import { IChain } from '../../../types/chain';
4
4
  import { WalletWithOpts } from '../../blockchainexplorers/v8';
5
+ import { Common } from '../../common';
5
6
  import { Defaults } from '../../common/defaults';
7
+ import { ClientError } from '../../errors/clienterror';
6
8
  import { Errors } from '../../errors/errordefinitions';
7
9
  import logger from '../../logger';
8
10
  import { IWallet, TxProposal, Wallet } from '../../model';
@@ -28,6 +30,7 @@ export class SolChain implements IChain {
28
30
  totalConfirmedAmount: confirmed,
29
31
  lockedAmount: activatedLocked,
30
32
  lockedConfirmedAmount: activatedLocked,
33
+ reserve: balance > 0 ? minRent : 0,
31
34
  availableAmount: balance - activatedLocked,
32
35
  availableConfirmedAmount: confirmed - activatedLocked,
33
36
  byAddress: []
@@ -93,11 +96,31 @@ export class SolChain implements IChain {
93
96
  return new Promise(resolve => {
94
97
  const numSignatures = opts.numSignatures || 1;
95
98
  const feePerKb = Defaults.SOL_BASE_FEE; // Fee per signature in lamports
96
- const fee = feePerKb * numSignatures;
99
+ let fee = feePerKb * numSignatures;
100
+ if (opts.priorityFee) {
101
+ fee += this.getPriorityFee(opts);
102
+ }
97
103
  return resolve({ fee, feePerKb });
98
104
  });
99
105
  }
100
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
+
120
+ isPrePublishRawBound(txp) {
121
+ return Common.Utils.isPrePublishRawBound(this, txp);
122
+ }
123
+
101
124
  getBitcoreTx(txp, opts = { signed: true }) {
102
125
  const {
103
126
  data,
@@ -227,16 +250,41 @@ export class SolChain implements IChain {
227
250
  const { totalAmount, availableAmount } = balance;
228
251
  // calculate how much space is needed to find rent amount
229
252
  const minRentException = opts.tokenAddress ? 0 : Defaults.MIN_SOL_BALANCE;
230
- if (totalAmount - minRentException < txp.getTotalAmount()) {
253
+ if (totalAmount - minRentException < txp.getTotalAmountBigInt()) {
231
254
  return cb(Errors.INSUFFICIENT_FUNDS);
232
- } else if (availableAmount < txp.getTotalAmount()) {
255
+ } else if (availableAmount < txp.getTotalAmountBigInt()) {
233
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));
234
272
  } else {
235
273
  return cb(this.checkTx(txp));
236
274
  }
237
275
  });
238
276
  }
239
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
+
240
288
  validateAddress(wallet, inaddr, opts) {
241
289
  const chain = this.chain.toLowerCase();
242
290
  const isValidTo = Validation.validateAddress(chain, wallet.network, inaddr);
@@ -29,6 +29,7 @@ export class XrpChain implements IChain {
29
29
  totalConfirmedAmount: confirmed,
30
30
  lockedAmount: activatedLocked,
31
31
  lockedConfirmedAmount: activatedLocked,
32
+ reserve: balance > 0 ? reserve : 0,
32
33
  availableAmount: balance - activatedLocked,
33
34
  availableConfirmedAmount: confirmed - activatedLocked,
34
35
  byAddress: []
@@ -143,6 +144,10 @@ export class XrpChain implements IChain {
143
144
  });
144
145
  }
145
146
 
147
+ isPrePublishRawBound(txp) {
148
+ return Common.Utils.isPrePublishRawBound(this, txp);
149
+ }
150
+
146
151
  getBitcoreTx(txp, opts = { signed: true }) {
147
152
  const { destinationTag, outputs, outputOrder, multiTx } = txp;
148
153
  const chain = 'XRP';
@@ -218,9 +223,9 @@ export class XrpChain implements IChain {
218
223
  if (err) return cb(err);
219
224
  const { totalAmount, availableAmount } = balance;
220
225
  const minXrpBalance = Defaults.MIN_XRP_BALANCE;
221
- if (totalAmount - minXrpBalance < txp.getTotalAmount()) {
226
+ if (totalAmount - minXrpBalance < txp.getTotalAmountBigInt()) {
222
227
  return cb(Errors.INSUFFICIENT_FUNDS);
223
- } else if (availableAmount < txp.getTotalAmount()) {
228
+ } else if (availableAmount < txp.getTotalAmountBigInt()) {
224
229
  return cb(Errors.LOCKED_FUNDS);
225
230
  } else {
226
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
 
@@ -3,7 +3,8 @@ import {
3
3
  BitcoreLibCash,
4
4
  BitcoreLibDoge,
5
5
  BitcoreLibLtc,
6
- Constants as CWConstants
6
+ Constants as CWConstants,
7
+ Transactions
7
8
  } from '@bitpay-labs/crypto-wallet-core';
8
9
  import _ from 'lodash';
9
10
  import { singleton } from 'preconditions';
@@ -12,6 +13,8 @@ import Config from '../../config';
12
13
  import { logger } from '../logger';
13
14
  import { Constants } from './constants';
14
15
  import { Defaults } from './defaults';
16
+ import type { IChain } from '../../types/chain';
17
+ import type { TxProposal } from '../model/txproposal';
15
18
 
16
19
  const $ = singleton();
17
20
  const Bitcore_ = {
@@ -23,6 +26,54 @@ const Bitcore_ = {
23
26
 
24
27
  export const Utils = {
25
28
 
29
+ /**
30
+ * txp.prePublishRaw as a list of raw transactions, or null when it isn't one. An empty list is truthy and
31
+ * hashes to nothing, so without this the value reaches the tx provider and fails as a thrown error rather
32
+ * than a refused check. Mirrors the client-side Verifier helper so both ends agree on a usable fallback.
33
+ *
34
+ * @param txp - the transaction proposal
35
+ */
36
+ prePublishRawParts(txp: TxProposal<any>): string[] | null {
37
+ if (!txp?.prePublishRaw) {
38
+ return null;
39
+ }
40
+ const parts = Array.isArray(txp.prePublishRaw) ? txp.prePublishRaw : [txp.prePublishRaw];
41
+ if (!parts.length || parts.some(raw => !raw || typeof raw !== 'string')) {
42
+ return null;
43
+ }
44
+ return parts;
45
+ },
46
+
47
+ /**
48
+ * Shared implementation of IChain.isPrePublishRawBound for account-based chains (SVM/EVM/XRP). True only if
49
+ * txp.prePublishRaw is the same transaction as the current proposal, differing solely in the field BWS
50
+ * mutates at publish (blockhash on SVM, nonce on EVM/XRP). Recovers that field from prePublishRaw, rebuilds
51
+ * the proposal via the chain's own getBitcoreTx, and requires byte-for-byte equality, so a tampered stored
52
+ * proposal cannot reuse an old, still-valid proposalSignature. Fails closed on any error.
53
+ *
54
+ * @param chain - the IChain implementation (provides chain + getBitcoreTx)
55
+ * @param txp - the transaction proposal (must carry prePublishRaw)
56
+ */
57
+ isPrePublishRawBound(chain: IChain, txp: TxProposal<any>): boolean {
58
+ try {
59
+ const prePublishRaw = Utils.prePublishRawParts(txp);
60
+ if (!prePublishRaw) return false;
61
+ const provider = Transactions.get({ chain: txp.chain }) as any;
62
+ if (typeof provider?.getMutableFields !== 'function') return false;
63
+ const mutableFields = provider.getMutableFields(prePublishRaw[0]);
64
+ if (!mutableFields || Object.values(mutableFields).every(v => v == null)) return false;
65
+ const cloned = Object.assign(Object.create(Object.getPrototypeOf(txp)), txp, mutableFields);
66
+ // signed: false to match prePublishRaw, which is captured before any copayer signature exists.
67
+ const rebuilt = chain.getBitcoreTx(cloned, { signed: false }).uncheckedSerialize();
68
+ const rebuiltArr = Array.isArray(rebuilt) ? rebuilt : [rebuilt];
69
+ if (rebuiltArr.length !== prePublishRaw.length) return false;
70
+ return rebuiltArr.every((raw, i) => raw === prePublishRaw[i]);
71
+ } catch (err) {
72
+ logger.warn('prePublishRaw binding check failed for txp %s on %s: %o', txp?.id, txp?.chain, err.stack || err.message || err);
73
+ return false;
74
+ }
75
+ },
76
+
26
77
  /**
27
78
  * @deprecated
28
79
  * Moved from ChainService to prevent circular dependency
@@ -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)
@@ -35,7 +35,7 @@ export function authTssRequest(): express.RequestHandler {
35
35
  if (req.path.includes('/tss/keygen/')) {
36
36
  session = await storage.fetchTssKeyGenSession({ id });
37
37
  partyId = session?.participants.indexOf(copayerId);
38
- pubKey = partyId > -1 ? session.rounds[0][partyId].messages.publicKey : null;
38
+ pubKey = partyId > -1 ? session.rounds[0].find(r => r.fromPartyId === partyId)?.messages.publicKey : null;
39
39
  } else if (req.path.includes('/tss/sign/')) {
40
40
  session = await storage.fetchTssSigSession({ id });
41
41
  partyId = session?.participants.find(p => p.copayerId === copayerId)?.partyId;
@@ -65,7 +65,7 @@ function respondWithAuthServer(req, res, context: RouteContext, handler: RouteHa
65
65
  }
66
66
 
67
67
  export function registerServiceRoutes(router: express.Router, context: RouteContext) {
68
- const { getServerWithAuth, setPublicCache, returnError } = context;
68
+ const { setPublicCache } = context;
69
69
 
70
70
  const transakCorsOptions = {
71
71
  origin: (origin, cb) => {
@@ -399,19 +399,6 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
399
399
  });
400
400
  });
401
401
 
402
- router.get('/v1/service/simplex/events', (req, res) => {
403
- getServerWithAuth(req, res, server => {
404
- Promise.resolve()
405
- .then(() => server.externalServices.simplex.simplexGetEvents({ env: req.query.env }))
406
- .then(response => {
407
- res.json(response);
408
- })
409
- .catch(err => {
410
- returnError(err ?? 'unknown', res, req);
411
- });
412
- });
413
- });
414
-
415
402
  router.post('/v1/service/thorswap/supportedChains', (req, res) => {
416
403
  respondWithPublicServer(req, res, context, server => {
417
404
  return server.externalServices.thorswap.thorswapGetSupportedChains(req);
@@ -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) => {