From de5dea7eb6275950a7c8917524d62c7ca0d3fe6f Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:45:23 -0700 Subject: [PATCH] fix: stop upcasting transaction `version` to `bigint` Kit's RPC response transformer upcasts every JSON integer to a `bigint` unless the field's keypath appears in a per-method allow-list. `version` was missing from that list on `getTransaction`, `getBlock` transactions, and `getTransactionsForAddress`, so it arrived at runtime as `0n` while still typechecking as `TransactionVersion` (`'legacy' | 0 | 1`). The result is a silent mismatch between the declared type and the runtime value. A check like `if (transaction.version === 0)` compiles cleanly and is always false, because `0n === 0` is false. There is no compiler error and no runtime error; the branch simply never runs. The keypath is now allow-listed on all three methods, so `version` arrives as a number and matches its declared type. --- .changeset/small-birds-cheer.md | 9 +++ .../allowed-numeric-keypaths-test.ts | 65 +++++++++++++++++++ packages/rpc-api/src/index.ts | 3 + 3 files changed, 77 insertions(+) create mode 100644 .changeset/small-birds-cheer.md create mode 100644 packages/rpc-api/src/__tests__/allowed-numeric-keypaths-test.ts diff --git a/.changeset/small-birds-cheer.md b/.changeset/small-birds-cheer.md new file mode 100644 index 000000000..80fa59edc --- /dev/null +++ b/.changeset/small-birds-cheer.md @@ -0,0 +1,9 @@ +--- +'@solana/rpc-api': patch +--- + +Stop upcasting transaction `version` to `bigint` + +The response transformer upcasts every JSON integer to a `bigint` unless its keypath appears in an allow-list. `version` was missing from that allow-list on `getTransaction`, `getBlock` transactions, and `getTransactionsForAddress`, so it arrived at runtime as `0n` while still typechecking as `TransactionVersion` (`'legacy' | 0 | 1`). + +A check like `if (transaction.version === 0)` therefore compiled cleanly and was always false, with no compiler error and no runtime error. The keypath is now allow-listed and `version` arrives as a number, matching its declared type. diff --git a/packages/rpc-api/src/__tests__/allowed-numeric-keypaths-test.ts b/packages/rpc-api/src/__tests__/allowed-numeric-keypaths-test.ts new file mode 100644 index 000000000..09c680f88 --- /dev/null +++ b/packages/rpc-api/src/__tests__/allowed-numeric-keypaths-test.ts @@ -0,0 +1,65 @@ +import type { Address } from '@solana/addresses'; +import type { Signature } from '@solana/keys'; +import { createRpc, type Rpc } from '@solana/rpc-spec'; + +import { createSolanaRpcApi, GetBlockApi, GetTransactionApi, GetTransactionsForAddressApi } from '../index'; + +const MOCK_SIGNATURE = + '4nHvMbxHURt2AXd7yQpKSKM5XCVKQiNbfsFmvPtHNJnJPSJHFT6cGUUNQGYK3wcxDCTvBMTLpQFf6HGqhLTUsxwj' as Signature; + +function createMockRpc(result: unknown): Rpc { + return createRpc({ + api: createSolanaRpcApi(), + transport: jest.fn().mockResolvedValue({ result }), + }); +} + +describe('the default response transformer for the Solana RPC', () => { + describe('getTransaction', () => { + it('leaves `version` as a number', async () => { + expect.assertions(1); + const rpc = createMockRpc({ meta: null, slot: 1, version: 0 }); + const result = await rpc + .getTransaction(MOCK_SIGNATURE, { encoding: 'json', maxSupportedTransactionVersion: 0 }) + .send(); + expect(result?.version).toBe(0); + }); + it('leaves the string `version` of a legacy transaction alone', async () => { + expect.assertions(1); + const rpc = createMockRpc({ meta: null, slot: 1, version: 'legacy' }); + const result = await rpc + .getTransaction(MOCK_SIGNATURE, { encoding: 'json', maxSupportedTransactionVersion: 0 }) + .send(); + expect(result?.version).toBe('legacy'); + }); + }); + + describe('getBlock', () => { + it('leaves the `version` of each transaction as a number', async () => { + expect.assertions(1); + const rpc = createMockRpc({ + blockhash: '4nHvMbxHURt2AXd7yQpKSKM5XCVKQiNbfsFmvPtHNJnJ', + transactions: [{ meta: null, version: 0 }], + }); + const result = await rpc.getBlock(1n, { encoding: 'json', maxSupportedTransactionVersion: 0 }).send(); + expect(result?.transactions[0].version).toBe(0); + }); + }); + + describe('getTransactionsForAddress', () => { + it('leaves the `version` of each transaction as a number', async () => { + expect.assertions(1); + const rpc = createMockRpc({ + data: [{ meta: null, version: 0 }], + }); + const result = await rpc + .getTransactionsForAddress('11111111111111111111111111111111' as Address, { + encoding: 'json', + maxSupportedTransactionVersion: 0, + transactionDetails: 'full', + }) + .send(); + expect(result.data[0].version).toBe(0); + }); + }); +}); diff --git a/packages/rpc-api/src/index.ts b/packages/rpc-api/src/index.ts index 29994fbec..afb6621ec 100644 --- a/packages/rpc-api/src/index.ts +++ b/packages/rpc-api/src/index.ts @@ -300,6 +300,7 @@ function getAllowedNumericKeypaths(): AllowedNumericKeypaths ['transactions', KEYPATH_WILDCARD, 'transaction', 'message', ...c] as const), + ['transactions', KEYPATH_WILDCARD, 'version'], ['rewards', KEYPATH_WILDCARD, 'commission'], ], getClusterNodes: [ @@ -348,6 +349,7 @@ function getAllowedNumericKeypaths(): AllowedNumericKeypaths ['meta', 'innerInstructions', KEYPATH_WILDCARD, ...c]), ...messageConfig.map(c => ['transaction', 'message', ...c] as const), + ['version'], ], getTransactionsForAddress: [ ['data', KEYPATH_WILDCARD, 'transactionIndex'], @@ -365,6 +367,7 @@ function getAllowedNumericKeypaths(): AllowedNumericKeypaths ['data', KEYPATH_WILDCARD, 'transaction', 'message', ...c] as const), + ['data', KEYPATH_WILDCARD, 'version'], ], getVersion: [['feature-set']], getVoteAccounts: [