@x1id/resolve 0.15.0 → 0.16.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/README.md +37 -17
- package/dist/accounts.d.ts +11 -4
- package/dist/accounts.js +26 -5
- package/dist/agent.js +6 -6
- package/dist/attestation.d.ts +87 -23
- package/dist/attestation.js +82 -23
- package/dist/domainProof.d.ts +6 -3
- package/dist/domainProof.js +7 -4
- package/dist/gate.d.ts +13 -6
- package/dist/gate.js +15 -8
- package/dist/gateClient.d.ts +10 -8
- package/dist/gateClient.js +27 -8
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/pause.d.ts +104 -0
- package/dist/pause.js +152 -0
- package/dist/payment.d.ts +58 -0
- package/dist/payment.js +45 -0
- package/dist/scoped.d.ts +10 -5
- package/dist/scoped.js +11 -5
- package/dist/signin.js +21 -10
- package/dist/voucher.d.ts +49 -9
- package/dist/voucher.js +68 -14
- package/dist/x402.d.ts +16 -0
- package/dist/x402.js +15 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ answered, and a mixed shape is refused rather than guessed.
|
|
|
12
12
|
import { createResolver, WasmResolver } from "@x1id/resolve";
|
|
13
13
|
|
|
14
14
|
const wasm = await WasmResolver.fromBytes(/* the module bytes, see below */);
|
|
15
|
-
const x1id = createResolver({ rpcUrl: "https://rpc.
|
|
15
|
+
const x1id = createResolver({ rpcUrl: "https://rpc.mainnet.x1.xyz", wasm });
|
|
16
16
|
|
|
17
17
|
const r = await x1id.resolve("@jack");
|
|
18
18
|
// { name: "jack", namespace: "handle", address: "H8Fs…", chain: "X1",
|
|
@@ -23,11 +23,11 @@ await x1id.resolve("@jack.x1"); // throws ResolveError { code: "ambiguous" }
|
|
|
23
23
|
await x1id.reverse("H8Fs…"); // "nike" — the address's primary @handle, or null
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
> **Which RPC?** The `@handle` registry
|
|
27
|
-
> (`https://rpc.
|
|
28
|
-
> (`https://rpc.
|
|
29
|
-
>
|
|
30
|
-
> an honest `not-found`, never a fabricated address.
|
|
26
|
+
> **Which RPC?** The `@handle` registry and X1NS domains are both on **X1
|
|
27
|
+
> mainnet** (`https://rpc.mainnet.x1.xyz`) — point the resolver there. X1
|
|
28
|
+
> testnet (`https://rpc.testnet.x1.xyz`) runs the same program and the SDK
|
|
29
|
+
> works against it identically for development. On an RPC where the registry
|
|
30
|
+
> is not deployed a handle is an honest `not-found`, never a fabricated address.
|
|
31
31
|
|
|
32
32
|
## Where this code lives
|
|
33
33
|
|
|
@@ -64,7 +64,7 @@ const require = createRequire(import.meta.url);
|
|
|
64
64
|
const wasmPath = require.resolve("@x1id/resolve/wasm/x1_resolve_wasm.wasm");
|
|
65
65
|
const wasm = await WasmResolver.fromBytes(readFileSync(wasmPath));
|
|
66
66
|
|
|
67
|
-
const x1id = createResolver({ rpcUrl: "https://rpc.
|
|
67
|
+
const x1id = createResolver({ rpcUrl: "https://rpc.mainnet.x1.xyz", wasm });
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
Node ≥ 20.6 can use `import.meta.resolve` instead of `createRequire`:
|
|
@@ -139,7 +139,7 @@ const wasm = await WasmResolver.fromBytes(/* module bytes */);
|
|
|
139
139
|
const scopedHandleDeriver = await makeScopedHandleDeriver(web3);
|
|
140
140
|
|
|
141
141
|
const x1id = createResolver({
|
|
142
|
-
rpcUrl: "https://rpc.
|
|
142
|
+
rpcUrl: "https://rpc.mainnet.x1.xyz",
|
|
143
143
|
wasm,
|
|
144
144
|
scopedHandleDeriver,
|
|
145
145
|
});
|
|
@@ -366,18 +366,30 @@ export async function POST(req: Request) { // any server runtime with
|
|
|
366
366
|
}
|
|
367
367
|
```
|
|
368
368
|
|
|
369
|
-
Fail-closed by default (re-throws if the decision can't be obtained
|
|
370
|
-
`
|
|
371
|
-
the full decision; pass a stored policy's `gp_…` id as
|
|
372
|
-
policy. The hosted check is keyed/metered — see
|
|
369
|
+
Fail-closed by default (re-throws if the decision can't be obtained). `onError:
|
|
370
|
+
"deny"` fails closed without throwing (clean `allowed: false`). `checkGate()` is
|
|
371
|
+
the lower-level client for the full decision; pass a stored policy's `gp_…` id as
|
|
372
|
+
the policy to reuse a named policy. The hosted check is keyed/metered — see
|
|
373
|
+
`services/dev-api`'s README.
|
|
374
|
+
|
|
375
|
+
> **⚠️ Never use `onError: "allow"` on a real gate.** It FAILS OPEN: on *any*
|
|
376
|
+
> error — a network blip, a timeout, a 5xx, an auth failure, a malformed
|
|
377
|
+
> response — it fabricates a `passed: true` decision and admits the caller
|
|
378
|
+
> **unverified**. Anyone who can make your gate call fail (trivially: induce a
|
|
379
|
+
> timeout) then bypasses the gate completely, so it is worse than no gate at all
|
|
380
|
+
> for anything it protects — a route, a mint, an airdrop, a write, an allowlist.
|
|
381
|
+
> It exists ONLY for soft, non-security personalization where a missing
|
|
382
|
+
> verification must not degrade UX (e.g. optionally showing a "verified"
|
|
383
|
+
> flourish). Using it logs a one-time `console.warn`. If the decision gates
|
|
384
|
+
> anything, use the default `"throw"` or `"deny"`.
|
|
373
385
|
|
|
374
386
|
## What works today
|
|
375
387
|
|
|
376
388
|
| | Status |
|
|
377
389
|
|---|---|
|
|
378
390
|
| X1NS resolution (`.x1/.xnt/.xen`) | ✅ mainnet |
|
|
379
|
-
| `@handle` resolution | ✅
|
|
380
|
-
| Reverse lookup | ✅ on-chain `@handle` primary
|
|
391
|
+
| `@handle` resolution | ✅ **X1 mainnet** (and any RPC where the registry is deployed, incl. testnet) |
|
|
392
|
+
| Reverse lookup | ✅ on-chain `@handle` primary with X1NS primary-domain fallback; X1NS results are `<domainAccount>.<tld>`, not a label |
|
|
381
393
|
| Per-chain ETH/BTC records | ✅ read with the staleness rule enforced structurally |
|
|
382
394
|
| Control proofs (prove you control a `@handle`) | ✅ `createControlChallenge` / `verifyControlProof` — spec: `docs/control-proof.md` |
|
|
383
395
|
| Records read (per-chain, staleness-enforced) | ✅ `fetchRecords` |
|
|
@@ -508,7 +520,7 @@ if (agent.manifest) {
|
|
|
508
520
|
const result = validateManifest(raw, {
|
|
509
521
|
expectedName: "payday-bot",
|
|
510
522
|
expectedHandlePda: handleAccount,
|
|
511
|
-
expectedNetwork: "
|
|
523
|
+
expectedNetwork: "mainnet",
|
|
512
524
|
addressRecords: liveRecords(records), // "the chain wins" cross-check
|
|
513
525
|
});
|
|
514
526
|
if (result.ok) {
|
|
@@ -575,7 +587,7 @@ built/verified — that needs real Solana parsing, so it lives in the
|
|
|
575
587
|
[x1id MCP server](../mcp), not here): [`docs/x402.md`](../docs/x402.md).
|
|
576
588
|
|
|
577
589
|
```ts
|
|
578
|
-
import { buildX402Requirements,
|
|
590
|
+
import { buildX402Requirements, X1_MAINNET_NETWORK } from "@x1id/resolve";
|
|
579
591
|
|
|
580
592
|
// server-side: one accepts[] entry for a 402 response
|
|
581
593
|
const requirements = buildX402Requirements({
|
|
@@ -583,6 +595,8 @@ const requirements = buildX402Requirements({
|
|
|
583
595
|
asset: usdcMint,
|
|
584
596
|
amount: "1000000", // smallest units, decimal string
|
|
585
597
|
buyer: buyerAddress, // x1id v1 is self-sponsored: buyer pays their own fee, no facilitator
|
|
598
|
+
network: X1_MAINNET_NETWORK, // defaults to mainnet; a server on another cluster
|
|
599
|
+
// should pass x402NetworkForGenesis(await getGenesisHash)
|
|
586
600
|
memo: "invoice-42",
|
|
587
601
|
});
|
|
588
602
|
```
|
|
@@ -592,6 +606,12 @@ or verify a transaction (that needs `@solana/web3.js`, deliberately kept out
|
|
|
592
606
|
of this zero-dependency package). Use the MCP server's `prepare_x402_payment`
|
|
593
607
|
/ `verify_x402_payment` tools for that.
|
|
594
608
|
|
|
609
|
+
> **Buyers: wait for `finalized`.** The x1id resource server verifies the
|
|
610
|
+
> payment transaction at **finalized** commitment, not `confirmed` — a buyer
|
|
611
|
+
> that submits its payment and retries the request while the tx is only
|
|
612
|
+
> `confirmed` gets a `402 "payment not yet confirmed; finalize it and retry"`.
|
|
613
|
+
> Poll the signature to `finalized` before the retry.
|
|
614
|
+
|
|
595
615
|
## Errors
|
|
596
616
|
|
|
597
617
|
`ResolveError` carries a `code` a UI can branch on:
|
|
@@ -623,7 +643,7 @@ that is a homograph vector (`@аlice` with a Cyrillic `а` renders identically t
|
|
|
623
643
|
createResolver(config: ResolverConfig): Resolver
|
|
624
644
|
|
|
625
645
|
interface ResolverConfig {
|
|
626
|
-
rpcUrl: string; // any X1 RPC (
|
|
646
|
+
rpcUrl: string; // any X1 RPC (mainnet for @handles; testnet works the same)
|
|
627
647
|
wasm: WasmResolver; // required — derivation runs in WASM, no JS fallback
|
|
628
648
|
fetchImpl?: typeof fetch; // custom transport (tests, proxies)
|
|
629
649
|
cacheTtlMs?: number; // default 30_000; 0 disables (resolve() only)
|
package/dist/accounts.d.ts
CHANGED
|
@@ -9,10 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type Verification } from "./types.js";
|
|
11
11
|
import type { WasmResolver } from "./wasm.js";
|
|
12
|
-
|
|
13
|
-
* is used on mainnet once deployed. Override via `handleProgramId` to point at
|
|
14
|
-
* a different deployment. */
|
|
15
|
-
export declare const DEFAULT_HANDLE_PROGRAM = "8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P";
|
|
12
|
+
export declare const DEFAULT_HANDLE_PROGRAM: string;
|
|
16
13
|
export declare const TOKEN_ACCOUNT_MIN_LEN = 72;
|
|
17
14
|
export declare const SPL_TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
18
15
|
/** The fields of a `Handle` account readers need. */
|
|
@@ -34,6 +31,16 @@ export interface ParsedHandle {
|
|
|
34
31
|
* of) the `registeredAt` epoch rule — see docs/record-trust.md.
|
|
35
32
|
*/
|
|
36
33
|
readonly recordsClearedAt: bigint;
|
|
34
|
+
/**
|
|
35
|
+
* `Handle.holder_epoch` (0 if absent/never bumped) — H5 / PR #466. Bumped to
|
|
36
|
+
* `now + 1` ONLY by `buy_handle`'s tokenized branch (a NATIVE pNFT ownership
|
|
37
|
+
* change); an EXTERNAL marketplace trade of the pNFT does NOT bump it. Read
|
|
38
|
+
* from the fixed raw byte offset 255 (discriminator included); an account
|
|
39
|
+
* shorter than 263 bytes reads as 0. The on-chain attestation gate requires
|
|
40
|
+
* `attested_at >= max(registered_at, holder_epoch)`, so it is a THIRD
|
|
41
|
+
* independent staleness anchor for attestations — see attestation.ts.
|
|
42
|
+
*/
|
|
43
|
+
readonly holderEpoch: bigint;
|
|
37
44
|
/** Set when the handle is tokenized (NFT extension present). */
|
|
38
45
|
readonly nftMint: Uint8Array | null;
|
|
39
46
|
}
|
package/dist/accounts.js
CHANGED
|
@@ -9,10 +9,20 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { ResolveError } from "./types.js";
|
|
11
11
|
import { encodeBase58 } from "./base58.js";
|
|
12
|
-
/** The @handle registry program on X1
|
|
13
|
-
*
|
|
14
|
-
* a
|
|
15
|
-
|
|
12
|
+
/** The @handle registry program on X1 — the TESTNET deployment, which is the
|
|
13
|
+
* default on every surface today. Deployed with the same keypair on mainnet,
|
|
14
|
+
* so the id is unchanged there by default; a future mainnet-only deployment
|
|
15
|
+
* can be pointed at per-call via `handleProgramId`, or globally (for Node /
|
|
16
|
+
* server consumers that construct the resolver without an explicit id) by
|
|
17
|
+
* setting `X1_HANDLE_PROGRAM_ID` in the environment.
|
|
18
|
+
*
|
|
19
|
+
* Read through `globalThis.process` (not a bare `process.env`) so this still
|
|
20
|
+
* compiles in the @types/node-free SDK build and is simply `undefined` in the
|
|
21
|
+
* browser — identical testnet behaviour everywhere until the env var is set at
|
|
22
|
+
* cutover. Same pattern as `signin.ts`. */
|
|
23
|
+
const TESTNET_HANDLE_PROGRAM = "8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P";
|
|
24
|
+
const ENV_HANDLE_PROGRAM = globalThis.process?.env?.X1_HANDLE_PROGRAM_ID;
|
|
25
|
+
export const DEFAULT_HANDLE_PROGRAM = ENV_HANDLE_PROGRAM || TESTNET_HANDLE_PROGRAM;
|
|
16
26
|
// Handle account layout, mirrored from the on-chain program:
|
|
17
27
|
// discriminator(8) name(32) name_len(1) owner(32) ...
|
|
18
28
|
// `owner` is the address an UNTOKENIZED handle resolves to. Once the handle
|
|
@@ -32,6 +42,16 @@ const HANDLE_BASE_LEN = 8 + 149;
|
|
|
32
42
|
// leniently — a short or zeroed account means "never cleared" (0), same
|
|
33
43
|
// convention as every other Handle extension region.
|
|
34
44
|
const RECORDS_CLEARED_EXT_OFFSET = 229;
|
|
45
|
+
// `holder_epoch` extension (`Handle::HOLDER_EPOCH_EXT_OFFSET`, H5 / PR #466):
|
|
46
|
+
// an i64 LE bumped (to now+1) ONLY by `buy_handle`'s tokenized branch — a
|
|
47
|
+
// NATIVE pNFT ownership change. External (off-platform, e.g. Magic Eden) pNFT
|
|
48
|
+
// trades do NOT bump it. The on-chain attestation gate now requires
|
|
49
|
+
// `attested_at >= max(registered_at, holder_epoch)`, so it is a THIRD staleness
|
|
50
|
+
// anchor alongside `registered_at` and `records_cleared_at`. KNOWN_EXT_END
|
|
51
|
+
// moved 255 -> 263 on-chain. Read leniently at the FIXED raw byte offset 255
|
|
52
|
+
// (discriminator included): an account SHORTER than 263 bytes never grew this
|
|
53
|
+
// region → read it as 0, the same convention as every other Handle extension.
|
|
54
|
+
const HOLDER_EPOCH_EXT_OFFSET = 255;
|
|
35
55
|
// SPL Token account: mint(32) | owner(32) | amount(u64 LE, 8) | ...
|
|
36
56
|
export const TOKEN_ACCOUNT_MIN_LEN = 72;
|
|
37
57
|
// SPL Token mint: mint_authority COption(4+32) | supply(u64 LE, 8) @36 | ...
|
|
@@ -89,7 +109,8 @@ export function parseHandleAccount(data) {
|
|
|
89
109
|
? data.slice(HANDLE_BASE_LEN + 1, HANDLE_BASE_LEN + 33)
|
|
90
110
|
: null;
|
|
91
111
|
const recordsClearedAt = data.length >= RECORDS_CLEARED_EXT_OFFSET + 8 ? readI64(data, RECORDS_CLEARED_EXT_OFFSET) : 0n;
|
|
92
|
-
|
|
112
|
+
const holderEpoch = data.length >= HOLDER_EPOCH_EXT_OFFSET + 8 ? readI64(data, HOLDER_EPOCH_EXT_OFFSET) : 0n;
|
|
113
|
+
return { name, owner, registeredAt, recordsClearedAt, holderEpoch, nftMint };
|
|
93
114
|
}
|
|
94
115
|
/** Build the RPC plumbing every reader in this package shares. */
|
|
95
116
|
export function makeAccountReader(rpcUrl, fetchImpl) {
|
package/dist/agent.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* WP #8133. `AgentVerificationLevel` still names level 2 so every surface
|
|
24
24
|
* that reports a level shares one vocabulary.
|
|
25
25
|
*/
|
|
26
|
-
import { X1_TESTNET_NETWORK } from "./x402.js";
|
|
26
|
+
import { X1_TESTNET_NETWORK, X1_MAINNET_NETWORK } from "./x402.js";
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
// Well-known text-record keys
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
@@ -188,11 +188,11 @@ export function x402AcceptsFromManifest(manifest, opts) {
|
|
|
188
188
|
if (x402Endpoints.length === 0)
|
|
189
189
|
return [];
|
|
190
190
|
const x402 = manifest.payment?.x402;
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
const network = x402?.network ?? (manifest.x1id.network === "testnet" ? X1_TESTNET_NETWORK :
|
|
191
|
+
// The verified CAIP-30 x402 network ids (see x402.ts). A manifest's own
|
|
192
|
+
// explicit `x402.network` wins; otherwise map its declared cluster to the
|
|
193
|
+
// matching id — "testnet" → X1_TESTNET_NETWORK, anything else (mainnet, the
|
|
194
|
+
// production default) → X1_MAINNET_NETWORK.
|
|
195
|
+
const network = x402?.network ?? (manifest.x1id.network === "testnet" ? X1_TESTNET_NETWORK : X1_MAINNET_NETWORK);
|
|
196
196
|
const payTo = opts?.payTo ?? manifest.payment?.addresses?.[0]?.address;
|
|
197
197
|
return x402Endpoints.map((e) => {
|
|
198
198
|
const hint = {
|
package/dist/attestation.d.ts
CHANGED
|
@@ -39,17 +39,22 @@
|
|
|
39
39
|
* re-register lands the new registration at the SAME pubkey, and the
|
|
40
40
|
* previous owner's attestation is physically attached to the new owner's
|
|
41
41
|
* name with no action by anyone. The mandatory read-side rule
|
|
42
|
-
* (docs/record-trust.md, the universal rule)
|
|
42
|
+
* (docs/record-trust.md, the universal rule), now anchored to the LATER of
|
|
43
|
+
* two native-ownership epochs (H5 / PR #466 added `holder_epoch`):
|
|
43
44
|
*
|
|
44
|
-
* attestation.attested_at >= handle.registered_at
|
|
45
|
+
* attestation.attested_at >= max(handle.registered_at, handle.holder_epoch)
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
47
|
+
* `holder_epoch` is bumped (to now+1) by `buy_handle`'s tokenized branch — a
|
|
48
|
+
* NATIVE pNFT sale — so a native trade now DOES move the epoch and invalidate
|
|
49
|
+
* the seller's attestation. An attestation that fails the rule belongs to a
|
|
50
|
+
* previous, unrelated owner and must never be rendered as verifying the
|
|
51
|
+
* current one. Like `records.ts`, there is deliberately no way to decode or
|
|
52
|
+
* fetch an attestation through this module without the handle's
|
|
53
|
+
* `registered_at` in hand. (The remaining blind spot — shared with
|
|
54
|
+
* `Handle.owner`/`Primary`/`RecordDelegate`: an EXTERNAL, off-platform
|
|
55
|
+
* bearer-NFT trade bumps no epoch, so the attestation keeps reading live
|
|
56
|
+
* until the attestor re-reviews or revokes — is what the separate
|
|
57
|
+
* `holderMismatch` overlay catches.)
|
|
53
58
|
*
|
|
54
59
|
* # `records_cleared_at` (#8139) — the SECOND, independent staleness rule
|
|
55
60
|
*
|
|
@@ -58,7 +63,10 @@
|
|
|
58
63
|
* explicitly alongside records/text-records as sharing this epoch rule. The
|
|
59
64
|
* same functions therefore also demand the handle's `recordsClearedAt` (0 if
|
|
60
65
|
* never cleared, from `ParsedHandle`), and an attestation is `stale` when
|
|
61
|
-
* EITHER `attested_at < registeredAt` OR
|
|
66
|
+
* EITHER `attested_at < max(registeredAt, holderEpoch)` OR
|
|
67
|
+
* `attested_at < recordsClearedAt` — where `holderEpoch` (H5 / PR #466, also
|
|
68
|
+
* from `ParsedHandle`, 0 if absent) folds the last native tokenized-buy epoch
|
|
69
|
+
* into the ownership anchor.
|
|
62
70
|
*/
|
|
63
71
|
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
64
72
|
import type { RpcFn } from "./accounts.js";
|
|
@@ -175,12 +183,31 @@ export interface HandleAttestation {
|
|
|
175
183
|
* not re-checked against the current `AttestorConfig`. */
|
|
176
184
|
readonly attestor: string;
|
|
177
185
|
/**
|
|
178
|
-
* The rule docs/record-trust.md mandates: this attestation only vouches
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
186
|
+
* The rule docs/record-trust.md mandates: this attestation only vouches for
|
|
187
|
+
* the current owner if `attested_at >= max(handle.registered_at,
|
|
188
|
+
* handle.holder_epoch)` AND `attested_at >= handle.records_cleared_at` (H5 /
|
|
189
|
+
* PR #466 added the `holder_epoch` term — the last native tokenized-buy
|
|
190
|
+
* epoch). A stale attestation belongs to a previous, unrelated owner of the
|
|
191
|
+
* same name and must never be rendered as verifying the current one.
|
|
182
192
|
*/
|
|
183
193
|
readonly stale: boolean;
|
|
194
|
+
/**
|
|
195
|
+
* H5 (the inherited-badge blind spot) — the OTHER, holder-level reason an
|
|
196
|
+
* attestation must not be rendered, independent of `stale`. An attestation
|
|
197
|
+
* records no subject wallet, only `attestedAt`; its epoch is anchored to
|
|
198
|
+
* `Handle.owner` (the owner stamped alongside `registered_at` at the last
|
|
199
|
+
* native ownership change). For a TOKENIZED handle, the real authority is the
|
|
200
|
+
* pNFT holder, which can diverge from `Handle.owner` with NO on-chain epoch
|
|
201
|
+
* bump (a bearer-NFT trade — native or external — writes no `registered_at`,
|
|
202
|
+
* see this struct's doc). When the current holder is no longer the mint-time
|
|
203
|
+
* `Handle.owner` the attestation was issued under, the attestation may have
|
|
204
|
+
* been inherited through a trade rather than earned, so it must NOT verify the
|
|
205
|
+
* current holder. `decodeAttestation` cannot know this (it sees no holder), so
|
|
206
|
+
* it always sets `false`; apply {@link markHolderMismatch} with the caller's
|
|
207
|
+
* holder-vs-owner comparison to set it. Mirrors tools/api's `holder_mismatch`
|
|
208
|
+
* (`build_verifications`). ALWAYS `false` for an untokenized handle.
|
|
209
|
+
*/
|
|
210
|
+
readonly holderMismatch: boolean;
|
|
184
211
|
}
|
|
185
212
|
/**
|
|
186
213
|
* Decode one Attestation account.
|
|
@@ -188,22 +215,55 @@ export interface HandleAttestation {
|
|
|
188
215
|
* `registeredAt` is the owning Handle's `registered_at`, decoded by the
|
|
189
216
|
* caller from the Handle account — it decides `stale`. `recordsClearedAt` is
|
|
190
217
|
* that same Handle's `recordsClearedAt` (0 if never cleared, #8139), a
|
|
191
|
-
* SECOND independent staleness anchor.
|
|
192
|
-
*
|
|
218
|
+
* SECOND independent staleness anchor. `holderEpoch` (H5 / PR #466, 0 if
|
|
219
|
+
* absent) is the THIRD: the effective ownership epoch is
|
|
220
|
+
* `max(registeredAt, holderEpoch)`, so `stale` is `attestedAt <
|
|
221
|
+
* max(registeredAt, holderEpoch) || attestedAt < recordsClearedAt` — mirroring
|
|
222
|
+
* the on-chain gate `attested_at >= max(registered_at, holder_epoch)`.
|
|
223
|
+
* `holderEpoch` defaults to `0n` for backward compatibility (a caller that has
|
|
224
|
+
* not read it degrades to the pre-H5 `>= registered_at` rule). There is
|
|
225
|
+
* intentionally no overload without `registeredAt`/`recordsClearedAt` (see the
|
|
226
|
+
* module docs).
|
|
193
227
|
*
|
|
194
228
|
* Returns null for anything that is not an Attestation: wrong length or
|
|
195
229
|
* wrong discriminator.
|
|
196
230
|
*/
|
|
197
|
-
export declare function decodeAttestation(raw: Uint8Array, account: string, registeredAt: bigint, recordsClearedAt: bigint): HandleAttestation | null;
|
|
198
|
-
/** The attestations that vouch for the CURRENT
|
|
231
|
+
export declare function decodeAttestation(raw: Uint8Array, account: string, registeredAt: bigint, recordsClearedAt: bigint, holderEpoch?: bigint): HandleAttestation | null;
|
|
232
|
+
/** The attestations that vouch for the CURRENT holder — `stale` ones (previous
|
|
233
|
+
* epoch) AND `holderMismatch` ones (H5: inherited by a different pNFT holder)
|
|
199
234
|
* excluded. This is the list to judge verification from. */
|
|
200
235
|
export declare function liveAttestations(attestations: readonly HandleAttestation[]): HandleAttestation[];
|
|
201
236
|
/**
|
|
202
237
|
* The #7145 verified rule: a handle is verified when at least ONE non-stale
|
|
203
238
|
* attestation of EITHER kind exists (a single strong signal is enough; the
|
|
204
|
-
* surviving `kindName`s say which signals proved it).
|
|
239
|
+
* surviving `kindName`s say which signals proved it). An attestation flagged
|
|
240
|
+
* {@link HandleAttestation.holderMismatch} (H5 — the pNFT moved to a holder it
|
|
241
|
+
* was not issued to) is excluded exactly like a stale one.
|
|
205
242
|
*/
|
|
206
243
|
export declare function isHandleVerified(attestations: readonly HandleAttestation[]): boolean;
|
|
244
|
+
/**
|
|
245
|
+
* H5 — the inherited-badge overlay. Returns `attestations` with
|
|
246
|
+
* {@link HandleAttestation.holderMismatch} forced `true` on every entry when a
|
|
247
|
+
* handle is tokenized AND its pNFT has moved away from the mint-time
|
|
248
|
+
* `Handle.owner` the attestation was issued under (`holderMatchesOwner ===
|
|
249
|
+
* false`); otherwise the list is returned unchanged. An attestation records no
|
|
250
|
+
* subject wallet, so this is the one honest, off-chain-derivable signal: the
|
|
251
|
+
* current bearer is compared to `Handle.owner` (the owner stamped alongside
|
|
252
|
+
* `registered_at`), not to the holder as-of each `attestedAt`.
|
|
253
|
+
*
|
|
254
|
+
* Mirrors tools/api's `build_verifications` exactly (same `tokenized &&
|
|
255
|
+
* !holder_matches_owner` condition, same force-not-verified result). Pure and
|
|
256
|
+
* synchronous: the caller resolves the current holder (e.g. via
|
|
257
|
+
* {@link import("./accounts.js").nftHolder} / `getTokenLargestAccounts`) and
|
|
258
|
+
* compares it to the handle's `owner` ({@link
|
|
259
|
+
* import("./accounts.js").parseHandleAccount}) to produce `holderMatchesOwner`.
|
|
260
|
+
*
|
|
261
|
+
* Interim limitation (shared with the resolver): it cannot read the NFT's
|
|
262
|
+
* transfer history, only its current bearer, so a legitimate re-attestation of a
|
|
263
|
+
* new bearer is suppressed until the on-chain H5 ownership-epoch bump lands —
|
|
264
|
+
* a false suppression, never a false assertion (the honest, conservative side).
|
|
265
|
+
*/
|
|
266
|
+
export declare function markHolderMismatch(attestations: readonly HandleAttestation[], tokenized: boolean, holderMatchesOwner: boolean): HandleAttestation[];
|
|
207
267
|
/**
|
|
208
268
|
* Fetch every Attestation account of a handle — one `getProgramAccounts`
|
|
209
269
|
* call, filtered by the RPC on size (114), the Attestation discriminator at
|
|
@@ -216,11 +276,15 @@ export declare function isHandleVerified(attestations: readonly HandleAttestatio
|
|
|
216
276
|
* account the caller already has — the staleness rule needs it, and there
|
|
217
277
|
* is no variant of this function without it. `recordsClearedAt` is that same
|
|
218
278
|
* Handle's `recordsClearedAt` (0 if never cleared, #8139) — pass it through.
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
279
|
+
* `holderEpoch` is that same Handle's `holderEpoch` (0 if absent, H5 / PR
|
|
280
|
+
* #466); pass it through so the effective ownership epoch is
|
|
281
|
+
* `max(registeredAt, holderEpoch)`. It defaults to `0n` for backward
|
|
282
|
+
* compatibility. Every attestation is returned, stale ones flagged, so an
|
|
283
|
+
* owner surface can show what a previous registration left behind; anything
|
|
284
|
+
* that renders a verified badge takes {@link isHandleVerified} /
|
|
285
|
+
* {@link liveAttestations}.
|
|
222
286
|
*/
|
|
223
|
-
export declare function fetchAttestations(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint): Promise<HandleAttestation[]>;
|
|
287
|
+
export declare function fetchAttestations(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint, holderEpoch?: bigint): Promise<HandleAttestation[]>;
|
|
224
288
|
export interface SetAttestorParams {
|
|
225
289
|
/** The registry program id. */
|
|
226
290
|
readonly programId: AddressLike;
|
package/dist/attestation.js
CHANGED
|
@@ -39,17 +39,22 @@
|
|
|
39
39
|
* re-register lands the new registration at the SAME pubkey, and the
|
|
40
40
|
* previous owner's attestation is physically attached to the new owner's
|
|
41
41
|
* name with no action by anyone. The mandatory read-side rule
|
|
42
|
-
* (docs/record-trust.md, the universal rule)
|
|
42
|
+
* (docs/record-trust.md, the universal rule), now anchored to the LATER of
|
|
43
|
+
* two native-ownership epochs (H5 / PR #466 added `holder_epoch`):
|
|
43
44
|
*
|
|
44
|
-
* attestation.attested_at >= handle.registered_at
|
|
45
|
+
* attestation.attested_at >= max(handle.registered_at, handle.holder_epoch)
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
47
|
+
* `holder_epoch` is bumped (to now+1) by `buy_handle`'s tokenized branch — a
|
|
48
|
+
* NATIVE pNFT sale — so a native trade now DOES move the epoch and invalidate
|
|
49
|
+
* the seller's attestation. An attestation that fails the rule belongs to a
|
|
50
|
+
* previous, unrelated owner and must never be rendered as verifying the
|
|
51
|
+
* current one. Like `records.ts`, there is deliberately no way to decode or
|
|
52
|
+
* fetch an attestation through this module without the handle's
|
|
53
|
+
* `registered_at` in hand. (The remaining blind spot — shared with
|
|
54
|
+
* `Handle.owner`/`Primary`/`RecordDelegate`: an EXTERNAL, off-platform
|
|
55
|
+
* bearer-NFT trade bumps no epoch, so the attestation keeps reading live
|
|
56
|
+
* until the attestor re-reviews or revokes — is what the separate
|
|
57
|
+
* `holderMismatch` overlay catches.)
|
|
53
58
|
*
|
|
54
59
|
* # `records_cleared_at` (#8139) — the SECOND, independent staleness rule
|
|
55
60
|
*
|
|
@@ -58,7 +63,10 @@
|
|
|
58
63
|
* explicitly alongside records/text-records as sharing this epoch rule. The
|
|
59
64
|
* same functions therefore also demand the handle's `recordsClearedAt` (0 if
|
|
60
65
|
* never cleared, from `ParsedHandle`), and an attestation is `stale` when
|
|
61
|
-
* EITHER `attested_at < registeredAt` OR
|
|
66
|
+
* EITHER `attested_at < max(registeredAt, holderEpoch)` OR
|
|
67
|
+
* `attested_at < recordsClearedAt` — where `holderEpoch` (H5 / PR #466, also
|
|
68
|
+
* from `ParsedHandle`, 0 if absent) folds the last native tokenized-buy epoch
|
|
69
|
+
* into the ownership anchor.
|
|
62
70
|
*/
|
|
63
71
|
import { encodeBase58 } from "./base58.js";
|
|
64
72
|
import { decodeBase58_32 } from "./base58.js";
|
|
@@ -238,13 +246,20 @@ export function textRecordKeyFor(kind) {
|
|
|
238
246
|
* `registeredAt` is the owning Handle's `registered_at`, decoded by the
|
|
239
247
|
* caller from the Handle account — it decides `stale`. `recordsClearedAt` is
|
|
240
248
|
* that same Handle's `recordsClearedAt` (0 if never cleared, #8139), a
|
|
241
|
-
* SECOND independent staleness anchor.
|
|
242
|
-
*
|
|
249
|
+
* SECOND independent staleness anchor. `holderEpoch` (H5 / PR #466, 0 if
|
|
250
|
+
* absent) is the THIRD: the effective ownership epoch is
|
|
251
|
+
* `max(registeredAt, holderEpoch)`, so `stale` is `attestedAt <
|
|
252
|
+
* max(registeredAt, holderEpoch) || attestedAt < recordsClearedAt` — mirroring
|
|
253
|
+
* the on-chain gate `attested_at >= max(registered_at, holder_epoch)`.
|
|
254
|
+
* `holderEpoch` defaults to `0n` for backward compatibility (a caller that has
|
|
255
|
+
* not read it degrades to the pre-H5 `>= registered_at` rule). There is
|
|
256
|
+
* intentionally no overload without `registeredAt`/`recordsClearedAt` (see the
|
|
257
|
+
* module docs).
|
|
243
258
|
*
|
|
244
259
|
* Returns null for anything that is not an Attestation: wrong length or
|
|
245
260
|
* wrong discriminator.
|
|
246
261
|
*/
|
|
247
|
-
export function decodeAttestation(raw, account, registeredAt, recordsClearedAt) {
|
|
262
|
+
export function decodeAttestation(raw, account, registeredAt, recordsClearedAt, holderEpoch = 0n) {
|
|
248
263
|
if (raw.length !== ATTESTATION_LEN)
|
|
249
264
|
return null;
|
|
250
265
|
for (let i = 0; i < 8; i++) {
|
|
@@ -262,6 +277,14 @@ export function decodeAttestation(raw, account, registeredAt, recordsClearedAt)
|
|
|
262
277
|
const attestedAt = dv.getBigInt64(o, true);
|
|
263
278
|
o += 8;
|
|
264
279
|
const attestor = encodeBase58(raw.slice(o, o + 32));
|
|
280
|
+
// H5 / PR #466: the ownership epoch an attestation must post-date is the
|
|
281
|
+
// LATER of `registered_at` (last native register/sale) and `holder_epoch`
|
|
282
|
+
// (last native tokenized buy). Mirrors the on-chain gate exactly —
|
|
283
|
+
// `attested_at >= max(registered_at, holder_epoch)` — and staleness stays
|
|
284
|
+
// INCLUSIVE at the boundary so the dangerous failure (a false "verified")
|
|
285
|
+
// can never occur: an attestation is live only if `attested_at >=` the
|
|
286
|
+
// effective epoch. `records_cleared_at` is a second, independent anchor.
|
|
287
|
+
const ownershipEpoch = registeredAt > holderEpoch ? registeredAt : holderEpoch;
|
|
265
288
|
return {
|
|
266
289
|
account,
|
|
267
290
|
handle,
|
|
@@ -270,21 +293,53 @@ export function decodeAttestation(raw, account, registeredAt, recordsClearedAt)
|
|
|
270
293
|
evidenceHash,
|
|
271
294
|
attestedAt,
|
|
272
295
|
attestor,
|
|
273
|
-
stale: attestedAt <
|
|
296
|
+
stale: attestedAt < ownershipEpoch || attestedAt < recordsClearedAt,
|
|
297
|
+
// The decoder sees no holder — only epoch staleness is knowable here.
|
|
298
|
+
// H5 holder-mismatch is overlaid later via markHolderMismatch.
|
|
299
|
+
holderMismatch: false,
|
|
274
300
|
};
|
|
275
301
|
}
|
|
276
|
-
/** The attestations that vouch for the CURRENT
|
|
302
|
+
/** The attestations that vouch for the CURRENT holder — `stale` ones (previous
|
|
303
|
+
* epoch) AND `holderMismatch` ones (H5: inherited by a different pNFT holder)
|
|
277
304
|
* excluded. This is the list to judge verification from. */
|
|
278
305
|
export function liveAttestations(attestations) {
|
|
279
|
-
return attestations.filter((a) => !a.stale);
|
|
306
|
+
return attestations.filter((a) => !a.stale && !a.holderMismatch);
|
|
280
307
|
}
|
|
281
308
|
/**
|
|
282
309
|
* The #7145 verified rule: a handle is verified when at least ONE non-stale
|
|
283
310
|
* attestation of EITHER kind exists (a single strong signal is enough; the
|
|
284
|
-
* surviving `kindName`s say which signals proved it).
|
|
311
|
+
* surviving `kindName`s say which signals proved it). An attestation flagged
|
|
312
|
+
* {@link HandleAttestation.holderMismatch} (H5 — the pNFT moved to a holder it
|
|
313
|
+
* was not issued to) is excluded exactly like a stale one.
|
|
285
314
|
*/
|
|
286
315
|
export function isHandleVerified(attestations) {
|
|
287
|
-
return attestations.some((a) => !a.stale);
|
|
316
|
+
return attestations.some((a) => !a.stale && !a.holderMismatch);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* H5 — the inherited-badge overlay. Returns `attestations` with
|
|
320
|
+
* {@link HandleAttestation.holderMismatch} forced `true` on every entry when a
|
|
321
|
+
* handle is tokenized AND its pNFT has moved away from the mint-time
|
|
322
|
+
* `Handle.owner` the attestation was issued under (`holderMatchesOwner ===
|
|
323
|
+
* false`); otherwise the list is returned unchanged. An attestation records no
|
|
324
|
+
* subject wallet, so this is the one honest, off-chain-derivable signal: the
|
|
325
|
+
* current bearer is compared to `Handle.owner` (the owner stamped alongside
|
|
326
|
+
* `registered_at`), not to the holder as-of each `attestedAt`.
|
|
327
|
+
*
|
|
328
|
+
* Mirrors tools/api's `build_verifications` exactly (same `tokenized &&
|
|
329
|
+
* !holder_matches_owner` condition, same force-not-verified result). Pure and
|
|
330
|
+
* synchronous: the caller resolves the current holder (e.g. via
|
|
331
|
+
* {@link import("./accounts.js").nftHolder} / `getTokenLargestAccounts`) and
|
|
332
|
+
* compares it to the handle's `owner` ({@link
|
|
333
|
+
* import("./accounts.js").parseHandleAccount}) to produce `holderMatchesOwner`.
|
|
334
|
+
*
|
|
335
|
+
* Interim limitation (shared with the resolver): it cannot read the NFT's
|
|
336
|
+
* transfer history, only its current bearer, so a legitimate re-attestation of a
|
|
337
|
+
* new bearer is suppressed until the on-chain H5 ownership-epoch bump lands —
|
|
338
|
+
* a false suppression, never a false assertion (the honest, conservative side).
|
|
339
|
+
*/
|
|
340
|
+
export function markHolderMismatch(attestations, tokenized, holderMatchesOwner) {
|
|
341
|
+
const mismatch = tokenized && !holderMatchesOwner;
|
|
342
|
+
return attestations.map((a) => (mismatch ? { ...a, holderMismatch: true } : a));
|
|
288
343
|
}
|
|
289
344
|
/**
|
|
290
345
|
* Fetch every Attestation account of a handle — one `getProgramAccounts`
|
|
@@ -298,11 +353,15 @@ export function isHandleVerified(attestations) {
|
|
|
298
353
|
* account the caller already has — the staleness rule needs it, and there
|
|
299
354
|
* is no variant of this function without it. `recordsClearedAt` is that same
|
|
300
355
|
* Handle's `recordsClearedAt` (0 if never cleared, #8139) — pass it through.
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
356
|
+
* `holderEpoch` is that same Handle's `holderEpoch` (0 if absent, H5 / PR
|
|
357
|
+
* #466); pass it through so the effective ownership epoch is
|
|
358
|
+
* `max(registeredAt, holderEpoch)`. It defaults to `0n` for backward
|
|
359
|
+
* compatibility. Every attestation is returned, stale ones flagged, so an
|
|
360
|
+
* owner surface can show what a previous registration left behind; anything
|
|
361
|
+
* that renders a verified badge takes {@link isHandleVerified} /
|
|
362
|
+
* {@link liveAttestations}.
|
|
304
363
|
*/
|
|
305
|
-
export async function fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt) {
|
|
364
|
+
export async function fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt, holderEpoch = 0n) {
|
|
306
365
|
const res = (await rpc("getProgramAccounts", [
|
|
307
366
|
programId,
|
|
308
367
|
{
|
|
@@ -321,7 +380,7 @@ export async function fetchAttestations(rpc, programId, handleAccount, registere
|
|
|
321
380
|
const raw = new Uint8Array(bin.length);
|
|
322
381
|
for (let i = 0; i < bin.length; i++)
|
|
323
382
|
raw[i] = bin.charCodeAt(i);
|
|
324
|
-
const decoded = decodeAttestation(raw, a.pubkey, registeredAt, recordsClearedAt);
|
|
383
|
+
const decoded = decodeAttestation(raw, a.pubkey, registeredAt, recordsClearedAt, holderEpoch);
|
|
325
384
|
// Defence in depth: the memcmp filter should guarantee the handle
|
|
326
385
|
// match, but a wrong offset would silently attribute someone else's
|
|
327
386
|
// attestation to this handle. A malformed account is skipped, not fatal.
|
package/dist/domainProof.d.ts
CHANGED
|
@@ -154,13 +154,16 @@ export interface VerifyDomainAttestationResult {
|
|
|
154
154
|
* `registeredAt` / `recordsClearedAt` are the owning Handle's fields the caller
|
|
155
155
|
* already decoded (see {@link import("./accounts.js").parseHandleAccount}); they
|
|
156
156
|
* are MANDATORY here for the same reason every attestation read demands them —
|
|
157
|
-
* the staleness rule (`attested_at >= registered_at`, and
|
|
158
|
-
* decides whether an attestation vouches for the
|
|
157
|
+
* the staleness rule (`attested_at >= max(registered_at, holder_epoch)`, and
|
|
158
|
+
* `>= records_cleared_at`) decides whether an attestation vouches for the
|
|
159
|
+
* *current* owner at all. `holderEpoch` (H5 / PR #466, also from the decoded
|
|
160
|
+
* Handle, 0 if absent) is the optional trailing arg that supplies that third
|
|
161
|
+
* anchor; it defaults to `0n`. The
|
|
159
162
|
* two halves are evaluated independently, so a caller can see which one holds:
|
|
160
163
|
* a chain-read failure does not suppress the DNS answer, and vice-versa.
|
|
161
164
|
*
|
|
162
165
|
* Never throws: a failed chain read degrades to `attested: false` with
|
|
163
166
|
* `reason: "attestation-error"`, mirroring `signin.ts`'s best-effort summary.
|
|
164
167
|
*/
|
|
165
|
-
export declare function verifyDomainAttestation(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint, domain: string, expectedWallet: string, opts?: DomainProofOptions): Promise<VerifyDomainAttestationResult>;
|
|
168
|
+
export declare function verifyDomainAttestation(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint, domain: string, expectedWallet: string, opts?: DomainProofOptions, holderEpoch?: bigint): Promise<VerifyDomainAttestationResult>;
|
|
166
169
|
export {};
|
package/dist/domainProof.js
CHANGED
|
@@ -244,21 +244,24 @@ export async function verifyDomainBinding(domain, expectedWallet, opts) {
|
|
|
244
244
|
* `registeredAt` / `recordsClearedAt` are the owning Handle's fields the caller
|
|
245
245
|
* already decoded (see {@link import("./accounts.js").parseHandleAccount}); they
|
|
246
246
|
* are MANDATORY here for the same reason every attestation read demands them —
|
|
247
|
-
* the staleness rule (`attested_at >= registered_at`, and
|
|
248
|
-
* decides whether an attestation vouches for the
|
|
247
|
+
* the staleness rule (`attested_at >= max(registered_at, holder_epoch)`, and
|
|
248
|
+
* `>= records_cleared_at`) decides whether an attestation vouches for the
|
|
249
|
+
* *current* owner at all. `holderEpoch` (H5 / PR #466, also from the decoded
|
|
250
|
+
* Handle, 0 if absent) is the optional trailing arg that supplies that third
|
|
251
|
+
* anchor; it defaults to `0n`. The
|
|
249
252
|
* two halves are evaluated independently, so a caller can see which one holds:
|
|
250
253
|
* a chain-read failure does not suppress the DNS answer, and vice-versa.
|
|
251
254
|
*
|
|
252
255
|
* Never throws: a failed chain read degrades to `attested: false` with
|
|
253
256
|
* `reason: "attestation-error"`, mirroring `signin.ts`'s best-effort summary.
|
|
254
257
|
*/
|
|
255
|
-
export async function verifyDomainAttestation(rpc, programId, handleAccount, registeredAt, recordsClearedAt, domain, expectedWallet, opts) {
|
|
258
|
+
export async function verifyDomainAttestation(rpc, programId, handleAccount, registeredAt, recordsClearedAt, domain, expectedWallet, opts, holderEpoch = 0n) {
|
|
256
259
|
// Half 1 (on-chain): a non-stale dns attestation for the current owner. A
|
|
257
260
|
// read failure must not sink the DNS half — degrade, never throw.
|
|
258
261
|
let attested = false;
|
|
259
262
|
let attestationReason;
|
|
260
263
|
try {
|
|
261
|
-
const attestations = await fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt);
|
|
264
|
+
const attestations = await fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt, holderEpoch);
|
|
262
265
|
attested = attestationsPassGate(attestations, { kind: GateKind.dns });
|
|
263
266
|
}
|
|
264
267
|
catch {
|