SDK. Issue receipts, publish mandates, verify both.
Install
viem is a peer dependency. The package is published from CI with provenance, so the registry holds a signed statement binding the tarball to the commit and the workflow that built it — which you can check rather than take on trust.
pnpm add @arcveildev/sdk viemnpm audit signatures# @arcveildev/sdk ... verified attestations
Verify
Parse first — anything arriving as text is untrusted — then run the checks against a chain reader. Arc allows cross-origin requests, so the same code runs in a browser with no backend in the path.
import { arc, ARC_REGISTRIES, createRpcChainReader, parseReceiptInput, verifyReceipts } from "@arcveildev/sdk"; // Anything arriving as text is untrusted until the schema says otherwise.const parsed = parseReceiptInput(json);if (!parsed.ok) return { error: parsed.errors }; const chain = createRpcChainReader({ endpoint: arc.rpcUrls.default.http[0], chainId: arc.id, ...ARC_REGISTRIES[arc.id],}); const report = await verifyReceipts(parsed.receipts, { chain });
Publish a mandate
Only the commitment reaches the chain. Keep the terms you hashed: without them you can prove a mandate was live, but never again show what it said. An epoch can be revoked, never overwritten — otherwise terms could change after receipts had been issued against them.
import { mandateCommitment, registerMandate, revokeMandate, ARC_REGISTRIES, arc } from "@arcveildev/sdk"; const terms = [ "assets: USDC only", "per action: 250 USDC", "active hours: 02:00-06:00 UTC",].join("\n"); const commitment = mandateCommitment(terms); // keccak256 of the termsconst writer = { client: walletClient, registry: ARC_REGISTRIES[arc.id].mandateRegistry }; await registerMandate(writer, 1, commitment); // epoch 1await revokeMandate(writer, 1); // stops it; the epoch can never be reused
Issue receipts
An issuer holds the head of the budget chain and the mandate the receipts are checked against.
import { createIssuer, generateSigner } from "@arcveildev/sdk"; const signer = await generateSigner(); // stand-in: in production this key lives in the enclave let issuer = createIssuer({ chainId: arc.id, account, mandate: { commitment, epoch: 1 }, agent: { id, session, vision: "relative-only" }, checks: ["asset_allowlist", "per_action_cap", "window_spend"], signer: { publicKey: signer.publicKey, privateKey: signer.privateKey }, counter: head, // last anchored budget commitment}); const { receipt, issuer: next } = await issuer.issue({ kind: "swap", userOpHash, settledTx });issuer = next; // issuing advances the chain — keep the issuer it hands back
Sign an account intent
The account executes only what two of its three keys signed, and only while the mandate named in the intent is live. The EIP-712 payload covers that mandate, so a signature gathered for one can never be replayed against another. Relaying is permissionless — authority is in the signatures, not in the sender.
import { encodeExecute, executeIntent, intentTypedData, signIntent } from "@arcveildev/sdk"; const intent = { call, nonce, deadline, epoch: 1, mandate: commitment }; // Two of the three keys sign the same EIP-712 payload, from wherever they live.const first = await signIntent(deviceClient, account, intent);const second = await signIntent(coSignerClient, account, intent); await executeIntent(relayClient, account, intent, [first, second]);
Exports
| Symbol | What it does |
|---|---|
| parseReceiptInput | zod parse of one receipt or a bundle. Returns { ok, receipts } or { ok: false, errors }. |
| verifyReceipts | Runs the five checks over a bundle and returns every verdict. |
| createRpcChainReader | Reads mandates, anchors and transactions from an Arc RPC endpoint. |
| createMemoryChainReader | The same interface over state you supply, for tests. |
| canonicalize · receiptBody · computeReceiptId | The exact bytes a receipt hashes, and the hash itself. |
| createIssuer · nextCounter | Issue receipts and advance the budget chain. |
| generateSigner · signBody · verifyBodySignature · issueReceipt | ECDSA P-256 over the canonical body. |
| mandateCommitment · registerMandate · revokeMandate · anchorCounter | Publish and retire mandates; anchor a budget commitment. |
| intentTypedData · signIntent · executeIntent · encodeExecute | The 2-of-3 account: sign one intent, relay it with two signatures. |
| adoptTypedData · encodeAdopt | Rotate the account onto a new mandate epoch, under the same quorum. |
| arc · arcTestnet · ARC_REGISTRIES · USDC_ERC20_ADDRESS | viem chain definitions and the deployed addresses. |
| MANDATE_REGISTRY_ABI · ANCHOR_REGISTRY_ABI · ARCVEIL_ACCOUNT_ABI | ABIs, for calls the SDK does not wrap. |