Skip to content

Commit

Permalink
refactor: using a single query
Browse files Browse the repository at this point in the history
  • Loading branch information
andreabadesso committed Dec 3, 2024
1 parent 26a5bbe commit 26225b1
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 159 deletions.
94 changes: 53 additions & 41 deletions packages/daemon/__tests__/db/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import {
getExpiredTimelocksUtxos,
getLastSyncedEvent,
getLockedUtxoFromInputs,
getMaxIndexAmongAddresses,
getMaxWalletAddressIndex,
getMaxIndicesForWallets,
getMinersList,
getTokenInformation,
getTokenSymbols,
Expand Down Expand Up @@ -1280,57 +1279,70 @@ describe('address generation and index methods', () => {
});
});

test('getMaxIndexAmongAddresses should return correct max index', async () => {
test('getMaxIndicesForWallets should return correct indices for multiple wallets', async () => {
expect.hasAssertions();

const walletId = 'wallet1';
const addresses = ['addr1', 'addr2', 'addr3'];
const indices = [5, 10, 15];
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 to the database with different indices
const entries = addresses.map((address, i) => ({
// Add addresses for wallet1
const entries1 = addresses1.map((address, i) => ({
address,
index: indices[i],
walletId,
index: indices1[i],
walletId: wallet1,
transactions: 0,
}));
await addToAddressTable(mysql, entries);

// Test getting max index for all addresses
const maxIndex = await getMaxIndexAmongAddresses(mysql, walletId, addresses);
expect(maxIndex).toBe(15);

// Test getting max index for subset of addresses
const maxIndexSubset = await getMaxIndexAmongAddresses(mysql, walletId, addresses.slice(0, 2));
expect(maxIndexSubset).toBe(10);

// Test with non-existent addresses
const maxIndexNonExistent = await getMaxIndexAmongAddresses(mysql, walletId, ['nonexistent']);
expect(maxIndexNonExistent).toBeNull();
});

test('getMaxWalletAddressIndex should return correct max index', async () => {
expect.hasAssertions();
await addToAddressTable(mysql, entries1);

const walletId = 'wallet1';
const addresses = ['addr1', 'addr2', 'addr3'];
const indices = [5, 10, 15];

// Add addresses to the database with different indices
const entries = addresses.map((address, i) => ({
// Add addresses for wallet2
const entries2 = addresses2.map((address, i) => ({
address,
index: indices[i],
walletId,
index: indices2[i],
walletId: wallet2,
transactions: 0,
}));
await addToAddressTable(mysql, entries);
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);

// Test getting max index for the wallet
const maxIndex = await getMaxWalletAddressIndex(mysql, walletId);
expect(maxIndex).toBe(15);
// 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 maxIndexNonExistent = await getMaxWalletAddressIndex(mysql, 'nonexistent');
expect(maxIndexNonExistent).toBeNull();
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);
});
});
19 changes: 14 additions & 5 deletions packages/daemon/__tests__/services/services.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getAddressWalletInfo,
generateAddresses,
storeTokenInformation,
getMaxIndicesForWallets,
} from '../../src/db';
import {
fetchInitialState,
Expand Down Expand Up @@ -83,8 +84,9 @@ jest.mock('../../src/db', () => ({
generateAddresses: jest.fn(),
addNewAddresses: jest.fn(),
updateWalletTablesWithTx: jest.fn(),
getMaxIndexAmongAddresses: jest.fn(),
getMaxWalletAddressIndex: jest.fn(),
getMaxIndicesForWallets: jest.fn(() => new Map([
['wallet1', { maxAmongAddresses: 10, maxWalletIndex: 15 }]
])),
}));

jest.mock('../../src/utils', () => ({
Expand Down Expand Up @@ -456,11 +458,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 () => {
Expand Down
96 changes: 36 additions & 60 deletions packages/daemon/src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,16 +1059,15 @@ export const incrementTokensTxCount = async (
};

/**
* Given an xpubkey, generate its addresses.
* Generate a batch of addresses from a given xpubkey.
*
* @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
* 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 (
xpubkey: string,
Expand Down Expand Up @@ -1564,66 +1563,43 @@ export const getTokenSymbols = async (
};

/**
* Get the maximum index among a set of addresses for a specific wallet.
*
* Get maximum indices for multiple wallets in a single query.
*
* @remarks
* This function is used to find the highest index used among a set of addresses that belong to a wallet.
* This is particularly useful when we need to determine where to start generating new addresses from.
*
* This is an optimized version that combines both getMaxIndexAmongAddresses and getMaxWalletAddressIndex
* into a single query for multiple wallets. This reduces the number of database round trips significantly.
*
* @param mysql - Database connection
* @param walletId - The ID of the wallet to check
* @param addresses - Array of addresses to check
* @returns The highest index found among the addresses, or null if no addresses are found
* @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 getMaxIndexAmongAddresses = async (
export const getMaxIndicesForWallets = async (
mysql: MysqlConnection,
walletId: string,
addresses: string[],
): Promise<number | null> => {
const [results] = await mysql.query<MaxAddressIndexRow[]>(
`SELECT MAX(\`index\`) AS max_index
FROM address
WHERE wallet_id = ?
AND address
IN (?)
`, [walletId, addresses]
);

if (results.length <= 0) {
console.error(`[ERROR] Addresses not found in database for the wallet (${walletId}), this should never happen: ${JSON.stringify(addresses)}`);
return null;
walletData: Array<{walletId: string, addresses: string[]}>
): Promise<Map<string, {maxAmongAddresses: number | null, maxWalletIndex: number | null}>> => {
if (walletData.length === 0) {
return new Map();
}

return results[0].max_index;
};
const allAddresses = walletData.flatMap(d => d.addresses);
const walletIds = walletData.map(d => d.walletId);

/**
* Get the maximum index used by any address in a wallet.
*
* @remarks
* This function retrieves the highest index used by any address in the given wallet.
* This is essential for wallet synchronization and address generation, as it helps determine
* the starting point for generating new addresses and maintaining the gap limit.
*
* @param mysql - Database connection
* @param walletId - The ID of the wallet to check
* @returns The highest index used in the wallet, or null if no addresses are found
*/
export const getMaxWalletAddressIndex = async (
mysql: MysqlConnection,
walletId: string,
): Promise<number | null> => {
const [results] = await mysql.query<MaxAddressIndexRow[]>(
`SELECT MAX(\`index\`) AS max_index
FROM address
WHERE wallet_id = ?
`, [walletId]
`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]
);

if (results.length <= 0) {
console.error(`[ERROR] Max index not found in database for the wallet (${walletId}), this should never happen.`);
return null;
}

return results[0].max_index;
return new Map(results.map(r => [
r.wallet_id,
{
maxAmongAddresses: r.max_among_addresses,
maxWalletIndex: r.max_wallet_index
}
]));
};
Loading

0 comments on commit 26225b1

Please sign in to comment.