@bitpay-labs/bitcore-wallet-service 11.10.11 → 11.10.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/externalservices/moonpay.ts +2 -1
- package/src/externalservices/simplex.ts +0 -30
- package/src/lib/chain/btc/index.ts +12 -7
- package/src/lib/chain/eth/index.ts +5 -1
- package/src/lib/chain/index.ts +4 -0
- package/src/lib/chain/sol/index.ts +51 -3
- package/src/lib/chain/xrp/index.ts +7 -2
- package/src/lib/common/defaults.ts +6 -0
- package/src/lib/common/utils.ts +52 -1
- package/src/lib/errors/errordefinitions.ts +5 -0
- package/src/lib/model/txproposal.ts +39 -11
- package/src/lib/routes/context.ts +3 -1
- package/src/lib/routes/helpers/error.ts +2 -2
- package/src/lib/routes/middleware/authTssRequest.ts +1 -1
- package/src/lib/routes/services.ts +1 -14
- package/src/lib/routes/transactions.ts +5 -5
- package/src/lib/routes/wallets.ts +2 -2
- package/src/lib/server.ts +11 -4
- package/src/types/chain.d.ts +1 -0
- package/src/types/expressapp.d.ts +1 -1
- package/test/chain/btc.test.ts +32 -0
- package/test/chain/eth.test.ts +43 -0
- package/test/chain/sol.test.ts +229 -0
- package/test/chain/xrp.test.ts +51 -0
- package/test/integration/externalservices/moonpay.test.ts +29 -0
- package/test/integration/externalservices/simplex.test.ts +0 -18
- package/test/integration/server.test.ts +193 -0
- package/test/integration/tss.test.ts +67 -0
- package/test/model/txproposal.test.ts +118 -0
- package/test/numberformat-routes.test.ts +88 -0
- package/ts_build/package.json +3 -3
- package/ts_build/src/externalservices/simplex.d.ts +0 -1
- package/ts_build/src/externalservices/simplex.d.ts.map +1 -1
- package/ts_build/src/externalservices/simplex.js +0 -25
- package/ts_build/src/externalservices/simplex.js.map +1 -1
- package/ts_build/src/lib/chain/btc/index.d.ts +2 -1
- package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/btc/index.js +3 -0
- package/ts_build/src/lib/chain/btc/index.js.map +1 -1
- package/ts_build/src/lib/chain/eth/index.d.ts +1 -0
- package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/eth/index.js +3 -0
- package/ts_build/src/lib/chain/eth/index.js.map +1 -1
- package/ts_build/src/lib/chain/index.d.ts +1 -0
- package/ts_build/src/lib/chain/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/index.js +3 -0
- package/ts_build/src/lib/chain/index.js.map +1 -1
- package/ts_build/src/lib/chain/sol/index.d.ts +1 -0
- package/ts_build/src/lib/chain/sol/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/sol/index.js +5 -0
- package/ts_build/src/lib/chain/sol/index.js.map +1 -1
- package/ts_build/src/lib/chain/xrp/index.d.ts +1 -0
- package/ts_build/src/lib/chain/xrp/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/xrp/index.js +4 -0
- package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
- package/ts_build/src/lib/common/index.d.ts +2 -0
- package/ts_build/src/lib/common/index.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.d.ts +4 -0
- package/ts_build/src/lib/common/utils.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.js +33 -0
- package/ts_build/src/lib/common/utils.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
- package/ts_build/src/lib/routes/services.d.ts.map +1 -1
- package/ts_build/src/lib/routes/services.js +1 -13
- package/ts_build/src/lib/routes/services.js.map +1 -1
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +1 -1
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/test/chain/sol.test.d.ts +2 -0
- package/ts_build/test/chain/sol.test.d.ts.map +1 -0
- package/ts_build/test/chain/sol.test.js +78 -0
- package/ts_build/test/chain/sol.test.js.map +1 -0
- package/ts_build/test/chain/xrp.test.js +28 -0
- package/ts_build/test/chain/xrp.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/simplex.test.js +0 -15
- package/ts_build/test/integration/externalservices/simplex.test.js.map +1 -1
- package/ts_build/test/integration/server.test.js +80 -0
- package/ts_build/test/integration/server.test.js.map +1 -1
- package/ts_build/test/integration/tss.test.js +57 -0
- package/ts_build/test/integration/tss.test.js.map +1 -1
package/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.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.
|
|
46
|
-
"@bitpay-labs/crypto-wallet-core": "^11.10.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
410
|
+
satoshis: amount
|
|
406
411
|
})
|
|
407
412
|
);
|
|
408
413
|
} else {
|
|
409
|
-
t.to(o.toAddress,
|
|
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.
|
|
471
|
+
: txp.getTotalAmountBigInt();
|
|
468
472
|
|
|
469
473
|
if (totalAmount < txpTotalAmount) {
|
|
470
474
|
return cb(Errors.INSUFFICIENT_FUNDS);
|
package/src/lib/chain/index.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
253
|
+
if (totalAmount - minRentException < txp.getTotalAmountBigInt()) {
|
|
231
254
|
return cb(Errors.INSUFFICIENT_FUNDS);
|
|
232
|
-
} else if (availableAmount < txp.
|
|
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.
|
|
226
|
+
if (totalAmount - minXrpBalance < txp.getTotalAmountBigInt()) {
|
|
222
227
|
return cb(Errors.INSUFFICIENT_FUNDS);
|
|
223
|
-
} else if (availableAmount < txp.
|
|
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
|
|
package/src/lib/common/utils.ts
CHANGED
|
@@ -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(
|
|
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)
|
|
@@ -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]
|
|
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 {
|
|
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) => {
|