Solana Transactions

SvmTransaction and SvmTxBuildParams — building from recipient/amount/instructions, supplying recentBlockhash for a fully offline build, and passing @solana/web3.js instructions.

SvmTransaction is the concrete Solana transaction. address.buildTransaction(params) binds the address so the transaction can sign, send, and simulate. The underlying @solana/web3.js Transaction is built lazily — on the first sign(), simulate(), or toRawTx() — so constructing one is always offline.

Constructing an SvmTransaction

Both an object form and a positional form (from, recipient, amount, opts?) are supported.

construct-svm.ts
import { SvmTransaction } from "wative-core";

const tx = new SvmTransaction({
  from: "SenderPubkey…",
  recipient: "RecipientPubkey…",
  amount: 5000n, // lamports (raw base units)
});

Built from an address, a native SOL transfer is just recipient + amount:

build-from-address.ts
const svm = acc.wallets[0].svm; // an SvmSigner
const tx = svm.buildTransaction({
  recipient: "RecipientPubkey…",
  amount: 5000n,
});

SvmTxBuildParams

FieldTypeNotes
recipientstring (optional)base58 destination for the default native / SPL transfer. Required for a transfer build; omit it for an instructions-only build
amountbigint (optional)lamports (raw base units); coerced, minimum 0. Required for a transfer; unused for an instructions-only build
tokenMintstringSPL mint address — builds an SPL token transfer (idempotent recipient-ATA create + transfer) from recipient and amount, with no hand-built instructions needed
tokenProgramstringwith tokenMint, selects the token program — classic SPL (default) or Token-2022
computeUnitLimitnumberemits a ComputeBudget setComputeUnitLimit instruction
computeUnitPricebigintpriority fee (micro-lamports); auto-defaulted when omitted and an address is bound
recentBlockhashstringsupply for a fully offline build (see below)
feePayerstringdefaults to from; see fee-payer rules
rpcUrlstringper-transaction endpoint override; validated
instructionsReadonlyArray<unknown>arbitrary instructions; overrides the default native transfer
addressLookupTablesReadonlyArray<SvmAddressLookupTable>resolved lookup tables to compress accounts against; supplying any makes the built transaction a v0 VersionedTransaction (see below)
version"legacy" | 0transaction version; default "legacy". 0 forces a v0 VersionedTransaction even with no lookup tables; supplying addressLookupTables implies 0
memostringappended as an SPL Memo instruction

instructions

When you pass a non-empty instructions array, it replaces the default native SOL transfer — the builder adds your instructions instead, and recipient / amount become optional (an instructions-only build carries the move in the instructions themselves). A build with neither a transfer (recipient / amount) nor instructions is a PARAMETER_ERROR. Two shapes are accepted:

  1. Real @solana/web3.js TransactionInstruction instances (e.g. from SystemProgram.transfer(...)), passed through unchanged.
  2. The library's plain instruction shape — an object with programId (base58 string), accounts ([{ pubkey, isSigner, isWritable }]), and data (Uint8Array) — which the builder converts to a real TransactionInstruction.
custom-instructions.ts
import { SystemProgram, PublicKey } from "@solana/web3.js";

const ix = SystemProgram.transfer({
  fromPubkey: new PublicKey(sender),
  toPubkey: new PublicKey(recipient),
  lamports: 5000n,
});

const tx = svm.buildTransaction({ instructions: [ix] }); // instructions-only — no recipient/amount needed

Any computeUnitLimit / computeUnitPrice ComputeBudget instructions are prepended, and a memo, if set, is appended — around whatever instructions you supply.

Fully offline build with recentBlockhash

Without a recentBlockhash, the transaction fetches one over RPC through its bound address the first time it is built (at sign / simulate / toRawTx). Supply one yourself and the build is purely structural — no address and no network call are needed, which is exactly what makes an offline toRawTx() handoff possible.

offline-svm.ts
const tx = new SvmTransaction({
  from: "SenderPubkey…",
  recipient: "RecipientPubkey…",
  amount: 5000n,
  recentBlockhash: "9xQe…recentBlockhash",
});

const built = await tx.toRawTx(); // @solana/web3.js Transaction, no RPC

Fee-payer rules

feePayer defaults to from. It accepts a base58 string, a PublicKey, or raw key bytes, and is resolved to canonical base58 at construction; null is treated the same as omitting it (use from).

sign() can only produce the from account's signature. A feePayer distinct from from is therefore refused as a precondition, before an unsignable transaction is built:

// feePayer !== from
await tx.sign();
// PARAMETER_ERROR: SvmTransaction.sign cannot sign a distinct feePayer; it must
// equal from or be undefined. For an external fee payer, use toRawTx() and sign
// it yourself.

For a sponsored / relayer flow where someone else pays the fee, use toRawTx() to get the underlying @solana/web3.js Transaction and have the external fee payer sign it.

Memo and lookup tables

  • memo — a non-empty memo is appended as an SPL Memo-program instruction, so the note actually lands on-chain rather than being dropped.
  • addressLookupTables — Address Lookup Tables (ALTs) are supported. Supplying a non-empty addressLookupTables — or version: 0 — makes the build a v0 VersionedTransaction instead of a legacy Transaction; the message compiler replaces each account with its slot index in a resolved table, so a transaction referencing many accounts fits under the size limit. Each entry is a resolved SvmAddressLookupTable — { address, addresses }, the table's own account address plus its ordered list of stored addresses — because the compiler substitutes an account by its position in that list. Passing the resolved tables (not just their addresses) keeps the build offline / air-gapped-safe: no untrusted RPC account set enters the signing path implicitly.

Fetch resolved tables with svmResolveAddressLookupTables(network, addresses) — an online convenience that reads each table via getAddressLookupTable (a missing table throws RECORD_NOT_FOUND, a transport fault RPC_UNREACHABLE) — or supply your own for a fully offline build:

v0-with-alt.ts
import { svmResolveAddressLookupTables } from "wative-core";

// Online: resolve the tables once…
const tables = await svmResolveAddressLookupTables(network, [
  "AddressLookupTablePubkey…",
]);

// …then the build compresses accounts against them and emits a v0 transaction.
const tx = svm.buildTransaction({
  recipient: "RecipientPubkey…",
  amount: 5000n,
  addressLookupTables: tables, // ⇒ version 0 (VersionedTransaction)
});

A v0 build also changes what toRawTx() hands back — a @solana/web3.js VersionedTransaction, not a legacy Transaction, and its signing API differs (.sign([keypair]) rather than .partialSign(...)). See External Signers & Raw Tx.

Last updated on