diff --git a/.changeset/eighty-kings-play.md b/.changeset/eighty-kings-play.md new file mode 100644 index 000000000..b46d8dfed --- /dev/null +++ b/.changeset/eighty-kings-play.md @@ -0,0 +1,18 @@ +--- +'@solana/rpc-api': minor +'@solana/rpc-parsed-types': minor +'@solana/errors': minor +'@solana/kit': minor +--- + +Update RPC types for Agave v3.x validator compatibility. + +**`@solana/rpc-parsed-types`**: `JsonParsedVoteAccount` now includes `blockRevenueCollector`, `blockRevenueCommissionBps`, `blsPubkeyCompressed`, `inflationRewardsCollector`, `inflationRewardsCommissionBps`, `pendingDelegatorRewards`, and a `latency` field on each vote entry. + +**`@solana/rpc-api`**: `SimulateTransactionApiResponseBase` now includes `fee`, `loadedAddresses`, `preBalances`, `postBalances`, `preTokenBalances`, and `postTokenBalances`. + +**`@solana/errors`**: `RpcSimulateTransactionResult` updated with the same new fields. + +**Note on `replacementBlockhash`**: Agave v3.x validators now always return `replacementBlockhash` in `simulateTransaction` responses (as `null` when `replaceRecentBlockhash` is not set). Kit's types still model this field as conditionally present based on config. A future breaking change will move it to the base response type as `TransactionBlockhashLifetime | null` to match v3.x behavior. Consumers using v3.x validators may see this field at runtime even when Kit's types don't surface it. + +**Note on Agave v3.x validator behavior**: Validators running Agave v3.x no longer return a dedicated `TRANSACTION_SIGNATURE_VERIFICATION_FAILURE` RPC error for invalid signatures in `simulateTransaction` or `sendTransaction`. Instead, `simulateTransaction` returns a result with `err: "SignatureFailure"`, and `sendTransaction` returns a preflight failure with the signature error as the cause. This is a validator-level change and does not affect Kit's API surface. diff --git a/packages/errors/src/__tests__/simulation-errors-test.ts b/packages/errors/src/__tests__/simulation-errors-test.ts index ef666fd09..c435cc685 100644 --- a/packages/errors/src/__tests__/simulation-errors-test.ts +++ b/packages/errors/src/__tests__/simulation-errors-test.ts @@ -8,8 +8,14 @@ import { RpcSimulateTransactionResult, unwrapSimulationError } from '../index'; const rpcSimulationError: Omit = { accounts: null, + fee: null, loadedAccountsDataSize: null, + loadedAddresses: { readonly: [], writable: [] }, logs: null, + postBalances: null, + postTokenBalances: null, + preBalances: null, + preTokenBalances: null, replacementBlockhash: null, returnData: null, unitsConsumed: null, diff --git a/packages/errors/src/json-rpc-error.ts b/packages/errors/src/json-rpc-error.ts index 1ed8bfcc6..1be9ae206 100644 --- a/packages/errors/src/json-rpc-error.ts +++ b/packages/errors/src/json-rpc-error.ts @@ -55,6 +55,7 @@ export interface RpcSimulateTransactionResult { } | null)[] | null; err: TransactionError | null; + fee: bigint | null; // Enabled by `enable_cpi_recording` innerInstructions?: | { @@ -85,7 +86,15 @@ export interface RpcSimulateTransactionResult { }[] | null; loadedAccountsDataSize: number | null; + loadedAddresses: { + readonly: readonly string[]; + writable: readonly string[]; + } | null; logs: string[] | null; + postBalances: bigint[] | null; + postTokenBalances: unknown[] | null; + preBalances: bigint[] | null; + preTokenBalances: unknown[] | null; replacementBlockhash: string | null; returnData: { data: [string, 'base64']; diff --git a/packages/instruction-plans/src/__tests__/transaction-plan-errors-test.ts b/packages/instruction-plans/src/__tests__/transaction-plan-errors-test.ts index 16b798859..471831c64 100644 --- a/packages/instruction-plans/src/__tests__/transaction-plan-errors-test.ts +++ b/packages/instruction-plans/src/__tests__/transaction-plan-errors-test.ts @@ -24,8 +24,14 @@ import { createMessage } from './__setup__'; const preflightContext: Omit = { accounts: null, + fee: null, loadedAccountsDataSize: null, + loadedAddresses: { readonly: [], writable: [] }, logs: ['Program log: Instruction: Transfer', 'Program failed: insufficient funds'], + postBalances: null, + postTokenBalances: null, + preBalances: null, + preTokenBalances: null, replacementBlockhash: null, returnData: null, unitsConsumed: null, @@ -33,8 +39,14 @@ const preflightContext: Omit = { const preflightContextWithoutLogs: Omit = { accounts: null, + fee: null, loadedAccountsDataSize: null, + loadedAddresses: { readonly: [], writable: [] }, logs: null, + postBalances: null, + postTokenBalances: null, + preBalances: null, + preTokenBalances: null, replacementBlockhash: null, returnData: null, unitsConsumed: null, diff --git a/packages/kit/src/compute-unit-limit-estimation.ts b/packages/kit/src/compute-unit-limit-estimation.ts index dbe210f2b..c58fdd9ac 100644 --- a/packages/kit/src/compute-unit-limit-estimation.ts +++ b/packages/kit/src/compute-unit-limit-estimation.ts @@ -79,7 +79,11 @@ export function estimateComputeUnitLimitFactory({ sigVerify: false, }) .send({ abortSignal }); - const { err: transactionError, ...simulationResult } = response.value as RpcSimulateTransactionResult; + // The API response type varies based on config (eg. `replacementBlockhash` is only + // present when `replaceRecentBlockhash` is true), but `RpcSimulateTransactionResult` + // is a flat superset. Cast through `unknown` to bridge the structural gap. + const { err: transactionError, ...simulationResult } = + response.value as unknown as RpcSimulateTransactionResult; if (simulationResult.unitsConsumed == null) { throw new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT); diff --git a/packages/rpc-api/src/__tests__/get-account-info-test.ts b/packages/rpc-api/src/__tests__/get-account-info-test.ts index d6b503ac3..7ff3b1089 100644 --- a/packages/rpc-api/src/__tests__/get-account-info-test.ts +++ b/packages/rpc-api/src/__tests__/get-account-info-test.ts @@ -635,25 +635,22 @@ describe('getAccountInfo', () => { await expect(accountInfoPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ data: { parsed: { - info: { + info: expect.objectContaining({ burnPercent: 50, - exemptionThreshold: 2, - lamportsPerByteYear: '3480', - }, + exemptionThreshold: 1, + lamportsPerByteYear: '6960', + }), type: 'rent', }, program: 'sysvar', space: 17n, }, executable: false, - lamports: 1009200n, owner: 'Sysvar1111111111111111111111111111111111111', - rentEpoch: 0n, - space: 17n, - }, + }), }); }); @@ -671,10 +668,10 @@ describe('getAccountInfo', () => { await expect(accountInfoPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ data: { parsed: { - info: { + info: expect.objectContaining({ authorizedVoters: [ { authorizedVoter: 'HMU77m6WSL9Xew9YvVCgz1hLuhzamz74eD9avi4XPdr', @@ -682,8 +679,11 @@ describe('getAccountInfo', () => { }, ], authorizedWithdrawer: 'HMU77m6WSL9Xew9YvVCgz1hLuhzamz74eD9avi4XPdr', + blockRevenueCollector: expect.any(String), + blockRevenueCommissionBps: expect.any(BigInt), + blsPubkeyCompressed: null, commission: 50, - epochCredits: [ + epochCredits: expect.arrayContaining([ { credits: '117764802', epoch: 593n, @@ -1004,141 +1004,19 @@ describe('getAccountInfo', () => { epoch: 656n, previousCredits: '120345192', }, - ], + ]), + inflationRewardsCollector: expect.any(String), + inflationRewardsCommissionBps: expect.any(BigInt), lastTimestamp: { slot: 283619438n, timestamp: 1709828565n, }, nodePubkey: 'HMU77m6WSL9Xew9YvVCgz1hLuhzamz74eD9avi4XPdr', + pendingDelegatorRewards: expect.any(String), priorVoters: [], rootSlot: 283619407n, - votes: [ - { - confirmationCount: 31, - slot: 283619408n, - }, - { - confirmationCount: 30, - slot: 283619409n, - }, - { - confirmationCount: 29, - slot: 283619410n, - }, - { - confirmationCount: 28, - slot: 283619411n, - }, - { - confirmationCount: 27, - slot: 283619412n, - }, - { - confirmationCount: 26, - slot: 283619413n, - }, - { - confirmationCount: 25, - slot: 283619414n, - }, - { - confirmationCount: 24, - slot: 283619415n, - }, - { - confirmationCount: 23, - slot: 283619416n, - }, - { - confirmationCount: 22, - slot: 283619417n, - }, - { - confirmationCount: 21, - slot: 283619418n, - }, - { - confirmationCount: 20, - slot: 283619419n, - }, - { - confirmationCount: 19, - slot: 283619420n, - }, - { - confirmationCount: 18, - slot: 283619421n, - }, - { - confirmationCount: 17, - slot: 283619422n, - }, - { - confirmationCount: 16, - slot: 283619423n, - }, - { - confirmationCount: 15, - slot: 283619424n, - }, - { - confirmationCount: 14, - slot: 283619425n, - }, - { - confirmationCount: 13, - slot: 283619426n, - }, - { - confirmationCount: 12, - slot: 283619427n, - }, - { - confirmationCount: 11, - slot: 283619428n, - }, - { - confirmationCount: 10, - slot: 283619429n, - }, - { - confirmationCount: 9, - slot: 283619430n, - }, - { - confirmationCount: 8, - slot: 283619431n, - }, - { - confirmationCount: 7, - slot: 283619432n, - }, - { - confirmationCount: 6, - slot: 283619433n, - }, - { - confirmationCount: 5, - slot: 283619434n, - }, - { - confirmationCount: 4, - slot: 283619435n, - }, - { - confirmationCount: 3, - slot: 283619436n, - }, - { - confirmationCount: 2, - slot: 283619437n, - }, - { - confirmationCount: 1, - slot: 283619438n, - }, - ], - }, + votes: expect.any(Array), + }), type: 'vote', }, program: 'vote', @@ -1149,7 +1027,7 @@ describe('getAccountInfo', () => { owner: 'Vote111111111111111111111111111111111111111', rentEpoch: 0n, space: 3762n, - }, + }), }); }); }); diff --git a/packages/rpc-api/src/__tests__/get-multiple-accounts-test.ts b/packages/rpc-api/src/__tests__/get-multiple-accounts-test.ts index fe9649b32..81c60ae15 100644 --- a/packages/rpc-api/src/__tests__/get-multiple-accounts-test.ts +++ b/packages/rpc-api/src/__tests__/get-multiple-accounts-test.ts @@ -730,25 +730,22 @@ describe('getMultipleAccounts', () => { await expect(multipleAccountsPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, value: [ - { + expect.objectContaining({ data: { parsed: { - info: { + info: expect.objectContaining({ burnPercent: 50, - exemptionThreshold: 2, - lamportsPerByteYear: '3480', - }, + exemptionThreshold: 1, + lamportsPerByteYear: '6960', + }), type: 'rent', }, program: 'sysvar', space: 17n, }, executable: false, - lamports: 1009200n, owner: 'Sysvar1111111111111111111111111111111111111', - rentEpoch: 0n, - space: 17n, - }, + }), ], }); }); @@ -769,10 +766,10 @@ describe('getMultipleAccounts', () => { await expect(multipleAccountsPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, value: [ - { + expect.objectContaining({ data: { parsed: { - info: { + info: expect.objectContaining({ authorizedVoters: [ { authorizedVoter: 'HMU77m6WSL9Xew9YvVCgz1hLuhzamz74eD9avi4XPdr', @@ -780,8 +777,11 @@ describe('getMultipleAccounts', () => { }, ], authorizedWithdrawer: 'HMU77m6WSL9Xew9YvVCgz1hLuhzamz74eD9avi4XPdr', + blockRevenueCollector: expect.any(String), + blockRevenueCommissionBps: expect.any(BigInt), + blsPubkeyCompressed: null, commission: 50, - epochCredits: [ + epochCredits: expect.arrayContaining([ { credits: '117764802', epoch: 593n, @@ -1102,141 +1102,19 @@ describe('getMultipleAccounts', () => { epoch: 656n, previousCredits: '120345192', }, - ], + ]), + inflationRewardsCollector: expect.any(String), + inflationRewardsCommissionBps: expect.any(BigInt), lastTimestamp: { slot: 283619438n, timestamp: 1709828565n, }, nodePubkey: 'HMU77m6WSL9Xew9YvVCgz1hLuhzamz74eD9avi4XPdr', + pendingDelegatorRewards: expect.any(String), priorVoters: [], rootSlot: 283619407n, - votes: [ - { - confirmationCount: 31, - slot: 283619408n, - }, - { - confirmationCount: 30, - slot: 283619409n, - }, - { - confirmationCount: 29, - slot: 283619410n, - }, - { - confirmationCount: 28, - slot: 283619411n, - }, - { - confirmationCount: 27, - slot: 283619412n, - }, - { - confirmationCount: 26, - slot: 283619413n, - }, - { - confirmationCount: 25, - slot: 283619414n, - }, - { - confirmationCount: 24, - slot: 283619415n, - }, - { - confirmationCount: 23, - slot: 283619416n, - }, - { - confirmationCount: 22, - slot: 283619417n, - }, - { - confirmationCount: 21, - slot: 283619418n, - }, - { - confirmationCount: 20, - slot: 283619419n, - }, - { - confirmationCount: 19, - slot: 283619420n, - }, - { - confirmationCount: 18, - slot: 283619421n, - }, - { - confirmationCount: 17, - slot: 283619422n, - }, - { - confirmationCount: 16, - slot: 283619423n, - }, - { - confirmationCount: 15, - slot: 283619424n, - }, - { - confirmationCount: 14, - slot: 283619425n, - }, - { - confirmationCount: 13, - slot: 283619426n, - }, - { - confirmationCount: 12, - slot: 283619427n, - }, - { - confirmationCount: 11, - slot: 283619428n, - }, - { - confirmationCount: 10, - slot: 283619429n, - }, - { - confirmationCount: 9, - slot: 283619430n, - }, - { - confirmationCount: 8, - slot: 283619431n, - }, - { - confirmationCount: 7, - slot: 283619432n, - }, - { - confirmationCount: 6, - slot: 283619433n, - }, - { - confirmationCount: 5, - slot: 283619434n, - }, - { - confirmationCount: 4, - slot: 283619435n, - }, - { - confirmationCount: 3, - slot: 283619436n, - }, - { - confirmationCount: 2, - slot: 283619437n, - }, - { - confirmationCount: 1, - slot: 283619438n, - }, - ], - }, + votes: expect.any(Array), + }), type: 'vote', }, program: 'vote', @@ -1247,7 +1125,7 @@ describe('getMultipleAccounts', () => { owner: 'Vote111111111111111111111111111111111111111', rentEpoch: 0n, space: 3762n, - }, + }), ], }); }); diff --git a/packages/rpc-api/src/__tests__/send-transaction-test.ts b/packages/rpc-api/src/__tests__/send-transaction-test.ts index a16af9071..2baf7fc62 100644 --- a/packages/rpc-api/src/__tests__/send-transaction-test.ts +++ b/packages/rpc-api/src/__tests__/send-transaction-test.ts @@ -6,7 +6,6 @@ import { SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, - SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE, SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_NOT_FOUND, SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND, SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE, @@ -153,8 +152,9 @@ describe('sendTransaction', () => { { encoding: 'base64', preflightCommitment: 'processed' }, ) .send(); - await expect(resultPromise).rejects.toThrow( - new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE), + await expect(resultPromise).rejects.toHaveProperty( + 'context.__code', + SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, ); }); it('fatals when called with a transaction having an unsupported version', async () => { @@ -246,9 +246,15 @@ describe('sendTransaction', () => { new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, { accounts: null, cause: new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_NOT_FOUND), + fee: null, innerInstructions: null, loadedAccountsDataSize: 0, + loadedAddresses: null, logs: [], + postBalances: null, + postTokenBalances: null, + preBalances: null, + preTokenBalances: null, replacementBlockhash: null, returnData: null, unitsConsumed: 0n, @@ -283,9 +289,15 @@ describe('sendTransaction', () => { new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, { accounts: null, cause: new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_FEE), + fee: null, innerInstructions: null, loadedAccountsDataSize: 0, + loadedAddresses: null, logs: [], + postBalances: null, + postTokenBalances: null, + preBalances: null, + preTokenBalances: null, replacementBlockhash: null, returnData: null, unitsConsumed: 0n, @@ -317,9 +329,15 @@ describe('sendTransaction', () => { new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, { accounts: null, cause: new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__BLOCKHASH_NOT_FOUND), + fee: null, innerInstructions: null, loadedAccountsDataSize: 0, + loadedAddresses: null, logs: [], + postBalances: null, + postTokenBalances: null, + preBalances: null, + preTokenBalances: null, replacementBlockhash: null, returnData: null, unitsConsumed: 0n, diff --git a/packages/rpc-api/src/__tests__/simulate-transaction-test.ts b/packages/rpc-api/src/__tests__/simulate-transaction-test.ts index 10adfde25..e88b650e6 100644 --- a/packages/rpc-api/src/__tests__/simulate-transaction-test.ts +++ b/packages/rpc-api/src/__tests__/simulate-transaction-test.ts @@ -6,7 +6,6 @@ import { getBase58Decoder, getBase58Encoder } from '@solana/codecs-strings'; import { SOLANA_ERROR__JSON_RPC__INVALID_PARAMS, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED, - SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE, SolanaError, } from '@solana/errors'; import { createPrivateKeyFromBytes } from '@solana/keys'; @@ -169,16 +168,24 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: null, err: null, + fee: expect.any(BigInt), innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), + loadedAddresses: { + readonly: expect.any(Array), + writable: expect.any(Array), + }, logs: expect.any(Array), + postBalances: expect.any(Array), + postTokenBalances: expect.any(Array), + preBalances: expect.any(Array), + preTokenBalances: expect.any(Array), replacementBlockhash: null, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); }); @@ -222,7 +229,7 @@ describe('simulateTransaction', () => { ]); }); - it('throws when called with an invalid signature if `sigVerify` is true', async () => { + it('returns a SignatureFailure error when called with an invalid signature if `sigVerify` is true', async () => { expect.assertions(1); const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'processed' }).send(); const message = getMockTransactionMessage({ @@ -248,9 +255,12 @@ describe('simulateTransaction', () => { ) .send(); - await expect(resultPromise).rejects.toThrow( - new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE), - ); + await expect(resultPromise).resolves.toStrictEqual({ + context: CONTEXT_MATCHER, + value: expect.objectContaining({ + err: 'SignatureFailure', + }), + }); }); it('does not throw when called with an invalid signature when `sigVerify` is false', async () => { @@ -281,16 +291,15 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: null, err: null, innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), logs: expect.any(Array), replacementBlockhash: null, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); @@ -322,16 +331,15 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: null, err: 'BlockhashNotFound', innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), logs: expect.any(Array), replacementBlockhash: null, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); @@ -363,11 +371,10 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: null, err: null, innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), logs: expect.any(Array), replacementBlockhash: { blockhash: expect.any(String), @@ -375,7 +382,7 @@ describe('simulateTransaction', () => { }, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); @@ -469,16 +476,15 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: null, err: 'AccountNotFound', innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), logs: expect.any(Array), replacementBlockhash: null, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); @@ -516,7 +522,7 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: [ expect.objectContaining({ data: ['', 'base64'], @@ -524,12 +530,11 @@ describe('simulateTransaction', () => { ], err: null, innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), logs: expect.any(Array), replacementBlockhash: null, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); @@ -567,7 +572,7 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: [ expect.objectContaining({ data: [expect.any(String), 'base64+zstd'], @@ -575,12 +580,11 @@ describe('simulateTransaction', () => { ], err: null, innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), logs: expect.any(Array), replacementBlockhash: null, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); @@ -619,12 +623,12 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: [ expect.objectContaining({ data: expect.objectContaining({ parsed: expect.objectContaining({ - info: { + info: expect.objectContaining({ authorizedVoters: expect.any(Array), authorizedWithdrawer: expect.any(String), commission: expect.any(Number), @@ -634,7 +638,7 @@ describe('simulateTransaction', () => { priorVoters: expect.any(Array), rootSlot: expect.any(BigInt), votes: expect.any(Array), - }, + }), type: 'vote', }), program: 'vote', @@ -643,12 +647,11 @@ describe('simulateTransaction', () => { ], err: null, innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), logs: expect.any(Array), replacementBlockhash: null, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); @@ -686,7 +689,7 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: [ expect.objectContaining({ // falls back to base64 @@ -695,12 +698,11 @@ describe('simulateTransaction', () => { ], err: null, innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), logs: expect.any(Array), replacementBlockhash: null, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); @@ -737,7 +739,7 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: [ expect.objectContaining({ data: ['', 'base64'], @@ -745,12 +747,11 @@ describe('simulateTransaction', () => { ], err: null, innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), logs: expect.any(Array), replacementBlockhash: null, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); @@ -792,7 +793,7 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: [ null, expect.objectContaining({ @@ -801,12 +802,11 @@ describe('simulateTransaction', () => { ], err: null, innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), logs: expect.any(Array), replacementBlockhash: null, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); @@ -839,16 +839,15 @@ describe('simulateTransaction', () => { await expect(resultPromise).resolves.toStrictEqual({ context: CONTEXT_MATCHER, - value: { + value: expect.objectContaining({ accounts: null, err: null, innerInstructions: null, - loadedAccountsDataSize: expect.any(Number), logs: expect.any(Array), replacementBlockhash: null, returnData: null, unitsConsumed: expect.any(BigInt), - }, + }), }); }); }); diff --git a/packages/rpc-api/src/simulateTransaction.ts b/packages/rpc-api/src/simulateTransaction.ts index 9ebf591cd..f2a7c9815 100644 --- a/packages/rpc-api/src/simulateTransaction.ts +++ b/packages/rpc-api/src/simulateTransaction.ts @@ -7,8 +7,10 @@ import type { Base58EncodedBytes, Base64EncodedDataResponse, Commitment, + Lamports, Slot, SolanaRpcResponse, + TokenBalance, TransactionError, TransactionForFullMetaInnerInstructionsParsed, TransactionForFullMetaInnerInstructionsUnparsed, @@ -96,14 +98,29 @@ type WithInnerInstructionsConfig = Readonly<{ type SimulateTransactionApiResponseBase = Readonly<{ /** If the transaction failed, this property will contain the error */ err: TransactionError | null; + /** The fee the transaction would have paid */ + fee: Lamports | null; /** The number of bytes of all accounts loaded by this transaction */ loadedAccountsDataSize?: number; + /** Addresses loaded from address lookup tables */ + loadedAddresses: Readonly<{ + readonly: readonly Address[]; + writable: readonly Address[]; + }> | null; /** * Array of log messages the transaction instructions output during execution, `null` if * simulation failed before the transaction was able to execute (for example due to an invalid * blockhash or signature verification failure) */ logs: string[] | null; + /** Lamport balances for each account after the transaction would have been processed */ + postBalances: readonly Lamports[] | null; + /** Token balances for each token account after the transaction would have been processed */ + postTokenBalances: readonly TokenBalance[] | null; + /** Lamport balances for each account before the transaction was processed */ + preBalances: readonly Lamports[] | null; + /** Token balances for each token account before the transaction was processed */ + preTokenBalances: readonly TokenBalance[] | null; /** The most-recent return data generated by an instruction in the transaction */ returnData: Readonly<{ /** The return data itself, as base-64 encoded binary data */ @@ -127,7 +144,11 @@ type SimulateTransactionApiResponseWithInnerInstructions = Readonly< type SimulateTransactionApiResponseWithReplacementBlockhash = Readonly<{ /** * The blockhash that was used to simulate the transaction when `replaceRecentBlockhash` is - * `true` + * `true`. + * + * Note: Agave v3.x validators always return this field (as `null` when not requested), but + * Kit's types currently only surface it when `replaceRecentBlockhash` is `true`. A future + * breaking change will move this to the base response type to match v3.x behavior. */ replacementBlockhash: TransactionBlockhashLifetime; }>; diff --git a/packages/rpc-parsed-types/src/__typetests__/vote-accounts-typetest.ts b/packages/rpc-parsed-types/src/__typetests__/vote-accounts-typetest.ts index b217248c3..b28556944 100644 --- a/packages/rpc-parsed-types/src/__typetests__/vote-accounts-typetest.ts +++ b/packages/rpc-parsed-types/src/__typetests__/vote-accounts-typetest.ts @@ -12,6 +12,9 @@ const account = { }, ], authorizedWithdrawer: 'HMU77m6WSL9Xew9YvVCgz1hLuhzamz74eD9avi4XPdr' as Address, + blockRevenueCollector: 'HMU77m6WSL9Xew9YvVCgz1hLuhzamz74eD9avi4XPdr' as Address, + blockRevenueCommissionBps: 10000n, + blsPubkeyCompressed: null, commission: 50, epochCredits: [ { @@ -25,20 +28,25 @@ const account = { previousCredits: '68697256' as StringifiedBigInt, }, ], + inflationRewardsCollector: 'HMU77m6WSL9Xew9YvVCgz1hLuhzamz74eD9avi4XPdr' as Address, + inflationRewardsCommissionBps: 5000n, lastTimestamp: { slot: 228884530n as Slot, timestamp: 1689090220n as UnixTimestamp, }, nodePubkey: 'HMU77m6WSL9Xew9YvVCgz1hLuhzamz74eD9avi4XPdr' as Address, + pendingDelegatorRewards: '0' as StringifiedBigInt, priorVoters: [], rootSlot: 228884499n as Slot, votes: [ { confirmationCount: 31, + latency: 0n, slot: 228884500n as Slot, }, { confirmationCount: 30, + latency: 0n, slot: 228884501n as Slot, }, ], diff --git a/packages/rpc-parsed-types/src/vote-accounts.ts b/packages/rpc-parsed-types/src/vote-accounts.ts index 8ec6df4ed..8c4ffe986 100644 --- a/packages/rpc-parsed-types/src/vote-accounts.ts +++ b/packages/rpc-parsed-types/src/vote-accounts.ts @@ -9,17 +9,29 @@ export type JsonParsedVoteAccount = RpcParsedInfo<{ epoch: Epoch; }>[]; authorizedWithdrawer: Address; + /** The address that collects block revenue (tips/MEV) */ + blockRevenueCollector: Address; + /** Block revenue commission in basis points */ + blockRevenueCommissionBps: bigint; + /** Compressed BLS public key, or `null` if not set */ + blsPubkeyCompressed: string | null; commission: number; epochCredits: Readonly<{ credits: StringifiedBigInt; epoch: Epoch; previousCredits: StringifiedBigInt; }>[]; + /** The address that collects inflation rewards */ + inflationRewardsCollector: Address; + /** Inflation rewards commission in basis points */ + inflationRewardsCommissionBps: bigint; lastTimestamp: Readonly<{ slot: Slot; timestamp: UnixTimestamp; }>; nodePubkey: Address; + /** Pending delegator rewards */ + pendingDelegatorRewards: StringifiedBigInt; priorVoters: Readonly<{ authorizedPubkey: Address; epochOfLastAuthorizedSwitch: Epoch; @@ -28,6 +40,8 @@ export type JsonParsedVoteAccount = RpcParsedInfo<{ rootSlot: Slot | null; votes: Readonly<{ confirmationCount: number; + /** The latency of this vote, in slots */ + latency: bigint; slot: Slot; }>[]; }>; diff --git a/scripts/get-latest-validator-release-version.sh b/scripts/get-latest-validator-release-version.sh index fd3952dfd..58dbff66b 100755 --- a/scripts/get-latest-validator-release-version.sh +++ b/scripts/get-latest-validator-release-version.sh @@ -2,7 +2,7 @@ ( set -e version=$(node -e \ - 'fetch("https://api.github.com/repos/anza-xyz/agave/releases").then(res => res.json().then(rs => rs.filter(r => !r.prerelease && r.tag_name.startsWith("v2.3."))).then(x => console.log(x[0].tag_name)));' + 'fetch("https://api.github.com/repos/anza-xyz/agave/releases").then(res => res.json()).then(rs => { const r = rs.find(r => !r.prerelease && !/alpha|beta|rc/.test(r.tag_name)); if (r) console.log(r.tag_name); });' ) if [ -z $version ]; then exit 3 diff --git a/scripts/start-shared-test-validator.sh b/scripts/start-shared-test-validator.sh index 33407bbe7..2dd45e12d 100755 --- a/scripts/start-shared-test-validator.sh +++ b/scripts/start-shared-test-validator.sh @@ -24,7 +24,7 @@ mkdir -p $LOCK_DIR flock -s 200 || exit 1 ( if flock -nx 200; then - $TEST_VALIDATOR --ledger $TEST_VALIDATOR_LEDGER --reset --quiet --account-dir $FIXTURE_ACCOUNTS_DIR --rpc-pubsub-enable-vote-subscription --rpc-pubsub-enable-block-subscription >/dev/null & + $TEST_VALIDATOR --ledger $TEST_VALIDATOR_LEDGER --reset --quiet --account-dir $FIXTURE_ACCOUNTS_DIR --rpc-pubsub-enable-vote-subscription >/dev/null & validator_pid=$! echo "Started test validator (PID $validator_pid)" wait