@provex/eas 1.9.2-rc.20260721051954.c2db778 → 1.9.2
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/eas-age.test.d.ts +1 -0
- package/dist/eas-age.test.js +212 -0
- package/dist/eas-builder.d.ts +352 -0
- package/dist/eas-builder.js +386 -0
- package/dist/eas-builder.test.d.ts +1 -0
- package/dist/eas-builder.test.js +398 -0
- package/dist/eas.d.ts +230 -0
- package/dist/eas.js +365 -0
- package/dist/eas.test.d.ts +1 -0
- package/dist/eas.test.js +157 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/offchain-verify.d.ts +100 -0
- package/dist/offchain-verify.js +213 -0
- package/dist/offchain-verify.test.d.ts +1 -0
- package/dist/offchain-verify.test.js +112 -0
- package/dist/viem-only.test.d.ts +1 -0
- package/dist/viem-only.test.js +79 -0
- package/package.json +2 -11
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { encodePacked, keccak256, zeroAddress } from 'viem';
|
|
4
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
5
|
+
import { buildOffchainAgeAttestation, buildOffchainAgeAttestationForChain, encodeAgeClaim, } from './eas-builder.js';
|
|
6
|
+
import { decodeAgeClaim } from './offchain-verify.js';
|
|
7
|
+
import { EAS_ADDRESSES_BY_CHAIN_ID, EAS_MAINNET_ADDRESS, PROVEX_AGE_SCHEMA, PROVEX_AGE_SCHEMA_UID_BY_CHAIN_ID, getAgeSchemaUid, getAgeSchemaUidOrPlaceholder, } from './eas.js';
|
|
8
|
+
const ZERO_BYTES32 = ('0x' + '0'.repeat(64));
|
|
9
|
+
/** Deterministic unique identifier of the age schema — kept in lockstep with eas.ts. */
|
|
10
|
+
const AGE_UID = '0x4ffe2fd75afc43a65b604589606c0996db197fcae4f834d240e3c023c40f297d';
|
|
11
|
+
/** Deterministic unique identifiers of the OTHER two schemas, used as miswiring canaries. */
|
|
12
|
+
const IDENTITY_UID_369 = '0xdda0f5748fee977851ba75a82b2d378dcdc20b2a816278ff55e411852c4a4092';
|
|
13
|
+
const RELEASE_UID_369 = '0xc9bf5f19b85d6a84403428964837965e16f72d40863f5d9351d3951249f0d043';
|
|
14
|
+
const CLAIM = {
|
|
15
|
+
nullifier: ('0x' + 'ab'.repeat(32)),
|
|
16
|
+
overN: true,
|
|
17
|
+
threshold: 21,
|
|
18
|
+
identityVerified: true,
|
|
19
|
+
expiresAt: 2000000000n,
|
|
20
|
+
};
|
|
21
|
+
describe('PROVEX_AGE_SCHEMA', () => {
|
|
22
|
+
it('encodes no personally identifying field', () => {
|
|
23
|
+
// The privacy invariant, as an executable check: a future edit that adds a
|
|
24
|
+
// name / date of birth / Social Security field to the schema fails HERE,
|
|
25
|
+
// loudly, rather than silently shipping personal data inside every
|
|
26
|
+
// attestation this schema produces.
|
|
27
|
+
for (const banned of ['name', 'handle', 'birth', 'dob', 'ssn', 'address ']) {
|
|
28
|
+
assert.ok(!PROVEX_AGE_SCHEMA.toLowerCase().includes(banned), `PROVEX_AGE_SCHEMA must not carry a "${banned}" field`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
it('is comma-separated typed fields in the documented order', () => {
|
|
32
|
+
// Field order is load-bearing: it fixes the encoding of every attestation
|
|
33
|
+
// issued under this schema, and the decoder reads positionally.
|
|
34
|
+
const fields = PROVEX_AGE_SCHEMA.split(',').map((field) => field.trim());
|
|
35
|
+
assert.deepEqual(fields, [
|
|
36
|
+
'bytes32 nullifier',
|
|
37
|
+
'bool overN',
|
|
38
|
+
'uint8 threshold',
|
|
39
|
+
'bool identityVerified',
|
|
40
|
+
'uint64 expiresAt',
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('age claim round-trip', () => {
|
|
45
|
+
it('decodes back to the same claim it encoded', () => {
|
|
46
|
+
assert.deepEqual(decodeAgeClaim(encodeAgeClaim(CLAIM)), CLAIM);
|
|
47
|
+
});
|
|
48
|
+
it('preserves a false overN rather than dropping the field', () => {
|
|
49
|
+
// WHY: "this person is NOT over the threshold" is a real, signed answer —
|
|
50
|
+
// not a missing field. A decoder that coerced it away (or an encoder that
|
|
51
|
+
// omitted it) would turn a refusal into an absence, which a consuming
|
|
52
|
+
// application could misread as "unknown" and then fail open.
|
|
53
|
+
const minor = { ...CLAIM, overN: false };
|
|
54
|
+
assert.equal(decodeAgeClaim(encodeAgeClaim(minor)).overN, false);
|
|
55
|
+
assert.deepEqual(decodeAgeClaim(encodeAgeClaim(minor)), minor);
|
|
56
|
+
});
|
|
57
|
+
it('round-trips both identityVerified states independently of overN', () => {
|
|
58
|
+
// The two booleans are adjacent in the schema; a field-order slip would
|
|
59
|
+
// swap them and silently mislabel an unverified identity as verified.
|
|
60
|
+
const claim = { ...CLAIM, overN: true, identityVerified: false };
|
|
61
|
+
const decoded = decodeAgeClaim(encodeAgeClaim(claim));
|
|
62
|
+
assert.equal(decoded.overN, true);
|
|
63
|
+
assert.equal(decoded.identityVerified, false);
|
|
64
|
+
});
|
|
65
|
+
it('round-trips the uint8 threshold at both boundaries', () => {
|
|
66
|
+
// 0 and 255 are the extremes the uint8 field can hold. 0 must not be
|
|
67
|
+
// confused with "unset", and 255 must not overflow into the next word.
|
|
68
|
+
for (const threshold of [0, 255]) {
|
|
69
|
+
const claim = { ...CLAIM, threshold };
|
|
70
|
+
assert.equal(decodeAgeClaim(encodeAgeClaim(claim)).threshold, threshold);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
it('round-trips an expiresAt at the uint64 ceiling', () => {
|
|
74
|
+
const claim = { ...CLAIM, expiresAt: 2n ** 64n - 1n };
|
|
75
|
+
assert.equal(decodeAgeClaim(encodeAgeClaim(claim)).expiresAt, 2n ** 64n - 1n);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('encodeAgeClaim', () => {
|
|
79
|
+
it('produces deterministic bytes for the same claim', () => {
|
|
80
|
+
assert.equal(encodeAgeClaim(CLAIM), encodeAgeClaim(CLAIM));
|
|
81
|
+
assert.match(encodeAgeClaim(CLAIM), /^0x[0-9a-fA-F]+$/);
|
|
82
|
+
});
|
|
83
|
+
it('produces different bytes when the nullifier differs', () => {
|
|
84
|
+
// WHY: the nullifier is what enforces one credential per person. If two
|
|
85
|
+
// different nullifiers encoded identically, the uniqueness guarantee
|
|
86
|
+
// would collapse.
|
|
87
|
+
assert.notEqual(encodeAgeClaim(CLAIM), encodeAgeClaim({ ...CLAIM, nullifier: ('0x' + 'cd'.repeat(32)) }));
|
|
88
|
+
});
|
|
89
|
+
it('produces different bytes when the threshold differs', () => {
|
|
90
|
+
// WHY: an "over 18" credential must not be byte-identical to an "over 21"
|
|
91
|
+
// one, or a signature over the former would verify as the latter.
|
|
92
|
+
assert.notEqual(encodeAgeClaim(CLAIM), encodeAgeClaim({ ...CLAIM, threshold: 18 }));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('getAgeSchemaUid', () => {
|
|
96
|
+
it('resolves the pinned unique identifier on the Provex chains', () => {
|
|
97
|
+
assert.equal(getAgeSchemaUid(369), AGE_UID);
|
|
98
|
+
assert.equal(getAgeSchemaUid(943), AGE_UID);
|
|
99
|
+
assert.equal(getAgeSchemaUid(11155111), AGE_UID);
|
|
100
|
+
});
|
|
101
|
+
it('throws while the per-chain unique identifier is still the placeholder', () => {
|
|
102
|
+
// Ethereum mainnet (1) stays a placeholder — there is no Provex deployment there.
|
|
103
|
+
assert.throws(() => getAgeSchemaUid(1), /Provex schema not registered on chainId 1/);
|
|
104
|
+
});
|
|
105
|
+
it('throws for chains with no unique-identifier slot at all', () => {
|
|
106
|
+
assert.throws(() => getAgeSchemaUid(999_999), /no schema UID slot for chainId 999999/);
|
|
107
|
+
});
|
|
108
|
+
it('returns the override when EAS_AGE_SCHEMA_UID_<chainId>_OVERRIDE is set', () => {
|
|
109
|
+
const override = '0x' + '99'.repeat(32);
|
|
110
|
+
process.env.EAS_AGE_SCHEMA_UID_369_OVERRIDE = override;
|
|
111
|
+
try {
|
|
112
|
+
assert.equal(getAgeSchemaUid(369), override);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
delete process.env.EAS_AGE_SCHEMA_UID_369_OVERRIDE;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('getAgeSchemaUidOrPlaceholder', () => {
|
|
120
|
+
it('returns the all-zero placeholder for unregistered or unknown chains', () => {
|
|
121
|
+
assert.equal(getAgeSchemaUidOrPlaceholder(1), ZERO_BYTES32);
|
|
122
|
+
assert.equal(getAgeSchemaUidOrPlaceholder(999_999), ZERO_BYTES32);
|
|
123
|
+
});
|
|
124
|
+
it('returns the pinned unique identifier for the Provex chains', () => {
|
|
125
|
+
assert.equal(getAgeSchemaUidOrPlaceholder(369), AGE_UID);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('PROVEX_AGE_SCHEMA_UID_BY_CHAIN_ID', () => {
|
|
129
|
+
it('pins the deterministic unique identifier the schema string produces', () => {
|
|
130
|
+
// An Ethereum Attestation Service schema unique identifier is
|
|
131
|
+
// keccak256(abi.encodePacked(schema, resolver, revocable)) — computable
|
|
132
|
+
// with no transaction. WHY this assertion: it makes editing the schema
|
|
133
|
+
// string without re-pinning the identifier fail the suite, instead of
|
|
134
|
+
// silently signing under an identifier that no registration would match.
|
|
135
|
+
const derived = keccak256(encodePacked(['string', 'address', 'bool'], [PROVEX_AGE_SCHEMA, zeroAddress, true]));
|
|
136
|
+
assert.equal(derived, AGE_UID);
|
|
137
|
+
assert.equal(PROVEX_AGE_SCHEMA_UID_BY_CHAIN_ID[369], derived);
|
|
138
|
+
});
|
|
139
|
+
it('is distinct from the identity and release schema unique identifiers', () => {
|
|
140
|
+
// Three schemas share one registry unique-identifier space; a collision
|
|
141
|
+
// would let an age attestation be read as an identity or release one.
|
|
142
|
+
assert.notEqual(AGE_UID, IDENTITY_UID_369);
|
|
143
|
+
assert.notEqual(AGE_UID, RELEASE_UID_369);
|
|
144
|
+
});
|
|
145
|
+
it('has the same chain keys as the Ethereum Attestation Service address map', () => {
|
|
146
|
+
assert.deepEqual(Object.keys(PROVEX_AGE_SCHEMA_UID_BY_CHAIN_ID).sort(), Object.keys(EAS_ADDRESSES_BY_CHAIN_ID).sort());
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('buildOffchainAgeAttestation', () => {
|
|
150
|
+
// Hardhat's deterministic account 0 — the same key the identity and release
|
|
151
|
+
// builder tests use, so the address/key pair is recognizable at a glance.
|
|
152
|
+
const ATTESTER_KEY = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d';
|
|
153
|
+
const HOLDER = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8';
|
|
154
|
+
const AGE_SCHEMA_UID = ('0x' + '77'.repeat(32));
|
|
155
|
+
it('binds the credential to the holder wallet as recipient', async () => {
|
|
156
|
+
// WHY: unlike a release attestation (subject is a manifest, recipient is
|
|
157
|
+
// the zero address), an age credential is ABOUT a person. The recipient is
|
|
158
|
+
// the only thing that tells a consuming application whose credential it is.
|
|
159
|
+
const attester = privateKeyToAccount(ATTESTER_KEY);
|
|
160
|
+
const result = await buildOffchainAgeAttestation({
|
|
161
|
+
claim: CLAIM,
|
|
162
|
+
attester,
|
|
163
|
+
recipient: HOLDER,
|
|
164
|
+
eas: EAS_MAINNET_ADDRESS,
|
|
165
|
+
schemaUid: AGE_SCHEMA_UID,
|
|
166
|
+
chainId: 1,
|
|
167
|
+
});
|
|
168
|
+
assert.match(result.signature, /^0x[0-9a-fA-F]{130}$/);
|
|
169
|
+
assert.equal(result.message.recipient.toLowerCase(), HOLDER.toLowerCase());
|
|
170
|
+
assert.notEqual(result.message.recipient.toLowerCase(), zeroAddress);
|
|
171
|
+
assert.equal(result.message.schema, AGE_SCHEMA_UID);
|
|
172
|
+
assert.equal(result.domain.chainId, 1);
|
|
173
|
+
});
|
|
174
|
+
it('embeds the encoded age claim and derives the envelope expiry from it', async () => {
|
|
175
|
+
const attester = privateKeyToAccount(ATTESTER_KEY);
|
|
176
|
+
const result = await buildOffchainAgeAttestation({
|
|
177
|
+
claim: CLAIM,
|
|
178
|
+
attester,
|
|
179
|
+
recipient: HOLDER,
|
|
180
|
+
eas: EAS_MAINNET_ADDRESS,
|
|
181
|
+
schemaUid: AGE_SCHEMA_UID,
|
|
182
|
+
chainId: 1,
|
|
183
|
+
});
|
|
184
|
+
assert.equal(result.message.data, encodeAgeClaim(CLAIM));
|
|
185
|
+
// WHY THIS MATTERS: the envelope expiry is DERIVED from claim.expiresAt,
|
|
186
|
+
// never passed alongside it. If a future edit reintroduces a separate
|
|
187
|
+
// expirationTime parameter, these two can drift — a consumer using standard
|
|
188
|
+
// Ethereum Attestation Service tooling would read the envelope while one
|
|
189
|
+
// decoding the claim reads the data, and they would disagree about whether
|
|
190
|
+
// the same credential is still valid, with nothing to flag the split.
|
|
191
|
+
assert.equal(result.message.expirationTime, CLAIM.expiresAt);
|
|
192
|
+
assert.equal(result.message.revocable, true);
|
|
193
|
+
// The signed bytes decode back to the claim the caller handed in — the
|
|
194
|
+
// end-to-end guarantee a verifier depends on.
|
|
195
|
+
assert.deepEqual(decodeAgeClaim(result.message.data), CLAIM);
|
|
196
|
+
});
|
|
197
|
+
it('signs with the age schema unique identifier via the ForChain wrapper', async () => {
|
|
198
|
+
// Proves the wrapper routes through getAgeSchemaUid rather than the
|
|
199
|
+
// identity or release lookup — a miswiring TypeScript cannot catch.
|
|
200
|
+
const attester = privateKeyToAccount(ATTESTER_KEY);
|
|
201
|
+
const result = await buildOffchainAgeAttestationForChain({
|
|
202
|
+
claim: CLAIM,
|
|
203
|
+
attester,
|
|
204
|
+
recipient: HOLDER,
|
|
205
|
+
chainId: 369,
|
|
206
|
+
});
|
|
207
|
+
assert.equal(result.message.schema, AGE_UID);
|
|
208
|
+
assert.notEqual(result.message.schema, IDENTITY_UID_369);
|
|
209
|
+
assert.notEqual(result.message.schema, RELEASE_UID_369);
|
|
210
|
+
assert.equal(result.message.recipient.toLowerCase(), HOLDER.toLowerCase());
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import * as easSdkNamespace from '@ethereum-attestation-service/eas-sdk';
|
|
2
|
+
import type { Address, Hex, LocalAccount } from 'viem';
|
|
3
|
+
type OffchainAttestationVersionEnum = (typeof easSdkNamespace)['OffchainAttestationVersion'][keyof (typeof easSdkNamespace)['OffchainAttestationVersion']];
|
|
4
|
+
/**
|
|
5
|
+
* @provex/eas — off-chain Ethereum Attestation Service attestation builder.
|
|
6
|
+
*
|
|
7
|
+
* Wraps the upstream `@ethereum-attestation-service/eas-sdk` so callers
|
|
8
|
+
* (notary-gate's webhook) can hand in a viem `LocalAccount` + an
|
|
9
|
+
* `IdentityClaim` and get back a fully-signed off-chain attestation
|
|
10
|
+
* without touching ethers directly.
|
|
11
|
+
*
|
|
12
|
+
* The SDK's signer interface is ethers-shaped (`signTypedData(domain,
|
|
13
|
+
* types, value)`), but we live in viem-land. The thin adapter below
|
|
14
|
+
* translates the SDK's three-arg call into viem's
|
|
15
|
+
* `account.signTypedData({ domain, types, primaryType, message })` and
|
|
16
|
+
* returns the resulting hex signature unchanged. Same EIP-712 wire bytes,
|
|
17
|
+
* different SDK conventions.
|
|
18
|
+
*
|
|
19
|
+
* Output normalization: the SDK returns `signature: { r, s, v }` and
|
|
20
|
+
* `domain.chainId: bigint`. We flatten the signature to a single 65-byte
|
|
21
|
+
* 0x-hex and convert chainId to `number` so downstream JSON serialization
|
|
22
|
+
* (notary-gate's attestationStore → polling UI) is straightforward and
|
|
23
|
+
* consumers can compare chainId without bigint juggling.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Identity claim destined for an off-chain EAS attestation. The `handle`
|
|
27
|
+
* is the proven platform username (e.g. Discord username, X screen_name);
|
|
28
|
+
* the contract-level wire encoding is the EAS schema string in `eas.ts`.
|
|
29
|
+
*/
|
|
30
|
+
export type IdentityClaim = {
|
|
31
|
+
platformHash: Hex;
|
|
32
|
+
handle: string;
|
|
33
|
+
user: Address;
|
|
34
|
+
/** Unix seconds at which this attestation expires; encoded as uint64. */
|
|
35
|
+
expiresAt: bigint;
|
|
36
|
+
};
|
|
37
|
+
export type BuildOffchainEasAttestationArgs = {
|
|
38
|
+
claim: IdentityClaim;
|
|
39
|
+
attester: LocalAccount;
|
|
40
|
+
/** EAS contract address on the target chain (used in the EIP-712 domain). */
|
|
41
|
+
eas: Address;
|
|
42
|
+
/** Pinned EAS schema UID — must match `PROVEX_IDENTITY_SCHEMA`. */
|
|
43
|
+
schemaUid: Hex;
|
|
44
|
+
/** EVM chain id the attestation is bound to (1 = Ethereum mainnet). */
|
|
45
|
+
chainId: number;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Final wire shape returned by the builder. Matches `SignedOffchainAttestation`
|
|
49
|
+
* from the EAS SDK with two normalizations applied:
|
|
50
|
+
* - signature flattened to single hex string
|
|
51
|
+
* - domain.chainId widened to plain number for JSON safety
|
|
52
|
+
*/
|
|
53
|
+
export type OffchainEasAttestation = {
|
|
54
|
+
version: OffchainAttestationVersionEnum;
|
|
55
|
+
uid: string;
|
|
56
|
+
domain: {
|
|
57
|
+
name: string;
|
|
58
|
+
version: string;
|
|
59
|
+
chainId: number;
|
|
60
|
+
verifyingContract: string;
|
|
61
|
+
};
|
|
62
|
+
primaryType: string;
|
|
63
|
+
types: Record<string, Array<{
|
|
64
|
+
name: string;
|
|
65
|
+
type: string;
|
|
66
|
+
}>>;
|
|
67
|
+
message: {
|
|
68
|
+
version: number;
|
|
69
|
+
schema: string;
|
|
70
|
+
recipient: string;
|
|
71
|
+
time: bigint;
|
|
72
|
+
expirationTime: bigint;
|
|
73
|
+
revocable: boolean;
|
|
74
|
+
refUID: string;
|
|
75
|
+
data: string;
|
|
76
|
+
salt?: string;
|
|
77
|
+
};
|
|
78
|
+
signature: Hex;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* EAS-encoded `bytes data` for a Provex identity claim.
|
|
82
|
+
*
|
|
83
|
+
* Pure (no I/O), idempotent, deterministic — the same claim always
|
|
84
|
+
* produces the same hex. Three callers depend on this byte-exact match:
|
|
85
|
+
*
|
|
86
|
+
* - {@link buildOffchainEasAttestation} — bytes signed by the gate's
|
|
87
|
+
* off-chain EIP-712 envelope.
|
|
88
|
+
* - {@link buildDelegatedEasAttestationRequest} — bytes signed by the
|
|
89
|
+
* gate's delegated EIP-712 envelope (sponsored on-chain publish).
|
|
90
|
+
* - `packages/ui/src/lib/identityChain.ts` — bytes the user's own
|
|
91
|
+
* wallet writes via plain `attest()` (self-pay on-chain publish).
|
|
92
|
+
*
|
|
93
|
+
* Centralized so a schema field reorder lands in one place — if any
|
|
94
|
+
* caller drifted out of sync, the on-chain delegated signature would
|
|
95
|
+
* verify against different bytes than the contract was handed, and EAS
|
|
96
|
+
* would revert with `InvalidSignature()`.
|
|
97
|
+
*
|
|
98
|
+
* Field order is load-bearing — it must match `PROVEX_IDENTITY_SCHEMA`
|
|
99
|
+
* in `eas.ts` exactly.
|
|
100
|
+
*/
|
|
101
|
+
export declare const encodeIdentityClaim: (claim: IdentityClaim) => Hex;
|
|
102
|
+
/**
|
|
103
|
+
* Default attestation validity window — 90 days, matching the off-chain
|
|
104
|
+
* signing validity in `signOffchainAttestation`. EAS reverts with
|
|
105
|
+
* `InvalidExpirationTime()` when `expirationTime <= block.timestamp`, so
|
|
106
|
+
* any value here must comfortably exceed worst-case mining time even on
|
|
107
|
+
* a congested network. A previous 5-minute window kept tripping that
|
|
108
|
+
* revert when a PulseChain tx took longer than 5 minutes to mine.
|
|
109
|
+
*
|
|
110
|
+
* Exposed as a function (rather than a constant) so callers compute the
|
|
111
|
+
* deadline relative to the moment they sign/submit, not module-load
|
|
112
|
+
* time.
|
|
113
|
+
*/
|
|
114
|
+
export declare const attestationExpirationTime: (options?: {
|
|
115
|
+
nowSeconds?: number;
|
|
116
|
+
}) => bigint;
|
|
117
|
+
/**
|
|
118
|
+
* Build a fully-signed off-chain EAS attestation for a Provex identity
|
|
119
|
+
* claim. Pure function modulo the signature: no network I/O, no on-chain
|
|
120
|
+
* verification (the SDK's `verifyOnchain` option is left disabled — we
|
|
121
|
+
* don't need an RPC just to sign).
|
|
122
|
+
*
|
|
123
|
+
* The returned object is JSON-serializable directly — bigints are kept
|
|
124
|
+
* as bigints so the caller decides how to encode them on the wire.
|
|
125
|
+
*/
|
|
126
|
+
export declare const buildOffchainEasAttestation: (args: BuildOffchainEasAttestationArgs) => Promise<OffchainEasAttestation>;
|
|
127
|
+
/**
|
|
128
|
+
* Convenience wrapper: resolve `eas` + `schemaUid` from the chainId via
|
|
129
|
+
* `getEasAddress` / `getSchemaUid`, then sign. Throws if either lookup
|
|
130
|
+
* fails (chain unknown, address not pinned, or schema not registered).
|
|
131
|
+
*
|
|
132
|
+
* Prefer this over the raw `buildOffchainEasAttestation` for any
|
|
133
|
+
* caller that can hand a `chainId` — it eliminates the chance of
|
|
134
|
+
* pairing the wrong EAS address with the wrong UID.
|
|
135
|
+
*/
|
|
136
|
+
export declare const buildOffchainEasAttestationForChain: (args: Omit<BuildOffchainEasAttestationArgs, "eas" | "schemaUid">) => Promise<OffchainEasAttestation>;
|
|
137
|
+
/**
|
|
138
|
+
* Release claim destined for a gate-signed off-chain Ethereum Attestation
|
|
139
|
+
* Service attestation. The subject is a dapp release, not a user: `dappKey`
|
|
140
|
+
* keys the per-application timeline and `manifestHash` is the exact release
|
|
141
|
+
* proven via Transport-Layer-Security. There is no `expiresAt` field — a
|
|
142
|
+
* release attestation's freshness is the EAS-native `message.expirationTime`,
|
|
143
|
+
* set by the caller.
|
|
144
|
+
*/
|
|
145
|
+
export type ReleaseClaim = {
|
|
146
|
+
dappKey: Hex;
|
|
147
|
+
manifestHash: Hex;
|
|
148
|
+
domain: string;
|
|
149
|
+
version: string;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* EAS-encoded `bytes data` for a Provex release claim. Pure and
|
|
153
|
+
* deterministic. Field order is load-bearing — it must match
|
|
154
|
+
* `PROVEX_RELEASE_SCHEMA` in `eas.ts` exactly.
|
|
155
|
+
*/
|
|
156
|
+
export declare const encodeReleaseClaim: (claim: ReleaseClaim) => Hex;
|
|
157
|
+
export type BuildOffchainReleaseAttestationArgs = {
|
|
158
|
+
claim: ReleaseClaim;
|
|
159
|
+
attester: LocalAccount;
|
|
160
|
+
/** EAS contract address on the target chain (used in the EIP-712 domain). */
|
|
161
|
+
eas: Address;
|
|
162
|
+
/** Pinned EAS schema UID — must match `PROVEX_RELEASE_SCHEMA`. */
|
|
163
|
+
schemaUid: Hex;
|
|
164
|
+
/** EVM chain id the attestation is bound to. */
|
|
165
|
+
chainId: number;
|
|
166
|
+
/** EAS-native freshness horizon (Unix seconds). Use `attestationExpirationTime()`. */
|
|
167
|
+
expirationTime: bigint;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Build a fully-signed off-chain release attestation. The recipient is the
|
|
171
|
+
* zero address — the subject is the manifest (in the encoded data), not a
|
|
172
|
+
* wallet. Gate-signed: the attester is the gate's own account, vouching that
|
|
173
|
+
* it verified a Transport-Layer-Security proof of this release.
|
|
174
|
+
*/
|
|
175
|
+
export declare const buildOffchainReleaseAttestation: (args: BuildOffchainReleaseAttestationArgs) => Promise<OffchainEasAttestation>;
|
|
176
|
+
/**
|
|
177
|
+
* Convenience wrapper: resolve `eas` + release `schemaUid` from the chainId,
|
|
178
|
+
* then sign. Throws if either lookup fails (chain unknown, address not
|
|
179
|
+
* pinned, or release schema not registered on that chain).
|
|
180
|
+
*/
|
|
181
|
+
export declare const buildOffchainReleaseAttestationForChain: (args: Omit<BuildOffchainReleaseAttestationArgs, "eas" | "schemaUid">) => Promise<OffchainEasAttestation>;
|
|
182
|
+
/**
|
|
183
|
+
* Age + identity claim destined for an off-chain Ethereum Attestation Service
|
|
184
|
+
* attestation. `nullifier` is the one-way, domain-salted uniqueness marker;
|
|
185
|
+
* `overN` answers only "at least `threshold` years old"; `identityVerified`
|
|
186
|
+
* records that the capture proved possession of an identity-proofed account.
|
|
187
|
+
*
|
|
188
|
+
* No personal field is carried — by design. The date of birth, name, Social
|
|
189
|
+
* Security Number, and account identifier stay inside the issuer's trust
|
|
190
|
+
* boundary; only these derived facts escape it. The subject is bound by the
|
|
191
|
+
* attestation's `recipient` (the holder's wallet), not by anything here.
|
|
192
|
+
*/
|
|
193
|
+
export type AgeClaim = {
|
|
194
|
+
nullifier: Hex;
|
|
195
|
+
overN: boolean;
|
|
196
|
+
threshold: number;
|
|
197
|
+
identityVerified: boolean;
|
|
198
|
+
/** Unix seconds at which this claim expires; encoded as uint64. */
|
|
199
|
+
expiresAt: bigint;
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* EAS-encoded `bytes data` for a Provex age claim. Pure and deterministic —
|
|
203
|
+
* no I/O, no side effects, the same claim always produces the same hex.
|
|
204
|
+
*
|
|
205
|
+
* Field order is load-bearing — it must match `PROVEX_AGE_SCHEMA` in `eas.ts`
|
|
206
|
+
* exactly, because `decodeAgeClaim` reads positionally. A reorder here without
|
|
207
|
+
* a matching one there would decode `overN` out of the `identityVerified` slot.
|
|
208
|
+
*/
|
|
209
|
+
export declare const encodeAgeClaim: (claim: AgeClaim) => Hex;
|
|
210
|
+
export type BuildOffchainAgeAttestationArgs = {
|
|
211
|
+
claim: AgeClaim;
|
|
212
|
+
attester: LocalAccount;
|
|
213
|
+
/** The holder's wallet — the subject this credential is bound to. */
|
|
214
|
+
recipient: Address;
|
|
215
|
+
/** EAS contract address on the target chain (used in the EIP-712 domain). */
|
|
216
|
+
eas: Address;
|
|
217
|
+
/** Pinned EAS schema UID — must match `PROVEX_AGE_SCHEMA`. */
|
|
218
|
+
schemaUid: Hex;
|
|
219
|
+
/** EVM chain id the attestation is bound to. */
|
|
220
|
+
chainId: number;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Build a fully-signed off-chain age + identity attestation.
|
|
224
|
+
*
|
|
225
|
+
* Unlike a release attestation (whose subject is a manifest, so its recipient
|
|
226
|
+
* is the zero address), the recipient here is the holder's wallet: the
|
|
227
|
+
* credential is ABOUT a person, and that wallet is how a consuming application
|
|
228
|
+
* knows whose credential it is. Gate-signed — the attester vouches that it
|
|
229
|
+
* derived these facts inside its trust boundary.
|
|
230
|
+
*
|
|
231
|
+
* There is deliberately no separate `expirationTime` parameter. The EAS-native
|
|
232
|
+
* `message.expirationTime` is DERIVED from `claim.expiresAt`, exactly as the
|
|
233
|
+
* identity builder derives it, so the two can never disagree. Accepting both
|
|
234
|
+
* would let a caller sign a credential whose envelope says "never expires"
|
|
235
|
+
* while its encoded data says otherwise — a consumer using standard Ethereum
|
|
236
|
+
* Attestation Service tooling and one decoding the claim would then reach
|
|
237
|
+
* opposite conclusions about the same credential, with nothing to flag it.
|
|
238
|
+
*
|
|
239
|
+
* `recipient` stays a separate parameter because {@link AgeClaim} carries no
|
|
240
|
+
* subject field by design — binding the holder inside the encoded data would
|
|
241
|
+
* put a personal identifier in the schema.
|
|
242
|
+
*/
|
|
243
|
+
export declare const buildOffchainAgeAttestation: (args: BuildOffchainAgeAttestationArgs) => Promise<OffchainEasAttestation>;
|
|
244
|
+
/**
|
|
245
|
+
* Convenience wrapper: resolve `eas` + age `schemaUid` from the chainId, then
|
|
246
|
+
* sign. Throws if either lookup fails (chain unknown, address not pinned, or
|
|
247
|
+
* no age schema unique identifier pinned for that chain).
|
|
248
|
+
*/
|
|
249
|
+
export declare const buildOffchainAgeAttestationForChain: (args: Omit<BuildOffchainAgeAttestationArgs, "eas" | "schemaUid">) => Promise<OffchainEasAttestation>;
|
|
250
|
+
/**
|
|
251
|
+
* Wire-shape delegated attestation signature suitable for the EAS
|
|
252
|
+
* contract's `attestByDelegation(DelegatedAttestationRequest)` call.
|
|
253
|
+
*
|
|
254
|
+
* Mirrors the on-chain `Signature` struct (`v, r, s`) plus the
|
|
255
|
+
* delegation envelope fields (`attester`, `deadline`, `nonce`) the
|
|
256
|
+
* contract needs to verify the signature without trusting `msg.sender`.
|
|
257
|
+
*
|
|
258
|
+
* Wire-safe: `deadline` and `nonce` are base-10 strings so the whole
|
|
259
|
+
* object survives `JSON.stringify` without bigint loss. Re-hydrate to
|
|
260
|
+
* bigint at the boundary that actually calls the contract.
|
|
261
|
+
*
|
|
262
|
+
* **Why this shape instead of the SDK's response shape:** the SDK
|
|
263
|
+
* returns a nested `{ message, signature, types, ... }` object intended
|
|
264
|
+
* for off-chain verification. We only need what `attestByDelegation`
|
|
265
|
+
* actually consumes — keeping the wire narrow makes the gate's
|
|
266
|
+
* attestationStore + the UI saved-proof both materially smaller and
|
|
267
|
+
* makes the contract-call site self-documenting.
|
|
268
|
+
*/
|
|
269
|
+
export type DelegatedAttestationSignature = {
|
|
270
|
+
signature: {
|
|
271
|
+
v: number;
|
|
272
|
+
r: Hex;
|
|
273
|
+
s: Hex;
|
|
274
|
+
};
|
|
275
|
+
attester: Address;
|
|
276
|
+
/** Base-10 stringified `uint64`. `'0'` = signature never expires. */
|
|
277
|
+
deadline: string;
|
|
278
|
+
/** Base-10 stringified `uint256`. EAS contract's nonce for `attester` at sign time. */
|
|
279
|
+
nonce: string;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Inputs to {@link buildDelegatedEasAttestationRequest}.
|
|
283
|
+
*
|
|
284
|
+
* `nonce` is required because we don't connect a provider to the SDK
|
|
285
|
+
* (server-side signing should not depend on the EAS contract being
|
|
286
|
+
* reachable mid-sign). Callers fetch it via viem `getNonce(attester)`
|
|
287
|
+
* before signing — see `notary-gate/src/lib/delegation-signer.ts`.
|
|
288
|
+
*
|
|
289
|
+
* `deadline: 0n` is the EAS sentinel for "no expiration"; recommended
|
|
290
|
+
* default since the Provex publish flow already enforces an
|
|
291
|
+
* `expirationTime` on the attestation itself.
|
|
292
|
+
*/
|
|
293
|
+
export type BuildDelegatedEasAttestationRequestArgs = {
|
|
294
|
+
attester: LocalAccount;
|
|
295
|
+
claim: IdentityClaim;
|
|
296
|
+
chainId: number;
|
|
297
|
+
eas: Address;
|
|
298
|
+
schemaUid: Hex;
|
|
299
|
+
/** EAS contract nonce for `attester.address`. Fetch via `getNonce`. */
|
|
300
|
+
nonce: bigint;
|
|
301
|
+
/** Use `0n` for no expiration (recommended). */
|
|
302
|
+
deadline: bigint;
|
|
303
|
+
/**
|
|
304
|
+
* EAS contract version string used in the EIP-712 domain. Must match
|
|
305
|
+
* the on-chain `eip712Domain().version` for `attestByDelegation` to
|
|
306
|
+
* verify. Same value as for off-chain attestations on the same chain.
|
|
307
|
+
*/
|
|
308
|
+
contractVersion: string;
|
|
309
|
+
};
|
|
310
|
+
export declare const buildDelegatedEasAttestationRequest: (args: BuildDelegatedEasAttestationRequestArgs) => Promise<DelegatedAttestationSignature>;
|
|
311
|
+
/**
|
|
312
|
+
* Convenience wrapper: resolve `eas` + `schemaUid` from the chainId,
|
|
313
|
+
* pair with the canonical contract version string for the chain, then
|
|
314
|
+
* sign. Throws if either chain lookup fails.
|
|
315
|
+
*
|
|
316
|
+
* Prefer this over the raw {@link buildDelegatedEasAttestationRequest}
|
|
317
|
+
* for any caller that has a chainId — eliminates the chance of pairing
|
|
318
|
+
* the wrong EAS address with the wrong schema UID with the wrong
|
|
319
|
+
* contract version.
|
|
320
|
+
*/
|
|
321
|
+
export declare const buildDelegatedEasAttestationRequestForChain: (args: Omit<BuildDelegatedEasAttestationRequestArgs, "eas" | "schemaUid" | "contractVersion"> & {
|
|
322
|
+
contractVersion?: string;
|
|
323
|
+
}) => Promise<DelegatedAttestationSignature>;
|
|
324
|
+
export type BuildDelegatedReleaseAttestationRequestArgs = {
|
|
325
|
+
attester: LocalAccount;
|
|
326
|
+
claim: ReleaseClaim;
|
|
327
|
+
chainId: number;
|
|
328
|
+
eas: Address;
|
|
329
|
+
schemaUid: Hex;
|
|
330
|
+
/** EAS contract nonce for `attester.address`. Fetch via `getNonce`. */
|
|
331
|
+
nonce: bigint;
|
|
332
|
+
/** Relayer submission deadline as a uint64; use `0n` for no deadline. */
|
|
333
|
+
deadline: bigint;
|
|
334
|
+
/** EAS contract version string used in the EIP-712 domain. */
|
|
335
|
+
contractVersion: string;
|
|
336
|
+
/** EAS-native freshness horizon (Unix seconds). Use `attestationExpirationTime()`. */
|
|
337
|
+
expirationTime: bigint;
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* Sign a delegated release attestation request — lets any caller submit
|
|
341
|
+
* `attestByDelegation` on-chain with the gate recorded as the attester. The
|
|
342
|
+
* recipient is the zero address (the subject is the manifest, not a wallet).
|
|
343
|
+
*/
|
|
344
|
+
export declare const buildDelegatedReleaseAttestationRequest: (args: BuildDelegatedReleaseAttestationRequestArgs) => Promise<DelegatedAttestationSignature>;
|
|
345
|
+
/**
|
|
346
|
+
* Convenience wrapper: resolve `eas` + release `schemaUid` + the canonical
|
|
347
|
+
* contract version from the chainId, then sign. Throws if any lookup fails.
|
|
348
|
+
*/
|
|
349
|
+
export declare const buildDelegatedReleaseAttestationRequestForChain: (args: Omit<BuildDelegatedReleaseAttestationRequestArgs, "eas" | "schemaUid" | "contractVersion"> & {
|
|
350
|
+
contractVersion?: string;
|
|
351
|
+
}) => Promise<DelegatedAttestationSignature>;
|
|
352
|
+
export {};
|