diff --git a/packages/daemon/__tests__/integration/balances.test.ts b/packages/daemon/__tests__/integration/balances.test.ts index 97355e64..7319c7b1 100644 --- a/packages/daemon/__tests__/integration/balances.test.ts +++ b/packages/daemon/__tests__/integration/balances.test.ts @@ -4,16 +4,20 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ + import * as Services from '../../src/services'; import { SyncMachine } from '../../src/machines'; import { interpret } from 'xstate'; -import { getLastSyncedEvent, getDbConnection } from '../../src/db'; +import { getDbConnection } from '../../src/db'; import { Connection } from 'mysql2/promise'; -import { cleanDatabase, fetchAddressBalances, validateBalances } from './utils'; +import { cleanDatabase, fetchAddressBalances, transitionUntilEvent, validateBalances } from './utils'; import unvoidedScenarioBalances from './scenario_configs/unvoided_transactions.balances'; import reorgScenarioBalances from './scenario_configs/reorg.balances'; import singleChainBlocksAndTransactionsBalances from './scenario_configs/single_chain_blocks_and_transactions.balances'; import invalidMempoolBalances from './scenario_configs/invalid_mempool_transaction.balances'; +import emptyScriptBalances from './scenario_configs/empty_script.balances'; +import customScriptBalances from './scenario_configs/custom_script.balances'; + import { DB_NAME, DB_USER, @@ -28,6 +32,10 @@ import { SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_PORT, SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT, INVALID_MEMPOOL_TRANSACTION_LAST_EVENT, + CUSTOM_SCRIPT_PORT, + CUSTOM_SCRIPT_LAST_EVENT, + EMPTY_SCRIPT_PORT, + EMPTY_SCRIPT_LAST_EVENT, } from './config'; jest.mock('../../src/config', () => { @@ -104,25 +112,11 @@ describe('unvoided transaction scenario', () => { const machine = interpret(SyncMachine); - await new Promise((resolve) => { - machine.onTransition(async (state) => { - if (state.matches('CONNECTED.idle')) { - // @ts-ignore - const lastSyncedEvent = await getLastSyncedEvent(mysql); - if (lastSyncedEvent?.last_event_id === UNVOIDED_SCENARIO_LAST_EVENT) { - const addressBalances = await fetchAddressBalances(mysql); - // @ts-ignore - expect(validateBalances(addressBalances, unvoidedScenarioBalances)); - - machine.stop(); - - resolve(); - } - } - }); - - machine.start(); - }); + // @ts-ignore + await transitionUntilEvent(mysql, machine, UNVOIDED_SCENARIO_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, unvoidedScenarioBalances)); }); }); @@ -153,25 +147,11 @@ describe('reorg scenario', () => { const machine = interpret(SyncMachine); - await new Promise((resolve) => { - machine.onTransition(async (state) => { - if (state.matches('CONNECTED.idle')) { - // @ts-ignore - const lastSyncedEvent = await getLastSyncedEvent(mysql); - if (lastSyncedEvent?.last_event_id === REORG_SCENARIO_LAST_EVENT) { - const addressBalances = await fetchAddressBalances(mysql); - // @ts-ignore - expect(validateBalances(addressBalances, reorgScenarioBalances)); - - machine.stop(); - - resolve(); - } - } - }); - - machine.start(); - }); + // @ts-ignore + await transitionUntilEvent(mysql, machine, REORG_SCENARIO_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, reorgScenarioBalances)); }); }); @@ -202,29 +182,50 @@ describe('single chain blocks and transactions scenario', () => { const machine = interpret(SyncMachine); - await new Promise((resolve) => { - machine.onTransition(async (state) => { - if (state.matches('CONNECTED.idle')) { - // @ts-ignore - const lastSyncedEvent = await getLastSyncedEvent(mysql); - if (lastSyncedEvent?.last_event_id === SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT) { - const addressBalances = await fetchAddressBalances(mysql); - // @ts-ignore - expect(validateBalances(addressBalances, singleChainBlocksAndTransactionsBalances)); - - machine.stop(); + // @ts-ignore + await transitionUntilEvent(mysql, machine, SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, singleChainBlocksAndTransactionsBalances)); + }); +}); - resolve(); - } - } - }); +describe('invalid mempool transactions scenario', () => { + beforeAll(() => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + }); - machine.start(); + it('should do a full sync and the balances should match', async () => { + // @ts-ignore + 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: 'simulator_network', + FULLNODE_HOST: `127.0.0.1:${INVALID_MEMPOOL_TRANSACTION_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, }); + + const machine = interpret(SyncMachine); + + // @ts-ignore + await transitionUntilEvent(mysql, machine, INVALID_MEMPOOL_TRANSACTION_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, invalidMempoolBalances)); }); }); -describe('invalid mempool transactions scenario', () => { +describe('custom script scenario', () => { beforeAll(() => { jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); }); @@ -240,7 +241,7 @@ describe('invalid mempool transactions scenario', () => { FULLNODE_PEER_ID: 'simulator_peer_id', STREAM_ID: 'simulator_stream_id', FULLNODE_NETWORK: 'simulator_network', - FULLNODE_HOST: `127.0.0.1:${INVALID_MEMPOOL_TRANSACTION_PORT}`, + FULLNODE_HOST: `127.0.0.1:${CUSTOM_SCRIPT_PORT}`, USE_SSL: false, DB_ENDPOINT, DB_NAME, @@ -251,25 +252,45 @@ describe('invalid mempool transactions scenario', () => { const machine = interpret(SyncMachine); - await new Promise((resolve) => { - machine.onTransition(async (state) => { - const addressBalances = await fetchAddressBalances(mysql); - if (state.matches('CONNECTED.idle')) { - // @ts-ignore - const lastSyncedEvent = await getLastSyncedEvent(mysql); - console.log(lastSyncedEvent); - if (lastSyncedEvent?.last_event_id === INVALID_MEMPOOL_TRANSACTION_LAST_EVENT) { - // @ts-ignore - expect(validateBalances(addressBalances, invalidMempoolBalances)); - - machine.stop(); - - resolve(); - } - } - }); - - machine.start(); + // @ts-ignore + await transitionUntilEvent(mysql, machine, CUSTOM_SCRIPT_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, customScriptBalances)); + }); +}); + +describe('empty script scenario', () => { + beforeAll(() => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + }); + + it('should do a full sync and the balances should match', async () => { + // @ts-ignore + 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: 'simulator_network', + FULLNODE_HOST: `127.0.0.1:${EMPTY_SCRIPT_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, }); + + const machine = interpret(SyncMachine); + + // @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__/integration/config.ts b/packages/daemon/__tests__/integration/config.ts index 53619d93..2ffa6812 100644 --- a/packages/daemon/__tests__/integration/config.ts +++ b/packages/daemon/__tests__/integration/config.ts @@ -28,4 +28,17 @@ export const SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT = 37; export const INVALID_MEMPOOL_TRANSACTION_PORT = 8085; export const INVALID_MEMPOOL_TRANSACTION_LAST_EVENT = 40; -export const SCENARIOS = ['UNVOIDED_SCENARIO', 'REORG_SCENARIO', 'SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS', 'INVALID_MEMPOOL_TRANSACTION']; +export const CUSTOM_SCRIPT_PORT = 8086; +export const CUSTOM_SCRIPT_LAST_EVENT = 37; + +export const EMPTY_SCRIPT_PORT = 8087; +export const EMPTY_SCRIPT_LAST_EVENT = 37; + +export const SCENARIOS = [ + 'UNVOIDED_SCENARIO', + 'REORG_SCENARIO', + 'SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS', + 'INVALID_MEMPOOL_TRANSACTION', + 'EMPTY_SCRIPT', + 'CUSTOM_SCRIPT', +]; diff --git a/packages/daemon/__tests__/integration/scenario_configs/custom_script.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/custom_script.balances.ts new file mode 100644 index 00000000..804cfdcf --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/custom_script.balances.ts @@ -0,0 +1,16 @@ +export default { + "HVayMofEDh4XGsaQJeRJKhutYxYodYNop6": 100000000000, + "HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns": 6400, + "HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ": 6400, + "HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu": 6400, + "HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW": 6400, + "HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26": 6400, + "HQijr325t63VJFdc4vYkaTyd87oeBLpSed": 6400, + "H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR": 6400, + "HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ": 6400, + "HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8": 6400, + "HJPSMHCFv2dRb78wZPMsAzwLQHSkBpfuLn": 6400, + "HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh": 1000, + "H9hHteu9QdAS5p6X743Mpfue6G19rV9GeY": 5400, + "HSd6PqXesUmHHv6MoN24aUiMuw7Pdcxrwk": 6400 +}; diff --git a/packages/daemon/__tests__/integration/scenario_configs/empty_script.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/empty_script.balances.ts new file mode 100644 index 00000000..e901327a --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/empty_script.balances.ts @@ -0,0 +1,16 @@ +export default { + "HVayMofEDh4XGsaQJeRJKhutYxYodYNop6": 100000000000, + "HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh": 6400, + "HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns": 6400, + "HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ": 6400, + "HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu": 6400, + "HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW": 6400, + "HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26": 6400, + "HQijr325t63VJFdc4vYkaTyd87oeBLpSed": 6400, + "H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR": 6400, + "HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ": 6400, + "HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8": 6400, + "HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs": 1000, + "HNqTfEASfdx7H4vMUGzfD2HyD3GeKuxjTJ": 5400, + "HRH8Wbmr1A3BrLswSBhvVE4hhsv4jUdyVA": 6400 +}; diff --git a/packages/daemon/__tests__/integration/scripts/docker-compose.yml b/packages/daemon/__tests__/integration/scripts/docker-compose.yml index 8296ba8a..afa5b5d4 100644 --- a/packages/daemon/__tests__/integration/scripts/docker-compose.yml +++ b/packages/daemon/__tests__/integration/scripts/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: mysql: image: mysql @@ -9,6 +7,7 @@ services: MYSQL_ROOT_PASSWORD: hathor ports: - "3380:3306" + unvoided_transaction: image: hathornetwork/hathor-core:stable command: [ @@ -46,5 +45,25 @@ services: ports: - "8085:8080" + custom_scripts: + image: hathornetwork/hathor-core:stable + command: [ + "events_simulator", + "--scenario", "CUSTOM_SCRIPT", + "--seed", "1" + ] + ports: + - "8086:8080" + + empty_script: + image: hathornetwork/hathor-core:stable + command: [ + "events_simulator", + "--scenario", "EMPTY_SCRIPT", + "--seed", "1" + ] + ports: + - "8087:8080" + networks: database: diff --git a/packages/daemon/__tests__/integration/utils/index.ts b/packages/daemon/__tests__/integration/utils/index.ts index 238e0eb4..fd38b055 100644 --- a/packages/daemon/__tests__/integration/utils/index.ts +++ b/packages/daemon/__tests__/integration/utils/index.ts @@ -5,7 +5,9 @@ * LICENSE file in the root directory of this source tree. */ import { Connection } from 'mysql2/promise'; -import { AddressBalance, AddressBalanceRow } from '../../../src/types'; +import { Interpreter } from 'xstate'; +import { getLastSyncedEvent } from '../../../src/db'; +import { AddressBalance, AddressBalanceRow, Context, Event } from '../../../src/types'; export const cleanDatabase = async (mysql: Connection): Promise => { const TABLES = [ @@ -68,9 +70,25 @@ export const validateBalances = async ( const totalBalanceA = balanceA.lockedBalance + balanceA.unlockedBalance; if (totalBalanceA !== balanceB) { - console.log(totalBalanceA); - console.log(balanceB); throw new Error(`Balances are not equal for address: ${address}, expected: ${balanceB}, received: ${totalBalanceA}`); } } }; + +export async function transitionUntilEvent(mysql: Connection, machine: Interpreter, eventId: number) { + return await new Promise((resolve) => { + machine.onTransition(async (state) => { + if (state.matches('CONNECTED.idle')) { + // @ts-ignore + const lastSyncedEvent = await getLastSyncedEvent(mysql); + if (lastSyncedEvent?.last_event_id === eventId) { + machine.stop(); + + resolve(); + } + } + }); + + machine.start(); + }); +} diff --git a/packages/daemon/src/utils/wallet.ts b/packages/daemon/src/utils/wallet.ts index d9b6a34c..4e0ac6cb 100644 --- a/packages/daemon/src/utils/wallet.ts +++ b/packages/daemon/src/utils/wallet.ts @@ -125,7 +125,9 @@ export const getAddressBalanceMap = ( for (const input of inputs) { if (!input.decoded) { - throw new Error('Input has no decoded script'); + // If we're unable to decode the script, we will also be unable to + // calculate the balance, so just skip this input. + continue; } const address = input.decoded?.address; @@ -294,11 +296,11 @@ export const prepareInputs = (inputs: EventTxInput[], tokens: string[]): TxInput // @ts-ignore script: utxo.script, token, - decoded: { + decoded: output.decoded ? { type: output.decoded.type, address: output.decoded.address, timelock: output.decoded.timelock, - }, + } : null, }; return [...newInputs, input];