@integraledger/lcp-binding-evm-common 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/events.js ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * On-chain event helpers for the EIP-3009 binding: the `AuthorizationUsed` ABI and the transport-free
3
+ * atrHash-recovery scan (the caller fetches the receipt with its own viem client and passes `{ logs }`).
4
+ */
5
+ import { atrHashEquals } from "@integraledger/lcp-kernel";
6
+ import { decodeEventLog } from "viem";
7
+ /** Build a `SettlementRef` without materializing absent fields — `exactOptionalPropertyTypes` forbids
8
+ * `txHash: undefined`, so the spreads keep the ref exactly as sparse as the caller's knowledge. Shared
9
+ * by the EVM adapters (it was byte-identical in two of them and inlined in the third). */
10
+ export function refOf(chainId, txHash, logIndex) {
11
+ return {
12
+ chainId,
13
+ ...(txHash !== undefined ? { txHash } : {}),
14
+ ...(logIndex !== null ? { logIndex } : {}),
15
+ };
16
+ }
17
+ /** USDC `AuthorizationUsed(authorizer, nonce)` — both indexed; the on-chain artifact of every EIP-3009
18
+ * settlement. `nonce` carries the atrHash (WLD-3: recoverable, forward-indexable on the nonce topic). */
19
+ export const AUTHORIZATION_USED_ABI = [
20
+ {
21
+ type: "event",
22
+ name: "AuthorizationUsed",
23
+ inputs: [
24
+ { name: "authorizer", type: "address", indexed: true },
25
+ { name: "nonce", type: "bytes32", indexed: true },
26
+ ],
27
+ anonymous: false,
28
+ },
29
+ ];
30
+ /**
31
+ * Decode every `AuthorizationUsed(authorizer, indexed nonce)` in a set of logs (a single settlement's
32
+ * logs, from `ChainReader.getTransactionLogs`, or a range query). This is the EXTRACT path (recover the
33
+ * committed nonce), distinct from `verifyAtrHashOnChain`'s CHECK path (assert a known nonce is present).
34
+ * When `asset` is supplied only that token's events are read; otherwise every AuthorizationUsed is decoded.
35
+ */
36
+ export function readAuthorizationUsed(logs, asset) {
37
+ const want = asset?.toLowerCase();
38
+ const out = [];
39
+ for (const log of logs) {
40
+ const address = log.address.toLowerCase();
41
+ if (want !== undefined && address !== want)
42
+ continue;
43
+ try {
44
+ const decoded = decodeEventLog({
45
+ abi: AUTHORIZATION_USED_ABI,
46
+ data: log.data,
47
+ topics: log.topics,
48
+ });
49
+ if (decoded.eventName === "AuthorizationUsed")
50
+ out.push({
51
+ nonce: decoded.args.nonce.toLowerCase(),
52
+ authorizer: decoded.args.authorizer.toLowerCase(),
53
+ logIndex: log.logIndex,
54
+ address,
55
+ });
56
+ }
57
+ catch {
58
+ // Not an AuthorizationUsed log from this ABI — ignore.
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ /**
64
+ * Confirm the atrHash was committed on-chain: scan a settlement tx receipt for the token's
65
+ * `AuthorizationUsed(authorizer, indexed nonce)` event and assert `nonce === atrHash` (case-insensitive).
66
+ */
67
+ export function verifyAtrHashOnChain(receipt, params) {
68
+ const asset = params.asset.toLowerCase();
69
+ for (const log of receipt.logs) {
70
+ if (log.address.toLowerCase() !== asset)
71
+ continue;
72
+ try {
73
+ const decoded = decodeEventLog({
74
+ abi: AUTHORIZATION_USED_ABI,
75
+ data: log.data,
76
+ topics: log.topics,
77
+ });
78
+ if (decoded.eventName === "AuthorizationUsed") {
79
+ const nonce = decoded.args.nonce.toLowerCase();
80
+ if (atrHashEquals(nonce, params.atrHash))
81
+ return { ok: true, onChainNonce: nonce };
82
+ }
83
+ }
84
+ catch {
85
+ // Not an AuthorizationUsed log from this ABI — ignore.
86
+ }
87
+ }
88
+ return { ok: false, onChainNonce: null };
89
+ }
90
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export { makeChainReader } from "./client.js";
2
+ export { decodeEasAttestation, EAS_GET_ATTESTATION_ABI, type EasAttestation, isEasValidAsOf, type RawEasAttestation, readEasAttestation, } from "./eas.js";
3
+ export { hashEip712 } from "./eip712.js";
4
+ export { assertBytes32, type BuildEip3009Input, buildEip3009TypedData, type Eip3009Authorization, type Eip3009TypedData, eip155ChainId, type LcpEvmSigner, TRANSFER_WITH_AUTHORIZATION_TYPE, } from "./eip3009.js";
5
+ export { assetWasTransferred, ERC20_TRANSFER_TOPIC0 } from "./erc20.js";
6
+ export { ACCEPTANCE_DOMAIN_NAME, ACCEPTANCE_DOMAIN_VERSION, ACCEPTANCE_ENVELOPE_TYPE, type AcceptanceSignatureInput, type AcceptanceTypedData, type AcceptanceVerifyOpts, buildAcceptanceTypedData, EVM_ACCEPTANCE_SCHEMES, EVM_PAYLOAD_TYPES, type EvmAcceptanceScheme, type EvmPayloadType, isEvmAcceptanceScheme, isEvmPayloadType, makeEvmAcceptanceVerifier, makeLocalEvmSigner, rfc3339ToUnixSeconds, verifyAcceptanceSignature, } from "./erc1271.js";
7
+ export { AUTHORIZATION_USED_ABI, type AuthorizationUsedEvent, type OnChainAtrHashProof, readAuthorizationUsed, refOf, verifyAtrHashOnChain, } from "./events.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,KAAK,cAAc,EACnB,cAAc,EACd,KAAK,iBAAiB,EACtB,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,aAAa,EACb,KAAK,iBAAiB,EACtB,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,aAAa,EACb,KAAK,YAAY,EACjB,gCAAgC,GACjC,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,wBAAwB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,sBAAsB,EACtB,iBAAiB,EACjB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,qBAAqB,EACrB,gBAAgB,EAChB,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,sBAAsB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,qBAAqB,EACrB,KAAK,EACL,oBAAoB,GACrB,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { makeChainReader } from "./client.js";
2
+ export { decodeEasAttestation, EAS_GET_ATTESTATION_ABI, isEasValidAsOf, readEasAttestation, } from "./eas.js";
3
+ export { hashEip712 } from "./eip712.js";
4
+ export { assertBytes32, buildEip3009TypedData, eip155ChainId, TRANSFER_WITH_AUTHORIZATION_TYPE, } from "./eip3009.js";
5
+ export { assetWasTransferred, ERC20_TRANSFER_TOPIC0 } from "./erc20.js";
6
+ export { ACCEPTANCE_DOMAIN_NAME, ACCEPTANCE_DOMAIN_VERSION, ACCEPTANCE_ENVELOPE_TYPE, buildAcceptanceTypedData, EVM_ACCEPTANCE_SCHEMES, EVM_PAYLOAD_TYPES, isEvmAcceptanceScheme, isEvmPayloadType, makeEvmAcceptanceVerifier, makeLocalEvmSigner, rfc3339ToUnixSeconds, verifyAcceptanceSignature, } from "./erc1271.js";
7
+ export { AUTHORIZATION_USED_ABI, readAuthorizationUsed, refOf, verifyAtrHashOnChain, } from "./events.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EAEvB,cAAc,EAEd,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,aAAa,EAEb,qBAAqB,EAGrB,aAAa,EAEb,gCAAgC,GACjC,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,wBAAwB,EAIxB,wBAAwB,EACxB,sBAAsB,EACtB,iBAAiB,EAGjB,qBAAqB,EACrB,gBAAgB,EAChB,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,sBAAsB,EAGtB,qBAAqB,EACrB,KAAK,EACL,oBAAoB,GACrB,MAAM,aAAa,CAAC"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@integraledger/lcp-binding-evm-common",
3
+ "version": "0.9.0",
4
+ "description": "Shared EVM machinery for LCP rail bindings: typed-data construction and signature verification.",
5
+ "keywords": [
6
+ "lcp",
7
+ "legal-context-protocol",
8
+ "legal-terms",
9
+ "agentic-commerce",
10
+ "settlement",
11
+ "atr-hash",
12
+ "evm",
13
+ "eip-712",
14
+ "eip-3009"
15
+ ],
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src",
28
+ "CHANGELOG.md",
29
+ "LICENSE",
30
+ "NOTICE"
31
+ ],
32
+ "publishConfig": {
33
+ "registry": "https://registry.npmjs.org",
34
+ "access": "public"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/IntegraLedger/integra-protocol.git",
39
+ "directory": "packages/binding-evm-common"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/IntegraLedger/integra-protocol/issues"
43
+ },
44
+ "homepage": "https://github.com/IntegraLedger/integra-protocol/tree/main/packages/binding-evm-common#readme",
45
+ "dependencies": {
46
+ "viem": "2.55.11",
47
+ "@integraledger/lcp-authority": "0.9.0",
48
+ "@integraledger/lcp-binding-core": "0.9.0",
49
+ "@integraledger/lcp-kernel": "0.9.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "24.13.3",
53
+ "fast-check": "4.9.0",
54
+ "vitest": "4.1.10"
55
+ },
56
+ "license": "Apache-2.0",
57
+ "engines": {
58
+ "node": ">=24"
59
+ },
60
+ "scripts": {
61
+ "build": "tsc -p tsconfig.build.json",
62
+ "typecheck": "tsc -p tsconfig.json --noEmit",
63
+ "test": "vitest run"
64
+ }
65
+ }
package/src/client.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type {
2
+ ChainReader,
3
+ SettlementRef,
4
+ } from "@integraledger/lcp-binding-core";
5
+ import type { PublicClient } from "viem";
6
+
7
+ /**
8
+ * Adapt a viem `PublicClient` into binding-core's `ChainReader` port (viem lives here and in the two
9
+ * EVM adapters, nowhere else). Imperative shell over viem — the pure verifier logic reads through the port.
10
+ */
11
+ export function makeChainReader(client: PublicClient): ChainReader {
12
+ return {
13
+ getLogs: (q) => client.getLogs(q as never),
14
+ getTransactionLogs: async (ref: SettlementRef): Promise<unknown[]> => {
15
+ if (ref.txHash === undefined)
16
+ throw new Error(
17
+ "getTransactionLogs requires a txHash on the SettlementRef",
18
+ );
19
+ const receipt = await client.getTransactionReceipt({ hash: ref.txHash });
20
+ return [...receipt.logs];
21
+ },
22
+ readContract: (c) => client.readContract(c as never),
23
+ blockTime: async (ref: SettlementRef): Promise<bigint> => {
24
+ if (ref.txHash === undefined)
25
+ throw new Error("blockTime requires a txHash on the SettlementRef");
26
+ const receipt = await client.getTransactionReceipt({ hash: ref.txHash });
27
+ const block = await client.getBlock({ blockNumber: receipt.blockNumber });
28
+ return block.timestamp;
29
+ },
30
+ };
31
+ }
package/src/eas.ts ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * EAS (Ethereum Attestation Service) read — the EVM substrate adapter for a profiled attestation
3
+ * (`authority`'s attestation-profile.ts interprets attestations generically; the per-substrate on-chain
4
+ * read lives here, pure over the injected viem client). This reads `getAttestation(uid)` and
5
+ * normalizes the on-chain `Attestation` struct into a typed, as-of-checkable value. It verifies nothing
6
+ * cryptographically beyond the chain's own state — EAS attestations are on-chain records, so their
7
+ * validity is a state read (attester, expiration, revocation), evaluated AS OF a settlement time exactly
8
+ * like `authority`'s status-list discipline (never "is it valid now" — "was it valid at settlement").
9
+ */
10
+ import type { Hex, PublicClient } from "viem";
11
+
12
+ /** EAS `getAttestation(bytes32) returns (Attestation)` — the canonical struct (EAS v0.26+ field order). */
13
+ export const EAS_GET_ATTESTATION_ABI = [
14
+ {
15
+ type: "function",
16
+ name: "getAttestation",
17
+ stateMutability: "view",
18
+ inputs: [{ name: "uid", type: "bytes32" }],
19
+ outputs: [
20
+ {
21
+ type: "tuple",
22
+ name: "",
23
+ components: [
24
+ { name: "uid", type: "bytes32" },
25
+ { name: "schema", type: "bytes32" },
26
+ { name: "time", type: "uint64" },
27
+ { name: "expirationTime", type: "uint64" },
28
+ { name: "revocationTime", type: "uint64" },
29
+ { name: "refUID", type: "bytes32" },
30
+ { name: "recipient", type: "address" },
31
+ { name: "attester", type: "address" },
32
+ { name: "revocable", type: "bool" },
33
+ { name: "data", type: "bytes" },
34
+ ],
35
+ },
36
+ ],
37
+ },
38
+ ] as const;
39
+
40
+ /** The raw shape viem decodes an EAS `Attestation` tuple into (named components → object). */
41
+ export interface RawEasAttestation {
42
+ uid: Hex;
43
+ schema: Hex;
44
+ time: bigint;
45
+ expirationTime: bigint;
46
+ revocationTime: bigint;
47
+ refUID: Hex;
48
+ recipient: Hex;
49
+ attester: Hex;
50
+ revocable: boolean;
51
+ data: Hex;
52
+ }
53
+
54
+ /** A normalized EAS attestation (addresses lowercased; times as bigints; `exists` = a non-zero uid). */
55
+ export interface EasAttestation {
56
+ uid: Hex;
57
+ schema: Hex;
58
+ time: bigint;
59
+ expirationTime: bigint;
60
+ revocationTime: bigint;
61
+ refUID: Hex;
62
+ recipient: Hex;
63
+ attester: Hex;
64
+ revocable: boolean;
65
+ data: Hex;
66
+ /** EAS returns a zero-filled struct for an unknown uid — this flags that non-existence explicitly. */
67
+ exists: boolean;
68
+ }
69
+
70
+ const ZERO_UID =
71
+ "0x0000000000000000000000000000000000000000000000000000000000000000";
72
+
73
+ /** Normalize viem's decoded tuple into an `EasAttestation` (idempotent field mapping; addresses lowercased). */
74
+ export function decodeEasAttestation(raw: RawEasAttestation): EasAttestation {
75
+ return {
76
+ uid: raw.uid.toLowerCase() as Hex,
77
+ schema: raw.schema.toLowerCase() as Hex,
78
+ time: raw.time,
79
+ expirationTime: raw.expirationTime,
80
+ revocationTime: raw.revocationTime,
81
+ refUID: raw.refUID.toLowerCase() as Hex,
82
+ recipient: raw.recipient.toLowerCase() as Hex,
83
+ attester: raw.attester.toLowerCase() as Hex,
84
+ revocable: raw.revocable,
85
+ data: raw.data.toLowerCase() as Hex,
86
+ exists: raw.uid.toLowerCase() !== ZERO_UID,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Was this attestation valid AS OF `asOfUnixSeconds`? — it must exist, not have been revoked at/before the
92
+ * as-of time (revocationTime 0 = never revoked), and not have expired by then (expirationTime 0 = no expiry).
93
+ * EAS has no historical query, so a live read yields today's revocationTime; a settlement-time snapshot of the
94
+ * uid is the authority (same as the Bitstring status-list as-of rule) — this predicate states the semantics.
95
+ */
96
+ export function isEasValidAsOf(
97
+ att: EasAttestation,
98
+ asOfUnixSeconds: bigint,
99
+ ): boolean {
100
+ if (!att.exists) return false;
101
+ if (att.revocationTime !== 0n && att.revocationTime <= asOfUnixSeconds)
102
+ return false;
103
+ if (att.expirationTime !== 0n && att.expirationTime <= asOfUnixSeconds)
104
+ return false;
105
+ return true;
106
+ }
107
+
108
+ /** Read + normalize an EAS attestation by uid, over the injected viem client (imperative shell). */
109
+ export async function readEasAttestation(
110
+ client: PublicClient,
111
+ params: { eas: string; uid: string },
112
+ ): Promise<EasAttestation> {
113
+ const raw = (await client.readContract({
114
+ address: params.eas as Hex,
115
+ abi: EAS_GET_ATTESTATION_ABI,
116
+ functionName: "getAttestation",
117
+ args: [params.uid as Hex],
118
+ })) as RawEasAttestation;
119
+ return decodeEasAttestation(raw);
120
+ }
package/src/eip3009.ts ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The EIP-3009 `TransferWithAuthorization` typed-data machinery for the LCP canonical EVM binding
3
+ * `LCP-X402-EVM-NONCE-1`: the buyer signs an EIP-3009 authorization whose `nonce` field IS the
4
+ * `atrHash`, so the fingerprint is committed on-chain in the token's `AuthorizationUsed(authorizer,
5
+ * indexed nonce)` event — no overlay contract.
6
+ *
7
+ * The x402-client scheme class is deliberately NOT here; it lives in binding-evm-x402. This module is
8
+ * protocol-agnostic typed-data construction, viem-only.
9
+ */
10
+ import { type Address, getAddress, type Hex, type TypedDataDomain } from "viem";
11
+
12
+ /** EIP-3009 TransferWithAuthorization typed-data field layout (USDC FiatTokenV2). */
13
+ export const TRANSFER_WITH_AUTHORIZATION_TYPE = {
14
+ TransferWithAuthorization: [
15
+ { name: "from", type: "address" },
16
+ { name: "to", type: "address" },
17
+ { name: "value", type: "uint256" },
18
+ { name: "validAfter", type: "uint256" },
19
+ { name: "validBefore", type: "uint256" },
20
+ { name: "nonce", type: "bytes32" },
21
+ ],
22
+ } as const;
23
+
24
+ /** Parse the EIP-155 chain id out of a CAIP-2 network string; throws on anything that is not
25
+ * `eip155:<digits>` (never defaults — the chain id binds the EIP-712 domain). */
26
+ export function eip155ChainId(network: string): number {
27
+ const m = /^eip155:(\d+)$/.exec(network);
28
+ if (!m)
29
+ throw new Error(`expected CAIP-2 network "eip155:<id>", got "${network}"`);
30
+ return Number(m[1]);
31
+ }
32
+
33
+ /** The atrHash rides the EIP-3009 nonce as a 0x-prefixed 32-byte value. Any-case digits accepted
34
+ * (ATR canon), then lowercased for the on-chain nonce (the nonce is matched lowercase on decode). */
35
+ export function assertBytes32(value: string): Hex {
36
+ if (!/^0x[0-9a-fA-F]{64}$/.test(value))
37
+ throw new Error(
38
+ `atrHash must be a 0x-prefixed 32-byte (64 hex) value to ride as the EIP-3009 nonce, got "${value}"`,
39
+ );
40
+ return value.toLowerCase() as Hex;
41
+ }
42
+
43
+ /** The EIP-3009 authorization object (string-encoded uint fields, as the x402 wire expects). */
44
+ export interface Eip3009Authorization {
45
+ from: Address;
46
+ to: Address;
47
+ value: string;
48
+ validAfter: string;
49
+ validBefore: string;
50
+ nonce: Hex;
51
+ }
52
+
53
+ /** Everything needed to build an EIP-3009 authorization whose `nonce` IS the atrHash. Two fields are the
54
+ * usual mistake: `tokenName`/`tokenVersion` are the token's own EIP-712 domain and DIFFER between USDC
55
+ * deployments, so copying them from one chain to another yields a signature the token rejects. Amounts
56
+ * and times are strings here (base units, unix seconds) and become `bigint` in the typed data. */
57
+ export interface BuildEip3009Input {
58
+ /** The LCP carrier value — becomes the on-chain nonce. */
59
+ atrHash: string;
60
+ /** Payer address; checksummed via `getAddress`. */
61
+ from: string;
62
+ /** Recipient address; checksummed via `getAddress`. */
63
+ to: string;
64
+ /** Amount in token base units (string). */
65
+ value: string;
66
+ validAfter: string;
67
+ validBefore: string;
68
+ chainId: number;
69
+ /** EIP-712 domain `name`/`version` for the token (USDC). */
70
+ tokenName: string;
71
+ tokenVersion: string;
72
+ /** Token contract — the EIP-712 `verifyingContract`; checksummed via `getAddress`. */
73
+ verifyingContract: string;
74
+ }
75
+
76
+ /** The EIP-712 payload a payer signs for an EIP-3009 transfer. Distinct from the authorization: the
77
+ * authorization is what goes on the wire, this is what is signed, and their numeric fields differ in type
78
+ * (`bigint` here, decimal strings there) because that is what each side requires. */
79
+ export type Eip3009TypedData = {
80
+ domain: TypedDataDomain;
81
+ types: typeof TRANSFER_WITH_AUTHORIZATION_TYPE;
82
+ primaryType: "TransferWithAuthorization";
83
+ message: {
84
+ from: Address;
85
+ to: Address;
86
+ value: bigint;
87
+ validAfter: bigint;
88
+ validBefore: bigint;
89
+ nonce: Hex;
90
+ };
91
+ };
92
+
93
+ /**
94
+ * Build the EIP-3009 authorization + the EIP-712 typed-data to sign, with `nonce = atrHash`.
95
+ * `authorization` is the wire payload; `typedData` is what the signer signs.
96
+ */
97
+ export function buildEip3009TypedData(i: BuildEip3009Input): {
98
+ authorization: Eip3009Authorization;
99
+ typedData: Eip3009TypedData;
100
+ } {
101
+ const nonce = assertBytes32(i.atrHash);
102
+ const from = getAddress(i.from);
103
+ const to = getAddress(i.to);
104
+ const authorization: Eip3009Authorization = {
105
+ from,
106
+ to,
107
+ value: i.value,
108
+ validAfter: i.validAfter,
109
+ validBefore: i.validBefore,
110
+ nonce,
111
+ };
112
+ const typedData: Eip3009TypedData = {
113
+ domain: {
114
+ name: i.tokenName,
115
+ version: i.tokenVersion,
116
+ chainId: i.chainId,
117
+ verifyingContract: getAddress(i.verifyingContract),
118
+ },
119
+ types: TRANSFER_WITH_AUTHORIZATION_TYPE,
120
+ primaryType: "TransferWithAuthorization",
121
+ message: {
122
+ from,
123
+ to,
124
+ value: BigInt(i.value),
125
+ validAfter: BigInt(i.validAfter),
126
+ validBefore: BigInt(i.validBefore),
127
+ nonce,
128
+ },
129
+ };
130
+ return { authorization, typedData };
131
+ }
132
+
133
+ /** Minimal EVM signer — the structural subset a scheme needs (viem LocalAccount / CDP account / injected wallet). */
134
+ export interface LcpEvmSigner {
135
+ readonly address: Address;
136
+ signTypedData(message: {
137
+ domain: Record<string, unknown>;
138
+ types: Record<string, unknown>;
139
+ primaryType: string;
140
+ message: Record<string, unknown>;
141
+ }): Promise<Hex>;
142
+ }
package/src/eip712.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { hashTypedData } from "viem";
2
+ import type { Eip3009TypedData } from "./eip3009.js";
3
+
4
+ /**
5
+ * The EIP-712 digest of an EIP-3009 typed-data message — viem's reference EIP-712 hashing
6
+ * (keccak256(0x1901 ‖ domainSeparator ‖ hashStruct(message))). This is what the signer signs over
7
+ * and what pins the typed-data structure in the vector fixtures.
8
+ */
9
+ export function hashEip712(typedData: Eip3009TypedData): `0x${string}` {
10
+ return hashTypedData(typedData);
11
+ }