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.
For a plain SOL or SPL send, the one-call facade svm.transfer({ to, amount, asset? }).whenConfirmed() is the common path (see Token Transfers). This page covers the lower-level buildTransaction params — custom instructions, compute budget, lookup tables, and fully offline builds.
Constructing an SvmTransaction
Both an object form and a positional form (from, recipient, amount, opts?) are supported.
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:
const svm = acc.wallets[0].svm; // an SvmSigner
const tx = svm.buildTransaction({
recipient: "RecipientPubkey…",
amount: 5000n,
});SvmTxBuildParams
| Field | Type | Notes |
|---|---|---|
recipient | string (optional) | base58 destination for the default native / SPL transfer. Required for a transfer build; omit it for an instructions-only build |
amount | bigint (optional) | lamports (raw base units); coerced, minimum 0. Required for a transfer; unused for an instructions-only build |
tokenMint | string | SPL mint address — builds an SPL token transfer (idempotent recipient-ATA create + transfer) from recipient and amount, with no hand-built instructions needed |
tokenProgram | string | with tokenMint, selects the token program — classic SPL (default) or Token-2022 |
computeUnitLimit | number | emits a ComputeBudget setComputeUnitLimit instruction |
computeUnitPrice | bigint | priority fee (micro-lamports); auto-defaulted when omitted and an address is bound |
recentBlockhash | string | supply for a fully offline build (see below) |
feePayer | string | defaults to from; see fee-payer rules |
rpcUrl | string | per-transaction endpoint override; validated |
instructions | ReadonlyArray<unknown> | arbitrary instructions; overrides the default native transfer |
addressLookupTables | ReadonlyArray<SvmAddressLookupTable> | resolved lookup tables to compress accounts against; supplying any makes the built transaction a v0 VersionedTransaction (see below) |
version | "legacy" | 0 | transaction version; default "legacy". 0 forces a v0 VersionedTransaction even with no lookup tables; supplying addressLookupTables implies 0 |
memo | string | appended 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:
- Real
@solana/web3.jsTransactionInstructioninstances (e.g. fromSystemProgram.transfer(...)), passed through unchanged. - The library's plain instruction shape — an object with
programId(base58string),accounts([{ pubkey, isSigner, isWritable }]), anddata(Uint8Array) — which the builder converts to a realTransactionInstruction.
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 neededAny 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.
const tx = new SvmTransaction({
from: "SenderPubkey…",
recipient: "RecipientPubkey…",
amount: 5000n,
recentBlockhash: "9xQe…recentBlockhash",
});
const built = await tx.toRawTx(); // @solana/web3.js Transaction, no RPCWhen the library fetches the blockhash it also records the block height past which it expires, so an expired transaction is reported as dropped promptly instead of waiting out the poll budget. A blockhash you supply carries no such height — its expiry is yours to track.
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-emptymemois 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-emptyaddressLookupTables— orversion: 0— makes the build a v0VersionedTransactioninstead of a legacyTransaction; 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 resolvedSvmAddressLookupTable—{ 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:
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.