diff --git a/db/migrations/20241203185227-add-address-wallet_id-index.js b/db/migrations/20241203185227-add-address-wallet_id-index.js new file mode 100644 index 00000000..b7d67a25 --- /dev/null +++ b/db/migrations/20241203185227-add-address-wallet_id-index.js @@ -0,0 +1,18 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface) { + await queryInterface.addIndex( + 'address', + ['wallet_id', 'index'], + { + name: 'idx_wallet_address_index', + } + ); + }, + + async down(queryInterface) { + await queryInterface.removeIndex('address', 'idx_wallet_address_index'); + } +}; diff --git a/packages/daemon/__tests__/db/index.test.ts b/packages/daemon/__tests__/db/index.test.ts index b9a389bc..930ed492 100644 --- a/packages/daemon/__tests__/db/index.test.ts +++ b/packages/daemon/__tests__/db/index.test.ts @@ -12,13 +12,13 @@ import { addUtxos, fetchAddressBalance, fetchAddressTxHistorySum, - generateAddresses, getAddressWalletInfo, getBestBlockHeight, getDbConnection, getExpiredTimelocksUtxos, getLastSyncedEvent, getLockedUtxoFromInputs, + getMaxIndicesForWallets, getMinersList, getTokenInformation, getTokenSymbols, @@ -73,6 +73,7 @@ import { DbTxOutput, StringMap, TokenInfo, WalletStatus } from '../../src/types' import { Authorities, TokenBalanceMap } from '@wallet-service/common'; // @ts-ignore import { constants } from '@hathor/wallet-lib'; +import { generateAddresses } from '../../src/utils'; // Use a single mysql connection for all tests let mysql: Connection; @@ -782,84 +783,69 @@ describe('address and wallet related tests', () => { await expect(checkWalletBalanceTable(mysql, 3, wallet1, tokenId, 25, 5, now, 5, 0b11, 0b01)).resolves.toBe(true); }); - test('generateAddresses', async () => { + test('should generate addresses correctly', async () => { expect.hasAssertions(); + const maxGap = 5; const address0 = ADDRESSES[0]; + const address1 = ADDRESSES[1]; + const address4 = ADDRESSES[4]; // check first with no addresses on database, so it should return only maxGap addresses - let addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); + let addresses = await generateAddresses('mainnet', XPUBKEY, 0, maxGap); - expect(addressesInfo.addresses).toHaveLength(maxGap); - expect(addressesInfo.existingAddresses).toStrictEqual({}); - expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(maxGap); - expect(addressesInfo.addresses[0]).toBe(address0); + expect(Object.keys(addresses).length).toBe(maxGap); + expect(addresses[address0]).toBe(0); - // add first address with no transactions. As it's not used, we should still only generate maxGap addresses + // add address0 to the database with no transactions await addToAddressTable(mysql, [{ address: address0, index: 0, - walletId: null, + walletId: 'wallet1', transactions: 0, }]); - addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); - expect(addressesInfo.addresses).toHaveLength(maxGap); - expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0 }); - expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(-1); - let totalLength = Object.keys(addressesInfo.addresses).length; - let existingLength = Object.keys(addressesInfo.existingAddresses).length; - expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); - expect(addressesInfo.addresses[0]).toBe(address0); + addresses = await generateAddresses('mainnet', XPUBKEY, 0, maxGap); + expect(Object.keys(addresses).length).toBe(maxGap); + expect(addresses[address0]).toBe(0); - // mark address as used and check again + // now mark address0 as used let usedIndex = 0; await mysql.query('UPDATE `address` SET `transactions` = ? WHERE `address` = ?', [1, address0]); - addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); - expect(addressesInfo.addresses).toHaveLength(maxGap + usedIndex + 1); - expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0 }); - expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(0); - - totalLength = Object.keys(addressesInfo.addresses).length; - existingLength = Object.keys(addressesInfo.existingAddresses).length; - expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); + addresses = await generateAddresses('mainnet', XPUBKEY, 0, maxGap + usedIndex + 1); + expect(Object.keys(addresses).length).toBe(maxGap + usedIndex + 1); + expect(addresses[address0]).toBe(0); - // add address with index 1 as used + // add address1 to the database with transactions usedIndex = 1; - const address1 = ADDRESSES[1]; await addToAddressTable(mysql, [{ address: address1, - index: usedIndex, - walletId: null, + index: 1, + walletId: 'wallet1', transactions: 1, }]); - addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); - expect(addressesInfo.addresses).toHaveLength(maxGap + usedIndex + 1); - expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0, [address1]: 1 }); - expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(1); - totalLength = Object.keys(addressesInfo.addresses).length; - existingLength = Object.keys(addressesInfo.existingAddresses).length; - expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); - - // add address with index 4 as used + + addresses = await generateAddresses('mainnet', XPUBKEY, 0, maxGap + usedIndex + 1); + expect(Object.keys(addresses).length).toBe(maxGap + usedIndex + 1); + expect(addresses[address0]).toBe(0); + expect(addresses[address1]).toBe(1); + + // add address4 to the database with transactions usedIndex = 4; - const address4 = ADDRESSES[4]; await addToAddressTable(mysql, [{ address: address4, - index: usedIndex, - walletId: null, + index: 4, + walletId: 'wallet1', transactions: 1, }]); - addressesInfo = await generateAddresses(mysql, XPUBKEY, maxGap); - expect(addressesInfo.addresses).toHaveLength(maxGap + usedIndex + 1); - expect(addressesInfo.existingAddresses).toStrictEqual({ [address0]: 0, [address1]: 1, [address4]: 4 }); - expect(addressesInfo.lastUsedAddressIndex).toStrictEqual(4); - totalLength = Object.keys(addressesInfo.addresses).length; - existingLength = Object.keys(addressesInfo.existingAddresses).length; - expect(Object.keys(addressesInfo.newAddresses)).toHaveLength(totalLength - existingLength); + + addresses = await generateAddresses('mainnet', XPUBKEY, 0, maxGap + usedIndex + 1); + expect(Object.keys(addresses).length).toBe(maxGap + usedIndex + 1); + expect(addresses[address0]).toBe(0); + expect(addresses[address4]).toBe(4); // make sure no address was skipped from being generated - for (const [index, address] of addressesInfo.addresses.entries()) { + for (const [address, index] of Object.entries(addresses)) { expect(ADDRESSES[index]).toBe(address); } }, 15000); @@ -1273,3 +1259,90 @@ describe('voidTransaction', () => { await expect(voidTransaction(mysql, 'mysterious-transaction', {})).rejects.toThrow('Tried to void a transaction that is not in the database.'); }); }); + +describe('address generation and index methods', () => { + test('generateAddresses should generate correct addresses', async () => { + expect.hasAssertions(); + + const startIndex = 0; + const count = 3; + const addresses = await generateAddresses('mainnet', XPUBKEY, startIndex, count); + + // Check if we got the expected number of addresses + expect(Object.keys(addresses).length).toBe(count); + + // Check if the addresses are mapped to correct indices + Object.entries(addresses).forEach(([address, index]) => { + expect(typeof address).toBe('string'); + expect(index).toBeGreaterThanOrEqual(startIndex); + expect(index).toBeLessThan(startIndex + count); + }); + }); + + test('getMaxIndicesForWallets should return correct indices for multiple wallets', async () => { + expect.hasAssertions(); + + const wallet1 = 'wallet1'; + const wallet2 = 'wallet2'; + const addresses1 = ['addr1', 'addr2', 'addr3']; + const addresses2 = ['addr4', 'addr5']; + const indices1 = [5, 10, 15]; + const indices2 = [7, 12]; + + // Add addresses for wallet1 + const entries1 = addresses1.map((address, i) => ({ + address, + index: indices1[i], + walletId: wallet1, + transactions: 0, + })); + await addToAddressTable(mysql, entries1); + + // Add addresses for wallet2 + const entries2 = addresses2.map((address, i) => ({ + address, + index: indices2[i], + walletId: wallet2, + transactions: 0, + })); + await addToAddressTable(mysql, entries2); + + // Test getting indices for both wallets + const walletData = [ + { walletId: wallet1, addresses: addresses1 }, + { walletId: wallet2, addresses: addresses2 }, + ]; + const indices = await getMaxIndicesForWallets(mysql, walletData); + + // Check wallet1 indices + const wallet1Indices = indices.get(wallet1); + expect(wallet1Indices).toBeDefined(); + expect(wallet1Indices?.maxAmongAddresses).toBe(15); + expect(wallet1Indices?.maxWalletIndex).toBe(15); + + // Check wallet2 indices + const wallet2Indices = indices.get(wallet2); + expect(wallet2Indices).toBeDefined(); + expect(wallet2Indices?.maxAmongAddresses).toBe(12); + expect(wallet2Indices?.maxWalletIndex).toBe(12); + + // Test with empty wallet data + const emptyIndices = await getMaxIndicesForWallets(mysql, []); + expect(emptyIndices.size).toBe(0); + + // Test with non-existent wallet + const nonExistentIndices = await getMaxIndicesForWallets(mysql, [ + { walletId: 'nonexistent', addresses: ['addr1'] } + ]); + expect(nonExistentIndices.size).toBe(0); + + // Test with subset of addresses + const subsetIndices = await getMaxIndicesForWallets(mysql, [ + { walletId: wallet1, addresses: addresses1.slice(0, 2) } + ]); + const subsetWallet1 = subsetIndices.get(wallet1); + expect(subsetWallet1).toBeDefined(); + expect(subsetWallet1?.maxAmongAddresses).toBe(10); + expect(subsetWallet1?.maxWalletIndex).toBe(15); + }); +}); diff --git a/packages/daemon/__tests__/services/services.test.ts b/packages/daemon/__tests__/services/services.test.ts index d0baf803..a57ea954 100644 --- a/packages/daemon/__tests__/services/services.test.ts +++ b/packages/daemon/__tests__/services/services.test.ts @@ -19,8 +19,8 @@ import { getUtxosLockedAtHeight, addOrUpdateTx, getAddressWalletInfo, - generateAddresses, storeTokenInformation, + getMaxIndicesForWallets, } from '../../src/db'; import { fetchInitialState, @@ -39,6 +39,7 @@ import { getFullnodeHttpUrl, invokeOnTxPushNotificationRequestedLambda, getWalletBalancesForTx, + generateAddresses, } from '../../src/utils'; import getConfig from '../../src/config'; @@ -83,6 +84,9 @@ jest.mock('../../src/db', () => ({ generateAddresses: jest.fn(), addNewAddresses: jest.fn(), updateWalletTablesWithTx: jest.fn(), + getMaxIndicesForWallets: jest.fn(() => new Map([ + ['wallet1', { maxAmongAddresses: 10, maxWalletIndex: 15 }] + ])), })); jest.mock('../../src/utils', () => ({ @@ -102,6 +106,7 @@ jest.mock('../../src/utils', () => ({ invokeOnTxPushNotificationRequestedLambda: jest.fn(), sendMessageSQS: jest.fn(), getWalletBalancesForTx: jest.fn(), + generateAddresses: jest.fn(), })); beforeEach(() => { @@ -454,11 +459,18 @@ describe('handleVertexAccepted', () => { jest.clearAllMocks(); (getDbConnection as jest.Mock).mockResolvedValue(mockDb); - (getAddressWalletInfo as jest.Mock).mockResolvedValue({}); + (getAddressWalletInfo as jest.Mock).mockResolvedValue({ + address1: { walletId: 'wallet1', xpubkey: 'xpubkey1', maxGap: 10 } + }); + (generateAddresses as jest.Mock).mockResolvedValue({ - newAddresses: ['mockAddress1', 'mockAddress2'], - lastUsedAddressIndex: 1 + 'new-address-1': 16, + 'new-address-2': 17, }); + + (getMaxIndicesForWallets as jest.Mock).mockResolvedValue(new Map([ + ['wallet1', { maxAmongAddresses: 10, maxWalletIndex: 15 }] + ])); }); it('should handle vertex accepted successfully', async () => { diff --git a/packages/daemon/src/db/index.ts b/packages/daemon/src/db/index.ts index 4148792b..7ea1ab56 100644 --- a/packages/daemon/src/db/index.ts +++ b/packages/daemon/src/db/index.ts @@ -19,6 +19,7 @@ import { TokenInfo, Miner, TokenSymbolsRow, + MaxAddressIndexRow, } from '../types'; import { TxInput, @@ -36,8 +37,6 @@ import { TransactionRow, TxOutputRow, } from '../types'; -// @ts-ignore -import { walletUtils } from '@hathor/wallet-lib'; import getConfig from '../config'; let pool: Pool; @@ -1057,81 +1056,6 @@ export const incrementTokensTxCount = async ( `, [tokenList]); }; -/** - * Given an xpubkey, generate its addresses. - * - * @remarks - * Also, check which addresses are used, taking into account the maximum gap of unused addresses (maxGap). - * This function doesn't update anything on the database, just reads data from it. - * - * @param mysql - Database connection - * @param xpubkey - The xpubkey - * @param maxGap - Number of addresses that should have no transactions before we consider all addresses loaded - * @returns Object with all addresses for the given xpubkey and corresponding index - */ -export const generateAddresses = async (mysql: MysqlConnection, xpubkey: string, maxGap: number): Promise => { - const existingAddresses: AddressIndexMap = {}; - const newAddresses: AddressIndexMap = {}; - const allAddresses: string[] = []; - - // We currently generate only addresses in change derivation path 0 - // (more details in https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#Change) - // so we derive our xpub to this path and use it to get the addresses - const derivedXpub = walletUtils.xpubDeriveChild(xpubkey, 0); - - let highestCheckedIndex = -1; - let lastUsedAddressIndex = -1; - do { - const { NETWORK } = getConfig(); - const addrMap = walletUtils.getAddresses(derivedXpub, highestCheckedIndex + 1, maxGap, NETWORK); - allAddresses.push(...Object.keys(addrMap)); - - const [results] = await mysql.query( - `SELECT \`address\`, - \`index\`, - \`transactions\` - FROM \`address\` - WHERE \`address\` - IN (?)`, - [Object.keys(addrMap)], - ); - - for (const entry of results) { - const address = entry.address as string; - // get index from addrMap as the one from entry might be null - const index = addrMap[address]; - // add to existingAddresses - existingAddresses[address] = index; - - // if address is used, check if its index is higher than the current highest used index - if (entry.transactions > 0 && index > lastUsedAddressIndex) { - lastUsedAddressIndex = index; - } - - delete addrMap[address]; - } - - highestCheckedIndex += maxGap; - Object.assign(newAddresses, addrMap); - } while (lastUsedAddressIndex + maxGap > highestCheckedIndex); - - // we probably generated more addresses than needed, as we always generate - // addresses in maxGap blocks - const totalAddresses = lastUsedAddressIndex + maxGap + 1; - for (const [address, index] of Object.entries(newAddresses)) { - if (index > lastUsedAddressIndex + maxGap) { - delete newAddresses[address]; - } - } - - return { - addresses: allAddresses.slice(0, totalAddresses), - newAddresses, - existingAddresses, - lastUsedAddressIndex, - }; -}; - /** * Add addresses to address table. * @@ -1609,3 +1533,58 @@ export const getTokenSymbols = async ( return prev; }, {}) as unknown as StringMap; }; + +/** + * Get maximum indices for multiple wallets in a single query. + * + * This function retrieves two key metrics for each wallet: + * + * 1. `max_among_addresses`: The highest `index` value for the wallet, but only considering the specified addresses provided in `walletData`. + * 2. `max_wallet_index`: The highest `index` value for the wallet across all its addresses in the database. + * + * How it works: + * - The SQL query operates on the `address` table. + * - It groups the rows by `wallet_id` using `GROUP BY wallet_id`. + * - For each wallet group: + * - The `MAX` function calculates the highest `index` value in two contexts: + * a. For addresses explicitly listed in the input (`CASE WHEN address IN (?) THEN index END`). + * b. For all addresses associated with the wallet (`MAX(index)`). + * - If no addresses for a wallet match the provided input, `max_among_addresses` will be `NULL`. + * - If a wallet has no addresses in the database, both `max_among_addresses` and `max_wallet_index` will be `NULL`. + * + * This allows the function to return a consolidated view of the maximum indices for each wallet + * + * @param mysql - Database connection + * @param walletData - Array of objects containing wallet IDs and their associated addresses + * @returns Map of wallet IDs to their maximum indices (both among specific addresses and overall) + */ +export const getMaxIndicesForWallets = async ( + mysql: MysqlConnection, + walletData: Array<{walletId: string, addresses: string[]}> +): Promise> => { + if (walletData.length === 0) { + return new Map(); + } + + const allAddresses = walletData.flatMap(d => d.addresses); + const walletIds = walletData.map(d => d.walletId); + + const [results] = await mysql.query( + `SELECT + wallet_id, + MAX(CASE WHEN address IN (?) THEN \`index\` END) as max_among_addresses, + MAX(\`index\`) as max_wallet_index + FROM address + WHERE wallet_id IN (?) + GROUP BY wallet_id`, + [allAddresses, walletIds] + ); + + return new Map(results.map(r => [ + r.wallet_id, + { + maxAmongAddresses: r.max_among_addresses, + maxWalletIndex: r.max_wallet_index + } + ])); +}; diff --git a/packages/daemon/src/services/index.ts b/packages/daemon/src/services/index.ts index 128f2812..77e5fc69 100644 --- a/packages/daemon/src/services/index.ts +++ b/packages/daemon/src/services/index.ts @@ -22,6 +22,7 @@ import { FullNodeEvent, EventTxInput, EventTxOutput, + WalletStatus, } from '../types'; import { TxInput, @@ -42,6 +43,8 @@ import { validateAddressBalances, getWalletBalancesForTx, getFullnodeHttpUrl, + sendMessageSQS, + generateAddresses, } from '../utils'; import { getDbConnection, @@ -56,7 +59,6 @@ import { getLockedUtxoFromInputs, incrementTokensTxCount, getAddressWalletInfo, - generateAddresses, addNewAddresses, updateWalletTablesWithTx, voidTransaction, @@ -65,10 +67,11 @@ import { getTxOutputsFromTx, markUtxosAsVoided, cleanupVoidedTx, + getMaxIndicesForWallets, } from '../db'; import getConfig from '../config'; import logger from '../logger'; -import { invokeOnTxPushNotificationRequestedLambda, sendMessageSQS } from '../utils'; +import { invokeOnTxPushNotificationRequestedLambda } from '../utils'; export const METADATA_DIFF_EVENT_TYPES = { IGNORE: 'IGNORE', @@ -162,11 +165,16 @@ export const isBlock = (version: number): boolean => version === hathorLib.const export const handleVertexAccepted = async (context: Context, _event: Event) => { const mysql = await getDbConnection(); await mysql.beginTransaction(); + const { + NETWORK, + STAGE, + PUSH_NOTIFICATION_ENABLED, + NEW_TX_SQS, + } = getConfig(); try { const fullNodeEvent = context.event as FullNodeEvent; const now = getUnixTimestamp(); - const { NEW_TX_SQS, PUSH_NOTIFICATION_ENABLED } = getConfig(); const blockRewardLock = context.rewardMinBlocks; if (!blockRewardLock) { @@ -272,6 +280,8 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { // Add utxos await addUtxos(mysql, hash, txOutputs, heightlock); + + // Mark tx utxos as spent await updateTxOutputSpentBy(mysql, txInputs, hash); // Genesis tx has no inputs and outputs, so nothing to be updated, avoid it @@ -289,31 +299,78 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { // for the addresses present on the tx, check if there are any wallets associated const addressWalletMap: StringMap = await getAddressWalletInfo(mysql, Object.keys(addressBalanceMap)); - // for each already started wallet, update databases - const seenWallets = new Set(); - for (const wallet of Object.values(addressWalletMap)) { - const walletId = wallet.walletId; - - // this map might contain duplicate wallet values, as 2 different addresses might belong to the same wallet - if (seenWallets.has(walletId)) continue; - seenWallets.add(walletId); - const { newAddresses, lastUsedAddressIndex } = await generateAddresses(mysql, wallet.xpubkey, wallet.maxGap); - // might need to generate new addresses to keep maxGap - await addNewAddresses(mysql, walletId, newAddresses, lastUsedAddressIndex); - // update existing addresses' walletId and index + 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, + } + } + + // Add the current key to the array + result[walletId].addresses.push(address); + + return result; + }, {}); + + const seenWallets = Object.keys(addressesPerWallet); + + // Convert to array format expected by getMaxIndicesForWallets + const walletDataArray = Object.entries(addressesPerWallet).map(([walletId, data]) => ({ + walletId, + addresses: data.addresses + })); + + // Get all max indices in a single query + const walletIndices = await getMaxIndicesForWallets(mysql, walletDataArray); + + // Process each wallet + for (const [walletId, data] of Object.entries(addressesPerWallet)) { + const { walletDetails } = data; + const indices = walletIndices.get(walletId); + + if (!indices) { + // This is unexpected as we just queried for this wallet + logger.error('Failed to get indices for wallet', { walletId }); + continue; + } + + const { maxAmongAddresses, maxWalletIndex } = indices; + + if (!maxAmongAddresses || !maxWalletIndex) { + // Do nothing, wallet is most likely not loaded yet. + if (walletDetails.status === WalletStatus.READY) { + logger.error('[ERROR] A wallet marked as READY does not have a max wallet index or address index was not found in the database'); + } + continue; + } + + const diff = maxWalletIndex - maxAmongAddresses; + + if (diff < walletDetails.maxGap) { + // We need to generate addresses + const addresses = await generateAddresses(NETWORK as string, walletDetails.xpubkey, maxWalletIndex + 1, walletDetails.maxGap - diff); + await addNewAddresses(mysql, walletId, addresses, maxAmongAddresses); + } } + // update wallet_balance and wallet_tx_history tables const walletBalanceMap: StringMap = getWalletBalanceMap(addressWalletMap, addressBalanceMap); await updateWalletTablesWithTx(mysql, hash, timestamp, walletBalanceMap); - const tx: Transaction = { + // prepare the transaction data to be sent to the SQS queue + const txData: Transaction = { tx_id: hash, nonce, timestamp, + version, voided: metadata.voided_by.length > 0, weight, parents, - version, inputs: txInputs, outputs: txOutputs, height: metadata.height, @@ -323,7 +380,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { }; try { - if (seenWallets.size > 0) { + if (seenWallets.length > 0) { const queueUrl = NEW_TX_SQS; if (!queueUrl) { throw new Error('Queue URL is invalid'); @@ -331,7 +388,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { await sendMessageSQS(JSON.stringify({ wallets: Array.from(seenWallets), - tx, + txData, }), queueUrl); } } catch (e) { @@ -341,7 +398,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { try { if (PUSH_NOTIFICATION_ENABLED) { - const walletBalanceMap = await getWalletBalancesForTx(mysql, tx); + const walletBalanceMap = await getWalletBalancesForTx(mysql, txData); const { length: hasAffectWallets } = Object.keys(walletBalanceMap); if (hasAffectWallets) { invokeOnTxPushNotificationRequestedLambda(walletBalanceMap) @@ -353,18 +410,13 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { logger.error(e); } - const { - NETWORK, - STAGE, - } = getConfig(); - const network = new hathorLib.Network(NETWORK); // Validating for NFTs only after the tx is successfully added - if (NftUtils.shouldInvokeNftHandlerForTx(tx, network, logger)) { + if (NftUtils.shouldInvokeNftHandlerForTx(txData, network, logger)) { // This process is not critical, so we run it in a fire-and-forget manner, not waiting for the promise. // In case of errors, just log the asynchronous exception and take no action on it. - NftUtils.invokeNftHandlerLambda(tx.tx_id, STAGE, logger) + NftUtils.invokeNftHandlerLambda(txData.tx_id, STAGE, logger) .catch((err: unknown) => logger.error('[ALERT] Error on nftHandlerLambda invocation', err)); } } @@ -374,7 +426,10 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { await mysql.commit(); } catch (e) { await mysql.rollback(); - logger.error(e); + logger.error('Error handling vertex accepted', { + error: (e as Error).message, + stack: (e as Error).stack, + }); throw e; } finally { diff --git a/packages/daemon/src/types/db.ts b/packages/daemon/src/types/db.ts index 2f8c413a..b9685685 100644 --- a/packages/daemon/src/types/db.ts +++ b/packages/daemon/src/types/db.ts @@ -140,3 +140,8 @@ export interface TokenSymbolsRow extends RowDataPacket { id: string; symbol: string; } + +export interface MaxAddressIndexRow extends RowDataPacket { + max_among_addresses: number, + max_wallet_index: number +} diff --git a/packages/daemon/src/utils/wallet.ts b/packages/daemon/src/utils/wallet.ts index 4e0ac6cb..11a2b0bc 100644 --- a/packages/daemon/src/utils/wallet.ts +++ b/packages/daemon/src/utils/wallet.ts @@ -40,6 +40,8 @@ import { updateWalletLockedBalance, } from '../db'; import logger from '../logger'; +// @ts-ignore +import { walletUtils } from '@hathor/wallet-lib'; import { stringMapIterator } from './helpers'; /** @@ -507,3 +509,29 @@ export class WalletBalanceMapConverter { return walletBalanceValueMap; } } + +/** + * Generate a batch of addresses from a given xpubkey. + * + * @remarks + * This function generates addresses starting from a specific index. + * + * @param xpubkey - The extended public key to derive addresses from + * @param startIndex - The index to start generating addresses from + * @param count - How many addresses to generate + * @returns A map of addresses to their corresponding indices + */ +export const generateAddresses = async ( + network: string, + xpubkey: string, + startIndex: number, + count: number, +): Promise> => { + // We currently generate only addresses in change derivation path 0 + // (more details in https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#Change) + // so we derive our xpub to this path and use it to get the addresses + const derivedXpub = walletUtils.xpubDeriveChild(xpubkey, 0); + const addrMap = walletUtils.getAddresses(derivedXpub, startIndex, count, network); + + return addrMap; +};