@x1id/resolve 0.11.0 → 0.12.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 CHANGED
@@ -274,6 +274,65 @@ for (const a of liveAttestations(atts)) {
274
274
  integrator joining a live attestation to its paired text record reads the same
275
275
  mapping the program, resolver and app badge use.
276
276
 
277
+ ## Attestation gating (allowlists)
278
+
279
+ Gate your app, Discord, mint, or airdrop by whether an `@handle` holds a
280
+ **verified on-chain attestation** — any signal, or a specific platform like
281
+ "verified GitHub." You reuse X1ID's blue-tick oracle as your allowlist instead of
282
+ running your own verification, and the staleness rule (an attestation only counts
283
+ for the *current* owner) is enforced for you.
284
+
285
+ ```ts
286
+ import {
287
+ handlePassesGate, attestationsPassGate, GateKind, type Gate,
288
+ } from "@x1id/resolve";
289
+
290
+ const anyVerified: Gate = "any-verified"; // any blue-tick
291
+ const githubOnly: Gate = { kind: GateKind.github }; // one platform
292
+ const xOrGithub: Gate = { anyOf: [GateKind.x, GateKind.github] };
293
+
294
+ // One-call fetch + check (handleAccount/registeredAt/recordsClearedAt come from
295
+ // the handle you resolved):
296
+ const { passed, attestations } = await handlePassesGate(
297
+ rpc, programId, handleAccount, registeredAt, recordsClearedAt, githubOnly,
298
+ );
299
+ if (!passed) throw new Error("Not allowed — verify your GitHub on X1ID first.");
300
+
301
+ // Already have the attestations? The check is pure + synchronous:
302
+ attestationsPassGate(attestations, xOrGithub); // boolean
303
+ ```
304
+
305
+ `GateKind`: `dns`, `social`, `x`, `discord`, `github`, `telegram`.
306
+
307
+ **On-chain gate (CPI):** another program can verify the same thing inside its own
308
+ transaction via the program-native `assert_attestation_gate` instruction — it
309
+ returns Ok when the gate passes and errors otherwise, so a CPI that errors fails
310
+ the caller's tx. Full call contract (discriminator, accounts, kind codes) is in
311
+ the program's `docs/attestation-gating.md`.
312
+
313
+ **Hosted gate + drop-in middleware (X1ID Gate):** if you'd rather not run your own
314
+ attestation reads, `requireGate()` calls the hosted decision service
315
+ (`POST /v1/gate/check`, api.x1id.io) and returns pass/fail. A tiny, framework-
316
+ agnostic **server** guard (keep your API key server-side):
317
+
318
+ ```ts
319
+ import { requireGate, GateKind } from "@x1id/resolve";
320
+
321
+ const gate = requireGate({ apiKey: process.env.X1ID_API_KEY! }, { kind: GateKind.github });
322
+
323
+ export async function POST(req: Request) { // any server runtime with fetch
324
+ const { handle } = await req.json();
325
+ const { allowed } = await gate({ handle }); // or gate({ wallet })
326
+ if (!allowed) return Response.json({ error: "verified GitHub required" }, { status: 403 });
327
+ // …proceed
328
+ }
329
+ ```
330
+
331
+ Fail-closed by default (re-throws if the decision can't be obtained; pass
332
+ `{ onError: "allow" }` to fail-open). `checkGate()` is the lower-level client for
333
+ the full decision; pass a stored policy's `gp_…` id as the policy to reuse a named
334
+ policy. The hosted check is keyed/metered — see `services/dev-api`'s README.
335
+
277
336
  ## What works today
278
337
 
279
338
  | | Status |
@@ -284,17 +343,19 @@ mapping the program, resolver and app badge use.
284
343
  | Per-chain ETH/BTC records | ✅ read with the staleness rule enforced structurally |
285
344
  | Control proofs (prove you control a `@handle`) | ✅ `createControlChallenge` / `verifyControlProof` — spec: `docs/control-proof.md` |
286
345
  | Records read (per-chain, staleness-enforced) | ✅ `fetchRecords` |
287
- | Subnames · gifts · integrator rev-share · name-lock · record-delegate · text records · attestations (write) | ✅ 0.3.0 — instruction builders; you sign & relay |
288
- | Registration / transfer / `set_primary` (write) | 🔜 roadmap — use [x1id.io](https://x1id.io) |
346
+ | Attestation gating (verified-only allowlists) | ✅ off-chain (`handlePassesGate`) + on-chain CPI (`assert_attestation_gate`) |
347
+ | Registration · lease-to-own · FORTI · commit-reveal · gift-voucher claim (write) | ✅ instruction builders; you sign & relay |
348
+ | Subnames · gifts · integrator rev-share · name-lock · record-delegate · text records · attestations (write) | ✅ instruction builders; you sign & relay |
349
+ | `transfer` / `set_primary` (write) | 🔜 roadmap — use [x1id.io](https://x1id.io) |
289
350
 
290
351
  The `@handle` registry program id is configurable (`handleProgramId`) and
291
352
  defaults to the canonical X1 deployment
292
353
  (`8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P`).
293
354
 
294
- ## Beyond resolve: the registry write surface (0.3.0)
355
+ ## Beyond resolve: the registry write surface
295
356
 
296
357
  `resolve()` / `reverse()` / `fetchRecords()` are read-only and need only an RPC.
297
- 0.3.0 also ships **instruction builders** for the on-chain registry. Each
358
+ The package also ships **instruction builders** for the on-chain registry. Each
298
359
  returns an unsigned `BuiltInstruction` (`{ programId, keys, data }`) — you
299
360
  assemble it into a transaction, **sign, and relay** yourself; the SDK never
300
361
  holds keys. Every discriminator and account order matches the deployed program.
@@ -328,7 +389,10 @@ const ix = buildCreateSubnameIx({
328
389
  ```
329
390
 
330
391
  **Register a handle** (pays whatever the on-chain ramp charges as of the
331
- landing slot — there is no client-supplied price):
392
+ landing slot — there is no client-supplied price). `buildRegisterIx` builds the
393
+ plain **name-only** `register`; the default **NFT-included** purchase
394
+ (`register_tokenized`, which mints the capability pNFT in the same transaction)
395
+ is driven by app.x1id.io today and has no standalone SDK builder yet:
332
396
 
333
397
  ```ts
334
398
  import { buildRegisterIx, fetchConfigTreasury, HandleType } from "@x1id/resolve";
@@ -369,6 +433,11 @@ Also shipped, same build → sign → relay shape:
369
433
  | Record-write delegation | `buildSetRecordDelegateIx`, `buildRevokeRecordDelegateIx` |
370
434
  | Typed text records (website / avatar / socials) | `buildCreateTextRecordIx`, `buildUpdateTextRecordIx`, `buildCloseTextRecordIx` |
371
435
  | Register a handle | `buildRegisterIx` (+ `fetchConfigTreasury`) |
436
+ | Lease-to-own | `buildRegisterLeasedIx`, `buildRenewLeaseIx`, `buildReclaimExpiredLeaseIx` |
437
+ | Register at a FORTI discount | `buildRegisterWithFortiIx` |
438
+ | Commit-reveal registration (front-run-safe) | `buildCommitIx`, `buildRegisterRevealedIx`, `buildCancelCommitmentIx` |
439
+ | Claim a gifted name (prepaid voucher) | `buildClaimVoucherIx`, `buildClaimVoucherTokenizedIx` |
440
+ | Attestation gating | `handlePassesGate`, `attestationsPassGate`, `GateKind` — see [Attestation gating](#attestation-gating-allowlists) |
372
441
  | Verify a non-SVM address record | `buildCreateRecordIx`, `buildVerifyRecordEthIx`, `buildVerifyRecordSvmIx`, `buildVerifyRecordBtcIx` (+ `buildRecordChallenge`, `splitEthSignature`, `splitBtcSignature`) — see [Verifying a non-SVM address record](#verifying-a-non-svm-address-record) |
373
442
  | Domain / social / per-platform attestations | `buildCreateAttestationIx` (kinds 0–5), `buildCloseAttestationIx` (+ `fetchAttestations`, `isHandleVerified`, `platformSlug`, `textRecordKeyFor`) — see [Per-platform verification](#per-platform-verification-verified-by-platform) |
374
443
  | Agent records (identity, manifest, x402 hints) | `validateManifest`, `agentVerificationLevel`, `x402AcceptsFromManifest`, `agentTextRecords` — see [Agent records](#agent-records) below |
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Admin-only `Config` setters — `set_tiers` / `set_nft_tiers` (#8398) and (as
3
+ * the on-chain admin batch lands) the rest of the AdminOnly config instructions.
4
+ *
5
+ * Hand-rolled like the rest of this package — no Anchor client, no
6
+ * `@solana/web3.js`. All of these share the `AdminOnly` account shape:
7
+ * admin (signer, NOT writable — these setters init nothing, so no rent)
8
+ * config (the ["config"] PDA, writable)
9
+ * and must be signed by `Config.admin`. PDAs are not derived here — derive
10
+ * `["config"]` with your runtime's `findProgramAddress`.
11
+ */
12
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
13
+ /** Anchor instruction discriminator: `sha256("global:set_tiers")[0..8]`. */
14
+ export declare const SET_TIERS_DISCRIMINATOR: Uint8Array;
15
+ /** Anchor instruction discriminator: `sha256("global:set_nft_tiers")[0..8]`. */
16
+ export declare const SET_NFT_TIERS_DISCRIMINATOR: Uint8Array;
17
+ /** The number of length-tiers in `Config.tier_lamports` / `nft_tier_lamports`.
18
+ * Tier index = `clamp(nameLen, 1, 10) - 1`, so tier[0] = 1-char … tier[9] = 10+. */
19
+ export declare const TIER_COUNT = 10;
20
+ export interface SetTiersParams {
21
+ readonly programId: AddressLike;
22
+ /** `Config.admin` — the ONLY key accepted (has_one = admin). Signs; not mut. */
23
+ readonly admin: AddressLike;
24
+ /** The `["config"]` PDA (writable). */
25
+ readonly config: AddressLike;
26
+ /** Exactly 10 per-length-tier base prices in lamports (tier[0]=1-char …
27
+ * tier[9]=10+). Accepts number or bigint; each must be a non-negative
28
+ * integer that fits u64. */
29
+ readonly tierLamports: ReadonlyArray<number | bigint>;
30
+ }
31
+ /** Build `set_tiers` — replace the 10 length-tier base registration prices. */
32
+ export declare function buildSetTiersIx(p: SetTiersParams): BuiltInstruction;
33
+ export interface SetNftTiersParams {
34
+ readonly programId: AddressLike;
35
+ readonly admin: AddressLike;
36
+ readonly config: AddressLike;
37
+ /** Exactly 10 per-length-tier NFT mint fees in lamports. */
38
+ readonly nftTierLamports: ReadonlyArray<number | bigint>;
39
+ }
40
+ /** Build `set_nft_tiers` — replace the 10 length-tier `mint_handle_nft` fees. */
41
+ export declare function buildSetNftTiersIx(p: SetNftTiersParams): BuiltInstruction;
42
+ export declare const SET_LEASE_RATE_DIVISOR_DISCRIMINATOR: Uint8Array;
43
+ export declare const SET_LEASE_GRACE_PERIOD_DISCRIMINATOR: Uint8Array;
44
+ export declare const SET_RECOVERY_TIMELOCK_DISCRIMINATOR: Uint8Array;
45
+ export declare const SET_MIN_COMMITMENT_AGE_DISCRIMINATOR: Uint8Array;
46
+ export declare const SET_MAX_COMMITMENT_AGE_DISCRIMINATOR: Uint8Array;
47
+ /** Map an admin config action name → its discriminator (for a relay executor
48
+ * that dispatches on the action string). set_admin/set_attestor intentionally
49
+ * absent — those are owner-gated, never dispatched here. */
50
+ export declare const ADMIN_SCALAR_DISCRIMINATORS: Readonly<Record<string, Uint8Array>>;
51
+ export interface AdminScalarParams {
52
+ readonly programId: AddressLike;
53
+ readonly admin: AddressLike;
54
+ readonly config: AddressLike;
55
+ /** The discriminator for the specific setter (from ADMIN_SCALAR_DISCRIMINATORS). */
56
+ readonly discriminator: Uint8Array;
57
+ /** The integer arg (u64/i64 encoding is identical for a non-negative value). */
58
+ readonly value: number | bigint;
59
+ }
60
+ /** Build any one-integer-arg AdminOnly setter (lease rate divisor, lease grace,
61
+ * recovery timelock, min/max commitment age). disc(8) + value as little-endian
62
+ * u64(8). The value must be a non-negative integer that fits u64. */
63
+ export declare function buildAdminScalarSetterIx(p: AdminScalarParams): BuiltInstruction;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Admin-only `Config` setters — `set_tiers` / `set_nft_tiers` (#8398) and (as
3
+ * the on-chain admin batch lands) the rest of the AdminOnly config instructions.
4
+ *
5
+ * Hand-rolled like the rest of this package — no Anchor client, no
6
+ * `@solana/web3.js`. All of these share the `AdminOnly` account shape:
7
+ * admin (signer, NOT writable — these setters init nothing, so no rent)
8
+ * config (the ["config"] PDA, writable)
9
+ * and must be signed by `Config.admin`. PDAs are not derived here — derive
10
+ * `["config"]` with your runtime's `findProgramAddress`.
11
+ */
12
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
13
+ function toBytes32(v, what) {
14
+ if (typeof v === "string") {
15
+ const b = decodeBase58_32(v);
16
+ if (!b)
17
+ throw new Error(`${what} is not a valid base58 address`);
18
+ return b;
19
+ }
20
+ if (v.length !== 32)
21
+ throw new Error(`${what} must be exactly 32 bytes`);
22
+ return v;
23
+ }
24
+ function toBase58(v, what) {
25
+ return encodeBase58(toBytes32(v, what));
26
+ }
27
+ /** Anchor instruction discriminator: `sha256("global:set_tiers")[0..8]`. */
28
+ export const SET_TIERS_DISCRIMINATOR = Uint8Array.from([
29
+ 170, 131, 109, 175, 156, 214, 177, 121,
30
+ ]);
31
+ /** Anchor instruction discriminator: `sha256("global:set_nft_tiers")[0..8]`. */
32
+ export const SET_NFT_TIERS_DISCRIMINATOR = Uint8Array.from([
33
+ 116, 68, 126, 146, 252, 206, 87, 61,
34
+ ]);
35
+ /** The number of length-tiers in `Config.tier_lamports` / `nft_tier_lamports`.
36
+ * Tier index = `clamp(nameLen, 1, 10) - 1`, so tier[0] = 1-char … tier[9] = 10+. */
37
+ export const TIER_COUNT = 10;
38
+ function encodeTiers(disc, tiers) {
39
+ if (tiers.length !== TIER_COUNT) {
40
+ throw new Error(`expected exactly ${TIER_COUNT} tiers, got ${tiers.length}`);
41
+ }
42
+ // Fixed-size [u64; 10] — borsh writes 10 little-endian u64s back to back, NO
43
+ // length prefix (unlike a Vec or a String).
44
+ const data = new Uint8Array(8 + TIER_COUNT * 8);
45
+ data.set(disc, 0);
46
+ const dv = new DataView(data.buffer);
47
+ for (let i = 0; i < TIER_COUNT; i++) {
48
+ const raw = tiers[i]; // length checked above
49
+ const v = typeof raw === "bigint" ? raw : BigInt(raw);
50
+ if (v < 0n || v > 0xffffffffffffffffn) {
51
+ throw new Error(`tier[${i}] = ${raw} is out of u64 range`);
52
+ }
53
+ dv.setBigUint64(8 + i * 8, v, true);
54
+ }
55
+ return data;
56
+ }
57
+ function adminOnlyKeys(admin, config) {
58
+ return [
59
+ // admin signs but is NOT writable — these setters init nothing (no rent).
60
+ { pubkey: toBase58(admin, "admin"), isSigner: true, isWritable: false },
61
+ { pubkey: toBase58(config, "config"), isSigner: false, isWritable: true },
62
+ ];
63
+ }
64
+ /** Build `set_tiers` — replace the 10 length-tier base registration prices. */
65
+ export function buildSetTiersIx(p) {
66
+ return {
67
+ programId: toBase58(p.programId, "programId"),
68
+ keys: adminOnlyKeys(p.admin, p.config),
69
+ data: encodeTiers(SET_TIERS_DISCRIMINATOR, p.tierLamports),
70
+ };
71
+ }
72
+ /** Build `set_nft_tiers` — replace the 10 length-tier `mint_handle_nft` fees. */
73
+ export function buildSetNftTiersIx(p) {
74
+ return {
75
+ programId: toBase58(p.programId, "programId"),
76
+ keys: adminOnlyKeys(p.admin, p.config),
77
+ data: encodeTiers(SET_NFT_TIERS_DISCRIMINATOR, p.nftTierLamports),
78
+ };
79
+ }
80
+ // ===== Scalar AdminOnly setters (#8399/recovery/commitment) =====
81
+ // Each takes one little-endian integer arg. Same AdminOnly account shape.
82
+ export const SET_LEASE_RATE_DIVISOR_DISCRIMINATOR = Uint8Array.from([181, 218, 76, 213, 80, 99, 181, 52]);
83
+ export const SET_LEASE_GRACE_PERIOD_DISCRIMINATOR = Uint8Array.from([212, 2, 110, 20, 130, 50, 194, 186]);
84
+ export const SET_RECOVERY_TIMELOCK_DISCRIMINATOR = Uint8Array.from([178, 183, 148, 243, 155, 166, 79, 6]);
85
+ export const SET_MIN_COMMITMENT_AGE_DISCRIMINATOR = Uint8Array.from([181, 20, 114, 43, 51, 23, 47, 248]);
86
+ export const SET_MAX_COMMITMENT_AGE_DISCRIMINATOR = Uint8Array.from([163, 106, 196, 206, 146, 156, 79, 249]);
87
+ /** Map an admin config action name → its discriminator (for a relay executor
88
+ * that dispatches on the action string). set_admin/set_attestor intentionally
89
+ * absent — those are owner-gated, never dispatched here. */
90
+ export const ADMIN_SCALAR_DISCRIMINATORS = {
91
+ set_lease_rate_divisor: SET_LEASE_RATE_DIVISOR_DISCRIMINATOR,
92
+ set_lease_grace_period: SET_LEASE_GRACE_PERIOD_DISCRIMINATOR,
93
+ set_recovery_timelock: SET_RECOVERY_TIMELOCK_DISCRIMINATOR,
94
+ set_min_commitment_age: SET_MIN_COMMITMENT_AGE_DISCRIMINATOR,
95
+ set_max_commitment_age: SET_MAX_COMMITMENT_AGE_DISCRIMINATOR,
96
+ };
97
+ /** Build any one-integer-arg AdminOnly setter (lease rate divisor, lease grace,
98
+ * recovery timelock, min/max commitment age). disc(8) + value as little-endian
99
+ * u64(8). The value must be a non-negative integer that fits u64. */
100
+ export function buildAdminScalarSetterIx(p) {
101
+ const raw = typeof p.value === "bigint" ? p.value : BigInt(p.value);
102
+ if (raw < 0n || raw > 0xffffffffffffffffn) {
103
+ throw new Error(`value ${p.value} is out of u64 range`);
104
+ }
105
+ const data = new Uint8Array(8 + 8);
106
+ data.set(p.discriminator, 0);
107
+ new DataView(data.buffer).setBigUint64(8, raw, true);
108
+ return {
109
+ programId: toBase58(p.programId, "programId"),
110
+ keys: adminOnlyKeys(p.admin, p.config),
111
+ data,
112
+ };
113
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Admin-only marketplace config (`set_market_config`, #8401) and the namespace
3
+ * directory setters (`create_namespace` / `set_namespace_status`, #8403). The
4
+ * integrator setters (add/set_rate/remove) already live in ./integrator.ts and
5
+ * are reused, not duplicated here.
6
+ *
7
+ * Hand-rolled like the rest of this package — no Anchor client, no
8
+ * `@solana/web3.js`. Unlike the AdminOnly setters in adminConfig.ts, these do
9
+ * NOT share one account shape: `config` is read-only here (it's the has_one =
10
+ * admin authority check), and the written account differs per instruction
11
+ * (`market_config` or a `Namespace`). PDAs are not derived here — derive
12
+ * `["market_config"]`, `["namespace", label]`, `["config"]` with your runtime's
13
+ * findProgramAddress and pass them in, same convention as adminConfig.ts.
14
+ */
15
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
16
+ /** The System Program — required by the two `init` instructions (add_integrator,
17
+ * create_namespace). A fixed well-known address, hardcoded so callers can't
18
+ * mistype it. */
19
+ export declare const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
20
+ /** Anchor discriminator sha256("global:set_market_config")[0..8]. */
21
+ export declare const SET_MARKET_CONFIG_DISCRIMINATOR: Uint8Array;
22
+ export interface SetMarketConfigParams {
23
+ readonly programId: AddressLike;
24
+ /** `Config.admin` — signs; NOT writable. */
25
+ readonly admin: AddressLike;
26
+ /** `["config"]` PDA — read-only (the has_one authority check). */
27
+ readonly config: AddressLike;
28
+ /** `["market_config"]` PDA — writable. */
29
+ readonly marketConfig: AddressLike;
30
+ readonly feeBps: number;
31
+ readonly minBidIncrementBps: number;
32
+ readonly antiSnipeWindow: number;
33
+ readonly antiSnipeExtend: number;
34
+ readonly minAuctionSecs: number;
35
+ readonly maxAuctionSecs: number;
36
+ readonly paused: boolean;
37
+ }
38
+ /** Build `set_market_config` — fee/auction params + the global pause flag.
39
+ * Enforces the same bounds the program's validate_market_params does. */
40
+ export declare function buildSetMarketConfigIx(p: SetMarketConfigParams): BuiltInstruction;
41
+ export declare const CREATE_NAMESPACE_DISCRIMINATOR: Uint8Array;
42
+ export declare const SET_NAMESPACE_STATUS_DISCRIMINATOR: Uint8Array;
43
+ export declare const NAMESPACE_MAX_LEN = 16;
44
+ export type NamespaceStatus = "Active" | "Paused" | "Retired";
45
+ export interface CreateNamespaceParams {
46
+ readonly programId: AddressLike;
47
+ /** admin — signs AND pays rent; writable. */
48
+ readonly admin: AddressLike;
49
+ readonly config: AddressLike;
50
+ /** `["namespace", label]` PDA — init, writable. */
51
+ readonly namespace: AddressLike;
52
+ /** Canonical ASCII label (a-z 0-9 -), 1..=16 bytes — an instruction ARG. */
53
+ readonly label: string;
54
+ readonly systemProgram?: AddressLike;
55
+ }
56
+ /** Build `create_namespace` — label is a borsh String arg. */
57
+ export declare function buildCreateNamespaceIx(p: CreateNamespaceParams): BuiltInstruction;
58
+ export interface SetNamespaceStatusParams {
59
+ readonly programId: AddressLike;
60
+ readonly admin: AddressLike;
61
+ readonly config: AddressLike;
62
+ /** `["namespace", label]` PDA — writable. */
63
+ readonly namespace: AddressLike;
64
+ readonly status: NamespaceStatus;
65
+ }
66
+ /** Build `set_namespace_status` — status enum as a single-byte discriminant. */
67
+ export declare function buildSetNamespaceStatusIx(p: SetNamespaceStatusParams): BuiltInstruction;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Admin-only marketplace config (`set_market_config`, #8401) and the namespace
3
+ * directory setters (`create_namespace` / `set_namespace_status`, #8403). The
4
+ * integrator setters (add/set_rate/remove) already live in ./integrator.ts and
5
+ * are reused, not duplicated here.
6
+ *
7
+ * Hand-rolled like the rest of this package — no Anchor client, no
8
+ * `@solana/web3.js`. Unlike the AdminOnly setters in adminConfig.ts, these do
9
+ * NOT share one account shape: `config` is read-only here (it's the has_one =
10
+ * admin authority check), and the written account differs per instruction
11
+ * (`market_config` or a `Namespace`). PDAs are not derived here — derive
12
+ * `["market_config"]`, `["namespace", label]`, `["config"]` with your runtime's
13
+ * findProgramAddress and pass them in, same convention as adminConfig.ts.
14
+ */
15
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
16
+ /** The System Program — required by the two `init` instructions (add_integrator,
17
+ * create_namespace). A fixed well-known address, hardcoded so callers can't
18
+ * mistype it. */
19
+ export const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
20
+ function toBytes32(v, what) {
21
+ if (typeof v === "string") {
22
+ const b = decodeBase58_32(v);
23
+ if (!b)
24
+ throw new Error(`${what} is not a valid base58 address`);
25
+ return b;
26
+ }
27
+ if (v.length !== 32)
28
+ throw new Error(`${what} must be exactly 32 bytes`);
29
+ return v;
30
+ }
31
+ function toBase58(v, what) {
32
+ return encodeBase58(toBytes32(v, what));
33
+ }
34
+ // ---- borsh scalar writers -------------------------------------------------
35
+ function u16le(n) {
36
+ if (!Number.isInteger(n) || n < 0 || n > 0xffff)
37
+ throw new Error(`u16 out of range: ${n}`);
38
+ const b = new Uint8Array(2);
39
+ new DataView(b.buffer).setUint16(0, n, true);
40
+ return b;
41
+ }
42
+ function u32le(n) {
43
+ if (!Number.isInteger(n) || n < 0 || n > 0xffff_ffff)
44
+ throw new Error(`u32 out of range: ${n}`);
45
+ const b = new Uint8Array(4);
46
+ new DataView(b.buffer).setUint32(0, n, true);
47
+ return b;
48
+ }
49
+ function concat(parts) {
50
+ const len = parts.reduce((n, p) => n + p.length, 0);
51
+ const out = new Uint8Array(len);
52
+ let o = 0;
53
+ for (const p of parts) {
54
+ out.set(p, o);
55
+ o += p.length;
56
+ }
57
+ return out;
58
+ }
59
+ // ===== #8401 set_market_config =====
60
+ /** Anchor discriminator sha256("global:set_market_config")[0..8]. */
61
+ export const SET_MARKET_CONFIG_DISCRIMINATOR = Uint8Array.from([128, 237, 216, 59, 122, 62, 156, 30]);
62
+ /** Build `set_market_config` — fee/auction params + the global pause flag.
63
+ * Enforces the same bounds the program's validate_market_params does. */
64
+ export function buildSetMarketConfigIx(p) {
65
+ if (p.feeBps > 10_000)
66
+ throw new Error("feeBps must be <= 10000");
67
+ if (p.minBidIncrementBps > 10_000)
68
+ throw new Error("minBidIncrementBps must be <= 10000");
69
+ if (p.minAuctionSecs <= 0)
70
+ throw new Error("minAuctionSecs must be > 0");
71
+ if (p.maxAuctionSecs < p.minAuctionSecs)
72
+ throw new Error("maxAuctionSecs must be >= minAuctionSecs");
73
+ if (p.antiSnipeExtend < p.antiSnipeWindow)
74
+ throw new Error("antiSnipeExtend must be >= antiSnipeWindow");
75
+ const data = concat([
76
+ SET_MARKET_CONFIG_DISCRIMINATOR,
77
+ u16le(p.feeBps),
78
+ u16le(p.minBidIncrementBps),
79
+ u32le(p.antiSnipeWindow),
80
+ u32le(p.antiSnipeExtend),
81
+ u32le(p.minAuctionSecs),
82
+ u32le(p.maxAuctionSecs),
83
+ Uint8Array.from([p.paused ? 1 : 0]),
84
+ ]);
85
+ const keys = [
86
+ { pubkey: toBase58(p.admin, "admin"), isSigner: true, isWritable: false },
87
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
88
+ { pubkey: toBase58(p.marketConfig, "marketConfig"), isSigner: false, isWritable: true },
89
+ ];
90
+ return { programId: toBase58(p.programId, "programId"), keys, data };
91
+ }
92
+ // NOTE: the integrator builders (add_integrator / set_integrator_rate /
93
+ // remove_integrator) + decodeAllowlistEntry already live in ./integrator.ts —
94
+ // reuse those, they are not duplicated here.
95
+ // ===== #8403 namespaces =====
96
+ export const CREATE_NAMESPACE_DISCRIMINATOR = Uint8Array.from([205, 189, 35, 255, 214, 116, 25, 107]);
97
+ export const SET_NAMESPACE_STATUS_DISCRIMINATOR = Uint8Array.from([41, 135, 103, 92, 151, 13, 61, 158]);
98
+ export const NAMESPACE_MAX_LEN = 16;
99
+ const NAMESPACE_STATUS_DISCRIMINANT = {
100
+ Active: 0,
101
+ Paused: 1,
102
+ Retired: 2,
103
+ };
104
+ function borshString(s) {
105
+ const bytes = new TextEncoder().encode(s);
106
+ return concat([u32le(bytes.length), bytes]);
107
+ }
108
+ /** Build `create_namespace` — label is a borsh String arg. */
109
+ export function buildCreateNamespaceIx(p) {
110
+ if (p.label.length < 1 || p.label.length > NAMESPACE_MAX_LEN) {
111
+ throw new Error(`label must be 1..=${NAMESPACE_MAX_LEN} chars`);
112
+ }
113
+ const data = concat([CREATE_NAMESPACE_DISCRIMINATOR, borshString(p.label)]);
114
+ const keys = [
115
+ { pubkey: toBase58(p.admin, "admin"), isSigner: true, isWritable: true },
116
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
117
+ { pubkey: toBase58(p.namespace, "namespace"), isSigner: false, isWritable: true },
118
+ { pubkey: toBase58(p.systemProgram ?? SYSTEM_PROGRAM_ID, "systemProgram"), isSigner: false, isWritable: false },
119
+ ];
120
+ return { programId: toBase58(p.programId, "programId"), keys, data };
121
+ }
122
+ /** Build `set_namespace_status` — status enum as a single-byte discriminant. */
123
+ export function buildSetNamespaceStatusIx(p) {
124
+ const disc = NAMESPACE_STATUS_DISCRIMINANT[p.status];
125
+ if (disc === undefined)
126
+ throw new Error(`unknown namespace status: ${p.status}`);
127
+ const data = concat([SET_NAMESPACE_STATUS_DISCRIMINATOR, Uint8Array.from([disc])]);
128
+ const keys = [
129
+ { pubkey: toBase58(p.admin, "admin"), isSigner: true, isWritable: false },
130
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
131
+ { pubkey: toBase58(p.namespace, "namespace"), isSigner: false, isWritable: true },
132
+ ];
133
+ return { programId: toBase58(p.programId, "programId"), keys, data };
134
+ }
@@ -91,6 +91,14 @@ export declare const ATTESTATION_KIND_TELEGRAM = 5;
91
91
  * `>=` this on-chain; the SDK's builder enforces the same bound. Mirrors
92
92
  * `Attestation::KIND_COUNT` (programs/x1-handles/src/state.rs). */
93
93
  export declare const ATTESTATION_KIND_COUNT = 6;
94
+ /** `Attestation.kind` — validator control proven TRUSTLESSLY on-chain (#8486):
95
+ * the vote account's `authorized_withdrawer` co-signed `attest_validator`, or
96
+ * signed the challenge that `attest_validator_signed` verifies via the Ed25519
97
+ * precompile. Value 6 — equal to {@link ATTESTATION_KIND_COUNT}, i.e. OUTSIDE
98
+ * the attestor-mintable range (`create_attestation` refuses `>= KIND_COUNT`),
99
+ * so only the cryptographic validator path can mint it. `evidenceHash` holds
100
+ * the vote account pubkey; surfaced as a gold shield, not the blue tick. */
101
+ export declare const ATTESTATION_KIND_VALIDATOR = 6;
94
102
  /** Anchor account discriminator: `sha256("account:Attestation")[0..8]`.
95
103
  * Pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
96
104
  * everywhere it runs); asserted against a re-derivation in the test suite
@@ -104,6 +112,18 @@ export declare const SET_ATTESTOR_DISCRIMINATOR: Uint8Array;
104
112
  export declare const CREATE_ATTESTATION_DISCRIMINATOR: Uint8Array;
105
113
  /** Anchor instruction discriminator: `sha256("global:close_attestation")[0..8]`. */
106
114
  export declare const CLOSE_ATTESTATION_DISCRIMINATOR: Uint8Array;
115
+ /** Anchor instruction discriminator: `sha256("global:attest_validator")[0..8]` — the co-sign path. */
116
+ export declare const ATTEST_VALIDATOR_DISCRIMINATOR: Uint8Array;
117
+ /** Anchor instruction discriminator: `sha256("global:attest_validator_signed")[0..8]` — the detached path. */
118
+ export declare const ATTEST_VALIDATOR_SIGNED_DISCRIMINATOR: Uint8Array;
119
+ /** The SVM Ed25519 signature-verification precompile — carries the withdraw
120
+ * authority's detached signature for `attest_validator_signed`. */
121
+ export declare const ED25519_PROGRAM_ID = "Ed25519SigVerify111111111111111111111111111";
122
+ /** The Instructions sysvar — `attest_validator_signed` introspects it. */
123
+ export declare const SYSVAR_INSTRUCTIONS_ID = "Sysvar1nstructions1111111111111111111111111";
124
+ /** Domain-separator prefix of the validator-attestation challenge. MUST byte-
125
+ * match `VALIDATOR_ATTEST_CHALLENGE_PREFIX` in programs/x1-handles/src/lib.rs. */
126
+ export declare const VALIDATOR_ATTEST_CHALLENGE_PREFIX: Uint8Array;
107
127
  /** `Attestation` account size — every field is fixed-width, so unlike a
108
128
  * `Handle` the account is exactly this long:
109
129
  * disc(8) + handle(32) + kind(1) + evidence_hash(32) + attested_at(8)
@@ -112,7 +132,7 @@ export declare const ATTESTATION_LEN = 114;
112
132
  /** `AttestorConfig` account size: disc(8) + attestor(32) + bump(1). */
113
133
  export declare const ATTESTOR_CONFIG_LEN = 41;
114
134
  /** Human name an attestation `kind` byte carries. */
115
- export type AttestationKindName = "dns" | "social" | "x" | "discord" | "github" | "telegram";
135
+ export type AttestationKindName = "dns" | "social" | "x" | "discord" | "github" | "telegram" | "validator";
116
136
  /** Which verification signal an attestation `kind` byte names, or null for a
117
137
  * kind this SDK does not know (future program versions may add kinds). For the
118
138
  * per-platform kinds this returns the platform slug; the two legacy kinds keep
@@ -272,6 +292,88 @@ export interface CloseAttestationParams {
272
292
  * a rotated-away key's output).
273
293
  */
274
294
  export declare function buildCloseAttestationIx(p: CloseAttestationParams): BuiltInstruction;
295
+ export interface AttestValidatorParams {
296
+ /** The registry program id. */
297
+ readonly programId: AddressLike;
298
+ /** The handle's authority (owner, or NFT holder if tokenized); signs + pays rent. */
299
+ readonly owner: AddressLike;
300
+ /** The vote account's withdraw authority; co-signs (MAY equal `owner`). */
301
+ readonly withdrawAuthority: AddressLike;
302
+ /** The `["handle", name]` PDA. */
303
+ readonly handle: AddressLike;
304
+ /** The validator's vote account (owned by the Vote program on-chain). */
305
+ readonly voteAccount: AddressLike;
306
+ /** The `["attestation", handle, [KIND_VALIDATOR]]` PDA. */
307
+ readonly attestation: AddressLike;
308
+ /** ONLY for a tokenized handle: the caller's associated token account for the
309
+ * handle's NFT mint (the holder-proof `require_current_authority` reads). */
310
+ readonly holderTokenAccount?: AddressLike;
311
+ }
312
+ /**
313
+ * Build `attest_validator` (co-sign path). Two signers — `owner` (authorizes
314
+ * the link) and `withdrawAuthority` (the proof) — which MAY be the same key. No
315
+ * instruction args: the co-signature IS the proof.
316
+ */
317
+ export declare function buildAttestValidatorIx(p: AttestValidatorParams): BuiltInstruction;
318
+ /**
319
+ * The exact 96-byte challenge the withdraw authority signs for the DETACHED
320
+ * path: `PREFIX(24) ‖ handle(32) ‖ voteAccount(32) ‖ registeredAt i64 LE(8)`.
321
+ * MUST byte-match the program. `registeredAt` is the handle's on-chain
322
+ * `registered_at` (seconds) — pass the exact value from the Handle account.
323
+ */
324
+ export declare function validatorAttestChallenge(handle: AddressLike, voteAccount: AddressLike, registeredAt: bigint | number): Uint8Array;
325
+ export interface Ed25519VerifyParams {
326
+ /** The 32-byte ed25519 public key (the withdraw authority). */
327
+ readonly publicKey: AddressLike;
328
+ /** The signed message (the validator challenge). */
329
+ readonly message: Uint8Array;
330
+ /** The 64-byte ed25519 signature over `message` by `publicKey`. */
331
+ readonly signature: Uint8Array;
332
+ }
333
+ /**
334
+ * Build the Ed25519 precompile instruction (no accounts) proving `signature` is
335
+ * `publicKey`'s over `message`. SELF-CONTAINED single signature — all three
336
+ * instruction-index fields are `0xFFFF` — matching the exact layout
337
+ * `attest_validator_signed` requires. Place it IMMEDIATELY BEFORE the attest ix
338
+ * (use {@link buildAttestValidatorSignedIxs}).
339
+ */
340
+ export declare function buildEd25519VerifyIx(p: Ed25519VerifyParams): BuiltInstruction;
341
+ export interface AttestValidatorSignedParams {
342
+ /** The registry program id. */
343
+ readonly programId: AddressLike;
344
+ /** The handle's authority (owner / NFT holder); the SOLE tx signer, pays rent. */
345
+ readonly owner: AddressLike;
346
+ readonly handle: AddressLike;
347
+ readonly voteAccount: AddressLike;
348
+ /** The `["attestation", handle, [KIND_VALIDATOR]]` PDA. */
349
+ readonly attestation: AddressLike;
350
+ /** ONLY for a tokenized handle: the caller's ATA for the handle's NFT mint. */
351
+ readonly holderTokenAccount?: AddressLike;
352
+ }
353
+ /**
354
+ * Build the `attest_validator_signed` ix ALONE. The withdraw authority's proof
355
+ * is NOT here — it rides in the Ed25519 precompile ix that MUST immediately
356
+ * precede this one. Prefer {@link buildAttestValidatorSignedIxs}, which returns
357
+ * both, in order. No instruction args.
358
+ */
359
+ export declare function buildAttestValidatorSignedIx(p: AttestValidatorSignedParams): BuiltInstruction;
360
+ export interface AttestValidatorSignedTxParams extends AttestValidatorSignedParams {
361
+ /** The vote account's `authorized_withdrawer` (32-byte ed25519 pubkey) whose
362
+ * offline signature over the challenge this carries. */
363
+ readonly withdrawAuthority: AddressLike;
364
+ /** The 64-byte ed25519 signature over {@link validatorAttestChallenge},
365
+ * produced offline by the withdraw authority. */
366
+ readonly signature: Uint8Array;
367
+ /** The handle's on-chain `registered_at` (seconds). */
368
+ readonly registeredAt: bigint | number;
369
+ }
370
+ /**
371
+ * Build BOTH instructions for the detached path, IN ORDER:
372
+ * `[ed25519VerifyIx, attestValidatorSignedIx]`. Submit them as ONE transaction
373
+ * in this order — the program loads the ed25519 ix at `current_index - 1`.
374
+ * `owner` is the only signer.
375
+ */
376
+ export declare function buildAttestValidatorSignedIxs(p: AttestValidatorSignedTxParams): BuiltInstruction[];
275
377
  /** The minimal shape the verification helpers read — any {@link Resolved}
276
378
  * satisfies it, as does a bare `{ verifications }` an integrator assembles
277
379
  * from a raw REST payload. */