Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
89fdbe1
fix: we should unspend transaction tx outputs when the tx spending it…
andreabadesso Aug 21, 2025
dc41675
fix: wallet_balance not being recalculated after a voided transaction
andreabadesso Aug 21, 2025
865a58f
fix: unlock tx outputs after a failure in tx proposal send
andreabadesso Aug 21, 2025
789d01b
fix: updated event tx output schema to match the fullnode message
andreabadesso Sep 4, 2025
c042b64
tests: added tests for the new transaction voiding chain scenario
andreabadesso Sep 4, 2025
bf6ff57
tests: inconsistent tests
andreabadesso Sep 4, 2025
5077356
chore: off-by-one error on unvoided tx scenario
andreabadesso Sep 5, 2025
4622f41
refactor(daemon): moved utils to the utils file
andreabadesso Sep 9, 2025
6297524
refactor(daemon): more strict balance checking and safer null check
andreabadesso Sep 9, 2025
864c58a
tests(daemon): tests should expect not to throw
andreabadesso Sep 10, 2025
a33b986
refactor(daemon): better comment on unlocked authorities calculation …
andreabadesso Sep 10, 2025
179605a
refactor(daemon): removed outdated comments
andreabadesso Sep 10, 2025
f7f41a6
tests(daemon): changed test structure to test wallet and address bala…
andreabadesso Sep 11, 2025
9d5ef79
fix(daemon): bug when voiding transactions that ADD authorities
andreabadesso Sep 11, 2025
32f1baf
tests(daemon): added the voided_token authority simulator balances
andreabadesso Sep 11, 2025
b255a29
tests(daemon): updated tests with correct locked balances
andreabadesso Sep 11, 2025
01a7a23
tests(daemon): refactor sql insert for the transction voiding chain s…
andreabadesso Sep 11, 2025
5708200
refactor(daemon): better handling of voided transactions
andreabadesso Sep 12, 2025
cffc902
tests(daemon): updated tests with new refactored voided transaction h…
andreabadesso Sep 12, 2025
dea6e49
docs(daemon): added critical doc
andreabadesso Sep 12, 2025
0380f76
tests(daemon): expect cafecafe balance
andreabadesso Sep 12, 2025
a237e5c
Merge branch 'master' into fix/unspend-tx-outputs
andreabadesso Sep 12, 2025
84ccea2
tests(daemon): removed .only
andreabadesso Sep 12, 2025
ee47926
refactor(daemon): changed console.log to console.warn
andreabadesso Sep 12, 2025
13def6f
tests(daemon): forceExit and other test cleanups
andreabadesso Sep 12, 2025
fcf0ea5
tests(daemon): using experimental docker image for new tests
andreabadesso Sep 12, 2025
ce6a19c
tests(daemon): temporarily disabled genesis tx check
andreabadesso Sep 12, 2025
d38d2be
tests(daemon): restore genesis balances
andreabadesso Sep 15, 2025
cd1c869
refactor(daemon): creating LRU cache on initializing state
andreabadesso Sep 15, 2025
7016f91
tests(daemon): properly mocking LRU cache
andreabadesso Sep 15, 2025
31b5cef
tests(daemon): added single_voided_create_token_tx scenario
andreabadesso Sep 17, 2025
6c62f71
Merge branch 'master' into fix/voided-token-addr-balance
andreabadesso Sep 17, 2025
8b99e43
fix(daemon): removing address_balance row and token row when token cr…
andreabadesso Sep 17, 2025
396fb1d
tests(daemon): validate that wallet_balance for the voided token does…
andreabadesso Sep 17, 2025
bdf0f66
fix(daemon): removing wallet_balance row for voided token creation tr…
andreabadesso Sep 17, 2025
c39b0dc
tests(daemon): restore all tests
andreabadesso Sep 17, 2025
545c9ec
refactor(daemon): sending version from the event instead of querying
andreabadesso Sep 17, 2025
fc9614a
tests(daemon): checking specific token instead of all tokens
andreabadesso Sep 17, 2025
e4efac8
tests(daemon): removed unused console log and comment
andreabadesso Sep 17, 2025
107891d
tests(daemon): using experimental build
andreabadesso Sep 17, 2025
e2a2d27
tests(daemon): experimental image for new tests
andreabadesso Sep 17, 2025
bba6af6
tests(daemon): renamed test case to actually reflect what is going on
andreabadesso Sep 17, 2025
24239d8
tests(daemon): added voided regular tx scenario failing before fix
andreabadesso Sep 18, 2025
51e6a28
fix(daemon): always remove from address_balance if the transaction wa…
andreabadesso Sep 18, 2025
ed375af
tests(daemon): test checking wallet_tx_history and wallet_history
andreabadesso Sep 18, 2025
f599883
fix(daemon): cleanup wallet_balance table when transactions = 0
andreabadesso Sep 18, 2025
f94e475
chore: using experimental hathor-core
andreabadesso Sep 18, 2025
f879849
Merge branch 'master' into fix/voided-token-addr-balance
andreabadesso Sep 18, 2025
98ccec3
refactor(daemon): version is no longer needed in voidWalletTransaction
andreabadesso Sep 18, 2025
30a16d5
tests(daemon): passing tx version on voidTx in tests
andreabadesso Sep 18, 2025
47ecc3f
tests(daemon): restored removed control mechanism
andreabadesso Sep 18, 2025
074dc4c
tests(daemon): removed balance check for the removed address_balance
andreabadesso Sep 18, 2025
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/daemon/__tests__/db/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1225,7 +1225,7 @@ describe('voidTransaction', () => {
};

await voidTransaction(mysql, txId);
await voidAddressTransaction(mysql, txId, addressBalance);
await voidAddressTransaction(mysql, txId, addressBalance, 1);

await expect(checkAddressBalanceTable(mysql, 2, addr1, token2, 1n, 0n, null, 3)).resolves.toBe(true);
await expect(checkAddressBalanceTable(mysql, 2, addr1, token1, 1n, 0n, null, 4)).resolves.toBe(true);
Expand Down
344 changes: 342 additions & 2 deletions packages/daemon/__tests__/integration/balances.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import customScriptBalances from './scenario_configs/custom_script.balances';
import ncEventsBalances from './scenario_configs/nc_events.balances';
import transactionVoidingChainBalances from './scenario_configs/transaction_voiding_chain.balances';
import voidedTokenAuthorityBalances from './scenario_configs/voided_token_authority.balances';
import singleVoidedCreateTokenTransactionBalances from './scenario_configs/single_voided_create_token_transaction.balances';
import singleVoidedRegularTransactionBalances from './scenario_configs/single_voided_regular_transaction.balances';

import {
DB_NAME,
Expand All @@ -54,6 +56,10 @@ import {
TRANSACTION_VOIDING_CHAIN_LAST_EVENT,
VOIDED_TOKEN_AUTHORITY_PORT,
VOIDED_TOKEN_AUTHORITY_LAST_EVENT,
SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_PORT,
SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_LAST_EVENT,
SINGLE_VOIDED_REGULAR_TRANSACTION_PORT,
SINGLE_VOIDED_REGULAR_TRANSACTION_LAST_EVENT,
} from './config';

jest.mock('../../src/config', () => {
Expand Down Expand Up @@ -207,7 +213,7 @@ describe('single chain blocks and transactions scenario', () => {
});

const machine = interpret(SyncMachine);

// @ts-expect-error
await transitionUntilEvent(mysql, machine, SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT);
const addressBalances = await fetchAddressBalances(mysql);
Expand Down Expand Up @@ -384,7 +390,6 @@ describe('transaction voiding chain scenario', () => {
});

const machine = interpret(SyncMachine);

// @ts-ignore
await transitionUntilEvent(mysql, machine, TRANSACTION_VOIDING_CHAIN_LAST_EVENT);
const addressBalances = await fetchAddressBalances(mysql);
Expand Down Expand Up @@ -527,3 +532,338 @@ describe('voided token authority scenario', () => {
validateVoidingConsistency(voidingChecks);
}, 30000); // 30 second timeout for voided token authority test
});

describe('single voided create token transaction scenario', () => {
const initializeWallet = async (mysql: Connection): Promise<void> => {
// Insert wallet records
const walletSQL = `
INSERT INTO wallet (
id,
xpubkey,
status,
max_gap,
created_at,
ready_at,
retry_count,
auth_xpubkey,
last_used_address_index
) VALUES
(
'test-wallet-voided-token',
'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x',
'ready',
20,
UNIX_TIMESTAMP(),
UNIX_TIMESTAMP(),
0,
'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x',
-1
)`;

// Insert address records that will receive the voided token
const addressSQL = `
INSERT INTO address (address, \`index\`, wallet_id, transactions, seqnum) VALUES
('HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs', 0, 'test-wallet-voided-token', 0, 0)`;

await mysql.query(walletSQL);
await mysql.query(addressSQL);
};

beforeAll(async () => {
jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300);
await cleanDatabase(mysql);
});

it('should do a full sync and the balances should match', async () => {
// @ts-expect-error
getConfig.mockReturnValue({
NETWORK: 'testnet',
SERVICE_NAME: 'daemon-test',
CONSOLE_LEVEL: 'debug',
TX_CACHE_SIZE: 100,
BLOCK_REWARD_LOCK: 300,
FULLNODE_PEER_ID: 'simulator_peer_id',
STREAM_ID: 'simulator_stream_id',
FULLNODE_NETWORK: 'unittests',
FULLNODE_HOST: `127.0.0.1:${SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_PORT}`,
USE_SSL: false,
DB_ENDPOINT,
DB_NAME,
DB_USER,
DB_PASS,
DB_PORT,
});

const machine = interpret(SyncMachine);

// @ts-expect-error
await transitionUntilEvent(mysql, machine, SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_LAST_EVENT);
const addressBalances = await fetchAddressBalances(mysql);
await expect(validateBalances(addressBalances, singleVoidedCreateTokenTransactionBalances.addressBalances)).resolves.not.toThrow();
}, 30000);

it('addresses_balance and address_tx_history row length must match after a void transaction scenario', async () => {
// @ts-expect-error
getConfig.mockReturnValue({
NETWORK: 'testnet',
SERVICE_NAME: 'daemon-test',
CONSOLE_LEVEL: 'debug',
TX_CACHE_SIZE: 100,
BLOCK_REWARD_LOCK: 300,
FULLNODE_PEER_ID: 'simulator_peer_id',
STREAM_ID: 'simulator_stream_id',
FULLNODE_NETWORK: 'unittests',
FULLNODE_HOST: `127.0.0.1:${SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_PORT}`,
USE_SSL: false,
DB_ENDPOINT,
DB_NAME,
DB_USER,
DB_PASS,
DB_PORT,
});

// Initialize wallet before processing events
await initializeWallet(mysql);

const machine = interpret(SyncMachine);

// @ts-expect-error
await transitionUntilEvent(mysql, machine, SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION_LAST_EVENT);

const voidedTokenId = 'efb08b3e79e0ddaa6bc288183f66fe49a07ba0b7b2595861000478cc56447539';

// Check for addresses that have balances for a specific token
const addressBalanceResults = await mysql.query(`
SELECT token_id,
SUM(total_received) AS total_received,
SUM(unlocked_balance) AS unlocked_balance,
SUM(locked_balance) AS locked_balance,
MIN(timelock_expires) AS timelock_expires,
BIT_OR(unlocked_authorities) AS unlocked_authorities,
BIT_OR(locked_authorities) AS locked_authorities
FROM address_balance
WHERE token_id = ?
GROUP BY token_id
ORDER BY token_id
`, [voidedTokenId]);

// Check for transaction history for the same tokens (excluding voided)
const txHistoryResults = await mysql.query(`
SELECT token_id,
SUM(balance) AS balance,
COUNT(DISTINCT tx_id) AS transactions
FROM address_tx_history
WHERE voided = FALSE
AND token_id = ?
GROUP BY token_id
ORDER BY token_id
`, [voidedTokenId]);

// Cast to array to access length property
const addressRows = addressBalanceResults[0] as any[];
const txHistoryRows = txHistoryResults[0] as any[];

expect(addressRows.length).toEqual(txHistoryRows.length);

// Verify that the voided token was removed from the token table
const tokenResults = await mysql.query(
'SELECT * FROM token WHERE id = ?',
[voidedTokenId]
);

// Token should not exist in the database after being voided
expect(tokenResults[0]).toHaveLength(0);

// Verify that the wallet_balance table doesn't contain the voided token
const walletBalanceResults = await mysql.query(
'SELECT * FROM wallet_balance WHERE token_id = ?',
[voidedTokenId]
);

// Wallet balance should not exist for the voided token
expect(walletBalanceResults[0]).toHaveLength(0);
}, 30000);
});

describe('single voided regular transaction scenario', () => {
const initializeWallet = async (mysql: Connection): Promise<void> => {
// Insert wallet records
const walletSQL = `
INSERT INTO wallet (
id,
xpubkey,
status,
max_gap,
created_at,
ready_at,
retry_count,
auth_xpubkey,
last_used_address_index
) VALUES
(
'test-wallet-voided-regular',
'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x',
'ready',
20,
UNIX_TIMESTAMP(),
UNIX_TIMESTAMP(),
0,
'xpub6F81iNtH5HVknoJ65cK2XAGA5F3okdJK7WHwVAAPZnSir2sfwbhvB9ffNKQ4wLor75QxPe9p12tqt8xUZSG8i8AAPMpkFho7fbWkBJQ5s1x',
-1
)`;

// Insert address record for the voided transaction address
const addressSQL = `
INSERT INTO address (address, \`index\`, wallet_id, transactions, seqnum) VALUES
('HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh', 0, 'test-wallet-voided-regular', 0, 0)`;

await mysql.query(walletSQL);
await mysql.query(addressSQL);
};

beforeAll(async () => {
jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300);
await cleanDatabase(mysql);
});

it('should do a full sync and the balances should match', async () => {
// @ts-expect-error
getConfig.mockReturnValue({
NETWORK: 'testnet',
SERVICE_NAME: 'daemon-test',
CONSOLE_LEVEL: 'debug',
TX_CACHE_SIZE: 100,
BLOCK_REWARD_LOCK: 300,
FULLNODE_PEER_ID: 'simulator_peer_id',
STREAM_ID: 'simulator_stream_id',
FULLNODE_NETWORK: 'unittests',
FULLNODE_HOST: `127.0.0.1:${SINGLE_VOIDED_REGULAR_TRANSACTION_PORT}`,
USE_SSL: false,
DB_ENDPOINT,
DB_NAME,
DB_USER,
DB_PASS,
DB_PORT,
});

const machine = interpret(SyncMachine);

// @ts-expect-error
await transitionUntilEvent(mysql, machine, SINGLE_VOIDED_REGULAR_TRANSACTION_LAST_EVENT);
const addressBalances = await fetchAddressBalances(mysql);
await expect(validateBalances(addressBalances, singleVoidedRegularTransactionBalances.addressBalances)).resolves.not.toThrow();
}, 30000);

it('addresses_balance and address_tx_history row length must match after a void transaction scenario', async () => {
// @ts-expect-error
getConfig.mockReturnValue({
NETWORK: 'testnet',
SERVICE_NAME: 'daemon-test',
CONSOLE_LEVEL: 'debug',
TX_CACHE_SIZE: 100,
BLOCK_REWARD_LOCK: 300,
FULLNODE_PEER_ID: 'simulator_peer_id',
STREAM_ID: 'simulator_stream_id',
FULLNODE_NETWORK: 'unittests',
FULLNODE_HOST: `127.0.0.1:${SINGLE_VOIDED_REGULAR_TRANSACTION_PORT}`,
USE_SSL: false,
DB_ENDPOINT,
DB_NAME,
DB_USER,
DB_PASS,
DB_PORT,
});

const machine = interpret(SyncMachine);

// @ts-expect-error
await transitionUntilEvent(mysql, machine, SINGLE_VOIDED_REGULAR_TRANSACTION_LAST_EVENT);

const voidedAddress = 'HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh';

// Check for address balances for the specific address
const addressBalanceResults = await mysql.query(`
SELECT address,
COUNT(*) AS balance_rows
FROM address_balance
WHERE address = ?
GROUP BY address
ORDER BY address
`, [voidedAddress]);

// Check for transaction history for the same address (excluding voided)
const txHistoryResults = await mysql.query(`
SELECT address,
COUNT(*) AS history_rows
FROM address_tx_history
WHERE voided = FALSE
AND address = ?
GROUP BY address
ORDER BY address
`, [voidedAddress]);

// Cast to array to access length property
const addressRows = addressBalanceResults[0] as any[];
const txHistoryRows = txHistoryResults[0] as any[];

expect(addressRows.length).toEqual(txHistoryRows.length);
}, 30000);

it('wallet_balance and wallet_tx_history row length must match after a void transaction scenario', async () => {
// @ts-expect-error
getConfig.mockReturnValue({
NETWORK: 'testnet',
SERVICE_NAME: 'daemon-test',
CONSOLE_LEVEL: 'debug',
TX_CACHE_SIZE: 100,
BLOCK_REWARD_LOCK: 300,
FULLNODE_PEER_ID: 'simulator_peer_id',
STREAM_ID: 'simulator_stream_id',
FULLNODE_NETWORK: 'unittests',
FULLNODE_HOST: `127.0.0.1:${SINGLE_VOIDED_REGULAR_TRANSACTION_PORT}`,
USE_SSL: false,
DB_ENDPOINT,
DB_NAME,
DB_USER,
DB_PASS,
DB_PORT,
});

// Initialize wallet before processing events
await initializeWallet(mysql);

const machine = interpret(SyncMachine);

// @ts-expect-error
await transitionUntilEvent(mysql, machine, SINGLE_VOIDED_REGULAR_TRANSACTION_LAST_EVENT);

const walletId = 'test-wallet-voided-regular';

// Check for wallet balances for the specific wallet
const walletBalanceResults = await mysql.query(`
SELECT wallet_id,
COUNT(*) AS balance_rows
FROM wallet_balance
WHERE wallet_id = ?
GROUP BY wallet_id
ORDER BY wallet_id
`, [walletId]);

// Check for wallet transaction history for the same wallet (excluding voided)
const walletTxHistoryResults = await mysql.query(`
SELECT wallet_id,
COUNT(*) AS history_rows
FROM wallet_tx_history
WHERE voided = FALSE
AND wallet_id = ?
GROUP BY wallet_id
ORDER BY wallet_id
`, [walletId]);

// Cast to array to access length property
const walletBalanceRows = walletBalanceResults[0] as any[];
const walletTxHistoryRows = walletTxHistoryResults[0] as any[];

expect(walletBalanceRows.length).toEqual(walletTxHistoryRows.length);
}, 30000);
});
Loading