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 three methods to the client:
| Method | Purpose |
|---|---|
sendFrameTransaction(raw) |
Submit a serialised type 0x06 transaction. |
waitForFrameReceipt(hash) |
Wait for a receipt, with typed payer and per-frame receipts. |
validateTransaction(raw) |
The submission-gate verdict, without submitting. |
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 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.
Sign and submit
Section titled “Sign and submit”- Build with an empty witness.
signFrameTransactionWitness(tx, signer)— hashes, signs, attaches.serializeFrameTransaction(signed)→0x06-prefixed bytes.client.sendFrameTransaction(raw).
import { serializeFrameTransaction, signFrameTransactionWitness } from 'viem-8141';
const signed = await signFrameTransactionWitness(tx, active);const hash = await client.sendFrameTransaction(serializeFrameTransaction(signed));const receipt = await client.waitForFrameReceipt(hash);If you need the pieces separately — for a hardware signer, or a custom flow — 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, // approve payment / execution / atomic batch 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.
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