Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/bitcore-wallet-client/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
6 changes: 4 additions & 2 deletions packages/bitcore-wallet-service/bws.example.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,21 +214,23 @@ 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',
// api: 'https://api.moonpay.com',
// 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',
Expand Down
6 changes: 4 additions & 2 deletions packages/bitcore-wallet-service/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,21 +289,23 @@ 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',
// api: 'https://api.moonpay.com',
// 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',
Expand Down
86 changes: 85 additions & 1 deletion packages/bitcore-wallet-service/src/externalservices/moonpay.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;
Expand Down Expand Up @@ -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<void> {
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);
}
}
);
});
}
}
17 changes: 12 additions & 5 deletions packages/bitcore-wallet-service/src/lib/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions packages/bitcore-wallet-service/src/lib/routes/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
});
});
Loading