@integraledger/lcp-binding-evm-common
v0.20.1
Published
Shared EVM machinery for LCP rail bindings: typed-data construction and signature verification.
Readme
@integraledger/lcp-binding-evm-common
Shared EVM machinery for the rail bindings: typed-data construction, signature verification, event
decoding, and attestation semantics. This is not itself a rail binding — it is what @integraledger/lcp-binding-evm-x402,
@integraledger/lcp-binding-evm-escrow and @integraledger/lcp-binding-evm-mpp are built from.
npm install @integraledger/lcp-binding-evm-commonAcceptance signatures
Implements the SignatureVerifier port that @integraledger/lcp-authority declares, across four schemes:
| Scheme | How it verifies |
|---|---|
| eip191 | Recovered offline — no chain access |
| eip712 | Recovered offline — no chain access |
| erc1271 | On-chain, through an injected viem client |
| erc6492 | On-chain — counterfactual accounts not yet deployed |
The EOA schemes need no network, which is what lets the deterministic conformance vectors cover them.
import {
type AcceptanceSignatureInput,
type AcceptanceVerifyOpts,
verifyAcceptanceSignature,
} from "@integraledger/lcp-binding-evm-common";
declare const acceptance: AcceptanceSignatureInput; // scheme: "eip191" | "eip712" | …
declare const smartAccountOpts: AcceptanceVerifyOpts; // { chainId, client } — a viem PublicClient
// EOA schemes need no network at all — no client, no chain id.
const ok = await verifyAcceptanceSignature(acceptance);
// The smart-account schemes do, and an absent client THROWS rather than answering `false`:
// "not verified" and "verified as forged" are different facts.
const onChain = await verifyAcceptanceSignature(acceptance, smartAccountOpts);Four outcomes, kept distinct
A malformed configuration throws — an absent chain id or a missing client is an integration error the
caller must fix. A malformed record throws — an unparseable timestamp is a different fact from a
forged signature, and letting it hide behind a bad-signature verdict would lose that. A malformed
signature returns false — recovery over forged bytes fails deep in curve math, and a verifier that
crashes on a forgery cannot report the forgery. A chain that could not answer throws — an HTTP 429 or
a timeout says nothing whatever about the signature, and reporting it as false publishes a valid buyer
acceptance as a forgery, a verdict the next run reverses.
The last two are told apart by the scheme, not by the error class: eip191 and eip712 verify by pure
offline recovery, so a throw there is the signature; erc1271 and erc6492 verify on-chain, and viem
already returns false for a signature its validator rejects and throws only when the node could not be
reached.
Collapsing any two of those into one another is how a verifier ends up reporting the wrong thing about the one case it exists for.
Attestations — the semantics, never the read
EAS (Ethereum Attestation Service) attestations turn up as the substrate behind a profiled attestation in
an authority chain. This package ships what such an attestation means and nothing that goes and fetches
one: EAS_GET_ATTESTATION_ABI (the call to make), RawEasAttestation (the shape viem decodes the
node's tuple into),
decodeEasAttestation (normalize it, and flag a zero uid as exists: false rather than as a valid
attestation with empty fields) and isEasValidAsOf.
import {
decodeEasAttestation,
isEasValidAsOf,
} from "@integraledger/lcp-binding-evm-common";
const attestation = decodeEasAttestation({
uid: "0x1111111111111111111111111111111111111111111111111111111111111111",
schema:
"0x2222222222222222222222222222222222222222222222222222222222222222",
time: 1700000000n,
expirationTime: 0n,
revocationTime: 0n,
refUID:
"0x0000000000000000000000000000000000000000000000000000000000000000",
recipient: "0xAbC0000000000000000000000000000000000001",
attester: "0xDeF0000000000000000000000000000000000002",
revocable: true,
data: "0xcafe",
});
attestation.attester; // "0xdef0000000000000000000000000000000000002" — lowercased
attestation.exists; // true
// Valid AS OF the settlement, never as of now. The bound is two-sided: an attestation minted AFTER the
// as-of second is not valid as of it, which is the direction backdating goes. The first call below is
// `true`; the second is `false`, because the attestation was minted one second after the as-of instant.
isEasValidAsOf(attestation, 1700000000n);
isEasValidAsOf(attestation, 1699999999n);⛔ There is deliberately no readEasAttestation here, and there was until 2026-09-17. A chain read is a
verifier's act: Integra records, and the parties verify.
⚠️ And reading one is two hops, not one. The envelope's ref is an lcp:sha256: content address for
the attestation artifact, never the EAS uid: you resolve the artifact where content is stored by its
digest — the evidence bundle's manifest names it — and take the uid from inside it, then call
getAttestation with that. Handing ref to the chain reads a real attestation back as exists: false,
which is indistinguishable from one nobody minted.
verify-a-settlement.md
Step 5 is the worked example, both hops. Nothing about the check moved out of reach; what moved is who
performs it.
Requirement ids
This package's source and its messages cite short ids — ATA-3, RCS-5, CMP-6 and their kin.
They are not LCP clause numbers. LCP is cited by section (§8.3.1, §C.2); anything shaped XXX-n
comes from Integra's functional specification of what a complete agent transaction requires, the fourteen
families below. Nothing in this package's behaviour depends on them, and where an id and an LCP section
disagree the section governs.
| | | | |
|---|---|---|---|
| IDN identity | ASP authority to spend | ATA authority to accept terms | TRM the terms record |
| RCS recourse | PAY payment and settlement | WLD the transactional weld | OFR offer integrity |
| FRC fraud, risk, and compliance | OPS commercial operations | DSC discovery and reputation | ORC orchestration |
| CMP composition | PRS persistence and verification infrastructure | | |
Requires Node >= 24. Part of the Legal Context Protocol open layer — see the documentation and the package index. Apache-2.0.
