diff --git a/packages/bitcore-wallet-client/src/lib/api.ts b/packages/bitcore-wallet-client/src/lib/api.ts index b68542eb55..ea58f5e0f9 100644 --- a/packages/bitcore-wallet-client/src/lib/api.ts +++ b/packages/bitcore-wallet-client/src/lib/api.ts @@ -4087,6 +4087,42 @@ export class API extends EventEmitter { async oneInchGetSwap(data) { return this.request.post('/v1/service/oneInch/getSwap', data); } + + async moralisGetWalletTokenBalances(data) { + return this.request.post('/v1/moralis/getWalletTokenBalances', data); + } + + async moralisGetTokenAllowance(data) { + return this.request.post('/v1/moralis/moralisGetTokenAllowance', data); + } + + async moralisGetNativeBalance(data) { + return this.request.post('/v1/moralis/moralisGetNativeBalance', data); + } + + async moralisGetTokenPrice(data) { + return this.request.post('/v1/moralis/GetTokenPrice', data); + } + + async moralisGetMultipleERC20TokenPrices(data) { + return this.request.post('/v1/moralis/getMultipleERC20TokenPrices', data); + } + + async moralisGetERC20TokenBalancesWithPricesByWallet(data) { + return this.request.post('/v1/moralis/getERC20TokenBalancesWithPricesByWallet', data); + } + + async moralisGetSolWalletPortfolio(data) { + return this.request.post('/v1/moralis/getSolWalletPortfolio', data); + } + + async moralisGetTransactionVerbose(data) { + return this.request.post('/v1/moralis/getTransactionVerbose', data); + } + + async moralisGetMultipleSolTokenPrices(data) { + return this.request.post('/v1/moralis/getMultipleSolTokenPrices', data); + } }; export type Network = 'livenet' | 'testnet' | 'regtest'; diff --git a/packages/bitcore-wallet-service/src/lib/expressapp.ts b/packages/bitcore-wallet-service/src/lib/expressapp.ts index 71f42d27bf..38194e8356 100644 --- a/packages/bitcore-wallet-service/src/lib/expressapp.ts +++ b/packages/bitcore-wallet-service/src/lib/expressapp.ts @@ -126,6 +126,7 @@ export class ExpressApp { registerMoralisRoutes(router, { getServer, + getServerWithAuth, returnError }); diff --git a/packages/bitcore-wallet-service/src/lib/routes/moralis.ts b/packages/bitcore-wallet-service/src/lib/routes/moralis.ts index 8c6e1b23d5..bc73146dbb 100644 --- a/packages/bitcore-wallet-service/src/lib/routes/moralis.ts +++ b/packages/bitcore-wallet-service/src/lib/routes/moralis.ts @@ -6,6 +6,7 @@ import type { WalletService } from '../server'; interface RouteContext { getServer: Types.GetServerFn; + getServerWithAuth: Types.GetServerWithAuthFn; returnError: Types.ReturnErrorFn; } @@ -33,6 +34,21 @@ function respondWithPublicServer(req, res, context: RouteContext, handler: Route }); } +function walletAuthOrPublic(context: RouteContext) { + return function(req, res, next) { + if (!req.header('x-identity')) { + // Not from wallet + return next(); + } + return context.getServerWithAuth(req, res, _server => { + if (!req.header('origin')) { + req.headers['origin'] = config.moralis?.whitelist?.[0]; + } + return next(); + }); + }; +} + export function registerMoralisRoutes(router: express.Router, context: RouteContext) { const moralisCorsOptions = { origin: (origin, cb) => { @@ -45,55 +61,55 @@ export function registerMoralisRoutes(router: express.Router, context: RouteCont } }; - router.post('/v1/moralis/getWalletTokenBalances', cors(moralisCorsOptions), (req, res) => { + router.post('/v1/moralis/getWalletTokenBalances', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => { respondWithPublicServer(req, res, context, server => { return server.moralisGetWalletTokenBalances(req); }); }); - router.post('/v1/moralis/moralisGetTokenAllowance', cors(moralisCorsOptions), (req, res) => { + router.post('/v1/moralis/moralisGetTokenAllowance', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => { respondWithPublicServer(req, res, context, server => { return server.moralisGetTokenAllowance(req); }); }); - router.post('/v1/moralis/moralisGetNativeBalance', cors(moralisCorsOptions), (req, res) => { + router.post('/v1/moralis/moralisGetNativeBalance', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => { respondWithPublicServer(req, res, context, server => { return server.moralisGetNativeBalance(req); }); }); - router.post('/v1/moralis/GetTokenPrice', cors(moralisCorsOptions), (req, res) => { + router.post('/v1/moralis/GetTokenPrice', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => { respondWithPublicServer(req, res, context, server => { return server.moralisGetTokenPrice(req); }); }); - router.post('/v1/moralis/getMultipleERC20TokenPrices', cors(moralisCorsOptions), (req, res) => { + router.post('/v1/moralis/getMultipleERC20TokenPrices', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => { respondWithPublicServer(req, res, context, server => { return server.moralisGetMultipleERC20TokenPrices(req); }); }); - router.post('/v1/moralis/getERC20TokenBalancesWithPricesByWallet', cors(moralisCorsOptions), (req, res) => { + router.post('/v1/moralis/getERC20TokenBalancesWithPricesByWallet', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => { respondWithPublicServer(req, res, context, server => { return server.moralisGetERC20TokenBalancesWithPricesByWallet(req); }); }); - router.post('/v1/moralis/getSolWalletPortfolio', cors(moralisCorsOptions), (req, res) => { + router.post('/v1/moralis/getSolWalletPortfolio', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => { respondWithPublicServer(req, res, context, server => { return server.moralisGetSolWalletPortfolio(req); }); }); - router.post('/v1/moralis/getTransactionVerbose', cors(moralisCorsOptions), (req, res) => { + router.post('/v1/moralis/getTransactionVerbose', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => { respondWithPublicServer(req, res, context, server => { return server.moralisGetTransactionVerbose(req); }); }); - router.post('/v1/moralis/getMultipleSolTokenPrices', cors(moralisCorsOptions), (req, res) => { + router.post('/v1/moralis/getMultipleSolTokenPrices', walletAuthOrPublic(context), cors(moralisCorsOptions), (req, res) => { respondWithPublicServer(req, res, context, server => { return server.moralisGetMultipleSolTokenPrices(req); }); diff --git a/packages/bitcore-wallet-service/test/expressapp.test.ts b/packages/bitcore-wallet-service/test/expressapp.test.ts index e2a3bde87f..8939ea6440 100644 --- a/packages/bitcore-wallet-service/test/expressapp.test.ts +++ b/packages/bitcore-wallet-service/test/expressapp.test.ts @@ -610,6 +610,89 @@ describe('ExpressApp', function() { }); }); + describe('Moralis routes (wallet-authenticated or public)', function() { + const whitelistedOrigin = 'https://whitelisted-dapp.example'; + let originalMoralisConfig; + beforeEach(function() { + originalMoralisConfig = config.moralis; + config.moralis = { whitelist: [whitelistedOrigin] }; + }); + afterEach(function() { + config.moralis = originalMoralisConfig; + }); + + it('/v1/moralis/getMultipleERC20TokenPrices should work without auth headers (public/dApp)', function(done) { + const server = { + moralisGetMultipleERC20TokenPrices: sinon.stub().resolves([{ tokenAddress: '0xaaa', usdPrice: 1 }]), + }; + sandbox.stub(WalletService, 'initialize').callsArg(1); + sandbox.stub(WalletService, 'getInstance').returns(server); + start(ExpressApp, function() { + const requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/moralis/getMultipleERC20TokenPrices', + method: 'post', + json: { chain: '0x1', tokens: [{ tokenAddress: '0xaaa' }] }, + headers: { + origin: whitelistedOrigin + } + }; + request(requestOptions, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.equal(200); + body[0].tokenAddress.should.equal('0xaaa'); + done(); + }); + }); + }); + + it('/v1/moralis/getMultipleERC20TokenPrices should work with valid wallet auth headers', function(done) { + const server = { + moralisGetMultipleERC20TokenPrices: sinon.stub().resolves([{ tokenAddress: '0xaaa', usdPrice: 1 }]), + }; + sandbox.stub(WalletService, 'initialize').callsArg(1); + sandbox.stub(WalletService, 'getInstance').returns(server); + sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, null, server); + start(ExpressApp, function() { + const requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/moralis/getMultipleERC20TokenPrices', + method: 'post', + json: { chain: '0x1', tokens: [{ tokenAddress: '0xaaa' }] }, + headers: { + 'x-identity': 'identity', + 'x-signature': 'signature' + } + }; + request(requestOptions, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.equal(200); + body[0].tokenAddress.should.equal('0xaaa'); + done(); + }); + }); + }); + + it('/v1/moralis/getMultipleERC20TokenPrices should fail with invalid wallet auth headers', function(done) { + sandbox.stub(WalletService, 'initialize').callsArg(1); + sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, new ClientError('NOT_AUTHORIZED', 'Not authorized')); + start(ExpressApp, function() { + const requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/moralis/getMultipleERC20TokenPrices', + method: 'post', + json: { chain: '0x1', tokens: [{ tokenAddress: '0xaaa' }] }, + headers: { + 'x-identity': 'identity', + 'x-signature': 'bad-signature' + } + }; + request(requestOptions, function(err, res) { + should.not.exist(err); + res.statusCode.should.equal(401); + done(); + }); + }); + }); + }); + describe('Token allowance', function() { it('/v1/token/allowance', function(done) { const server = {