@tokenops/sdk/contract-kit is a viem-only boundary for contract-aware screens, signing review, indexers, and release gates. It does not load Zama or React. Every TokenOps ABI lookup is explicit about its abiVersion, so stored transactions remain decodable after a later ABI is added.
ABIs, calls, events, and reverts#
| Contract kind | ABI coverage |
|---|---|
factory / manager | The versioned confidential vesting factory and manager ABIs. |
token | ERC-7984 metadata, balances, operators, confidential transfers, disclosures, and ERC-165. |
wrapper | Wrap, unwrap, finalize, underlying/rate reads, and unwrap events. |
acl | Allowed/delegation events and delegateForUserDecryption. |
safe | Owners, threshold, nonce, ERC-1271, transaction hash, execution, and core events. |
import {
ABI_VERSIONS,
KitError,
ROLES,
abiOf,
decodeCallData,
decodeRevert,
encodeCall,
} from "@tokenops/sdk/contract-kit";
const holder = "0x1111111111111111111111111111111111111111";
const [version] = ABI_VERSIONS;
if (!version) throw new Error("no supported ABI version");
const data = encodeCall(
"manager",
"grantRole",
[ROLES.CLAIMER_ROLE, holder],
version,
);
const decoded = decodeCallData("manager", data, version);
const managerAbi = abiOf("manager", version);
const revert = decodeRevert("0x", version);
try {
encodeCall("manager", "doesNotExist", [], version);
} catch (error) {
if (error instanceof KitError && error.code === "unknown-function") {
// Render a bounded application message.
}
}encodeCall and decodeCallData throw unknown-function when the selected ABI has no match. decodeEvent returns null for an unknown event signature. decodeRevert maps known manager and factory custom errors to stable kebab-case codes; short or unknown data becomes unknown-revert.
Handles and ACL receipts#
asHandle validates a 32-byte hex value, lowercases it, and brands it so application code does not perform arithmetic on a ciphertext reference. After a manager read grants ACL access, extractAclHandle finds exactly one Allowed log matching ACL, manager, and account.
import {
extractAclHandle,
type KitLog,
} from "@tokenops/sdk/contract-kit";
declare const receiptLogs: readonly KitLog[];
const handle = extractAclHandle(receiptLogs, {
acl: "0x1111111111111111111111111111111111111111",
manager: "0x2222222222222222222222222222222222222222",
account: "0x3333333333333333333333333333333333333333",
});No match throws acl-event-missing; more than one throws acl-event-ambiguous. Do not pick the first log: ambiguity is a receipt-integrity failure.
Decode a transaction for signing review#
import {
createCallKit,
type ReleaseData,
} from "@tokenops/sdk/contract-kit";
const factory = "0x1111111111111111111111111111111111111111";
const manager = "0x2222222222222222222222222222222222222222";
const acl = "0x3333333333333333333333333333333333333333";
// Application-owned synthetic inventory for this review example.
const release: ReleaseData = {
inventory: [
{
chainId: 11_155_111,
kind: "manager",
address: manager,
abiVersion: "v1",
deploymentBlock: 7_000_000,
bytecodeHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
auditFixes: [],
},
],
addresses: [
{
chainId: 11_155_111,
environment: "testnet",
factory,
acl,
wrappers: [],
},
],
};
const kit = createCallKit({ release });
const review = kit.decodeCalls({
kind: "transaction",
chainId: 11_155_111,
to: manager,
data: "0x",
});
const factories = kit.factories(11_155_111);
const digest = kit.callsHash([
{ chainId: 11_155_111, target: manager, data: "0x" },
]);The example's ReleaseData is an application-owned decode inventory, not the canonical TokenOps deployment registry. createCallKit trusts this input: it does not run checkRelease, query bytecode, or prove runtime conformance.
createCallKit resolves a target from the supplied release inventory, decodes its selector, attaches one or more policy families, and extracts grantRole role and holder fields. An unknown target remains visible with null function and ABI fields. In 2.1.0, user-operation account encodings and sponsorship-delegate batches return null; callers must treat that as “not decoded,” never as an empty safe batch.
| Selector family | Typical intent |
|---|---|
| admin | Role and factory/manager configuration |
| operations | Create, revoke, pause, and administrator claims |
| claim | Recipient claim and partial claim |
| grant-transfer | Transfer, accept, cancel, direct transfer, and split |
| transfer / unwrap | ERC-7984 movement and wrapper settlement |
| disclosure | Manager views, disclosure, and ACL delegation |
| withdraw-approved | Explicitly opted-in manager withdrawals |
| denied | Sensitive calls excluded from ordinary approval policy |
Hashes and typed data#
callsHash is SHA-256 over ABI-encoded (chainId, target, data)[]. It is order-sensitive and normalizes target addresses. Use it to bind an approval to the reviewed call list, not as a transaction hash.
import {
encodeIsValidSignature,
safeTxHash,
type SafeTransactionData,
} from "@tokenops/sdk/contract-kit";
const safeAddress = "0x1111111111111111111111111111111111111111";
const target = "0x2222222222222222222222222222222222222222";
const zero = "0x0000000000000000000000000000000000000000" as const;
const safeTransactionData: SafeTransactionData = {
to: target,
value: "0",
data: "0x",
operation: 0,
safeTxGas: "0",
baseGas: "0",
gasPrice: "0",
gasToken: zero,
refundReceiver: zero,
nonce: 7,
};
const hash = safeTxHash({
chainId: 11_155_111,
safeAddress,
safeTransactionData,
});
const erc1271Call = encodeIsValidSignature(hash, "0x1234");safeTxHash computes the Safe EIP-712 digest for the exact no-refund transaction shape in SafeTransactionData. encodeIsValidSignatureproduces the matching ERC-1271 call. The call kit's typedDataKind returns safe-tx or zama-user-decrypt from ordered field arrays, not from typed-data values. Safe classification checks the primary type and exact SafeTx fields but ignores its domain definition, extra type entries, and message values. Zama classification checks the primary type plus exact EIP-712 domain and request fields, while still ignoring extra type entries, domain values, and message values. Anything else is other.
Reads and release gates#
The read helpers accept a small KitReader port, which a viem public client can implement. They cover manager fee, factory fee defaults, maximum batch size, token decimals/symbol, ERC-7984 support, and bytecode presence. unknown-fee-type is thrown for any fee enum except 0 (gas) or 1 (distribution token).
checkRelease(record, previous) returns reason strings instead of throwing. A clean release has:
- lower-case 32-byte bytecode hashes for every inventory row;
- a deployment block for every factory;
- factory, ACL, and wrapper addresses represented in inventory;
- passed parity on every release;
- ceremony evidence when inventory or addresses changed;
- a roster entry for every factory and no role still held by the launch key.
Treat a non-empty result as a deployment blocker. The helper validates release evidence; it does not query a chain or repair a record.