@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/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.
|
|
11
|
-
*
|
|
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 `
|
|
38
|
-
* (name → `deriveHandleAccount` → decode).
|
|
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.
|
|
11
|
-
*
|
|
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 `
|
|
33
|
-
* (name → `deriveHandleAccount` → decode).
|
|
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/gateClient.d.ts
CHANGED
|
@@ -80,14 +80,16 @@ export interface GuardResult {
|
|
|
80
80
|
* silently admitting an unverified caller. `onError: "deny"` instead resolves to
|
|
81
81
|
* a clean `allowed: false` (fail-closed without a throw).
|
|
82
82
|
*
|
|
83
|
-
* ⚠️ SECURITY — `onError: "allow"` FAILS OPEN
|
|
84
|
-
* timeout, 5xx, auth failure, a malformed
|
|
85
|
-
* `passed: true` decision and admits the caller
|
|
86
|
-
*
|
|
87
|
-
* timeout)
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
83
|
+
* ⚠️ SECURITY — `onError: "allow"` FAILS OPEN, and is NEVER acceptable on a real
|
|
84
|
+
* gate. On ANY error (network blip, timeout, 5xx, auth failure, a malformed
|
|
85
|
+
* response) it fabricates a `passed: true` decision and admits the caller
|
|
86
|
+
* UNVERIFIED — so anyone who can make your gate call fail (trivial: induce a
|
|
87
|
+
* timeout) bypasses it completely. That makes it strictly WORSE than no gate for
|
|
88
|
+
* anything it is supposed to protect: a route, a mint, an airdrop, a write, an
|
|
89
|
+
* allowlist. It exists ONLY for soft, non-security personalization where a
|
|
90
|
+
* missing verification must not degrade UX (e.g. optionally showing a "verified"
|
|
91
|
+
* flourish). Constructing a guard with it logs a one-time `console.warn`. If the
|
|
92
|
+
* decision gates ANYTHING, use the default `"throw"` or `"deny"` (fail-closed).
|
|
91
93
|
*
|
|
92
94
|
* @example
|
|
93
95
|
* ```ts
|
package/dist/gateClient.js
CHANGED
|
@@ -81,14 +81,16 @@ export async function checkGate(cfg, subject, policy) {
|
|
|
81
81
|
* silently admitting an unverified caller. `onError: "deny"` instead resolves to
|
|
82
82
|
* a clean `allowed: false` (fail-closed without a throw).
|
|
83
83
|
*
|
|
84
|
-
* ⚠️ SECURITY — `onError: "allow"` FAILS OPEN
|
|
85
|
-
* timeout, 5xx, auth failure, a malformed
|
|
86
|
-
* `passed: true` decision and admits the caller
|
|
87
|
-
*
|
|
88
|
-
* timeout)
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
84
|
+
* ⚠️ SECURITY — `onError: "allow"` FAILS OPEN, and is NEVER acceptable on a real
|
|
85
|
+
* gate. On ANY error (network blip, timeout, 5xx, auth failure, a malformed
|
|
86
|
+
* response) it fabricates a `passed: true` decision and admits the caller
|
|
87
|
+
* UNVERIFIED — so anyone who can make your gate call fail (trivial: induce a
|
|
88
|
+
* timeout) bypasses it completely. That makes it strictly WORSE than no gate for
|
|
89
|
+
* anything it is supposed to protect: a route, a mint, an airdrop, a write, an
|
|
90
|
+
* allowlist. It exists ONLY for soft, non-security personalization where a
|
|
91
|
+
* missing verification must not degrade UX (e.g. optionally showing a "verified"
|
|
92
|
+
* flourish). Constructing a guard with it logs a one-time `console.warn`. If the
|
|
93
|
+
* decision gates ANYTHING, use the default `"throw"` or `"deny"` (fail-closed).
|
|
92
94
|
*
|
|
93
95
|
* @example
|
|
94
96
|
* ```ts
|
|
@@ -106,6 +108,10 @@ export async function checkGate(cfg, subject, policy) {
|
|
|
106
108
|
*/
|
|
107
109
|
export function requireGate(cfg, policy, opts = {}) {
|
|
108
110
|
const onError = opts.onError ?? "throw";
|
|
111
|
+
// Warn HARD (once) the moment a fail-open guard is built — `onError: "allow"`
|
|
112
|
+
// admits unverified callers on any error and must never guard anything real.
|
|
113
|
+
if (onError === "allow")
|
|
114
|
+
warnFailOpenOnce();
|
|
109
115
|
return async (subject) => {
|
|
110
116
|
try {
|
|
111
117
|
const decision = await checkGate(cfg, subject, policy);
|
|
@@ -126,3 +132,16 @@ export function requireGate(cfg, policy, opts = {}) {
|
|
|
126
132
|
}
|
|
127
133
|
};
|
|
128
134
|
}
|
|
135
|
+
/** Process-wide latch so the fail-open warning fires at most once, not on every
|
|
136
|
+
* guard construction. */
|
|
137
|
+
let failOpenWarned = false;
|
|
138
|
+
function warnFailOpenOnce() {
|
|
139
|
+
if (failOpenWarned)
|
|
140
|
+
return;
|
|
141
|
+
failOpenWarned = true;
|
|
142
|
+
// eslint-disable-next-line no-console
|
|
143
|
+
console.warn("[@x1id/resolve] requireGate({ onError: \"allow\" }) FAILS OPEN: on any gate-call " +
|
|
144
|
+
"error it admits the caller UNVERIFIED. Never use it to protect a route, mint, " +
|
|
145
|
+
"airdrop, write, or allowlist — use the default \"throw\" or \"deny\" instead. " +
|
|
146
|
+
"(This warning is for soft personalization-only uses; it logs once.)");
|
|
147
|
+
}
|
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
|
|
10
|
-
* const r = createResolver({ rpcUrl: "https://rpc.
|
|
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
|
|
10
|
-
* const r = createResolver({ rpcUrl: "https://rpc.
|
|
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";
|
package/dist/pause.d.ts
ADDED
|
@@ -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;
|
package/dist/payment.js
ADDED
|
@@ -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
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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
|
-
|
|
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
|
};
|