@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/CHANGELOG.md +13 -0
- package/LICENSE +202 -0
- package/NOTICE +14 -0
- package/README.md +75 -0
- package/dist/client.d.ts +8 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +24 -0
- package/dist/client.js.map +1 -0
- package/dist/eas.d.ts +98 -0
- package/dist/eas.d.ts.map +1 -0
- package/dist/eas.js +70 -0
- package/dist/eas.js.map +1 -0
- package/dist/eip3009.d.ts +104 -0
- package/dist/eip3009.d.ts.map +1 -0
- package/dist/eip3009.js +73 -0
- package/dist/eip3009.js.map +1 -0
- package/dist/eip712.d.ts +8 -0
- package/dist/eip712.d.ts.map +1 -0
- package/dist/eip712.js +10 -0
- package/dist/eip712.js.map +1 -0
- package/dist/erc1271.d.ts +147 -0
- package/dist/erc1271.d.ts.map +1 -0
- package/dist/erc1271.js +197 -0
- package/dist/erc1271.js.map +1 -0
- package/dist/erc20.d.ts +36 -0
- package/dist/erc20.d.ts.map +1 -0
- package/dist/erc20.js +20 -0
- package/dist/erc20.js.map +1 -0
- package/dist/events.d.ts +67 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +90 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
- package/src/client.ts +31 -0
- package/src/eas.ts +120 -0
- package/src/eip3009.ts +142 -0
- package/src/eip712.ts +11 -0
- package/src/erc1271.ts +321 -0
- package/src/erc20.ts +48 -0
- package/src/events.ts +126 -0
- package/src/index.ts +48 -0
package/dist/eip3009.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
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 { getAddress } from "viem";
|
|
11
|
+
/** EIP-3009 TransferWithAuthorization typed-data field layout (USDC FiatTokenV2). */
|
|
12
|
+
export const TRANSFER_WITH_AUTHORIZATION_TYPE = {
|
|
13
|
+
TransferWithAuthorization: [
|
|
14
|
+
{ name: "from", type: "address" },
|
|
15
|
+
{ name: "to", type: "address" },
|
|
16
|
+
{ name: "value", type: "uint256" },
|
|
17
|
+
{ name: "validAfter", type: "uint256" },
|
|
18
|
+
{ name: "validBefore", type: "uint256" },
|
|
19
|
+
{ name: "nonce", type: "bytes32" },
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
/** Parse the EIP-155 chain id out of a CAIP-2 network string; throws on anything that is not
|
|
23
|
+
* `eip155:<digits>` (never defaults — the chain id binds the EIP-712 domain). */
|
|
24
|
+
export function eip155ChainId(network) {
|
|
25
|
+
const m = /^eip155:(\d+)$/.exec(network);
|
|
26
|
+
if (!m)
|
|
27
|
+
throw new Error(`expected CAIP-2 network "eip155:<id>", got "${network}"`);
|
|
28
|
+
return Number(m[1]);
|
|
29
|
+
}
|
|
30
|
+
/** The atrHash rides the EIP-3009 nonce as a 0x-prefixed 32-byte value. Any-case digits accepted
|
|
31
|
+
* (ATR canon), then lowercased for the on-chain nonce (the nonce is matched lowercase on decode). */
|
|
32
|
+
export function assertBytes32(value) {
|
|
33
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(value))
|
|
34
|
+
throw new Error(`atrHash must be a 0x-prefixed 32-byte (64 hex) value to ride as the EIP-3009 nonce, got "${value}"`);
|
|
35
|
+
return value.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build the EIP-3009 authorization + the EIP-712 typed-data to sign, with `nonce = atrHash`.
|
|
39
|
+
* `authorization` is the wire payload; `typedData` is what the signer signs.
|
|
40
|
+
*/
|
|
41
|
+
export function buildEip3009TypedData(i) {
|
|
42
|
+
const nonce = assertBytes32(i.atrHash);
|
|
43
|
+
const from = getAddress(i.from);
|
|
44
|
+
const to = getAddress(i.to);
|
|
45
|
+
const authorization = {
|
|
46
|
+
from,
|
|
47
|
+
to,
|
|
48
|
+
value: i.value,
|
|
49
|
+
validAfter: i.validAfter,
|
|
50
|
+
validBefore: i.validBefore,
|
|
51
|
+
nonce,
|
|
52
|
+
};
|
|
53
|
+
const typedData = {
|
|
54
|
+
domain: {
|
|
55
|
+
name: i.tokenName,
|
|
56
|
+
version: i.tokenVersion,
|
|
57
|
+
chainId: i.chainId,
|
|
58
|
+
verifyingContract: getAddress(i.verifyingContract),
|
|
59
|
+
},
|
|
60
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPE,
|
|
61
|
+
primaryType: "TransferWithAuthorization",
|
|
62
|
+
message: {
|
|
63
|
+
from,
|
|
64
|
+
to,
|
|
65
|
+
value: BigInt(i.value),
|
|
66
|
+
validAfter: BigInt(i.validAfter),
|
|
67
|
+
validBefore: BigInt(i.validBefore),
|
|
68
|
+
nonce,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
return { authorization, typedData };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=eip3009.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eip3009.js","sourceRoot":"","sources":["../src/eip3009.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAgB,UAAU,EAAkC,MAAM,MAAM,CAAC;AAEhF,qFAAqF;AACrF,MAAM,CAAC,MAAM,gCAAgC,GAAG;IAC9C,yBAAyB,EAAE;QACzB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE;QACjC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE;QAC/B,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;QAClC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE;QACvC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE;QACxC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;KACnC;CACO,CAAC;AAEX;kFACkF;AAClF,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,MAAM,CAAC,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,CAAC,CAAC;QACJ,MAAM,IAAI,KAAK,CAAC,+CAA+C,OAAO,GAAG,CAAC,CAAC;IAC7E,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACtB,CAAC;AAED;sGACsG;AACtG,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,4FAA4F,KAAK,GAAG,CACrG,CAAC;IACJ,OAAO,KAAK,CAAC,WAAW,EAAS,CAAC;AACpC,CAAC;AAoDD;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,CAAoB;IAIxD,MAAM,KAAK,GAAG,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC5B,MAAM,aAAa,GAAyB;QAC1C,IAAI;QACJ,EAAE;QACF,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,KAAK;KACN,CAAC;IACF,MAAM,SAAS,GAAqB;QAClC,MAAM,EAAE;YACN,IAAI,EAAE,CAAC,CAAC,SAAS;YACjB,OAAO,EAAE,CAAC,CAAC,YAAY;YACvB,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,iBAAiB,EAAE,UAAU,CAAC,CAAC,CAAC,iBAAiB,CAAC;SACnD;QACD,KAAK,EAAE,gCAAgC;QACvC,WAAW,EAAE,2BAA2B;QACxC,OAAO,EAAE;YACP,IAAI;YACJ,EAAE;YACF,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;YACtB,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;YAChC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC;YAClC,KAAK;SACN;KACF,CAAC;IACF,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC;AACtC,CAAC"}
|
package/dist/eip712.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Eip3009TypedData } from "./eip3009.js";
|
|
2
|
+
/**
|
|
3
|
+
* The EIP-712 digest of an EIP-3009 typed-data message — viem's reference EIP-712 hashing
|
|
4
|
+
* (keccak256(0x1901 ‖ domainSeparator ‖ hashStruct(message))). This is what the signer signs over
|
|
5
|
+
* and what pins the typed-data structure in the vector fixtures.
|
|
6
|
+
*/
|
|
7
|
+
export declare function hashEip712(typedData: Eip3009TypedData): `0x${string}`;
|
|
8
|
+
//# sourceMappingURL=eip712.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eip712.d.ts","sourceRoot":"","sources":["../src/eip712.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,gBAAgB,GAAG,KAAK,MAAM,EAAE,CAErE"}
|
package/dist/eip712.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { hashTypedData } from "viem";
|
|
2
|
+
/**
|
|
3
|
+
* The EIP-712 digest of an EIP-3009 typed-data message — viem's reference EIP-712 hashing
|
|
4
|
+
* (keccak256(0x1901 ‖ domainSeparator ‖ hashStruct(message))). This is what the signer signs over
|
|
5
|
+
* and what pins the typed-data structure in the vector fixtures.
|
|
6
|
+
*/
|
|
7
|
+
export function hashEip712(typedData) {
|
|
8
|
+
return hashTypedData(typedData);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=eip712.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eip712.js","sourceRoot":"","sources":["../src/eip712.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAGrC;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,SAA2B;IACpD,OAAO,aAAa,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acceptance-signature verification (TRM-6) — the EVM implementation of `authority`'s `SignatureVerifier`
|
|
3
|
+
* port. viem lives here. It verifies the LCP acceptance envelope: a stock
|
|
4
|
+
* Coinbase Smart Wallet (CDP Server Wallet v2) exposes only `signTypedData`, so raw-`atrHash` signing is
|
|
5
|
+
* refused and the acceptance rides an EIP-712 envelope `Acceptance{ bytes32 atrHash, uint256 signedAt }`
|
|
6
|
+
* under domain `{ name: "LCP Acceptance", version: "1", chainId }` (no verifyingContract). Note:
|
|
7
|
+
* `verifyTypedData` returns true for the stock-SDK signature — counterfactual accounts wrap it ERC-6492,
|
|
8
|
+
* deployed accounts return a plain ERC-1271 sig; both verify through viem's universal validator.
|
|
9
|
+
*
|
|
10
|
+
* Two verification paths, split by scheme:
|
|
11
|
+
* - EOA (`evm:eip191`/`evm:eip712`) — recovered OFFLINE (no chain), so these are the schemes the
|
|
12
|
+
* deterministic conformance corpus and this package's tests exercise;
|
|
13
|
+
* - smart account (`evm:erc1271`/`evm:erc6492`) — verified on-chain through the injected `PublicClient`
|
|
14
|
+
* (viem's universal `verifyTypedData`/`verifyMessage` handle ERC-1271 and the ERC-6492 counterfactual
|
|
15
|
+
* wrapper). No client → fail-loud (never a silent false — that would impeach a valid smart-account
|
|
16
|
+
* signature).
|
|
17
|
+
*
|
|
18
|
+
* `AcceptanceScheme` is an OPEN namespaced set and `signer` is scheme-canonical (rail-neutral core); THIS
|
|
19
|
+
* adapter is where the narrowing happens: it accepts exactly the four `evm:*` schemes and the 0x-address
|
|
20
|
+
* signer form, and throws — never a silent false — on a record that belongs to a different family.
|
|
21
|
+
*/
|
|
22
|
+
import type { SignatureVerifier } from "@integraledger/lcp-authority";
|
|
23
|
+
import { type Hex, type PublicClient } from "viem";
|
|
24
|
+
import { type LcpEvmSigner } from "./eip3009.js";
|
|
25
|
+
/** The EIP-712 acceptance-envelope struct. */
|
|
26
|
+
export declare const ACCEPTANCE_ENVELOPE_TYPE: {
|
|
27
|
+
readonly Acceptance: readonly [{
|
|
28
|
+
readonly name: "atrHash";
|
|
29
|
+
readonly type: "bytes32";
|
|
30
|
+
}, {
|
|
31
|
+
readonly name: "signedAt";
|
|
32
|
+
readonly type: "uint256";
|
|
33
|
+
}];
|
|
34
|
+
};
|
|
35
|
+
/** The acceptance envelope's EIP-712 domain — name/version pinned, chainId per network, no verifyingContract. */
|
|
36
|
+
export declare const ACCEPTANCE_DOMAIN_NAME = "LCP Acceptance";
|
|
37
|
+
/** The acceptance envelope's EIP-712 domain `version`. Pinned, and part of what every acceptance
|
|
38
|
+
* signature commits to — changing it invalidates every signature made under the old value, so it moves
|
|
39
|
+
* only alongside a deliberate envelope revision. */
|
|
40
|
+
export declare const ACCEPTANCE_DOMAIN_VERSION = "1";
|
|
41
|
+
/** The typed-data object the accepting party signs (message.signedAt is unix seconds — the on-chain uint256). */
|
|
42
|
+
export type AcceptanceTypedData = {
|
|
43
|
+
domain: {
|
|
44
|
+
name: string;
|
|
45
|
+
version: string;
|
|
46
|
+
chainId: number;
|
|
47
|
+
};
|
|
48
|
+
types: typeof ACCEPTANCE_ENVELOPE_TYPE;
|
|
49
|
+
primaryType: "Acceptance";
|
|
50
|
+
message: {
|
|
51
|
+
atrHash: Hex;
|
|
52
|
+
signedAt: bigint;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Convert TRM-6's RFC 3339 `signedAt` (the acceptance record's timestamp) into the uint256 unix seconds the
|
|
57
|
+
* EIP-712 envelope commits to. Fail-loud on anything Date cannot parse — a mis-parsed timestamp would build
|
|
58
|
+
* a different envelope and silently fail verification, hiding a malformed record behind a bad-signature error.
|
|
59
|
+
*/
|
|
60
|
+
export declare function rfc3339ToUnixSeconds(signedAt: string): bigint;
|
|
61
|
+
/** Build the acceptance envelope typed-data for a fixed atrHash + unix-seconds signedAt + chainId. */
|
|
62
|
+
export declare function buildAcceptanceTypedData(i: {
|
|
63
|
+
atrHash: string;
|
|
64
|
+
signedAt: bigint;
|
|
65
|
+
chainId: number;
|
|
66
|
+
}): AcceptanceTypedData;
|
|
67
|
+
/** The four schemes this port implements — the EVM half of the open `AcceptanceScheme` set. */
|
|
68
|
+
export declare const EVM_ACCEPTANCE_SCHEMES: readonly ["evm:eip191", "evm:eip712", "evm:erc1271", "evm:erc6492"];
|
|
69
|
+
/** How an acceptance was signed. Two are EOA schemes (`eip191`, `eip712`) and verify offline; two are
|
|
70
|
+
* smart-account schemes (`erc1271`, `erc6492`) and REQUIRE a chain read, which is why
|
|
71
|
+
* {@link AcceptanceVerifyOpts.client} is conditionally required. */
|
|
72
|
+
export type EvmAcceptanceScheme = (typeof EVM_ACCEPTANCE_SCHEMES)[number];
|
|
73
|
+
/** Narrow a string to an {@link EvmAcceptanceScheme}. The set is closed — an unrecognised scheme is not a
|
|
74
|
+
* scheme this port can verify, and treating it as one would mean reporting an unchecked signature. */
|
|
75
|
+
export declare function isEvmAcceptanceScheme(v: string): v is EvmAcceptanceScheme;
|
|
76
|
+
/** The two payload formats this port can reconstruct for recovery. */
|
|
77
|
+
export declare const EVM_PAYLOAD_TYPES: readonly ["atrHash", "eip712-acceptance-envelope"];
|
|
78
|
+
/** WHAT was signed, as distinct from HOW. `atrHash` means the bare hash was signed;
|
|
79
|
+
* `eip712-acceptance-envelope` means the structured envelope was, and that form additionally needs a
|
|
80
|
+
* `chainId` to reconstruct its domain. The two are not interchangeable — verifying under the wrong one
|
|
81
|
+
* reconstructs a different payload and fails. */
|
|
82
|
+
export type EvmPayloadType = (typeof EVM_PAYLOAD_TYPES)[number];
|
|
83
|
+
/** Narrow a string to an {@link EvmPayloadType}. Closed set, same reason as the scheme guard. */
|
|
84
|
+
export declare function isEvmPayloadType(v: string): v is EvmPayloadType;
|
|
85
|
+
/** The fields the EVM verifier reads off an acceptance record (a structural subset of `SignedAcceptance`). */
|
|
86
|
+
export interface AcceptanceSignatureInput {
|
|
87
|
+
atrHash: string;
|
|
88
|
+
signer: string;
|
|
89
|
+
scheme: EvmAcceptanceScheme;
|
|
90
|
+
signature: string;
|
|
91
|
+
/** RFC 3339 — converted to the envelope's uint256 signedAt. */
|
|
92
|
+
signedAt: string;
|
|
93
|
+
payloadType: EvmPayloadType;
|
|
94
|
+
}
|
|
95
|
+
/** The context an acceptance check may need. Both fields are optional in the TYPE and conditionally
|
|
96
|
+
* REQUIRED in fact: `chainId` for the `eip712-acceptance-envelope` payload, `client` for the
|
|
97
|
+
* smart-account schemes. Omitting a required one is refused rather than defaulted — a signature verified
|
|
98
|
+
* against a guessed domain, or not verified at all, must never read as verified. */
|
|
99
|
+
export interface AcceptanceVerifyOpts {
|
|
100
|
+
/** The EIP-155 chain id binding the envelope domain — REQUIRED for the eip712-acceptance-envelope payload. */
|
|
101
|
+
chainId?: number;
|
|
102
|
+
/** A viem public client — REQUIRED for smart-account (erc1271/erc6492) schemes; unused for EOA schemes. */
|
|
103
|
+
client?: PublicClient;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Verify an acceptance signature. EOA schemes recover offline; smart-account schemes verify on-chain through
|
|
107
|
+
* `opts.client`. Returns a boolean (the port contract).
|
|
108
|
+
*
|
|
109
|
+
* THREE OUTCOMES, and keeping them distinct is the whole point:
|
|
110
|
+
*
|
|
111
|
+
* - **A malformed CONFIGURATION throws** (absent `chainId` for the envelope, absent client for a smart
|
|
112
|
+
* account) — an integration error the caller must fix, not a fact about the record.
|
|
113
|
+
* - **A malformed RECORD throws** (an unparseable `signedAt`, a non-32-byte `atrHash`) — a garbled
|
|
114
|
+
* timestamp is a different fact from a forged signature, and `rfc3339ToUnixSeconds` exists precisely to
|
|
115
|
+
* stop a malformed record hiding behind a bad-signature verdict. These are normalized BEFORE the guard
|
|
116
|
+
* below, so they stay loud.
|
|
117
|
+
* - **A malformed SIGNATURE returns `false`.** Recovery over forged or corrupted bytes fails deep in the
|
|
118
|
+
* curve math ("Point is not on curve"), and a verifier that crashes on a forgery cannot report the
|
|
119
|
+
* forgery — the adversarial case is exactly the one verification exists for. An unrecoverable signature
|
|
120
|
+
* is definitively not a valid signature, so the walk records an honest `failed(verification-failure)`.
|
|
121
|
+
*
|
|
122
|
+
* The guard therefore wraps ONLY the recovery/verification calls. Widening it to cover the normalization
|
|
123
|
+
* above would collapse the second outcome into the third.
|
|
124
|
+
*/
|
|
125
|
+
export declare function verifyAcceptanceSignature(input: AcceptanceSignatureInput, opts?: AcceptanceVerifyOpts): Promise<boolean>;
|
|
126
|
+
/**
|
|
127
|
+
* Adapt the EVM verifier to `authority`'s `SignatureVerifier` port (hexagonal: the adapter depends on the
|
|
128
|
+
* domain's port). `authority`'s pure `verifyAcceptance` composes this to check the cryptographic gate.
|
|
129
|
+
*
|
|
130
|
+
* This is where the OPEN acceptance surface narrows to the closed set this port implements. A scheme or
|
|
131
|
+
* payload type outside that set is a ROUTING error — the caller wired the wrong port for the record — and
|
|
132
|
+
* throws loud (the configuration outcome of the three-outcomes doctrine above). Returning `false` instead
|
|
133
|
+
* would report "forged signature" about a signature this port never examined, collapsing "we cannot check
|
|
134
|
+
* this family" into "checked, and it failed" — the exact distinction fail-closed verification exists to keep.
|
|
135
|
+
*/
|
|
136
|
+
export declare function makeEvmAcceptanceVerifier(opts?: AcceptanceVerifyOpts): SignatureVerifier;
|
|
137
|
+
/**
|
|
138
|
+
* A local EOA `LcpEvmSigner` over a raw private key (viem's `privateKeyToAccount`). The producer-side
|
|
139
|
+
* counterpart to `makeEvmAcceptanceVerifier`: it keeps viem isolated in this package so a producer
|
|
140
|
+
* can sign the acceptance envelope through the injected `LcpEvmSigner` port without importing viem. The
|
|
141
|
+
* returned signer's `signTypedData` is viem's `LocalAccount.signTypedData`, which produces the exact
|
|
142
|
+
* EIP-712 signature `verifyAcceptanceSignature` recovers offline (eip191/eip712) — producer and verifier
|
|
143
|
+
* agree by construction. This is a reference/test signer: production buyer wallets inject their own
|
|
144
|
+
* `LcpEvmSigner` (a viem account, a CDP server account, or any wallet exposing address + signTypedData).
|
|
145
|
+
*/
|
|
146
|
+
export declare function makeLocalEvmSigner(privateKey: Hex): LcpEvmSigner;
|
|
147
|
+
//# sourceMappingURL=erc1271.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erc1271.d.ts","sourceRoot":"","sources":["../src/erc1271.ts"],"names":[],"mappings":"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"}
|
package/dist/erc1271.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { isAddress, recoverMessageAddress, recoverTypedDataAddress, } from "viem";
|
|
2
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
3
|
+
import { assertBytes32 } from "./eip3009.js";
|
|
4
|
+
/** The EIP-712 acceptance-envelope struct. */
|
|
5
|
+
export const ACCEPTANCE_ENVELOPE_TYPE = {
|
|
6
|
+
Acceptance: [
|
|
7
|
+
{ name: "atrHash", type: "bytes32" },
|
|
8
|
+
{ name: "signedAt", type: "uint256" },
|
|
9
|
+
],
|
|
10
|
+
};
|
|
11
|
+
/** The acceptance envelope's EIP-712 domain — name/version pinned, chainId per network, no verifyingContract. */
|
|
12
|
+
export const ACCEPTANCE_DOMAIN_NAME = "LCP Acceptance";
|
|
13
|
+
/** The acceptance envelope's EIP-712 domain `version`. Pinned, and part of what every acceptance
|
|
14
|
+
* signature commits to — changing it invalidates every signature made under the old value, so it moves
|
|
15
|
+
* only alongside a deliberate envelope revision. */
|
|
16
|
+
export const ACCEPTANCE_DOMAIN_VERSION = "1";
|
|
17
|
+
/**
|
|
18
|
+
* Convert TRM-6's RFC 3339 `signedAt` (the acceptance record's timestamp) into the uint256 unix seconds the
|
|
19
|
+
* EIP-712 envelope commits to. Fail-loud on anything Date cannot parse — a mis-parsed timestamp would build
|
|
20
|
+
* a different envelope and silently fail verification, hiding a malformed record behind a bad-signature error.
|
|
21
|
+
*/
|
|
22
|
+
export function rfc3339ToUnixSeconds(signedAt) {
|
|
23
|
+
const ms = Date.parse(signedAt);
|
|
24
|
+
if (Number.isNaN(ms))
|
|
25
|
+
throw new Error(`acceptance signedAt is not a parseable RFC 3339 timestamp: "${signedAt}"`);
|
|
26
|
+
return BigInt(Math.floor(ms / 1000));
|
|
27
|
+
}
|
|
28
|
+
/** Build the acceptance envelope typed-data for a fixed atrHash + unix-seconds signedAt + chainId. */
|
|
29
|
+
export function buildAcceptanceTypedData(i) {
|
|
30
|
+
return {
|
|
31
|
+
domain: {
|
|
32
|
+
name: ACCEPTANCE_DOMAIN_NAME,
|
|
33
|
+
version: ACCEPTANCE_DOMAIN_VERSION,
|
|
34
|
+
chainId: i.chainId,
|
|
35
|
+
},
|
|
36
|
+
types: ACCEPTANCE_ENVELOPE_TYPE,
|
|
37
|
+
primaryType: "Acceptance",
|
|
38
|
+
message: { atrHash: assertBytes32(i.atrHash), signedAt: i.signedAt },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** The four schemes this port implements — the EVM half of the open `AcceptanceScheme` set. */
|
|
42
|
+
export const EVM_ACCEPTANCE_SCHEMES = [
|
|
43
|
+
"evm:eip191",
|
|
44
|
+
"evm:eip712",
|
|
45
|
+
"evm:erc1271",
|
|
46
|
+
"evm:erc6492",
|
|
47
|
+
];
|
|
48
|
+
/** Narrow a string to an {@link EvmAcceptanceScheme}. The set is closed — an unrecognised scheme is not a
|
|
49
|
+
* scheme this port can verify, and treating it as one would mean reporting an unchecked signature. */
|
|
50
|
+
export function isEvmAcceptanceScheme(v) {
|
|
51
|
+
return EVM_ACCEPTANCE_SCHEMES.includes(v);
|
|
52
|
+
}
|
|
53
|
+
/** The two payload formats this port can reconstruct for recovery. */
|
|
54
|
+
export const EVM_PAYLOAD_TYPES = [
|
|
55
|
+
"atrHash",
|
|
56
|
+
"eip712-acceptance-envelope",
|
|
57
|
+
];
|
|
58
|
+
/** Narrow a string to an {@link EvmPayloadType}. Closed set, same reason as the scheme guard. */
|
|
59
|
+
export function isEvmPayloadType(v) {
|
|
60
|
+
return EVM_PAYLOAD_TYPES.includes(v);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Verify an acceptance signature. EOA schemes recover offline; smart-account schemes verify on-chain through
|
|
64
|
+
* `opts.client`. Returns a boolean (the port contract).
|
|
65
|
+
*
|
|
66
|
+
* THREE OUTCOMES, and keeping them distinct is the whole point:
|
|
67
|
+
*
|
|
68
|
+
* - **A malformed CONFIGURATION throws** (absent `chainId` for the envelope, absent client for a smart
|
|
69
|
+
* account) — an integration error the caller must fix, not a fact about the record.
|
|
70
|
+
* - **A malformed RECORD throws** (an unparseable `signedAt`, a non-32-byte `atrHash`) — a garbled
|
|
71
|
+
* timestamp is a different fact from a forged signature, and `rfc3339ToUnixSeconds` exists precisely to
|
|
72
|
+
* stop a malformed record hiding behind a bad-signature verdict. These are normalized BEFORE the guard
|
|
73
|
+
* below, so they stay loud.
|
|
74
|
+
* - **A malformed SIGNATURE returns `false`.** Recovery over forged or corrupted bytes fails deep in the
|
|
75
|
+
* curve math ("Point is not on curve"), and a verifier that crashes on a forgery cannot report the
|
|
76
|
+
* forgery — the adversarial case is exactly the one verification exists for. An unrecoverable signature
|
|
77
|
+
* is definitively not a valid signature, so the walk records an honest `failed(verification-failure)`.
|
|
78
|
+
*
|
|
79
|
+
* The guard therefore wraps ONLY the recovery/verification calls. Widening it to cover the normalization
|
|
80
|
+
* above would collapse the second outcome into the third.
|
|
81
|
+
*/
|
|
82
|
+
export async function verifyAcceptanceSignature(input, opts) {
|
|
83
|
+
// Normalization + configuration: OUTSIDE the guard, so both stay loud.
|
|
84
|
+
const recover = prepareRecovery(input, opts);
|
|
85
|
+
try {
|
|
86
|
+
return await recover();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false; // the signature itself did not recover — not a valid signature
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Raised for integration errors (absent chainId/client). */
|
|
93
|
+
class ConfigurationError extends Error {
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Normalize the record's fields and bind the scheme-appropriate recovery call, returning it UNEXECUTED so
|
|
97
|
+
* the caller can guard the recovery alone. Anything wrong with the configuration or the record's own fields
|
|
98
|
+
* throws from here, before the guard exists.
|
|
99
|
+
*/
|
|
100
|
+
function prepareRecovery(input, opts) {
|
|
101
|
+
// A signer that is not an address is a malformed RECORD, not a forgery — thrown here, before the guard,
|
|
102
|
+
// for the same reason rfc3339ToUnixSeconds throws: recovery over it would fail deep in viem and read as
|
|
103
|
+
// a bad-signature verdict about a signature nobody could have examined.
|
|
104
|
+
if (!isAddress(input.signer, { strict: false }))
|
|
105
|
+
throw new Error(`evm:* acceptance signer must be a 0x-address (the scheme's canonical form); got "${input.signer}"`);
|
|
106
|
+
const signer = input.signer;
|
|
107
|
+
const isContract = input.scheme === "evm:erc1271" || input.scheme === "evm:erc6492";
|
|
108
|
+
const signature = input.signature;
|
|
109
|
+
if (input.payloadType === "eip712-acceptance-envelope") {
|
|
110
|
+
if (opts?.chainId === undefined)
|
|
111
|
+
throw new ConfigurationError("eip712-acceptance-envelope verification requires opts.chainId to bind the envelope domain");
|
|
112
|
+
// Throws loud on an unparseable signedAt or a malformed atrHash — a malformed RECORD, not a forgery.
|
|
113
|
+
const typedData = buildAcceptanceTypedData({
|
|
114
|
+
atrHash: input.atrHash,
|
|
115
|
+
signedAt: rfc3339ToUnixSeconds(input.signedAt),
|
|
116
|
+
chainId: opts.chainId,
|
|
117
|
+
});
|
|
118
|
+
if (isContract) {
|
|
119
|
+
// `opts` (not `opts?.`): the chainId gate above already proved it defined — an optional chain here
|
|
120
|
+
// would be dead syntax guarding an impossible state, and the mutation run flags it as equivalent.
|
|
121
|
+
const client = requireClient(opts.client, input.scheme);
|
|
122
|
+
return () => client.verifyTypedData({ address: signer, ...typedData, signature });
|
|
123
|
+
}
|
|
124
|
+
return async () => {
|
|
125
|
+
const recovered = await recoverTypedDataAddress({
|
|
126
|
+
...typedData,
|
|
127
|
+
signature,
|
|
128
|
+
});
|
|
129
|
+
return recovered.toLowerCase() === signer.toLowerCase();
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// payloadType === "atrHash": the raw 32-byte atrHash signed as a personal_sign message.
|
|
133
|
+
const raw = assertBytes32(input.atrHash); // throws loud on a malformed atrHash
|
|
134
|
+
if (isContract) {
|
|
135
|
+
const client = requireClient(opts?.client, input.scheme);
|
|
136
|
+
return () => client.verifyMessage({ address: signer, message: { raw }, signature });
|
|
137
|
+
}
|
|
138
|
+
return async () => {
|
|
139
|
+
const recovered = await recoverMessageAddress({
|
|
140
|
+
message: { raw },
|
|
141
|
+
signature,
|
|
142
|
+
});
|
|
143
|
+
return recovered.toLowerCase() === signer.toLowerCase();
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function requireClient(client, scheme) {
|
|
147
|
+
if (client === undefined)
|
|
148
|
+
throw new ConfigurationError(`${scheme} verification requires a chain client (opts.client) — smart-account signatures verify on-chain (ERC-1271/6492)`);
|
|
149
|
+
return client;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Adapt the EVM verifier to `authority`'s `SignatureVerifier` port (hexagonal: the adapter depends on the
|
|
153
|
+
* domain's port). `authority`'s pure `verifyAcceptance` composes this to check the cryptographic gate.
|
|
154
|
+
*
|
|
155
|
+
* This is where the OPEN acceptance surface narrows to the closed set this port implements. A scheme or
|
|
156
|
+
* payload type outside that set is a ROUTING error — the caller wired the wrong port for the record — and
|
|
157
|
+
* throws loud (the configuration outcome of the three-outcomes doctrine above). Returning `false` instead
|
|
158
|
+
* would report "forged signature" about a signature this port never examined, collapsing "we cannot check
|
|
159
|
+
* this family" into "checked, and it failed" — the exact distinction fail-closed verification exists to keep.
|
|
160
|
+
*/
|
|
161
|
+
export function makeEvmAcceptanceVerifier(opts) {
|
|
162
|
+
return {
|
|
163
|
+
// async so the routing throws below surface as REJECTIONS — the same channel the
|
|
164
|
+
// configuration/record throws inside `verifyAcceptanceSignature` already use.
|
|
165
|
+
verify: async (acceptance) => {
|
|
166
|
+
if (!isEvmAcceptanceScheme(acceptance.scheme))
|
|
167
|
+
throw new ConfigurationError(`scheme "${acceptance.scheme}" is not an EVM acceptance scheme (${EVM_ACCEPTANCE_SCHEMES.join(", ")}) — route the record to the port that implements its family`);
|
|
168
|
+
if (!isEvmPayloadType(acceptance.payloadType))
|
|
169
|
+
throw new ConfigurationError(`payloadType "${acceptance.payloadType}" is not one this port reconstructs (${EVM_PAYLOAD_TYPES.join(", ")})`);
|
|
170
|
+
return verifyAcceptanceSignature({
|
|
171
|
+
atrHash: acceptance.atrHash,
|
|
172
|
+
signer: acceptance.signer,
|
|
173
|
+
scheme: acceptance.scheme,
|
|
174
|
+
signature: acceptance.signature,
|
|
175
|
+
signedAt: acceptance.signedAt,
|
|
176
|
+
payloadType: acceptance.payloadType,
|
|
177
|
+
}, opts);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* A local EOA `LcpEvmSigner` over a raw private key (viem's `privateKeyToAccount`). The producer-side
|
|
183
|
+
* counterpart to `makeEvmAcceptanceVerifier`: it keeps viem isolated in this package so a producer
|
|
184
|
+
* can sign the acceptance envelope through the injected `LcpEvmSigner` port without importing viem. The
|
|
185
|
+
* returned signer's `signTypedData` is viem's `LocalAccount.signTypedData`, which produces the exact
|
|
186
|
+
* EIP-712 signature `verifyAcceptanceSignature` recovers offline (eip191/eip712) — producer and verifier
|
|
187
|
+
* agree by construction. This is a reference/test signer: production buyer wallets inject their own
|
|
188
|
+
* `LcpEvmSigner` (a viem account, a CDP server account, or any wallet exposing address + signTypedData).
|
|
189
|
+
*/
|
|
190
|
+
export function makeLocalEvmSigner(privateKey) {
|
|
191
|
+
const account = privateKeyToAccount(privateKey);
|
|
192
|
+
return {
|
|
193
|
+
address: account.address,
|
|
194
|
+
signTypedData: (message) => account.signTypedData(message),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=erc1271.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erc1271.js","sourceRoot":"","sources":["../src/erc1271.ts"],"names":[],"mappings":"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"}
|
package/dist/erc20.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one piece of ERC-20 knowledge the EVM welds share: did this transaction move a given token at all?
|
|
3
|
+
*
|
|
4
|
+
* It lives here because two rails need the same answer for the same reason, and the reason is not
|
|
5
|
+
* MPP-specific or x402-specific. Every EVM weld reads its binding out of an event the settlement emits. When
|
|
6
|
+
* that event is absent, the absence has TWO meanings and they are not the same report:
|
|
7
|
+
*
|
|
8
|
+
* - the token did not move ⇒ this transaction settled none of that asset. A true, useful answer.
|
|
9
|
+
* - the token DID move ⇒ the transaction settled, through a path this binding cannot read the weld from.
|
|
10
|
+
* Reporting that as an absence is a silent wrong answer at a verification boundary.
|
|
11
|
+
*
|
|
12
|
+
* The rails differ in WHICH unreadable path they are looking at — MPP's three non-`authorization` credential
|
|
13
|
+
* types, x402's Permit2 fallback — so each states its own refusal in its own vocabulary. What they share is
|
|
14
|
+
* this predicate, and duplicating it once per rail is how the two drift.
|
|
15
|
+
*
|
|
16
|
+
* Topic-0 identity only, deliberately: this module classifies, it never decodes amounts or recipients.
|
|
17
|
+
* Matching a transfer against an offer's terms is the settle path's job (`executedTransferCoversOffer`), and
|
|
18
|
+
* doing it here would be a second, weaker copy of a check that already has an owner.
|
|
19
|
+
*/
|
|
20
|
+
import type { Log } from "viem";
|
|
21
|
+
/**
|
|
22
|
+
* `keccak256("Transfer(address,address,uint256)")` — ERC-20's `Transfer` event topic0.
|
|
23
|
+
*
|
|
24
|
+
* Derived by two independent keccak-256 oracles that agree byte-for-byte, neither of them viem:
|
|
25
|
+
* `cast keccak 'Transfer(address,address,uint256)'` and pycryptodome `keccak(digest_bits=256)`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ERC20_TRANSFER_TOPIC0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
28
|
+
/**
|
|
29
|
+
* Did this transaction move the configured token at all?
|
|
30
|
+
*
|
|
31
|
+
* Topic-0 identity only — no `decodeEventLog`, because the question is "did this asset move", not "how much,
|
|
32
|
+
* to whom". An indexed-parameter count is not checked either: any log the token emitted under ERC-20's
|
|
33
|
+
* `Transfer` signature settles the question this predicate is asked.
|
|
34
|
+
*/
|
|
35
|
+
export declare function assetWasTransferred(logs: readonly Log[], asset: string): boolean;
|
|
36
|
+
//# sourceMappingURL=erc20.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erc20.d.ts","sourceRoot":"","sources":["../src/erc20.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAEhC;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,uEACoC,CAAC;AAEvE;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,SAAS,GAAG,EAAE,EACpB,KAAK,EAAE,MAAM,GACZ,OAAO,CAOT"}
|
package/dist/erc20.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `keccak256("Transfer(address,address,uint256)")` — ERC-20's `Transfer` event topic0.
|
|
3
|
+
*
|
|
4
|
+
* Derived by two independent keccak-256 oracles that agree byte-for-byte, neither of them viem:
|
|
5
|
+
* `cast keccak 'Transfer(address,address,uint256)'` and pycryptodome `keccak(digest_bits=256)`.
|
|
6
|
+
*/
|
|
7
|
+
export const ERC20_TRANSFER_TOPIC0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
8
|
+
/**
|
|
9
|
+
* Did this transaction move the configured token at all?
|
|
10
|
+
*
|
|
11
|
+
* Topic-0 identity only — no `decodeEventLog`, because the question is "did this asset move", not "how much,
|
|
12
|
+
* to whom". An indexed-parameter count is not checked either: any log the token emitted under ERC-20's
|
|
13
|
+
* `Transfer` signature settles the question this predicate is asked.
|
|
14
|
+
*/
|
|
15
|
+
export function assetWasTransferred(logs, asset) {
|
|
16
|
+
const want = asset.toLowerCase();
|
|
17
|
+
return logs.some((log) => log.address.toLowerCase() === want &&
|
|
18
|
+
log.topics[0]?.toLowerCase() === ERC20_TRANSFER_TOPIC0);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=erc20.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erc20.js","sourceRoot":"","sources":["../src/erc20.ts"],"names":[],"mappings":"AAqBA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAChC,oEAAoE,CAAC;AAEvE;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAoB,EACpB,KAAa;IAEb,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IACjC,OAAO,IAAI,CAAC,IAAI,CACd,CAAC,GAAG,EAAE,EAAE,CACN,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,IAAI;QAClC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,qBAAqB,CACzD,CAAC;AACJ,CAAC"}
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
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 type { SettlementRef } from "@integraledger/lcp-binding-core";
|
|
6
|
+
import { type Hex, type Log } 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 declare function refOf(chainId: number, txHash: `0x${string}` | undefined, logIndex: number | null): SettlementRef;
|
|
11
|
+
/** USDC `AuthorizationUsed(authorizer, nonce)` — both indexed; the on-chain artifact of every EIP-3009
|
|
12
|
+
* settlement. `nonce` carries the atrHash (WLD-3: recoverable, forward-indexable on the nonce topic). */
|
|
13
|
+
export declare const AUTHORIZATION_USED_ABI: readonly [{
|
|
14
|
+
readonly type: "event";
|
|
15
|
+
readonly name: "AuthorizationUsed";
|
|
16
|
+
readonly inputs: readonly [{
|
|
17
|
+
readonly name: "authorizer";
|
|
18
|
+
readonly type: "address";
|
|
19
|
+
readonly indexed: true;
|
|
20
|
+
}, {
|
|
21
|
+
readonly name: "nonce";
|
|
22
|
+
readonly type: "bytes32";
|
|
23
|
+
readonly indexed: true;
|
|
24
|
+
}];
|
|
25
|
+
readonly anonymous: false;
|
|
26
|
+
}];
|
|
27
|
+
/** The answer to one question: did this settlement transaction commit this atrHash as an EIP-3009 nonce?
|
|
28
|
+
* `ok` is the whole verdict and `onChainNonce` is the matched value for a report to quote. It proves the
|
|
29
|
+
* NONCE matched — it says nothing about which token moved, or how much, which is a separate check. */
|
|
30
|
+
export interface OnChainAtrHashProof {
|
|
31
|
+
/** True iff the settlement tx emitted `AuthorizationUsed` with `nonce === atrHash`. */
|
|
32
|
+
ok: boolean;
|
|
33
|
+
/** The on-chain nonce that matched (lowercase 0x hex), or `null` if none did. */
|
|
34
|
+
onChainNonce: Hex | null;
|
|
35
|
+
}
|
|
36
|
+
/** A decoded `AuthorizationUsed` occurrence: the nonce it committed and where it sat in the tx. */
|
|
37
|
+
export interface AuthorizationUsedEvent {
|
|
38
|
+
/** The committed nonce (lowercase 0x hex) — the atrHash on the x402 binding. */
|
|
39
|
+
nonce: Hex;
|
|
40
|
+
/** The authorizing account (lowercase 0x hex) — EIP-3009's `authorizer`, the account the executed
|
|
41
|
+
* transfer draws from. Faithful decoding of the event's first indexed argument, NOT an identity
|
|
42
|
+
* attestation: who the counterparty IS remains the acceptance/authority layer's claim. Carried so a
|
|
43
|
+
* consumer can pair this authorization with the `Transfer` it executed in the same settlement. */
|
|
44
|
+
authorizer: Hex;
|
|
45
|
+
/** The log's position in the transaction, or `null` if the source log carried none. */
|
|
46
|
+
logIndex: number | null;
|
|
47
|
+
/** The emitting contract (lowercase 0x hex) — the payment token. */
|
|
48
|
+
address: Hex;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Decode every `AuthorizationUsed(authorizer, indexed nonce)` in a set of logs (a single settlement's
|
|
52
|
+
* logs, from `ChainReader.getTransactionLogs`, or a range query). This is the EXTRACT path (recover the
|
|
53
|
+
* committed nonce), distinct from `verifyAtrHashOnChain`'s CHECK path (assert a known nonce is present).
|
|
54
|
+
* When `asset` is supplied only that token's events are read; otherwise every AuthorizationUsed is decoded.
|
|
55
|
+
*/
|
|
56
|
+
export declare function readAuthorizationUsed(logs: readonly Log[], asset?: string): AuthorizationUsedEvent[];
|
|
57
|
+
/**
|
|
58
|
+
* Confirm the atrHash was committed on-chain: scan a settlement tx receipt for the token's
|
|
59
|
+
* `AuthorizationUsed(authorizer, indexed nonce)` event and assert `nonce === atrHash` (case-insensitive).
|
|
60
|
+
*/
|
|
61
|
+
export declare function verifyAtrHashOnChain(receipt: {
|
|
62
|
+
logs: readonly Log[];
|
|
63
|
+
}, params: {
|
|
64
|
+
asset: string;
|
|
65
|
+
atrHash: string;
|
|
66
|
+
}): OnChainAtrHashProof;
|
|
67
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,OAAO,EAAkB,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,MAAM,CAAC;AAE1D;;2FAE2F;AAC3F,wBAAgB,KAAK,CACnB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,KAAK,MAAM,EAAE,GAAG,SAAS,EACjC,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,aAAa,CAMf;AAED;0GAC0G;AAC1G,eAAO,MAAM,sBAAsB;aAE/B,IAAI,EAAE,OAAO;aACb,IAAI,EAAE,mBAAmB;aACzB,MAAM;iBACF,IAAI,EAAE,YAAY;iBAAE,IAAI,EAAE,SAAS;iBAAE,OAAO;;iBAC5C,IAAI,EAAE,OAAO;iBAAE,IAAI,EAAE,SAAS;iBAAE,OAAO;;aAE3C,SAAS;EAEH,CAAC;AAEX;;uGAEuG;AACvG,MAAM,WAAW,mBAAmB;IAClC,uFAAuF;IACvF,EAAE,EAAE,OAAO,CAAC;IACZ,iFAAiF;IACjF,YAAY,EAAE,GAAG,GAAG,IAAI,CAAC;CAC1B;AAED,mGAAmG;AACnG,MAAM,WAAW,sBAAsB;IACrC,gFAAgF;IAChF,KAAK,EAAE,GAAG,CAAC;IACX;;;uGAGmG;IACnG,UAAU,EAAE,GAAG,CAAC;IAChB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,oEAAoE;IACpE,OAAO,EAAE,GAAG,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,SAAS,GAAG,EAAE,EACpB,KAAK,CAAC,EAAE,MAAM,GACb,sBAAsB,EAAE,CAwB1B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE;IAAE,IAAI,EAAE,SAAS,GAAG,EAAE,CAAA;CAAE,EACjC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACzC,mBAAmB,CAoBrB"}
|