diff --git a/packages/bitcore-wallet-client/src/lib/api.ts b/packages/bitcore-wallet-client/src/lib/api.ts index 06ee382d52d..5686d9e0225 100644 --- a/packages/bitcore-wallet-client/src/lib/api.ts +++ b/packages/bitcore-wallet-client/src/lib/api.ts @@ -3953,6 +3953,14 @@ export class API extends EventEmitter { return this.request.post('/v1/service/moonpay/cancelSellTransaction', data); } + async moonpayCreateSession(data) { + return this.request.post('/v1/service/moonpay/createSession', data); + } + + async moonpayRevokeActiveSession(data) { + return this.request.post('/v1/service/moonpay/revokeActiveSession', data); + } + async rampGetQuote(data) { return this.request.post('/v1/service/ramp/quote', data); } diff --git a/packages/bitcore-wallet-service/bws.example.config.js b/packages/bitcore-wallet-service/bws.example.config.js index ad6f8c3e502..32e60f10e28 100644 --- a/packages/bitcore-wallet-service/bws.example.config.js +++ b/packages/bitcore-wallet-service/bws.example.config.js @@ -214,10 +214,11 @@ module.exports = { // moonpay: { // sandbox: { // apiKey: 'moonpay_sandbox_api_key_here', - // api: 'https://api.moonpay.com', + // api: 'https://api.moonpay.dev', // widgetApi: 'https://buy-sandbox.moonpay.com', // sellWidgetApi: 'https://sell-sandbox.moonpay.com', // secretKey: 'moonpay_sandbox_secret_key_here', + // secretKeyEmbedded: 'moonpay_sandbox_secret_key_embedded_here', // }, // production: { // apiKey: 'moonpay_production_api_key_here', @@ -225,10 +226,11 @@ module.exports = { // widgetApi: 'https://buy.moonpay.com', // sellWidgetApi: 'https://sell.moonpay.com', // secretKey: 'moonpay_production_secret_key_here', + // secretKeyEmbedded: 'moonpay_production_secret_key_embedded_here', // }, // sandboxWeb: { // apiKey: 'moonpay_sandbox_web_api_key_here', - // api: 'https://api.moonpay.com', + // api: 'https://api.moonpay.dev', // widgetApi: 'https://buy-sandbox.moonpay.com', // sellWidgetApi: 'https://sell-sandbox.moonpay.com', // secretKey: 'moonpay_sandbox_web_secret_key_here', diff --git a/packages/bitcore-wallet-service/src/config.ts b/packages/bitcore-wallet-service/src/config.ts index 645531f6b99..7273a6ce28c 100644 --- a/packages/bitcore-wallet-service/src/config.ts +++ b/packages/bitcore-wallet-service/src/config.ts @@ -289,10 +289,11 @@ const Config = (): any => { // moonpay: { // sandbox: { // apiKey: 'moonpay_sandbox_api_key_here', - // api: 'https://api.moonpay.com', + // api: 'https://api.moonpay.dev', // widgetApi: 'https://buy-sandbox.moonpay.com', // sellWidgetApi: 'https://sell-sandbox.moonpay.com', // secretKey: 'moonpay_sandbox_secret_key_here', + // secretKeyEmbedded: 'moonpay_sandbox_secret_key_embedded_here', // }, // production: { // apiKey: 'moonpay_production_api_key_here', @@ -300,10 +301,11 @@ const Config = (): any => { // widgetApi: 'https://buy.moonpay.com', // sellWidgetApi: 'https://sell.moonpay.com', // secretKey: 'moonpay_production_secret_key_here', + // secretKeyEmbedded: 'moonpay_production_secret_key_embedded_here', // }, // sandboxWeb: { // apiKey: 'moonpay_sandbox_web_api_key_here', - // api: 'https://api.moonpay.com', + // api: 'https://api.moonpay.dev', // widgetApi: 'https://buy-sandbox.moonpay.com', // sellWidgetApi: 'https://sell-sandbox.moonpay.com', // secretKey: 'moonpay_sandbox_web_secret_key_here', diff --git a/packages/bitcore-wallet-service/src/externalservices/moonpay.ts b/packages/bitcore-wallet-service/src/externalservices/moonpay.ts index ef36bd74a11..ad3dc64114c 100644 --- a/packages/bitcore-wallet-service/src/externalservices/moonpay.ts +++ b/packages/bitcore-wallet-service/src/externalservices/moonpay.ts @@ -1,6 +1,7 @@ import { BitcoreLib as Bitcore } from '@bitpay-labs/crypto-wallet-core'; import * as request from 'request'; import config from '../config'; +import { Utils } from '../lib/common/utils'; import { ClientError } from '../lib/errors/clienterror'; import { checkRequired } from '../lib/server'; @@ -25,12 +26,14 @@ export class MoonpayService { SELL_WIDGET_API: string; API_KEY: string; SECRET_KEY: string; + SECRET_KEY_EMBEDDED: string | undefined; } = { API: config.moonpay[env].api, WIDGET_API: config.moonpay[env].widgetApi, SELL_WIDGET_API: config.moonpay[env].sellWidgetApi, API_KEY: config.moonpay[env].apiKey, - SECRET_KEY: config.moonpay[env].secretKey + SECRET_KEY: config.moonpay[env].secretKey, + SECRET_KEY_EMBEDDED: config.moonpay[env].secretKeyEmbedded }; return keys; @@ -435,4 +438,85 @@ export class MoonpayService { ); }); } + + moonpayCreateSession(req): Promise<{ sessionToken: string }> { + return new Promise((resolve, reject) => { + const keys = this.moonpayGetKeys(req); + const API = keys.API; + const SECRET_KEY = keys.SECRET_KEY_EMBEDDED; + + if (!checkRequired(req.body, ['externalCustomerId'])) { + return reject(new ClientError("Moonpay's request missing arguments")); + } + + const deviceIp = Utils.getIpFromReq(req); + if (!deviceIp) { + return reject(new ClientError('Could not determine device IP address')); + } + + const headers = { + 'Content-Type': 'application/json', + Authorization: 'Api-Key ' + SECRET_KEY, + }; + + const body: any = { + externalCustomerId: req.body.externalCustomerId, + deviceIp + }; + if (req.body.email) body.email = req.body.email; + if (req.body.phoneNumber) body.phoneNumber = req.body.phoneNumber; + + const URL = API + '/platform/v1/session'; + + this.request.post( + URL, + { + headers, + body, + json: true + }, + (err, data) => { + if (err) { + return reject(err.body ?? err); + } else { + return resolve(data.body ?? data); + } + } + ); + }); + } + + moonpayRevokeActiveSession(req): Promise { + return new Promise((resolve, reject) => { + const keys = this.moonpayGetKeys(req); + const API = keys.API; + const SECRET_KEY = keys.SECRET_KEY_EMBEDDED; + + if (!checkRequired(req.body, ['externalCustomerId'])) { + return reject(new ClientError("Moonpay's request missing arguments")); + } + + const headers = { + Accept: 'application/json', + Authorization: 'Api-Key ' + SECRET_KEY + }; + + const URL = API + '/platform/v1/sessions?externalCustomerId=' + encodeURIComponent(req.body.externalCustomerId); + + this.request.delete( + URL, + { + headers, + json: true + }, + (err, data) => { + if (err) { + return reject(err.body ? err.body : err); + } else { + return resolve(data.body ? data.body : data); + } + } + ); + }); + } } \ No newline at end of file diff --git a/packages/bitcore-wallet-service/src/lib/common/utils.ts b/packages/bitcore-wallet-service/src/lib/common/utils.ts index af75330135f..6c5e9706d08 100644 --- a/packages/bitcore-wallet-service/src/lib/common/utils.ts +++ b/packages/bitcore-wallet-service/src/lib/common/utils.ts @@ -282,13 +282,20 @@ export const Utils = { }, getIpFromReq(req): string { + let ip = ''; if (req.headers) { - if (req.headers['x-forwarded-for']) return req.headers['x-forwarded-for'].split(',')[0]; - if (req.headers['x-real-ip']) return req.headers['x-real-ip'].split(',')[0]; + if (req.headers['x-forwarded-for']) { + ip = req.headers['x-forwarded-for'].split(',')[0]; + } else if (req.headers['x-real-ip']) { + ip = req.headers['x-real-ip'].split(',')[0]; + } } - if (req.ip) return req.ip; - if (req.connection && req.connection.remoteAddress) return req.connection.remoteAddress; - return ''; + if (!ip && req.ip) ip = req.ip; + if (!ip && req.connection?.remoteAddress) ip = req.connection.remoteAddress; + + if (ip && typeof ip === 'string') ip = ip.trim(); + + return ip; }, checkValueInCollection(value, collection) { diff --git a/packages/bitcore-wallet-service/src/lib/routes/services.ts b/packages/bitcore-wallet-service/src/lib/routes/services.ts index 02d894d036f..2256fdc6dc0 100644 --- a/packages/bitcore-wallet-service/src/lib/routes/services.ts +++ b/packages/bitcore-wallet-service/src/lib/routes/services.ts @@ -281,6 +281,18 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont }); }); + router.post('/v1/service/moonpay/createSession', (req, res) => { + respondWithAuthServer(req, res, context, server => { + return server.externalServices.moonpay.moonpayCreateSession(req); + }); + }); + + router.post('/v1/service/moonpay/revokeActiveSession', (req, res) => { + respondWithAuthServer(req, res, context, server => { + return server.externalServices.moonpay.moonpayRevokeActiveSession(req); + }); + }); + router.post('/v1/service/ramp/quote', (req, res) => { respondWithAuthServer(req, res, context, server => { return server.externalServices.ramp.rampGetQuote(req); diff --git a/packages/bitcore-wallet-service/test/integration/externalservices/moonpay.test.ts b/packages/bitcore-wallet-service/test/integration/externalservices/moonpay.test.ts index 5b6e452563a..5cd271b8c9f 100644 --- a/packages/bitcore-wallet-service/test/integration/externalservices/moonpay.test.ts +++ b/packages/bitcore-wallet-service/test/integration/externalservices/moonpay.test.ts @@ -566,4 +566,153 @@ describe('Moonpay integration', () => { } }); }); + + describe('#moonpayCreateSession', () => { + beforeEach(() => { + req = { + headers: { + 'x-forwarded-for': '192.168.1.1' + }, + body: { + env: 'sandbox', + externalCustomerId: 'externalCustomerId1' + } + }; + server.externalServices.moonpay.request = fakeRequest; + }); + + it('should work properly if req is OK', async () => { + const data = await server.externalServices.moonpay.moonpayCreateSession(req); + should.exist(data); + }); + + it('should work properly if req is OK for web', async () => { + req.body.context = 'web'; + const data = await server.externalServices.moonpay.moonpayCreateSession(req); + should.exist(data); + }); + + it('should work properly with optional email and phoneNumber', async () => { + req.body.email = 'user@example.com'; + req.body.phoneNumber = '+14155551234'; + const data = await server.externalServices.moonpay.moonpayCreateSession(req); + should.exist(data); + }); + + it('should return error if post returns error', async () => { + const fakeRequest2 = { + post: (_url, _opts, _cb) => { return _cb(new Error('Error'), null); }, + }; + + server.externalServices.moonpay.request = fakeRequest2; + try { + await server.externalServices.moonpay.moonpayCreateSession(req); + should.fail('should have thrown'); + } catch (err) { + err.message.should.equal('Error'); + } + }); + + it('should return error if there is some missing arguments', async () => { + delete req.body.externalCustomerId; + try { + await server.externalServices.moonpay.moonpayCreateSession(req); + should.fail('should have thrown'); + } catch (err) { + err.message.should.equal('Moonpay\'s request missing arguments'); + } + }); + + it('should return error if device IP cannot be determined', async () => { + req.headers = {}; + delete req.ip; + delete req.connection; + try { + await server.externalServices.moonpay.moonpayCreateSession(req); + should.fail('should have thrown'); + } catch (err) { + err.message.should.equal('Could not determine device IP address'); + } + }); + + it('should extract IP from x-forwarded-for header', async () => { + req.headers = { 'x-forwarded-for': '10.0.0.1, 10.0.0.2' }; + let capturedBody; + const fakeRequest2 = { + post: (_url, _opts, _cb) => { + capturedBody = _opts.body; + return _cb(null, { body: { sessionToken: 'token123' } }); + }, + }; + server.externalServices.moonpay.request = fakeRequest2; + await server.externalServices.moonpay.moonpayCreateSession(req); + capturedBody.deviceIp.should.equal('10.0.0.1'); + }); + + it('should return error if moonpay is commented in config', async () => { + config.moonpay = undefined; + try { + await server.externalServices.moonpay.moonpayCreateSession(req); + should.fail('should have thrown'); + } catch (err) { + err.message.should.equal('Moonpay missing credentials'); + } + }); + }); + + describe('#moonpayRevokeActiveSession', () => { + beforeEach(() => { + req = { + headers: {}, + body: { + env: 'sandbox', + externalCustomerId: 'externalCustomerId1' + } + }; + server.externalServices.moonpay.request = fakeRequest; + }); + + it('should work properly if req is OK', async () => { + await server.externalServices.moonpay.moonpayRevokeActiveSession(req); + }); + + it('should work properly if req is OK for web', async () => { + req.body.context = 'web'; + await server.externalServices.moonpay.moonpayRevokeActiveSession(req); + }); + + it('should return error if delete returns error', async () => { + const fakeRequest2 = { + delete: (_url, _opts, _cb) => { return _cb(new Error('Error'), null); }, + }; + + server.externalServices.moonpay.request = fakeRequest2; + try { + await server.externalServices.moonpay.moonpayRevokeActiveSession(req); + should.fail('should have thrown'); + } catch (err) { + err.message.should.equal('Error'); + } + }); + + it('should return error if there is some missing arguments', async () => { + delete req.body.externalCustomerId; + try { + await server.externalServices.moonpay.moonpayRevokeActiveSession(req); + should.fail('should have thrown'); + } catch (err) { + err.message.should.equal('Moonpay\'s request missing arguments'); + } + }); + + it('should return error if moonpay is commented in config', async () => { + config.moonpay = undefined; + try { + await server.externalServices.moonpay.moonpayRevokeActiveSession(req); + should.fail('should have thrown'); + } catch (err) { + err.message.should.equal('Moonpay missing credentials'); + } + }); + }); }); diff --git a/packages/bitcore-wallet-service/test/utils.test.ts b/packages/bitcore-wallet-service/test/utils.test.ts index 9fa80b3c890..6f7f4bfff15 100644 --- a/packages/bitcore-wallet-service/test/utils.test.ts +++ b/packages/bitcore-wallet-service/test/utils.test.ts @@ -360,6 +360,35 @@ describe('Utils', function() { const ip = Utils.getIpFromReq(req); ip.should.equal(''); }); + + it('should trim whitespace from the ip', function() { + const req = { + headers: { + 'x-forwarded-for': ' 1.2.3.4 , 5.6.7.8' + } + }; + + const ip = Utils.getIpFromReq(req); + ip.should.equal('1.2.3.4'); + }); + + it('should preserve IPv4-mapped IPv6 addresses', function() { + const req = { + ip: '::ffff:192.168.1.1' + }; + + const ip = Utils.getIpFromReq(req); + ip.should.equal('::ffff:192.168.1.1'); + }); + + it('should preserve regular IPv6 addresses', function() { + const req = { + ip: '2001:0db8:85a3:0000:0000:8a2e:0370:7334' + }; + + const ip = Utils.getIpFromReq(req); + ip.should.equal('2001:0db8:85a3:0000:0000:8a2e:0370:7334'); + }); }); describe('#sortAsc', function() {