diff --git a/packages/common/__tests__/utils/nft.utils.test.ts b/packages/common/__tests__/utils/nft.utils.test.ts index 6b52f4b9..99f2e5e2 100644 --- a/packages/common/__tests__/utils/nft.utils.test.ts +++ b/packages/common/__tests__/utils/nft.utils.test.ts @@ -15,7 +15,7 @@ jest.mock('winston', () => { error = jest.fn(); info = jest.fn(); debug = jest.fn(); - }; + } return { Logger: FakeLogger, diff --git a/packages/common/__tests__/utils/wallet.utils.test.ts b/packages/common/__tests__/utils/wallet.utils.test.ts new file mode 100644 index 00000000..6316ec65 --- /dev/null +++ b/packages/common/__tests__/utils/wallet.utils.test.ts @@ -0,0 +1,29 @@ +import { isDecodedValid } from '@src/utils/wallet.utils'; + +describe('walletUtils', () => { + it('should validate common invalid inputs', () => { + expect.hasAssertions(); + + expect(isDecodedValid({})).toBeFalsy(); + expect(isDecodedValid(false)).toBeFalsy(); + expect(isDecodedValid(null)).toBeFalsy(); + expect(isDecodedValid(undefined)).toBeFalsy(); + expect(isDecodedValid({ + address: 'addr1', + type: 'PPK', + })).toBeTruthy(); + }); + + it('should validate requiredKeys', () => { + expect.hasAssertions(); + + expect(isDecodedValid({ + address: 'addr1', + type: 'PPK', + }, ['address', 'type'])).toBeTruthy(); + + expect(isDecodedValid({ + address: 'addr1', + }, ['address', 'type'])).toBeFalsy(); + }); +}); diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 99d7571e..55521504 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -11,7 +11,7 @@ */ import { constants } from '@hathor/wallet-lib'; -import { isAuthority } from './utils/wallet.utils'; +import { isAuthority, isDecodedValid } from './utils/wallet.utils'; export interface StringMap { [x: string]: T; @@ -376,7 +376,7 @@ export class TokenBalanceMap { * @returns The TokenBalanceMap object */ static fromTxOutput(output: TxOutput): TokenBalanceMap { - if (!output.decoded) { + if (!isDecodedValid(output.decoded)) { throw new Error('Output has no decoded script'); } const token = output.token; diff --git a/packages/common/src/utils/alerting.utils.ts b/packages/common/src/utils/alerting.utils.ts index 213642de..46e9dfc6 100644 --- a/packages/common/src/utils/alerting.utils.ts +++ b/packages/common/src/utils/alerting.utils.ts @@ -61,7 +61,7 @@ export const addAlert = async ( try { await client.send(command); - } catch(err) { + } catch (err) { logger.error('[ALERT] Erroed while sending message to the alert sqs queue', err); } }; diff --git a/packages/common/src/utils/wallet.utils.ts b/packages/common/src/utils/wallet.utils.ts index e0ea9ea9..e2edf273 100644 --- a/packages/common/src/utils/wallet.utils.ts +++ b/packages/common/src/utils/wallet.utils.ts @@ -17,3 +17,19 @@ import { constants } from '@hathor/wallet-lib'; export const isAuthority = (tokenData: number): boolean => ( (tokenData & constants.TOKEN_AUTHORITY_MASK) > 0 ); + +/** + * Checks if a decoded output object is valid (not null, undefined or empty object). + * + * @param decoded - The decoded output object to check + * @param requiredKeys - A list of keys to check + * @returns true if the decoded object is valid, false otherwise + */ +export const isDecodedValid = (decoded: any, requiredKeys: string[] = []): boolean => { + return (decoded != null + && typeof decoded === 'object' + && Object.keys(decoded).length > 0) + && requiredKeys.reduce((state, key: string) => ( + state && decoded[key] != null + ), true); +}; diff --git a/packages/daemon/__tests__/integration/balances.test.ts b/packages/daemon/__tests__/integration/balances.test.ts index b6e4d95d..be99e9d4 100644 --- a/packages/daemon/__tests__/integration/balances.test.ts +++ b/packages/daemon/__tests__/integration/balances.test.ts @@ -71,7 +71,7 @@ let mysql: Connection; beforeAll(async () => { try { mysql = await getDbConnection(); - } catch(e) { + } catch (e) { console.error('Failed to establish db connection', e); throw e; } @@ -290,6 +290,7 @@ describe('empty script scenario', () => { // @ts-ignore await transitionUntilEvent(mysql, machine, EMPTY_SCRIPT_LAST_EVENT); const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore expect(validateBalances(addressBalances, emptyScriptBalances)); }); diff --git a/packages/daemon/__tests__/utils/wallet.test.ts b/packages/daemon/__tests__/utils/wallet.test.ts index 4d60c86e..b3584a26 100644 --- a/packages/daemon/__tests__/utils/wallet.test.ts +++ b/packages/daemon/__tests__/utils/wallet.test.ts @@ -25,9 +25,9 @@ describe('prepareOutputs', () => { token_data: 0, script: 'dqkUCU1EY3YLi8WURhDOEsspok4Y0XiIrA==', decoded: { - type: 'P2PKH', - address: 'H7NK2gjt5oaHzBEPoiH7y3d1NcPQi3Tr2F', - timelock: null, + type: 'P2PKH', + address: 'H7NK2gjt5oaHzBEPoiH7y3d1NcPQi3Tr2F', + timelock: null, } }, { value: 1, diff --git a/packages/daemon/src/services/index.ts b/packages/daemon/src/services/index.ts index f253e2b3..aa8ff9a7 100644 --- a/packages/daemon/src/services/index.ts +++ b/packages/daemon/src/services/index.ts @@ -29,6 +29,7 @@ import { Transaction, TokenBalanceMap, TxOutputWithIndex, + isDecodedValid, } from '@wallet-service/common'; import { prepareOutputs, @@ -133,8 +134,8 @@ export const metadataDiff = async (_context: Context, event: Event) => { } if (first_block - && first_block.length - && first_block.length > 0) { + && first_block.length + && first_block.length > 0) { if (!dbTx.height) { return { type: METADATA_DIFF_EVENT_TYPES.TX_FIRST_BLOCK, @@ -161,7 +162,7 @@ export const metadataDiff = async (_context: Context, event: Event) => { }; export const isBlock = (version: number): boolean => version === hathorLib.constants.BLOCK_VERSION - || version === hathorLib.constants.MERGED_MINED_BLOCK_VERSION; + || version === hathorLib.constants.MERGED_MINED_BLOCK_VERSION; export const handleVertexAccepted = async (context: Context, _event: Event) => { const mysql = await getDbConnection(); @@ -217,7 +218,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { const txOutputs: TxOutputWithIndex[] = prepareOutputs(outputs, tokens); const txInputs: TxInput[] = prepareInputs(inputs, tokens); - let heightlock: number|null = null; + let heightlock: number | null = null; if (isBlock(version)) { if (typeof height !== 'number' && !height) { throw new Error('Block with no height set in metadata.'); @@ -238,7 +239,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { const blockRewardOutput = outputs[0]; // add miner to the miners table - if (blockRewardOutput.decoded) { + if (isDecodedValid(blockRewardOutput.decoded, ['address'])) { await addMiner(mysql, blockRewardOutput.decoded.address, hash); } @@ -303,21 +304,21 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { const addressesPerWallet = Object.entries(addressWalletMap).reduce( (result: StringMap<{ addresses: string[], walletDetails: Wallet }>, [address, wallet]: [string, Wallet]) => { - const { walletId } = wallet; - - // Initialize the array if the walletId is not yet a key in result - if (!result[walletId]) { - result[walletId] = { - addresses: [], - walletDetails: wallet, + const { walletId } = wallet; + + // Initialize the array if the walletId is not yet a key in result + if (!result[walletId]) { + result[walletId] = { + addresses: [], + walletDetails: wallet, + } } - } - // Add the current key to the array - result[walletId].addresses.push(address); + // Add the current key to the array + result[walletId].addresses.push(address); - return result; - }, {}); + return result; + }, {}); const seenWallets = Object.keys(addressesPerWallet); @@ -420,7 +421,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { await mysql.commit(); } catch (e) { await mysql.rollback(); - logger.error('Error handling vertex accepted', { + console.error('Error handling vertex accepted', { error: (e as Error).message, stack: (e as Error).stack, }); @@ -615,13 +616,13 @@ export const updateLastSyncedEvent = async (context: Context) => { const lastEventId = context.event.event.id; if (lastDbSyncedEvent - && lastDbSyncedEvent.last_event_id > lastEventId) { - logger.error('Tried to store an event lower than the one on the database', { - lastEventId, - lastDbSyncedEvent: JSON.stringify(lastDbSyncedEvent), - }); - mysql.destroy(); - throw new Error('Event lower than stored one.'); + && lastDbSyncedEvent.last_event_id > lastEventId) { + logger.error('Tried to store an event lower than the one on the database', { + lastEventId, + lastDbSyncedEvent: JSON.stringify(lastDbSyncedEvent), + }); + mysql.destroy(); + throw new Error('Event lower than stored one.'); } await dbUpdateLastSyncedEvent(mysql, lastEventId); diff --git a/packages/daemon/src/utils/wallet.ts b/packages/daemon/src/utils/wallet.ts index ec906770..f9752269 100644 --- a/packages/daemon/src/utils/wallet.ts +++ b/packages/daemon/src/utils/wallet.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { constants, Output, walletUtils, addressUtils } from '@hathor/wallet-lib'; +import hathorLib, { constants, Output, walletUtils, addressUtils } from '@hathor/wallet-lib'; import { Connection as MysqlConnection } from 'mysql2/promise'; import { strict as assert } from 'assert'; import { @@ -27,6 +27,7 @@ import { TxInput, TxOutput, TokenBalanceMap, + isDecodedValid, } from '@wallet-service/common'; import { fetchAddressBalance, @@ -75,9 +76,9 @@ export const prepareOutputs = (outputs: EventTxOutput[], tokens: string[]): TxOu // @ts-ignore output.token = token; - if (!_output.decoded - || _output.decoded.type === null - || _output.decoded.type === undefined) { + if (!isDecodedValid(_output.decoded) + || _output.decoded.type === null + || _output.decoded.type === undefined) { console.log('Decode failed, skipping..'); return [currIndex + 1, newOutputs]; } @@ -93,7 +94,7 @@ export const prepareOutputs = (outputs: EventTxOutput[], tokens: string[]): TxOu }; // @ts-ignore - return [ currIndex + 1, [ ...newOutputs, finalOutput, ], ]; + return [currIndex + 1, [...newOutputs, finalOutput,],]; }, [0, []], ); @@ -124,7 +125,7 @@ export const getAddressBalanceMap = ( const addressBalanceMap = {}; for (const input of inputs) { - if (!input.decoded) { + if (!isDecodedValid(input.decoded)) { // If we're unable to decode the script, we will also be unable to // calculate the balance, so just skip this input. continue; @@ -140,7 +141,7 @@ export const getAddressBalanceMap = ( } for (const output of outputs) { - if (!output.decoded) { + if (!isDecodedValid(output.decoded)) { throw new Error('Output has no decoded script'); } @@ -283,7 +284,7 @@ export const prepareInputs = (inputs: EventTxInput[], tokens: string[]): TxInput const utxo: Output = new Output(output.value, Buffer.from(output.script, 'base64'), { tokenData: output.token_data, }); - let token = '00'; + let token = hathorLib.constants.NATIVE_TOKEN_UID; if (!utxo.isTokenHTR()) { token = tokens[utxo.getTokenIndex()]; } @@ -296,9 +297,10 @@ export const prepareInputs = (inputs: EventTxInput[], tokens: string[]): TxInput // @ts-ignore script: utxo.script, token, - decoded: output.decoded ? { + decoded: isDecodedValid(output.decoded, ['type', 'address']) ? { type: output.decoded.type, address: output.decoded.address, + // timelock might actually be null, so don't pass it to requiredKeys timelock: output.decoded.timelock, } : null, };