Skip to content

fix: io decoded handling #229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/common/__tests__/utils/nft.utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jest.mock('winston', () => {
error = jest.fn();
info = jest.fn();
debug = jest.fn();
};
}

return {
Logger: FakeLogger,
Expand Down
29 changes: 29 additions & 0 deletions packages/common/__tests__/utils/wallet.utils.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
4 changes: 2 additions & 2 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
[x: string]: T;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/utils/alerting.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
16 changes: 16 additions & 0 deletions packages/common/src/utils/wallet.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
3 changes: 2 additions & 1 deletion packages/daemon/__tests__/integration/balances.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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));
});
Expand Down
6 changes: 3 additions & 3 deletions packages/daemon/__tests__/utils/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
51 changes: 26 additions & 25 deletions packages/daemon/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
Transaction,
TokenBalanceMap,
TxOutputWithIndex,
isDecodedValid,
} from '@wallet-service/common';
import {
prepareOutputs,
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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.');
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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);

Expand Down
20 changes: 11 additions & 9 deletions packages/daemon/src/utils/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -27,6 +27,7 @@ import {
TxInput,
TxOutput,
TokenBalanceMap,
isDecodedValid,
} from '@wallet-service/common';
import {
fetchAddressBalance,
Expand Down Expand Up @@ -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];
}
Expand All @@ -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, []],
);
Expand Down Expand Up @@ -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;
Expand All @@ -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');
}

Expand Down Expand Up @@ -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()];
}
Expand All @@ -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,
};
Expand Down