From 8bfcc2793c6eb866a43c2bdc84c180ec9c43e507 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Thu, 12 Jun 2025 12:12:10 -0600 Subject: [PATCH 1/7] feat: customized cadence account loader bby --- .../customizedCadenceBulkAccountLoader.ts | 140 +++++++++++++ sdk/src/index.ts | 1 + ...customizedCadenceBulkAccountLoader.test.ts | 105 ++++++++++ sdk/tests/ci/verifyConstants.ts | 189 ++++++++++++++---- 4 files changed, 395 insertions(+), 40 deletions(-) create mode 100644 sdk/src/accounts/customizedCadenceBulkAccountLoader.ts create mode 100644 sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts diff --git a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts new file mode 100644 index 0000000000..388edf54dd --- /dev/null +++ b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts @@ -0,0 +1,140 @@ +import { BulkAccountLoader } from './bulkAccountLoader'; +import { Commitment, Connection, PublicKey } from '@solana/web3.js'; + +export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { + private customIntervalIds: Map; + private customPollingGroups: Map>; + private defaultPollingFrequency: number; + + constructor( + connection: Connection, + commitment: Commitment, + defaultPollingFrequency: number + ) { + super(connection, commitment, defaultPollingFrequency); + this.customIntervalIds = new Map(); + this.customPollingGroups = new Map(); + this.defaultPollingFrequency = defaultPollingFrequency; + } + + private updateCustomPolling(frequency: number): void { + const frequencyStr = frequency.toString(); + const existingInterval = this.customIntervalIds.get(frequencyStr); + if (existingInterval) { + clearInterval(existingInterval); + this.customIntervalIds.delete(frequencyStr); + } + + const group = this.customPollingGroups.get(frequency); + if (group && group.size > 0) { + const intervalId = setInterval(async () => { + const accounts = Array.from(group) + .map((key) => this.accountsToLoad.get(key)) + .filter((account) => account !== undefined); + + if (accounts.length > 0) { + await this.loadChunk([accounts]); + } + }, frequency); + this.customIntervalIds.set(frequencyStr, intervalId); + } + } + + public setCustomPollingFrequency( + publicKey: PublicKey, + newFrequency: number + ): void { + const key = publicKey.toBase58(); + + // Remove from old frequency group + for (const [frequency, group] of this.customPollingGroups.entries()) { + if (group.has(key)) { + group.delete(key); + if (group.size === 0) { + const intervalId = this.customIntervalIds.get(frequency.toString()); + if (intervalId) { + clearInterval(intervalId); + this.customIntervalIds.delete(frequency.toString()); + } + this.customPollingGroups.delete(frequency); + } + this.updateCustomPolling(frequency); + break; + } + } + + // Add to new frequency group + let group = this.customPollingGroups.get(newFrequency); + if (!group) { + group = new Set(); + this.customPollingGroups.set(newFrequency, group); + } + group.add(key); + + this.updateCustomPolling(newFrequency); + } + + public async addAccount( + publicKey: PublicKey, + callback: (buffer: Buffer, slot: number) => void, + customPollingFrequency?: number + ): Promise { + const id = await super.addAccount(publicKey, callback); + + const key = publicKey.toBase58(); + const frequency = customPollingFrequency || this.defaultPollingFrequency; + + // Add to frequency group + let group = this.customPollingGroups.get(frequency); + if (!group) { + group = new Set(); + this.customPollingGroups.set(frequency, group); + } + group.add(key); + + this.updateCustomPolling(frequency); + + return id; + } + + public removeAccount(publicKey: PublicKey, id?: string): void { + super.removeAccount(publicKey, id); + + const key = publicKey.toBase58(); + + // Remove from any polling groups + for (const [frequency, group] of this.customPollingGroups.entries()) { + if (group.has(key)) { + group.delete(key); + if (group.size === 0) { + const intervalId = this.customIntervalIds.get(frequency.toString()); + if (intervalId) { + clearInterval(intervalId); + this.customIntervalIds.delete(frequency.toString()); + } + this.customPollingGroups.delete(frequency); + } + this.updateCustomPolling(frequency); + break; + } + } + } + + public startPolling(): void { + // Don't start the default polling interval + // Only start custom polling for accounts that have custom frequencies + for (const frequency of this.customPollingGroups.keys()) { + this.updateCustomPolling(frequency); + } + } + + public stopPolling(): void { + super.stopPolling(); + + // Clear all custom intervals + for (const intervalId of this.customIntervalIds.values()) { + clearInterval(intervalId); + } + this.customIntervalIds.clear(); + } +} diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 966e574d8c..a07ea85d42 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -15,6 +15,7 @@ export * from './accounts/webSocketHighLeverageModeConfigAccountSubscriber'; export * from './accounts/bulkAccountLoader'; export * from './accounts/bulkUserSubscription'; export * from './accounts/bulkUserStatsSubscription'; +export { CustomizedCadenceBulkAccountLoader } from './accounts/customizedCadenceBulkAccountLoader'; export * from './accounts/pollingDriftClientAccountSubscriber'; export * from './accounts/pollingOracleAccountSubscriber'; export * from './accounts/pollingTokenAccountSubscriber'; diff --git a/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts b/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts new file mode 100644 index 0000000000..e09668988d --- /dev/null +++ b/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts @@ -0,0 +1,105 @@ +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { CustomizedCadenceBulkAccountLoader } from '../../src/accounts/customizedCadenceBulkAccountLoader'; +import { expect } from 'chai'; + +describe('CustomizedCadenceBulkAccountLoader', () => { + let connection: Connection; + let loader: CustomizedCadenceBulkAccountLoader; + const defaultPollingFrequency = 1000; + + beforeEach(() => { + connection = new Connection('http://localhost:8899', 'processed'); + loader = new CustomizedCadenceBulkAccountLoader( + connection, + 'processed', + defaultPollingFrequency + ); + }); + + afterEach(() => { + loader.stopPolling(); + }); + + it('should add account with custom polling frequency', async () => { + const pubkey = new PublicKey(Keypair.generate().publicKey); + const customFrequency = 500; + const callback = () => {}; // Empty spy function for mocha + + const id = await loader.addAccount(pubkey, callback, customFrequency); + + expect(id).to.exist; + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups.get(customFrequency)?.has(pubkey.toBase58()) + ).to.equal(true); + }); + + it('should remove account and clean up polling', async () => { + const pubkey = new PublicKey(Keypair.generate().publicKey); + const customFrequency = 500; + const callback = () => { + + }; + + await loader.addAccount(pubkey, callback, customFrequency); + loader.removeAccount(pubkey); + + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups.get(customFrequency)?.has(pubkey.toBase58()) + ).to.equal(undefined); + }); + + it('should update custom polling frequency', async () => { + const pubkey = new PublicKey(Keypair.generate().publicKey); + const initialFrequency = 500; + const newFrequency = 200; + const callback = () => { + + }; + + await loader.addAccount(pubkey, callback, initialFrequency); + loader.setCustomPollingFrequency(pubkey, newFrequency); + + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups.get(initialFrequency)?.has(pubkey.toBase58()) + ).to.equal(undefined); + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups.get(newFrequency)?.has(pubkey.toBase58()) + ).to.equal(true); + }); + + it('should use default polling frequency when no custom frequency provided', async () => { + const pubkey = new PublicKey(Keypair.generate().publicKey); + const callback = () => { + + }; + + await loader.addAccount(pubkey, callback); + + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups + .get(defaultPollingFrequency) + ?.has(pubkey.toBase58()) + ).to.equal(true); + }); + + it('should clear all polling on stopPolling', async () => { + const pubkey1 = new PublicKey(Keypair.generate().publicKey); + const pubkey2 = new PublicKey(Keypair.generate().publicKey); + const callback = () => { + + }; + + await loader.addAccount(pubkey1, callback, 500); + await loader.addAccount(pubkey2, callback, 1000); + + loader.stopPolling(); + + // @ts-ignore - accessing private property for testing + expect(loader.customIntervalIds.size).to.equal(0); + }); +}); diff --git a/sdk/tests/ci/verifyConstants.ts b/sdk/tests/ci/verifyConstants.ts index d7e2ee9fca..08f78696ef 100644 --- a/sdk/tests/ci/verifyConstants.ts +++ b/sdk/tests/ci/verifyConstants.ts @@ -81,7 +81,12 @@ describe('Verify Constants', function () { it('has all mainnet markets', async () => { const errors: string[] = []; - const missingLutAddresses: { type: string; marketIndex: number; address: string; description: string }[] = []; + const missingLutAddresses: { + type: string; + marketIndex: number; + address: string; + description: string; + }[] = []; const spotMarkets = mainnetDriftClient.getSpotMarketAccounts(); spotMarkets.sort((a, b) => a.marketIndex - b.marketIndex); @@ -90,22 +95,47 @@ describe('Verify Constants', function () { const correspondingConfigMarket = MainnetSpotMarkets.find( (configMarket) => configMarket.marketIndex === market.marketIndex ); - + if (correspondingConfigMarket === undefined) { - errors.push(`Market ${market.marketIndex} not found in MainnetSpotMarkets. market: ${market.pubkey.toBase58()}`); + errors.push( + `Market ${ + market.marketIndex + } not found in MainnetSpotMarkets. market: ${market.pubkey.toBase58()}` + ); continue; } - if (correspondingConfigMarket.oracle.toBase58() !== market.oracle.toBase58()) { - errors.push(`Oracle mismatch for mainnet spot market ${market.marketIndex}, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.oracle.toBase58()}, chain: ${market.oracle.toBase58()}`); + if ( + correspondingConfigMarket.oracle.toBase58() !== market.oracle.toBase58() + ) { + errors.push( + `Oracle mismatch for mainnet spot market ${ + market.marketIndex + }, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.oracle.toBase58()}, chain: ${market.oracle.toBase58()}` + ); } - if (getVariant(correspondingConfigMarket.oracleSource) !== getVariant(market.oracleSource)) { - errors.push(`Oracle source mismatch for mainnet spot market ${market.marketIndex}, market: ${market.pubkey.toBase58()}, constants: ${getVariant(correspondingConfigMarket.oracleSource)}, chain: ${getVariant(market.oracleSource)}`); + if ( + getVariant(correspondingConfigMarket.oracleSource) !== + getVariant(market.oracleSource) + ) { + errors.push( + `Oracle source mismatch for mainnet spot market ${ + market.marketIndex + }, market: ${market.pubkey.toBase58()}, constants: ${getVariant( + correspondingConfigMarket.oracleSource + )}, chain: ${getVariant(market.oracleSource)}` + ); } - if (correspondingConfigMarket.mint.toBase58() !== market.mint.toBase58()) { - errors.push(`Mint mismatch for mainnet spot market ${market.marketIndex}, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.mint.toBase58()}, chain: ${market.mint.toBase58()}`); + if ( + correspondingConfigMarket.mint.toBase58() !== market.mint.toBase58() + ) { + errors.push( + `Mint mismatch for mainnet spot market ${ + market.marketIndex + }, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.mint.toBase58()}, chain: ${market.mint.toBase58()}` + ); } const lutHasMarket = lutAccounts.includes(market.pubkey.toBase58()); @@ -114,7 +144,7 @@ describe('Verify Constants', function () { type: 'spot', marketIndex: market.marketIndex, address: market.pubkey.toBase58(), - description: 'market' + description: 'market', }); } @@ -124,7 +154,7 @@ describe('Verify Constants', function () { type: 'spot', marketIndex: market.marketIndex, address: market.oracle.toBase58(), - description: 'oracle' + description: 'oracle', }); } @@ -149,18 +179,38 @@ describe('Verify Constants', function () { const correspondingConfigMarket = MainnetPerpMarkets.find( (configMarket) => configMarket.marketIndex === market.marketIndex ); - + if (correspondingConfigMarket === undefined) { - errors.push(`Market ${market.marketIndex} not found in MainnetPerpMarkets, market: ${market.pubkey.toBase58()}`); + errors.push( + `Market ${ + market.marketIndex + } not found in MainnetPerpMarkets, market: ${market.pubkey.toBase58()}` + ); continue; } - if (correspondingConfigMarket.oracle.toBase58() !== market.amm.oracle.toBase58()) { - errors.push(`Oracle mismatch for mainnet perp market ${market.marketIndex}, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.oracle.toBase58()}, chain: ${market.amm.oracle.toBase58()}`); + if ( + correspondingConfigMarket.oracle.toBase58() !== + market.amm.oracle.toBase58() + ) { + errors.push( + `Oracle mismatch for mainnet perp market ${ + market.marketIndex + }, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.oracle.toBase58()}, chain: ${market.amm.oracle.toBase58()}` + ); } - if (getVariant(correspondingConfigMarket.oracleSource) !== getVariant(market.amm.oracleSource)) { - errors.push(`Oracle source mismatch for mainnet perp market ${market.marketIndex}, market: ${market.pubkey.toBase58()}, constants: ${getVariant(correspondingConfigMarket.oracleSource)}, chain: ${getVariant(market.amm.oracleSource)}`); + if ( + getVariant(correspondingConfigMarket.oracleSource) !== + getVariant(market.amm.oracleSource) + ) { + errors.push( + `Oracle source mismatch for mainnet perp market ${ + market.marketIndex + }, market: ${market.pubkey.toBase58()}, constants: ${getVariant( + correspondingConfigMarket.oracleSource + )}, chain: ${getVariant(market.amm.oracleSource)}` + ); } const lutHasMarket = lutAccounts.includes(market.pubkey.toBase58()); @@ -169,17 +219,19 @@ describe('Verify Constants', function () { type: 'perp', marketIndex: market.marketIndex, address: market.pubkey.toBase58(), - description: 'market' + description: 'market', }); } - const lutHasMarketOracle = lutAccounts.includes(market.amm.oracle.toBase58()); + const lutHasMarketOracle = lutAccounts.includes( + market.amm.oracle.toBase58() + ); if (!lutHasMarketOracle) { missingLutAddresses.push({ type: 'perp', marketIndex: market.marketIndex, address: market.amm.oracle.toBase58(), - description: 'oracle' + description: 'oracle', }); } @@ -200,10 +252,16 @@ describe('Verify Constants', function () { // Print all missing LUT addresses if (missingLutAddresses.length > 0) { console.log('\n=== MISSING LUT ADDRESSES ==='); - missingLutAddresses.forEach(({ type, marketIndex, address, description }) => { - console.log(`${type.toUpperCase()} Market ${marketIndex} ${description}: ${address}`); - }); - console.log(`\nTotal missing LUT addresses: ${missingLutAddresses.length}`); + missingLutAddresses.forEach( + ({ type, marketIndex, address, description }) => { + console.log( + `${type.toUpperCase()} Market ${marketIndex} ${description}: ${address}` + ); + } + ); + console.log( + `\nTotal missing LUT addresses: ${missingLutAddresses.length}` + ); } // Print all errors @@ -218,7 +276,10 @@ describe('Verify Constants', function () { // Fail if there are any issues const totalIssues = errors.length + missingLutAddresses.length; if (totalIssues > 0) { - assert(false, `Found ${totalIssues} issues (${errors.length} validation errors, ${missingLutAddresses.length} missing LUT addresses). See details above.`); + assert( + false, + `Found ${totalIssues} issues (${errors.length} validation errors, ${missingLutAddresses.length} missing LUT addresses). See details above.` + ); } }); @@ -232,22 +293,47 @@ describe('Verify Constants', function () { const correspondingConfigMarket = DevnetSpotMarkets.find( (configMarket) => configMarket.marketIndex === market.marketIndex ); - + if (correspondingConfigMarket === undefined) { - errors.push(`Market ${market.marketIndex} not found in DevnetSpotMarkets, market: ${market.pubkey.toBase58()}`); + errors.push( + `Market ${ + market.marketIndex + } not found in DevnetSpotMarkets, market: ${market.pubkey.toBase58()}` + ); continue; } - if (correspondingConfigMarket.oracle.toBase58() !== market.oracle.toBase58()) { - errors.push(`Oracle mismatch for devnet spot market ${market.marketIndex}, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.oracle.toBase58()}, chain: ${market.oracle.toBase58()}`); + if ( + correspondingConfigMarket.oracle.toBase58() !== market.oracle.toBase58() + ) { + errors.push( + `Oracle mismatch for devnet spot market ${ + market.marketIndex + }, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.oracle.toBase58()}, chain: ${market.oracle.toBase58()}` + ); } - if (getVariant(correspondingConfigMarket.oracleSource) !== getVariant(market.oracleSource)) { - errors.push(`Oracle source mismatch for devnet spot market ${market.marketIndex}, market: ${market.pubkey.toBase58()}, constants: ${getVariant(correspondingConfigMarket.oracleSource)}, chain: ${getVariant(market.oracleSource)}`); + if ( + getVariant(correspondingConfigMarket.oracleSource) !== + getVariant(market.oracleSource) + ) { + errors.push( + `Oracle source mismatch for devnet spot market ${ + market.marketIndex + }, market: ${market.pubkey.toBase58()}, constants: ${getVariant( + correspondingConfigMarket.oracleSource + )}, chain: ${getVariant(market.oracleSource)}` + ); } - if (correspondingConfigMarket.mint.toBase58() !== market.mint.toBase58()) { - errors.push(`Mint mismatch for devnet spot market ${market.marketIndex}, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.mint.toBase58()}, chain: ${market.mint.toBase58()}`); + if ( + correspondingConfigMarket.mint.toBase58() !== market.mint.toBase58() + ) { + errors.push( + `Mint mismatch for devnet spot market ${ + market.marketIndex + }, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.mint.toBase58()}, chain: ${market.mint.toBase58()}` + ); } } @@ -258,18 +344,38 @@ describe('Verify Constants', function () { const correspondingConfigMarket = DevnetPerpMarkets.find( (configMarket) => configMarket.marketIndex === market.marketIndex ); - + if (correspondingConfigMarket === undefined) { - errors.push(`Market ${market.marketIndex} not found in DevnetPerpMarkets, market: ${market.pubkey.toBase58()}`); + errors.push( + `Market ${ + market.marketIndex + } not found in DevnetPerpMarkets, market: ${market.pubkey.toBase58()}` + ); continue; } - if (correspondingConfigMarket.oracle.toBase58() !== market.amm.oracle.toBase58()) { - errors.push(`Oracle mismatch for devnet perp market ${market.marketIndex}, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.oracle.toBase58()}, chain: ${market.amm.oracle.toBase58()}`); + if ( + correspondingConfigMarket.oracle.toBase58() !== + market.amm.oracle.toBase58() + ) { + errors.push( + `Oracle mismatch for devnet perp market ${ + market.marketIndex + }, market: ${market.pubkey.toBase58()}, constants: ${correspondingConfigMarket.oracle.toBase58()}, chain: ${market.amm.oracle.toBase58()}` + ); } - if (getVariant(correspondingConfigMarket.oracleSource) !== getVariant(market.amm.oracleSource)) { - errors.push(`Oracle source mismatch for devnet perp market ${market.marketIndex}, market: ${market.pubkey.toBase58()}, constants: ${getVariant(correspondingConfigMarket.oracleSource)}, chain: ${getVariant(market.amm.oracleSource)}`); + if ( + getVariant(correspondingConfigMarket.oracleSource) !== + getVariant(market.amm.oracleSource) + ) { + errors.push( + `Oracle source mismatch for devnet perp market ${ + market.marketIndex + }, market: ${market.pubkey.toBase58()}, constants: ${getVariant( + correspondingConfigMarket.oracleSource + )}, chain: ${getVariant(market.amm.oracleSource)}` + ); } } @@ -284,7 +390,10 @@ describe('Verify Constants', function () { // Fail if there are any issues if (errors.length > 0) { - assert(false, `Found ${errors.length} devnet validation errors. See details above.`); + assert( + false, + `Found ${errors.length} devnet validation errors. See details above.` + ); } }); }); From 01990a3d98e633f7036e3c24e4b4d22d25929e13 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Thu, 12 Jun 2025 14:53:07 -0600 Subject: [PATCH 2/7] feat: method to read account cadence on custom cadence account loader --- .../customizedCadenceBulkAccountLoader.ts | 10 +++ ...customizedCadenceBulkAccountLoader.test.ts | 77 +++++++++++++++---- 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts index 388edf54dd..c483aa99f1 100644 --- a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts +++ b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts @@ -120,6 +120,16 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { } } + public getAccountCadence(publicKey: PublicKey): number | null { + const key = publicKey.toBase58(); + for (const [frequency, group] of this.customPollingGroups.entries()) { + if (group.has(key)) { + return frequency; + } + } + return null; + } + public startPolling(): void { // Don't start the default polling interval // Only start custom polling for accounts that have custom frequencies diff --git a/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts b/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts index e09668988d..07ce18ddb9 100644 --- a/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts +++ b/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts @@ -29,7 +29,7 @@ describe('CustomizedCadenceBulkAccountLoader', () => { expect(id).to.exist; expect( - // @ts-ignore - accessing private property for testing + // @ts-ignore - accessing private property for testing loader.customPollingGroups.get(customFrequency)?.has(pubkey.toBase58()) ).to.equal(true); }); @@ -37,15 +37,13 @@ describe('CustomizedCadenceBulkAccountLoader', () => { it('should remove account and clean up polling', async () => { const pubkey = new PublicKey(Keypair.generate().publicKey); const customFrequency = 500; - const callback = () => { - - }; + const callback = () => {}; await loader.addAccount(pubkey, callback, customFrequency); loader.removeAccount(pubkey); expect( - // @ts-ignore - accessing private property for testing + // @ts-ignore - accessing private property for testing loader.customPollingGroups.get(customFrequency)?.has(pubkey.toBase58()) ).to.equal(undefined); }); @@ -54,33 +52,29 @@ describe('CustomizedCadenceBulkAccountLoader', () => { const pubkey = new PublicKey(Keypair.generate().publicKey); const initialFrequency = 500; const newFrequency = 200; - const callback = () => { - - }; + const callback = () => {}; await loader.addAccount(pubkey, callback, initialFrequency); loader.setCustomPollingFrequency(pubkey, newFrequency); expect( - // @ts-ignore - accessing private property for testing + // @ts-ignore - accessing private property for testing loader.customPollingGroups.get(initialFrequency)?.has(pubkey.toBase58()) ).to.equal(undefined); expect( - // @ts-ignore - accessing private property for testing + // @ts-ignore - accessing private property for testing loader.customPollingGroups.get(newFrequency)?.has(pubkey.toBase58()) ).to.equal(true); }); it('should use default polling frequency when no custom frequency provided', async () => { const pubkey = new PublicKey(Keypair.generate().publicKey); - const callback = () => { - - }; + const callback = () => {}; await loader.addAccount(pubkey, callback); expect( - // @ts-ignore - accessing private property for testing + // @ts-ignore - accessing private property for testing loader.customPollingGroups .get(defaultPollingFrequency) ?.has(pubkey.toBase58()) @@ -90,9 +84,7 @@ describe('CustomizedCadenceBulkAccountLoader', () => { it('should clear all polling on stopPolling', async () => { const pubkey1 = new PublicKey(Keypair.generate().publicKey); const pubkey2 = new PublicKey(Keypair.generate().publicKey); - const callback = () => { - - }; + const callback = () => {}; await loader.addAccount(pubkey1, callback, 500); await loader.addAccount(pubkey2, callback, 1000); @@ -102,4 +94,55 @@ describe('CustomizedCadenceBulkAccountLoader', () => { // @ts-ignore - accessing private property for testing expect(loader.customIntervalIds.size).to.equal(0); }); + + it('should remove key from previous polling group when setting new frequency', async () => { + const pubkey = new PublicKey(Keypair.generate().publicKey); + const pubkey2 = new PublicKey(Keypair.generate().publicKey); + const pubkey3 = new PublicKey(Keypair.generate().publicKey); + const initialFrequency = 500; + const newFrequency = 1000; + const callback = () => {}; + + // Add accounts with initial frequency + await loader.addAccount(pubkey, callback, initialFrequency); + await loader.addAccount(pubkey2, callback, initialFrequency); + await loader.addAccount(pubkey3, callback, initialFrequency); + + // Verify they're all in the initial frequency group + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups.get(initialFrequency)?.has(pubkey.toBase58()) + ).to.equal(true); + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups.get(initialFrequency)?.has(pubkey2.toBase58()) + ).to.equal(true); + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups.get(initialFrequency)?.has(pubkey3.toBase58()) + ).to.equal(true); + + // Change polling frequency for first pubkey only + loader.setCustomPollingFrequency(pubkey, newFrequency); + + // Verify first pubkey is removed from initial group and added to new group + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups.get(initialFrequency)?.has(pubkey.toBase58()) + ).to.equal(false); + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups.get(newFrequency)?.has(pubkey.toBase58()) + ).to.equal(true); + + // Verify other pubkeys remain in initial group + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups.get(initialFrequency)?.has(pubkey2.toBase58()) + ).to.equal(true); + expect( + // @ts-ignore - accessing private property for testing + loader.customPollingGroups.get(initialFrequency)?.has(pubkey3.toBase58()) + ).to.equal(true); + }); }); From 870075a9150fdb862140123ecc0ebe894b2876d8 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Fri, 13 Jun 2025 13:36:48 -0600 Subject: [PATCH 3/7] feat: PR feedback on customized loader cleaup code and better naming --- sdk/src/accounts/bulkAccountLoader.ts | 3 +- .../customizedCadenceBulkAccountLoader.ts | 72 ++++++++++++++----- sdk/src/constants/numericConstants.ts | 2 + 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/sdk/src/accounts/bulkAccountLoader.ts b/sdk/src/accounts/bulkAccountLoader.ts index 084c5b9811..f59d5797f5 100644 --- a/sdk/src/accounts/bulkAccountLoader.ts +++ b/sdk/src/accounts/bulkAccountLoader.ts @@ -3,13 +3,14 @@ import { v4 as uuidv4 } from 'uuid'; import { BufferAndSlot } from './types'; import { promiseTimeout } from '../util/promiseTimeout'; import { Connection } from '../bankrun/bankrunConnection'; +import { GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE } from '../constants/numericConstants'; export type AccountToLoad = { publicKey: PublicKey; callbacks: Map void>; }; -const GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE = 99; + const oneMinute = 60 * 1000; diff --git a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts index c483aa99f1..67f35c4620 100644 --- a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts +++ b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts @@ -1,5 +1,7 @@ +import { GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE } from '../constants/numericConstants'; import { BulkAccountLoader } from './bulkAccountLoader'; import { Commitment, Connection, PublicKey } from '@solana/web3.js'; +import { v4 as uuidv4 } from 'uuid'; export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { private customIntervalIds: Map; @@ -17,7 +19,7 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { this.defaultPollingFrequency = defaultPollingFrequency; } - private updateCustomPolling(frequency: number): void { + private reloadFrequencyGroup(frequency: number): void { const frequencyStr = frequency.toString(); const existingInterval = this.customIntervalIds.get(frequencyStr); if (existingInterval) { @@ -27,15 +29,29 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { const group = this.customPollingGroups.get(frequency); if (group && group.size > 0) { - const intervalId = setInterval(async () => { + + const handleAccountLoading = async () => { const accounts = Array.from(group) .map((key) => this.accountsToLoad.get(key)) .filter((account) => account !== undefined); if (accounts.length > 0) { - await this.loadChunk([accounts]); + const chunks = this.chunks( + this.chunks( + Array.from(accounts), + GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE + ), + 10 + ); + + await Promise.all( + chunks.map((chunk) => { + return this.loadChunk(chunk); + }) + ); } - }, frequency); + } + const intervalId = setInterval(handleAccountLoading, frequency); this.customIntervalIds.set(frequencyStr, intervalId); } } @@ -46,9 +62,14 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { ): void { const key = publicKey.toBase58(); + let removedFromOldGroup = false; // Remove from old frequency group for (const [frequency, group] of this.customPollingGroups.entries()) { if (group.has(key)) { + if(newFrequency === frequency) { + // if frequency is the same, we do nothing + break; + } group.delete(key); if (group.size === 0) { const intervalId = this.customIntervalIds.get(frequency.toString()); @@ -58,20 +79,23 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { } this.customPollingGroups.delete(frequency); } - this.updateCustomPolling(frequency); + removedFromOldGroup = true; break; } } // Add to new frequency group - let group = this.customPollingGroups.get(newFrequency); - if (!group) { - group = new Set(); - this.customPollingGroups.set(newFrequency, group); - } - group.add(key); + if(removedFromOldGroup) { - this.updateCustomPolling(newFrequency); + let group = this.customPollingGroups.get(newFrequency); + if (!group) { + group = new Set(); + this.customPollingGroups.set(newFrequency, group); + } + group.add(key); + + this.reloadFrequencyGroup(newFrequency); + } } public async addAccount( @@ -79,7 +103,17 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { callback: (buffer: Buffer, slot: number) => void, customPollingFrequency?: number ): Promise { - const id = await super.addAccount(publicKey, callback); + const callbackId = uuidv4(); + const callbacks = new Map< + string, + (buffer: Buffer, slot: number) => void + >(); + callbacks.set(callbackId, callback); + const newAccountToLoad = { + publicKey, + callbacks, + }; + this.accountsToLoad.set(publicKey.toString(), newAccountToLoad); const key = publicKey.toBase58(); const frequency = customPollingFrequency || this.defaultPollingFrequency; @@ -92,9 +126,9 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { } group.add(key); - this.updateCustomPolling(frequency); + this.reloadFrequencyGroup(frequency); - return id; + return callbackId; } public removeAccount(publicKey: PublicKey, id?: string): void { @@ -114,7 +148,7 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { } this.customPollingGroups.delete(frequency); } - this.updateCustomPolling(frequency); + this.reloadFrequencyGroup(frequency); break; } } @@ -131,10 +165,10 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { } public startPolling(): void { - // Don't start the default polling interval - // Only start custom polling for accounts that have custom frequencies + // Don't start the polling in the base class + // Only start polling in these custom frequencies for (const frequency of this.customPollingGroups.keys()) { - this.updateCustomPolling(frequency); + this.reloadFrequencyGroup(frequency); } } diff --git a/sdk/src/constants/numericConstants.ts b/sdk/src/constants/numericConstants.ts index 7ac5304949..d9d4a288eb 100644 --- a/sdk/src/constants/numericConstants.ts +++ b/sdk/src/constants/numericConstants.ts @@ -114,3 +114,5 @@ export const FUEL_WINDOW = new BN(60 * 60 * 24 * 28); // 28 days export const FUEL_START_TS = new BN(1723147200); // unix timestamp export const MAX_PREDICTION_PRICE = PRICE_PRECISION; + +export const GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE = 99; \ No newline at end of file From 3e40a4353a9dd058124fc54f570adffe8e642c26 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 16 Jun 2025 09:11:11 -0600 Subject: [PATCH 4/7] fix: lint and prettify --- sdk/src/accounts/bulkAccountLoader.ts | 2 -- .../customizedCadenceBulkAccountLoader.ts | 24 +++++++------------ sdk/src/constants/numericConstants.ts | 2 +- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/sdk/src/accounts/bulkAccountLoader.ts b/sdk/src/accounts/bulkAccountLoader.ts index f59d5797f5..c76d91b1cd 100644 --- a/sdk/src/accounts/bulkAccountLoader.ts +++ b/sdk/src/accounts/bulkAccountLoader.ts @@ -10,8 +10,6 @@ export type AccountToLoad = { callbacks: Map void>; }; - - const oneMinute = 60 * 1000; export class BulkAccountLoader { diff --git a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts index 67f35c4620..e965ca0e95 100644 --- a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts +++ b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts @@ -29,7 +29,6 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { const group = this.customPollingGroups.get(frequency); if (group && group.size > 0) { - const handleAccountLoading = async () => { const accounts = Array.from(group) .map((key) => this.accountsToLoad.get(key)) @@ -37,20 +36,17 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { if (accounts.length > 0) { const chunks = this.chunks( - this.chunks( - Array.from(accounts), - GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE - ), + this.chunks(Array.from(accounts), GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE), 10 ); - + await Promise.all( chunks.map((chunk) => { return this.loadChunk(chunk); }) ); } - } + }; const intervalId = setInterval(handleAccountLoading, frequency); this.customIntervalIds.set(frequencyStr, intervalId); } @@ -66,7 +62,7 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { // Remove from old frequency group for (const [frequency, group] of this.customPollingGroups.entries()) { if (group.has(key)) { - if(newFrequency === frequency) { + if (newFrequency === frequency) { // if frequency is the same, we do nothing break; } @@ -85,15 +81,14 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { } // Add to new frequency group - if(removedFromOldGroup) { - + if (removedFromOldGroup) { let group = this.customPollingGroups.get(newFrequency); if (!group) { group = new Set(); this.customPollingGroups.set(newFrequency, group); } group.add(key); - + this.reloadFrequencyGroup(newFrequency); } } @@ -104,11 +99,8 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { customPollingFrequency?: number ): Promise { const callbackId = uuidv4(); - const callbacks = new Map< - string, - (buffer: Buffer, slot: number) => void - >(); - callbacks.set(callbackId, callback); + const callbacks = new Map void>(); + callbacks.set(callbackId, callback); const newAccountToLoad = { publicKey, callbacks, diff --git a/sdk/src/constants/numericConstants.ts b/sdk/src/constants/numericConstants.ts index d9d4a288eb..1bf447a097 100644 --- a/sdk/src/constants/numericConstants.ts +++ b/sdk/src/constants/numericConstants.ts @@ -115,4 +115,4 @@ export const FUEL_START_TS = new BN(1723147200); // unix timestamp export const MAX_PREDICTION_PRICE = PRICE_PRECISION; -export const GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE = 99; \ No newline at end of file +export const GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE = 99; From b99f563c3882d7cd1347b9105f838b9b5a20439d Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 16 Jun 2025 11:59:39 -0600 Subject: [PATCH 5/7] feat: more efficient rpc polling on custom polling intervals --- .../customizedCadenceBulkAccountLoader.ts | 205 +++++++++--------- sdk/src/constants/numericConstants.ts | 2 +- ...customizedCadenceBulkAccountLoader.test.ts | 162 +++++++++----- 3 files changed, 207 insertions(+), 162 deletions(-) diff --git a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts index e965ca0e95..d9ae64d352 100644 --- a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts +++ b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts @@ -4,8 +4,9 @@ import { Commitment, Connection, PublicKey } from '@solana/web3.js'; import { v4 as uuidv4 } from 'uuid'; export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { - private customIntervalIds: Map; - private customPollingGroups: Map>; + private customIntervalId: NodeJS.Timeout | null; + private accountFrequencies: Map; + private lastPollingTime: Map; private defaultPollingFrequency: number; constructor( @@ -14,41 +15,50 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { defaultPollingFrequency: number ) { super(connection, commitment, defaultPollingFrequency); - this.customIntervalIds = new Map(); - this.customPollingGroups = new Map(); + this.customIntervalId = null; + this.accountFrequencies = new Map(); + this.lastPollingTime = new Map(); this.defaultPollingFrequency = defaultPollingFrequency; } - private reloadFrequencyGroup(frequency: number): void { - const frequencyStr = frequency.toString(); - const existingInterval = this.customIntervalIds.get(frequencyStr); - if (existingInterval) { - clearInterval(existingInterval); - this.customIntervalIds.delete(frequencyStr); + private getAccountsToLoad(): Array<{ + publicKey: PublicKey; + callbacks: Map void>; + }> { + const currentTime = Date.now(); + const accountsToLoad: Array<{ + publicKey: PublicKey; + callbacks: Map void>; + }> = []; + + for (const [key, frequency] of this.accountFrequencies.entries()) { + const lastPollTime = this.lastPollingTime.get(key) || 0; + if (currentTime - lastPollTime >= frequency) { + const account = this.accountsToLoad.get(key); + if (account) { + accountsToLoad.push(account); + this.lastPollingTime.set(key, currentTime); + } + } } - const group = this.customPollingGroups.get(frequency); - if (group && group.size > 0) { - const handleAccountLoading = async () => { - const accounts = Array.from(group) - .map((key) => this.accountsToLoad.get(key)) - .filter((account) => account !== undefined); - - if (accounts.length > 0) { - const chunks = this.chunks( - this.chunks(Array.from(accounts), GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE), - 10 - ); - - await Promise.all( - chunks.map((chunk) => { - return this.loadChunk(chunk); - }) - ); - } - }; - const intervalId = setInterval(handleAccountLoading, frequency); - this.customIntervalIds.set(frequencyStr, intervalId); + return accountsToLoad; + } + + private async handleAccountLoading(): Promise { + const accounts = this.getAccountsToLoad(); + + if (accounts.length > 0) { + const chunks = this.chunks( + this.chunks(accounts, GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE), + 10 + ); + + await Promise.all( + chunks.map((chunk) => { + return this.loadChunk(chunk); + }) + ); } } @@ -57,42 +67,37 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { newFrequency: number ): void { const key = publicKey.toBase58(); + this.accountFrequencies.set(key, newFrequency); + this.lastPollingTime.set(key, 0); // Reset last polling time to ensure immediate load + this.restartPollingIfNeeded(newFrequency); + } - let removedFromOldGroup = false; - // Remove from old frequency group - for (const [frequency, group] of this.customPollingGroups.entries()) { - if (group.has(key)) { - if (newFrequency === frequency) { - // if frequency is the same, we do nothing - break; - } - group.delete(key); - if (group.size === 0) { - const intervalId = this.customIntervalIds.get(frequency.toString()); - if (intervalId) { - clearInterval(intervalId); - this.customIntervalIds.delete(frequency.toString()); - } - this.customPollingGroups.delete(frequency); - } - removedFromOldGroup = true; - break; - } - } - - // Add to new frequency group - if (removedFromOldGroup) { - let group = this.customPollingGroups.get(newFrequency); - if (!group) { - group = new Set(); - this.customPollingGroups.set(newFrequency, group); - } - group.add(key); + private restartPollingIfNeeded(newFrequency: number): void { + const currentMinFrequency = Math.min( + ...Array.from(this.accountFrequencies.values()), + this.defaultPollingFrequency + ); - this.reloadFrequencyGroup(newFrequency); + if (newFrequency < currentMinFrequency || !this.customIntervalId) { + this.stopPolling(); + this.startPolling(); } } + /** + * Adds an account to be monitored by the bulk account loader + * @param publicKey The public key of the account to monitor + * @param callback Function to be called when account data is received + * @param customPollingFrequency Optional custom polling frequency in ms for this specific account. + * If not provided, will use the default polling frequency + * @returns A unique callback ID that can be used to remove this specific callback later + * + * The method will: + * 1. Create a new callback mapping for the account + * 2. Set up polling frequency tracking for the account + * 3. Reset last polling time to 0 to ensure immediate data fetch + * 4. Automatically restart polling if this account needs a faster frequency than existing accounts + */ public async addAccount( publicKey: PublicKey, callback: (buffer: Buffer, slot: number) => void, @@ -109,68 +114,60 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { const key = publicKey.toBase58(); const frequency = customPollingFrequency || this.defaultPollingFrequency; + this.accountFrequencies.set(key, frequency); + this.lastPollingTime.set(key, 0); // Reset last polling time to ensure immediate load - // Add to frequency group - let group = this.customPollingGroups.get(frequency); - if (!group) { - group = new Set(); - this.customPollingGroups.set(frequency, group); - } - group.add(key); - - this.reloadFrequencyGroup(frequency); + this.restartPollingIfNeeded(frequency); return callbackId; } public removeAccount(publicKey: PublicKey, id?: string): void { - super.removeAccount(publicKey, id); - const key = publicKey.toBase58(); - - // Remove from any polling groups - for (const [frequency, group] of this.customPollingGroups.entries()) { - if (group.has(key)) { - group.delete(key); - if (group.size === 0) { - const intervalId = this.customIntervalIds.get(frequency.toString()); - if (intervalId) { - clearInterval(intervalId); - this.customIntervalIds.delete(frequency.toString()); - } - this.customPollingGroups.delete(frequency); - } - this.reloadFrequencyGroup(frequency); - break; - } + this.accountFrequencies.delete(key); + this.lastPollingTime.delete(key); + + if (this.accountsToLoad.size === 0) { + this.stopPolling(); + } else { + // Restart polling in case we removed the account with the smallest frequency + this.restartPollingIfNeeded(this.defaultPollingFrequency); } } public getAccountCadence(publicKey: PublicKey): number | null { const key = publicKey.toBase58(); - for (const [frequency, group] of this.customPollingGroups.entries()) { - if (group.has(key)) { - return frequency; - } - } - return null; + return this.accountFrequencies.get(key) || null; } public startPolling(): void { - // Don't start the polling in the base class - // Only start polling in these custom frequencies - for (const frequency of this.customPollingGroups.keys()) { - this.reloadFrequencyGroup(frequency); + if (this.customIntervalId) { + return; } + + const minFrequency = Math.min( + ...Array.from(this.accountFrequencies.values()), + this.defaultPollingFrequency + ); + + this.customIntervalId = setInterval(() => { + this.handleAccountLoading().catch((error) => { + console.error('Error in account loading:', error); + }); + }, minFrequency); } public stopPolling(): void { super.stopPolling(); - // Clear all custom intervals - for (const intervalId of this.customIntervalIds.values()) { - clearInterval(intervalId); + if (this.customIntervalId) { + clearInterval(this.customIntervalId); + this.customIntervalId = null; } - this.customIntervalIds.clear(); + this.lastPollingTime.clear(); + } + + public clearAccountFrequencies(): void { + this.accountFrequencies.clear(); } } diff --git a/sdk/src/constants/numericConstants.ts b/sdk/src/constants/numericConstants.ts index 1bf447a097..e82de7d81e 100644 --- a/sdk/src/constants/numericConstants.ts +++ b/sdk/src/constants/numericConstants.ts @@ -1,5 +1,5 @@ import { LAMPORTS_PER_SOL } from '@solana/web3.js'; -import { BN } from '../'; +import { BN } from '@coral-xyz/anchor'; export const ZERO = new BN(0); export const ONE = new BN(1); diff --git a/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts b/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts index 07ce18ddb9..708e3f7427 100644 --- a/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts +++ b/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts @@ -8,7 +8,19 @@ describe('CustomizedCadenceBulkAccountLoader', () => { const defaultPollingFrequency = 1000; beforeEach(() => { - connection = new Connection('http://localhost:8899', 'processed'); + connection = { + _rpcBatchRequest: async () => { + return Promise.resolve([{ + result: { + context: { slot: 1 }, + value: Array(10).fill(null).map(() => ({ + data: [Buffer.from(Math.random().toString()).toString('base64'), 'base64'] + })), + }, + }, + ]); + }, + } as unknown as Connection; loader = new CustomizedCadenceBulkAccountLoader( connection, 'processed', @@ -28,10 +40,7 @@ describe('CustomizedCadenceBulkAccountLoader', () => { const id = await loader.addAccount(pubkey, callback, customFrequency); expect(id).to.exist; - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups.get(customFrequency)?.has(pubkey.toBase58()) - ).to.equal(true); + expect(loader.getAccountCadence(pubkey)).to.equal(customFrequency); }); it('should remove account and clean up polling', async () => { @@ -42,10 +51,7 @@ describe('CustomizedCadenceBulkAccountLoader', () => { await loader.addAccount(pubkey, callback, customFrequency); loader.removeAccount(pubkey); - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups.get(customFrequency)?.has(pubkey.toBase58()) - ).to.equal(undefined); + expect(loader.getAccountCadence(pubkey)).to.equal(null); }); it('should update custom polling frequency', async () => { @@ -57,14 +63,7 @@ describe('CustomizedCadenceBulkAccountLoader', () => { await loader.addAccount(pubkey, callback, initialFrequency); loader.setCustomPollingFrequency(pubkey, newFrequency); - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups.get(initialFrequency)?.has(pubkey.toBase58()) - ).to.equal(undefined); - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups.get(newFrequency)?.has(pubkey.toBase58()) - ).to.equal(true); + expect(loader.getAccountCadence(pubkey)).to.equal(newFrequency); }); it('should use default polling frequency when no custom frequency provided', async () => { @@ -73,26 +72,20 @@ describe('CustomizedCadenceBulkAccountLoader', () => { await loader.addAccount(pubkey, callback); - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups - .get(defaultPollingFrequency) - ?.has(pubkey.toBase58()) - ).to.equal(true); + expect(loader.getAccountCadence(pubkey)).to.equal(defaultPollingFrequency); }); - it('should clear all polling on stopPolling', async () => { + it('should clear all polling on clearAccountFrequencies', async () => { const pubkey1 = new PublicKey(Keypair.generate().publicKey); const pubkey2 = new PublicKey(Keypair.generate().publicKey); const callback = () => {}; await loader.addAccount(pubkey1, callback, 500); await loader.addAccount(pubkey2, callback, 1000); + loader.clearAccountFrequencies(); - loader.stopPolling(); - - // @ts-ignore - accessing private property for testing - expect(loader.customIntervalIds.size).to.equal(0); + expect(loader.getAccountCadence(pubkey1)).to.equal(null); + expect(loader.getAccountCadence(pubkey2)).to.equal(null); }); it('should remove key from previous polling group when setting new frequency', async () => { @@ -109,40 +102,95 @@ describe('CustomizedCadenceBulkAccountLoader', () => { await loader.addAccount(pubkey3, callback, initialFrequency); // Verify they're all in the initial frequency group - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups.get(initialFrequency)?.has(pubkey.toBase58()) - ).to.equal(true); - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups.get(initialFrequency)?.has(pubkey2.toBase58()) - ).to.equal(true); - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups.get(initialFrequency)?.has(pubkey3.toBase58()) - ).to.equal(true); + expect(loader.getAccountCadence(pubkey)).to.equal(initialFrequency); + expect(loader.getAccountCadence(pubkey2)).to.equal(initialFrequency); + expect(loader.getAccountCadence(pubkey3)).to.equal(initialFrequency); // Change polling frequency for first pubkey only loader.setCustomPollingFrequency(pubkey, newFrequency); - // Verify first pubkey is removed from initial group and added to new group - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups.get(initialFrequency)?.has(pubkey.toBase58()) - ).to.equal(false); - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups.get(newFrequency)?.has(pubkey.toBase58()) - ).to.equal(true); + // Verify first pubkey is updated to new frequency + expect(loader.getAccountCadence(pubkey)).to.equal(newFrequency); // Verify other pubkeys remain in initial group - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups.get(initialFrequency)?.has(pubkey2.toBase58()) - ).to.equal(true); - expect( - // @ts-ignore - accessing private property for testing - loader.customPollingGroups.get(initialFrequency)?.has(pubkey3.toBase58()) - ).to.equal(true); + expect(loader.getAccountCadence(pubkey2)).to.equal(initialFrequency); + expect(loader.getAccountCadence(pubkey3)).to.equal(initialFrequency); + }); + + it('accounts in different polling groups fire at appropriate intervals', async () => { + const loader = new CustomizedCadenceBulkAccountLoader( + connection, + 'processed', + 1000 + ); + + // Create test accounts and callbacks with counters + const oneSecGroupPubkey = new PublicKey(Keypair.generate().publicKey); + const oneSecGroup = { + pubkey: oneSecGroupPubkey, + callCount: 0, + }; + + const threeSecGroup = Array.from({ length: 5 }, () => ({ + pubkey: new PublicKey(Keypair.generate().publicKey), + callCount: 0, + })); + + const fourSecGroup = Array.from({ length: 10 }, () => ({ + pubkey: new PublicKey(Keypair.generate().publicKey), + callCount: 0, + })); + + // Add accounts with different frequencies + await loader.addAccount( + oneSecGroup.pubkey, + () => { + oneSecGroup.callCount += 1; + }, + 1000 + ); + + for (const account of threeSecGroup) { + await loader.addAccount( + account.pubkey, + () => { + account.callCount++; + }, + 3000 + ); + } + + for (const account of fourSecGroup) { + await loader.addAccount( + account.pubkey, + () => { + account.callCount++; + }, + 4000 + ); + } + + loader.startPolling(); + + // Wait for 6 seconds to allow multiple intervals to fire + await new Promise((resolve) => setTimeout(resolve, 4500)); + + // 1s group should have fired ~4 times + expect(oneSecGroup.callCount).to.be.greaterThanOrEqual(3); + expect(oneSecGroup.callCount).to.be.lessThanOrEqual(5); + + // 3s group should have fired ~2 times + for (const account of threeSecGroup) { + expect(account.callCount).to.be.greaterThanOrEqual(1); + expect(account.callCount).to.be.lessThanOrEqual(3); + } + + // 5s group should have fired ~1 time + for (const account of fourSecGroup) { + expect(account.callCount).to.be.greaterThanOrEqual(1); + expect(account.callCount).to.be.lessThanOrEqual(2); + } + + loader.stopPolling(); }); }); From e74ca7efa162ed2e31921880f9cd3cdb3cb72ebc Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 16 Jun 2025 13:45:19 -0600 Subject: [PATCH 6/7] feat: custom cadence acct loader override load --- sdk/src/accounts/customizedCadenceBulkAccountLoader.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts index d9ae64d352..137b595342 100644 --- a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts +++ b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts @@ -45,6 +45,10 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { return accountsToLoad; } + public async load(): Promise { + return this.handleAccountLoading(); + } + private async handleAccountLoading(): Promise { const accounts = this.getAccountsToLoad(); @@ -122,7 +126,7 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { return callbackId; } - public removeAccount(publicKey: PublicKey, id?: string): void { + public removeAccount(publicKey: PublicKey): void { const key = publicKey.toBase58(); this.accountFrequencies.delete(key); this.lastPollingTime.delete(key); From 549e396c904c3b8bc8a5cc452fa5a55ae25852b6 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 16 Jun 2025 13:46:12 -0600 Subject: [PATCH 7/7] chore: prettify --- .../customizedCadenceBulkAccountLoader.ts | 4 ++-- .../customizedCadenceBulkAccountLoader.test.ts | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts index 137b595342..5efaed7532 100644 --- a/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts +++ b/sdk/src/accounts/customizedCadenceBulkAccountLoader.ts @@ -95,7 +95,7 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { * @param customPollingFrequency Optional custom polling frequency in ms for this specific account. * If not provided, will use the default polling frequency * @returns A unique callback ID that can be used to remove this specific callback later - * + * * The method will: * 1. Create a new callback mapping for the account * 2. Set up polling frequency tracking for the account @@ -130,7 +130,7 @@ export class CustomizedCadenceBulkAccountLoader extends BulkAccountLoader { const key = publicKey.toBase58(); this.accountFrequencies.delete(key); this.lastPollingTime.delete(key); - + if (this.accountsToLoad.size === 0) { this.stopPolling(); } else { diff --git a/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts b/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts index 708e3f7427..0f669e1be2 100644 --- a/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts +++ b/sdk/tests/accounts/customizedCadenceBulkAccountLoader.test.ts @@ -10,12 +10,18 @@ describe('CustomizedCadenceBulkAccountLoader', () => { beforeEach(() => { connection = { _rpcBatchRequest: async () => { - return Promise.resolve([{ - result: { - context: { slot: 1 }, - value: Array(10).fill(null).map(() => ({ - data: [Buffer.from(Math.random().toString()).toString('base64'), 'base64'] - })), + return Promise.resolve([ + { + result: { + context: { slot: 1 }, + value: Array(10) + .fill(null) + .map(() => ({ + data: [ + Buffer.from(Math.random().toString()).toString('base64'), + 'base64', + ], + })), }, }, ]);