diff --git a/sdk/scripts/client-test.ts b/sdk/scripts/client-test.ts index 2579ab97e..c2d7c7947 100644 --- a/sdk/scripts/client-test.ts +++ b/sdk/scripts/client-test.ts @@ -19,12 +19,14 @@ import { ProgramAccount, } from '@coral-xyz/anchor'; import driftIDL from '../src/idl/drift.json'; +import assert from 'assert'; const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; const TOKEN = process.env.TOKEN; +const RPC_ENDPOINT = process.env.RPC_ENDPOINT; async function initializeGrpcDriftClientV2VersusV1() { - const connection = new Connection('https://api.mainnet-beta.solana.com'); + const connection = new Connection(RPC_ENDPOINT); const wallet = new Wallet(new Keypair()); dotenv.config({ path: '../' }); @@ -34,49 +36,38 @@ async function initializeGrpcDriftClientV2VersusV1() { // @ts-ignore wallet, { - commitment: 'confirmed', + commitment: 'processed', } ); const program = new Program(driftIDL as Idl, programId, provider); - const perpMarketIndexes = [4]; - const spotMarketIndexes = [32]; - - const perpMarketProgramAccounts = ( - await program.account.perpMarket.all() - ).filter((a) => - perpMarketIndexes.includes(a.account.marketIndex as number) - ) as ProgramAccount[]; - const spotMarketProgramAccounts = ( - await program.account.spotMarket.all() - ).filter((a) => - spotMarketIndexes.includes(a.account.marketIndex as number) - ) as ProgramAccount[]; - - // const perpMarketIndexes = perpMarketProgramAccounts.map( - // (a) => a.account.marketIndex - // ); - // const spotMarketIndexes = spotMarketProgramAccounts.map( - // (a) => a.account.marketIndex - // ); - // const oracleInfos = [ - // { - // publicKey: new PublicKey('BERaNi6cpEresbq6HC1EQGaB1H1UjvEo4NGnmYSSJof4'), - // source: OracleSource.PYTH_LAZER, - // }, - // { - // publicKey: new PublicKey('BERaNi6cpEresbq6HC1EQGaB1H1UjvEo4NGnmYSSJof4'), - // source: OracleSource.PYTH_LAZER_1M, - // }, - // ]; + const allPerpMarketProgramAccounts = + (await program.account.perpMarket.all()) as ProgramAccount[]; + const perpMarketProgramAccounts = allPerpMarketProgramAccounts.filter((val) => + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].includes( + val.account.marketIndex + ) + ); + const perpMarketIndexes = perpMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + const allSpotMarketProgramAccounts = + (await program.account.spotMarket.all()) as ProgramAccount[]; + const spotMarketProgramAccounts = allSpotMarketProgramAccounts.filter((val) => + [0, 1, 2, 3, 4, 5].includes(val.account.marketIndex) + ); + const spotMarketIndexes = spotMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); const seen = new Set(); const oracleInfos: OracleInfo[] = []; for (const acct of perpMarketProgramAccounts) { - const key = `${acct.account.amm.oracle.toBase58()}-${Object.keys( - acct.account.amm.oracleSource ?? {} - )?.[0]}`; + const key = `${acct.account.amm.oracle.toBase58()}-${ + Object.keys(acct.account.amm.oracleSource)[0] + }`; if (!seen.has(key)) { seen.add(key); oracleInfos.push({ @@ -86,9 +77,9 @@ async function initializeGrpcDriftClientV2VersusV1() { } } for (const acct of spotMarketProgramAccounts) { - const key = `${acct.account.oracle.toBase58()}-${Object.keys( - acct.account.oracleSource ?? {} - )?.[0]}`; + const key = `${acct.account.oracle.toBase58()}-${ + Object.keys(acct.account.oracleSource)[0] + }`; if (!seen.has(key)) { seen.add(key); oracleInfos.push({ @@ -103,7 +94,7 @@ async function initializeGrpcDriftClientV2VersusV1() { grpcConfigs: { endpoint: GRPC_ENDPOINT, token: TOKEN, - commitmentLevel: 'confirmed' as unknown as CommitmentLevel, + commitmentLevel: CommitmentLevel.PROCESSED, channelOptions: { 'grpc.keepalive_time_ms': 10_000, 'grpc.keepalive_timeout_ms': 1_000, @@ -119,7 +110,6 @@ async function initializeGrpcDriftClientV2VersusV1() { accountSubscription: { ...baseAccountSubscription, driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, - logResubMessages: true, }, perpMarketIndexes, spotMarketIndexes, @@ -133,7 +123,6 @@ async function initializeGrpcDriftClientV2VersusV1() { accountSubscription: { ...baseAccountSubscription, driftClientAccountSubscriber: grpcDriftClientAccountSubscriber, - // logResubMessages: true, }, perpMarketIndexes, spotMarketIndexes, @@ -144,28 +133,227 @@ async function initializeGrpcDriftClientV2VersusV1() { const clientV1 = new DriftClient(configV1); await Promise.all([clientV1.subscribe(), clientV2.subscribe()]); - - clientV2.eventEmitter.on('oraclePriceUpdate', (pubkey, source, data) => { - const key = pubkey.toBase58(); - const src = Object.keys(source ?? {})[0]; - console.log( - `v2 oracle update ${key} (${src}) price ${data.price.toString()}` - ); - }); const compare = () => { - for (const idx of perpMarketIndexes) { - const p1 = clientV1.getOracleDataForPerpMarket(idx).price; - const p2 = clientV2.getOracleDataForPerpMarket(idx).price; - console.log( - `perp mkt ${idx} | v1 ${p1.toString()} | v2 ${p2.toString()}` + try { + // 1. Test getStateAccountAndSlot + const state1 = clientV1.accountSubscriber.getStateAccountAndSlot(); + const state2 = clientV2.accountSubscriber.getStateAccountAndSlot(); + assert.deepStrictEqual( + state1.data, + state2.data, + 'State accounts should match' ); - } - for (const idx of spotMarketIndexes) { - const s1 = clientV1.getOracleDataForSpotMarket(idx).price; - const s2 = clientV2.getOracleDataForSpotMarket(idx).price; - console.log( - `spot mkt ${idx} | v1 ${s1.toString()} | v2 ${s2.toString()}` + if ( + state1.slot !== undefined && + state2.slot !== undefined && + state2.slot < state1.slot + ) { + console.error( + `State account slot regression: v2 slot ${state2.slot} < v1 slot ${state1.slot}` + ); + } + + // 2. Test getMarketAccountsAndSlots (all perp markets) - sorted comparison + const allPerpMarkets1 = clientV1.accountSubscriber + .getMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + const allPerpMarkets2 = clientV2.accountSubscriber + .getMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + assert.strictEqual( + allPerpMarkets1.length, + allPerpMarkets2.length, + 'Number of perp markets should match' ); + + // Compare each perp market in the sorted arrays + for (let i = 0; i < allPerpMarkets1.length; i++) { + const market1 = allPerpMarkets1[i]; + const market2 = allPerpMarkets2[i]; + assert.strictEqual( + market1.data.marketIndex, + market2.data.marketIndex, + `Perp market at position ${i} should have same marketIndex` + ); + // assert.deepStrictEqual( + // market1.data, + // market2.data, + // `Perp market ${market1.data.marketIndex} (from getMarketAccountsAndSlots) should match` + // ); + } + + // 3. Test getMarketAccountAndSlot for each perp market + for (const idx of perpMarketIndexes) { + const market1 = clientV1.accountSubscriber.getMarketAccountAndSlot(idx); + const market2 = clientV2.accountSubscriber.getMarketAccountAndSlot(idx); + // assert.deepStrictEqual( + // market1?.data, + // market2?.data, + // `Perp market ${idx} data should match` + // ); + // assert.strictEqual( + // market1?.slot, + // market2?.slot, + // `Perp market ${idx} slot should match` + // ); + if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot < market1.slot + ) { + console.error( + `Perp market ${idx} slot regression: v2 slot ${market2.slot} < v1 slot ${market1.slot}` + ); + } else if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot > market1.slot + ) { + console.info( + `Perp market ${idx} slot is FASTER! v2: ${market2.slot}, v1: ${market1.slot}` + ); + } + } + + // 4. Test getSpotMarketAccountsAndSlots (all spot markets) - sorted comparison + const allSpotMarkets1 = clientV1.accountSubscriber + .getSpotMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + const allSpotMarkets2 = clientV2.accountSubscriber + .getSpotMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + assert.strictEqual( + allSpotMarkets1.length, + allSpotMarkets2.length, + 'Number of spot markets should match' + ); + + // Compare each spot market in the sorted arrays + for (let i = 0; i < allSpotMarkets1.length; i++) { + const market1 = allSpotMarkets1[i]; + const market2 = allSpotMarkets2[i]; + assert.strictEqual( + market1.data.marketIndex, + market2.data.marketIndex, + `Spot market at position ${i} should have same marketIndex` + ); + // assert.deepStrictEqual( + // market1.data, + // market2.data, + // `Spot market ${market1.data.marketIndex} (from getSpotMarketAccountsAndSlots) should match` + // ); + } + + // 5. Test getSpotMarketAccountAndSlot for each spot market + for (const idx of spotMarketIndexes) { + const market1 = + clientV1.accountSubscriber.getSpotMarketAccountAndSlot(idx); + const market2 = + clientV2.accountSubscriber.getSpotMarketAccountAndSlot(idx); + // assert.deepStrictEqual( + // market1?.data, + // market2?.data, + // `Spot market ${idx} data should match` + // ); + // assert.strictEqual( + // market1?.slot, + // market2?.slot, + // `Spot market ${idx} slot should match` + // ); + if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot < market1.slot + ) { + console.error( + `Spot market ${idx} slot regression: v2 slot ${market2.slot} < v1 slot ${market1.slot}` + ); + } else if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot > market1.slot + ) { + console.info( + `Spot market ${idx} slot is FASTER! v2: ${market2.slot}, v1: ${market1.slot}` + ); + } + } + + // 6. Test getOraclePriceDataAndSlotForPerpMarket + for (const idx of perpMarketIndexes) { + const oracle1 = + clientV1.accountSubscriber.getOraclePriceDataAndSlotForPerpMarket( + idx + ); + const oracle2 = + clientV2.accountSubscriber.getOraclePriceDataAndSlotForPerpMarket( + idx + ); + // assert.deepStrictEqual( + // oracle1?.data, + // oracle2?.data, + // `Perp market ${idx} oracle data should match` + // ); + // Note: slots might differ slightly due to timing, so we can optionally skip this check or be lenient + // assert.strictEqual(oracle1?.slot, oracle2?.slot, `Perp market ${idx} oracle slot should match`); + if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot < oracle1.slot + ) { + console.error( + `Perp market ${idx} oracle slot regression: v2 slot ${oracle2.slot} < v1 slot ${oracle1.slot}` + ); + } else if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot > oracle1.slot + ) { + console.info( + `Perp market ${idx} oracle slot is FASTER! v2: ${oracle2.slot}, v1: ${oracle1.slot}` + ); + } + } + + // 7. Test getOraclePriceDataAndSlotForSpotMarket + for (const idx of spotMarketIndexes) { + const oracle1 = + clientV1.accountSubscriber.getOraclePriceDataAndSlotForSpotMarket( + idx + ); + const oracle2 = + clientV2.accountSubscriber.getOraclePriceDataAndSlotForSpotMarket( + idx + ); + // assert.deepStrictEqual( + // oracle1?.data, + // oracle2?.data, + // `Spot market ${idx} oracle data should match` + // ); + // Note: slots might differ slightly due to timing + // assert.strictEqual(oracle1?.slot, oracle2?.slot, `Spot market ${idx} oracle slot should match`); + if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot < oracle1.slot + ) { + console.error( + `Spot market ${idx} oracle slot regression: v2 slot ${oracle2.slot} < v1 slot ${oracle1.slot}` + ); + } else if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot > oracle1.slot + ) { + console.info( + `Spot market ${idx} oracle slot is FASTER! v2: ${oracle2.slot}, v1: ${oracle1.slot}` + ); + } + } + + console.log('✓ All comparisons passed'); + } catch (error) { + console.error('✗ Comparison failed:', error); } }; diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts index 7e47b71fa..fc79e06c6 100644 --- a/sdk/src/accounts/grpcMultiAccountSubscriber.ts +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -160,11 +160,14 @@ export class grpcMultiAccountSubscriber { const accountId = chunk[i]; const accountInfo = rpcResponse[i]; if (accountInfo) { - const perpMarket = this.program.coder.accounts.decode( - 'PerpMarket', - accountInfo.data - ); - this.setAccountData(accountId, perpMarket, currentSlot); + const existingData = this.getAccountData(accountId); + if (!existingData || currentSlot > existingData.slot) { + const accountDecoded = this.program.coder.accounts.decode( + this.capitalize(this.accountName), + accountInfo.data + ); + this.setAccountData(accountId, accountDecoded, currentSlot); + } } } }) @@ -238,6 +241,12 @@ export class grpcMultiAccountSubscriber { if (!accountPubkey || !this.subscribedAccounts.has(accountPubkey)) { return; } + + // Skip processing if we already have data for this account at an equal or newer slot + const existing = this.dataMap.get(accountPubkey); + if (existing?.slot !== undefined && existing.slot >= slot) { + return; + } const accountInfo: AccountInfoLike = { owner: new PublicKey(chunk.account.account.owner), lamports: Number(chunk.account.account.lamports),