SDK
Generate keys, sign operations and submit them, in TypeScript.
@quantum-shield/sdk is the TypeScript side of the protocol: the same signature scheme as the contract, the leaf
bookkeeping a wallet needs, and the ERC-4337 plumbing. It is built on viem and @noble/hashes, and lives in
packages/sdk.
| Module | Contains |
|---|---|
xmss | XmssKey (generate, sign), recover, signedDigits |
wallet | LeafSigner, LeafStore, MemoryLeafStore, KeyExhausted |
op | Op, emptyOp, opDigest, opId, vaultAbi, routerAbi |
userop | buildUserOp, placeholderSignature, bundlerSubmitter, directSubmitter, saleExit |
Make a key
import { XmssKey, LeafSigner, MemoryLeafStore } from "@quantum-shield/sdk";
import { bytesToHex } from "viem";
const master = crypto.getRandomValues(new Uint8Array(32)); // the account's secret
const key = XmssKey.generate(master, 0, 8); // tree 0, height 8: 256 signatures
const account = bytesToHex(key.key); // the account's name
const signer = new LeafSigner(key, new MemoryLeafStore());XmssKey.generate(master, treeIndex, height, onLeaf?) derives every leaf from the master secret and the tree's
number. Tree 1 is the account's next key, tree 2 the one after. onLeaf reports progress.
In a real wallet, implement LeafStore over durable storage: save must complete before a signature is used.
Buy into the account
import { routerAbi } from "@quantum-shield/sdk";
await wallet.writeContract({
address: router,
abi: routerAbi,
functionName: "buy",
args: [account, minTokensOut],
value: parseEther("0.01"),
});Sign an operation
import { emptyOp, opDigest, vaultAbi, type Op } from "@quantum-shield/sdk";
const [, nextLeaf, epoch] = await client.readContract({ address: vault, abi: vaultAbi, functionName: "accounts", args: [account] });
const op: Op = { ...emptyOp(account), to: otherAccount, transferShares: 1_000n };
const digest = opDigest(op, { chainId: 4663, vault, epoch });
const { leaf, signature } = await signer.sign(digest, Number(nextLeaf));signer.sign picks the leaf, records it, and returns the signature. Asking again for the same digest returns the
same leaf.
Submit with a wallet
await wallet.writeContract({ address: vault, abi: vaultAbi, functionName: "transact", args: [op, signature] });Submit with no wallet
A sale of the whole balance through a bundler, paying the network fee from the account:
import {
DEFAULT_VERIFICATION_GAS, SALE_CALL_GAS, buildUserOp, bundlerSubmitter,
networkFeeError, placeholderSignature, saleExit,
} from "@quantum-shield/sdk";
const shares = await client.readContract({ address: vault, abi: vaultAbi, functionName: "sharesOf", args: [account] });
const fee = await client.readContract({ address: vault, abi: vaultAbi, functionName: "networkFee" });
const gasFee = (await client.readContract({ address: vault, abi: vaultAbi, functionName: "convertToShares", args: [fee] })) + 1n;
const op: Op = {
...emptyOp(account),
exitShares: shares - gasFee,
gasFee,
recipient, // receives the ETH
...saleExit(router, minEthOut),
caller: vault, // ERC-4337 operations name the vault
minCallGasLimit: SALE_CALL_GAS,
};
const submitter = bundlerSubmitter(bundlerUrl, { entryPoint });
let gas = { verificationGasLimit: DEFAULT_VERIFICATION_GAS, callGasLimit: SALE_CALL_GAS, preVerificationGas: 60_000n, ...(await submitter.gasPrice()) };
// Estimate with a placeholder: no leaf is spent until the price is known.
gas = await submitter.estimate(buildUserOp(vault, op, epoch, 0, placeholderSignature(key.height, 0), gas), gas);
const limit = await client.readContract({ address: vault, abi: vaultAbi, functionName: "maxOpCostWei" });
const refused = networkFeeError(gas, limit);
if (refused) throw new Error(refused);
const digest = opDigest(op, { chainId: 4663, vault, epoch });
const { leaf, signature } = await signer.sign(digest, Number(nextLeaf));
const { transactionHash, success } = await submitter.send(buildUserOp(vault, op, epoch, leaf, signature, gas), gas);bundlerSubmitter works with any ERC-4337 bundler that serves EntryPoint v0.8 on the chain. directSubmitter calls
EntryPoint.handleOps from a wallet instead, with no external service.
Change the key
const next = XmssKey.generate(master, 1, 8);
const op: Op = { ...emptyOp(account), newKey: bytesToHex(next.key) };
// sign with the current key, submit either way; then sign with `next`, starting from leaf 0Verify off-chain
import { recover } from "@quantum-shield/sdk";
const result = recover(digestBytes, signatureBytes); // { key, leaf } or nullrecover returns exactly what Xmss.recover returns on-chain.
Gas constants
| Constant | Value | For |
|---|---|---|
DEFAULT_VERIFICATION_GAS | 400,000 | Signature check and applying the operation |
SALE_CALL_GAS | 650,000 | Execution of a sale |
PLAIN_CALL_GAS | 120,000 | Execution of an exit paid out as tokens |