Skip to content

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.

Terminal window
npm install viem
# then viem-8141 from source
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.
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 commitment
await signer.sign(hash); // FIPS 204 signature over a 32-byte message

The 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.

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.

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.

  1. Build with an empty witness.
  2. signFrameTransactionWitness(tx, signer) — hashes, signs, attaches.
  3. serializeFrameTransaction(signed)0x06-prefixed bytes.
  4. 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 bytes
const signature = await hardware.sign(sigHash);
tx.signatures[0].signature = signature;

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';

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.

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.

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.

Terminal window
VIEM_8141_RPC_URL=https://rpc.frostfi.net npx tsx examples/pq-account-lifecycle.ts