Transactions Overview

The build → sign → simulate → send → track flow shared by EVM and Solana, driven from an Address with buildTransaction(), signTransaction(), simulateTransaction() and sendTransaction().

Every value transfer and contract call in wative-core moves through one lifecycle: build → sign → simulate → send → track. The same four verbs work on EVM and Solana — you drive them from an Address (or its per-chain wallet.evm / wallet.svm view), which owns the key and the Network. The shapes differ per chain, but the steps and the method names do not.

The common path — one call

For a plain value or token send you do not need the full lifecycle below: address.transfer(req) lowers, builds, signs, and broadcasts in one call and returns a TransactionTracker.

one-call.ts
const receipt = await acc.wallets[0].evm
  .transfer({ to: recipient, amount: 1_000_000_000_000_000n })
  .whenConfirmed();

It composes the steps below — lower → build → sign → send — with chainId / rpcUrl defaulting from the Address's network. Drop to the explicit lifecycle when you need to simulate first, set gas or compute budget, or send a contract call rather than a transfer. See Token Transfers for the asset forms (native, ERC-20, SPL, by symbol).

The lifecycle

Build — address.buildTransaction(params) returns a concrete EvmTransaction or SvmTransaction. This is a structural, offline step: no network call is made.

Sign — address.signTransaction(tx) (or await tx.sign()) produces the signed bytes. Signing is local ECDSA/ed25519; it touches the network only to auto-fill fields you left out (EVM nonce/gas/fees, Solana recentBlockhash).

Simulate (optional) — address.simulateTransaction(tx) pre-flights the transaction (eth_call + gas estimate on EVM, simulateTransaction on Solana) and resolves a SimulationResult. It never broadcasts.

Send — address.sendTransaction(tx, opts?) broadcasts and returns a TransactionTracker synchronously. It signs first if the transaction is not yet signed; opts are per-broadcast send options.

Track — await the tracker's lifecycle promises (whenSubmitted(), whenMined(), whenConfirmed()) or subscribe to its change / confirmed / failed events.

send-flow.ts
import { Workspace } from "wative-core";

const ws = await Workspace.open({ path, password: "wsp-pwd" });
const acc = await ws.accounts.create("Desk", "wsp-pwd", MNEMONIC);
const evm = acc.wallets[0].evm; // an EvmSigner (an Address view)

// 1 — build (offline; nonce/gas fill in at sign time)
const tx = evm.buildTransaction({
  to: "0x1234567890123456789012345678901234567890",
  value: 1_000_000_000_000_000n,
  chainId: 1,
});

// 2 — simulate (optional pre-flight, RPC)
const sim = await evm.simulateTransaction(tx);
if (!sim.success) throw new Error(sim.error);

// 3 — send + track (RPC)
const tracker = evm.sendTransaction(tx);
const hash = await tracker.whenSubmitted();
const receipt = await tracker.whenConfirmed();

Installation, if you have not added the package yet:

pnpm add wative-core
npm install wative-core

Offline vs RPC steps

Building is always offline. Only signing, simulating, sending, and tracking reach the network — and even signing is fully offline when you supply every field yourself.

StepMethodReaches the network?
BuildbuildTransaction()No — structural only
SignsignTransaction() / tx.sign()Only to auto-fill omitted fields (EVM nonce/gas/fees, SVM recentBlockhash); fully offline if you supply them
SimulatesimulateTransaction()Yes
SendsendTransaction()Yes
TrackTransactionTracker promises/eventsYes (polls the node)

The Transaction type

Transaction is the polymorphic interface both chains implement. EvmTransaction and SvmTransaction are the concrete classes — you can think of a built transaction as EvmTransaction | SvmTransaction.

The interface declares only the members that are identical across chains:

MemberTypeNotes
vmVmToken"evm" or "svm" — discriminates the union
statusTransactionStatus"draft" → … → "confirmed" / "failed" (see Tracking & Lifecycle)
sign()Promise<this> | thisIdempotent; caches the signed bytes
send(opts?: SendOptions)TransactionTrackerSigns first if needed; opts are per-broadcast send options
simulate()Promise<SimulationResult>Pre-flight only

Per-chain members such as toRawTx(), hash, and from live on the concrete classes and are intentionally not on the interface — hold the concrete EvmTransaction / SvmTransaction type (which buildTransaction() returns) to reach them. See External Signers & Raw Tx.

Driving it from an Address

The four *Transaction helpers on an Address are thin delegates. The three that take a tx — signTransaction, simulateTransaction, sendTransaction — are ownership-checked: each throws PARAMETER_ERROR if tx.from does not match the address's own public key. buildTransaction(params) takes no tx — it sets from to this address itself — so there is nothing to mismatch.

MethodReturnsDelegates to
buildTransaction(params)EvmTransaction / SvmTransactionthe chain dialect
signTransaction(tx)the same tx (generic)tx.sign()
simulateTransaction(tx)Promise<SimulationResult>tx.simulate()
sendTransaction(tx, opts?)TransactionTrackertx.send(opts)

Send options

send(opts?) — and sendTransaction(tx, opts?) — takes an optional per-broadcast bag, SendOptions. Every field is optional and its absence reproduces the prior behavior exactly, so send() and send({}) are the same send. Build-time concerns (gas, fees, nonce, compute budget, blockhash) belong to buildTransaction(...), never here. A field meant for the other chain is ignored.

FieldChainEffect
skipPreflightSVMSkip the node's preflight simulation (default false). ⚠️ Changes the failure taxonomy: a bad-blockhash / insufficient-funds error a preflight would surface as a clean refusal at broadcast instead appears later, while polling the signature status.
preflightCommitmentSVMCommitment the node uses for the preflight simulation.
maxRetriesSVMMax times the node re-sends the tx to the leader before giving up.
minContextSlotSVMReject the broadcast if the node is behind this slot.
commitmentSVMConfirmation level the poll waits for. Default "confirmed" (resolves at whichever of confirmed / finalized arrives first); "finalized" keeps polling; "processed" clamps to confirmed for the wait.
confirmationsEVMBlock confirmations to wait for before resolving. Default 1 (first inclusion). Depth > 1 is refused on both chains — on EVM it is not tracked, and on SVM you use commitment: "finalized" instead.
timeoutbothMilliseconds to wait for confirmation before ending at "timeout". Default 120_000. A non-finite / non-positive value falls back to the default.
routebothWhich endpoint pool / submission path the broadcast takes — see below.

Send routes

route selects which endpoint the single broadcast targets; it never makes the broadcast retry.

  • "default" (or absent) — the ordinary write pool.
  • "staked" — the network's landing-optimized (still public) staked pool. Requires network.routes.staked.endpoints to be configured, otherwise PARAMETER_ERROR.
  • "jito" (Solana bundle → block engine) / "mev" (EVM private tx → relay) — private-submission protocols. Not implemented yet: passing one is refused with UNSUPPORTED_OP, never silently downgraded to a public broadcast (which would leak a transaction meant to stay private). "jito" is Solana-only and "mev" is EVM-only — the wrong chain is a PARAMETER_ERROR.
  • A one-time build rpcUrl override and a non-default route are contradictory (the URL pins one endpoint; the route selects a pool) → PARAMETER_ERROR. Pass one, not both.
send-with-options.ts
// Wait for finality on Solana, with a 60s budget instead of the 120s default.
const tracker = svm.sendTransaction(tx, { commitment: "finalized", timeout: 60_000 });
await tracker.whenFinalized();

Per-chain and topic pages

Last updated on