@bitpay-labs/bitcore-wallet-service 11.7.0 → 11.7.2
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/README.md +1 -0
- package/package.json +4 -4
- package/src/externalservices/banxa.ts +6 -0
- package/src/lib/chain/xrp/index.ts +1 -1
- package/src/lib/expressapp.ts +2 -0
- package/src/lib/server.ts +37 -3
- package/test/integration/helpers.ts +2 -1
- package/test/integration/history.test.ts +152 -1
- package/test/integration/server.test.ts +218 -0
- package/test/txhistory.paging.test.ts +166 -0
- package/ts_build/package.json +4 -4
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +3 -0
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/test/integration/server.test.js +110 -0
- package/ts_build/test/integration/server.test.js.map +1 -1
package/README.md
CHANGED
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.7.
|
|
5
|
+
"version": "11.7.2",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "ts_build/src/index.js",
|
|
8
8
|
"types": "./ts_build/src/index.d.ts",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"cp:templates": "cp -r ./templates ./ts_build/templates"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@bitpay-labs/bitcore-tss": "^11.7.
|
|
47
|
-
"@bitpay-labs/crypto-wallet-core": "^11.7.
|
|
46
|
+
"@bitpay-labs/bitcore-tss": "^11.7.2",
|
|
47
|
+
"@bitpay-labs/crypto-wallet-core": "^11.7.2",
|
|
48
48
|
"@sendgrid/mail": "6.5.4",
|
|
49
49
|
"abi-decoder": "2.4.0",
|
|
50
50
|
"async": "0.9.2",
|
|
@@ -126,5 +126,5 @@
|
|
|
126
126
|
"email": "kjoseph@bitpay.com"
|
|
127
127
|
}
|
|
128
128
|
],
|
|
129
|
-
"gitHead": "
|
|
129
|
+
"gitHead": "d7f26cd2fae506cba9bb8965c5a196b13d8f0586"
|
|
130
130
|
}
|
|
@@ -190,6 +190,12 @@ export class BanxaService {
|
|
|
190
190
|
return reject(new ClientError("Banxa's request missing arguments"));
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
if (!req.body.payment_method || req.body.payment_method === 'other') {
|
|
194
|
+
// Workaround to allow older versions of the app to freely choose the payment method on the checkout page when they select "other".
|
|
195
|
+
delete req.body.payment_method_id;
|
|
196
|
+
}
|
|
197
|
+
delete req.body.payment_method;
|
|
198
|
+
|
|
193
199
|
const UriPath = '/orders';
|
|
194
200
|
const URL: string = API + UriPath;
|
|
195
201
|
const auth = this.getBanxaSignature('post', UriPath, API_KEY, SECRET_KEY, req.body);
|
|
@@ -161,7 +161,7 @@ export class XrpChain implements IChain {
|
|
|
161
161
|
...txp,
|
|
162
162
|
tag: _tag ? Number(_tag) : undefined,
|
|
163
163
|
chain,
|
|
164
|
-
nonce: Number(txp.nonce) + Number(index),
|
|
164
|
+
nonce: (txp.nonce != null ? Number(txp.nonce) : 0) + Number(index),
|
|
165
165
|
recipients: [recepient]
|
|
166
166
|
});
|
|
167
167
|
unsignedTxs.push(rawTx);
|
package/src/lib/expressapp.ts
CHANGED
|
@@ -1180,12 +1180,14 @@ export class ExpressApp {
|
|
|
1180
1180
|
const opts: {
|
|
1181
1181
|
skip?: number;
|
|
1182
1182
|
limit?: number;
|
|
1183
|
+
reverse?: boolean;
|
|
1183
1184
|
includeExtendedInfo?: boolean;
|
|
1184
1185
|
tokenAddress?: string;
|
|
1185
1186
|
multisigContractAddress?: string;
|
|
1186
1187
|
} = {};
|
|
1187
1188
|
if (req.query.skip) opts.skip = +req.query.skip;
|
|
1188
1189
|
if (req.query.limit) opts.limit = +req.query.limit;
|
|
1190
|
+
if (req.query.reverse == '1') opts.reverse = true;
|
|
1189
1191
|
if (req.query.tokenAddress) opts.tokenAddress = req.query.tokenAddress as string;
|
|
1190
1192
|
if (req.query.multisigContractAddress)
|
|
1191
1193
|
opts.multisigContractAddress = req.query.multisigContractAddress as string;
|
package/src/lib/server.ts
CHANGED
|
@@ -3314,6 +3314,11 @@ export class WalletService implements IWalletService {
|
|
|
3314
3314
|
if (action) return cb(Errors.COPAYER_VOTED);
|
|
3315
3315
|
if (!txp.isPending()) return cb(Errors.TX_NOT_PENDING);
|
|
3316
3316
|
|
|
3317
|
+
// Client-supplied nonce override
|
|
3318
|
+
if (opts.nonce != null && [...Object.keys(Constants.EVM_CHAINS), 'XRP'].includes(wallet.chain.toUpperCase())) {
|
|
3319
|
+
txp.nonce = Number(opts.nonce);
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3317
3322
|
if (this._upgradeNeeded(UPGRADES.BCH_schnorr, { signingMethod: txp.signingMethod, supportBchSchnorr: opts.supportBchSchnorr })) {
|
|
3318
3323
|
return cb(Errors.UPGRADE_NEEDED);
|
|
3319
3324
|
}
|
|
@@ -3407,7 +3412,7 @@ export class WalletService implements IWalletService {
|
|
|
3407
3412
|
return cb(null, txp);
|
|
3408
3413
|
}
|
|
3409
3414
|
|
|
3410
|
-
if (!Constants.EVM_CHAINS[wallet.chain.toUpperCase()]) {
|
|
3415
|
+
if (!Constants.EVM_CHAINS[wallet.chain.toUpperCase()] && wallet.chain.toUpperCase() !== 'XRP') {
|
|
3411
3416
|
return cb(null, txp);
|
|
3412
3417
|
}
|
|
3413
3418
|
|
|
@@ -4482,6 +4487,13 @@ export class WalletService implements IWalletService {
|
|
|
4482
4487
|
next => {
|
|
4483
4488
|
if (streamData) {
|
|
4484
4489
|
lastTxs = streamData;
|
|
4490
|
+
if (cacheStatus.tipTxId) {
|
|
4491
|
+
// Stream data can outlive cache promotion, so trim any entries that
|
|
4492
|
+
// are now part of the durable cache before paging.
|
|
4493
|
+
lastTxs = _.takeWhile(lastTxs, (tx: any) => {
|
|
4494
|
+
return tx.txid != cacheStatus.tipTxId;
|
|
4495
|
+
});
|
|
4496
|
+
}
|
|
4485
4497
|
return next();
|
|
4486
4498
|
}
|
|
4487
4499
|
|
|
@@ -4514,6 +4526,24 @@ export class WalletService implements IWalletService {
|
|
|
4514
4526
|
});
|
|
4515
4527
|
},
|
|
4516
4528
|
next => {
|
|
4529
|
+
if (opts.reverse) {
|
|
4530
|
+
const cachedTxs = _.isNumber(cacheStatus.tipIndex) ? cacheStatus.tipIndex + 1 : 0;
|
|
4531
|
+
const totalTxs = cachedTxs + lastTxs.length;
|
|
4532
|
+
const oldestFirstSkip = skip;
|
|
4533
|
+
|
|
4534
|
+
// Keep the existing newest-first cache math by translating the requested
|
|
4535
|
+
// oldest-first cursor into the equivalent newest-first slice.
|
|
4536
|
+
limit = Math.max(0, Math.min(limit, totalTxs - oldestFirstSkip));
|
|
4537
|
+
skip = Math.max(0, totalTxs - oldestFirstSkip - limit);
|
|
4538
|
+
}
|
|
4539
|
+
|
|
4540
|
+
if (limit === 0) {
|
|
4541
|
+
resultTxs = [];
|
|
4542
|
+
fromCache = false;
|
|
4543
|
+
fromBc = false;
|
|
4544
|
+
return next();
|
|
4545
|
+
}
|
|
4546
|
+
|
|
4517
4547
|
// Case 1.
|
|
4518
4548
|
// t -->
|
|
4519
4549
|
// | Old TXS | ======= LAST TXS ========== \
|
|
@@ -4598,6 +4628,9 @@ export class WalletService implements IWalletService {
|
|
|
4598
4628
|
],
|
|
4599
4629
|
err => {
|
|
4600
4630
|
if (err) return cb(err);
|
|
4631
|
+
if (opts.reverse) {
|
|
4632
|
+
resultTxs.reverse();
|
|
4633
|
+
}
|
|
4601
4634
|
return cb(null, {
|
|
4602
4635
|
items: resultTxs,
|
|
4603
4636
|
fromCache,
|
|
@@ -4615,10 +4648,11 @@ export class WalletService implements IWalletService {
|
|
|
4615
4648
|
* @param {Object} opts
|
|
4616
4649
|
* @param {Number} opts.skip (defaults to 0)
|
|
4617
4650
|
* @param {Number} opts.limit
|
|
4651
|
+
* @param {Boolean} opts.reverse[=false] - Return oldest transactions first.
|
|
4618
4652
|
* @param {String} opts.tokenAddress ERC20 Token Contract Address
|
|
4619
4653
|
* @param {String} opts.multisigContractAddress MULTISIG ETH Contract Address
|
|
4620
4654
|
* @param {Number} opts.includeExtendedInfo[=false] - Include all inputs/outputs for every tx.
|
|
4621
|
-
* @returns {TxProposal[]} Transaction proposals, first
|
|
4655
|
+
* @returns {TxProposal[]} Transaction proposals, newer first unless opts.reverse is set
|
|
4622
4656
|
*/
|
|
4623
4657
|
getTxHistory(opts, cb) {
|
|
4624
4658
|
opts = opts || {};
|
|
@@ -5516,4 +5550,4 @@ export const UPGRADES = {
|
|
|
5516
5550
|
bwc_$lt_1_2: 'bwc<1.2',
|
|
5517
5551
|
} as const;
|
|
5518
5552
|
|
|
5519
|
-
type Upgrade = typeof UPGRADES[keyof typeof UPGRADES];
|
|
5553
|
+
type Upgrade = typeof UPGRADES[keyof typeof UPGRADES];
|
|
@@ -559,6 +559,7 @@ class Helpers {
|
|
|
559
559
|
// For eth => account, 0, change = 0
|
|
560
560
|
const priv = xpriv.derive('m/0/0').privateKey;
|
|
561
561
|
const privKey = priv.toString('hex');
|
|
562
|
+
const pubKey = priv.publicKey.toString('hex');
|
|
562
563
|
let tx = ChainService.getBitcoreTx(txp).uncheckedSerialize();
|
|
563
564
|
const isERC20 = txp.tokenAddress && !txp.payProUrl;
|
|
564
565
|
const chain = isERC20 ? Utils.getChain(txp.coin) + 'ERC20' : Utils.getChain(txp.coin);
|
|
@@ -568,7 +569,7 @@ class Helpers {
|
|
|
568
569
|
const signed = Transactions.getSignature({
|
|
569
570
|
chain: chain.toUpperCase(),
|
|
570
571
|
tx: rawTx,
|
|
571
|
-
key: { privKey: privKey.toString('hex') },
|
|
572
|
+
key: { privKey: privKey.toString('hex'), pubKey },
|
|
572
573
|
});
|
|
573
574
|
signatures.push(signed);
|
|
574
575
|
}
|
|
@@ -85,6 +85,158 @@ describe('History', function() {
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
it('should page from oldest first with reverse and keep skip stable if new txs arrive', async function() {
|
|
89
|
+
const _cache = Defaults.CONFIRMATIONS_TO_START_CACHING;
|
|
90
|
+
(Defaults.CONFIRMATIONS_TO_START_CACHING as any) = 1;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const baseTxs = helpers.createTxsV8(5, BCHEIGHT);
|
|
94
|
+
helpers.stubHistory(null, null, baseTxs);
|
|
95
|
+
|
|
96
|
+
const firstPage = await new Promise<any[]>((resolve, reject) => {
|
|
97
|
+
server.getTxHistory({ limit: 2, reverse: true }, function(err, txs) {
|
|
98
|
+
if (err) return reject(err);
|
|
99
|
+
resolve(txs);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
firstPage.map(tx => tx.id).should.deep.equal(['id4', 'id3']);
|
|
104
|
+
|
|
105
|
+
const txTemplate = baseTxs[0];
|
|
106
|
+
const updatedTxs = [
|
|
107
|
+
{
|
|
108
|
+
...txTemplate,
|
|
109
|
+
id: 'newid0',
|
|
110
|
+
txid: 'newtxid0',
|
|
111
|
+
height: -1,
|
|
112
|
+
blockTime: '2018-09-21T18:08:33.000Z'
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
...txTemplate,
|
|
116
|
+
id: 'newid1',
|
|
117
|
+
txid: 'newtxid1',
|
|
118
|
+
height: -1,
|
|
119
|
+
blockTime: '2018-09-21T18:08:32.000Z'
|
|
120
|
+
},
|
|
121
|
+
...baseTxs
|
|
122
|
+
];
|
|
123
|
+
helpers.stubHistory(null, null, updatedTxs);
|
|
124
|
+
|
|
125
|
+
const secondPage = await new Promise<any[]>((resolve, reject) => {
|
|
126
|
+
server.getTxHistory({ skip: 2, limit: 2, reverse: true }, function(err, txs) {
|
|
127
|
+
if (err) return reject(err);
|
|
128
|
+
resolve(txs);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
secondPage.map(tx => tx.id).should.deep.equal(['id2', 'id1']);
|
|
133
|
+
} finally {
|
|
134
|
+
(Defaults.CONFIRMATIONS_TO_START_CACHING as any) = _cache;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should trim reused stream data against the current cache tip in reverse mode', async function() {
|
|
139
|
+
const _cache = Defaults.CONFIRMATIONS_TO_START_CACHING;
|
|
140
|
+
(Defaults.CONFIRMATIONS_TO_START_CACHING as any) = 1;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const baseTxs = helpers.createTxsV8(4, BCHEIGHT);
|
|
144
|
+
helpers.stubHistory(null, null, baseTxs);
|
|
145
|
+
|
|
146
|
+
await new Promise<void>((resolve, reject) => {
|
|
147
|
+
server.storage.storeTxHistoryCacheV8(
|
|
148
|
+
wallet.id,
|
|
149
|
+
null,
|
|
150
|
+
[
|
|
151
|
+
{
|
|
152
|
+
id: baseTxs[3].id,
|
|
153
|
+
txid: baseTxs[3].txid,
|
|
154
|
+
blockheight: baseTxs[3].height
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
0,
|
|
158
|
+
err => {
|
|
159
|
+
if (err) return reject(err);
|
|
160
|
+
resolve();
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const firstPage = await new Promise<{ txs: any[]; useStream: boolean }>((resolve, reject) => {
|
|
166
|
+
server.getTxHistory({ limit: 2, reverse: true }, function(err, txs, _fromCache, useStream) {
|
|
167
|
+
if (err) return reject(err);
|
|
168
|
+
resolve({ txs, useStream });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
firstPage.useStream.should.equal(false);
|
|
173
|
+
firstPage.txs.map(tx => tx.id).should.deep.equal(['id3', 'id2']);
|
|
174
|
+
|
|
175
|
+
const secondPage = await new Promise<{ txs: any[]; useStream: boolean }>((resolve, reject) => {
|
|
176
|
+
server.getTxHistory({ skip: 2, limit: 2, reverse: true }, function(err, txs, _fromCache, useStream) {
|
|
177
|
+
if (err) return reject(err);
|
|
178
|
+
resolve({ txs, useStream });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
secondPage.useStream.should.equal(true);
|
|
183
|
+
secondPage.txs.map(tx => tx.id).should.deep.equal(['id1', 'id0']);
|
|
184
|
+
} finally {
|
|
185
|
+
(Defaults.CONFIRMATIONS_TO_START_CACHING as any) = _cache;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should trim reused stream data against the current cache tip in newest-first mode', async function() {
|
|
190
|
+
const _cache = Defaults.CONFIRMATIONS_TO_START_CACHING;
|
|
191
|
+
(Defaults.CONFIRMATIONS_TO_START_CACHING as any) = 1;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const baseTxs = helpers.createTxsV8(4, BCHEIGHT);
|
|
195
|
+
helpers.stubHistory(null, null, baseTxs);
|
|
196
|
+
|
|
197
|
+
await new Promise<void>((resolve, reject) => {
|
|
198
|
+
server.storage.storeTxHistoryCacheV8(
|
|
199
|
+
wallet.id,
|
|
200
|
+
null,
|
|
201
|
+
[
|
|
202
|
+
{
|
|
203
|
+
id: baseTxs[3].id,
|
|
204
|
+
txid: baseTxs[3].txid,
|
|
205
|
+
blockheight: baseTxs[3].height
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
0,
|
|
209
|
+
err => {
|
|
210
|
+
if (err) return reject(err);
|
|
211
|
+
resolve();
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const firstPage = await new Promise<{ txs: any[]; useStream: boolean }>((resolve, reject) => {
|
|
217
|
+
server.getTxHistory({ limit: 2 }, function(err, txs, _fromCache, useStream) {
|
|
218
|
+
if (err) return reject(err);
|
|
219
|
+
resolve({ txs, useStream });
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
firstPage.useStream.should.equal(false);
|
|
224
|
+
firstPage.txs.map(tx => tx.id).should.deep.equal(['id0', 'id1']);
|
|
225
|
+
|
|
226
|
+
const secondPage = await new Promise<{ txs: any[]; useStream: boolean }>((resolve, reject) => {
|
|
227
|
+
server.getTxHistory({ skip: 2, limit: 2 }, function(err, txs, _fromCache, useStream) {
|
|
228
|
+
if (err) return reject(err);
|
|
229
|
+
resolve({ txs, useStream });
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
secondPage.useStream.should.equal(true);
|
|
234
|
+
secondPage.txs.map(tx => tx.id).should.deep.equal(['id2', 'id3']);
|
|
235
|
+
} finally {
|
|
236
|
+
(Defaults.CONFIRMATIONS_TO_START_CACHING as any) = _cache;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
88
240
|
it('should filter out DUST amount', function(done) {
|
|
89
241
|
const txs= helpers.createTxsV8(50, BCHEIGHT);
|
|
90
242
|
txs[5].satoshis=100;
|
|
@@ -1139,4 +1291,3 @@ describe('History', function() {
|
|
|
1139
1291
|
});
|
|
1140
1292
|
});
|
|
1141
1293
|
});
|
|
1142
|
-
|
|
@@ -8134,6 +8134,224 @@ describe('Wallet service', function() {
|
|
|
8134
8134
|
});
|
|
8135
8135
|
});
|
|
8136
8136
|
|
|
8137
|
+
describe('#prepareTx XRP (deferred nonce)', function() {
|
|
8138
|
+
const XRP_ADDR = 'rDzTZxa7NwD9vmNf5dvTbW4FQDNSRsfPv6';
|
|
8139
|
+
let server, wallet, fromAddr;
|
|
8140
|
+
|
|
8141
|
+
beforeEach(async function() {
|
|
8142
|
+
({ server, wallet } = await helpers.createAndJoinWallet(1, 1, { coin: 'xrp' }));
|
|
8143
|
+
const address = await util.promisify(server.createAddress).call(server, {});
|
|
8144
|
+
fromAddr = address.address;
|
|
8145
|
+
await helpers.stubUtxos(server, wallet, [1, 2], { coin: 'xrp' });
|
|
8146
|
+
blockchainExplorer.getTransactionCount = sinon.stub().callsArgWith(1, null, '5');
|
|
8147
|
+
});
|
|
8148
|
+
|
|
8149
|
+
it('should create XRP txp with nonce=null when deferNonce is true', async function() {
|
|
8150
|
+
const txp = await util.promisify(server.createTx).call(server, {
|
|
8151
|
+
outputs: [{ toAddress: XRP_ADDR, amount: 8000 }],
|
|
8152
|
+
feePerKb: 123e2,
|
|
8153
|
+
from: fromAddr,
|
|
8154
|
+
deferNonce: true
|
|
8155
|
+
});
|
|
8156
|
+
should.exist(txp);
|
|
8157
|
+
txp.deferNonce.should.be.true;
|
|
8158
|
+
should.not.exist(txp.nonce);
|
|
8159
|
+
});
|
|
8160
|
+
|
|
8161
|
+
it('should assign nonce to a deferred XRP txp via prepareTx', async function() {
|
|
8162
|
+
blockchainExplorer.getTransactionCount = sinon.stub().callsArgWith(1, null, '10');
|
|
8163
|
+
|
|
8164
|
+
const txp = await helpers.createAndPublishTx(server, {
|
|
8165
|
+
outputs: [{ toAddress: XRP_ADDR, amount: 8000 }],
|
|
8166
|
+
feePerKb: 123e2,
|
|
8167
|
+
from: fromAddr,
|
|
8168
|
+
deferNonce: true
|
|
8169
|
+
}, TestData.copayers[0].privKey_1H_0);
|
|
8170
|
+
|
|
8171
|
+
should.not.exist(txp.nonce);
|
|
8172
|
+
|
|
8173
|
+
const result = await util.promisify(server.prepareTx).call(server, {
|
|
8174
|
+
txProposalId: txp.id
|
|
8175
|
+
});
|
|
8176
|
+
result.nonce.should.equal(10);
|
|
8177
|
+
});
|
|
8178
|
+
|
|
8179
|
+
it('should calculate gap-free nonce for XRP skipping pending nonces', async function() {
|
|
8180
|
+
blockchainExplorer.getTransactionCount = sinon.stub().callsArgWith(1, null, '5');
|
|
8181
|
+
|
|
8182
|
+
const normalTxp = await helpers.createAndPublishTx(server, {
|
|
8183
|
+
outputs: [{ toAddress: XRP_ADDR, amount: 1000 }],
|
|
8184
|
+
feePerKb: 123e2,
|
|
8185
|
+
from: fromAddr
|
|
8186
|
+
}, TestData.copayers[0].privKey_1H_0);
|
|
8187
|
+
normalTxp.nonce.should.equal('5');
|
|
8188
|
+
|
|
8189
|
+
const deferred = await helpers.createAndPublishTx(server, {
|
|
8190
|
+
outputs: [{ toAddress: XRP_ADDR, amount: 2000 }],
|
|
8191
|
+
feePerKb: 123e2,
|
|
8192
|
+
from: fromAddr,
|
|
8193
|
+
deferNonce: true
|
|
8194
|
+
}, TestData.copayers[0].privKey_1H_0);
|
|
8195
|
+
|
|
8196
|
+
const result = await util.promisify(server.prepareTx).call(server, {
|
|
8197
|
+
txProposalId: deferred.id
|
|
8198
|
+
});
|
|
8199
|
+
result.nonce.should.equal(6);
|
|
8200
|
+
});
|
|
8201
|
+
|
|
8202
|
+
it('should sign XRP txp after prepareTx assigns nonce', async function() {
|
|
8203
|
+
blockchainExplorer.getTransactionCount = sinon.stub().callsArgWith(1, null, '7');
|
|
8204
|
+
helpers.stubBroadcast('txid123');
|
|
8205
|
+
|
|
8206
|
+
const txp = await helpers.createAndPublishTx(server, {
|
|
8207
|
+
outputs: [{ toAddress: XRP_ADDR, amount: 8000 }],
|
|
8208
|
+
feePerKb: 123e2,
|
|
8209
|
+
from: fromAddr,
|
|
8210
|
+
deferNonce: true
|
|
8211
|
+
}, TestData.copayers[0].privKey_1H_0);
|
|
8212
|
+
|
|
8213
|
+
const withNonce = await util.promisify(server.prepareTx).call(server, {
|
|
8214
|
+
txProposalId: txp.id
|
|
8215
|
+
});
|
|
8216
|
+
withNonce.nonce.should.equal(7);
|
|
8217
|
+
|
|
8218
|
+
const fetched = await util.promisify(server.getTx).call(server, { txProposalId: txp.id });
|
|
8219
|
+
const signatures = helpers.clientSign(fetched, TestData.copayers[0].xPrivKey_44H_0H_0H);
|
|
8220
|
+
const signed = await util.promisify(server.signTx).call(server, {
|
|
8221
|
+
txProposalId: txp.id,
|
|
8222
|
+
signatures
|
|
8223
|
+
});
|
|
8224
|
+
signed.status.should.equal('accepted');
|
|
8225
|
+
});
|
|
8226
|
+
});
|
|
8227
|
+
|
|
8228
|
+
describe('#signTx nonce override', function() {
|
|
8229
|
+
const ETH_ADDR = '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A';
|
|
8230
|
+
let server, wallet, fromAddr;
|
|
8231
|
+
|
|
8232
|
+
beforeEach(async function() {
|
|
8233
|
+
({ server, wallet } = await helpers.createAndJoinWallet(1, 1, { coin: 'eth' }));
|
|
8234
|
+
const address = await util.promisify(server.createAddress).call(server, {});
|
|
8235
|
+
fromAddr = address.address;
|
|
8236
|
+
await helpers.stubUtxos(server, wallet, [1, 2], { coin: 'eth' });
|
|
8237
|
+
blockchainExplorer.getTransactionCount = sinon.stub().callsArgWith(1, null, '5');
|
|
8238
|
+
});
|
|
8239
|
+
|
|
8240
|
+
it('should accept nonce passthrough in signTx', async function() {
|
|
8241
|
+
const txp = await helpers.createAndPublishTx(server, {
|
|
8242
|
+
outputs: [{ toAddress: ETH_ADDR, amount: 8000 }],
|
|
8243
|
+
feePerKb: 123e2,
|
|
8244
|
+
from: fromAddr
|
|
8245
|
+
}, TestData.copayers[0].privKey_1H_0);
|
|
8246
|
+
txp.nonce.should.equal('5');
|
|
8247
|
+
|
|
8248
|
+
const signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
|
|
8249
|
+
const signed = await util.promisify(server.signTx).call(server, {
|
|
8250
|
+
txProposalId: txp.id,
|
|
8251
|
+
signatures,
|
|
8252
|
+
nonce: Number(txp.nonce)
|
|
8253
|
+
});
|
|
8254
|
+
signed.status.should.equal('accepted');
|
|
8255
|
+
signed.nonce.should.equal(5);
|
|
8256
|
+
});
|
|
8257
|
+
|
|
8258
|
+
it('should accept nonce override in signTx', async function() {
|
|
8259
|
+
const txp = await helpers.createAndPublishTx(server, {
|
|
8260
|
+
outputs: [{ toAddress: ETH_ADDR, amount: 8000 }],
|
|
8261
|
+
feePerKb: 123e2,
|
|
8262
|
+
from: fromAddr
|
|
8263
|
+
}, TestData.copayers[0].privKey_1H_0);
|
|
8264
|
+
txp.nonce.should.equal('5');
|
|
8265
|
+
|
|
8266
|
+
const overrideNonce = 99;
|
|
8267
|
+
const fetched = await util.promisify(server.getTx).call(server, { txProposalId: txp.id });
|
|
8268
|
+
fetched.nonce = overrideNonce;
|
|
8269
|
+
|
|
8270
|
+
const signatures = helpers.clientSign(fetched, TestData.copayers[0].xPrivKey_44H_0H_0H);
|
|
8271
|
+
const signed = await util.promisify(server.signTx).call(server, {
|
|
8272
|
+
txProposalId: txp.id,
|
|
8273
|
+
signatures,
|
|
8274
|
+
nonce: overrideNonce
|
|
8275
|
+
});
|
|
8276
|
+
signed.status.should.equal('accepted');
|
|
8277
|
+
signed.nonce.should.equal(overrideNonce);
|
|
8278
|
+
});
|
|
8279
|
+
|
|
8280
|
+
it('should reject nonce override that conflicts with pending txp', async function() {
|
|
8281
|
+
const txp1 = await helpers.createAndPublishTx(server, {
|
|
8282
|
+
outputs: [{ toAddress: ETH_ADDR, amount: 1000 }],
|
|
8283
|
+
feePerKb: 123e2,
|
|
8284
|
+
from: fromAddr
|
|
8285
|
+
}, TestData.copayers[0].privKey_1H_0);
|
|
8286
|
+
txp1.nonce.should.equal('5');
|
|
8287
|
+
|
|
8288
|
+
blockchainExplorer.getTransactionCount = sinon.stub().callsArgWith(1, null, '6');
|
|
8289
|
+
const txp2 = await helpers.createAndPublishTx(server, {
|
|
8290
|
+
outputs: [{ toAddress: ETH_ADDR, amount: 2000 }],
|
|
8291
|
+
feePerKb: 123e2,
|
|
8292
|
+
from: fromAddr
|
|
8293
|
+
}, TestData.copayers[0].privKey_1H_0);
|
|
8294
|
+
txp2.nonce.should.equal('6');
|
|
8295
|
+
|
|
8296
|
+
// sign txp2 with nonce 5 (conflicts with txp1)
|
|
8297
|
+
const fetched = await util.promisify(server.getTx).call(server, { txProposalId: txp2.id });
|
|
8298
|
+
fetched.nonce = 5;
|
|
8299
|
+
const signatures = helpers.clientSign(fetched, TestData.copayers[0].xPrivKey_44H_0H_0H);
|
|
8300
|
+
|
|
8301
|
+
try {
|
|
8302
|
+
await util.promisify(server.signTx).call(server, {
|
|
8303
|
+
txProposalId: txp2.id,
|
|
8304
|
+
signatures,
|
|
8305
|
+
nonce: 5
|
|
8306
|
+
});
|
|
8307
|
+
throw new Error('should have thrown');
|
|
8308
|
+
} catch (err) {
|
|
8309
|
+
err.code.should.equal('TX_NONCE_CONFLICT');
|
|
8310
|
+
}
|
|
8311
|
+
});
|
|
8312
|
+
|
|
8313
|
+
it('should not crash on nonce conflict check when nonce is null', async function() {
|
|
8314
|
+
const txp = await helpers.createAndPublishTx(server, {
|
|
8315
|
+
outputs: [{ toAddress: ETH_ADDR, amount: 8000 }],
|
|
8316
|
+
feePerKb: 123e2,
|
|
8317
|
+
from: fromAddr,
|
|
8318
|
+
deferNonce: true
|
|
8319
|
+
}, TestData.copayers[0].privKey_1H_0);
|
|
8320
|
+
should.not.exist(txp.nonce);
|
|
8321
|
+
|
|
8322
|
+
// null nonce on other txps should not throw in conflict check
|
|
8323
|
+
const fetched = await util.promisify(server.getTx).call(server, { txProposalId: txp.id });
|
|
8324
|
+
fetched.nonce = 99;
|
|
8325
|
+
const signatures = helpers.clientSign(fetched, TestData.copayers[0].xPrivKey_44H_0H_0H);
|
|
8326
|
+
const signed = await util.promisify(server.signTx).call(server, {
|
|
8327
|
+
txProposalId: txp.id,
|
|
8328
|
+
signatures,
|
|
8329
|
+
nonce: 99
|
|
8330
|
+
});
|
|
8331
|
+
signed.status.should.equal('accepted');
|
|
8332
|
+
signed.nonce.should.equal(99);
|
|
8333
|
+
});
|
|
8334
|
+
|
|
8335
|
+
it('should ignore nonce override for non-EVM chains', async function() {
|
|
8336
|
+
const { server: btcServer, wallet: btcWallet } = await helpers.createAndJoinWallet(1, 1);
|
|
8337
|
+
await helpers.stubUtxos(btcServer, btcWallet, [1, 2]);
|
|
8338
|
+
|
|
8339
|
+
const txp = await helpers.createAndPublishTx(btcServer, {
|
|
8340
|
+
outputs: [{ toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 8000 }],
|
|
8341
|
+
feePerKb: 100e2,
|
|
8342
|
+
}, TestData.copayers[0].privKey_1H_0);
|
|
8343
|
+
|
|
8344
|
+
const signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
|
|
8345
|
+
const signed = await util.promisify(btcServer.signTx).call(btcServer, {
|
|
8346
|
+
txProposalId: txp.id,
|
|
8347
|
+
signatures,
|
|
8348
|
+
nonce: 99
|
|
8349
|
+
});
|
|
8350
|
+
signed.status.should.equal('accepted');
|
|
8351
|
+
should.not.exist(signed.nonce);
|
|
8352
|
+
});
|
|
8353
|
+
});
|
|
8354
|
+
|
|
8137
8355
|
describe('#broadcastTx & #broadcastRawTx', function() {
|
|
8138
8356
|
let server: WalletService;
|
|
8139
8357
|
let wallet: Model.Wallet;
|