casper-eip-712 is a multi-language EIP-712 toolkit for Casper. This repository contains the no_std-compatible Rust core crate, plus companion TypeScript and Go packages that application developers can use to generate and verify EIP-712 typed data messages.
no_std+alloc- EIP-712-compatible type hashing and
\x19\x01typed data digest construction - Flexible
DomainBuilderwith standard EVM fields and custom Casper-native fields - Prebuilt
Permit,Approval, andTransferstructs - Optional
verifyfeature for signer recovery and verification - Optional
casper-nativefeature: Casper PublicKey verification +TransferAuthorization/BatchTransferAuthorizationstructs (v1.2.0+) - Companion TypeScript and Go packages for application integration
- Shared cross-language vectors to keep Rust, TypeScript, and Go outputs in sync
- Rust (
src/): corecasper-eip-712crate for hashing, domain separation, encoding helpers, and optional secp256k1 recovery. - TypeScript (
js/): companion package for dApps/services to generate and verify EIP-712 typed data messages. - Go (
go/): companion package for backend/services to hash typed data and verify EIP-712 signatures.
- Rust crate docs:
README.md - TypeScript package docs:
js/README.md - Go package docs:
go/README.md
use casper_eip_712::prelude::*;
let domain = DomainBuilder::new()
.name("MyToken")
.version("1")
.chain_id(1)
.verifying_contract([0x11; 20])
.build();
let permit = Permit {
owner: [0x22; 20],
spender: [0x33; 20],
value: [0u8; 32],
nonce: [0u8; 32],
deadline: [0u8; 32],
};
let digest = hash_typed_data(&domain, &permit);
assert_eq!(digest.len(), 32);- Rust (nightly recommended — see
rust-toolchain.toml) - Node.js 18+ (for the TypeScript package and demo)
- npm
cargo build
cargo testcd js
npm install
npm run build
npm testThe demo showcases a CEP-18 token with gasless permit/approve using EIP-712 signatures. It runs locally without a Casper node.
# 1. Build the TypeScript package first (the demo depends on it)
cd js && npm install && npm run build && cd ..
# 2. Install and run the demo
cd examples/permit-token/demo
npm install
npx tsx demo.tscd examples/permit-token
cargo odra testNote:
casper-clientis pinned to 5.0.0 in the lock file. Version 5.0.1 introduced breaking API changes that are incompatible with Odra 2.5.0. If you regenerate the lock file, runcargo update casper-client --precise 5.0.0to re-pin.
src/ — Core Rust crate (no_std, EIP-712 encoding + hashing)
js/ — TypeScript companion package (@casper-ecosystem/casper-eip-712)
src/ — TypeScript source
dist/ — Built output (run npm run build)
go/ — Go companion package (typed data hashing + verification helpers)
examples/
permit-token/ — Demo contract: CEP-18 with permit/approve pattern
src/ — Odra smart contract (Rust)
tests/ — Rust integration tests
demo/ — Standalone TypeScript demo
scripts/ — Test vector generation
use alloc::vec::Vec;
use casper_eip_712::prelude::*;
struct Attestation {
subject: Address,
claim_hash: [u8; 32],
}
impl Eip712Struct for Attestation {
fn type_string() -> &'static str {
"Attestation(address subject,bytes32 claim_hash)"
}
fn type_hash() -> [u8; 32] {
keccak256(Self::type_string().as_bytes())
}
fn encode_data(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(64);
out.extend_from_slice(&encode_address(self.subject));
out.extend_from_slice(&encode_bytes32(self.claim_hash));
out
}
}The chain_name field is recommended to be a CAIP-2 chain id of the form casper:<chainspec_name> — for example casper:casper for mainnet and casper:casper-test for testnet. Using the canonical CAIP-2 form keeps Casper domain separators interoperable with multi-chain wallets and tooling.
use casper_eip_712::prelude::*;
let domain = DomainBuilder::new()
.name("Bridge")
.version("1")
.custom_field("chain_name", DomainFieldValue::String("casper:casper-test".into()))
.custom_field("contract_package_hash", DomainFieldValue::Bytes32([0x99; 32]))
.build();See examples/permit-token/ for a complete working
example — a CEP-18 token with gasless permit/approve using EIP-712 signatures,
supporting both EVM-compatible and Casper-native domain separators. Includes a
standalone TypeScript demo you can run without a Casper node.
EIP-712 signatures enable a "gasless" user experience where token owners authorize actions by signing a typed message off-chain — without submitting a transaction or paying gas. A relayer or counterparty submits the signed authorization on-chain on their behalf.
This pattern, widely adopted on Ethereum as ERC-2612 (used by Uniswap, Aave, OpenSea, and others), becomes possible on Casper through this crate:
- A user signs an EIP-712
Permitmessage with their private key (off-chain, zero cost) - A relayer or dApp backend submits the permit to the Casper contract, paying the deploy cost
- The contract verifies the signature, recovers the signer, and executes the authorized action
This unlocks several powerful patterns:
- User onboarding without CSPR — new users can interact with dApps before acquiring native tokens for gas
- Meta-transactions — dApp operators subsidize gas costs to reduce friction
- Batch authorization — a relayer collects multiple signed permits and submits them in a single deploy
The x402 protocol enables AI agents to pay for API access and services using HTTP 402 payment flows with stablecoins. EIP-712 typed data signing is the natural authorization layer for agent-initiated transactions on Casper:
- An AI agent signs a structured, domain-separated payment authorization
- A facilitator or smart contract verifies the signature and executes the payment
- The typed data schema makes the authorization human-readable and auditable — critical when autonomous agents are moving value
As x402 expands to Casper, casper-eip-712 provides the cryptographic foundation for agents to sign verifiable, scoped authorizations that smart contracts can trust.
In multi-chain environments — bridges, cross-chain messaging protocols, and multi-deployment systems — domain separation is critical for security. Without it, a signature intended for one deployment or chain can be replayed against another, potentially draining funds or corrupting state.
EIP-712 domain separators solve this by binding every signature to a specific:
- Contract identity (address or package hash)
- Chain (chain ID or chain name)
- Version (protocol version for upgrade safety)
This crate's DomainBuilder supports both standard EVM fields (chainId, verifyingContract) and Casper-native fields (chain_name, contract_package_hash), making it suitable for hybrid environments where Casper contracts verify attestations originating from EVM chains — or where multiple Casper deployments (testnet, mainnet, staging) must be cryptographically isolated from each other. When using the Casper-native domain, chain_name is recommended to follow the CAIP-2 format (casper:<chainspec_name>).
- default: minimal hashing/encoding support
verify: enables secp256k1 signer recovery viak256casper-native: Casper Network native signer verification + authorized transfer structs (see below)
Enable in Cargo.toml:
casper-eip-712 = { version = "1.2.0", features = ["casper-native"] }Requires casper-types v7 (added automatically as a dependency when the feature is enabled).
use casper_eip_712::prelude::*;
use casper_eip_712::casper_native::verify_casper_signer;
use casper_types::{PublicKey, Signature};
// domain and my_struct are your EIP-712 domain separator and typed struct
// public_key and signature come from the Casper transaction/contract call
// Verify signature and check that the signer matches the expected `from` field.
// Returns Ok(AccountHash) on success.
let account_hash = verify_casper_signer(
&domain,
&my_struct,
&public_key,
&signature,
Some(&expected_from), // optional: verify AccountHash matches
).expect("verification failed");use casper_eip_712::prelude::*;
use casper_eip_712::casper_native::{TransferAuthorization, BatchTransferAuthorization, BatchEntry};
// Single authorized transfer
let auth = TransferAuthorization {
from: [0x11; 32], // 32-byte AccountHash
to: [0x22; 32], // 32-byte AccountHash
value: [0u8; 32], // U256 big-endian
valid_after: 0, // Unix timestamp (u64)
valid_before: 9_999_999_999,
nonce: [0xAA; 32],
};
let digest = hash_typed_data(&domain, &auth);
// Multi-transfer for x402 payment flows
let batch = BatchTransferAuthorization {
from: [0x11; 32],
transfers: vec![
BatchEntry { to: [0x22; 32], value: [0u8; 32] },
BatchEntry { to: [0x33; 32], value: [0u8; 32] },
],
valid_after: 0,
valid_before: 9_999_999_999,
nonce: [0xBB; 32],
};import {
TransferAuthorizationTypes,
BatchTransferAuthorizationTypes,
hashTypedData,
type TransferAuthorizationMessage,
type BatchTransferAuthorizationMessage,
} from "@casper-ecosystem/casper-eip-712";
const auth: TransferAuthorizationMessage = {
from: "0x" + "11".repeat(32), // 32-byte AccountHash hex
to: "0x" + "22".repeat(32),
value: 0n,
valid_after: 0n,
valid_before: 9_999_999_999n,
nonce: "0x" + "aa".repeat(32),
};
const digest = hashTypedData(domain, TransferAuthorizationTypes, "TransferAuthorization", auth);The crate is designed for #![no_std] environments with alloc available, making it suitable for WASM-based contract targets.
- Generate test vectors with
npm --prefix scripts run generate(the generator is TypeScript and is executed withtsx). Cargo.lockis committed to pincasper-clientto 5.0.0 (see note above about Odra compatibility). If you regenerate it, re-pin withcargo update casper-client --precise 5.0.0.