10 KiB
title, description
| title | description |
|---|---|
| Common Gotchas | Common type errors and runtime pitfalls with @solana/kit and their fixes, including signer types, lifetime assertions, plugin ordering, and account existence. |
Solana Kit Gotchas
Common type errors and runtime pitfalls with their fixes.
Plugin Client Gotchas
Plugin Ordering — Type Error
Cause: Plugins installed before their dependencies. solanaRpc / solanaLocalRpc / solanaDevnetRpc / litesvm all require a payer to be installed first; low-level rpcTransactionPlanner / rpcTransactionPlanExecutor require rpc and payer.
// ❌ Type error — solanaRpc requires payer
createClient()
.use(solanaRpc({ rpcUrl: url }))
.use(signer(mySigner));
// ✅ Fix: signer first (sets payer + identity), then RPC bundle
createClient()
.use(signer(mySigner))
.use(solanaRpc({ rpcUrl: url }));
Forgetting to await Async Client
Cause: Some plugins (e.g., signerFromFile, generatedSigner, generatedSignerWithSol) are async, and .use() automatically threads the promise through the chain.
// ❌ Runtime error — client is a Promise, not a client
const client = createClient()
.use(signerFromFile('./id.json'))
.use(solanaLocalRpc());
client.sendTransaction([ix]); // TypeError: not a function
// ✅ Fix: await the client
const client = await createClient()
.use(signerFromFile('./id.json'))
.use(solanaLocalRpc());
await client.sendTransaction([ix]);
Type Errors
IInstruction does not exist
Cause: Using old type name from legacy web3.js.
// ❌ Type error
import { IInstruction } from '@solana/kit';
// ✅ Fix: Use Instruction
import type { Instruction } from '@solana/kit';
"Transaction message must be signed"
Cause: Trying to send unsigned message (manual pipeline only).
// ✅ Fix: Assert the signed transaction is fully signed
import { assertIsFullySignedTransaction } from '@solana/transactions';
assertIsFullySignedTransaction(signedTransaction);
"Missing blockhash lifetime"
Cause: Message missing lifetime before signing/sending (manual pipeline only).
// ✅ Fix: Assert lifetime exists
import { assertIsTransactionMessageWithBlockhashLifetime } from '@solana/transaction-messages';
assertIsTransactionMessageWithBlockhashLifetime(message);
signAndSendTransactionMessageWithSigners type error
Cause: Fee payer set as address, not signer.
// ❌ Type error — fee payer is address only
setTransactionMessageFeePayer(address, message);
// ✅ Fix: Use signer version
setTransactionMessageFeePayerSigner(signer, message);
Wrong signer type for wallet
Cause: Using TransactionSigner for wallet that needs to send.
// Wallets that submit transactions need TransactionSendingSigner
type TransactionSendingSigner = {
signAndSendTransactions(txs): Promise<SignatureBytes[]>;
};
Missing Lifetime Type Assertion
Cause: sendAndConfirm requires typed lifetime assertion (manual pipeline only).
// ❌ Type error: Property '"__transactionWithBlockhashLifetime"' is missing
const signed = await signTransactionMessageWithSigners(message);
await sendAndConfirm(signed, { commitment: 'confirmed' });
// ✅ Fix: Assert lifetime + size types
assertIsTransactionWithBlockhashLifetime(signed);
assertIsTransactionWithinSizeLimit(signed);
await sendAndConfirm(signed, { commitment: 'confirmed' });
Missing TransactionWithinSizeLimit
Cause: Recent Kit versions require size assertion for send factories.
// ✅ Fix: Add size assertion
import { assertIsTransactionWithinSizeLimit } from '@solana/kit';
assertIsTransactionWithinSizeLimit(signed);
RPC URL String vs Cluster Wrapper
Cause: Using devnet()/mainnet() wrappers when raw URL string expected.
// ❌ May cause issues
import { devnet } from '@solana/rpc-types';
const rpc = createSolanaRpc(devnet('https://my-custom-endpoint.com'));
// ✅ Simple: use raw URL strings directly
const rpc = createSolanaRpc('https://api.devnet.solana.com');
Runtime Errors
"Account does not exist"
Cause: Decoding account that may not exist.
// ❌ Runtime error if account missing
const account = await fetchEncodedAccount(rpc, address);
const decoded = decodeAccount(account, decoder);
// ✅ Fix: Assert existence first
const account = await fetchEncodedAccount(rpc, address);
assertAccountExists(account);
const decoded = decodeAccount(account, decoder);
Blockhash expired after CU estimation
Cause: Simulation takes time, blockhash ages out. Only applies to manual pipeline — plugin clients handle this automatically.
// ❌ Blockhash may expire
let message = pipe(...blockhash...);
message = await estimateAndUpdateCU(message);
await signAndSendTransactionMessageWithSigners(message);
// ✅ Fix: Refresh blockhash AFTER estimation
let message = pipe(...blockhash...);
message = await estimateAndUpdateCU(message);
const { value: freshBlockhash } = await rpc.getLatestBlockhash().send();
message = setTransactionMessageLifetimeUsingBlockhash(freshBlockhash, message);
await signAndSendTransactionMessageWithSigners(message);
Simulation fails with "account not found"
Cause: Account doesn't exist yet (e.g., PDA not initialized).
const account = await fetchEncodedAccount(rpc, address);
if (!account.exists) {
// Handle missing account — may need to create it first
}
Transaction v1 Gotchas
Full reference: transactions-v1.md.
version: 1 throws on the plugin client
Cause: rpcTransactionPlanner in @solana/kit-plugin-rpc defines the version: 1 config shape for forward compatibility but rejects it at runtime — still true as of 0.18.0.
// ❌ Runtime error: "Version 1 transactions are not yet supported by `rpcTransactionPlanner`."
createClient().use(signer(s)).use(solanaRpc({ rpcUrl, transactionConfig: { version: 1 } }));
// ✅ Fix: build v1 through the manual pipe with @solana/kit 8 directly
const message = pipe(
createTransactionMessage({ version: 1 }),
m => setTransactionMessageFeePayerSigner(payer, m),
m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
m => appendTransactionMessageInstruction(ix, m),
m => setTransactionMessageConfig({ computeUnitLimit: 20_000, loadedAccountsDataSizeLimit: 64 * 1024 }, m),
);
createTransactionMessage({ version: 1 }) is a type error
Cause: @solana/kit 7.x has the v1 codecs and config setters but not the builder types. 8.0.0 is the first release that types it.
# ✅ Fix
pnpm add @solana/kit@^8.0.0
v1 transaction fails with MaxLoadedAccountsDataSizeExceeded
Cause: Unset v1 config fields budget zero, not a default. Only heapSize falls back (32 KiB).
// ❌ Zero CU and zero loaded-accounts bytes — cannot run
createTransactionMessage({ version: 1 });
// ✅ Fix: set both explicitly, or measure them by simulation
const estimateResourceLimits = estimateResourceLimitsFactory({ rpc });
const message = await estimateAndSetResourceLimitsFactory(estimateResourceLimits)(
fillTransactionMessageProvisoryResourceLimits(draft),
);
The estimate has no margin, and an account created between simulation and send is a step change from 0 to 64+ bytes — add headroom, rounding data size up to the next 32 KiB page.
setTransactionMessageComputeUnitPrice type error on a v1 message
Cause: v0 states the priority fee as a price in micro-lamports per CU; v1 states a total in lamports. Kit splits them and enforces the split by type.
// ❌ Type error on a v1 message
setTransactionMessageComputeUnitPrice(250_000n, v1Message);
// ✅ Fix: total lamports, not a per-CU price
setTransactionMessagePriorityFeeLamports(5_000n, v1Message);
setTransactionMessageConfig is the mirror image — v1-only, rejected on legacy/v0.
Priority fee or CU limit reads as 0 for some transactions
Cause: Scanning instructions for the ComputeBudget program. On v1 those values live in message.config, so the scan finds nothing and reports zero without erroring.
// ✅ Fix: version-agnostic readers
getTransactionMessageComputeUnitLimit(message); // any version
getTransactionMessagePriorityFeeLamports(v1Message); // v1 only
Over gRPC, discriminate on Message.config presence — never on the versioned boolean, which is true for both v0 and v1.
Quick Reference
| Gotcha | Fix |
|---|---|
| Plugin ordering type error | Install dependencies before dependents (signer() before solanaRpc/litesvm) |
Forgot to await async client |
const client = await createClient().use(signerFromFile(...)).use(solanaLocalRpc()) |
IInstruction doesn't exist |
Use Instruction from @solana/kit |
| "Transaction message must be signed" | assertIsFullySignedTransaction(signedTx) |
| "Missing blockhash lifetime" | assertIsTransactionMessageWithBlockhashLifetime(msg) |
| Blockhash expired after CU estimation | Refresh blockhash AFTER estimateAndUpdateCU() |
signAndSendTransactionMessageWithSigners type error |
Use setTransactionMessageFeePayerSigner (not address) |
| Account doesn't exist runtime error | assertAccountExists(account) before decode |
| Wrong signer type for wallet | Use TransactionSendingSigner for wallets |
| Missing lifetime type on send | assertIsTransactionWithBlockhashLifetime(signed) |
| Missing size type on send | assertIsTransactionWithinSizeLimit(signed) |
| Durable nonce send type error | assertIsTransactionWithDurableNonceLifetime(signed) |
lifetimeConstraint lost after deserialize |
Re-attach lifetimeConstraint metadata manually |
| RPC URL wrapper issues | Use raw URL strings instead of devnet()/mainnet() |
version: 1 throws on plugin client |
Build v1 with the manual pipe() and @solana/kit 8 |
createTransactionMessage({ version: 1 }) type error |
Upgrade to @solana/kit 8.0.0+ |
v1 MaxLoadedAccountsDataSizeExceeded |
Unset v1 limits are zero — set CU limit and loaded-accounts size explicitly |
setTransactionMessageComputeUnitPrice rejected on v1 |
Use setTransactionMessagePriorityFeeLamports (total lamports, not per-CU) |
| Priority fee / CU limit reads as 0 | v1 keeps them in message.config, not ComputeBudget instructions |