Skip to content

Commit

Permalink
tests: added tests for the voidTransaction method
Browse files Browse the repository at this point in the history
  • Loading branch information
andreabadesso committed Jun 21, 2024
1 parent 05fe782 commit cfc6f79
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 12 deletions.
99 changes: 98 additions & 1 deletion packages/daemon/__tests__/db/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import {
updateLastSyncedEvent,
updateTxOutputSpentBy,
updateWalletLockedBalance,
updateWalletTablesWithTx
updateWalletTablesWithTx,
voidTransaction
} from '../../src/db';
import { Connection } from 'mysql2/promise';
import {
Expand All @@ -48,13 +49,15 @@ import {
addToAddressTable,
addToAddressTxHistoryTable,
addToTokenTable,
addToTransactionTable,
addToUtxoTable,
addToWalletBalanceTable,
addToWalletTable,
checkAddressBalanceTable,
checkAddressTable,
checkAddressTxHistoryTable,
checkTokenTable,
checkTransactionTable,
checkUtxoTable,
checkWalletBalanceTable,
checkWalletTxHistoryTable,
Expand All @@ -68,6 +71,8 @@ import {
import { isAuthority } from '@wallet-service/common';
import { DbTxOutput, StringMap, TokenInfo, WalletStatus } from '../../src/types';
import { Authorities, TokenBalanceMap } from '@wallet-service/common';
// @ts-ignore
import { constants } from '@hathor/wallet-lib';

// Use a single mysql connection for all tests
let mysql: Connection;
Expand Down Expand Up @@ -1164,3 +1169,95 @@ describe('getTokenSymbols', () => {
expect(tokenSymbolMap).toBeNull();
});
});

describe('voidTransaction', () => {
const txId = 'tx1';
const addr1 = 'addr1';
const token1 = 'token1';
const token2 = 'other-token';

it('should re-calculate address balances properly', async () => {
expect.hasAssertions();

await addToTransactionTable(mysql, [{
txId,
timestamp: 0,
version: constants.BLOCK_VERSION,
voided: false,
height: 1,
}]);

await addToAddressTable(mysql, [{
address: addr1,
index: 0,
walletId: null,
transactions: 2,
}]);

await addToAddressBalanceTable(mysql, [
[addr1, token1, 50, 5, null, 5, 0, 0, 100],
[addr1, token2, 25, 10, null, 4, 0, 0, 50],
]);

await addToAddressTxHistoryTable(mysql, [{
address: addr1,
txId,
tokenId: token1,
balance: 50,
timestamp: 1,
}, {
address: addr1,
txId,
tokenId: token2,
balance: 25,
timestamp: 1,
}]);

const addressBalance: StringMap<TokenBalanceMap> = {
[addr1]: TokenBalanceMap.fromStringMap({
[token1]: {
unlocked: 49,
locked: 5,
},
[token2]: {
unlocked: 24,
locked: 10,
}
}),
};

await voidTransaction(mysql, txId, addressBalance);

await expect(checkAddressBalanceTable(mysql, 2, addr1, token2, 1, 0, null, 3)).resolves.toBe(true);
await expect(checkAddressBalanceTable(mysql, 2, addr1, token1, 1, 0, null, 4)).resolves.toBe(true);
// Address tx history entry should have been deleted for both tokens:
await expect(checkAddressTxHistoryTable(mysql, 0, addr1, txId, token1, -1, 0)).resolves.toBe(true);
await expect(checkAddressTxHistoryTable(mysql, 0, addr1, txId, token2, -1, 0)).resolves.toBe(true);

await expect(checkTransactionTable(mysql, 1, txId, 0, constants.BLOCK_VERSION, true, 1)).resolves.toBe(true);
});

it('should not fail when balances are empty (from a tx with no inputs and outputs)', async () => {
expect.hasAssertions();

await addToTransactionTable(mysql, [{
txId,
timestamp: 0,
version: constants.BLOCK_VERSION,
voided: false,
height: 1,
}]);

const addressBalance: StringMap<TokenBalanceMap> = {};

await expect(voidTransaction(mysql, txId, addressBalance)).resolves.not.toThrow();
// Tx should be voided
await expect(checkTransactionTable(mysql, 1, txId, 0, constants.BLOCK_VERSION, true, 1)).resolves.toBe(true);
});

it('should throw an error if the transaction is not found in the database', async () => {
expect.hasAssertions();

await expect(voidTransaction(mysql, 'mysterious-transaction', {})).rejects.toThrow('Tried to void a transaction that is not in the database.');
});
});
8 changes: 8 additions & 0 deletions packages/daemon/__tests__/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ export interface AddressTableEntry {
transactions: number;
}

export interface TransactionTableEntry {
txId: string;
timestamp: number;
version: number;
voided: boolean;
height: number;
}

export interface WalletBalanceEntry {
walletId: string;
tokenId: string;
Expand Down
72 changes: 70 additions & 2 deletions packages/daemon/__tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { Connection as MysqlConnection, RowDataPacket } from 'mysql2/promise';
import { DbTxOutput, EventTxInput } from '../src/types';
import { DbTxOutput, EventTxInput, TransactionTableRow } from '../src/types';
import { TxInput, TxOutputWithIndex } from '@wallet-service/common/src/types';
import {
AddressBalanceRow,
Expand All @@ -23,7 +23,8 @@ import {
TokenTableEntry,
WalletBalanceEntry,
WalletTableEntry,
AddressTxHistoryTableEntry
AddressTxHistoryTableEntry,
TransactionTableEntry
} from './types';
import { isEqual } from 'lodash';

Expand Down Expand Up @@ -245,6 +246,26 @@ export const addToAddressTable = async (
[payload]);
};

export const addToTransactionTable = async (
mysql: MysqlConnection,
transactions: TransactionTableEntry[],
): Promise<void> => {
const payload = transactions.map((entry) => ([
entry.txId,
entry.timestamp,
entry.version,
entry.voided,
entry.height,
]));

await mysql.query(`
INSERT INTO \`transaction\` (\`tx_id\`, \`timestamp\`,
\`version\`, \`voided\`,
\`height\`)
VALUES ?`,
[payload]);
};

export const checkAddressTable = async (
mysql: MysqlConnection,
totalResults: number,
Expand Down Expand Up @@ -289,6 +310,53 @@ export const checkAddressTable = async (
return true;
};

export const checkTransactionTable = async (
mysql: MysqlConnection,
totalResults: number,
txId: string,
timestamp: number,
version: number,
voided: boolean,
height: number,
): Promise<boolean | Record<string, unknown>> => {
// first check the total number of rows in the table
let [results] = await mysql.query<TransactionTableRow[]>('SELECT * FROM `transaction`');

if (results.length !== totalResults) {
return {
error: 'checkTransactionTable total results',
expected: totalResults,
received: results.length,
results,
};
}

if (totalResults === 0) return true;

// now fetch the exact entry

[results] = await mysql.query<TransactionTableRow[]>(`
SELECT *
FROM \`transaction\`
WHERE \`tx_id\` = ?
AND \`timestamp\` = ?
AND \`version\` = ?
AND \`voided\` = ?
AND \`height\` = ?
`, [txId, timestamp, version, voided, height],
);

if (results.length !== 1) {
return {
error: 'checkAddressTable query',
params: { txId, timestamp, version, voided, height },
results,
};
}

return true;
};

export const checkAddressBalanceTable = async (
mysql: MysqlConnection,
totalResults: number,
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"test_images_wait_for_ws": "yarn dlx ts-node ./__tests__/integration/scripts/wait-for-ws-up.ts",
"test_images_setup_database": "yarn dlx ts-node ./__tests__/integration/scripts/setup-database.ts",
"test": "jest --coverage",
"test_integration": "yarn run test_images_up && yarn run test_images_wait_for_db && yarn run test_images_wait_for_ws && yarn run test_images_setup_database && yarn run test_images_migrate && yarn run test_images_integration && yarn run test_images_down"
"test_integration": "yarn run test_images_up && yarn run test_images_wait_for_db && yarn run test_images_wait_for_ws && yarn run test_images_setup_database && yarn run test_images_migrate && yarn run test_images_integration"
},
"name": "sync-daemon",
"author": "André Abadesso",
Expand Down
38 changes: 30 additions & 8 deletions packages/daemon/src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import mysql, { Connection as MysqlConnection, Pool } from 'mysql2/promise';
import mysql, { Connection as MysqlConnection, OkPacket, Pool, ResultSetHeader } from 'mysql2/promise';
import {
DbTxOutput,
StringMap,
Expand Down Expand Up @@ -362,11 +362,40 @@ export const getTxOutputsAtHeight = async (
return utxos;
};

/**
* Void a transaction by updating the related address and balance information in the database.
*
* @param mysql - The MySQL connection object
* @param txId - The ID of the transaction to be voided.
* @param addressBalanceMap - A map where the key is an address and the value is a map of token balances.
* The TokenBalanceMap contains information about the total amount sent, unlocked and locked amounts, and authorities.
*
* @returns {Promise<void>} - A promise that resolves when the transaction has been voided and the database updated
*
* This function performs the following steps:
* 1. Inserts addresses with a transaction count of 0 into the `address` table or subtracts 1 from the transaction count if they already exist
* 2. Iterates over the addressBalanceMap to update the `address_balance` table with the received token balances.
* 3. Deletes the transaction entry from the `address_tx_history` table.
* 4. Updates the transaction entry in the `transaction` table to mark it as voided.
*
* The function ensures that the authorities are correctly updated and the smallest timelock expiration value is preserved.
*/
export const voidTransaction = async (
mysql: any,
txId: string,
addressBalanceMap: StringMap<TokenBalanceMap>,
): Promise<void> => {
const [result]: [ResultSetHeader] = await mysql.query(
`UPDATE \`transaction\`
SET \`voided\` = TRUE
WHERE \`tx_id\` = ?`,
[txId],
);

if (result.affectedRows !== 1) {
throw new Error('Tried to void a transaction that is not in the database.');
}

const addressEntries = Object.keys(addressBalanceMap).map((address) => [address, 0]);

if (addressEntries.length > 0) {
Expand Down Expand Up @@ -448,13 +477,6 @@ export const voidTransaction = async (
WHERE \`tx_id\` = ?`,
[txId],
);

await mysql.query(
`UPDATE \`transaction\`
SET \`voided\` = TRUE
WHERE \`tx_id\` = ?`,
[txId],
);
};

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/daemon/src/types/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export interface AddressTableRow extends RowDataPacket {
transactions: number;
}

export interface TransactionTableRow extends RowDataPacket {
tx_id: string;
timestamp: number;
version: number;
voided: boolean;
height: number;
}

export interface AddressBalanceRow extends RowDataPacket {
address: string;
token_id: string;
Expand Down

0 comments on commit cfc6f79

Please sign in to comment.