@bitpay-labs/bitcore-wallet-service 11.10.2 → 11.10.3

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 (72) hide show
  1. package/bws.example.config.js +4 -3
  2. package/package.json +5 -5
  3. package/src/config.ts +4 -3
  4. package/src/externalservices/moonpay.ts +10 -0
  5. package/src/externalservices/transak.ts +15 -0
  6. package/src/lib/blockchainexplorers/v8.ts +19 -0
  7. package/src/lib/chain/xrp/index.ts +13 -8
  8. package/src/lib/common/defaults.ts +3 -0
  9. package/src/lib/common/utils.ts +3 -3
  10. package/src/lib/emailservice.ts +2 -1
  11. package/src/lib/model/txproposal.ts +4 -0
  12. package/src/lib/pushnotificationsservice.ts +2 -1
  13. package/src/lib/routes/services.ts +22 -6
  14. package/src/lib/routes/tss.ts +1 -1
  15. package/src/lib/routes/wallets.ts +16 -1
  16. package/src/lib/server.ts +47 -2
  17. package/test/chain/xrp.test.ts +21 -0
  18. package/test/integration/externalservices/moonpay.test.ts +14 -2
  19. package/test/integration/server.test.ts +138 -0
  20. package/ts_build/package.json +1 -1
  21. package/ts_build/src/config.d.ts.map +1 -1
  22. package/ts_build/src/config.js.map +1 -1
  23. package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
  24. package/ts_build/src/externalservices/moonpay.js +6 -0
  25. package/ts_build/src/externalservices/moonpay.js.map +1 -1
  26. package/ts_build/src/externalservices/transak.d.ts.map +1 -1
  27. package/ts_build/src/externalservices/transak.js +15 -0
  28. package/ts_build/src/externalservices/transak.js.map +1 -1
  29. package/ts_build/src/lib/blockchainexplorers/v8.d.ts +3 -0
  30. package/ts_build/src/lib/blockchainexplorers/v8.d.ts.map +1 -1
  31. package/ts_build/src/lib/blockchainexplorers/v8.js +18 -0
  32. package/ts_build/src/lib/blockchainexplorers/v8.js.map +1 -1
  33. package/ts_build/src/lib/chain/xrp/index.d.ts.map +1 -1
  34. package/ts_build/src/lib/chain/xrp/index.js +12 -8
  35. package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
  36. package/ts_build/src/lib/common/defaults.d.ts +1 -0
  37. package/ts_build/src/lib/common/defaults.d.ts.map +1 -1
  38. package/ts_build/src/lib/common/defaults.js +1 -0
  39. package/ts_build/src/lib/common/defaults.js.map +1 -1
  40. package/ts_build/src/lib/common/index.d.ts +2 -1
  41. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  42. package/ts_build/src/lib/common/utils.d.ts +1 -1
  43. package/ts_build/src/lib/common/utils.d.ts.map +1 -1
  44. package/ts_build/src/lib/common/utils.js.map +1 -1
  45. package/ts_build/src/lib/emailservice.d.ts.map +1 -1
  46. package/ts_build/src/lib/emailservice.js +1 -1
  47. package/ts_build/src/lib/emailservice.js.map +1 -1
  48. package/ts_build/src/lib/model/txproposal.d.ts +2 -0
  49. package/ts_build/src/lib/model/txproposal.d.ts.map +1 -1
  50. package/ts_build/src/lib/model/txproposal.js +3 -0
  51. package/ts_build/src/lib/model/txproposal.js.map +1 -1
  52. package/ts_build/src/lib/pushnotificationsservice.d.ts.map +1 -1
  53. package/ts_build/src/lib/pushnotificationsservice.js +1 -1
  54. package/ts_build/src/lib/pushnotificationsservice.js.map +1 -1
  55. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  56. package/ts_build/src/lib/routes/services.js +22 -6
  57. package/ts_build/src/lib/routes/services.js.map +1 -1
  58. package/ts_build/src/lib/routes/tss.js +0 -1
  59. package/ts_build/src/lib/routes/tss.js.map +1 -1
  60. package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
  61. package/ts_build/src/lib/routes/wallets.js +15 -0
  62. package/ts_build/src/lib/routes/wallets.js.map +1 -1
  63. package/ts_build/src/lib/server.d.ts +3 -0
  64. package/ts_build/src/lib/server.d.ts.map +1 -1
  65. package/ts_build/src/lib/server.js +41 -2
  66. package/ts_build/src/lib/server.js.map +1 -1
  67. package/ts_build/test/chain/xrp.test.js +19 -0
  68. package/ts_build/test/chain/xrp.test.js.map +1 -1
  69. package/ts_build/test/integration/externalservices/moonpay.test.js +14 -2
  70. package/ts_build/test/integration/externalservices/moonpay.test.js.map +1 -1
  71. package/ts_build/test/integration/server.test.js +115 -0
  72. package/ts_build/test/integration/server.test.js.map +1 -1
@@ -325,7 +325,7 @@ module.exports = {
325
325
  // publicKey: 'simplex_web_public_key_here',
326
326
  // }
327
327
  // },
328
- // thorswap : {
328
+ // thorswap: {
329
329
  // sandbox: {
330
330
  // api: 'https://dev-api.thorswap.net',
331
331
  // apiKey: 'thorswap_sandbox_api_key_here',
@@ -339,7 +339,7 @@ module.exports = {
339
339
  // referer: 'thorswap_production_referer_here'
340
340
  // },
341
341
  // },
342
- // transak : {
342
+ // transak: {
343
343
  // sandbox: {
344
344
  // api: 'https://api-stg.transak.com',
345
345
  // widgetApi: 'https://api-gateway-stg.transak.com',
@@ -363,7 +363,8 @@ module.exports = {
363
363
  // widgetApi: 'https://api-gateway.transak.com',
364
364
  // apiKey: 'transak_production_web_api_key_here',
365
365
  // secretKey: 'transak_production_web_secret_key_here',
366
- // }
366
+ // },
367
+ // whitelist: []
367
368
  // },
368
369
  // wyre: {
369
370
  // sandbox: {
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.2",
5
+ "version": "11.10.3",
6
6
  "license": "MIT",
7
7
  "main": "ts_build/src/index.js",
8
8
  "types": "./ts_build/src/index.d.ts",
@@ -43,9 +43,9 @@
43
43
  "cp:templates": "cp -r ./templates ./ts_build/templates"
44
44
  },
45
45
  "dependencies": {
46
- "@bitpay-labs/bitcore-logging": "^11.8.1",
47
- "@bitpay-labs/bitcore-tss": "^11.10.0",
48
- "@bitpay-labs/crypto-wallet-core": "^11.10.0",
46
+ "@bitpay-labs/bitcore-logging": "^11.10.3",
47
+ "@bitpay-labs/bitcore-tss": "^11.10.3",
48
+ "@bitpay-labs/crypto-wallet-core": "^11.10.3",
49
49
  "@sendgrid/mail": "6.5.4",
50
50
  "abi-decoder": "2.4.0",
51
51
  "async": "0.9.2",
@@ -126,5 +126,5 @@
126
126
  "email": "kjoseph@bitpay.com"
127
127
  }
128
128
  ],
129
- "gitHead": "bcf06b7f572a535f778a6060e1843fccf33ee91b"
129
+ "gitHead": "70298eedd1e458b23139e275125a07c577219bac"
130
130
  }
package/src/config.ts CHANGED
@@ -400,7 +400,7 @@ const Config = (): any => {
400
400
  // publicKey: 'simplex_web_public_key_here',
401
401
  // }
402
402
  // },
403
- // thorswap : {
403
+ // thorswap: {
404
404
  // sandbox: {
405
405
  // api: 'https://dev-api.thorswap.net',
406
406
  // apiKey: 'thorswap_sandbox_api_key_here',
@@ -414,7 +414,7 @@ const Config = (): any => {
414
414
  // referer: 'thorswap_production_referer_here'
415
415
  // },
416
416
  // },
417
- // transak : {
417
+ // transak: {
418
418
  // sandbox: {
419
419
  // api: 'https://api-stg.transak.com',
420
420
  // widgetApi: 'https://global-stg.transak.com',
@@ -438,7 +438,8 @@ const Config = (): any => {
438
438
  // widgetApi: 'https://global.transak.com',
439
439
  // apiKey: 'transak_production_web_api_key_here',
440
440
  // secretKey: 'transak_production_web_secret_key_here',
441
- // }
441
+ // },
442
+ // whitelist: []
442
443
  // },
443
444
  // wyre: {
444
445
  // sandbox: {
@@ -225,6 +225,16 @@ export class MoonpayService {
225
225
  if (req.body.paymentMethod) qs.push('paymentMethod=' + encodeURIComponent(req.body.paymentMethod));
226
226
  if (req.body.areFeesIncluded) qs.push('areFeesIncluded=' + encodeURIComponent(req.body.areFeesIncluded));
227
227
 
228
+ const deviceIp = Utils.getIpFromReq(req);
229
+ if (!deviceIp) {
230
+ throw new ClientError('Could not determine device IP address');
231
+ }
232
+ const allowedIpAddress: string = Bitcore.crypto.Hash.sha256hmac(
233
+ Buffer.from(deviceIp),
234
+ Buffer.from(SECRET_KEY)
235
+ ).toString('base64');
236
+ qs.push('allowedIpAddress=' + encodeURIComponent(allowedIpAddress));
237
+
228
238
  const URL_SEARCH: string = `?${qs.join('&')}`;
229
239
 
230
240
  const URLSignatureHash: string = Bitcore.crypto.Hash.sha256hmac(
@@ -1,5 +1,6 @@
1
1
  import * as request from 'request';
2
2
  import config from '../config';
3
+ import { Utils } from '../lib/common/utils';
3
4
  import { ClientError } from '../lib/errors/clienterror';
4
5
  import { checkRequired } from '../lib/server';
5
6
 
@@ -50,6 +51,8 @@ export class TransakService {
50
51
  const headers = {
51
52
  'Content-Type': 'application/json',
52
53
  'api-secret': SECRET_KEY,
54
+ 'x-api-key': API_KEY,
55
+ 'x-user-ip': Utils.getIpFromReq(req),
53
56
  };
54
57
 
55
58
  const body = {
@@ -85,10 +88,13 @@ export class TransakService {
85
88
  return reject(err);
86
89
  }
87
90
  const API = keys.API;
91
+ const API_KEY = keys.API_KEY;
88
92
 
89
93
  const headers = {
90
94
  'Accept': 'application/json',
91
95
  'Content-Type': 'application/json',
96
+ 'x-api-key': API_KEY,
97
+ 'x-user-ip': Utils.getIpFromReq(req),
92
98
  };
93
99
 
94
100
  const URL: string = API + '/api/v2/currencies/crypto-currencies';
@@ -124,6 +130,8 @@ export class TransakService {
124
130
  const headers = {
125
131
  'Accept': 'application/json',
126
132
  'Content-Type': 'application/json',
133
+ 'x-api-key': API_KEY,
134
+ 'x-user-ip': Utils.getIpFromReq(req),
127
135
  };
128
136
 
129
137
  const URL: string = API + `/api/v2/currencies/fiat-currencies?apiKey=${API_KEY}`;
@@ -162,6 +170,8 @@ export class TransakService {
162
170
 
163
171
  const headers = {
164
172
  Accept: 'application/json',
173
+ 'x-api-key': API_KEY,
174
+ 'x-user-ip': Utils.getIpFromReq(req),
165
175
  };
166
176
 
167
177
  const qs: string[] = [];
@@ -227,6 +237,8 @@ export class TransakService {
227
237
  Accept: 'application/json',
228
238
  'Content-Type': 'application/json',
229
239
  'access-token': req.body.accessToken,
240
+ 'x-api-key': API_KEY,
241
+ 'x-user-ip': Utils.getIpFromReq(req),
230
242
  };
231
243
 
232
244
  const body = {
@@ -266,6 +278,7 @@ export class TransakService {
266
278
  return reject(err);
267
279
  }
268
280
  const API = keys.API;
281
+ const API_KEY = keys.API_KEY;
269
282
 
270
283
  if (!checkRequired(req.body, ['orderId', 'accessToken'])) {
271
284
  return reject(new ClientError("Transak's request missing arguments"));
@@ -274,6 +287,8 @@ export class TransakService {
274
287
  const headers = {
275
288
  Accept: 'application/json',
276
289
  'access-token': req.body.accessToken,
290
+ 'x-api-key': API_KEY,
291
+ 'x-user-ip': Utils.getIpFromReq(req),
277
292
  };
278
293
 
279
294
  const URL: string = API + `/partners/api/v2/order/${req.body.orderId}`;
@@ -43,6 +43,13 @@ function v8network(bwsNetwork, chain = 'btc') {
43
43
  return bwsNetwork;
44
44
  }
45
45
 
46
+ function scrubResponseError(err) {
47
+ if (err.response) {
48
+ return new Error(`HTTP ${err.response.statusCode} - ${err.response.statusMessage}: ${err.response.body}\n${err.response.request.method} ${err.response.request.href}`);
49
+ }
50
+ return new Error(err);
51
+ }
52
+
46
53
  export type WalletWithOpts = IWallet & { tokenAddress?: string; multisigContractAddress?: string };
47
54
 
48
55
  export class V8 {
@@ -765,6 +772,18 @@ export class V8 {
765
772
  return callbacks.onIncomingPayments(notification);
766
773
  });
767
774
  }
775
+
776
+ async getFlags(opts: { address: string }) {
777
+ try {
778
+ const url = `${this.baseUrl}/address/${opts.address}/flags`;
779
+ const ret = await this.request.get(url);
780
+ return JSON.parse(ret).flags;
781
+ } catch (err) {
782
+ const e = scrubResponseError(err);
783
+ logger.error(`Error getting flags for address ${opts.address}: %o`, e);
784
+ throw e;
785
+ }
786
+ }
768
787
  }
769
788
 
770
789
  const getPerformanceKey = (name: string) => {
@@ -90,14 +90,19 @@ export class XrpChain implements IChain {
90
90
  getWalletSendMaxInfo(server, wallet, opts, cb) {
91
91
  server.getBalance({}, (err, balance) => {
92
92
  if (err) return cb(err);
93
- const { availableAmount } = balance;
94
- const fee = opts.feePerKb;
95
- return cb(null, {
96
- utxosBelowFee: 0,
97
- amountBelowFee: 0,
98
- amount: availableAmount - fee,
99
- feePerKb: opts.feePerKb,
100
- fee
93
+
94
+ server._getFeePerKb(wallet, opts, (err, feePerKb) => {
95
+ if (err) return cb(err);
96
+
97
+ const { availableAmount } = balance;
98
+ const fee = feePerKb;
99
+ return cb(null, {
100
+ utxosBelowFee: 0,
101
+ amountBelowFee: 0,
102
+ amount: availableAmount - fee,
103
+ feePerKb,
104
+ fee
105
+ });
101
106
  });
102
107
  });
103
108
  }
@@ -19,6 +19,9 @@ export const Defaults = {
19
19
  // TODO: should allow different gap sizes for external/internal chains
20
20
  SCAN_ADDRESS_GAP: 30,
21
21
 
22
+ // Lookback window (in seconds) for scanning broadcasted txps during nonce assignment
23
+ BROADCASTED_NONCE_SCAN_WINDOW: 24 * 3600,
24
+
22
25
  FEE_LEVELS: {
23
26
  btc: [
24
27
  {
@@ -81,11 +81,11 @@ export const Utils = {
81
81
 
82
82
  /**
83
83
  *
84
- * @desc rounds a JAvascript number
85
- * @param number
84
+ * @desc rounds a Javascript number
85
+ * @param {number} number
86
86
  * @return {number}
87
87
  */
88
- strip(number) {
88
+ strip(number: number): number {
89
89
  return parseFloat(number.toPrecision(12));
90
90
  },
91
91
 
@@ -561,7 +561,8 @@ export class EmailService {
561
561
  _checkShouldSendEmail(notification: Notification, cb) {
562
562
  if (notification.type != 'NewTxProposal') return cb(null, true);
563
563
  this.storage.fetchWallet(notification.walletId, (err, wallet) => {
564
- return cb(err, wallet.m > 1);
564
+ // TSS wallets are stored as m=1 but still need other signers
565
+ return cb(err, wallet && (wallet.m > 1 || !!wallet.tssKeyId));
565
566
  });
566
567
  }
567
568
 
@@ -81,6 +81,7 @@ export interface ITxProposal<NumberType = number> {
81
81
  multisigTxId?: string;
82
82
  destinationTag?: string;
83
83
  invoiceID?: string;
84
+ flags?: string; // Comma-delimited list of XRP account flags to set (e.g. "tfRequireDestTag,tfDisallowXRP")
84
85
  lockUntilBlockHeight?: NumberType;
85
86
  instantAcceptanceEscrow?: NumberType;
86
87
  isTokenSwap?: boolean;
@@ -175,6 +176,7 @@ export class TxProposal<NumberType = number> implements ITxProposal<NumberType>
175
176
  multisigTxId?: string;
176
177
  destinationTag?: string;
177
178
  invoiceID?: string;
179
+ flags?: string; // Comma-delimited list of XRP account flags to set (e.g. "tfRequireDestTag,tfDisallowXRP")
178
180
  lockUntilBlockHeight?: NumberType;
179
181
  instantAcceptanceEscrow?: NumberType;
180
182
  isTokenSwap?: boolean;
@@ -305,6 +307,7 @@ export class TxProposal<NumberType = number> implements ITxProposal<NumberType>
305
307
  x.destinationTag = opts.destinationTag;
306
308
  x.invoiceID = opts.invoiceID;
307
309
  x.multiTx = opts.multiTx; // proposal contains multiple transactions
310
+ x.flags = opts.flags; // XRP account flags to set
308
311
 
309
312
  // SOL
310
313
  x.space = opts.space; // space to allocate for account creation
@@ -400,6 +403,7 @@ export class TxProposal<NumberType = number> implements ITxProposal<NumberType>
400
403
  x.destinationTag = obj.destinationTag;
401
404
  x.invoiceID = obj.invoiceID;
402
405
  x.multiTx = obj.multiTx;
406
+ x.flags = obj.flags; // XRP account flags to set
403
407
 
404
408
  // SOL
405
409
  x.space = obj.space; // space to allocate for account creation
@@ -358,7 +358,8 @@ export class PushNotificationsService {
358
358
  _checkShouldSendNotif(notification, cb) {
359
359
  if (notification.type != 'NewTxProposal') return cb(null, true);
360
360
  this.storage.fetchWallet(notification.walletId, (err, wallet) => {
361
- return cb(err, wallet && wallet.m > 1);
361
+ // TSS wallets are stored as m=1 but still need other signers
362
+ return cb(err, wallet && (wallet.m > 1 || !!wallet.tssKeyId));
362
363
  });
363
364
  }
364
365
 
@@ -1,5 +1,7 @@
1
+ import cors from 'cors';
1
2
  import express from 'express';
2
3
  import rp from 'request-promise-native';
4
+ import config from '../../config';
3
5
  import { Common } from '../common';
4
6
  import type * as Types from '../../types/expressapp';
5
7
  import type { WalletService } from '../server';
@@ -65,6 +67,20 @@ function respondWithAuthServer(req, res, context: RouteContext, handler: RouteHa
65
67
  export function registerServiceRoutes(router: express.Router, context: RouteContext) {
66
68
  const { getServerWithAuth, setPublicCache, returnError } = context;
67
69
 
70
+ const transakCorsOptions = {
71
+ origin: (origin, cb) => {
72
+ if (!origin) {
73
+ return cb(null, true);
74
+ }
75
+ const transakWhiteList = config.transak?.whitelist ?? [];
76
+ if (transakWhiteList.indexOf(origin) !== -1) {
77
+ cb(null, true);
78
+ } else {
79
+ cb(new Error('Not allowed by CORS'));
80
+ }
81
+ }
82
+ };
83
+
68
84
  router.get('/latest-version', async (req, res) => {
69
85
  setPublicCache(res, 10 * ONE_MINUTE);
70
86
  try {
@@ -420,37 +436,37 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
420
436
  });
421
437
  });
422
438
 
423
- router.post('/v1/service/transak/getAccessToken', (req, res) => {
439
+ router.post('/v1/service/transak/getAccessToken', cors(transakCorsOptions), (req, res) => {
424
440
  respondWithAuthServer(req, res, context, server => {
425
441
  return server.externalServices.transak.transakGetAccessToken(req);
426
442
  });
427
443
  });
428
444
 
429
- router.post('/v1/service/transak/cryptoCurrencies', (req, res) => {
445
+ router.post('/v1/service/transak/cryptoCurrencies', cors(transakCorsOptions), (req, res) => {
430
446
  respondWithPublicServer(req, res, context, server => {
431
447
  return server.externalServices.transak.transakGetCryptoCurrencies(req);
432
448
  });
433
449
  });
434
450
 
435
- router.post('/v1/service/transak/fiatCurrencies', (req, res) => {
451
+ router.post('/v1/service/transak/fiatCurrencies', cors(transakCorsOptions), (req, res) => {
436
452
  respondWithPublicServer(req, res, context, server => {
437
453
  return server.externalServices.transak.transakGetFiatCurrencies(req);
438
454
  });
439
455
  });
440
456
 
441
- router.post('/v1/service/transak/quote', (req, res) => {
457
+ router.post('/v1/service/transak/quote', cors(transakCorsOptions), (req, res) => {
442
458
  respondWithAuthServer(req, res, context, server => {
443
459
  return server.externalServices.transak.transakGetQuote(req);
444
460
  });
445
461
  });
446
462
 
447
- router.post('/v1/service/transak/signedPaymentUrl', (req, res) => {
463
+ router.post('/v1/service/transak/signedPaymentUrl', cors(transakCorsOptions), (req, res) => {
448
464
  respondWithAuthServer(req, res, context, server => {
449
465
  return server.externalServices.transak.transakGetSignedPaymentUrl(req);
450
466
  });
451
467
  });
452
468
 
453
- router.post('/v1/service/transak/orderDetails', (req, res) => {
469
+ router.post('/v1/service/transak/orderDetails', cors(transakCorsOptions), (req, res) => {
454
470
  respondWithPublicServer(req, res, context, server => {
455
471
  return server.externalServices.transak.transakGetOrderDetails(req);
456
472
  });
@@ -120,4 +120,4 @@ export class TssRouter {
120
120
 
121
121
  this.router = router;
122
122
  }
123
- };
123
+ }
@@ -291,4 +291,19 @@ export function registerWalletRoutes(router: express.Router, context: RouteConte
291
291
  });
292
292
  });
293
293
  });
294
- }
294
+
295
+ router.get('/v1/flags', (req, res) => {
296
+ getServerWithAuth(req, res, async server => {
297
+ try {
298
+ const opts = { account: 0 };
299
+ if (req.query.account) {
300
+ opts.account = parseInt(req.query.account);
301
+ }
302
+ const flags = await server.getFlags(opts);
303
+ res.json(flags);
304
+ } catch (err) {
305
+ returnError(err, res, req);
306
+ }
307
+ });
308
+ });
309
+ }
package/src/lib/server.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import util from 'util';
1
2
  import {
2
3
  BitcoreLib as Bitcore,
3
4
  BitcoreLibCash as BitcoreCash,
@@ -2924,7 +2925,8 @@ export class WalletService implements IWalletService {
2924
2925
  fromAta: opts.fromAta,
2925
2926
  decimals: opts.decimals,
2926
2927
  refreshOnPublish: opts.refreshOnPublish,
2927
- deferNonce: opts.deferNonce
2928
+ deferNonce: opts.deferNonce,
2929
+ flags: opts.flags
2928
2930
  };
2929
2931
  txp = TxProposal.create(txOpts);
2930
2932
  next();
@@ -3358,6 +3360,19 @@ export class WalletService implements IWalletService {
3358
3360
  return cb(Errors.TX_NONCE_CONFLICT);
3359
3361
  }
3360
3362
  }
3363
+ // A deferred txp matching a broadcasted txp's nonce means its JIT-assigned
3364
+ // value went stale. Non-deferred txps may reuse an in-flight nonce
3365
+ // intentionally (e.g. speed-up).
3366
+ if (txp.nonce != null && txp.deferNonce) {
3367
+ const broadcastedTxps = await util.promisify(this.storage.fetchBroadcastedTxs).call(this.storage, this.walletId, {
3368
+ minTs: Math.floor(Date.now() / 1000) - Defaults.BROADCASTED_NONCE_SCAN_WINDOW
3369
+ });
3370
+ for (const t of broadcastedTxps || []) {
3371
+ if (t.id !== txp.id && t.nonce != null && Number(t.nonce) === Number(txp.nonce)) {
3372
+ return cb(Errors.TX_NONCE_CONFLICT);
3373
+ }
3374
+ }
3375
+ }
3361
3376
  } catch (err) {
3362
3377
  return cb(err);
3363
3378
  }
@@ -3486,9 +3501,18 @@ export class WalletService implements IWalletService {
3486
3501
  .filter(t => t.id !== txp.id && t.nonce != null && t.status !== 'rejected')
3487
3502
  .map(t => Number(t.nonce));
3488
3503
 
3504
+ // 2b. Broadcasted-but-unmined txps still occupy their nonces but appear in
3505
+ // neither the confirmed count nor the pending set
3506
+ const broadcastedTxps = await util.promisify(this.storage.fetchBroadcastedTxs).call(this.storage, this.walletId, {
3507
+ minTs: Math.floor(Date.now() / 1000) - Defaults.BROADCASTED_NONCE_SCAN_WINDOW
3508
+ });
3509
+ const inFlightNonces = (broadcastedTxps || [])
3510
+ .filter(t => t.nonce != null && Number(t.nonce) >= Number(confirmedNonce))
3511
+ .map(t => Number(t.nonce));
3512
+
3489
3513
  // 3. Calculate gap-free nonce
3490
3514
  let suggestedNonce = Number(confirmedNonce);
3491
- const allNonces = pendingNonces.sort((a, b) => a - b);
3515
+ const allNonces = [...pendingNonces, ...inFlightNonces].sort((a, b) => a - b);
3492
3516
  for (const n of allNonces) {
3493
3517
  if (n === suggestedNonce) {
3494
3518
  suggestedNonce++;
@@ -5303,6 +5327,27 @@ export class WalletService implements IWalletService {
5303
5327
  });
5304
5328
  }
5305
5329
 
5330
+ async getFlags(opts: { account: number }) {
5331
+ try {
5332
+ const wallet = await util.promisify(this.getWallet).call(this, {});
5333
+ if (wallet.chain !== 'xrp') throw new Error('Flags are only supported for XRP wallets');
5334
+
5335
+ const addresses = await util.promisify(this.storage.fetchAddresses).call(this.storage, this.walletId);
5336
+ const addressObj = addresses.find(a => a.path === `m/0/${opts.account || 0}`);
5337
+ if (!addressObj) throw new Error('Could not find address for account ' + opts.account);
5338
+
5339
+ const bc = this._getBlockchainExplorer(wallet.chain, wallet.network);
5340
+ if (!bc) throw new Error('Could not get blockchain explorer instance');
5341
+
5342
+ const address = addressObj.address.split(':')[0]; // Remove testnet suffix
5343
+ const flags = await bc.getFlags({ address });
5344
+ return flags;
5345
+ } catch (err) {
5346
+ this.logw('Error getting flags: %o', err);
5347
+ throw err;
5348
+ }
5349
+ }
5350
+
5306
5351
  static upgradeNeeded(
5307
5352
  paths: Upgrade | Upgrade[],
5308
5353
  opts: UpgradeCheckOpts & { clientVersion: string; userAgent: string }
@@ -10,6 +10,27 @@ const should = chai.should();
10
10
  const xpub = 'tpubDD7tYYerLNNm65Ez7pRjxQ2NpRHDoyRWoLWudnQ8agXjr7qs9BsPsRXk8Z6spPPJodnaY158YqeCKT5oXuZvbuNLfm1R4kXGJ2vPd9pUxDT';
11
11
 
12
12
  describe('Chain XRP', function() {
13
+ describe('#getWalletSendMaxInfo', function() {
14
+ it('should resolve fee level before calculating sendMax amount', function(done) {
15
+ const server = {
16
+ getBalance: (_opts, cb) => cb(null, { availableAmount: 1000000 }),
17
+ _getFeePerKb: (_wallet, opts, cb) => {
18
+ opts.feeLevel.should.equal('normal');
19
+ return cb(null, 12);
20
+ }
21
+ };
22
+ const wallet = { chain: 'xrp' };
23
+
24
+ ChainService.get('xrp').getWalletSendMaxInfo(server as any, wallet as any, { feeLevel: 'normal' }, (err, info) => {
25
+ should.not.exist(err);
26
+ info.amount.should.equal(999988);
27
+ info.fee.should.equal(12);
28
+ info.feePerKb.should.equal(12);
29
+ done();
30
+ });
31
+ });
32
+ });
33
+
13
34
  describe('#getBitcoreTx', function() {
14
35
  it('should create a valid bitcore TX', function() {
15
36
  const txp = TxProposal.fromObj(aTXP()) as TxProposal;
@@ -260,7 +260,9 @@ describe('Moonpay integration', () => {
260
260
  describe('#moonpayGetSignedPaymentUrl', () => {
261
261
  beforeEach(() => {
262
262
  req = {
263
- headers: {},
263
+ headers: {
264
+ 'x-forwarded-for': '1.2.3.4'
265
+ },
264
266
  body: {
265
267
  env: 'production',
266
268
  currencyCode: 'btc',
@@ -277,7 +279,17 @@ describe('Moonpay integration', () => {
277
279
  it('should get the paymentUrl properly if req is OK', () => {
278
280
  const data = server.externalServices.moonpay.moonpayGetSignedPaymentUrl(req);
279
281
  should.exist(data.urlWithSignature);
280
- data.urlWithSignature.should.equal('widgetApi2?apiKey=apiKey2&currencyCode=btc&walletAddress=bitcoin%3A123123&baseCurrencyCode=usd&baseCurrencyAmount=500&externalTransactionId=123123&redirectURL=bitpay%3A%2F%2Fmoonpay&signature=%2FDnbsboySgE%2FeAvMrwzROCLuuctkhgw5C2t2OofjOzo%3D');
282
+ data.urlWithSignature.should.equal('widgetApi2?apiKey=apiKey2&currencyCode=btc&walletAddress=bitcoin%3A123123&baseCurrencyCode=usd&baseCurrencyAmount=500&externalTransactionId=123123&redirectURL=bitpay%3A%2F%2Fmoonpay&allowedIpAddress=CN35SFB5PKS4vkiZ4CglTxRgTAaUHBLGZcenAw6gHEY%3D&signature=3XxjRX3EMj2RNaoAwgOwFBOiVTXsgAS7C50uJf9SsvM%3D');
283
+ });
284
+
285
+ it('should return error if request does not have IP', () => {
286
+ delete req.headers['x-forwarded-for'];
287
+ try {
288
+ server.externalServices.moonpay.moonpayGetSignedPaymentUrl(req);
289
+ should.fail('should have thrown');
290
+ } catch (err) {
291
+ err.message.should.equal('Could not determine device IP address');
292
+ }
281
293
  });
282
294
 
283
295
  it('should return error if there is some missing arguments', () => {