External Signers & Raw Tx

toRawTx() to hand a built transaction to an external signer or existing RPC client — a synchronous ethers/viem/web3.js request (EvmRawTx) on EVM, or the underlying @solana/web3.js Transaction on Solana.

toRawTx() is the lowest-level handoff: it hands a transaction you built with wative-core to your own signer or RPC client, so you can slot it into an existing ethers / viem / @solana/web3.js stack. Neither form signs or broadcasts.

EVM — EvmTransaction.toRawTx()

Synchronous. Returns a plain EvmRawTx (the TransactionRequest shape every major EVM client consumes). It returns whatever fields are currently set; fields not yet resolved (nonce, gasLimit, the gas-price family) are simply omitted, leaving your consumer to auto-fill them from its own provider. It does not mutate, sign, or hit the network.

evm-handoff.ts
const evm = acc.wallets[0].evm;
const tx = evm.buildTransaction({
  to: "0x1234567890123456789012345678901234567890",
  value: 1_000_000_000_000_000n,
  chainId: 1,
});

const raw = tx.toRawTx(); // synchronous — EvmRawTx

// Hand it to ethers, viem, web3.js, or raw JSON-RPC:
const response = await ethersWallet.sendTransaction(raw);

EvmRawTx

FieldTypeAlways present?
fromstringyes
tostring | nullyes (null = contract creation)
valuebigintyes
datastringyes
typeEvmTxTypeyes
chainIdChainIdyes
noncenumberonly if set
gasPricebigintonly if set
maxFeePerGasbigintonly if set
maxPriorityFeePerGasbigintonly if set
rpcUrlstringonly if set
gasLimitbigintonly if set
accessListReadonlyArray<{ address: string; storageKeys: ReadonlyArray<string> }>only if set

Solana — SvmTransaction.toRawTx()

Asynchronous. Returns the underlying @solana/web3.js Transaction, fully populated with its instructions, fee payer, and recentBlockhash. You can sign it with your own Keypair, append instructions, or route it through your own RPC. It does not sign or broadcast.

svm-handoff.ts
const svm = acc.wallets[0].svm;
const tx = svm.buildTransaction({ recipient, amount: 5000n });

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

// Sign and send it with your own stack:
built.partialSign(myKeypair);
const sig = await connection.sendRawTransaction(built.serialize());

Because Solana's toRawTx() builds the transaction, calling it also seals the SvmTransaction's fields — they can no longer be reassigned afterwards (build a new transaction to change one). The object it returns is the live built transaction, handed back by reference.

External fee payer

SvmTransaction.sign() can only produce the from account's signature and refuses a distinct feePayer. For a sponsored / relayer flow, take the raw transaction and have the external fee payer sign it there:

external-fee-payer.ts
const tx = new SvmTransaction({
  from: senderPubkey,
  recipient,
  amount: 5000n,
  feePayer: relayerPubkey,   // distinct from `from`
  recentBlockhash,
});

const built = await tx.toRawTx(); // sign() would refuse this feePayer; toRawTx does not
built.partialSign(relayerKeypair); // fee payer signs externally
built.partialSign(senderKeypair);

Offline Solana handoff with recentBlockhash

Solana's toRawTx() needs a recentBlockhash at build time. Supply one in the constructor and it can produce the @solana/web3.js Transaction with no bound address and no RPC call — the whole point of a raw-tx handoff. Without a supplied blockhash, it needs a bound address so it can fetch one over RPC.

offline-handoff.ts
import { SvmTransaction } from "wative-core";

const tx = new SvmTransaction({
  from: "SenderPubkey…",
  recipient: "RecipientPubkey…",
  amount: 5000n,
  recentBlockhash: "9xQe…recentBlockhash", // makes the build fully offline
});

const built = await tx.toRawTx(); // no Address, no network

Signing a foreign transaction

toRawTx() hands your transaction out. The inbound counterpart — putting your signature on a foreign serialized transaction a dApp, Jupiter, or WalletConnect handed you — is address.signRawTransaction(input, opts?), and previewing one without signing is address.estimate(input, opts?). Both accept a RawTx or a serialized string and re-decode it through the exported tx-codec helpers (isRawTx, deserializeTx, normalizeTxInput).

Transaction codec

The functions and types that signRawTransaction / estimate route foreign transactions through are exported for direct use. They are pure — no signing, no network.

tx-codec.ts
// Type guard: is this a RawTx envelope (vs. build params or a serialized string)?
isRawTx(x: unknown): x is RawTx

// Decode a serialized transaction. deserializeTx dispatches on vm; the two
// per-chain functions skip that dispatch. All return the opaque bytes plus a
// best-effort decode of the common fields.
deserializeTx(vm: VmToken, serialized: string): { raw: RawTx; decoded: DecodedTx }
deserializeEvmTx(serialized: string): { raw: RawTx; decoded: DecodedTx }
deserializeSvmTx(serialized: string): { raw: RawTx; decoded: DecodedTx }

// Classify an input as our own build params or a foreign transaction.
normalizeTxInput<P>(vm: VmToken, input: P | RawTx | string): NormalizedTx<P>

RawTx is the opaque, signable envelope — the bytes exactly as supplied, plus how they are encoded:

interface RawTx {
  readonly vm: VmToken;
  readonly serialized: string;          // the bytes, exactly as supplied
  readonly encoding: "hex" | "base64";  // hex on EVM, base64 on SVM
}

DecodedTx is a best-effort decode of a foreign transaction's common fields, for inspect / estimate — every field is optional, and a shape the codec does not fully understand still signs and sends off the opaque RawTx:

interface DecodedTx {
  readonly vm: VmToken;
  // EVM: to, value, data, chainId, evmType (0–4), nonce, gasLimit,
  // gasPrice, maxFeePerGas, maxPriorityFeePerGas, and from (the recovered
  // sender when the tx is signed, else null), …
}

NormalizedTx<P> is the trichotomy result — either our own build params, or a foreign RawTx with its decode:

type NormalizedTx<P> =
  | { kind: "params"; params: P }
  | { kind: "raw"; raw: RawTx; decoded: DecodedTx };

Last updated on