From 6b4aed3f958713e7f1a552385dcb9075aafadded Mon Sep 17 00:00:00 2001 From: tmcollins4 Date: Fri, 20 Mar 2026 09:33:32 -0400 Subject: [PATCH 1/3] add aave and tokenAllowance passthroughs and tests --- .../src/lib/blockchainexplorers/v8.ts | 42 +++++++ .../src/lib/expressapp.ts | 13 +++ .../src/lib/routes/aave.ts | 51 +++++++++ .../bitcore-wallet-service/src/lib/server.ts | 56 ++++++++++ .../test/expressapp.test.ts | 105 ++++++++++++++++++ .../bitcore-wallet-service/test/v8.test.ts | 99 +++++++++++++++++ 6 files changed, 366 insertions(+) create mode 100644 packages/bitcore-wallet-service/src/lib/routes/aave.ts diff --git a/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts b/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts index a9c3af0c278..c588673fe2c 100644 --- a/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts +++ b/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts @@ -486,6 +486,48 @@ export class V8 { }); } + getAaveUserAccountData(opts: { address: string; version?: string }, cb) { + const url = this.baseUrl + '/aave/account/' + opts.address + '?version=' + (opts.version || 'v3'); + logger.debug('[v8.js] GETTING AAVE USER ACCOUNT DATA %o', url); + this.request + .get(url, {}) + .then(accountData => { + accountData = JSON.parse(accountData); + return cb(null, accountData); + }) + .catch(err => { + return cb(err); + }); + } + + getAaveReserveData(opts: { asset: string; version?: string }, cb) { + const url = this.baseUrl + '/aave/reserve/' + opts.asset + '?version=' + (opts.version || 'v3'); + logger.debug('[v8.js] GETTING AAVE RESERVE DATA %o', url); + this.request + .get(url, {}) + .then(reserveData => { + reserveData = JSON.parse(reserveData); + return cb(null, reserveData); + }) + .catch(err => { + return cb(err); + }); + } + + getAaveReserveTokensAddresses(opts: { asset: string; version?: string }, cb) { + const url = this.baseUrl + '/aave/reserve-tokens/' + opts.asset + '?version=' + (opts.version || 'v3'); + logger.debug('[v8.js] GETTING AAVE RESERVE TOKENS ADDRESSES %o', url); + this.request + .get(url, {}) + .then(tokensAddresses => { + tokensAddresses = JSON.parse(tokensAddresses); + return cb(null, tokensAddresses); + }) + .catch(err => { + return cb(err); + }); + } + getMultisigTxpsInfo(opts: { multisigContractAddress: string }, cb) { const url = this.baseUrl + '/ethmultisig/txps/' + opts.multisigContractAddress; logger.debug('[v8.js] CHECKING CONTRACT TXPS INFO %o', url); diff --git a/packages/bitcore-wallet-service/src/lib/expressapp.ts b/packages/bitcore-wallet-service/src/lib/expressapp.ts index 349ca34ef9c..b4222319664 100644 --- a/packages/bitcore-wallet-service/src/lib/expressapp.ts +++ b/packages/bitcore-wallet-service/src/lib/expressapp.ts @@ -13,6 +13,7 @@ import { Common } from './common'; import { ClientError } from './errors/clienterror'; import { Errors } from './errors/errordefinitions'; import { logger, transports } from './logger'; +import { AaveRouter } from './routes/aave'; import { error } from './routes/helpers'; import { createWalletLimiter } from './routes/middleware/createWalletLimiter'; import { LogMiddleware } from './routes/middleware/log'; @@ -973,6 +974,17 @@ export class ExpressApp { }); }); + router.post('/v1/token/allowance', (req, res) => { + getServerWithAuth(req, res, async server => { + try { + const allowance = await server.getTokenAllowance(req.body); + res.json(allowance); + } catch (err) { + returnError(err, res, req); + } + }); + }); + router.get('/v1/sendmaxinfo/', (req, res) => { getServerWithAuth(req, res, server => { const q = req.query; @@ -2391,6 +2403,7 @@ export class ExpressApp { }); /** Imported routes */ + router.use(new AaveRouter({ returnError, getServerWithAuth }).router); router.use(new TssRouter({ returnError, opts }).router); diff --git a/packages/bitcore-wallet-service/src/lib/routes/aave.ts b/packages/bitcore-wallet-service/src/lib/routes/aave.ts new file mode 100644 index 00000000000..77334384725 --- /dev/null +++ b/packages/bitcore-wallet-service/src/lib/routes/aave.ts @@ -0,0 +1,51 @@ +import express from 'express'; +import * as Types from '../../types/expressapp'; + +interface AaveRouterOpts { + returnError: Types.ReturnErrorFn; + getServerWithAuth: Types.GetServerWithAuthFn; +} + +export class AaveRouter { + router: express.Router; + + constructor(params: AaveRouterOpts) { + const { returnError, getServerWithAuth } = params; + const router = express.Router(); + + router.post('/v1/service/aave/userAccountData', (req, res) => { + getServerWithAuth(req, res, async server => { + try { + const accountData = await server.getAaveUserAccountData(req.body); + res.json(accountData); + } catch (err) { + returnError(err, res, req); + } + }); + }); + + router.post('/v1/service/aave/reserveData', (req, res) => { + getServerWithAuth(req, res, async server => { + try { + const reserveData = await server.getAaveReserveData(req.body); + res.json(reserveData); + } catch (err) { + returnError(err, res, req); + } + }); + }); + + router.post('/v1/service/aave/reserveTokensAddresses', (req, res) => { + getServerWithAuth(req, res, async server => { + try { + const tokensAddresses = await server.getAaveReserveTokensAddresses(req.body); + res.json(tokensAddresses); + } catch (err) { + returnError(err, res, req); + } + }); + }); + + this.router = router; + } +} diff --git a/packages/bitcore-wallet-service/src/lib/server.ts b/packages/bitcore-wallet-service/src/lib/server.ts index a53a98ada93..16614c71211 100644 --- a/packages/bitcore-wallet-service/src/lib/server.ts +++ b/packages/bitcore-wallet-service/src/lib/server.ts @@ -2559,6 +2559,62 @@ export class WalletService implements IWalletService { }); } + getTokenAllowance(opts) { + const bc = this._getBlockchainExplorer(opts.chain || Defaults.EVM_CHAIN, opts.network); + return new Promise((resolve, reject) => { + if (!bc) return reject(new Error('Could not get blockchain explorer instance')); + bc.getTokenAllowance(opts, (err, allowance) => { + if (err) { + this.logw('Error getting token allowance:', err); + return reject(err); + } + return resolve(allowance); + }); + }); + } + + getAaveUserAccountData(opts) { + const bc = this._getBlockchainExplorer(opts.chain || Defaults.EVM_CHAIN, opts.network); + return new Promise((resolve, reject) => { + if (!bc) return reject(new Error('Could not get blockchain explorer instance')); + bc.getAaveUserAccountData(opts, (err, accountData) => { + if (err) { + this.logw('Error getting Aave user account data:', err); + return reject(err); + } + return resolve(accountData); + }); + }); + } + + getAaveReserveData(opts) { + const bc = this._getBlockchainExplorer(opts.chain || Defaults.EVM_CHAIN, opts.network); + return new Promise((resolve, reject) => { + if (!bc) return reject(new Error('Could not get blockchain explorer instance')); + bc.getAaveReserveData(opts, (err, reserveData) => { + if (err) { + this.logw('Error getting Aave reserve data:', err); + return reject(err); + } + return resolve(reserveData); + }); + }); + } + + getAaveReserveTokensAddresses(opts) { + const bc = this._getBlockchainExplorer(opts.chain || Defaults.EVM_CHAIN, opts.network); + return new Promise((resolve, reject) => { + if (!bc) return reject(new Error('Could not get blockchain explorer instance')); + bc.getAaveReserveTokensAddresses(opts, (err, tokensAddresses) => { + if (err) { + this.logw('Error getting Aave reserve tokens addresses:', err); + return reject(err); + } + return resolve(tokensAddresses); + }); + }); + } + getMultisigTxpsInfo(opts) { const bc = this._getBlockchainExplorer(opts.chain || Defaults.EVM_CHAIN, opts.network); return new Promise((resolve, reject) => { diff --git a/packages/bitcore-wallet-service/test/expressapp.test.ts b/packages/bitcore-wallet-service/test/expressapp.test.ts index ed61a3ff382..1f0990c463b 100644 --- a/packages/bitcore-wallet-service/test/expressapp.test.ts +++ b/packages/bitcore-wallet-service/test/expressapp.test.ts @@ -483,6 +483,111 @@ describe('ExpressApp', function() { }); }); + describe('Aave service routes', function() { + it('/v1/service/aave/userAccountData', function(done) { + const server = { + getAaveUserAccountData: sinon.stub().resolves({ totalCollateralBase: '1000', healthFactor: '2.0' }), + }; + sandbox.stub(WalletService, 'initialize').callsArg(1); + sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, null, server); + start(ExpressApp, function() { + const requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/service/aave/userAccountData', + method: 'post', + json: { chain: 'eth', network: 'mainnet', address: '0x123', version: 'v3' }, + headers: { + 'x-identity': 'identity', + 'x-signature': 'signature' + } + }; + request(requestOptions, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.equal(200); + body.totalCollateralBase.should.equal('1000'); + body.healthFactor.should.equal('2.0'); + done(); + }); + }); + }); + + it('/v1/service/aave/reserveData', function(done) { + const server = { + getAaveReserveData: sinon.stub().resolves({ currentVariableBorrowRate: '35000000' }), + }; + sandbox.stub(WalletService, 'initialize').callsArg(1); + sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, null, server); + start(ExpressApp, function() { + const requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/service/aave/reserveData', + method: 'post', + json: { chain: 'eth', network: 'mainnet', asset: '0xabc', version: 'v3' }, + headers: { + 'x-identity': 'identity', + 'x-signature': 'signature' + } + }; + request(requestOptions, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.equal(200); + body.currentVariableBorrowRate.should.equal('35000000'); + done(); + }); + }); + }); + + it('/v1/service/aave/reserveTokensAddresses', function(done) { + const server = { + getAaveReserveTokensAddresses: sinon.stub().resolves({ variableDebtTokenAddress: '0xdef' }), + }; + sandbox.stub(WalletService, 'initialize').callsArg(1); + sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, null, server); + start(ExpressApp, function() { + const requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/service/aave/reserveTokensAddresses', + method: 'post', + json: { chain: 'eth', network: 'mainnet', asset: '0xabc', version: 'v3' }, + headers: { + 'x-identity': 'identity', + 'x-signature': 'signature' + } + }; + request(requestOptions, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.equal(200); + body.variableDebtTokenAddress.should.equal('0xdef'); + done(); + }); + }); + }); + }); + + describe('Token allowance', function() { + it('/v1/token/allowance', function(done) { + const server = { + getTokenAllowance: sinon.stub().resolves({ allowance: '1000000000000000000' }), + }; + sandbox.stub(WalletService, 'initialize').callsArg(1); + sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, null, server); + start(ExpressApp, function() { + const requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/token/allowance', + method: 'post', + json: { chain: 'eth', network: 'mainnet', tokenAddress: '0xtoken', ownerAddress: '0xowner', spenderAddress: '0xspender' }, + headers: { + 'x-identity': 'identity', + 'x-signature': 'signature' + } + }; + request(requestOptions, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.equal(200); + body.allowance.should.equal('1000000000000000000'); + done(); + }); + }); + }); + }); + describe('Clear cache', function() { it('/v1/clearcache/', function(done) { const resolveStub = sinon.stub().callsFake( () => { return Promise.resolve(true);}); diff --git a/packages/bitcore-wallet-service/test/v8.test.ts b/packages/bitcore-wallet-service/test/v8.test.ts index f7edf033b93..1208d5b9ede 100644 --- a/packages/bitcore-wallet-service/test/v8.test.ts +++ b/packages/bitcore-wallet-service/test/v8.test.ts @@ -247,6 +247,105 @@ describe('V8', () => { }); + describe('#getAaveUserAccountData', () => { + it('should get aave user account data', (done) => { + const fakeRequest = { + get: sinon.stub().resolves('{"totalCollateralBase":"1000","totalDebtBase":"500","healthFactor":"2.0"}'), + }; + + const be = new V8({ + chain: 'eth', + network: 'livenet', + url: 'http://dummy/', + apiPrefix: 'dummyPath', + userAgent: 'testAgent', + request: fakeRequest, + }); + + be.getAaveUserAccountData({ address: '0x123', version: 'v3' }, (err, accountData) => { + should.not.exist(err); + should.exist(accountData); + accountData.totalCollateralBase.should.equal('1000'); + accountData.healthFactor.should.equal('2.0'); + return done(); + }); + }); + }); + + describe('#getAaveReserveData', () => { + it('should get aave reserve data', (done) => { + const fakeRequest = { + get: sinon.stub().resolves('{"currentVariableBorrowRate":"35000000000000000000000000"}'), + }; + + const be = new V8({ + chain: 'eth', + network: 'livenet', + url: 'http://dummy/', + apiPrefix: 'dummyPath', + userAgent: 'testAgent', + request: fakeRequest, + }); + + be.getAaveReserveData({ asset: '0xabc', version: 'v3' }, (err, reserveData) => { + should.not.exist(err); + should.exist(reserveData); + reserveData.currentVariableBorrowRate.should.equal('35000000000000000000000000'); + return done(); + }); + }); + }); + + describe('#getAaveReserveTokensAddresses', () => { + it('should get aave reserve tokens addresses', (done) => { + const fakeRequest = { + get: sinon.stub().resolves('{"variableDebtTokenAddress":"0xdef456"}'), + }; + + const be = new V8({ + chain: 'eth', + network: 'livenet', + url: 'http://dummy/', + apiPrefix: 'dummyPath', + userAgent: 'testAgent', + request: fakeRequest, + }); + + be.getAaveReserveTokensAddresses({ asset: '0xabc', version: 'v3' }, (err, tokensAddresses) => { + should.not.exist(err); + should.exist(tokensAddresses); + tokensAddresses.variableDebtTokenAddress.should.equal('0xdef456'); + return done(); + }); + }); + }); + + describe('#getTokenAllowance', () => { + it('should get token allowance', (done) => { + const fakeRequest = { + get: sinon.stub().resolves('{"allowance":"1000000000000000000"}'), + }; + + const be = new V8({ + chain: 'eth', + network: 'livenet', + url: 'http://dummy/', + apiPrefix: 'dummyPath', + userAgent: 'testAgent', + request: fakeRequest, + }); + + be.getTokenAllowance( + { tokenAddress: '0xtoken', ownerAddress: '0xowner', spenderAddress: '0xspender' }, + (err, token) => { + should.not.exist(err); + should.exist(token); + token.allowance.should.equal('1000000000000000000'); + return done(); + } + ); + }); + }); describe('#broadcast', () => { it('should broadcast a TX', (done) => { From effa27569694b2caf1acb07b613767eb18d85fb4 Mon Sep 17 00:00:00 2001 From: tmcollins4 Date: Fri, 20 Mar 2026 10:15:29 -0400 Subject: [PATCH 2/3] Update token allowance and Aave user account data tests with new response formats --- .../bitcore-wallet-service/test/expressapp.test.ts | 4 ++-- packages/bitcore-wallet-service/test/v8.test.ts | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/bitcore-wallet-service/test/expressapp.test.ts b/packages/bitcore-wallet-service/test/expressapp.test.ts index 1f0990c463b..f341cf724b0 100644 --- a/packages/bitcore-wallet-service/test/expressapp.test.ts +++ b/packages/bitcore-wallet-service/test/expressapp.test.ts @@ -564,7 +564,7 @@ describe('ExpressApp', function() { describe('Token allowance', function() { it('/v1/token/allowance', function(done) { const server = { - getTokenAllowance: sinon.stub().resolves({ allowance: '1000000000000000000' }), + getTokenAllowance: sinon.stub().resolves(5000000), }; sandbox.stub(WalletService, 'initialize').callsArg(1); sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, null, server); @@ -581,7 +581,7 @@ describe('ExpressApp', function() { request(requestOptions, function(err, res, body) { should.not.exist(err); res.statusCode.should.equal(200); - body.allowance.should.equal('1000000000000000000'); + body.should.equal(5000000); done(); }); }); diff --git a/packages/bitcore-wallet-service/test/v8.test.ts b/packages/bitcore-wallet-service/test/v8.test.ts index 1208d5b9ede..59310e205bc 100644 --- a/packages/bitcore-wallet-service/test/v8.test.ts +++ b/packages/bitcore-wallet-service/test/v8.test.ts @@ -250,7 +250,7 @@ describe('V8', () => { describe('#getAaveUserAccountData', () => { it('should get aave user account data', (done) => { const fakeRequest = { - get: sinon.stub().resolves('{"totalCollateralBase":"1000","totalDebtBase":"500","healthFactor":"2.0"}'), + get: sinon.stub().resolves('{"totalCollateralBase":"1000","totalDebtBase":"500","availableBorrowsBase":"200","currentLiquidationThreshold":"8000","ltv":"7500","healthFactor":"2.0"}'), }; const be = new V8({ @@ -266,6 +266,10 @@ describe('V8', () => { should.not.exist(err); should.exist(accountData); accountData.totalCollateralBase.should.equal('1000'); + accountData.totalDebtBase.should.equal('500'); + accountData.availableBorrowsBase.should.equal('200'); + accountData.currentLiquidationThreshold.should.equal('8000'); + accountData.ltv.should.equal('7500'); accountData.healthFactor.should.equal('2.0'); return done(); }); @@ -323,7 +327,7 @@ describe('V8', () => { describe('#getTokenAllowance', () => { it('should get token allowance', (done) => { const fakeRequest = { - get: sinon.stub().resolves('{"allowance":"1000000000000000000"}'), + get: sinon.stub().resolves('5000000'), }; const be = new V8({ @@ -337,10 +341,10 @@ describe('V8', () => { be.getTokenAllowance( { tokenAddress: '0xtoken', ownerAddress: '0xowner', spenderAddress: '0xspender' }, - (err, token) => { + (err, allowance) => { should.not.exist(err); - should.exist(token); - token.allowance.should.equal('1000000000000000000'); + should.exist(allowance); + allowance.should.equal(5000000); return done(); } ); From 95f01e4b04c5ed08956b42f1f2ead4a46a21ae32 Mon Sep 17 00:00:00 2001 From: tmcollins4 Date: Tue, 24 Mar 2026 15:47:46 -0400 Subject: [PATCH 3/3] - Expose Aave and token allowance endpoints through BWS (routes/aave.ts, expressapp.ts, server.ts, v8.ts) - Add BWC client methods for Aave and token allowance (api.ts) - Add JSDoc and input validation (checkRequired in server.ts, $.checkArgument in api.ts) - Use getServer instead of getServerWithAuth so invoice frontend can call endpoints directly - Add unit tests (BWS: v8.test.ts, expressapp.test.ts) and integration tests (BWC: api.test.ts, helpers.ts) - Fix error contract in expressapp tests: use ClientError instead of Error so tests correctly assert HTTP 400 (not 500) for missing arguments - Add Aave version validation in server.ts: reject invalid version values (only v2 and v3 supported) --- packages/bitcore-wallet-client/src/lib/api.ts | 92 ++++++++++++++ .../bitcore-wallet-client/test/api.test.ts | 89 ++++++++++++++ .../bitcore-wallet-client/test/helpers.ts | 12 ++ .../src/lib/expressapp.ts | 19 ++- .../src/lib/routes/aave.ts | 55 ++++----- .../bitcore-wallet-service/src/lib/server.ts | 66 +++++++++- .../test/expressapp.test.ts | 113 ++++++++++++++---- 7 files changed, 379 insertions(+), 67 deletions(-) diff --git a/packages/bitcore-wallet-client/src/lib/api.ts b/packages/bitcore-wallet-client/src/lib/api.ts index fffe6a33ad6..331d0688050 100644 --- a/packages/bitcore-wallet-client/src/lib/api.ts +++ b/packages/bitcore-wallet-client/src/lib/api.ts @@ -3161,6 +3161,98 @@ export class API extends EventEmitter { } } + /** + * Get ERC20 token allowance for a given owner/spender pair + */ + async getTokenAllowance( + opts: { + /** EVM chain name (e.g. 'eth', 'matic') */ + chain: string; + /** Network name (e.g. 'mainnet', 'sepolia') */ + network: string; + /** Token contract address */ + tokenAddress: string; + /** Token owner address */ + ownerAddress: string; + /** Spender contract address */ + spenderAddress: string; + } + ) { + $.checkArgument(opts?.chain, 'Missing argument: chain at '); + $.checkArgument(opts?.network, 'Missing argument: network at '); + $.checkArgument(opts?.tokenAddress, 'Missing argument: tokenAddress at '); + $.checkArgument(opts?.ownerAddress, 'Missing argument: ownerAddress at '); + $.checkArgument(opts?.spenderAddress, 'Missing argument: spenderAddress at '); + const { body: allowance } = await this.request.post('/v1/token/allowance', opts); + return allowance; + } + + /** + * Get Aave user account data (health factor, collateral, debt, etc.) + */ + async getAaveUserAccountData( + opts: { + /** EVM chain name (e.g. 'eth', 'matic') */ + chain: string; + /** Network name (e.g. 'mainnet', 'sepolia') */ + network: string; + /** User wallet address */ + address: string; + /** Aave protocol version. Default: 'v3' */ + version?: string; + } + ) { + $.checkArgument(opts?.chain, 'Missing argument: chain at '); + $.checkArgument(opts?.network, 'Missing argument: network at '); + $.checkArgument(opts?.address, 'Missing argument: address at '); + const { body: accountData } = await this.request.post('/v1/service/aave/userAccountData', opts); + return accountData; + } + + /** + * Get Aave reserve data (variable borrow rate, etc.) + */ + async getAaveReserveData( + opts: { + /** EVM chain name (e.g. 'eth', 'matic') */ + chain: string; + /** Network name (e.g. 'mainnet', 'sepolia') */ + network: string; + /** Reserve asset token address */ + asset: string; + /** Aave protocol version. Default: 'v3' */ + version?: string; + } + ) { + $.checkArgument(opts?.chain, 'Missing argument: chain at '); + $.checkArgument(opts?.network, 'Missing argument: network at '); + $.checkArgument(opts?.asset, 'Missing argument: asset at '); + const { body: reserveData } = await this.request.post('/v1/service/aave/reserveData', opts); + return reserveData; + } + + /** + * Get Aave reserve token addresses (aToken, variableDebtToken, etc.) + */ + async getAaveReserveTokensAddresses( + opts: { + /** EVM chain name (e.g. 'eth', 'matic') */ + chain: string; + /** Network name (e.g. 'mainnet', 'sepolia') */ + network: string; + /** Reserve asset token address */ + asset: string; + /** Aave protocol version. Default: 'v3' */ + version?: string; + } + ) { + $.checkArgument(opts?.chain, 'Missing argument: chain at '); + $.checkArgument(opts?.network, 'Missing argument: network at '); + $.checkArgument(opts?.asset, 'Missing argument: asset at '); + const { body: tokensAddresses } = await this.request.post('/v1/service/aave/reserveTokensAddresses', opts); + return tokensAddresses; + } + /** * Get wallet status based on a string identifier */ diff --git a/packages/bitcore-wallet-client/test/api.test.ts b/packages/bitcore-wallet-client/test/api.test.ts index 13286598f07..36f1d0333f4 100644 --- a/packages/bitcore-wallet-client/test/api.test.ts +++ b/packages/bitcore-wallet-client/test/api.test.ts @@ -9023,4 +9023,93 @@ describe('client API', function() { }); }); }); + + describe('Aave service', () => { + beforeEach(function(done) { + helpers.createAndJoinWallet(clients, keys, 1, 1, { coin: 'eth', chain: 'eth' }, () => { + done(); + }); + }); + + describe('#getAaveUserAccountData', () => { + it('should get aave user account data', async function() { + const accountData: any = await clients[0].getAaveUserAccountData({ + chain: 'eth', network: 'mainnet', address: '0x123', version: 'v3' + }); + should.exist(accountData); + accountData.totalCollateralBase.should.equal('1000'); + accountData.totalDebtBase.should.equal('500'); + accountData.availableBorrowsBase.should.equal('200'); + accountData.currentLiquidationThreshold.should.equal('8000'); + accountData.ltv.should.equal('7500'); + accountData.healthFactor.should.equal('2.0'); + }); + + it('should fail with missing arguments', async function() { + try { + await clients[0].getAaveUserAccountData({ network: 'mainnet' } as any); + should.fail('should have thrown'); + } catch (err) { + err.message.should.equal('Missing argument: chain at '); + } + }); + }); + + describe('#getAaveReserveData', () => { + it('should get aave reserve data', async function() { + const reserveData: any = await clients[0].getAaveReserveData({ + chain: 'eth', network: 'mainnet', asset: '0xabc', version: 'v3' + }); + should.exist(reserveData); + reserveData.currentVariableBorrowRate.should.equal('35000000000000000000000000'); + }); + + it('should fail with missing arguments', async function() { + try { + await clients[0].getAaveReserveData({ network: 'mainnet' } as any); + should.fail('should have thrown'); + } catch (err) { + err.message.should.equal('Missing argument: chain at '); + } + }); + }); + + describe('#getAaveReserveTokensAddresses', () => { + it('should get aave reserve tokens addresses', async function() { + const tokensAddresses: any = await clients[0].getAaveReserveTokensAddresses({ + chain: 'eth', network: 'mainnet', asset: '0xabc', version: 'v3' + }); + should.exist(tokensAddresses); + tokensAddresses.variableDebtTokenAddress.should.equal('0xdef456'); + }); + + it('should fail with missing arguments', async function() { + try { + await clients[0].getAaveReserveTokensAddresses({ network: 'mainnet' } as any); + should.fail('should have thrown'); + } catch (err) { + err.message.should.equal('Missing argument: chain at '); + } + }); + }); + + describe('#getTokenAllowance', () => { + it('should get token allowance', async function() { + const allowance = await clients[0].getTokenAllowance({ + chain: 'eth', network: 'mainnet', tokenAddress: '0xtoken', ownerAddress: '0xowner', spenderAddress: '0xspender' + }); + should.exist(allowance); + allowance.should.equal(5000000); + }); + + it('should fail with missing arguments', async function() { + try { + await clients[0].getTokenAllowance({ network: 'mainnet' } as any); + should.fail('should have thrown'); + } catch (err) { + err.message.should.equal('Missing argument: chain at '); + } + }); + }); + }); }); diff --git a/packages/bitcore-wallet-client/test/helpers.ts b/packages/bitcore-wallet-client/test/helpers.ts index 48d60366470..ecedd79b8ae 100644 --- a/packages/bitcore-wallet-client/test/helpers.ts +++ b/packages/bitcore-wallet-client/test/helpers.ts @@ -346,6 +346,18 @@ export const blockchainExplorerMock = { getTransactionCount: (addr, cb) => { return cb(null, 0); }, + getAaveUserAccountData: (opts, cb) => { + return cb(null, { totalCollateralBase: '1000', totalDebtBase: '500', availableBorrowsBase: '200', currentLiquidationThreshold: '8000', ltv: '7500', healthFactor: '2.0' }); + }, + getAaveReserveData: (opts, cb) => { + return cb(null, { currentVariableBorrowRate: '35000000000000000000000000' }); + }, + getAaveReserveTokensAddresses: (opts, cb) => { + return cb(null, { variableDebtTokenAddress: '0xdef456' }); + }, + getTokenAllowance: (opts, cb) => { + return cb(null, 5000000); + }, reset: () => { blockchainExplorerMock.utxos = []; blockchainExplorerMock.txHistory = []; diff --git a/packages/bitcore-wallet-service/src/lib/expressapp.ts b/packages/bitcore-wallet-service/src/lib/expressapp.ts index b4222319664..4a2651bd58f 100644 --- a/packages/bitcore-wallet-service/src/lib/expressapp.ts +++ b/packages/bitcore-wallet-service/src/lib/expressapp.ts @@ -974,15 +974,14 @@ export class ExpressApp { }); }); - router.post('/v1/token/allowance', (req, res) => { - getServerWithAuth(req, res, async server => { - try { - const allowance = await server.getTokenAllowance(req.body); - res.json(allowance); - } catch (err) { - returnError(err, res, req); - } - }); + router.post('/v1/token/allowance', async (req, res) => { + try { + const server = getServer(req, res); + const allowance = await server.getTokenAllowance(req.body); + res.json(allowance); + } catch (err) { + returnError(err, res, req); + } }); router.get('/v1/sendmaxinfo/', (req, res) => { @@ -2403,7 +2402,7 @@ export class ExpressApp { }); /** Imported routes */ - router.use(new AaveRouter({ returnError, getServerWithAuth }).router); + router.use(new AaveRouter({ returnError, getServer }).router); router.use(new TssRouter({ returnError, opts }).router); diff --git a/packages/bitcore-wallet-service/src/lib/routes/aave.ts b/packages/bitcore-wallet-service/src/lib/routes/aave.ts index 77334384725..ba8292c52cf 100644 --- a/packages/bitcore-wallet-service/src/lib/routes/aave.ts +++ b/packages/bitcore-wallet-service/src/lib/routes/aave.ts @@ -3,47 +3,44 @@ import * as Types from '../../types/expressapp'; interface AaveRouterOpts { returnError: Types.ReturnErrorFn; - getServerWithAuth: Types.GetServerWithAuthFn; + getServer: Types.GetServerFn; } export class AaveRouter { router: express.Router; constructor(params: AaveRouterOpts) { - const { returnError, getServerWithAuth } = params; + const { returnError, getServer } = params; const router = express.Router(); - router.post('/v1/service/aave/userAccountData', (req, res) => { - getServerWithAuth(req, res, async server => { - try { - const accountData = await server.getAaveUserAccountData(req.body); - res.json(accountData); - } catch (err) { - returnError(err, res, req); - } - }); + router.post('/v1/service/aave/userAccountData', async (req, res) => { + try { + const server = getServer(req, res); + const accountData = await server.getAaveUserAccountData(req.body); + res.json(accountData); + } catch (err) { + returnError(err, res, req); + } }); - router.post('/v1/service/aave/reserveData', (req, res) => { - getServerWithAuth(req, res, async server => { - try { - const reserveData = await server.getAaveReserveData(req.body); - res.json(reserveData); - } catch (err) { - returnError(err, res, req); - } - }); + router.post('/v1/service/aave/reserveData', async (req, res) => { + try { + const server = getServer(req, res); + const reserveData = await server.getAaveReserveData(req.body); + res.json(reserveData); + } catch (err) { + returnError(err, res, req); + } }); - router.post('/v1/service/aave/reserveTokensAddresses', (req, res) => { - getServerWithAuth(req, res, async server => { - try { - const tokensAddresses = await server.getAaveReserveTokensAddresses(req.body); - res.json(tokensAddresses); - } catch (err) { - returnError(err, res, req); - } - }); + router.post('/v1/service/aave/reserveTokensAddresses', async (req, res) => { + try { + const server = getServer(req, res); + const tokensAddresses = await server.getAaveReserveTokensAddresses(req.body); + res.json(tokensAddresses); + } catch (err) { + returnError(err, res, req); + } }); this.router = router; diff --git a/packages/bitcore-wallet-service/src/lib/server.ts b/packages/bitcore-wallet-service/src/lib/server.ts index 16614c71211..0dd18fda06c 100644 --- a/packages/bitcore-wallet-service/src/lib/server.ts +++ b/packages/bitcore-wallet-service/src/lib/server.ts @@ -2559,8 +2559,21 @@ export class WalletService implements IWalletService { }); } + /** + * Get ERC20 token allowance for a given owner/spender pair. + * @param {Object} opts + * @param {string} opts.chain - EVM chain name (e.g. 'eth', 'matic') + * @param {string} opts.network - Network name (e.g. 'mainnet', 'sepolia') + * @param {string} opts.tokenAddress - Token contract address + * @param {string} opts.ownerAddress - Token owner address + * @param {string} opts.spenderAddress - Spender contract address + * @returns {Promise} Token allowance amount + */ getTokenAllowance(opts) { - const bc = this._getBlockchainExplorer(opts.chain || Defaults.EVM_CHAIN, opts.network); + if (!checkRequired(opts, ['chain', 'network', 'tokenAddress', 'ownerAddress', 'spenderAddress'])) { + return Promise.reject(new ClientError('getTokenAllowance request missing arguments')); + } + const bc = this._getBlockchainExplorer(opts.chain, opts.network); return new Promise((resolve, reject) => { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.getTokenAllowance(opts, (err, allowance) => { @@ -2573,8 +2586,23 @@ export class WalletService implements IWalletService { }); } + /** + * Get Aave user account data (health factor, collateral, debt, etc.). + * @param {Object} opts + * @param {string} opts.chain - EVM chain name (e.g. 'eth', 'matic') + * @param {string} opts.network - Network name (e.g. 'mainnet', 'sepolia') + * @param {string} opts.address - User wallet address + * @param {string} [opts.version='v3'] - Aave protocol version ('v2' or 'v3') + * @returns {Promise} Aave account data (totalCollateralBase, totalDebtBase, healthFactor, etc.) + */ getAaveUserAccountData(opts) { - const bc = this._getBlockchainExplorer(opts.chain || Defaults.EVM_CHAIN, opts.network); + if (!checkRequired(opts, ['chain', 'network', 'address'])) { + return Promise.reject(new ClientError('getAaveUserAccountData request missing arguments')); + } + if (opts.version && !['v2', 'v3'].includes(opts.version)) { + return Promise.reject(new ClientError('Invalid Aave version. Supported: v2, v3')); + } + const bc = this._getBlockchainExplorer(opts.chain, opts.network); return new Promise((resolve, reject) => { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.getAaveUserAccountData(opts, (err, accountData) => { @@ -2587,8 +2615,23 @@ export class WalletService implements IWalletService { }); } + /** + * Get Aave reserve data (variable borrow rate, etc.). + * @param {Object} opts + * @param {string} opts.chain - EVM chain name (e.g. 'eth', 'matic') + * @param {string} opts.network - Network name (e.g. 'mainnet', 'sepolia') + * @param {string} opts.asset - Reserve asset token address + * @param {string} [opts.version='v3'] - Aave protocol version ('v2' or 'v3') + * @returns {Promise} Aave reserve data (currentVariableBorrowRate, etc.) + */ getAaveReserveData(opts) { - const bc = this._getBlockchainExplorer(opts.chain || Defaults.EVM_CHAIN, opts.network); + if (!checkRequired(opts, ['chain', 'network', 'asset'])) { + return Promise.reject(new ClientError('getAaveReserveData request missing arguments')); + } + if (opts.version && !['v2', 'v3'].includes(opts.version)) { + return Promise.reject(new ClientError('Invalid Aave version. Supported: v2, v3')); + } + const bc = this._getBlockchainExplorer(opts.chain, opts.network); return new Promise((resolve, reject) => { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.getAaveReserveData(opts, (err, reserveData) => { @@ -2601,8 +2644,23 @@ export class WalletService implements IWalletService { }); } + /** + * Get Aave reserve token addresses (aToken, variableDebtToken, etc.). + * @param {Object} opts + * @param {string} opts.chain - EVM chain name (e.g. 'eth', 'matic') + * @param {string} opts.network - Network name (e.g. 'mainnet', 'sepolia') + * @param {string} opts.asset - Reserve asset token address + * @param {string} [opts.version='v3'] - Aave protocol version ('v2' or 'v3') + * @returns {Promise} Aave reserve token addresses (variableDebtTokenAddress, etc.) + */ getAaveReserveTokensAddresses(opts) { - const bc = this._getBlockchainExplorer(opts.chain || Defaults.EVM_CHAIN, opts.network); + if (!checkRequired(opts, ['chain', 'network', 'asset'])) { + return Promise.reject(new ClientError('getAaveReserveTokensAddresses request missing arguments')); + } + if (opts.version && !['v2', 'v3'].includes(opts.version)) { + return Promise.reject(new ClientError('Invalid Aave version. Supported: v2, v3')); + } + const bc = this._getBlockchainExplorer(opts.chain, opts.network); return new Promise((resolve, reject) => { if (!bc) return reject(new Error('Could not get blockchain explorer instance')); bc.getAaveReserveTokensAddresses(opts, (err, tokensAddresses) => { diff --git a/packages/bitcore-wallet-service/test/expressapp.test.ts b/packages/bitcore-wallet-service/test/expressapp.test.ts index f341cf724b0..e2a3bde87f2 100644 --- a/packages/bitcore-wallet-service/test/expressapp.test.ts +++ b/packages/bitcore-wallet-service/test/expressapp.test.ts @@ -10,6 +10,7 @@ import config from '../src/config'; import { Common } from '../src/lib/common'; import { WalletService } from '../src/lib/server'; import { ExpressApp } from '../src/lib/expressapp'; +import { ClientError } from '../src/lib/errors/clienterror'; const Defaults = Common.Defaults; const should = chai.should(); @@ -489,16 +490,12 @@ describe('ExpressApp', function() { getAaveUserAccountData: sinon.stub().resolves({ totalCollateralBase: '1000', healthFactor: '2.0' }), }; sandbox.stub(WalletService, 'initialize').callsArg(1); - sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, null, server); + sandbox.stub(WalletService, 'getInstance').returns(server); start(ExpressApp, function() { const requestOptions = { url: testHost + ':' + testPort + config.basePath + '/v1/service/aave/userAccountData', method: 'post', - json: { chain: 'eth', network: 'mainnet', address: '0x123', version: 'v3' }, - headers: { - 'x-identity': 'identity', - 'x-signature': 'signature' - } + json: { chain: 'eth', network: 'mainnet', address: '0x123', version: 'v3' } }; request(requestOptions, function(err, res, body) { should.not.exist(err); @@ -510,21 +507,37 @@ describe('ExpressApp', function() { }); }); + it('/v1/service/aave/userAccountData should fail with missing arguments', function(done) { + const server = { + getAaveUserAccountData: sinon.stub().rejects(new ClientError('getAaveUserAccountData request missing arguments')), + }; + sandbox.stub(WalletService, 'initialize').callsArg(1); + sandbox.stub(WalletService, 'getInstance').returns(server); + start(ExpressApp, function() { + const requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/service/aave/userAccountData', + method: 'post', + json: { version: 'v3' } + }; + request(requestOptions, function(err, res) { + should.not.exist(err); + res.statusCode.should.equal(400); + done(); + }); + }); + }); + it('/v1/service/aave/reserveData', function(done) { const server = { getAaveReserveData: sinon.stub().resolves({ currentVariableBorrowRate: '35000000' }), }; sandbox.stub(WalletService, 'initialize').callsArg(1); - sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, null, server); + sandbox.stub(WalletService, 'getInstance').returns(server); start(ExpressApp, function() { const requestOptions = { url: testHost + ':' + testPort + config.basePath + '/v1/service/aave/reserveData', method: 'post', - json: { chain: 'eth', network: 'mainnet', asset: '0xabc', version: 'v3' }, - headers: { - 'x-identity': 'identity', - 'x-signature': 'signature' - } + json: { chain: 'eth', network: 'mainnet', asset: '0xabc', version: 'v3' } }; request(requestOptions, function(err, res, body) { should.not.exist(err); @@ -535,21 +548,37 @@ describe('ExpressApp', function() { }); }); + it('/v1/service/aave/reserveData should fail with missing arguments', function(done) { + const server = { + getAaveReserveData: sinon.stub().rejects(new ClientError('getAaveReserveData request missing arguments')), + }; + sandbox.stub(WalletService, 'initialize').callsArg(1); + sandbox.stub(WalletService, 'getInstance').returns(server); + start(ExpressApp, function() { + const requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/service/aave/reserveData', + method: 'post', + json: { version: 'v3' } + }; + request(requestOptions, function(err, res) { + should.not.exist(err); + res.statusCode.should.equal(400); + done(); + }); + }); + }); + it('/v1/service/aave/reserveTokensAddresses', function(done) { const server = { getAaveReserveTokensAddresses: sinon.stub().resolves({ variableDebtTokenAddress: '0xdef' }), }; sandbox.stub(WalletService, 'initialize').callsArg(1); - sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, null, server); + sandbox.stub(WalletService, 'getInstance').returns(server); start(ExpressApp, function() { const requestOptions = { url: testHost + ':' + testPort + config.basePath + '/v1/service/aave/reserveTokensAddresses', method: 'post', - json: { chain: 'eth', network: 'mainnet', asset: '0xabc', version: 'v3' }, - headers: { - 'x-identity': 'identity', - 'x-signature': 'signature' - } + json: { chain: 'eth', network: 'mainnet', asset: '0xabc', version: 'v3' } }; request(requestOptions, function(err, res, body) { should.not.exist(err); @@ -559,6 +588,26 @@ describe('ExpressApp', function() { }); }); }); + + it('/v1/service/aave/reserveTokensAddresses should fail with missing arguments', function(done) { + const server = { + getAaveReserveTokensAddresses: sinon.stub().rejects(new ClientError('getAaveReserveTokensAddresses request missing arguments')), + }; + sandbox.stub(WalletService, 'initialize').callsArg(1); + sandbox.stub(WalletService, 'getInstance').returns(server); + start(ExpressApp, function() { + const requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/service/aave/reserveTokensAddresses', + method: 'post', + json: { version: 'v3' } + }; + request(requestOptions, function(err, res) { + should.not.exist(err); + res.statusCode.should.equal(400); + done(); + }); + }); + }); }); describe('Token allowance', function() { @@ -567,16 +616,12 @@ describe('ExpressApp', function() { getTokenAllowance: sinon.stub().resolves(5000000), }; sandbox.stub(WalletService, 'initialize').callsArg(1); - sandbox.stub(WalletService, 'getInstanceWithAuth').callsArgWith(1, null, server); + sandbox.stub(WalletService, 'getInstance').returns(server); start(ExpressApp, function() { const requestOptions = { url: testHost + ':' + testPort + config.basePath + '/v1/token/allowance', method: 'post', - json: { chain: 'eth', network: 'mainnet', tokenAddress: '0xtoken', ownerAddress: '0xowner', spenderAddress: '0xspender' }, - headers: { - 'x-identity': 'identity', - 'x-signature': 'signature' - } + json: { chain: 'eth', network: 'mainnet', tokenAddress: '0xtoken', ownerAddress: '0xowner', spenderAddress: '0xspender' } }; request(requestOptions, function(err, res, body) { should.not.exist(err); @@ -586,6 +631,26 @@ describe('ExpressApp', function() { }); }); }); + + it('/v1/token/allowance should fail with missing arguments', function(done) { + const server = { + getTokenAllowance: sinon.stub().rejects(new ClientError('getTokenAllowance request missing arguments')), + }; + sandbox.stub(WalletService, 'initialize').callsArg(1); + sandbox.stub(WalletService, 'getInstance').returns(server); + start(ExpressApp, function() { + const requestOptions = { + url: testHost + ':' + testPort + config.basePath + '/v1/token/allowance', + method: 'post', + json: {} + }; + request(requestOptions, function(err, res) { + should.not.exist(err); + res.statusCode.should.equal(400); + done(); + }); + }); + }); }); describe('Clear cache', function() {