diff --git a/packages/wallet-service/src/api/tokens.ts b/packages/wallet-service/src/api/tokens.ts index 2325a5ae..fc982f39 100644 --- a/packages/wallet-service/src/api/tokens.ts +++ b/packages/wallet-service/src/api/tokens.ts @@ -3,19 +3,15 @@ import 'source-map-support/register'; import { walletIdProxyHandler } from '@src/commons'; import { getWalletTokens, - getTotalSupply, - getTotalTransactions, - getTokenInformation, - getAuthorityUtxo, } from '@src/db'; import { - TokenInfo, -} from '@src/types'; -import { getDbConnection } from '@src/utils'; + closeDbConnection, + getDbConnection, +} from '@src/utils'; import { ApiError } from '@src/api/errors'; import { closeDbAndGetError, warmupMiddleware, txIdJoiValidator } from '@src/api/utils'; +import fullnode from '@src/fullnode'; import Joi from 'joi'; -import { bigIntUtils, constants } from '@hathor/wallet-lib'; import middy from '@middy/core'; import cors from '@middy/http-cors'; import errorHandler from '@src/api/middlewares/errorHandler'; @@ -49,8 +45,9 @@ const getTokenDetailsParamsSchema = Joi.object({ * Get token details * * This lambda is called by API Gateway on GET /wallet/tokens/:token_id/details + * It proxies the request to the fullnode's thin_wallet/token API */ -export const getTokenDetails = middy(walletIdProxyHandler(async (walletId, event) => { +export const getTokenDetails = middy(walletIdProxyHandler(async (_walletId, event) => { const params = event.pathParameters || {}; const { value, error } = getTokenDetailsParamsSchema.validate(params, { @@ -68,51 +65,44 @@ export const getTokenDetails = middy(walletIdProxyHandler(async (walletId, event } const tokenId = value.token_id; - const tokenInfo: TokenInfo = await getTokenInformation(mysql, tokenId); - if (tokenId === constants.NATIVE_TOKEN_UID) { - const details = [{ - message: 'Invalid tokenId', - }]; - - return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details }); - } - - if (!tokenInfo) { - const details = [{ - message: 'Token not found', - }]; - - return closeDbAndGetError(mysql, ApiError.TOKEN_NOT_FOUND, { details }); - } - - const [ - totalSupply, - totalTransactions, - meltAuthority, - mintAuthority, - ] = await Promise.all([ - getTotalSupply(mysql, tokenId), - getTotalTransactions(mysql, tokenId), - getAuthorityUtxo(mysql, tokenId, Number(constants.TOKEN_MELT_MASK)), - getAuthorityUtxo(mysql, tokenId, Number(constants.TOKEN_MINT_MASK)), - ]); - - return { - statusCode: 200, - body: bigIntUtils.JSONBigInt.stringify({ - success: true, - details: { - tokenInfo, - totalSupply, - totalTransactions, - authorities: { - mint: mintAuthority !== null, - melt: meltAuthority !== null, + try { + const data = await fullnode.getTokenDetails(tokenId); + + if (!data?.success) { + return { + statusCode: 404, + body: JSON.stringify({ + success: false, + error: ApiError.TOKEN_NOT_FOUND, + details: [{ message: 'Token not found' }], + }), + }; + } + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + details: { + tokenInfo: { + id: tokenId, + name: data.name, + symbol: data.symbol, + version: data.version, + }, + totalSupply: data.total, + totalTransactions: data.transactions_count, + authorities: { + mint: data.can_mint, + melt: data.can_melt, + }, }, - }, - }), - }; + }), + }; + } finally { + await closeDbConnection(mysql); + } })).use(cors()) .use(warmupMiddleware()) .use(errorHandler()); diff --git a/packages/wallet-service/src/fullnode.ts b/packages/wallet-service/src/fullnode.ts index 1eb97f69..7ddac1d6 100644 --- a/packages/wallet-service/src/fullnode.ts +++ b/packages/wallet-service/src/fullnode.ts @@ -118,6 +118,16 @@ export const create = (baseURL = BASE_URL) => { return response.data; } + const getTokenDetails = async (tokenId: string) => { + const response = await api.get('thin_wallet/token', { + data: null, + params: { id: tokenId }, + headers: { 'content-type': 'application/json' }, + }); + + return response.data; + } + return { api, // exported so we can mock it on the tests version, @@ -129,6 +139,7 @@ export const create = (baseURL = BASE_URL) => { getNCState, getNCHistory, getNCBlueprintInfo, + getTokenDetails, }; }; diff --git a/packages/wallet-service/tests/api.test.ts b/packages/wallet-service/tests/api.test.ts index 5e8f4a88..0abd7a7f 100644 --- a/packages/wallet-service/tests/api.test.ts +++ b/packages/wallet-service/tests/api.test.ts @@ -1472,6 +1472,25 @@ test('GET /wallet/tokens/token_id/details', async () => { // check CORS headers await _testCORSHeaders(getTokenDetails, null, null); + const mockFullnodeData = { + name: 'MyToken1', + symbol: 'MT1', + version: 1, + success: true, + mint: [ + { tx_id: 'txId', index: 2 }, + ], + melt: [], + can_mint: true, + can_melt: false, + total: 100, + transactions_count: 1, + }; + + const spy = jest.spyOn(fullnode, 'getTokenDetails'); + const mockFullnodeResponse = jest.fn(() => Promise.resolve(mockFullnodeData)); + spy.mockImplementation(mockFullnodeResponse); + await addToWalletTable(mysql, [{ id: 'my-wallet', xpubkey: 'xpubkey', @@ -1482,158 +1501,68 @@ test('GET /wallet/tokens/token_id/details', async () => { readyAt: 10001, }]); - let event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: TX_IDS[0] }); + // Missing token_id should return validation error + let event = makeGatewayEventWithAuthorizer('my-wallet', null); let result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; let returnBody = JSON.parse(result.body as string); - expect(result.statusCode).toBe(404); - expect(returnBody.success).toBe(false); - expect(returnBody.details[0]).toStrictEqual({ message: 'Token not found' }); - - event = makeGatewayEventWithAuthorizer('my-wallet', null); - result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; - returnBody = JSON.parse(result.body as string); - expect(result.statusCode).toBe(400); expect(returnBody.success).toBe(false); expect(returnBody.details[0]).toStrictEqual({ message: '"token_id" is required', path: ['token_id'] }); - // add tokens - const token1 = { id: TX_IDS[1], name: 'MyToken1', symbol: 'MT1', version: TokenVersion.DEPOSIT }; - const token2 = { id: TX_IDS[2], name: 'MyToken2', symbol: 'MT2', version: TokenVersion.DEPOSIT }; - - await addToTokenTable(mysql, [ - { id: token1.id, name: token1.name, symbol: token1.symbol, version: TokenVersion.DEPOSIT, transactions: 0 }, - { id: token2.id, name: token2.name, symbol: token2.symbol, version: TokenVersion.DEPOSIT, transactions: 0 }, - ]); - - await addToUtxoTable(mysql, [{ - // Total tokens created - txId: 'txId', - index: 0, - tokenId: token1.id, - address: ADDRESSES[0], - value: 100n, - authorities: 0, - timelock: null, - heightlock: null, - locked: false, - spentBy: null, - }, { - // Mint UTXO: - txId: 'txId', - index: 1, - tokenId: token1.id, - address: ADDRESSES[0], - value: 0n, - authorities: Number(constants.TOKEN_MINT_MASK), - timelock: null, - heightlock: null, - locked: false, - spentBy: null, - }, { - // Another Mint UTXO - txId: 'txId', - index: 2, - tokenId: token1.id, - address: ADDRESSES[0], - value: 0n, - authorities: Number(constants.TOKEN_MINT_MASK), - timelock: null, - heightlock: null, - locked: false, - spentBy: null, - }, { - // Total tokens created - txId: 'txId2', - index: 0, - tokenId: token2.id, - address: ADDRESSES[0], - value: 250n, - authorities: 0, - timelock: null, - heightlock: null, - locked: true, - spentBy: null, - }, { - // Locked utxo - txId: 'txId2', - index: 1, - tokenId: token2.id, - address: ADDRESSES[0], - value: 0n, - authorities: Number(constants.TOKEN_MINT_MASK), - timelock: 1000, - heightlock: null, - locked: true, - spentBy: null, - }, { - // Spent utxo - txId: 'txId2', - index: 2, - tokenId: token2.id, - address: ADDRESSES[0], - value: 0n, - authorities: Number(constants.TOKEN_MINT_MASK), - timelock: 1000, - heightlock: null, - locked: true, - spentBy: 'txid2', - }, { - txId: 'txId3', - index: 0, - tokenId: token2.id, - address: ADDRESSES[0], - value: 0n, - authorities: Number(constants.TOKEN_MINT_MASK), - timelock: null, - heightlock: null, - locked: false, - spentBy: null, - }, { - // Melt UTXO - txId: 'txId3', - index: 1, - tokenId: token2.id, - address: ADDRESSES[0], - value: 0n, - authorities: Number(constants.TOKEN_MELT_MASK), - timelock: null, - heightlock: null, - locked: false, - spentBy: null, - }]); - - await addToAddressTxHistoryTable(mysql, [ - { address: ADDRESSES[0], txId: 'txId', tokenId: token1.id, balance: 100n, timestamp: 0 }, - { address: ADDRESSES[0], txId: 'txId2', tokenId: token2.id, balance: 250n, timestamp: 0 }, - { address: ADDRESSES[0], txId: 'txId3', tokenId: token2.id, balance: 0n, timestamp: 0 }, - ]); - - event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: token1.id }); + // Valid token_id should proxy to fullnode and return in the old response format + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: TX_IDS[0] }); result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; returnBody = JSON.parse(result.body as string); expect(result.statusCode).toBe(200); expect(returnBody.success).toBe(true); + expect(returnBody.details.tokenInfo).toStrictEqual({ + id: TX_IDS[0], + name: 'MyToken1', + symbol: 'MT1', + version: 1, + }); expect(returnBody.details.totalSupply).toStrictEqual(100); expect(returnBody.details.totalTransactions).toStrictEqual(1); expect(returnBody.details.authorities.mint).toStrictEqual(true); expect(returnBody.details.authorities.melt).toStrictEqual(false); - expect(returnBody.details.tokenInfo).toStrictEqual(token1); + expect(mockFullnodeResponse).toHaveBeenNthCalledWith(1, TX_IDS[0]); + + // Test with a token that has both mint and melt authorities + const mockFullnodeData2 = { + name: 'MyToken2', + symbol: 'MT2', + version: 1, + success: true, + mint: [{ tx_id: 'txId2', index: 1 }], + melt: [{ tx_id: 'txId2', index: 2 }], + can_mint: true, + can_melt: true, + total: 250, + transactions_count: 2, + }; + mockFullnodeResponse.mockResolvedValue(mockFullnodeData2); - event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: token2.id }); + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: TX_IDS[1] }); result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; returnBody = JSON.parse(result.body as string); expect(result.statusCode).toBe(200); expect(returnBody.success).toBe(true); + expect(returnBody.details.tokenInfo).toStrictEqual({ + id: TX_IDS[1], + name: 'MyToken2', + symbol: 'MT2', + version: 1, + }); expect(returnBody.details.totalSupply).toStrictEqual(250); expect(returnBody.details.totalTransactions).toStrictEqual(2); expect(returnBody.details.authorities.mint).toStrictEqual(true); expect(returnBody.details.authorities.melt).toStrictEqual(true); - expect(returnBody.details.tokenInfo).toStrictEqual(token2); + expect(mockFullnodeResponse).toHaveBeenNthCalledWith(2, TX_IDS[1]); + // Short token_id should fail validation event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: constants.NATIVE_TOKEN_UID }); result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; returnBody = JSON.parse(result.body as string); @@ -1651,21 +1580,7 @@ test('GET /wallet/tokens/token_id/details', async () => { ] `); - const oldHathorTokenConfig = constants.NATIVE_TOKEN_UID; - - // @ts-ignore - constants.NATIVE_TOKEN_UID = TX_IDS[4]; - - event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: constants.NATIVE_TOKEN_UID }); - result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; - returnBody = JSON.parse(result.body as string); - - expect(result.statusCode).toBe(400); - expect(returnBody.success).toBe(false); - expect(returnBody.details).toStrictEqual([{ message: 'Invalid tokenId' }]); - - // @ts-ignore - constants.NATIVE_TOKEN_UID = oldHathorTokenConfig; + spy.mockRestore(); }); test('GET /wallet/utxos', async () => {