@bitpay-labs/bitcore-wallet-service 11.10.4 → 11.10.6

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 (31) hide show
  1. package/package.json +2 -2
  2. package/src/externalservices/moonpay.ts +22 -7
  3. package/src/externalservices/ramp.ts +31 -11
  4. package/src/lib/expressapp.ts +1 -0
  5. package/src/lib/routes/moralis.ts +25 -9
  6. package/src/lib/routes/services.ts +8 -8
  7. package/test/expressapp.test.ts +83 -0
  8. package/test/integration/externalservices/moonpay.test.ts +30 -0
  9. package/test/integration/externalservices/ramp.test.ts +151 -20
  10. package/ts_build/package.json +4 -4
  11. package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
  12. package/ts_build/src/externalservices/moonpay.js +8 -4
  13. package/ts_build/src/externalservices/moonpay.js.map +1 -1
  14. package/ts_build/src/externalservices/ramp.d.ts.map +1 -1
  15. package/ts_build/src/externalservices/ramp.js +19 -18
  16. package/ts_build/src/externalservices/ramp.js.map +1 -1
  17. package/ts_build/src/lib/expressapp.d.ts.map +1 -1
  18. package/ts_build/src/lib/expressapp.js +1 -0
  19. package/ts_build/src/lib/expressapp.js.map +1 -1
  20. package/ts_build/src/lib/routes/moralis.d.ts +1 -0
  21. package/ts_build/src/lib/routes/moralis.d.ts.map +1 -1
  22. package/ts_build/src/lib/routes/moralis.js +22 -9
  23. package/ts_build/src/lib/routes/moralis.js.map +1 -1
  24. package/ts_build/src/lib/routes/services.js +8 -8
  25. package/ts_build/src/lib/routes/services.js.map +1 -1
  26. package/ts_build/test/expressapp.test.js +79 -0
  27. package/ts_build/test/expressapp.test.js.map +1 -1
  28. package/ts_build/test/integration/externalservices/moonpay.test.js +26 -0
  29. package/ts_build/test/integration/externalservices/moonpay.test.js.map +1 -1
  30. package/ts_build/test/integration/externalservices/ramp.test.js +113 -20
  31. package/ts_build/test/integration/externalservices/ramp.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.4",
5
+ "version": "11.10.6",
6
6
  "license": "MIT",
7
7
  "main": "ts_build/src/index.js",
8
8
  "types": "./ts_build/src/index.d.ts",
@@ -107,5 +107,5 @@
107
107
  "email": "kjoseph@bitpay.com"
108
108
  }
109
109
  ],
110
- "gitHead": "3cdd407df1a56d452b243083c884735757c10a2a"
110
+ "gitHead": "68cba09ab40e3e91528cae682333303c08b3f565"
111
111
  }
@@ -190,6 +190,8 @@ export class MoonpayService {
190
190
  }
191
191
 
192
192
  moonpayGetSignedPaymentUrl(req): { urlWithSignature: string } {
193
+ // moonpayGetKeys deletes env/context from the body, so read context first
194
+ const isWebContext = req.body.context === 'web';
193
195
  const keys = this.moonpayGetKeys(req);
194
196
  const SECRET_KEY = keys.SECRET_KEY;
195
197
  const API_KEY = keys.API_KEY;
@@ -225,15 +227,28 @@ export class MoonpayService {
225
227
  if (req.body.paymentMethod) qs.push('paymentMethod=' + encodeURIComponent(req.body.paymentMethod));
226
228
  if (req.body.areFeesIncluded) qs.push('areFeesIncluded=' + encodeURIComponent(req.body.areFeesIncluded));
227
229
 
228
- const deviceIp = Utils.getIpFromReq(req);
229
- if (!deviceIp) {
230
+ // Web requests are proxied through the bitpay backend, so the IP on this
231
+ // request belongs to that server, not the customer. The proxy captures the
232
+ // customer's public IP and forwards it as deviceIp. Web credentials are only
233
+ // held by the proxy, so the forwarded value is trusted for that context only.
234
+ // If the proxy does not forward an IP, omit allowedIpAddress rather than
235
+ // signing an IP the customer will never match.
236
+ let deviceIp = isWebContext ? req.body.deviceIp : Utils.getIpFromReq(req);
237
+ if (!deviceIp && !isWebContext) {
230
238
  throw new ClientError('Could not determine device IP address');
231
239
  }
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));
240
+ if (deviceIp) {
241
+ // Canonicalize before hashing: HMAC is byte-exact and MoonPay hashes the
242
+ // plain IPv4 it observes, while dual-stack sockets report IPv4 clients as
243
+ // IPv4-mapped IPv6 (::ffff:1.2.3.4) - strip the prefix so both sides
244
+ // hash the same string
245
+ deviceIp = String(deviceIp).trim().replace(/^::ffff:/i, '');
246
+ const allowedIpAddress: string = Bitcore.crypto.Hash.sha256hmac(
247
+ Buffer.from(deviceIp),
248
+ Buffer.from(SECRET_KEY)
249
+ ).toString('base64');
250
+ qs.push('allowedIpAddress=' + encodeURIComponent(allowedIpAddress));
251
+ }
237
252
 
238
253
  const URL_SEARCH: string = `?${qs.join('&')}`;
239
254
 
@@ -112,14 +112,13 @@ export class RampService {
112
112
  const webRequiredParams = [
113
113
  'selectedCountryCode',
114
114
  ];
115
- const appRequiredParams = [
115
+ let appRequiredParams = [
116
116
  'enabledFlows',
117
117
  'defaultFlow',
118
118
  'selectedCountryCode',
119
- 'defaultAsset',
120
119
  ];
121
- const extraRequiredParams = req.body.flow && req.body.flow === 'sell' ? ['offrampAsset'] : ['finalUrl', 'userAddress', 'swapAmount', 'swapAsset'];
122
- appRequiredParams.concat(extraRequiredParams);
120
+ const extraRequiredParams = req.body.flow && req.body.flow === 'sell' ? [] : ['finalUrl', 'userAddress'];
121
+ appRequiredParams = appRequiredParams.concat(extraRequiredParams);
123
122
 
124
123
  const requiredParams = req.body.context === 'web' ? webRequiredParams : appRequiredParams;
125
124
  const keys = this.rampGetKeys(req);
@@ -138,21 +137,42 @@ export class RampService {
138
137
  qs.push('selectedCountryCode=' + encodeURIComponent(req.body.selectedCountryCode));
139
138
  if (req.body.finalUrl) qs.push('finalUrl=' + encodeURIComponent(req.body.finalUrl));
140
139
  if (req.body.userAddress) qs.push('userAddress=' + encodeURIComponent(req.body.userAddress));
141
- if (req.body.swapAsset) qs.push('swapAsset=' + encodeURIComponent(req.body.swapAsset));
142
- if (req.body.offrampAsset) qs.push('offrampAsset=' + encodeURIComponent(req.body.offrampAsset));
143
140
  if (req.body.enabledFlows) qs.push('enabledFlows=' + encodeURIComponent(req.body.enabledFlows));
144
141
  if (req.body.defaultFlow) qs.push('defaultFlow=' + encodeURIComponent(req.body.defaultFlow));
145
142
  if (req.body.hostLogoUrl) qs.push('hostLogoUrl=' + encodeURIComponent(req.body.hostLogoUrl));
146
143
  if (req.body.hostAppName) qs.push('hostAppName=' + encodeURIComponent(req.body.hostAppName));
147
- if (req.body.swapAmount) qs.push('swapAmount=' + encodeURIComponent(req.body.swapAmount));
148
- if (req.body.fiatValue) qs.push('fiatValue=' + encodeURIComponent(req.body.fiatValue));
149
- if (req.body.fiatCurrency) qs.push('fiatCurrency=' + encodeURIComponent(req.body.fiatCurrency));
150
- if (req.body.defaultAsset) qs.push('defaultAsset=' + encodeURIComponent(req.body.defaultAsset));
151
144
  if (req.body.userEmailAddress) qs.push('userEmailAddress=' + encodeURIComponent(req.body.userEmailAddress));
152
145
  if (req.body.useSendCryptoCallback) qs.push('useSendCryptoCallback=' + encodeURIComponent(req.body.useSendCryptoCallback));
153
146
  if (req.body.paymentMethodType) qs.push('paymentMethodType=' + encodeURIComponent(req.body.paymentMethodType));
154
147
  if (req.body.hideExitButton) qs.push('hideExitButton=' + encodeURIComponent(req.body.hideExitButton));
155
- if (req.body.useSendCryptoCallbackVersion) qs.push('useSendCryptoCallbackVersion=' + encodeURIComponent(req.body.useSendCryptoCallbackVersion));
148
+
149
+ // Ramp deprecated the legacy per-flow search params in favor of a unified format.
150
+ // Ref: https://docs.rampnetwork.com/search-params-migration
151
+ // Older app versions still send the legacy fields, so we translate them into the new
152
+ // unified params here to keep those requests working going forward.
153
+ // `flow === 'sell'` => OFFRAMP; otherwise => ONRAMP.
154
+ // If a newer client already sends the new params, those take precedence.
155
+ const isOfframp = req.body.flow === 'sell';
156
+
157
+ // enabledCryptoAssets <- swapAsset (onramp) / offrampAsset (offramp)
158
+ const enabledCryptoAssets = req.body.enabledCryptoAssets ?? (isOfframp ? req.body.offrampAsset : req.body.swapAsset);
159
+ if (enabledCryptoAssets) qs.push('enabledCryptoAssets=' + encodeURIComponent(enabledCryptoAssets));
160
+
161
+ // inAsset (incoming, paid by the user) <- fiatCurrency (onramp) / defaultAsset (offramp)
162
+ const inAsset = req.body.inAsset ?? (isOfframp ? req.body.defaultAsset : req.body.fiatCurrency);
163
+ if (inAsset) qs.push('inAsset=' + encodeURIComponent(inAsset));
164
+
165
+ // outAsset (outgoing, received by the user) <- defaultAsset (onramp) / fiatCurrency (offramp)
166
+ const outAsset = req.body.outAsset ?? (isOfframp ? req.body.fiatCurrency : req.body.defaultAsset);
167
+ if (outAsset) qs.push('outAsset=' + encodeURIComponent(outAsset));
168
+
169
+ // inAssetValue (units, no decimals) <- fiatValue (onramp) / swapAmount (offramp)
170
+ const inAssetValue = req.body.inAssetValue ?? (isOfframp ? req.body.swapAmount : req.body.fiatValue);
171
+ if (inAssetValue) qs.push('inAssetValue=' + encodeURIComponent(inAssetValue));
172
+
173
+ // outAssetValue (units, no decimals) <- swapAmount (onramp) / fiatValue (offramp)
174
+ const outAssetValue = req.body.outAssetValue ?? (isOfframp ? req.body.fiatValue : req.body.swapAmount);
175
+ if (outAssetValue) qs.push('outAssetValue=' + encodeURIComponent(outAssetValue));
156
176
 
157
177
  const queryString = qs.join('&');
158
178
 
@@ -126,6 +126,7 @@ export class ExpressApp {
126
126
 
127
127
  registerMoralisRoutes(router, {
128
128
  getServer,
129
+ getServerWithAuth,
129
130
  returnError
130
131
  });
131
132
 
@@ -6,6 +6,7 @@ import type { WalletService } from '../server';
6
6
 
7
7
  interface RouteContext {
8
8
  getServer: Types.GetServerFn;
9
+ getServerWithAuth: Types.GetServerWithAuthFn;
9
10
  returnError: Types.ReturnErrorFn;
10
11
  }
11
12
 
@@ -33,6 +34,21 @@ function respondWithPublicServer(req, res, context: RouteContext, handler: Route
33
34
  });
34
35
  }
35
36
 
37
+ function walletAuthOrPublic(context: RouteContext) {
38
+ return function(req, res, next) {
39
+ if (!req.header('x-identity')) {
40
+ // Not from wallet
41
+ return next();
42
+ }
43
+ return context.getServerWithAuth(req, res, _server => {
44
+ if (!req.header('origin')) {
45
+ req.headers['origin'] = config.moralis?.whitelist?.[0];
46
+ }
47
+ return next();
48
+ });
49
+ };
50
+ }
51
+
36
52
  export function registerMoralisRoutes(router: express.Router, context: RouteContext) {
37
53
  const moralisCorsOptions = {
38
54
  origin: (origin, cb) => {
@@ -45,55 +61,55 @@ export function registerMoralisRoutes(router: express.Router, context: RouteCont
45
61
  }
46
62
  };
47
63
 
48
- router.post('/v1/moralis/getWalletTokenBalances', cors(moralisCorsOptions), (req, res) => {
64
+ router.post('/v1/moralis/getWalletTokenBalances', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => {
49
65
  respondWithPublicServer(req, res, context, server => {
50
66
  return server.moralisGetWalletTokenBalances(req);
51
67
  });
52
68
  });
53
69
 
54
- router.post('/v1/moralis/moralisGetTokenAllowance', cors(moralisCorsOptions), (req, res) => {
70
+ router.post('/v1/moralis/moralisGetTokenAllowance', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => {
55
71
  respondWithPublicServer(req, res, context, server => {
56
72
  return server.moralisGetTokenAllowance(req);
57
73
  });
58
74
  });
59
75
 
60
- router.post('/v1/moralis/moralisGetNativeBalance', cors(moralisCorsOptions), (req, res) => {
76
+ router.post('/v1/moralis/moralisGetNativeBalance', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => {
61
77
  respondWithPublicServer(req, res, context, server => {
62
78
  return server.moralisGetNativeBalance(req);
63
79
  });
64
80
  });
65
81
 
66
- router.post('/v1/moralis/GetTokenPrice', cors(moralisCorsOptions), (req, res) => {
82
+ router.post('/v1/moralis/GetTokenPrice', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => {
67
83
  respondWithPublicServer(req, res, context, server => {
68
84
  return server.moralisGetTokenPrice(req);
69
85
  });
70
86
  });
71
87
 
72
- router.post('/v1/moralis/getMultipleERC20TokenPrices', cors(moralisCorsOptions), (req, res) => {
88
+ router.post('/v1/moralis/getMultipleERC20TokenPrices', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => {
73
89
  respondWithPublicServer(req, res, context, server => {
74
90
  return server.moralisGetMultipleERC20TokenPrices(req);
75
91
  });
76
92
  });
77
93
 
78
- router.post('/v1/moralis/getERC20TokenBalancesWithPricesByWallet', cors(moralisCorsOptions), (req, res) => {
94
+ router.post('/v1/moralis/getERC20TokenBalancesWithPricesByWallet', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => {
79
95
  respondWithPublicServer(req, res, context, server => {
80
96
  return server.moralisGetERC20TokenBalancesWithPricesByWallet(req);
81
97
  });
82
98
  });
83
99
 
84
- router.post('/v1/moralis/getSolWalletPortfolio', cors(moralisCorsOptions), (req, res) => {
100
+ router.post('/v1/moralis/getSolWalletPortfolio', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => {
85
101
  respondWithPublicServer(req, res, context, server => {
86
102
  return server.moralisGetSolWalletPortfolio(req);
87
103
  });
88
104
  });
89
105
 
90
- router.post('/v1/moralis/getTransactionVerbose', cors(moralisCorsOptions), (req, res) => {
106
+ router.post('/v1/moralis/getTransactionVerbose', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => {
91
107
  respondWithPublicServer(req, res, context, server => {
92
108
  return server.moralisGetTransactionVerbose(req);
93
109
  });
94
110
  });
95
111
 
96
- router.post('/v1/moralis/getMultipleSolTokenPrices', cors(moralisCorsOptions), (req, res) => {
112
+ router.post('/v1/moralis/getMultipleSolTokenPrices', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => {
97
113
  respondWithPublicServer(req, res, context, server => {
98
114
  return server.moralisGetMultipleSolTokenPrices(req);
99
115
  });
@@ -232,7 +232,7 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
232
232
  });
233
233
 
234
234
  router.post('/v1/service/banxa/getOrder', (req, res) => {
235
- respondWithPublicServer(req, res, context, server => {
235
+ respondWithAuthServer(req, res, context, server => {
236
236
  return server.externalServices.banxa.banxaGetOrder(req);
237
237
  });
238
238
  });
@@ -274,7 +274,7 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
274
274
  });
275
275
 
276
276
  router.post('/v1/service/moonpay/transactionDetails', (req, res) => {
277
- respondWithPublicServer(req, res, context, server => {
277
+ respondWithAuthServer(req, res, context, server => {
278
278
  return server.externalServices.moonpay.moonpayGetTransactionDetails(req);
279
279
  });
280
280
  });
@@ -292,7 +292,7 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
292
292
  });
293
293
 
294
294
  router.post('/v1/service/moonpay/accountDetails', (req, res) => {
295
- respondWithPublicServer(req, res, context, server => {
295
+ respondWithAuthServer(req, res, context, server => {
296
296
  return server.externalServices.moonpay.moonpayGetAccountDetails(req);
297
297
  });
298
298
  });
@@ -364,7 +364,7 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
364
364
  });
365
365
 
366
366
  router.post('/v1/service/sardine/ordersDetails', (req, res) => {
367
- respondWithPublicServer(req, res, context, server => {
367
+ respondWithAuthServer(req, res, context, server => {
368
368
  return server.externalServices.sardine.sardineGetOrdersDetails(req);
369
369
  });
370
370
  });
@@ -431,7 +431,7 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
431
431
  });
432
432
 
433
433
  router.post('/v1/service/thorswap/getSwapTx', (req, res) => {
434
- respondWithPublicServer(req, res, context, server => {
434
+ respondWithAuthServer(req, res, context, server => {
435
435
  return server.externalServices.thorswap.thorswapGetSwapTx(req);
436
436
  });
437
437
  });
@@ -467,7 +467,7 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
467
467
  });
468
468
 
469
469
  router.post('/v1/service/transak/orderDetails', cors(transakCorsOptions), (req, res) => {
470
- respondWithPublicServer(req, res, context, server => {
470
+ respondWithAuthServer(req, res, context, server => {
471
471
  return server.externalServices.transak.transakGetOrderDetails(req);
472
472
  });
473
473
  });
@@ -509,13 +509,13 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
509
509
  });
510
510
 
511
511
  router.post('/v1/service/changelly/getTransactions', (req, res) => {
512
- respondWithPublicServer(req, res, context, server => {
512
+ respondWithAuthServer(req, res, context, server => {
513
513
  return server.externalServices.changelly.changellyGetTransactions(req);
514
514
  });
515
515
  });
516
516
 
517
517
  router.post('/v1/service/changelly/getStatus', (req, res) => {
518
- respondWithPublicServer(req, res, context, server => {
518
+ respondWithAuthServer(req, res, context, server => {
519
519
  return server.externalServices.changelly.changellyGetStatus(req);
520
520
  });
521
521
  });
@@ -610,6 +610,89 @@ describe('ExpressApp', function() {
610
610
  });
611
611
  });
612
612
 
613
+ describe('Moralis routes (wallet-authenticated or public)', function() {
614
+ const whitelistedOrigin = 'https://whitelisted-dapp.example';
615
+ let originalMoralisConfig;
616
+ beforeEach(function() {
617
+ originalMoralisConfig = config.moralis;
618
+ config.moralis = { whitelist: [whitelistedOrigin] };
619
+ });
620
+ afterEach(function() {
621
+ config.moralis = originalMoralisConfig;
622
+ });
623
+
624
+ it('/v1/moralis/getMultipleERC20TokenPrices should work without auth headers (public/dApp)', function(done) {
625
+ const server = {
626
+ moralisGetMultipleERC20TokenPrices: sinon.stub().resolves([{ tokenAddress: '0xaaa', usdPrice: 1 }]),
627
+ };
628
+ sandbox.stub(WalletService, 'initialize').callsArg(1);
629
+ sandbox.stub(WalletService, 'getInstance').returns(server);
630
+ start(ExpressApp, function() {
631
+ const requestOptions = {
632
+ url: testHost + ':' + testPort + config.basePath + '/v1/moralis/getMultipleERC20TokenPrices',
633
+ method: 'post',
634
+ json: { chain: '0x1', tokens: [{ tokenAddress: '0xaaa' }] },
635
+ headers: {
636
+ origin: whitelistedOrigin
637
+ }
638
+ };
639
+ request(requestOptions, function(err, res, body) {
640
+ should.not.exist(err);
641
+ res.statusCode.should.equal(200);
642
+ body[0].tokenAddress.should.equal('0xaaa');
643
+ done();
644
+ });
645
+ });
646
+ });
647
+
648
+ it('/v1/moralis/getMultipleERC20TokenPrices should work with valid wallet auth headers', function(done) {
649
+ const server = {
650
+ moralisGetMultipleERC20TokenPrices: sinon.stub().resolves([{ tokenAddress: '0xaaa', usdPrice: 1 }]),
651
+ };
652
+ sandbox.stub(WalletService, 'initialize').callsArg(1);
653
+ sandbox.stub(WalletService, 'getInstance').returns(server);
654
+ sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, null, server);
655
+ start(ExpressApp, function() {
656
+ const requestOptions = {
657
+ url: testHost + ':' + testPort + config.basePath + '/v1/moralis/getMultipleERC20TokenPrices',
658
+ method: 'post',
659
+ json: { chain: '0x1', tokens: [{ tokenAddress: '0xaaa' }] },
660
+ headers: {
661
+ 'x-identity': 'identity',
662
+ 'x-signature': 'signature'
663
+ }
664
+ };
665
+ request(requestOptions, function(err, res, body) {
666
+ should.not.exist(err);
667
+ res.statusCode.should.equal(200);
668
+ body[0].tokenAddress.should.equal('0xaaa');
669
+ done();
670
+ });
671
+ });
672
+ });
673
+
674
+ it('/v1/moralis/getMultipleERC20TokenPrices should fail with invalid wallet auth headers', function(done) {
675
+ sandbox.stub(WalletService, 'initialize').callsArg(1);
676
+ sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, new ClientError('NOT_AUTHORIZED', 'Not authorized'));
677
+ start(ExpressApp, function() {
678
+ const requestOptions = {
679
+ url: testHost + ':' + testPort + config.basePath + '/v1/moralis/getMultipleERC20TokenPrices',
680
+ method: 'post',
681
+ json: { chain: '0x1', tokens: [{ tokenAddress: '0xaaa' }] },
682
+ headers: {
683
+ 'x-identity': 'identity',
684
+ 'x-signature': 'bad-signature'
685
+ }
686
+ };
687
+ request(requestOptions, function(err, res) {
688
+ should.not.exist(err);
689
+ res.statusCode.should.equal(401);
690
+ done();
691
+ });
692
+ });
693
+ });
694
+ });
695
+
613
696
  describe('Token allowance', function() {
614
697
  it('/v1/token/allowance', function(done) {
615
698
  const server = {
@@ -292,6 +292,36 @@ describe('Moonpay integration', () => {
292
292
  }
293
293
  });
294
294
 
295
+ it('should hash the forwarded deviceIp instead of the request IP for web context', () => {
296
+ req.body.context = 'web';
297
+ req.body.deviceIp = '203.0.113.42';
298
+ const data = server.externalServices.moonpay.moonpayGetSignedPaymentUrl(req);
299
+ should.exist(data.urlWithSignature);
300
+ data.urlWithSignature.should.equal('widgetApi4?apiKey=apiKey4&currencyCode=btc&walletAddress=bitcoin%3A123123&baseCurrencyCode=usd&baseCurrencyAmount=500&externalTransactionId=123123&redirectURL=bitpay%3A%2F%2Fmoonpay&allowedIpAddress=HkPyqsZMUzAgsEx27Tlz%2B5XfZHaH0fSfWV%2FMKR7JAPc%3D&signature=B%2Bw0TTQiy8%2Ffq6QeoeSf4dKpdPHZ%2F2EnBB1S4UotNGM%3D');
301
+ });
302
+
303
+ it('should canonicalize IPv4-mapped IPv6 deviceIp before hashing', () => {
304
+ req.body.context = 'web';
305
+ req.body.deviceIp = '::ffff:203.0.113.42';
306
+ const data = server.externalServices.moonpay.moonpayGetSignedPaymentUrl(req);
307
+ should.exist(data.urlWithSignature);
308
+ data.urlWithSignature.should.equal('widgetApi4?apiKey=apiKey4&currencyCode=btc&walletAddress=bitcoin%3A123123&baseCurrencyCode=usd&baseCurrencyAmount=500&externalTransactionId=123123&redirectURL=bitpay%3A%2F%2Fmoonpay&allowedIpAddress=HkPyqsZMUzAgsEx27Tlz%2B5XfZHaH0fSfWV%2FMKR7JAPc%3D&signature=B%2Bw0TTQiy8%2Ffq6QeoeSf4dKpdPHZ%2F2EnBB1S4UotNGM%3D');
309
+ });
310
+
311
+ it('should omit allowedIpAddress for web context when no deviceIp is forwarded', () => {
312
+ req.body.context = 'web';
313
+ const data = server.externalServices.moonpay.moonpayGetSignedPaymentUrl(req);
314
+ should.exist(data.urlWithSignature);
315
+ data.urlWithSignature.should.equal('widgetApi4?apiKey=apiKey4&currencyCode=btc&walletAddress=bitcoin%3A123123&baseCurrencyCode=usd&baseCurrencyAmount=500&externalTransactionId=123123&redirectURL=bitpay%3A%2F%2Fmoonpay&signature=13Q%2BET1UQLnCqCyg3stDAN4%2FTQ8QB009LcuAP1y6B%2FI%3D');
316
+ });
317
+
318
+ it('should ignore a body deviceIp for non-web context and use the request IP', () => {
319
+ req.body.deviceIp = '203.0.113.42';
320
+ const data = server.externalServices.moonpay.moonpayGetSignedPaymentUrl(req);
321
+ should.exist(data.urlWithSignature);
322
+ 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');
323
+ });
324
+
295
325
  it('should return error if there is some missing arguments', () => {
296
326
  delete req.body.currencyCode;
297
327
  try {