@provex/eas 1.11.0 → 2.0.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/eas-builder-chains.test.d.ts +1 -0
- package/dist/eas-builder-chains.test.js +93 -0
- package/dist/eas-builder.d.ts +53 -0
- package/dist/eas-builder.js +15 -5
- package/dist/eas-overrides.test.d.ts +1 -0
- package/dist/eas-overrides.test.js +72 -0
- package/dist/eas.js +1 -1
- package/dist/offchain-verify-rejections.test.d.ts +1 -0
- package/dist/offchain-verify-rejections.test.js +70 -0
- package/dist/offchain-verify.js +3 -8
- package/dist/viem-only.test.js +5 -2
- package/package.json +3 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
4
|
+
import { adaptViemAccountToEasSigner, buildDelegatedEasAttestationRequest, buildDelegatedEasAttestationRequestForChain, buildOffchainEasAttestationForChain, resolveEasSdkExports, } from './eas-builder.js';
|
|
5
|
+
import { getEasAddress, getSchemaUid } from './eas.js';
|
|
6
|
+
const attester = privateKeyToAccount(generatePrivateKey());
|
|
7
|
+
const CLAIM = {
|
|
8
|
+
platformHash: `0x${'11'.repeat(32)}`,
|
|
9
|
+
handle: 'alice',
|
|
10
|
+
user: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
|
|
11
|
+
expiresAt: 1800000000n,
|
|
12
|
+
};
|
|
13
|
+
describe('buildOffchainEasAttestationForChain', () => {
|
|
14
|
+
it('binds the pinned EAS address and schema unique identifier of the chain', async () => {
|
|
15
|
+
// WHY: pairing the wrong contract with the wrong schema would produce an
|
|
16
|
+
// attestation that no verifier on that chain accepts.
|
|
17
|
+
const result = await buildOffchainEasAttestationForChain({ claim: CLAIM, attester, chainId: 369 });
|
|
18
|
+
assert.equal(result.domain.chainId, 369);
|
|
19
|
+
assert.equal(result.domain.verifyingContract.toLowerCase(), getEasAddress(369).toLowerCase());
|
|
20
|
+
assert.equal(result.message.schema, getSchemaUid(369));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe('buildDelegatedEasAttestationRequestForChain', () => {
|
|
24
|
+
const baseArgs = { claim: CLAIM, attester, nonce: 3n, deadline: 0n };
|
|
25
|
+
const signRaw = (chainId, contractVersion) => buildDelegatedEasAttestationRequest({
|
|
26
|
+
...baseArgs,
|
|
27
|
+
chainId,
|
|
28
|
+
eas: getEasAddress(chainId),
|
|
29
|
+
schemaUid: getSchemaUid(chainId),
|
|
30
|
+
contractVersion,
|
|
31
|
+
});
|
|
32
|
+
it('signs PulseChain with the forked EAS contract version 1.4.0', async () => {
|
|
33
|
+
// WHY: the EIP-712 domain version must match the deployed contract or
|
|
34
|
+
// attestByDelegation reverts with InvalidSignature.
|
|
35
|
+
const viaChain = await buildDelegatedEasAttestationRequestForChain({ ...baseArgs, chainId: 369 });
|
|
36
|
+
assert.deepEqual(viaChain, await signRaw(369, '1.4.0'));
|
|
37
|
+
});
|
|
38
|
+
it('signs Ethereum mainnet with the official EAS contract version 0.26', async () => {
|
|
39
|
+
process.env.EAS_SCHEMA_UID_1_OVERRIDE = `0x${'99'.repeat(32)}`;
|
|
40
|
+
try {
|
|
41
|
+
const viaChain = await buildDelegatedEasAttestationRequestForChain({ ...baseArgs, chainId: 1 });
|
|
42
|
+
assert.deepEqual(viaChain, await signRaw(1, '0.26'));
|
|
43
|
+
assert.notDeepEqual(viaChain, await signRaw(1, '1.4.0'));
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
delete process.env.EAS_SCHEMA_UID_1_OVERRIDE;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it('signs Sepolia with version 0.26 as well', async () => {
|
|
50
|
+
const viaChain = await buildDelegatedEasAttestationRequestForChain({ ...baseArgs, chainId: 11155111 });
|
|
51
|
+
assert.deepEqual(viaChain, await signRaw(11155111, '0.26'));
|
|
52
|
+
});
|
|
53
|
+
it('honors an explicit contract version over the chain default', async () => {
|
|
54
|
+
// WHY: the publish path reads the live contract version() and must win.
|
|
55
|
+
const viaChain = await buildDelegatedEasAttestationRequestForChain({
|
|
56
|
+
...baseArgs,
|
|
57
|
+
chainId: 369,
|
|
58
|
+
contractVersion: '2.0.0',
|
|
59
|
+
});
|
|
60
|
+
assert.deepEqual(viaChain, await signRaw(369, '2.0.0'));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('adaptViemAccountToEasSigner', () => {
|
|
64
|
+
it('refuses to sign when the SDK hands over no types', async () => {
|
|
65
|
+
// WHY: signing with no primary type would hash an empty struct; fail fast.
|
|
66
|
+
const signer = adaptViemAccountToEasSigner(attester);
|
|
67
|
+
await assert.rejects(() => signer.signTypedData({}, {}, {}), /empty types/);
|
|
68
|
+
});
|
|
69
|
+
it('exposes the account address to the SDK', async () => {
|
|
70
|
+
assert.equal(await adaptViemAccountToEasSigner(attester).getAddress(), attester.address);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('resolveEasSdkExports', () => {
|
|
74
|
+
const kit = {
|
|
75
|
+
Offchain: class {
|
|
76
|
+
},
|
|
77
|
+
OffchainAttestationVersion: {},
|
|
78
|
+
SchemaEncoder: class {
|
|
79
|
+
},
|
|
80
|
+
Delegated: class {
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
it('accepts a flat namespace', () => {
|
|
84
|
+
assert.equal(resolveEasSdkExports(kit), kit);
|
|
85
|
+
});
|
|
86
|
+
it('unwraps a namespace that nests the kit under default', () => {
|
|
87
|
+
assert.equal(resolveEasSdkExports({ default: kit }), kit);
|
|
88
|
+
});
|
|
89
|
+
it('throws when the kit is on neither the namespace nor its default', () => {
|
|
90
|
+
assert.throws(() => resolveEasSdkExports({ default: {} }), /cannot locate SchemaEncoder/);
|
|
91
|
+
assert.throws(() => resolveEasSdkExports({}), /cannot locate SchemaEncoder/);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/dist/eas-builder.d.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import type * as EasSdkModule from '@ethereum-attestation-service/eas-sdk';
|
|
2
2
|
import type { Address, Hex, LocalAccount } from 'viem';
|
|
3
3
|
declare const easSdkNamespace: typeof EasSdkModule;
|
|
4
|
+
export type EasSdkExports = {
|
|
5
|
+
Offchain: typeof easSdkNamespace.Offchain;
|
|
6
|
+
OffchainAttestationVersion: typeof easSdkNamespace.OffchainAttestationVersion;
|
|
7
|
+
SchemaEncoder: typeof easSdkNamespace.SchemaEncoder;
|
|
8
|
+
Delegated: typeof easSdkNamespace.Delegated;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Locate the SDK's named exports on whichever namespace shape the runtime
|
|
12
|
+
* resolver produced: the flat namespace (Node CommonJS, bundlers) or a
|
|
13
|
+
* namespace that nests them under `default`.
|
|
14
|
+
*
|
|
15
|
+
* @throws if neither shape carries `SchemaEncoder` — signing with a half-loaded
|
|
16
|
+
* kit would fail later with a far less useful error.
|
|
17
|
+
*/
|
|
18
|
+
export declare const resolveEasSdkExports: (namespace: unknown) => EasSdkExports;
|
|
4
19
|
type OffchainAttestationVersionEnum = (typeof easSdkNamespace)['OffchainAttestationVersion'][keyof (typeof easSdkNamespace)['OffchainAttestationVersion']];
|
|
5
20
|
/**
|
|
6
21
|
* @provex/eas — off-chain Ethereum Attestation Service attestation builder.
|
|
@@ -115,6 +130,25 @@ export declare const encodeIdentityClaim: (claim: IdentityClaim) => Hex;
|
|
|
115
130
|
export declare const attestationExpirationTime: (options?: {
|
|
116
131
|
nowSeconds?: number;
|
|
117
132
|
}) => bigint;
|
|
133
|
+
/**
|
|
134
|
+
* Adapt a viem `LocalAccount` to the ethers-shaped `TypeDataSigner`
|
|
135
|
+
* the EAS SDK expects. The SDK only calls `signTypedData(domain, types,
|
|
136
|
+
* value)` and reads `getAddress()`; everything else on `Addressable` is
|
|
137
|
+
* unused.
|
|
138
|
+
*
|
|
139
|
+
* The `as never` casts are unavoidable — viem's typed-data types are
|
|
140
|
+
* deeply generic and ethers' `TypedDataField` shape doesn't unify with
|
|
141
|
+
* them. Both libraries produce the same EIP-712 bytes, so the cast is a
|
|
142
|
+
* type-erasure boundary, not a semantic one.
|
|
143
|
+
*/
|
|
144
|
+
/** Adapt a viem account to the ethers-shaped signer the SDK expects. Exported for tests. */
|
|
145
|
+
export declare const adaptViemAccountToEasSigner: (account: LocalAccount) => {
|
|
146
|
+
getAddress: () => Promise<string>;
|
|
147
|
+
signTypedData: (domain: Record<string, unknown>, types: Record<string, Array<{
|
|
148
|
+
name: string;
|
|
149
|
+
type: string;
|
|
150
|
+
}>>, value: Record<string, unknown>) => Promise<string>;
|
|
151
|
+
};
|
|
118
152
|
/**
|
|
119
153
|
* Build a fully-signed off-chain EAS attestation for a Provex identity
|
|
120
154
|
* claim. Pure function modulo the signature: no network I/O, no on-chain
|
|
@@ -322,6 +356,25 @@ export declare const buildDelegatedEasAttestationRequest: (args: BuildDelegatedE
|
|
|
322
356
|
export declare const buildDelegatedEasAttestationRequestForChain: (args: Omit<BuildDelegatedEasAttestationRequestArgs, "eas" | "schemaUid" | "contractVersion"> & {
|
|
323
357
|
contractVersion?: string;
|
|
324
358
|
}) => Promise<DelegatedAttestationSignature>;
|
|
359
|
+
/**
|
|
360
|
+
* Canonical EAS contract version per chain. Pulse mainnet/testnet
|
|
361
|
+
* deploys forked the EAS contract reporting version v1.4.0; Ethereum
|
|
362
|
+
* mainnet/Sepolia are on the official v0.26 deploy. The EIP-712 domain
|
|
363
|
+
* MUST use this exact value or `attestByDelegation` rejects the
|
|
364
|
+
* signature with `InvalidSignature()`.
|
|
365
|
+
*
|
|
366
|
+
* This is only a FALLBACK default. The on-chain publish path
|
|
367
|
+
* (`packages/notary-gate/src/lib/chain-attest.ts`) reads the deployed
|
|
368
|
+
* contract's `version()` live and passes it as `contractVersion`, so it
|
|
369
|
+
* stays correct even if the PulseChain EAS fork bumps its version again.
|
|
370
|
+
* Keep this constant aligned with the live deploys regardless.
|
|
371
|
+
*
|
|
372
|
+
* Mirrors `easVersionForChain` in
|
|
373
|
+
* `packages/notary-gate/src/lib/offchain-easscan.ts:43` — kept in sync
|
|
374
|
+
* by hand because pulling the gate file into `@provex/eas` would
|
|
375
|
+
* create a dependency loop.
|
|
376
|
+
*/
|
|
377
|
+
export declare const easContractVersionForChain: (chainId: number) => string;
|
|
325
378
|
export type BuildDelegatedReleaseAttestationRequestArgs = {
|
|
326
379
|
attester: LocalAccount;
|
|
327
380
|
claim: ReleaseClaim;
|
package/dist/eas-builder.js
CHANGED
|
@@ -4,15 +4,24 @@ import { getAgeSchemaUid, getEasAddress, getReleaseSchemaUid, getSchemaUid, PROV
|
|
|
4
4
|
// Runtime namespace, typed against the erased `import type` above so the shims
|
|
5
5
|
// below (`typeof easSdkNamespace`) keep their exact SDK types.
|
|
6
6
|
const easSdkNamespace = easSdkNamespaceRaw;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Locate the SDK's named exports on whichever namespace shape the runtime
|
|
9
|
+
* resolver produced: the flat namespace (Node CommonJS, bundlers) or a
|
|
10
|
+
* namespace that nests them under `default`.
|
|
11
|
+
*
|
|
12
|
+
* @throws if neither shape carries `SchemaEncoder` — signing with a half-loaded
|
|
13
|
+
* kit would fail later with a far less useful error.
|
|
14
|
+
*/
|
|
15
|
+
export const resolveEasSdkExports = (namespace) => {
|
|
16
|
+
const flat = namespace;
|
|
9
17
|
if (typeof flat.SchemaEncoder === 'function')
|
|
10
18
|
return flat;
|
|
11
19
|
if (flat.default && typeof flat.default.SchemaEncoder === 'function') {
|
|
12
20
|
return flat.default;
|
|
13
21
|
}
|
|
14
22
|
throw new Error('[eas-builder] @ethereum-attestation-service/eas-sdk: cannot locate SchemaEncoder on namespace or default export');
|
|
15
|
-
}
|
|
23
|
+
};
|
|
24
|
+
const easSdkExports = resolveEasSdkExports(easSdkNamespace);
|
|
16
25
|
const { Offchain, OffchainAttestationVersion, SchemaEncoder, Delegated } = easSdkExports;
|
|
17
26
|
const ZERO_REF_UID = ('0x' + '0'.repeat(64));
|
|
18
27
|
/**
|
|
@@ -73,7 +82,8 @@ export const attestationExpirationTime = (options = {}) => {
|
|
|
73
82
|
* them. Both libraries produce the same EIP-712 bytes, so the cast is a
|
|
74
83
|
* type-erasure boundary, not a semantic one.
|
|
75
84
|
*/
|
|
76
|
-
|
|
85
|
+
/** Adapt a viem account to the ethers-shaped signer the SDK expects. Exported for tests. */
|
|
86
|
+
export const adaptViemAccountToEasSigner = (account) => ({
|
|
77
87
|
getAddress: async () => account.address,
|
|
78
88
|
signTypedData: async (domain, types, value) => {
|
|
79
89
|
// The EAS SDK passes the message types only (no EIP712Domain entry)
|
|
@@ -343,7 +353,7 @@ export const buildDelegatedEasAttestationRequestForChain = async (args) => build
|
|
|
343
353
|
* by hand because pulling the gate file into `@provex/eas` would
|
|
344
354
|
* create a dependency loop.
|
|
345
355
|
*/
|
|
346
|
-
const easContractVersionForChain = (chainId) => {
|
|
356
|
+
export const easContractVersionForChain = (chainId) => {
|
|
347
357
|
if (chainId === 1 || chainId === 11155111)
|
|
348
358
|
return '0.26';
|
|
349
359
|
return '1.4.0';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { EAS_ADDRESSES_BY_CHAIN_ID, PROVEX_AGE_SCHEMA_UID_BY_CHAIN_ID, getAgeSchemaUidOrPlaceholder, getEasAddress, getSchemaUid, } from './eas.js';
|
|
4
|
+
const ZERO_BYTES32 = `0x${'0'.repeat(64)}`;
|
|
5
|
+
const withEnv = (name, value, run) => {
|
|
6
|
+
process.env[name] = value;
|
|
7
|
+
try {
|
|
8
|
+
run();
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
delete process.env[name];
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
describe('environment overrides', () => {
|
|
15
|
+
it('uses a well-formed EAS address override instead of the pinned address', () => {
|
|
16
|
+
// WHY: staging points at a temporary EAS deploy through this override.
|
|
17
|
+
const override = `0x${'77'.repeat(20)}`;
|
|
18
|
+
withEnv('EAS_ADDRESS_369_OVERRIDE', override, () => {
|
|
19
|
+
assert.equal(getEasAddress(369), override);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
it('rejects a malformed address override rather than signing against it', () => {
|
|
23
|
+
// WHY: a typo in an override must fail loudly; a silent fallback would
|
|
24
|
+
// sign attestations for the wrong contract.
|
|
25
|
+
withEnv('EAS_ADDRESS_369_OVERRIDE', '0x1234', () => {
|
|
26
|
+
assert.throws(() => getEasAddress(369), /EAS_ADDRESS_369_OVERRIDE must be 0x-prefixed 20-byte hex/);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it('treats an empty override as unset', () => {
|
|
30
|
+
withEnv('EAS_ADDRESS_369_OVERRIDE', '', () => {
|
|
31
|
+
assert.equal(getEasAddress(369), EAS_ADDRESSES_BY_CHAIN_ID[369]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('rejects a malformed schema unique identifier override', () => {
|
|
35
|
+
withEnv('EAS_SCHEMA_UID_369_OVERRIDE', '0xabc', () => {
|
|
36
|
+
assert.throws(() => getSchemaUid(369), /EAS_SCHEMA_UID_369_OVERRIDE must be 0x-prefixed 32-byte hex/);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('reads no overrides when the runtime has no process object (browser)', () => {
|
|
40
|
+
// WHY: the user interface bundle has no `process`; overrides must simply
|
|
41
|
+
// be absent there instead of throwing a ReferenceError.
|
|
42
|
+
const original = Object.getOwnPropertyDescriptor(globalThis, 'process');
|
|
43
|
+
Object.defineProperty(globalThis, 'process', { value: undefined, configurable: true });
|
|
44
|
+
try {
|
|
45
|
+
assert.equal(getEasAddress(369), EAS_ADDRESSES_BY_CHAIN_ID[369]);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
if (original)
|
|
49
|
+
Object.defineProperty(globalThis, 'process', original);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('getAgeSchemaUidOrPlaceholder', () => {
|
|
54
|
+
it('returns the pinned identifier on registered chains and zero elsewhere', () => {
|
|
55
|
+
assert.equal(getAgeSchemaUidOrPlaceholder(369), PROVEX_AGE_SCHEMA_UID_BY_CHAIN_ID[369]);
|
|
56
|
+
assert.equal(getAgeSchemaUidOrPlaceholder(999_999), ZERO_BYTES32);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('unpinned chain placeholders', () => {
|
|
60
|
+
it('throws with a deploy hint when a known chain has no pinned address yet', () => {
|
|
61
|
+
// WHY: a null placeholder must never be returned as an address; signing
|
|
62
|
+
// against it would bind attestations to a nonexistent contract.
|
|
63
|
+
const original = EAS_ADDRESSES_BY_CHAIN_ID[369];
|
|
64
|
+
EAS_ADDRESSES_BY_CHAIN_ID[369] = null;
|
|
65
|
+
try {
|
|
66
|
+
assert.throws(() => getEasAddress(369), /address for chainId 369 is not pinned/);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
EAS_ADDRESSES_BY_CHAIN_ID[369] = original;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
package/dist/eas.js
CHANGED
|
@@ -362,4 +362,4 @@ export const EAS_MAINNET_ADDRESS = EAS_ADDRESSES_BY_CHAIN_ID[1];
|
|
|
362
362
|
/** @deprecated use `getSchemaRegistryAddress(1)`. */
|
|
363
363
|
export const EAS_SCHEMA_REGISTRY_MAINNET = SCHEMA_REGISTRY_ADDRESSES_BY_CHAIN_ID[1];
|
|
364
364
|
/** @deprecated use `getSchemaUid(chainId)` (or the soft variant for UI). */
|
|
365
|
-
export const PROVEX_IDENTITY_SCHEMA_UID = PROVEX_IDENTITY_SCHEMA_UID_BY_CHAIN_ID[1]
|
|
365
|
+
export const PROVEX_IDENTITY_SCHEMA_UID = PROVEX_IDENTITY_SCHEMA_UID_BY_CHAIN_ID[1];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
4
|
+
import { buildOffchainEasAttestation } from './eas-builder.js';
|
|
5
|
+
import { EAS_MAINNET_ADDRESS, PROVEX_IDENTITY_SCHEMA_UID } from './eas.js';
|
|
6
|
+
import { isOffchainAttestationShape, liftWireOffchainAttestation, recoverOffchainAttestationSigner, toWireOffchainAttestation, } from './offchain-verify.js';
|
|
7
|
+
const attester = privateKeyToAccount(generatePrivateKey());
|
|
8
|
+
const signedSample = () => buildOffchainEasAttestation({
|
|
9
|
+
claim: {
|
|
10
|
+
platformHash: `0x${'11'.repeat(32)}`,
|
|
11
|
+
handle: 'alice',
|
|
12
|
+
user: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
|
|
13
|
+
expiresAt: 1800000000n,
|
|
14
|
+
},
|
|
15
|
+
attester,
|
|
16
|
+
eas: EAS_MAINNET_ADDRESS,
|
|
17
|
+
schemaUid: PROVEX_IDENTITY_SCHEMA_UID,
|
|
18
|
+
chainId: 1,
|
|
19
|
+
});
|
|
20
|
+
const wireWith = async (patch) => {
|
|
21
|
+
const wire = JSON.parse(JSON.stringify(toWireOffchainAttestation(await signedSample())));
|
|
22
|
+
return { ...wire, message: { ...wire.message, ...patch } };
|
|
23
|
+
};
|
|
24
|
+
describe('liftWireOffchainAttestation rejects unusable payloads', () => {
|
|
25
|
+
it('accepts finite numbers and bigints for the time fields', async () => {
|
|
26
|
+
const lifted = liftWireOffchainAttestation(await wireWith({ time: 5, expirationTime: 7n }));
|
|
27
|
+
assert.equal(lifted?.message.time, 5n);
|
|
28
|
+
assert.equal(lifted?.message.expirationTime, 7n);
|
|
29
|
+
});
|
|
30
|
+
it('returns null for a non-numeric string, so garbage never becomes a deadline', async () => {
|
|
31
|
+
assert.equal(liftWireOffchainAttestation(await wireWith({ time: '12abc' })), null);
|
|
32
|
+
});
|
|
33
|
+
it('returns null for a non-finite number', async () => {
|
|
34
|
+
assert.equal(liftWireOffchainAttestation(await wireWith({ expirationTime: Number.NaN })), null);
|
|
35
|
+
});
|
|
36
|
+
it('returns null for a missing expiration, so a no-expiry replay is refused', async () => {
|
|
37
|
+
assert.equal(liftWireOffchainAttestation(await wireWith({ expirationTime: undefined })), null);
|
|
38
|
+
});
|
|
39
|
+
it('returns null when the payload is not attestation-shaped', () => {
|
|
40
|
+
assert.equal(liftWireOffchainAttestation({ uid: 'x' }), null);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('isOffchainAttestationShape', () => {
|
|
44
|
+
it('rejects null and non-objects before reading any key', () => {
|
|
45
|
+
assert.equal(isOffchainAttestationShape(null), false);
|
|
46
|
+
assert.equal(isOffchainAttestationShape('attestation'), false);
|
|
47
|
+
});
|
|
48
|
+
it('rejects an object missing the signature', async () => {
|
|
49
|
+
const { signature: _omitted, ...rest } = await signedSample();
|
|
50
|
+
assert.equal(isOffchainAttestationShape(rest), false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('recoverOffchainAttestationSigner rejection paths', () => {
|
|
54
|
+
it('reports valid false when the recovered signer is not the expected attester', async () => {
|
|
55
|
+
// WHY: a genuine signature from the wrong key must not pass as the gate's.
|
|
56
|
+
const other = privateKeyToAccount(generatePrivateKey());
|
|
57
|
+
const check = await recoverOffchainAttestationSigner(await signedSample(), other.address);
|
|
58
|
+
assert.equal(check.valid, false);
|
|
59
|
+
assert.equal(check.signer?.toLowerCase(), attester.address.toLowerCase());
|
|
60
|
+
});
|
|
61
|
+
it('reports valid true when the expected attester matches', async () => {
|
|
62
|
+
const check = await recoverOffchainAttestationSigner(await signedSample(), attester.address);
|
|
63
|
+
assert.equal(check.valid, true);
|
|
64
|
+
});
|
|
65
|
+
it('returns valid false with no signer for an unrecoverable signature', async () => {
|
|
66
|
+
const signed = await signedSample();
|
|
67
|
+
const check = await recoverOffchainAttestationSigner({ ...signed, signature: '0x1234' });
|
|
68
|
+
assert.deepEqual(check, { valid: false, signer: null });
|
|
69
|
+
});
|
|
70
|
+
});
|
package/dist/offchain-verify.js
CHANGED
|
@@ -110,14 +110,9 @@ const parseBigintField = (raw) => {
|
|
|
110
110
|
return raw;
|
|
111
111
|
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
112
112
|
return BigInt(raw);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
113
|
+
// The pattern guarantees `BigInt` parses the string, so no try/catch is needed.
|
|
114
|
+
if (typeof raw === 'string' && /^-?\d+$/.test(raw))
|
|
115
|
+
return BigInt(raw);
|
|
121
116
|
return null;
|
|
122
117
|
};
|
|
123
118
|
/**
|
package/dist/viem-only.test.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { dirname, resolve, sep } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
6
|
+
// Coverage runs execute the compiled tests from `dist`; the walker must still
|
|
7
|
+
// read the original `.ts` sources, which sit in the sibling `src` directory.
|
|
8
|
+
const COMPILED_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const HERE = COMPILED_DIRECTORY.endsWith(`${sep}dist`) ? resolve(COMPILED_DIRECTORY, '..', 'src') : COMPILED_DIRECTORY;
|
|
7
10
|
const EAS_SOFTWARE_DEVELOPMENT_KIT = '@ethereum-attestation-service/eas-sdk';
|
|
8
11
|
/**
|
|
9
12
|
* Strip type-only import statements (`import type { … } from '…'`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@provex/eas",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsc",
|
|
49
49
|
"test": "node --import tsx --test 'src/**/*.test.ts'",
|
|
50
|
-
"test:watch": "node --import tsx --test --watch 'src/**/*.test.ts'"
|
|
50
|
+
"test:watch": "node --import tsx --test --watch 'src/**/*.test.ts'",
|
|
51
|
+
"test:coverage": "tsc --sourceMap && node --test --test-concurrency=2 --experimental-test-coverage --test-coverage-exclude='**/*.test.*' --test-coverage-lines=100 --test-coverage-branches=100 --test-coverage-functions=100 'dist/**/*.test.js'"
|
|
51
52
|
},
|
|
52
53
|
"files": [
|
|
53
54
|
"dist",
|