Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Solana development skill covering @solana/kit v5, Anchor programs, LiteSVM testing, and security patterns.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/kit/advanced.md
1---2title: "Advanced: Manual Transactions, Direct RPC & Custom Plugins"3description: Manual transaction building with pipe composition, direct RPC client usage, RPC method reference, building custom plugins, and assembling domain-specific clients.4---56# Advanced: Manual Transactions, Direct RPC & Custom Plugins78This reference covers low-level patterns for when you need full control over the transaction lifecycle, direct RPC access, or want to build custom plugins and domain-specific clients.910For most use cases, prefer the plugin clients in [overview.md](overview.md) and [plugins.md](plugins.md).1112---1314## Manual Transaction Pipeline1516### Transaction Flow17181. Create message → 2. Fee payer → 3. Lifetime → 4. Instructions → 5. Sign → 6. Send1920### Pipe Composition2122```ts23import {24pipe, createTransactionMessage, setTransactionMessageFeePayerSigner,25setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstruction,26prependTransactionMessageInstruction,27} from '@solana/kit';2829const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();3031const message = pipe(32createTransactionMessage({ version: 0 }),33m => setTransactionMessageFeePayerSigner(signer, m),34m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),35m => appendTransactionMessageInstruction(instruction, m),36);37```3839### Fee Payer4041```ts42// With signer (recommended) — enables signTransactionMessageWithSigners()43const msg = setTransactionMessageFeePayerSigner(signer, message);4445// Address only — for multisig or when fee payer is a different party46const msg = setTransactionMessageFeePayer(feePayerAddress, message);47```4849### Lifetime5051```ts52// Blockhash53const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();54const msg = setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, message);5556// Durable nonce — auto-adds AdvanceNonceAccount instruction57const msg = setTransactionMessageLifetimeUsingDurableNonce(nonceInfo, message);58```5960### Instructions6162```ts63// Append64const msg = appendTransactionMessageInstruction(instruction, message);65const msg = appendTransactionMessageInstructions([i1, i2, i3], message);6667// Prepend (for compute budget)68const msg = prependTransactionMessageInstruction(computeBudgetIx, message);69```7071### Creating Raw Instructions7273```ts74import { AccountRole } from '@solana/instructions';7576const instruction: Instruction = {77programAddress: address('Token...'),78accounts: [79{ address: source, role: AccountRole.WRITABLE_SIGNER },80{ address: dest, role: AccountRole.WRITABLE },81{ address: owner, role: AccountRole.READONLY_SIGNER },82],83data: instructionData,84};85```8687---8889## Compute Budget9091Should be used for production transactions.9293### Setup CU Estimator9495```ts96import {97getSetComputeUnitPriceInstruction,98estimateComputeUnitLimitFactory,99estimateAndUpdateProvisoryComputeUnitLimitFactory,100} from '@solana-program/compute-budget';101102const estimateAndUpdateCU = estimateAndUpdateProvisoryComputeUnitLimitFactory(103estimateComputeUnitLimitFactory({ rpc })104);105```106107### Full Pattern: Priority Fee + CU Estimation + Blockhash Refresh108109```ts110// 1. Build message with priority fee111let message = pipe(112createTransactionMessage({ version: 0 }),113m => setTransactionMessageFeePayerSigner(signer, m),114m => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),115m => appendTransactionMessageInstruction(instruction, m),116m => prependTransactionMessageInstruction(117getSetComputeUnitPriceInstruction({ microLamports: 1000n }), m118),119);120121// 2. Estimate CU via simulation122message = await estimateAndUpdateCU(message);123124// 3. REFRESH blockhash (simulation takes time, old one may expire)125const { value: freshBlockhash } = await rpc.getLatestBlockhash().send();126message = setTransactionMessageLifetimeUsingBlockhash(freshBlockhash, message);127128// 4. Sign and send129await signAndSendTransactionMessageWithSigners(message);130```131132### Update Priority Fee Dynamically133134```ts135import { updateOrAppendSetComputeUnitPriceInstruction } from '@solana-program/compute-budget';136137const updated = updateOrAppendSetComputeUnitPriceInstruction(138(current) => current === null ? 1000n : current * 2n,139message140);141```142143See [programs/compute-budget.md](programs/compute-budget.md) for the full CU reference.144145---146147## Signing148149### With Embedded Signers (Recommended)150151```ts152import { signTransactionMessageWithSigners } from '@solana/kit';153154// Auto-discovers signers from fee payer + instruction accounts155const signed = await signTransactionMessageWithSigners(message);156```157158### Sign and Send159160```ts161import { signAndSendTransactionMessageWithSigners } from '@solana/kit';162const signature = await signAndSendTransactionMessageWithSigners(message);163```164165### Manual: Compile + Sign Separately166167```ts168import { compileTransaction, signTransaction, partiallySignTransaction } from '@solana/transactions';169170const compiled = compileTransaction(message);171const signed = await signTransaction([keypair1, keypair2], compiled);172173// Partial signing for multi-party flows174const partial = await partiallySignTransaction([keypair1], compiled);175```176177---178179## Sending180181### Send and Confirm Factory182183```ts184const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });185const signed = await signTransactionMessageWithSigners(message);186187// Required type assertions before sending188assertIsTransactionWithBlockhashLifetime(signed);189assertIsTransactionWithinSizeLimit(signed);190await sendAndConfirm(signed, { commitment: 'confirmed' });191```192193### Durable Nonce194195```ts196const sendNonceTx = sendAndConfirmDurableNonceTransactionFactory({ rpc, rpcSubscriptions });197assertIsFullySignedTransaction(signed);198assertIsTransactionWithDurableNonceLifetime(signed);199assertIsTransactionWithinSizeLimit(signed);200await sendNonceTx(signed, { commitment: 'confirmed' });201```202203### Utilities204205```ts206import { getSignatureFromTransaction, getBase64EncodedWireTransaction } from '@solana/transactions';207208const sig = getSignatureFromTransaction(signedTx);209const base64 = getBase64EncodedWireTransaction(signedTx);210```211212---213214## Complete Manual Example215216```ts217import {218pipe, createTransactionMessage, setTransactionMessageFeePayerSigner,219setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstruction,220prependTransactionMessageInstruction, signTransactionMessageWithSigners,221sendAndConfirmTransactionFactory, assertIsTransactionWithBlockhashLifetime,222assertIsTransactionWithinSizeLimit,223} from '@solana/kit';224import {225getSetComputeUnitPriceInstruction,226estimateComputeUnitLimitFactory,227estimateAndUpdateProvisoryComputeUnitLimitFactory,228} from '@solana-program/compute-budget';229230async function sendTx(rpc, rpcSubscriptions, signer, instruction) {231const estimateAndUpdateCU = estimateAndUpdateProvisoryComputeUnitLimitFactory(232estimateComputeUnitLimitFactory({ rpc })233);234235const { value: simBlockhash } = await rpc.getLatestBlockhash().send();236237let message = pipe(238createTransactionMessage({ version: 0 }),239m => setTransactionMessageFeePayerSigner(signer, m),240m => setTransactionMessageLifetimeUsingBlockhash(simBlockhash, m),241m => appendTransactionMessageInstruction(instruction, m),242m => prependTransactionMessageInstruction(243getSetComputeUnitPriceInstruction({ microLamports: 1000n }), m244),245);246247message = await estimateAndUpdateCU(message);248249// Refresh blockhash after estimation250const { value: freshBlockhash } = await rpc.getLatestBlockhash().send();251message = setTransactionMessageLifetimeUsingBlockhash(freshBlockhash, message);252253const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });254const signed = await signTransactionMessageWithSigners(message);255assertIsTransactionWithBlockhashLifetime(signed);256assertIsTransactionWithinSizeLimit(signed);257await sendAndConfirm(signed, { commitment: 'confirmed' });258}259```260261---262263## Direct RPC Client264265### Creating Clients266267```ts268import { createSolanaRpc, createSolanaRpcSubscriptions } from '@solana/kit';269270const rpc = createSolanaRpc('https://api.devnet.solana.com');271const rpcSubs = createSolanaRpcSubscriptions('wss://api.devnet.solana.com');272```273274### Custom Transport275276```ts277const transport = createDefaultRpcTransport({278url: 'https://my-rpc.example.com',279headers: { 'Authorization': 'Bearer token' },280});281const rpc = createSolanaRpcFromTransport(transport);282```283284### Making Calls285286```ts287// All methods return pending request — call .send()288const { value: balance } = await rpc.getBalance(address).send();289290// With abort291const controller = new AbortController();292await rpc.getBalance(address).send({ abortSignal: controller.signal });293```294295### Return Types296297Most methods return `{ value: T }`:298```ts299const { value: balance } = await rpc.getBalance(address).send();300const { value: blockhash } = await rpc.getLatestBlockhash().send();301```302303Some return `T` directly:304```ts305const rentExempt = await rpc.getMinimumBalanceForRentExemption(80n).send();306const slot = await rpc.getSlot().send();307```308309### Subscriptions310311```ts312const sub = await rpcSubs.accountNotifications(address, {313encoding: 'base64',314commitment: 'confirmed',315}).subscribe();316317for await (const notif of sub) {318console.log('Changed:', notif);319}320```321322### Commitment Levels323324```ts325type Commitment = 'processed' | 'confirmed' | 'finalized';326// processed: seen by node327// confirmed: supermajority confirmed328// finalized: max lockout329```330331### Airdrop (devnet/testnet)332333```ts334import { airdropFactory, lamports } from '@solana/kit';335336const airdrop = airdropFactory({ rpc, rpcSubscriptions });337await airdrop({338recipientAddress: address('...'),339lamports: lamports(1_000_000_000n),340commitment: 'confirmed',341});342```343344### RPC Method Reference345346**Accounts**: `getAccountInfo`, `getMultipleAccounts`, `getBalance`, `getTokenAccountBalance`, `getTokenAccountsByOwner`, `getProgramAccounts`347348**Transactions**: `sendTransaction`, `simulateTransaction`, `getTransaction`, `getSignatureStatuses`, `getSignaturesForAddress`349350**Blocks**: `getBlock`, `getBlockHeight`, `getSlot`, `getLatestBlockhash`, `isBlockhashValid`351352**Cluster**: `getClusterNodes`, `getEpochInfo`, `getHealth`, `getVersion`353354**Misc**: `requestAirdrop`, `getMinimumBalanceForRentExemption`, `getFeeForMessage`355356---357358## Error Handling359360```ts361import {362isSolanaError,363SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED,364SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,365} from '@solana/errors';366367try {368await sendAndConfirm(tx, { commitment: 'confirmed' });369} catch (e) {370if (isSolanaError(e, SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED)) {371console.error('Blockhash expired');372}373if (isSolanaError(e, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE)) {374console.error('Preflight failed:', e.cause);375}376}377```378379---380381## Building Custom Plugins382383A plugin is a function that takes a client object and returns a new one (or a promise):384385```ts386export type ClientPlugin<TInput extends object, TOutput extends Promise<object> | object> =387(input: TInput) => TOutput;388```389390### Basic Plugin391392```ts393import { createClient } from '@solana/kit';394395function apple() {396return <T extends object>(client: T) => ({397...client,398fruit: 'apple' as const,399});400}401402const client = createClient().use(apple());403client.fruit; // 'apple'404```405406### Plugin with Requirements407408Require that other plugins are installed first:409410```ts411function appleTart() {412return <T extends { fruit: 'apple' }>(client: T) => ({413...client,414dessert: 'appleTart' as const,415});416}417418createClient().use(apple()).use(appleTart()); // ✅ Ok419createClient().use(appleTart()); // ❌ TypeScript error420```421422### Async Plugin423424```ts425function magicFruit() {426return async <T extends object>(client: T) => {427const fruit = await fetchSomeMagicFruit();428return { ...client, fruit };429};430}431432// use() handles awaiting automatically433const client = await createClient().use(magicFruit()).use(apple());434```435436---437438## Assembling Domain-Specific Clients439440The plugin system enables building purpose-built clients for specific domains. Here are real-world examples:441442### Example: Kora (Gasless Transactions)443444[Kora](https://github.com/solana-foundation/kora) builds a gasless payment client by composing standard plugins with a custom Kora plugin:445446```ts447import { createClient } from '@solana/kit';448import { planAndSendTransactions, transactionPlanExecutor, transactionPlanner } from '@solana/kit-plugin-instruction-plan';449import { payer } from '@solana/kit-plugin-signer';450import { rpc } from '@solana/kit-plugin-rpc';451452export async function createKitKoraClient(config) {453return createClient()454.use(rpc(config.rpcUrl))455.use(koraPlugin({ apiKey: config.apiKey, endpoint: config.endpoint }))456.use(payer(payerSigner))457.use(transactionPlanner(koraTransactionPlanner)) // Custom planning logic458.use(transactionPlanExecutor(koraTransactionExecutor)) // Custom execution via Kora API459.use(planAndSendTransactions());460}461462// Usage463const client = await createKitKoraClient({ endpoint, rpcUrl, feeToken, feePayerWallet });464await client.sendTransaction([myInstruction]); // Gasless!465```466467Key pattern: Standard plugins (`rpc`, `payer`, `planAndSendTransactions`) combined with custom `transactionPlanner` and `transactionPlanExecutor` that route through Kora's gasless API.468469### Example: Solana Pay470471[Solana Pay](https://github.com/amilz/solana-pay) builds role-specific clients — a read-only merchant client and a full wallet client:472473```ts474import { createClient } from '@solana/kit';475import { planAndSendTransactions } from '@solana/kit-plugin-instruction-plan';476import { payer } from '@solana/kit-plugin-signer';477import { rpc, rpcTransactionPlanExecutor, rpcTransactionPlanner } from '@solana/kit-plugin-rpc';478479// Merchant: read-only, no payer needed480function createMerchantClient(config) {481return createClient()482.use(rpc(config.rpcUrl))483.use(solanaPayMerchant()); // Adds client.pay.encodeURL, findReference, validateTransfer484}485486// Wallet: full tx capabilities487function createWalletClient(config) {488return createClient()489.use(rpc(config.rpcUrl))490.use(payer(config.payer))491.use(rpcTransactionPlanner())492.use(rpcTransactionPlanExecutor())493.use(planAndSendTransactions())494.use(solanaPayWallet()); // Adds client.pay.parseURL, createTransfer495}496497// Usage498const merchant = createMerchantClient({ rpcUrl });499const url = merchant.pay.encodeURL({ recipient, amount: 1.5 });500501const wallet = createWalletClient({ rpcUrl, payer: myWalletSigner });502const instructions = await wallet.pay.createTransfer({ recipient, amount: 1.5 });503await wallet.sendTransaction(instructions);504```505506Key pattern: Same base plugins, different compositions for different roles. Domain logic added as custom plugins (`solanaPayMerchant`, `solanaPayWallet`).507508### Pattern Summary509510When building a domain-specific client:5115121. Start with `createClient()` from `@solana/kit`5132. Add standard plugins for capabilities you need (`rpc`, `payer` from `@solana/kit-plugin-signer`, `planAndSendTransactions`)5143. Swap `transactionPlanner` / `transactionPlanExecutor` if you need custom tx lifecycle (like Kora)5154. Add your domain plugin(s) that extend the client with domain-specific methods5165. Export a factory function (`createMyClient(config)`) for consumers517