@x1id/resolve 0.15.1 → 0.17.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/gate.d.ts CHANGED
@@ -7,8 +7,12 @@
7
7
  * The gate operates on the attestations {@link fetchAttestations} returns; the
8
8
  * staleness rule (an attestation only counts for the CURRENT owner —
9
9
  * `attested_at >= handle.registered_at`) is already baked into `HandleAttestation.stale`,
10
- * and this module always ignores stale ones. Nothing here trusts an off-chain
11
- * claim: every input is an on-chain, attestor-signed `Attestation` account.
10
+ * and this module always ignores stale ones. It likewise ignores any attestation
11
+ * flagged `HandleAttestation.holderMismatch` (H5 — a tokenized handle whose pNFT
12
+ * moved to a holder it was not issued to), so a gate caller that overlays
13
+ * {@link markHolderMismatch} onto the fetched set never admits an inherited
14
+ * badge. Nothing here trusts an off-chain claim: every input is an on-chain,
15
+ * attestor-signed `Attestation` account.
12
16
  */
13
17
  import { type HandleAttestation } from "./attestation.js";
14
18
  import type { RpcFn } from "./accounts.js";
@@ -31,13 +35,16 @@ export declare const GateKind: {
31
35
  readonly telegram: 5;
32
36
  };
33
37
  /** Pure check: do these (already-fetched) attestations satisfy the gate?
34
- * Stale attestations never count. */
38
+ * Stale and holder-mismatched (H5) attestations never count. */
35
39
  export declare function attestationsPassGate(attestations: readonly HandleAttestation[], gate: Gate): boolean;
36
40
  /** Fetch a handle's attestations and test the gate in one call. `handleAccount`,
37
- * `registeredAt` and `recordsClearedAt` come from the handle you resolved
38
- * (name → `deriveHandleAccount` → decode). Returns the pass/fail plus the
41
+ * `registeredAt`, `recordsClearedAt` and `holderEpoch` come from the handle you
42
+ * resolved (name → `deriveHandleAccount` → decode). `holderEpoch` (H5 / PR
43
+ * #466, 0 if absent) folds into the staleness epoch so the gate matches the
44
+ * on-chain rule `attested_at >= max(registered_at, holder_epoch)`; it defaults
45
+ * to `0n` for backward compatibility. Returns the pass/fail plus the
39
46
  * attestations so a caller can also render WHICH signals matched. */
40
- export declare function handlePassesGate(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint, gate: Gate): Promise<{
47
+ export declare function handlePassesGate(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint, gate: Gate, holderEpoch?: bigint): Promise<{
41
48
  readonly passed: boolean;
42
49
  readonly attestations: HandleAttestation[];
43
50
  }>;
package/dist/gate.js CHANGED
@@ -7,8 +7,12 @@
7
7
  * The gate operates on the attestations {@link fetchAttestations} returns; the
8
8
  * staleness rule (an attestation only counts for the CURRENT owner —
9
9
  * `attested_at >= handle.registered_at`) is already baked into `HandleAttestation.stale`,
10
- * and this module always ignores stale ones. Nothing here trusts an off-chain
11
- * claim: every input is an on-chain, attestor-signed `Attestation` account.
10
+ * and this module always ignores stale ones. It likewise ignores any attestation
11
+ * flagged `HandleAttestation.holderMismatch` (H5 — a tokenized handle whose pNFT
12
+ * moved to a holder it was not issued to), so a gate caller that overlays
13
+ * {@link markHolderMismatch} onto the fetched set never admits an inherited
14
+ * badge. Nothing here trusts an off-chain claim: every input is an on-chain,
15
+ * attestor-signed `Attestation` account.
12
16
  */
13
17
  import { fetchAttestations, isHandleVerified, ATTESTATION_KIND_DNS, ATTESTATION_KIND_SOCIAL, ATTESTATION_KIND_X, ATTESTATION_KIND_DISCORD, ATTESTATION_KIND_GITHUB, ATTESTATION_KIND_TELEGRAM, } from "./attestation.js";
14
18
  /** Named platform kinds, re-exported so callers gate without magic numbers. */
@@ -21,18 +25,21 @@ export const GateKind = {
21
25
  telegram: ATTESTATION_KIND_TELEGRAM,
22
26
  };
23
27
  /** Pure check: do these (already-fetched) attestations satisfy the gate?
24
- * Stale attestations never count. */
28
+ * Stale and holder-mismatched (H5) attestations never count. */
25
29
  export function attestationsPassGate(attestations, gate) {
26
30
  if (gate === "any-verified")
27
31
  return isHandleVerified(attestations);
28
32
  const kinds = "kind" in gate ? [gate.kind] : gate.anyOf;
29
- return attestations.some((a) => !a.stale && kinds.includes(a.kind));
33
+ return attestations.some((a) => !a.stale && !a.holderMismatch && kinds.includes(a.kind));
30
34
  }
31
35
  /** Fetch a handle's attestations and test the gate in one call. `handleAccount`,
32
- * `registeredAt` and `recordsClearedAt` come from the handle you resolved
33
- * (name → `deriveHandleAccount` → decode). Returns the pass/fail plus the
36
+ * `registeredAt`, `recordsClearedAt` and `holderEpoch` come from the handle you
37
+ * resolved (name → `deriveHandleAccount` → decode). `holderEpoch` (H5 / PR
38
+ * #466, 0 if absent) folds into the staleness epoch so the gate matches the
39
+ * on-chain rule `attested_at >= max(registered_at, holder_epoch)`; it defaults
40
+ * to `0n` for backward compatibility. Returns the pass/fail plus the
34
41
  * attestations so a caller can also render WHICH signals matched. */
35
- export async function handlePassesGate(rpc, programId, handleAccount, registeredAt, recordsClearedAt, gate) {
36
- const attestations = await fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt);
42
+ export async function handlePassesGate(rpc, programId, handleAccount, registeredAt, recordsClearedAt, gate, holderEpoch = 0n) {
43
+ const attestations = await fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt, holderEpoch);
37
44
  return { passed: attestationsPassGate(attestations, gate), attestations };
38
45
  }
package/dist/index.d.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * // bytes of @x1id/resolve/wasm/x1_resolve_wasm.wasm — see the README for loading
8
8
  * const wasm = await WasmResolver.fromBytes(wasmBytes);
9
- * // The @handle registry is on X1 testnet today; X1NS domains are on mainnet.
10
- * const r = createResolver({ rpcUrl: "https://rpc.testnet.x1.xyz", wasm });
9
+ * // The @handle registry and X1NS domains are on X1 mainnet (testnet works the same).
10
+ * const r = createResolver({ rpcUrl: "https://rpc.mainnet.x1.xyz", wasm });
11
11
  * const res = await r.resolve("@jack", { chain: "X1" });
12
12
  * // { name: "jack", namespace: "handle", address: "...", verification: "verified" }
13
13
  * ```
@@ -45,11 +45,13 @@ export * from "./register.js";
45
45
  export * from "./adminConfig.js";
46
46
  export * from "./lease.js";
47
47
  export * from "./accounts.js";
48
+ export * from "./pause.js";
48
49
  export * from "./x402.js";
49
50
  export * from "./recordWrite.js";
50
51
  export * from "./clearRecords.js";
51
52
  export * from "./commitReveal.js";
52
53
  export * from "./pnftTransfer.js";
54
+ export * from "./payment.js";
53
55
  export * from "./signin.js";
54
56
  export * from "./connect.js";
55
57
  export * from "./domainProof.js";
package/dist/index.js CHANGED
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * // bytes of @x1id/resolve/wasm/x1_resolve_wasm.wasm — see the README for loading
8
8
  * const wasm = await WasmResolver.fromBytes(wasmBytes);
9
- * // The @handle registry is on X1 testnet today; X1NS domains are on mainnet.
10
- * const r = createResolver({ rpcUrl: "https://rpc.testnet.x1.xyz", wasm });
9
+ * // The @handle registry and X1NS domains are on X1 mainnet (testnet works the same).
10
+ * const r = createResolver({ rpcUrl: "https://rpc.mainnet.x1.xyz", wasm });
11
11
  * const res = await r.resolve("@jack", { chain: "X1" });
12
12
  * // { name: "jack", namespace: "handle", address: "...", verification: "verified" }
13
13
  * ```
@@ -52,11 +52,13 @@ export * from "./lease.js";
52
52
  // the kind of drift docs/record-trust.md's "one implementation" rule
53
53
  // exists to prevent).
54
54
  export * from "./accounts.js";
55
+ export * from "./pause.js";
55
56
  export * from "./x402.js";
56
57
  export * from "./recordWrite.js";
57
58
  export * from "./clearRecords.js";
58
59
  export * from "./commitReveal.js";
59
60
  export * from "./pnftTransfer.js";
61
+ export * from "./payment.js";
60
62
  export * from "./signin.js";
61
63
  export * from "./connect.js";
62
64
  export * from "./domainProof.js";
@@ -34,7 +34,7 @@
34
34
  * {@link integratorAccountsForRegister} / {@link integratorAccountsForCreateVoucher}
35
35
  * to build the pair.
36
36
  * - The rate is read from the on-chain `AllowlistEntry`, never trusted from
37
- * the transaction. It is capped at {@link MAX_INTEGRATOR_RATE_BPS} (40%);
37
+ * the transaction. It is capped at {@link MAX_INTEGRATOR_RATE_BPS} (20%);
38
38
  * {@link DEFAULT_INTEGRATOR_RATE_BPS} (20%) applies when `add_integrator`
39
39
  * omits a rate.
40
40
  */
@@ -43,8 +43,12 @@ import type { AddressLike, BuiltInstruction, InstructionKey } from "./delegate.j
43
43
  export declare const INTEGRATOR_SEED = "integrator";
44
44
  /** Default integrator share when `add_integrator` omits a rate: 20%. */
45
45
  export declare const DEFAULT_INTEGRATOR_RATE_BPS = 2000;
46
- /** Hard cap on an integrator's share of a mint fee: 40%. */
47
- export declare const MAX_INTEGRATOR_RATE_BPS = 4000;
46
+ /** Hard cap on an integrator's share of a mint fee: 20%. Mirrors the program's
47
+ * `MAX_INTEGRATOR_RATE_BPS` (lib.rs) — the admin `register_integrator` /
48
+ * `set_integrator_rate` paths `require!(rate <= MAX_INTEGRATOR_RATE_BPS)` and
49
+ * REJECT anything higher, so a looser SDK cap only builds txs the program
50
+ * refuses. (Self-service paths clamp down to it instead of rejecting.) */
51
+ export declare const MAX_INTEGRATOR_RATE_BPS = 2000;
48
52
  /** Anchor instruction discriminator `sha256("global:add_integrator")[0..8]`. */
49
53
  export declare const ADD_INTEGRATOR_DISCRIMINATOR: Uint8Array;
50
54
  /** Anchor instruction discriminator `sha256("global:set_integrator_rate")[0..8]`. */
@@ -34,7 +34,7 @@
34
34
  * {@link integratorAccountsForRegister} / {@link integratorAccountsForCreateVoucher}
35
35
  * to build the pair.
36
36
  * - The rate is read from the on-chain `AllowlistEntry`, never trusted from
37
- * the transaction. It is capped at {@link MAX_INTEGRATOR_RATE_BPS} (40%);
37
+ * the transaction. It is capped at {@link MAX_INTEGRATOR_RATE_BPS} (20%);
38
38
  * {@link DEFAULT_INTEGRATOR_RATE_BPS} (20%) applies when `add_integrator`
39
39
  * omits a rate.
40
40
  */
@@ -43,8 +43,12 @@ import { encodeBase58, decodeBase58_32 } from "./base58.js";
43
43
  export const INTEGRATOR_SEED = "integrator";
44
44
  /** Default integrator share when `add_integrator` omits a rate: 20%. */
45
45
  export const DEFAULT_INTEGRATOR_RATE_BPS = 2_000;
46
- /** Hard cap on an integrator's share of a mint fee: 40%. */
47
- export const MAX_INTEGRATOR_RATE_BPS = 4_000;
46
+ /** Hard cap on an integrator's share of a mint fee: 20%. Mirrors the program's
47
+ * `MAX_INTEGRATOR_RATE_BPS` (lib.rs) — the admin `register_integrator` /
48
+ * `set_integrator_rate` paths `require!(rate <= MAX_INTEGRATOR_RATE_BPS)` and
49
+ * REJECT anything higher, so a looser SDK cap only builds txs the program
50
+ * refuses. (Self-service paths clamp down to it instead of rejecting.) */
51
+ export const MAX_INTEGRATOR_RATE_BPS = 2_000;
48
52
  /** Anchor instruction discriminator `sha256("global:add_integrator")[0..8]`. */
49
53
  export const ADD_INTEGRATOR_DISCRIMINATOR = Uint8Array.from([85, 249, 72, 201, 17, 187, 227, 38]);
50
54
  /** Anchor instruction discriminator `sha256("global:set_integrator_rate")[0..8]`. */
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Emergency lockdown (global pause) — the off-chain READER for the registry's
3
+ * trailing `Config` pause region, plus transport-neutral builders for the two
4
+ * instructions that write it. The on-chain side lives in
5
+ * `programs/x1-handles/src/{state,lib}.rs` (PR feat/emergency-pause); this
6
+ * module only READS the region and BUILDS the instructions — it never changes
7
+ * the on-chain authority rules (pause = admin OR any guardian; unpause = admin
8
+ * only; both enforced by signature on-chain).
9
+ *
10
+ * # The pause region (raw byte layout, mirrored from `Config` in state.rs)
11
+ *
12
+ * It is a TRAILING region appended past the base `Config` struct at a FIXED raw
13
+ * byte offset — NOT a Borsh field, so it is read by absolute offset, never by
14
+ * walking the struct. Relative to the registry program's `Config::LEN` (445):
15
+ *
16
+ * [445] paused: u8 (1 = locked down, 0 = open)
17
+ * [446..454] paused_at: i64 LE (unix secs the lockdown last turned ON; 0 = never)
18
+ * [454..614] pause_guardians: 5 × Pubkey (32 bytes each; all-zero = unused slot)
19
+ *
20
+ * Region length 169; the account grows to 614 bytes on first write. A `Config`
21
+ * created before this region existed (or any account shorter than 614 bytes)
22
+ * reads as NOT paused with NO guardians — exactly as the program's
23
+ * `require_not_locked` treats it. This reader therefore NEVER throws on a short
24
+ * account; it returns the open state.
25
+ *
26
+ * Hand-rolled like the rest of this package — no Anchor client, no
27
+ * `@solana/web3.js` import (it stays an optional peer). Builders return a
28
+ * transport-neutral {@link BuiltInstruction}; adapt to web3.js with the
29
+ * two-line snippet in `delegate.ts`'s module docs.
30
+ */
31
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
32
+ /** Raw byte offset of the pause region — the registry program's `Config::LEN`.
33
+ * The region is read by this ABSOLUTE offset, never by walking Borsh fields. */
34
+ export declare const PAUSE_EXT_OFFSET = 445;
35
+ /** Number of guardian slots in the region (mirrors `Config::PAUSE_GUARDIAN_SLOTS`). */
36
+ export declare const PAUSE_GUARDIAN_SLOTS = 5;
37
+ /** Length of the region: paused(1) + paused_at(8) + 5 × Pubkey(32) = 169. */
38
+ export declare const PAUSE_EXT_LEN: number;
39
+ /** First byte PAST the region. An account shorter than this has NO region. */
40
+ export declare const PAUSE_EXT_END: number;
41
+ /** Decoded pause state of the registry `Config` account. */
42
+ export interface PauseState {
43
+ /** `true` iff the lockdown flag is set (byte 445 === 1). */
44
+ readonly paused: boolean;
45
+ /** Unix seconds the lockdown last turned on; `0n` when never / short account. */
46
+ readonly pausedAt: bigint;
47
+ /** The configured guardian pubkeys (base58), with all-zero (unused) slots dropped. */
48
+ readonly guardians: string[];
49
+ }
50
+ /**
51
+ * Decode the pause region from a registry `Config` account's raw data.
52
+ *
53
+ * Robust by contract: an account whose data is SHORTER than {@link PAUSE_EXT_END}
54
+ * (614) has no region and reads as `{ paused: false, pausedAt: 0n, guardians: [] }`
55
+ * — the same "absent region ⇒ open" rule the on-chain `require_not_locked`
56
+ * enforces. Never throws on a short or malformed-length account.
57
+ */
58
+ export declare function readPauseState(data: Uint8Array): PauseState;
59
+ /** `set_lockdown(paused: bool)` — Anchor discriminator (final, program author). */
60
+ export declare const SET_LOCKDOWN_DISCRIMINATOR: Uint8Array;
61
+ /** `set_pause_guardians(guardians: [Pubkey;5])` — Anchor discriminator (final). */
62
+ export declare const SET_PAUSE_GUARDIANS_DISCRIMINATOR: Uint8Array;
63
+ export interface SetLockdownParams {
64
+ readonly programId: AddressLike;
65
+ /** The signer — any guardian OR `Config.admin` for a PAUSE; `Config.admin`
66
+ * ONLY for an UNPAUSE (the program enforces this by signature — the builder
67
+ * does not, since it cannot know the account's admin/guardians). */
68
+ readonly caller: AddressLike;
69
+ /** The `["config"]` PDA (writable). */
70
+ readonly config: AddressLike;
71
+ /** The `["market_config"]` PDA (writable) — `set_lockdown` mirrors the flag
72
+ * onto MarketConfig so listings/offers/auctions stop together. */
73
+ readonly marketConfig: AddressLike;
74
+ /** `true` = lock down, `false` = reopen. */
75
+ readonly paused: boolean;
76
+ }
77
+ /**
78
+ * Build `set_lockdown(paused)`. Accounts IN ORDER (mirrors lib.rs):
79
+ * 0 caller (signer, writable)
80
+ * 1 config (writable) — ["config"]
81
+ * 2 market_config (writable) — ["market_config"]
82
+ * 3 system_program
83
+ * data = disc(8) ‖ paused(1 byte: 1 = lock / 0 = unlock).
84
+ */
85
+ export declare function buildSetLockdownIx(p: SetLockdownParams): BuiltInstruction;
86
+ export interface SetPauseGuardiansParams {
87
+ readonly programId: AddressLike;
88
+ /** `Config.admin` — the ONLY key the program accepts (admin-only). Signs; writable. */
89
+ readonly admin: AddressLike;
90
+ /** The `["config"]` PDA (writable). */
91
+ readonly config: AddressLike;
92
+ /** Exactly 5 guardian pubkeys in slot order; a zero address (or the system
93
+ * program) marks an unused slot. */
94
+ readonly guardians: ReadonlyArray<AddressLike>;
95
+ }
96
+ /**
97
+ * Build `set_pause_guardians([Pubkey;5])`. Admin-only. Accounts IN ORDER:
98
+ * 0 admin (signer, writable)
99
+ * 1 config (writable) — ["config"]
100
+ * 2 system_program
101
+ * data = disc(8) ‖ 5 × 32 raw pubkey bytes in order (fixed `[Pubkey;5]`, NO
102
+ * length prefix). Pass fewer than 5 to zero-fill the trailing slots.
103
+ */
104
+ export declare function buildSetPauseGuardiansIx(p: SetPauseGuardiansParams): BuiltInstruction;
package/dist/pause.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Emergency lockdown (global pause) — the off-chain READER for the registry's
3
+ * trailing `Config` pause region, plus transport-neutral builders for the two
4
+ * instructions that write it. The on-chain side lives in
5
+ * `programs/x1-handles/src/{state,lib}.rs` (PR feat/emergency-pause); this
6
+ * module only READS the region and BUILDS the instructions — it never changes
7
+ * the on-chain authority rules (pause = admin OR any guardian; unpause = admin
8
+ * only; both enforced by signature on-chain).
9
+ *
10
+ * # The pause region (raw byte layout, mirrored from `Config` in state.rs)
11
+ *
12
+ * It is a TRAILING region appended past the base `Config` struct at a FIXED raw
13
+ * byte offset — NOT a Borsh field, so it is read by absolute offset, never by
14
+ * walking the struct. Relative to the registry program's `Config::LEN` (445):
15
+ *
16
+ * [445] paused: u8 (1 = locked down, 0 = open)
17
+ * [446..454] paused_at: i64 LE (unix secs the lockdown last turned ON; 0 = never)
18
+ * [454..614] pause_guardians: 5 × Pubkey (32 bytes each; all-zero = unused slot)
19
+ *
20
+ * Region length 169; the account grows to 614 bytes on first write. A `Config`
21
+ * created before this region existed (or any account shorter than 614 bytes)
22
+ * reads as NOT paused with NO guardians — exactly as the program's
23
+ * `require_not_locked` treats it. This reader therefore NEVER throws on a short
24
+ * account; it returns the open state.
25
+ *
26
+ * Hand-rolled like the rest of this package — no Anchor client, no
27
+ * `@solana/web3.js` import (it stays an optional peer). Builders return a
28
+ * transport-neutral {@link BuiltInstruction}; adapt to web3.js with the
29
+ * two-line snippet in `delegate.ts`'s module docs.
30
+ */
31
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
32
+ /** Raw byte offset of the pause region — the registry program's `Config::LEN`.
33
+ * The region is read by this ABSOLUTE offset, never by walking Borsh fields. */
34
+ export const PAUSE_EXT_OFFSET = 445;
35
+ /** Number of guardian slots in the region (mirrors `Config::PAUSE_GUARDIAN_SLOTS`). */
36
+ export const PAUSE_GUARDIAN_SLOTS = 5;
37
+ /** Length of the region: paused(1) + paused_at(8) + 5 × Pubkey(32) = 169. */
38
+ export const PAUSE_EXT_LEN = 1 + 8 + 32 * PAUSE_GUARDIAN_SLOTS;
39
+ /** First byte PAST the region. An account shorter than this has NO region. */
40
+ export const PAUSE_EXT_END = PAUSE_EXT_OFFSET + PAUSE_EXT_LEN; // 614
41
+ /**
42
+ * Decode the pause region from a registry `Config` account's raw data.
43
+ *
44
+ * Robust by contract: an account whose data is SHORTER than {@link PAUSE_EXT_END}
45
+ * (614) has no region and reads as `{ paused: false, pausedAt: 0n, guardians: [] }`
46
+ * — the same "absent region ⇒ open" rule the on-chain `require_not_locked`
47
+ * enforces. Never throws on a short or malformed-length account.
48
+ */
49
+ export function readPauseState(data) {
50
+ if (data.length < PAUSE_EXT_END) {
51
+ return { paused: false, pausedAt: 0n, guardians: [] };
52
+ }
53
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
54
+ const paused = data[PAUSE_EXT_OFFSET] === 1;
55
+ // paused_at: i64 LE at offset 446.
56
+ const pausedAt = dv.getBigInt64(PAUSE_EXT_OFFSET + 1, true);
57
+ // Five 32-byte pubkeys from offset 454; drop all-zero (unused) slots.
58
+ const guardians = [];
59
+ for (let i = 0; i < PAUSE_GUARDIAN_SLOTS; i++) {
60
+ const o = PAUSE_EXT_OFFSET + 9 + 32 * i;
61
+ const slot = data.subarray(o, o + 32);
62
+ let allZero = true;
63
+ for (let j = 0; j < slot.length; j++) {
64
+ if (slot[j] !== 0) {
65
+ allZero = false;
66
+ break;
67
+ }
68
+ }
69
+ if (allZero)
70
+ continue;
71
+ guardians.push(encodeBase58(slot));
72
+ }
73
+ return { paused, pausedAt, guardians };
74
+ }
75
+ // ===== Instruction builders ================================================
76
+ //
77
+ // Anchor instruction data = 8-byte discriminator ‖ Borsh-encoded args. The
78
+ // discriminators are the program author's final values (pinned by a byte test
79
+ // in sdk/test/pause.test.js), NOT re-derived here, so this module needs no
80
+ // sha256. Derive the PDAs with your runtime's `findProgramAddress`:
81
+ // config = ["config"] under the registry program
82
+ // market_config = ["market_config"] under the registry program
83
+ /** `set_lockdown(paused: bool)` — Anchor discriminator (final, program author). */
84
+ export const SET_LOCKDOWN_DISCRIMINATOR = Uint8Array.from([
85
+ 50, 46, 87, 85, 162, 169, 232, 61,
86
+ ]);
87
+ /** `set_pause_guardians(guardians: [Pubkey;5])` — Anchor discriminator (final). */
88
+ export const SET_PAUSE_GUARDIANS_DISCRIMINATOR = Uint8Array.from([
89
+ 6, 7, 64, 90, 220, 50, 172, 166,
90
+ ]);
91
+ /** The System Program id — the final account on both instructions. Not
92
+ * re-exported (adminMarket.ts already owns that public name); internal here. */
93
+ const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
94
+ function toBytes32(v, what) {
95
+ if (typeof v === "string") {
96
+ const b = decodeBase58_32(v);
97
+ if (!b)
98
+ throw new Error(`${what} is not a valid base58 address`);
99
+ return b;
100
+ }
101
+ if (v.length !== 32)
102
+ throw new Error(`${what} must be exactly 32 bytes`);
103
+ return v;
104
+ }
105
+ function toBase58(v, what) {
106
+ return encodeBase58(toBytes32(v, what));
107
+ }
108
+ /**
109
+ * Build `set_lockdown(paused)`. Accounts IN ORDER (mirrors lib.rs):
110
+ * 0 caller (signer, writable)
111
+ * 1 config (writable) — ["config"]
112
+ * 2 market_config (writable) — ["market_config"]
113
+ * 3 system_program
114
+ * data = disc(8) ‖ paused(1 byte: 1 = lock / 0 = unlock).
115
+ */
116
+ export function buildSetLockdownIx(p) {
117
+ const data = new Uint8Array(8 + 1);
118
+ data.set(SET_LOCKDOWN_DISCRIMINATOR, 0);
119
+ data[8] = p.paused ? 1 : 0;
120
+ const keys = [
121
+ { pubkey: toBase58(p.caller, "caller"), isSigner: true, isWritable: true },
122
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
123
+ { pubkey: toBase58(p.marketConfig, "marketConfig"), isSigner: false, isWritable: true },
124
+ { pubkey: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false },
125
+ ];
126
+ return { programId: toBase58(p.programId, "programId"), keys, data };
127
+ }
128
+ /**
129
+ * Build `set_pause_guardians([Pubkey;5])`. Admin-only. Accounts IN ORDER:
130
+ * 0 admin (signer, writable)
131
+ * 1 config (writable) — ["config"]
132
+ * 2 system_program
133
+ * data = disc(8) ‖ 5 × 32 raw pubkey bytes in order (fixed `[Pubkey;5]`, NO
134
+ * length prefix). Pass fewer than 5 to zero-fill the trailing slots.
135
+ */
136
+ export function buildSetPauseGuardiansIx(p) {
137
+ if (p.guardians.length > PAUSE_GUARDIAN_SLOTS) {
138
+ throw new Error(`at most ${PAUSE_GUARDIAN_SLOTS} guardians, got ${p.guardians.length}`);
139
+ }
140
+ const data = new Uint8Array(8 + 32 * PAUSE_GUARDIAN_SLOTS);
141
+ data.set(SET_PAUSE_GUARDIANS_DISCRIMINATOR, 0);
142
+ for (let i = 0; i < p.guardians.length; i++) {
143
+ data.set(toBytes32(p.guardians[i], `guardians[${i}]`), 8 + 32 * i);
144
+ }
145
+ // Slots past guardians.length stay zero = unused.
146
+ const keys = [
147
+ { pubkey: toBase58(p.admin, "admin"), isSigner: true, isWritable: true },
148
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
149
+ { pubkey: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false },
150
+ ];
151
+ return { programId: toBase58(p.programId, "programId"), keys, data };
152
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The ONE payment-resolution rule (W16, owner ruling 2026-10-07):
3
+ * **verified-record-else-owner, uniform on every payment surface.**
4
+ *
5
+ * A handle's per-chain address `Record` can be written by a record-editing
6
+ * DELEGATE, not just the owner — so an UNVERIFIED record must never receive
7
+ * money: a malicious delegate could silently redirect payments. On-chain
8
+ * VERIFICATION (`verify_record_svm` — the recorded address itself co-signed)
9
+ * is the guard, so the rule every pay surface shares is:
10
+ *
11
+ * 1. a LIVE, VERIFIED record for the payment chain → that record's address
12
+ * (`basis: "verified-record"`). Staleness is judged first: a record from
13
+ * a previous registration epoch or before a `clear_records` wipe never
14
+ * counts, verified bit or not (docs/record-trust.md — the `stale` flag
15
+ * records.ts already computes).
16
+ * 2. otherwise → the handle's CURRENT AUTHORITY (`basis: "owner"`):
17
+ * `Handle.owner` untokenized, the pNFT holder once tokenized. An
18
+ * unverified record is IGNORED, not an error.
19
+ * 3. no authority known either (e.g. a tokenized name whose holder could
20
+ * not be located) → `null`: there is no honest payment address.
21
+ *
22
+ * Every surface that shows a "pay @name" flow must also SHOW the basis next
23
+ * to the resolved address before the user confirms ("verified X1 record" vs
24
+ * "owner wallet") — the rule is only safe when it is visible.
25
+ */
26
+ import type { Chain } from "./types.js";
27
+ /** Why `address` is the one to pay: a live on-chain-verified record for the
28
+ * chain, or the handle's current authority (owner / NFT holder). */
29
+ export type PaymentBasis = "verified-record" | "owner";
30
+ export interface PaymentResolution {
31
+ /** The address a payment to this name must target. */
32
+ readonly address: string;
33
+ readonly basis: PaymentBasis;
34
+ }
35
+ /** The slice of a decoded record the rule needs — satisfied structurally by
36
+ * the SDK's `HandleRecord` (records.ts) and the app's `DecodedRecord`. */
37
+ export interface PaymentRecordLike {
38
+ /** Which chain the record's coin_type maps to (null = unknown coin type). */
39
+ readonly chain: Chain | null;
40
+ /** Human-readable address stored in the record. */
41
+ readonly address: string;
42
+ /** `Record.verified` as decoded — already forced false for stale records by
43
+ * both decoders, but `stale` is re-checked here regardless. */
44
+ readonly verified: boolean;
45
+ /** The staleness verdict (updated_at vs registered_at / records_cleared_at). */
46
+ readonly stale: boolean;
47
+ }
48
+ /**
49
+ * Apply the rule. `records` may be the FULL decoded list (stale ones
50
+ * included — they are re-excluded here, never trusted on the `verified` bit
51
+ * alone). `authority` is the handle's CURRENT authority — `Handle.owner`
52
+ * untokenized, the pNFT holder tokenized — or `null` when it is unknown
53
+ * (tokenized, holder not found): then a missing verified record yields
54
+ * `null`, the honest "no payment address exists".
55
+ */
56
+ export declare function paymentAddressFor(records: readonly PaymentRecordLike[], authority: string | null, chain?: Chain): PaymentResolution | null;
57
+ /** The user-facing basis line, so every surface words it identically. */
58
+ export declare function paymentBasisLabel(basis: PaymentBasis, chain?: Chain): string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The ONE payment-resolution rule (W16, owner ruling 2026-10-07):
3
+ * **verified-record-else-owner, uniform on every payment surface.**
4
+ *
5
+ * A handle's per-chain address `Record` can be written by a record-editing
6
+ * DELEGATE, not just the owner — so an UNVERIFIED record must never receive
7
+ * money: a malicious delegate could silently redirect payments. On-chain
8
+ * VERIFICATION (`verify_record_svm` — the recorded address itself co-signed)
9
+ * is the guard, so the rule every pay surface shares is:
10
+ *
11
+ * 1. a LIVE, VERIFIED record for the payment chain → that record's address
12
+ * (`basis: "verified-record"`). Staleness is judged first: a record from
13
+ * a previous registration epoch or before a `clear_records` wipe never
14
+ * counts, verified bit or not (docs/record-trust.md — the `stale` flag
15
+ * records.ts already computes).
16
+ * 2. otherwise → the handle's CURRENT AUTHORITY (`basis: "owner"`):
17
+ * `Handle.owner` untokenized, the pNFT holder once tokenized. An
18
+ * unverified record is IGNORED, not an error.
19
+ * 3. no authority known either (e.g. a tokenized name whose holder could
20
+ * not be located) → `null`: there is no honest payment address.
21
+ *
22
+ * Every surface that shows a "pay @name" flow must also SHOW the basis next
23
+ * to the resolved address before the user confirms ("verified X1 record" vs
24
+ * "owner wallet") — the rule is only safe when it is visible.
25
+ */
26
+ /**
27
+ * Apply the rule. `records` may be the FULL decoded list (stale ones
28
+ * included — they are re-excluded here, never trusted on the `verified` bit
29
+ * alone). `authority` is the handle's CURRENT authority — `Handle.owner`
30
+ * untokenized, the pNFT holder tokenized — or `null` when it is unknown
31
+ * (tokenized, holder not found): then a missing verified record yields
32
+ * `null`, the honest "no payment address exists".
33
+ */
34
+ export function paymentAddressFor(records, authority, chain = "X1") {
35
+ const rec = records.find((r) => r.chain === chain && !r.stale && r.verified);
36
+ if (rec)
37
+ return { address: rec.address, basis: "verified-record" };
38
+ if (authority !== null)
39
+ return { address: authority, basis: "owner" };
40
+ return null;
41
+ }
42
+ /** The user-facing basis line, so every surface words it identically. */
43
+ export function paymentBasisLabel(basis, chain = "X1") {
44
+ return basis === "verified-record" ? `verified ${chain} record` : "owner wallet";
45
+ }
package/dist/scoped.d.ts CHANGED
@@ -68,16 +68,21 @@ export declare function namespaceIsActive(data: Uint8Array): boolean;
68
68
  * dependency), and so tests can supply a deterministic fake.
69
69
  */
70
70
  export interface ScopedHandleDeriver {
71
- /** The `["handle", label, name]` PDA under `programId`, base58. */
71
+ /** The `["handle", [label_len], label, name]` PDA under `programId`, base58
72
+ * (H1: the label is length-prefixed so a scoped name never collides with a
73
+ * bare `@<label><name>`). */
72
74
  scopedHandleKey(label: string, name: string, programId: string): Promise<string> | string;
73
75
  }
74
76
  /**
75
77
  * Build the default scoped-handle deriver using `@solana/web3.js`.
76
78
  *
77
- * Seeds are `["handle", label, name]` under the registry program — exactly the
78
- * order `tools/api`'s `scoped_handle_pda` and the program's
79
- * `register_under_namespace` use, so the derived PDA is the account the name
80
- * actually lives at. Prefers an INJECTED web3 module (robust under `file:` /
79
+ * Seeds are `["handle", [label_len], label, name]` under the registry program
80
+ * (H1, 2026-10-06: the label is LENGTH-PREFIXED with a single byte = its UTF-8
81
+ * byte length, so a scoped `name.tld` never derives the same address as a bare
82
+ * `@<label><name>` — e.g. `ck.ja` ≠ `@jack`). This MUST match byte-for-byte the
83
+ * program's `verify_handle_pda_core` / `register_under_namespace` and
84
+ * `tools/api`'s `scoped_handle_pda`, or resolution breaks. Prefers an INJECTED
85
+ * web3 module (robust under `file:` /
81
86
  * `npm link`, where a bare specifier resolves from THIS package's realpath, not
82
87
  * the consumer's); falls back to a dynamic import for a normal install.
83
88
  */
package/dist/scoped.js CHANGED
@@ -93,10 +93,13 @@ export function namespaceIsActive(data) {
93
93
  /**
94
94
  * Build the default scoped-handle deriver using `@solana/web3.js`.
95
95
  *
96
- * Seeds are `["handle", label, name]` under the registry program — exactly the
97
- * order `tools/api`'s `scoped_handle_pda` and the program's
98
- * `register_under_namespace` use, so the derived PDA is the account the name
99
- * actually lives at. Prefers an INJECTED web3 module (robust under `file:` /
96
+ * Seeds are `["handle", [label_len], label, name]` under the registry program
97
+ * (H1, 2026-10-06: the label is LENGTH-PREFIXED with a single byte = its UTF-8
98
+ * byte length, so a scoped `name.tld` never derives the same address as a bare
99
+ * `@<label><name>` — e.g. `ck.ja` ≠ `@jack`). This MUST match byte-for-byte the
100
+ * program's `verify_handle_pda_core` / `register_under_namespace` and
101
+ * `tools/api`'s `scoped_handle_pda`, or resolution breaks. Prefers an INJECTED
102
+ * web3 module (robust under `file:` /
100
103
  * `npm link`, where a bare specifier resolves from THIS package's realpath, not
101
104
  * the consumer's); falls back to a dynamic import for a normal install.
102
105
  */
@@ -119,7 +122,10 @@ export async function makeScopedHandleDeriver(web3Module) {
119
122
  return {
120
123
  scopedHandleKey(label, name, programId) {
121
124
  const program = new web3.PublicKey(programId);
122
- const [key] = web3.PublicKey.findProgramAddressSync([handleSeed, enc.encode(label), enc.encode(name)], program);
125
+ // H1: length-prefix the label with its UTF-8 BYTE length (matches Rust
126
+ // `label.len() as u8`; labels are a–z0–9- ≤ 16 bytes so a u8 suffices).
127
+ const labelBytes = enc.encode(label);
128
+ const [key] = web3.PublicKey.findProgramAddressSync([handleSeed, Uint8Array.of(labelBytes.length), labelBytes, enc.encode(name)], program);
123
129
  return key.toBase58();
124
130
  },
125
131
  };
package/dist/signin.js CHANGED
@@ -373,7 +373,7 @@ export async function resolveIdentity(cfg, address) {
373
373
  const info = await reader.accountInfo(pda);
374
374
  const handleAcct = info ? parseHandleAccount(info.data) : null;
375
375
  if (handleAcct) {
376
- const all = await fetchAttestations(reader.rpc, encodeBase58(programBytes), pda, handleAcct.registeredAt, handleAcct.recordsClearedAt);
376
+ const all = await fetchAttestations(reader.rpc, encodeBase58(programBytes), pda, handleAcct.registeredAt, handleAcct.recordsClearedAt, handleAcct.holderEpoch);
377
377
  const live = liveAttestations(all);
378
378
  verified = live.length > 0;
379
379
  verifications = live.map((a) => ({