From cfc6f79d924e81fab219612138402b902da6e5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Abadesso?= Date: Thu, 20 Jun 2024 21:58:10 -0300 Subject: [PATCH] tests: added tests for the voidTransaction method --- packages/daemon/__tests__/db/index.test.ts | 99 +++++++++++++++++++++- packages/daemon/__tests__/types.ts | 8 ++ packages/daemon/__tests__/utils.ts | 72 +++++++++++++++- packages/daemon/package.json | 2 +- packages/daemon/src/db/index.ts | 38 +++++++-- packages/daemon/src/types/db.ts | 8 ++ 6 files changed, 215 insertions(+), 12 deletions(-) diff --git a/packages/daemon/__tests__/db/index.test.ts b/packages/daemon/__tests__/db/index.test.ts index a53d14d4..a399a85d 100644 --- a/packages/daemon/__tests__/db/index.test.ts +++ b/packages/daemon/__tests__/db/index.test.ts @@ -39,7 +39,8 @@ import { updateLastSyncedEvent, updateTxOutputSpentBy, updateWalletLockedBalance, - updateWalletTablesWithTx + updateWalletTablesWithTx, + voidTransaction } from '../../src/db'; import { Connection } from 'mysql2/promise'; import { @@ -48,6 +49,7 @@ import { addToAddressTable, addToAddressTxHistoryTable, addToTokenTable, + addToTransactionTable, addToUtxoTable, addToWalletBalanceTable, addToWalletTable, @@ -55,6 +57,7 @@ import { checkAddressTable, checkAddressTxHistoryTable, checkTokenTable, + checkTransactionTable, checkUtxoTable, checkWalletBalanceTable, checkWalletTxHistoryTable, @@ -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; @@ -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 = { + [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 = {}; + + 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.'); + }); +}); diff --git a/packages/daemon/__tests__/types.ts b/packages/daemon/__tests__/types.ts index b78c7e32..dfe806c2 100644 --- a/packages/daemon/__tests__/types.ts +++ b/packages/daemon/__tests__/types.ts @@ -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; diff --git a/packages/daemon/__tests__/utils.ts b/packages/daemon/__tests__/utils.ts index a20f362b..a5a0f7ab 100644 --- a/packages/daemon/__tests__/utils.ts +++ b/packages/daemon/__tests__/utils.ts @@ -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, @@ -23,7 +23,8 @@ import { TokenTableEntry, WalletBalanceEntry, WalletTableEntry, - AddressTxHistoryTableEntry + AddressTxHistoryTableEntry, + TransactionTableEntry } from './types'; import { isEqual } from 'lodash'; @@ -245,6 +246,26 @@ export const addToAddressTable = async ( [payload]); }; +export const addToTransactionTable = async ( + mysql: MysqlConnection, + transactions: TransactionTableEntry[], +): Promise => { + 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, @@ -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> => { + // first check the total number of rows in the table + let [results] = await mysql.query('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(` + 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, diff --git a/packages/daemon/package.json b/packages/daemon/package.json index d950264f..5a32364d 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -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", diff --git a/packages/daemon/src/db/index.ts b/packages/daemon/src/db/index.ts index 9a8d4854..68e7c143 100644 --- a/packages/daemon/src/db/index.ts +++ b/packages/daemon/src/db/index.ts @@ -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, @@ -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} - 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, ): Promise => { + 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) { @@ -448,13 +477,6 @@ export const voidTransaction = async ( WHERE \`tx_id\` = ?`, [txId], ); - - await mysql.query( - `UPDATE \`transaction\` - SET \`voided\` = TRUE - WHERE \`tx_id\` = ?`, - [txId], - ); }; /** diff --git a/packages/daemon/src/types/db.ts b/packages/daemon/src/types/db.ts index 435ed076..2f8c413a 100644 --- a/packages/daemon/src/types/db.ts +++ b/packages/daemon/src/types/db.ts @@ -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;