viem-8141
A companion package for viem. Stock viem stays untouched; the frame-transaction surface arrives as a client extension, a chain definition, and a set of pure functions.
@noble/post-quantum is the only runtime dependency. ML-DSA signing is native
JavaScript, byte-identical to the reference implementation, and works in the
browser.
npm install viem# then viem-8141 from sourceChain and client
Section titled “Chain and client”import { createPublicClient, http } from 'viem';import { frameActions, frost } from 'viem-8141';
const client = createPublicClient({ chain: frost, transport: http('https://rpc.frostfi.net'),}).extend(frameActions());frost is a viem chain definition with a frame-aware serializer and
receipt/block formatters, so decoded blocks and receipts carry the extra fields.
frameActions() adds six methods. Every one takes a single options
object — there are no positional forms:
| Method | Purpose |
|---|---|
sendFrameTransaction({ transaction }) |
Submit a type 0x06 transaction. Serialises for you; pass { serializedTransaction } if you already have bytes. |
waitForFrameReceipt({ hash }) |
Poll until the receipt lands. Also takes timeout and pollingInterval. |
getFrameReceipt({ hash }) |
The receipt now, or null. |
validateTransaction({ transaction }) |
The submission-gate verdict, without submitting. |
getSignatureSidecar({ blockNumber }) |
A block’s elided witness bytes, or null once pruned. |
getStrippedTransaction({ hash }) |
The stripped record plus its inclusion proof, verified before it returns. |
Keys and signers
Section titled “Keys and signers”import { createMldsaSigner, mldsaPublicKey } from 'viem-8141';
const signer = createMldsaSigner({ seed: '0x…32 bytes' });
signer.publicKey; // 1,952 bytes (ML-DSA-65)signer.publicKeyKeccak; // 32-byte commitmentawait signer.sign(hash); // FIPS 204 signature over a 32-byte messageThe 32-byte seed is the secret key. mldsaPublicKey derives a public key
from a seed without constructing a signer.
For hardware, implement the WitnessSigner interface, or use
connectRemoteWitnessSigner to talk to an out-of-process signer daemon such as
an Apple Secure Enclave bridge. Anything that can
return a signature over 32 bytes is a valid signer.
Addresses
Section titled “Addresses”import { counterfactualAddress, accountSalt, AccountCodeId, factoryAddress, saltDomain } from 'viem-8141';
const account = counterfactualAddress({ activePublicKey: active.publicKey, backupPublicKeyHash: backup.publicKeyKeccak, index: 0n, // optional, defaults to 0});Pure function, no network access. Also exported: create2Address,
createAddress, rotatableInitcode, rotatableRuntime, and the
mldsaVariants registry mapping parameter sets to their verifier precompiles.
Transaction builders
Section titled “Transaction builders”Three builders cover the single-call account lifecycle:
import { buildFirstSendTransaction, // materialise the account + do the first send buildSpendTransaction, // ordinary spend or contract call buildRecoveryTransaction, // rotate via the backup key} from 'viem-8141';Each returns an unsigned transaction with a correctly sized signed gas_limit
and a zero-filled witness placeholder of the right length — so hashing, signing,
and attaching never changes the hash.
Two more build atomic batches: several SENDER frames under one VERIFY, with
the atomic-batch flag on every SENDER frame but the last, so a revert anywhere
rolls the whole run back and the frames after it are skipped. This is what
EIP-5792 wallet_sendCalls maps to on Frost.
import { buildBatchSpendTransaction, type BatchCall } from 'viem-8141';
const calls: BatchCall[] = [ { to: token, data: approveCalldata }, // gas defaults to 50,000n { to: router, data: swapCalldata, gas: 200_000n }, // value defaults to 0n];
const tx = buildBatchSpendTransaction({ sender: account, nonce, calls });buildBatchSpendTransaction takes up to 63 calls (64 frames minus the VERIFY);
buildBatchFirstSendTransaction({ activePublicKey, backupPublicKeyHash, calls })
prepends the deploy frame and takes up to 62. A one-call batch serialises
identically to the single-call builder.
The package also exports builders for the slim hash-commit v3 account (code ID
0x04) — buildSlimFirstSendTransaction, buildSlimSpendTransaction,
buildPromoteTransaction, and buildSlimRecoveryTransaction — but default
onboarding remains the rotatable v2 account. See
Account contracts.
Sign and submit
Section titled “Sign and submit”- Build with an empty witness.
signFrameTransactionWitness({ transaction, signer })— hashes, signs, attaches, and returns a new transaction.client.sendFrameTransaction({ transaction }).
import { signFrameTransactionWitness } from 'viem-8141';
const signed = await signFrameTransactionWitness({ transaction: tx, signer: active });const hash = await client.sendFrameTransaction({ transaction: signed });const receipt = await client.waitForFrameReceipt({ hash });
receipt.status; // 1 on successreceipt.payer; // the account chargedreceipt.frameReceipts; // per-frame outcomessendFrameTransaction serialises for you, so an explicit
serializeFrameTransaction is only needed when you want the raw bytes
themselves.
A hardware signer usually needs none of this. signFrameTransactionWitness
accepts anything with a sign method, so an enclave bridge goes straight in:
const signed = await signFrameTransactionWitness({ transaction: tx, signer: hardware });If you do need the pieces separately — a custom flow, or a signer you cannot wrap — the codec is exported directly:
import { frameTransactionSigHash, // what you sign frameTransactionHash, // the canonical txid serializeFrameTransaction, parseFrameTransaction, intrinsicGas, calldataGas, signedGasLimit,} from 'viem-8141';
const sigHash = frameTransactionSigHash(tx); // 32 bytesconst signature = await hardware.sign(sigHash);tx.signatures[0].signature = signature;Types and constants
Section titled “Types and constants”Exported for building transactions by hand:
import { FrameMode, // .default | .verify | .sender FrameFlags, // .none | .approvePayment | .approveExecution | .atomicBatch SignatureScheme, // .arbitrary | .secp256k1 | .p256 frameTxType, // 0x06 frameEntryPoint, // 0x…aa maxFrames, // 64 maxSignatures, // 128 permanentSigByteGas, txBaseGas, perFrameGas, type Frame, type FrameTransaction, type ParsedFrameTransaction,} from 'viem-8141';Witness segregation
Section titled “Witness segregation”Client-side support for the chain’s witness segregation extension:
import { calcStrippedTransactionsRoot, strippedTransactionProof, verifyStrippedTransactionProof, extractSidecarEntries,} from 'viem-8141';These let a client fetch a stripped transaction record with a proof and verify
it against the block header’s strippedTransactionsRoot — trustless historical
queries against nodes that have pruned the witness bytes.
Sign in with Frost
Section titled “Sign in with Frost”The package also implements Sign in with Frost — off-chain authentication for ML-DSA accounts. Build and parse the EIP-4361-shaped message, and verify a wallet’s reply:
import { createSiwfMessage, // build the canonical message string parseSiwfMessage, // parse it back signedMessageDigest, // the personal-message digest that gets signed verifySiwfMessage, // full server-side verification} from 'viem-8141';verifySiwfMessage checks the signature under the frost-siwf context and binds
the public key to the account across the whole account
family — counterfactual, fixed-key, and
rotatable — using a viem public client for the chain reads. See
Sign in with Frost for the flow and the security checklist.
EOA transactions
Section titled “EOA transactions”For completeness, frame transactions signed by a classical key:
import { buildEoaFrameTransaction, signEoaFrameTransaction } from 'viem-8141';Useful for testing and for sponsorship flows. Not quantum-safe.
Full example
Section titled “Full example”The package ships examples/pq-account-lifecycle.ts, which runs the entire
lifecycle against a live chain: generate keys, derive the counterfactual
address, fund it, self-deploy on the first send, spend, rotate via the backup
key, and spend again with the new key — all signed natively in JavaScript.
VIEM_8141_RPC_URL=https://rpc.frostfi.net npx tsx examples/pq-account-lifecycle.ts