diff --git a/packages/wallet-service/serverless.yml b/packages/wallet-service/serverless.yml index d0ab53b9..4daa1e60 100644 --- a/packages/wallet-service/serverless.yml +++ b/packages/wallet-service/serverless.yml @@ -324,10 +324,6 @@ functions: method: get cors: true authorizer: ${self:custom.authorizer.walletBearer} - request: - parameters: - paths: - index: false warmup: walletWarmer: enabled: true diff --git a/packages/wallet-service/src/api/addresses.ts b/packages/wallet-service/src/api/addresses.ts index a9e31839..52c394bc 100644 --- a/packages/wallet-service/src/api/addresses.ts +++ b/packages/wallet-service/src/api/addresses.ts @@ -39,7 +39,7 @@ class AddressAtIndexValidator { index: Joi.number().min(0).optional(), }); - static validate(payload: unknown): { value: AddressAtIndexRequest, error: ValidationError} { + static validate(payload: unknown): { value: AddressAtIndexRequest, error: ValidationError } { return AddressAtIndexValidator.bodySchema.validate(payload, { abortEarly: false, // We want it to return all the errors not only the first convert: true, // We need to convert as parameters are sent on the QueryString @@ -92,7 +92,7 @@ export const checkMine: APIGatewayProxyHandler = middy(walletIdProxyHandler(asyn await closeDbConnection(mysql); - const addressBelongMap = sentAddresses.reduce((acc: {string: boolean}, address: string) => { + const addressBelongMap = sentAddresses.reduce((acc: { string: boolean }, address: string) => { acc[address] = walletAddresses.has(address); return acc; @@ -108,7 +108,7 @@ export const checkMine: APIGatewayProxyHandler = middy(walletIdProxyHandler(asyn })).use(cors()) .use(errorHandler()); -/* +/** * Get the addresses of a wallet, allowing an index filter * Notice: If the index filter is passed, it will only find addresses * that are already in our database, this will not derive new addresses @@ -127,7 +127,7 @@ export const get: APIGatewayProxyHandler = middy( return closeDbAndGetError(mysql, ApiError.WALLET_NOT_READY); } - const { value: body, error } = AddressAtIndexValidator.validate(event.pathParameters || {}); + const { value: body, error } = AddressAtIndexValidator.validate(event.queryStringParameters || {}); if (error) { const details = error.details.map((err) => ({ diff --git a/packages/wallet-service/src/api/healthcheck.ts b/packages/wallet-service/src/api/healthcheck.ts index 30f9b4ec..0d74d69d 100644 --- a/packages/wallet-service/src/api/healthcheck.ts +++ b/packages/wallet-service/src/api/healthcheck.ts @@ -1,11 +1,11 @@ import middy from '@middy/core'; import { - Healthcheck, - HealthcheckInternalComponent, - HealthcheckDatastoreComponent, - HealthcheckHTTPComponent, - HealthcheckCallbackResponse, - HealthcheckStatus, + Healthcheck, + HealthcheckInternalComponent, + HealthcheckDatastoreComponent, + HealthcheckHTTPComponent, + HealthcheckCallbackResponse, + HealthcheckStatus, } from '@hathor/healthcheck-lib'; import { getLatestHeight } from '@src/db'; import fullnode from '@src/fullnode'; diff --git a/packages/wallet-service/src/db/index.ts b/packages/wallet-service/src/db/index.ts index 796d4b49..2eb29cdd 100644 --- a/packages/wallet-service/src/db/index.ts +++ b/packages/wallet-service/src/db/index.ts @@ -426,7 +426,7 @@ export const getWalletAddressDetail = async (mysql: ServerlessMysql, walletId: s FROM \`address\` WHERE \`wallet_id\` = ? AND \`address\` = ?`, - [walletId, address]); + [walletId, address]); if (results.length > 0) { const data = results[0]; @@ -1160,12 +1160,12 @@ export const updateAddressLockedBalance = async ( \`unlocked_authorities\` = (unlocked_authorities | ?) WHERE \`address\` = ? AND \`token_id\` = ?`, [ - tokenBalance.unlockedAmount, - tokenBalance.unlockedAmount, - tokenBalance.unlockedAuthorities.toInteger(), - address, - token, - ], + tokenBalance.unlockedAmount, + tokenBalance.unlockedAmount, + tokenBalance.unlockedAuthorities.toInteger(), + address, + token, + ], ); // if any authority has been unlocked, we have to refresh the locked authorities @@ -1201,7 +1201,7 @@ export const updateAddressLockedBalance = async ( ) WHERE \`address\` = ? AND \`token_id\` = ?`, - [address, token, address, token]); + [address, token, address, token]); } } } @@ -1233,7 +1233,7 @@ export const updateWalletLockedBalance = async ( WHERE \`wallet_id\` = ? AND \`token_id\` = ?`, [tokenBalance.unlockedAmount, tokenBalance.unlockedAmount, - tokenBalance.unlockedAuthorities.toInteger(), walletId, token], + tokenBalance.unlockedAuthorities.toInteger(), walletId, token], ); // if any authority has been unlocked, we have to refresh the locked authorities @@ -1418,7 +1418,7 @@ export const getWalletTokens = async ( ); for (const result of results) { - tokenList.push( result.token_id); + tokenList.push(result.token_id); } return tokenList; @@ -1464,7 +1464,7 @@ LEFT OUTER JOIN transaction ON transaction.tx_id = wallet_tx_history.tx_id ORDER BY wallet_tx_history.timestamp DESC LIMIT ?, ?`, - [walletId, tokenId, skip, count]); + [walletId, tokenId, skip, count]); for (const result of results) { const tx: TxTokenBalance = { @@ -2074,7 +2074,7 @@ export const markUtxosAsVoided = async ( UPDATE \`tx_output\` SET \`voided\` = TRUE WHERE \`tx_id\` IN (?)`, - [txIds]); + [txIds]); }; /** @@ -2812,7 +2812,7 @@ export const existsPushDevice = async ( mysql: ServerlessMysql, deviceId: string, walletId: string, -) : Promise => { +): Promise => { const [{ count }] = await mysql.query( ` SELECT COUNT(1) as \`count\` @@ -2820,7 +2820,7 @@ export const existsPushDevice = async ( WHERE device_id = ? AND wallet_id = ?`, [deviceId, walletId], - ) as unknown as Array<{count}>; + ) as unknown as Array<{ count }>; return count > 0; }; @@ -2840,7 +2840,7 @@ export const registerPushDevice = async ( enablePush: boolean, enableShowAmounts: boolean, }, -) : Promise => { +): Promise => { await mysql.query( ` INSERT @@ -2889,7 +2889,7 @@ export const updatePushDevice = async ( enablePush: boolean, enableShowAmounts: boolean, }, -) : Promise => { +): Promise => { await mysql.query( ` UPDATE \`push_devices\` @@ -2912,7 +2912,7 @@ export const unregisterPushDevice = async ( mysql: ServerlessMysql, deviceId: string, walletId?: string, -) : Promise => { +): Promise => { if (walletId) { await mysql.query( ` @@ -2964,8 +2964,8 @@ export const getTransactionById = async ( WHERE transaction.tx_id = ? AND transaction.voided = FALSE AND wallet_tx_history.wallet_id = ?`, - // eslint-disable-next-line camelcase - [txId, walletId]) as Array<{tx_id, timestamp, version, voided, weight, balance, token_id, name, symbol }>; + // eslint-disable-next-line camelcase + [txId, walletId]) as Array<{ tx_id, timestamp, version, voided, weight, balance, token_id, name, symbol }>; const txTokens = []; result.forEach((eachTxToken) => { @@ -2995,7 +2995,7 @@ export const getTransactionById = async ( export const existsWallet = async ( mysql: ServerlessMysql, walletId: string, -) : Promise => { +): Promise => { const [{ count }] = (await mysql.query( ` SELECT COUNT(1) as \`count\` @@ -3016,15 +3016,15 @@ export const existsWallet = async ( export const getPushDevice = async ( mysql: ServerlessMysql, deviceId: string, -) : Promise => { +): Promise => { const [pushDevice] = await mysql.query( ` SELECT * FROM \`push_devices\` WHERE device_id = ?`, [deviceId], - // eslint-disable-next-line camelcase - ) as Array<{wallet_id, device_id, push_provider, enable_push, enable_show_amounts}>; + // eslint-disable-next-line camelcase + ) as Array<{ wallet_id, device_id, push_provider, enable_push, enable_show_amounts }>; if (!pushDevice) { return null; @@ -3049,7 +3049,7 @@ export const getPushDevice = async ( export const getPushDeviceSettingsList = async ( mysql: ServerlessMysql, walletIdList: string[], -) : Promise => { +): Promise => { const pushDeviceSettingsResult = await mysql.query( ` SELECT wallet_id @@ -3059,8 +3059,8 @@ export const getPushDeviceSettingsList = async ( FROM \`push_devices\` WHERE wallet_id in (?)`, [walletIdList], - // eslint-disable-next-line camelcase - ) as Array<{wallet_id, device_id, enable_push, enable_show_amounts}>; + // eslint-disable-next-line camelcase + ) as Array<{ wallet_id, device_id, enable_push, enable_show_amounts }>; const pushDeviceSettignsList = pushDeviceSettingsResult.map((each) => ({ walletId: each.wallet_id, @@ -3121,7 +3121,7 @@ export const getTokenSymbols = async ( ); if (results.length === 0) return null; - return results.reduce((prev: Record, token: { id: string, symbol: string}) => { + return results.reduce((prev: Record, token: { id: string, symbol: string }) => { // eslint-disable-next-line no-param-reassign prev[token.id] = token.symbol; return prev; @@ -3176,7 +3176,7 @@ export const getAddressAtIndex = async ( WHERE \`index\` = ? AND \`wallet_id\` = ? LIMIT 1`, - [walletId, index], + [index, walletId], ); if (addresses.length <= 0) { diff --git a/packages/wallet-service/tests/api.test.ts b/packages/wallet-service/tests/api.test.ts index ca17a30c..41098c59 100644 --- a/packages/wallet-service/tests/api.test.ts +++ b/packages/wallet-service/tests/api.test.ts @@ -188,8 +188,8 @@ test('GET /addresses', async () => { expect(result.statusCode).toBe(STATUS_CODE_TABLE[ApiError.INVALID_PAYLOAD]); expect(returnBody.details).toHaveLength(1); - expect(returnBody.details[0].message) - .toMatchInlineSnapshot('"\"index\" must be greater than or equal to 0"'); + expect(returnBody.details[0].message.trim()) + .toMatchInlineSnapshot(`""index" must be greater than or equal to 0"`); // we should be able to filter for a specific index event = makeGatewayEventWithAuthorizer('my-wallet', { @@ -1681,7 +1681,7 @@ test('GET /version', async () => { genesis_block_hash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', genesis_tx1_hash: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', genesis_tx2_hash: 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', - native_token: { name: 'Hathor', symbol: 'HTR'}, + native_token: { name: 'Hathor', symbol: 'HTR' }, }; const returnData = convertApiVersionData(mockData); @@ -2264,3 +2264,86 @@ describe('GET /health', () => { }); }); }); + +test('GET /addresses (query string index parameter)', async () => { + expect.hasAssertions(); + + await addToWalletTable(mysql, [{ + id: 'my-wallet', + xpubkey: 'xpubkey', + authXpubkey: 'auth_xpubkey', + status: 'ready', + maxGap: 5, + createdAt: 10000, + readyAt: 10001, + }]); + + const addresses = [ + { address: ADDRESSES[0], index: 0, walletId: 'my-wallet', transactions: 0 }, + { address: ADDRESSES[1], index: 1, walletId: 'my-wallet', transactions: 0 }, + ]; + + await addToAddressTable(mysql, addresses); + + // 1. No index parameter (should return all addresses) + let event = makeGatewayEventWithAuthorizer('my-wallet', null); + let result = await addressesGet(event, null, null) as APIGatewayProxyResult; + let returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.addresses).toHaveLength(2); + expect(returnBody.addresses).toContainEqual({ + address: addresses[0].address, + index: addresses[0].index, + transactions: addresses[0].transactions, + }); + expect(returnBody.addresses).toContainEqual({ + address: addresses[1].address, + index: addresses[1].index, + transactions: addresses[1].transactions, + }); + + // 2. index as a string (should return the address at that index) + event = makeGatewayEventWithAuthorizer('my-wallet', { index: '1' }); + result = await addressesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.addresses).toHaveLength(1); + expect(returnBody.addresses[0].index).toBe(1); + + // 3. index as a number (should return the address at that index) + event = makeGatewayEventWithAuthorizer('my-wallet', { index: '0' }); + result = await addressesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(200); + expect(returnBody.success).toBe(true); + expect(returnBody.addresses).toHaveLength(1); + expect(returnBody.addresses[0].index).toBe(0); + + // 4. index is invalid (non-numeric) + event = makeGatewayEventWithAuthorizer('my-wallet', { index: 'not-a-number' }); + result = await addressesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + expect(returnBody.details[0].message).toMatch(/must be a number/); + + // 5. index is negative (should error) + event = makeGatewayEventWithAuthorizer('my-wallet', { index: '-1' }); + result = await addressesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.INVALID_PAYLOAD); + expect(returnBody.details[0].message).toMatch(/greater than or equal to 0/); + + // 6. index not found (should return ADDRESS_NOT_FOUND) + event = makeGatewayEventWithAuthorizer('my-wallet', { index: '999' }); + result = await addressesGet(event, null, null) as APIGatewayProxyResult; + returnBody = JSON.parse(result.body as string); + expect(result.statusCode).toBe(404); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe(ApiError.ADDRESS_NOT_FOUND); +});