@x1id/resolve 0.2.1 → 0.6.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 +258 -8
- package/dist/accounts.d.ts +93 -0
- package/dist/accounts.js +190 -0
- package/dist/agent.d.ts +186 -0
- package/dist/agent.js +213 -0
- package/dist/attestation.d.ts +226 -0
- package/dist/attestation.js +290 -0
- package/dist/clearRecords.d.ts +60 -0
- package/dist/clearRecords.js +69 -0
- package/dist/control.d.ts +201 -0
- package/dist/control.js +316 -0
- package/dist/delegate.d.ts +153 -0
- package/dist/delegate.js +166 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +66 -179
- package/dist/integrator.d.ts +117 -0
- package/dist/integrator.js +166 -0
- package/dist/lock.d.ts +180 -0
- package/dist/lock.js +211 -0
- package/dist/recordCount.d.ts +98 -0
- package/dist/recordCount.js +114 -0
- package/dist/recordWrite.d.ts +136 -0
- package/dist/recordWrite.js +228 -0
- package/dist/records.d.ts +130 -0
- package/dist/records.js +195 -0
- package/dist/register.d.ts +119 -0
- package/dist/register.js +183 -0
- package/dist/subname.d.ts +282 -0
- package/dist/subname.js +371 -0
- package/dist/textRecords.d.ts +259 -0
- package/dist/textRecords.js +368 -0
- package/dist/voucher.d.ts +130 -0
- package/dist/voucher.js +185 -0
- package/dist/x402.d.ts +107 -0
- package/dist/x402.js +76 -0
- package/package.json +9 -1
- package/schema/agent-manifest.json +91 -0
- package/wasm/x1_resolve_wasm.wasm +0 -0
package/dist/lock.d.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name locks (#7377): decode helpers for the on-chain lock state, plus the
|
|
3
|
+
* instruction builders for `lock_handle` and the timelocked unlock flow
|
|
4
|
+
* (`initiate_unlock` / `complete_unlock` / `cancel_unlock`).
|
|
5
|
+
*
|
|
6
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
7
|
+
* `@solana/web3.js` import (it stays an optional peer). Builders return the
|
|
8
|
+
* transport-neutral {@link BuiltInstruction} `delegate.ts` uses; adapt to
|
|
9
|
+
* web3.js exactly as that module's header documents.
|
|
10
|
+
*
|
|
11
|
+
* # The lock extension, mirrored from state.rs's EXTENSION OFFSET REGISTRY
|
|
12
|
+
*
|
|
13
|
+
* A `Handle` account's struct is frozen at 157 bytes (`8 + INIT_SPACE`);
|
|
14
|
+
* features append fixed-offset raw regions past it:
|
|
15
|
+
*
|
|
16
|
+
* offset size region
|
|
17
|
+
* 157 33 NFT capability extension (presence tag + mint pubkey)
|
|
18
|
+
* 190 1 RESERVED, permanently-unused gap (#7377 — see below)
|
|
19
|
+
* 191 5 record_count: presence tag + u32 LE live-record count
|
|
20
|
+
* 196 9 lock: locked(u8) + unlock_initiated_at(i64 LE) ← this module
|
|
21
|
+
* 205 — next free offset
|
|
22
|
+
*
|
|
23
|
+
* The lock region lives at 196, PAST record_count — NOT at the byte 190 that
|
|
24
|
+
* was originally reserved for it. The shipped feature needs 9 bytes (a flag
|
|
25
|
+
* plus an `unlock_initiated_at` timestamp for the timelocked unlock), which
|
|
26
|
+
* does not fit in one byte, and record_count (191–195) is deployed with live
|
|
27
|
+
* data whose offset cannot move. Byte 190 stays a documented, zeroed gap.
|
|
28
|
+
*
|
|
29
|
+
* # No presence tag — unlocked and uninitialized are the same value
|
|
30
|
+
*
|
|
31
|
+
* Unlike record_count (whose `0` count is distinct from "uninitialized"), the
|
|
32
|
+
* lock uses NO presence byte: `locked === 0` — which is also what a short or
|
|
33
|
+
* never-locked account reads as — IS the unlocked state. So {@link readLock}
|
|
34
|
+
* is total: a pre-lock handle (account too short) and a zeroed flag both read
|
|
35
|
+
* as `false` (unlocked), never an error and never a wrong answer.
|
|
36
|
+
*
|
|
37
|
+
* # The wire rules the on-chain program enforces (mirror of lib.rs)
|
|
38
|
+
*
|
|
39
|
+
* - All four instructions are gated by the STRICT authority check
|
|
40
|
+
* (`require_current_authority` in lib.rs): the untokenized owner, OR —
|
|
41
|
+
* for a TOKENIZED handle — the current NFT holder, who appends their ATA for
|
|
42
|
+
* the handle's NFT mint as the first extra (remaining) account
|
|
43
|
+
* ({@link LockParams.holderTokenAccount} etc.).
|
|
44
|
+
* - `lock_handle` refuses if a live `Listing` (`HandleListedCannotLock`, 6053)
|
|
45
|
+
* or `Auction` (`AuctionLive`, 6031) exists — a lock and an active market are
|
|
46
|
+
* mutually exclusive. It grows the handle account through the lock region on
|
|
47
|
+
* first lock (paid by `payer`), so `payer` must be a writable signer.
|
|
48
|
+
* - HARD LOCK: while locked, NOTHING about the handle can change. All 23
|
|
49
|
+
* mutating instructions refuse with `HandleLocked` (6049) — the 11
|
|
50
|
+
* name-MOVING ones (`transfer`, `release_handle`, `list_handle`, `buy_handle`,
|
|
51
|
+
* `accept_offer`, `start_auction`, `settle_auction`, `mint_handle_nft`,
|
|
52
|
+
* `set_recovery`, `initiate_recovery`, `complete_recovery`) AND the 12
|
|
53
|
+
* record/primary/text/delegate editors (`create_record`, `update_record`,
|
|
54
|
+
* `verify_record_eth`/`svm`/`btc`, `close_record`, `create_text_record`,
|
|
55
|
+
* `update_text_record`, `close_text_record`, `set_record_delegate`,
|
|
56
|
+
* `revoke_record_delegate`, `set_primary`) — so a stolen key cannot redirect a
|
|
57
|
+
* locked handle's addresses. Only `cancel_recovery` and `clear_primary` stay
|
|
58
|
+
* available (the compromised-key owner can still cancel an attacker's
|
|
59
|
+
* recovery; `clear_primary` cannot encumber the handle).
|
|
60
|
+
* - Unlock is timelocked: `initiate_unlock` stamps the clock (refused if not
|
|
61
|
+
* locked, `HandleNotLocked` 6050); `complete_unlock` clears the lock only
|
|
62
|
+
* after `Config.recovery_timelock_secs` has elapsed (`UnlockNotInitiated`
|
|
63
|
+
* 6051 / `UnlockTimelockNotElapsed` 6052); `cancel_unlock` clears a pending
|
|
64
|
+
* unlock but KEEPS the lock — the owner's defence against a stolen-key thief.
|
|
65
|
+
* - `lock_handle` is idempotent: re-locking re-asserts the flag and clears any
|
|
66
|
+
* pending unlock (same effect as `cancel_unlock`).
|
|
67
|
+
*/
|
|
68
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
69
|
+
/** Byte offset of the lock extension: base(157) + NFT ext(33) + reserved gap
|
|
70
|
+
* byte(1) + record_count ext(5) = 196. */
|
|
71
|
+
export declare const LOCK_EXT_OFFSET = 196;
|
|
72
|
+
/** `locked: u8` (1) + `unlock_initiated_at: i64` LE (8). */
|
|
73
|
+
export declare const LOCK_EXT_LEN = 9;
|
|
74
|
+
/** Hard floor (seconds) on the UNLOCK timelock, mirrored from the program's
|
|
75
|
+
* `MIN_UNLOCK_TIMELOCK_SECS`. `complete_unlock` enforces
|
|
76
|
+
* `max(Config.recovery_timelock_secs, MIN_UNLOCK_TIMELOCK_SECS)`, so no config
|
|
77
|
+
* value can collapse the reaction window. A UI computing when an unlock becomes
|
|
78
|
+
* available MUST apply the same clamp — see {@link unlockAvailableAt}. */
|
|
79
|
+
export declare const MIN_UNLOCK_TIMELOCK_SECS: number;
|
|
80
|
+
/**
|
|
81
|
+
* The unix timestamp at which a pending `complete_unlock` becomes valid, given
|
|
82
|
+
* the handle's `unlock_initiated_at` and the config's `recovery_timelock_secs`.
|
|
83
|
+
* Applies the same `MIN_UNLOCK_TIMELOCK_SECS` floor the program enforces, so a
|
|
84
|
+
* UI never shows an ETA earlier than the program will actually allow. Returns
|
|
85
|
+
* `null` when no unlock is pending (`unlockInitiatedAt == 0n`).
|
|
86
|
+
*/
|
|
87
|
+
export declare function unlockAvailableAt(unlockInitiatedAt: bigint, recoveryTimelockSecs: bigint): bigint | null;
|
|
88
|
+
/** Anchor instruction discriminators — `sha256("global:<name>")[0..8]`,
|
|
89
|
+
* pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
|
|
90
|
+
* everywhere it runs) and asserted against a re-derivation in the test suite
|
|
91
|
+
* so a typo can never silently pass. */
|
|
92
|
+
export declare const LOCK_HANDLE_DISCRIMINATOR: Uint8Array;
|
|
93
|
+
export declare const INITIATE_UNLOCK_DISCRIMINATOR: Uint8Array;
|
|
94
|
+
export declare const COMPLETE_UNLOCK_DISCRIMINATOR: Uint8Array;
|
|
95
|
+
export declare const CANCEL_UNLOCK_DISCRIMINATOR: Uint8Array;
|
|
96
|
+
/** The decoded lock state of a `Handle` account. */
|
|
97
|
+
export interface LockState {
|
|
98
|
+
/** Whether the handle is locked — every name-moving instruction refuses. */
|
|
99
|
+
readonly locked: boolean;
|
|
100
|
+
/** Unix timestamp of a pending `initiate_unlock` (`0n` when none is pending).
|
|
101
|
+
* Meaningful only while `locked`; `complete_unlock` can land once
|
|
102
|
+
* `Config.recovery_timelock_secs` has elapsed since this. */
|
|
103
|
+
readonly unlockInitiatedAt: bigint;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Read the lock flag from a `Handle` account's raw data (exactly what an RPC
|
|
107
|
+
* `getAccountInfo` returns for the `["handle", name]` PDA). Mirrors the
|
|
108
|
+
* program's `Handle::read_lock` tolerance exactly: a short account (a handle
|
|
109
|
+
* registered or grown before the lock region existed) and a zeroed flag both
|
|
110
|
+
* read as `false` (unlocked) — never an error, never a wrong answer.
|
|
111
|
+
*/
|
|
112
|
+
export declare function readLock(data: Uint8Array): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Read the pending-unlock timestamp (`0n` == none pending). Tolerant exactly
|
|
115
|
+
* like {@link readLock}: a region too short to hold the i64 reads as `0n`.
|
|
116
|
+
* Meaningful only while {@link readLock} is `true`.
|
|
117
|
+
*/
|
|
118
|
+
export declare function readUnlockInitiatedAt(data: Uint8Array): bigint;
|
|
119
|
+
/**
|
|
120
|
+
* Decode the full lock state ({@link readLock} + {@link readUnlockInitiatedAt})
|
|
121
|
+
* from a `Handle` account's raw data.
|
|
122
|
+
*/
|
|
123
|
+
export declare function readLockState(data: Uint8Array): LockState;
|
|
124
|
+
export interface LockParams {
|
|
125
|
+
/** The registry program id. */
|
|
126
|
+
readonly programId: AddressLike;
|
|
127
|
+
/** Pays the one-time account grow through the lock region on first lock.
|
|
128
|
+
* Signer, writable. */
|
|
129
|
+
readonly payer: AddressLike;
|
|
130
|
+
/** The handle's CURRENT authority (untokenized owner, or NFT holder).
|
|
131
|
+
* Signer. */
|
|
132
|
+
readonly owner: AddressLike;
|
|
133
|
+
/** The `["handle", name]` PDA. */
|
|
134
|
+
readonly handle: AddressLike;
|
|
135
|
+
/** The `["listing", handle]` PDA — must NOT exist (mutual exclusion). Derive
|
|
136
|
+
* per `delegate.ts`'s module docs. */
|
|
137
|
+
readonly listing: AddressLike;
|
|
138
|
+
/** The `["auction", handle]` PDA — must NOT exist (mutual exclusion). */
|
|
139
|
+
readonly auction: AddressLike;
|
|
140
|
+
/** TOKENIZED handles only: the holder's associated token account for the
|
|
141
|
+
* handle's NFT mint, appended as the strict check's remaining-account proof.
|
|
142
|
+
* Omit for an untokenized handle. */
|
|
143
|
+
readonly holderTokenAccount?: AddressLike;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Build `lock_handle` — freeze every name-moving instruction until a
|
|
147
|
+
* timelocked unlock. Idempotent (re-locking clears any pending unlock).
|
|
148
|
+
*/
|
|
149
|
+
export declare function buildLockHandleIx(p: LockParams): BuiltInstruction;
|
|
150
|
+
export interface UnlockParams {
|
|
151
|
+
/** The registry program id. */
|
|
152
|
+
readonly programId: AddressLike;
|
|
153
|
+
/** The handle's CURRENT authority (untokenized owner, or NFT holder).
|
|
154
|
+
* Signer. */
|
|
155
|
+
readonly owner: AddressLike;
|
|
156
|
+
/** The `["handle", name]` PDA. */
|
|
157
|
+
readonly handle: AddressLike;
|
|
158
|
+
/** TOKENIZED handles only — same rule as {@link LockParams.holderTokenAccount}. */
|
|
159
|
+
readonly holderTokenAccount?: AddressLike;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Build `initiate_unlock` — begin the timelocked unlock (stamps the clock;
|
|
163
|
+
* unlocks nothing yet). Refused if the handle is not locked.
|
|
164
|
+
*/
|
|
165
|
+
export declare function buildInitiateUnlockIx(p: UnlockParams): BuiltInstruction;
|
|
166
|
+
/**
|
|
167
|
+
* Build `cancel_unlock` — cancel a pending unlock but LEAVE the handle locked
|
|
168
|
+
* (the owner's defence against a stolen-key thief who called `initiate_unlock`).
|
|
169
|
+
*/
|
|
170
|
+
export declare function buildCancelUnlockIx(p: UnlockParams): BuiltInstruction;
|
|
171
|
+
export interface CompleteUnlockParams extends UnlockParams {
|
|
172
|
+
/** The `["config"]` PDA — the handler reads `recovery_timelock_secs` from it
|
|
173
|
+
* (reused as the unlock timelock). */
|
|
174
|
+
readonly config: AddressLike;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Build `complete_unlock` — finish the timelocked unlock: clears the lock once
|
|
178
|
+
* `Config.recovery_timelock_secs` has elapsed since `initiate_unlock`.
|
|
179
|
+
*/
|
|
180
|
+
export declare function buildCompleteUnlockIx(p: CompleteUnlockParams): BuiltInstruction;
|
package/dist/lock.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name locks (#7377): decode helpers for the on-chain lock state, plus the
|
|
3
|
+
* instruction builders for `lock_handle` and the timelocked unlock flow
|
|
4
|
+
* (`initiate_unlock` / `complete_unlock` / `cancel_unlock`).
|
|
5
|
+
*
|
|
6
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
7
|
+
* `@solana/web3.js` import (it stays an optional peer). Builders return the
|
|
8
|
+
* transport-neutral {@link BuiltInstruction} `delegate.ts` uses; adapt to
|
|
9
|
+
* web3.js exactly as that module's header documents.
|
|
10
|
+
*
|
|
11
|
+
* # The lock extension, mirrored from state.rs's EXTENSION OFFSET REGISTRY
|
|
12
|
+
*
|
|
13
|
+
* A `Handle` account's struct is frozen at 157 bytes (`8 + INIT_SPACE`);
|
|
14
|
+
* features append fixed-offset raw regions past it:
|
|
15
|
+
*
|
|
16
|
+
* offset size region
|
|
17
|
+
* 157 33 NFT capability extension (presence tag + mint pubkey)
|
|
18
|
+
* 190 1 RESERVED, permanently-unused gap (#7377 — see below)
|
|
19
|
+
* 191 5 record_count: presence tag + u32 LE live-record count
|
|
20
|
+
* 196 9 lock: locked(u8) + unlock_initiated_at(i64 LE) ← this module
|
|
21
|
+
* 205 — next free offset
|
|
22
|
+
*
|
|
23
|
+
* The lock region lives at 196, PAST record_count — NOT at the byte 190 that
|
|
24
|
+
* was originally reserved for it. The shipped feature needs 9 bytes (a flag
|
|
25
|
+
* plus an `unlock_initiated_at` timestamp for the timelocked unlock), which
|
|
26
|
+
* does not fit in one byte, and record_count (191–195) is deployed with live
|
|
27
|
+
* data whose offset cannot move. Byte 190 stays a documented, zeroed gap.
|
|
28
|
+
*
|
|
29
|
+
* # No presence tag — unlocked and uninitialized are the same value
|
|
30
|
+
*
|
|
31
|
+
* Unlike record_count (whose `0` count is distinct from "uninitialized"), the
|
|
32
|
+
* lock uses NO presence byte: `locked === 0` — which is also what a short or
|
|
33
|
+
* never-locked account reads as — IS the unlocked state. So {@link readLock}
|
|
34
|
+
* is total: a pre-lock handle (account too short) and a zeroed flag both read
|
|
35
|
+
* as `false` (unlocked), never an error and never a wrong answer.
|
|
36
|
+
*
|
|
37
|
+
* # The wire rules the on-chain program enforces (mirror of lib.rs)
|
|
38
|
+
*
|
|
39
|
+
* - All four instructions are gated by the STRICT authority check
|
|
40
|
+
* (`require_current_authority` in lib.rs): the untokenized owner, OR —
|
|
41
|
+
* for a TOKENIZED handle — the current NFT holder, who appends their ATA for
|
|
42
|
+
* the handle's NFT mint as the first extra (remaining) account
|
|
43
|
+
* ({@link LockParams.holderTokenAccount} etc.).
|
|
44
|
+
* - `lock_handle` refuses if a live `Listing` (`HandleListedCannotLock`, 6053)
|
|
45
|
+
* or `Auction` (`AuctionLive`, 6031) exists — a lock and an active market are
|
|
46
|
+
* mutually exclusive. It grows the handle account through the lock region on
|
|
47
|
+
* first lock (paid by `payer`), so `payer` must be a writable signer.
|
|
48
|
+
* - HARD LOCK: while locked, NOTHING about the handle can change. All 23
|
|
49
|
+
* mutating instructions refuse with `HandleLocked` (6049) — the 11
|
|
50
|
+
* name-MOVING ones (`transfer`, `release_handle`, `list_handle`, `buy_handle`,
|
|
51
|
+
* `accept_offer`, `start_auction`, `settle_auction`, `mint_handle_nft`,
|
|
52
|
+
* `set_recovery`, `initiate_recovery`, `complete_recovery`) AND the 12
|
|
53
|
+
* record/primary/text/delegate editors (`create_record`, `update_record`,
|
|
54
|
+
* `verify_record_eth`/`svm`/`btc`, `close_record`, `create_text_record`,
|
|
55
|
+
* `update_text_record`, `close_text_record`, `set_record_delegate`,
|
|
56
|
+
* `revoke_record_delegate`, `set_primary`) — so a stolen key cannot redirect a
|
|
57
|
+
* locked handle's addresses. Only `cancel_recovery` and `clear_primary` stay
|
|
58
|
+
* available (the compromised-key owner can still cancel an attacker's
|
|
59
|
+
* recovery; `clear_primary` cannot encumber the handle).
|
|
60
|
+
* - Unlock is timelocked: `initiate_unlock` stamps the clock (refused if not
|
|
61
|
+
* locked, `HandleNotLocked` 6050); `complete_unlock` clears the lock only
|
|
62
|
+
* after `Config.recovery_timelock_secs` has elapsed (`UnlockNotInitiated`
|
|
63
|
+
* 6051 / `UnlockTimelockNotElapsed` 6052); `cancel_unlock` clears a pending
|
|
64
|
+
* unlock but KEEPS the lock — the owner's defence against a stolen-key thief.
|
|
65
|
+
* - `lock_handle` is idempotent: re-locking re-asserts the flag and clears any
|
|
66
|
+
* pending unlock (same effect as `cancel_unlock`).
|
|
67
|
+
*/
|
|
68
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
69
|
+
/** Byte offset of the lock extension: base(157) + NFT ext(33) + reserved gap
|
|
70
|
+
* byte(1) + record_count ext(5) = 196. */
|
|
71
|
+
export const LOCK_EXT_OFFSET = 196;
|
|
72
|
+
/** `locked: u8` (1) + `unlock_initiated_at: i64` LE (8). */
|
|
73
|
+
export const LOCK_EXT_LEN = 9;
|
|
74
|
+
/** Hard floor (seconds) on the UNLOCK timelock, mirrored from the program's
|
|
75
|
+
* `MIN_UNLOCK_TIMELOCK_SECS`. `complete_unlock` enforces
|
|
76
|
+
* `max(Config.recovery_timelock_secs, MIN_UNLOCK_TIMELOCK_SECS)`, so no config
|
|
77
|
+
* value can collapse the reaction window. A UI computing when an unlock becomes
|
|
78
|
+
* available MUST apply the same clamp — see {@link unlockAvailableAt}. */
|
|
79
|
+
export const MIN_UNLOCK_TIMELOCK_SECS = 24 * 60 * 60;
|
|
80
|
+
/**
|
|
81
|
+
* The unix timestamp at which a pending `complete_unlock` becomes valid, given
|
|
82
|
+
* the handle's `unlock_initiated_at` and the config's `recovery_timelock_secs`.
|
|
83
|
+
* Applies the same `MIN_UNLOCK_TIMELOCK_SECS` floor the program enforces, so a
|
|
84
|
+
* UI never shows an ETA earlier than the program will actually allow. Returns
|
|
85
|
+
* `null` when no unlock is pending (`unlockInitiatedAt == 0n`).
|
|
86
|
+
*/
|
|
87
|
+
export function unlockAvailableAt(unlockInitiatedAt, recoveryTimelockSecs) {
|
|
88
|
+
if (unlockInitiatedAt === 0n)
|
|
89
|
+
return null;
|
|
90
|
+
const floor = BigInt(MIN_UNLOCK_TIMELOCK_SECS);
|
|
91
|
+
const timelock = recoveryTimelockSecs > floor ? recoveryTimelockSecs : floor;
|
|
92
|
+
return unlockInitiatedAt + timelock;
|
|
93
|
+
}
|
|
94
|
+
/** Anchor instruction discriminators — `sha256("global:<name>")[0..8]`,
|
|
95
|
+
* pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
|
|
96
|
+
* everywhere it runs) and asserted against a re-derivation in the test suite
|
|
97
|
+
* so a typo can never silently pass. */
|
|
98
|
+
export const LOCK_HANDLE_DISCRIMINATOR = Uint8Array.from([
|
|
99
|
+
104, 51, 234, 115, 96, 138, 171, 223,
|
|
100
|
+
]);
|
|
101
|
+
export const INITIATE_UNLOCK_DISCRIMINATOR = Uint8Array.from([
|
|
102
|
+
41, 118, 177, 198, 27, 237, 231, 69,
|
|
103
|
+
]);
|
|
104
|
+
export const COMPLETE_UNLOCK_DISCRIMINATOR = Uint8Array.from([
|
|
105
|
+
22, 203, 15, 189, 239, 125, 25, 132,
|
|
106
|
+
]);
|
|
107
|
+
export const CANCEL_UNLOCK_DISCRIMINATOR = Uint8Array.from([
|
|
108
|
+
116, 58, 36, 132, 24, 104, 170, 228,
|
|
109
|
+
]);
|
|
110
|
+
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
111
|
+
function toBase58(v, what) {
|
|
112
|
+
if (typeof v === "string") {
|
|
113
|
+
const b = decodeBase58_32(v);
|
|
114
|
+
if (!b)
|
|
115
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
116
|
+
return encodeBase58(b);
|
|
117
|
+
}
|
|
118
|
+
if (v.length !== 32)
|
|
119
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
120
|
+
return encodeBase58(v);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Read the lock flag from a `Handle` account's raw data (exactly what an RPC
|
|
124
|
+
* `getAccountInfo` returns for the `["handle", name]` PDA). Mirrors the
|
|
125
|
+
* program's `Handle::read_lock` tolerance exactly: a short account (a handle
|
|
126
|
+
* registered or grown before the lock region existed) and a zeroed flag both
|
|
127
|
+
* read as `false` (unlocked) — never an error, never a wrong answer.
|
|
128
|
+
*/
|
|
129
|
+
export function readLock(data) {
|
|
130
|
+
if (data.length < LOCK_EXT_OFFSET + 1)
|
|
131
|
+
return false;
|
|
132
|
+
return data[LOCK_EXT_OFFSET] !== 0;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Read the pending-unlock timestamp (`0n` == none pending). Tolerant exactly
|
|
136
|
+
* like {@link readLock}: a region too short to hold the i64 reads as `0n`.
|
|
137
|
+
* Meaningful only while {@link readLock} is `true`.
|
|
138
|
+
*/
|
|
139
|
+
export function readUnlockInitiatedAt(data) {
|
|
140
|
+
const start = LOCK_EXT_OFFSET + 1;
|
|
141
|
+
if (data.length < start + 8)
|
|
142
|
+
return 0n;
|
|
143
|
+
return new DataView(data.buffer, data.byteOffset, data.byteLength).getBigInt64(start, true);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Decode the full lock state ({@link readLock} + {@link readUnlockInitiatedAt})
|
|
147
|
+
* from a `Handle` account's raw data.
|
|
148
|
+
*/
|
|
149
|
+
export function readLockState(data) {
|
|
150
|
+
return { locked: readLock(data), unlockInitiatedAt: readUnlockInitiatedAt(data) };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build `lock_handle` — freeze every name-moving instruction until a
|
|
154
|
+
* timelocked unlock. Idempotent (re-locking clears any pending unlock).
|
|
155
|
+
*/
|
|
156
|
+
export function buildLockHandleIx(p) {
|
|
157
|
+
const keys = [
|
|
158
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
159
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
|
|
160
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
161
|
+
{ pubkey: toBase58(p.listing, "listing"), isSigner: false, isWritable: false },
|
|
162
|
+
{ pubkey: toBase58(p.auction, "auction"), isSigner: false, isWritable: false },
|
|
163
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
164
|
+
];
|
|
165
|
+
if (p.holderTokenAccount !== undefined) {
|
|
166
|
+
keys.push({ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"), isSigner: false, isWritable: false });
|
|
167
|
+
}
|
|
168
|
+
return { programId: toBase58(p.programId, "programId"), keys, data: LOCK_HANDLE_DISCRIMINATOR.slice() };
|
|
169
|
+
}
|
|
170
|
+
/** Shared builder for the two `UnlockHandle`-context instructions
|
|
171
|
+
* (`initiate_unlock`, `cancel_unlock`): identical account list, different
|
|
172
|
+
* discriminator. */
|
|
173
|
+
function buildUnlockHandleIx(disc, p) {
|
|
174
|
+
const keys = [
|
|
175
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
|
|
176
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
177
|
+
];
|
|
178
|
+
if (p.holderTokenAccount !== undefined) {
|
|
179
|
+
keys.push({ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"), isSigner: false, isWritable: false });
|
|
180
|
+
}
|
|
181
|
+
return { programId: toBase58(p.programId, "programId"), keys, data: disc.slice() };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Build `initiate_unlock` — begin the timelocked unlock (stamps the clock;
|
|
185
|
+
* unlocks nothing yet). Refused if the handle is not locked.
|
|
186
|
+
*/
|
|
187
|
+
export function buildInitiateUnlockIx(p) {
|
|
188
|
+
return buildUnlockHandleIx(INITIATE_UNLOCK_DISCRIMINATOR, p);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Build `cancel_unlock` — cancel a pending unlock but LEAVE the handle locked
|
|
192
|
+
* (the owner's defence against a stolen-key thief who called `initiate_unlock`).
|
|
193
|
+
*/
|
|
194
|
+
export function buildCancelUnlockIx(p) {
|
|
195
|
+
return buildUnlockHandleIx(CANCEL_UNLOCK_DISCRIMINATOR, p);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Build `complete_unlock` — finish the timelocked unlock: clears the lock once
|
|
199
|
+
* `Config.recovery_timelock_secs` has elapsed since `initiate_unlock`.
|
|
200
|
+
*/
|
|
201
|
+
export function buildCompleteUnlockIx(p) {
|
|
202
|
+
const keys = [
|
|
203
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
|
|
204
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
205
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
|
|
206
|
+
];
|
|
207
|
+
if (p.holderTokenAccount !== undefined) {
|
|
208
|
+
keys.push({ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"), isSigner: false, isWritable: false });
|
|
209
|
+
}
|
|
210
|
+
return { programId: toBase58(p.programId, "programId"), keys, data: COMPLETE_UNLOCK_DISCRIMINATOR.slice() };
|
|
211
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record_count Handle extension (#7384): decode helper for the on-chain
|
|
3
|
+
* live-record counter, plus the instruction builder for the admin-only
|
|
4
|
+
* `backfill_record_count` migration path.
|
|
5
|
+
*
|
|
6
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
7
|
+
* `@solana/web3.js` import (it stays an optional peer). The builder returns
|
|
8
|
+
* the same transport-neutral {@link BuiltInstruction} `delegate.ts` uses.
|
|
9
|
+
*
|
|
10
|
+
* # The extension, mirrored from state.rs's EXTENSION OFFSET REGISTRY
|
|
11
|
+
*
|
|
12
|
+
* A `Handle` account's struct is frozen at 157 bytes (`8 + INIT_SPACE`);
|
|
13
|
+
* features append fixed-offset raw regions past it:
|
|
14
|
+
*
|
|
15
|
+
* offset size region
|
|
16
|
+
* 157 33 NFT capability extension (presence tag + mint pubkey)
|
|
17
|
+
* 190 1 RESERVED lock byte (#7377, unbuilt)
|
|
18
|
+
* 191 5 record_count: presence tag + u32 LE live-record count
|
|
19
|
+
* 196 — next free offset
|
|
20
|
+
*
|
|
21
|
+
* `create_record` increments the count (initializing the extension and
|
|
22
|
+
* growing the account on first use — which is why the record-editing
|
|
23
|
+
* create/close instructions pass the HANDLE account WRITABLE as of this
|
|
24
|
+
* feature; the account LIST is unchanged), `close_record` decrements it
|
|
25
|
+
* (saturating, only when initialized), and `release_handle` refuses while an
|
|
26
|
+
* initialized count is nonzero (`HandleHasRecords`, code 6044).
|
|
27
|
+
*
|
|
28
|
+
* # Uninitialized is NOT zero
|
|
29
|
+
*
|
|
30
|
+
* {@link readRecordCount} returns `null` for a handle whose extension is
|
|
31
|
+
* uninitialized — an account too short (a pre-#7384 handle) or a presence
|
|
32
|
+
* byte still zero (grown for the NFT extension, never counted). Such a
|
|
33
|
+
* handle keeps the OLD unguarded release behaviour until the admin stamps it
|
|
34
|
+
* via `backfill_record_count` or a post-upgrade `create_record` initializes
|
|
35
|
+
* it. A `0` return, by contrast, is an initialized, trustworthy
|
|
36
|
+
* "provably no live records". Never conflate the two: rendering `null` as
|
|
37
|
+
* "0 records" would tell an owner their release is safe when the program
|
|
38
|
+
* cannot actually vouch for that.
|
|
39
|
+
*/
|
|
40
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
41
|
+
/** `8 + Handle::INIT_SPACE` — the frozen base struct size, and the first
|
|
42
|
+
* extension offset. */
|
|
43
|
+
export declare const HANDLE_BASE_LEN = 157;
|
|
44
|
+
/** Byte offset of the record_count extension: base(157) + NFT ext(33) +
|
|
45
|
+
* reserved lock byte(1). */
|
|
46
|
+
export declare const RECORD_COUNT_EXT_OFFSET = 191;
|
|
47
|
+
/** presence tag(1) + u32 LE count(4). */
|
|
48
|
+
export declare const RECORD_COUNT_EXT_LEN = 5;
|
|
49
|
+
/** One past the last known extension region (must match the program's
|
|
50
|
+
* `Handle::KNOWN_EXT_END`) — the size a fully-grown Handle account has under
|
|
51
|
+
* this program version. Layout: base(157) + NFT(33) + reserved(1) +
|
|
52
|
+
* record_count(5) + lock(9) = 205. Was 196 (pre-lock); the #7377 lock extension
|
|
53
|
+
* (see LOCK_EXT_OFFSET=196 / LOCK_EXT_LEN=9 in lock.ts) pushed it to 205, so any
|
|
54
|
+
* consumer allocating/resizing to this constant must use the current value. */
|
|
55
|
+
export declare const HANDLE_KNOWN_EXT_END = 205;
|
|
56
|
+
/** Anchor instruction discriminator:
|
|
57
|
+
* `sha256("global:backfill_record_count")[0..8]`. Pinned (this SDK is
|
|
58
|
+
* zero-dependency and cannot assume WebCrypto SHA-256 everywhere it runs);
|
|
59
|
+
* asserted against a re-derivation in the test suite so a typo can never
|
|
60
|
+
* silently pass. */
|
|
61
|
+
export declare const BACKFILL_RECORD_COUNT_DISCRIMINATOR: Uint8Array;
|
|
62
|
+
/**
|
|
63
|
+
* Read the live-record count from a `Handle` account's raw data (exactly
|
|
64
|
+
* what an RPC `getAccountInfo` returns for the `["handle", name]` PDA).
|
|
65
|
+
*
|
|
66
|
+
* Returns the count when the extension is INITIALIZED, and `null` when it is
|
|
67
|
+
* not — see the module docs for why `null` and `0` mean different things and
|
|
68
|
+
* must be rendered differently. Mirrors the program's
|
|
69
|
+
* `Handle::read_record_count` tolerance exactly: a short account and a
|
|
70
|
+
* zeroed region are both `null`, never an error.
|
|
71
|
+
*/
|
|
72
|
+
export declare function readRecordCount(data: Uint8Array): number | null;
|
|
73
|
+
export interface BackfillRecordCountParams {
|
|
74
|
+
/** The registry program id. */
|
|
75
|
+
readonly programId: AddressLike;
|
|
76
|
+
/** The registry admin (`Config.admin`). Signer; also pays the one-time
|
|
77
|
+
* account grow when the target handle's extension region isn't allocated
|
|
78
|
+
* yet. */
|
|
79
|
+
readonly admin: AddressLike;
|
|
80
|
+
/** The `["config"]` PDA. */
|
|
81
|
+
readonly config: AddressLike;
|
|
82
|
+
/** The `["handle", name]` PDA being stamped. */
|
|
83
|
+
readonly handle: AddressLike;
|
|
84
|
+
/** The true live-record count, computed OFF-chain — a `getProgramAccounts`
|
|
85
|
+
* scan filtered on the handle pubkey (what `fetchRecords` runs; count the
|
|
86
|
+
* full list, stale records included, since stale records are still live
|
|
87
|
+
* accounts that block a clean release). Overwrites whatever is stored —
|
|
88
|
+
* the instruction is deliberately idempotent/re-runnable. */
|
|
89
|
+
readonly count: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build `backfill_record_count` — admin-only: stamp a pre-#7384 handle's
|
|
93
|
+
* record_count extension with the off-chain-computed truth, moving it
|
|
94
|
+
* permanently out of the unguarded-release window. Mainnet never needs it
|
|
95
|
+
* (the mainnet registry starts fresh at cutover, counted from the first
|
|
96
|
+
* record); this is testnet migration + permanent correction tooling.
|
|
97
|
+
*/
|
|
98
|
+
export declare function buildBackfillRecordCountIx(p: BackfillRecordCountParams): BuiltInstruction;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record_count Handle extension (#7384): decode helper for the on-chain
|
|
3
|
+
* live-record counter, plus the instruction builder for the admin-only
|
|
4
|
+
* `backfill_record_count` migration path.
|
|
5
|
+
*
|
|
6
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
7
|
+
* `@solana/web3.js` import (it stays an optional peer). The builder returns
|
|
8
|
+
* the same transport-neutral {@link BuiltInstruction} `delegate.ts` uses.
|
|
9
|
+
*
|
|
10
|
+
* # The extension, mirrored from state.rs's EXTENSION OFFSET REGISTRY
|
|
11
|
+
*
|
|
12
|
+
* A `Handle` account's struct is frozen at 157 bytes (`8 + INIT_SPACE`);
|
|
13
|
+
* features append fixed-offset raw regions past it:
|
|
14
|
+
*
|
|
15
|
+
* offset size region
|
|
16
|
+
* 157 33 NFT capability extension (presence tag + mint pubkey)
|
|
17
|
+
* 190 1 RESERVED lock byte (#7377, unbuilt)
|
|
18
|
+
* 191 5 record_count: presence tag + u32 LE live-record count
|
|
19
|
+
* 196 — next free offset
|
|
20
|
+
*
|
|
21
|
+
* `create_record` increments the count (initializing the extension and
|
|
22
|
+
* growing the account on first use — which is why the record-editing
|
|
23
|
+
* create/close instructions pass the HANDLE account WRITABLE as of this
|
|
24
|
+
* feature; the account LIST is unchanged), `close_record` decrements it
|
|
25
|
+
* (saturating, only when initialized), and `release_handle` refuses while an
|
|
26
|
+
* initialized count is nonzero (`HandleHasRecords`, code 6044).
|
|
27
|
+
*
|
|
28
|
+
* # Uninitialized is NOT zero
|
|
29
|
+
*
|
|
30
|
+
* {@link readRecordCount} returns `null` for a handle whose extension is
|
|
31
|
+
* uninitialized — an account too short (a pre-#7384 handle) or a presence
|
|
32
|
+
* byte still zero (grown for the NFT extension, never counted). Such a
|
|
33
|
+
* handle keeps the OLD unguarded release behaviour until the admin stamps it
|
|
34
|
+
* via `backfill_record_count` or a post-upgrade `create_record` initializes
|
|
35
|
+
* it. A `0` return, by contrast, is an initialized, trustworthy
|
|
36
|
+
* "provably no live records". Never conflate the two: rendering `null` as
|
|
37
|
+
* "0 records" would tell an owner their release is safe when the program
|
|
38
|
+
* cannot actually vouch for that.
|
|
39
|
+
*/
|
|
40
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
41
|
+
/** `8 + Handle::INIT_SPACE` — the frozen base struct size, and the first
|
|
42
|
+
* extension offset. */
|
|
43
|
+
export const HANDLE_BASE_LEN = 157;
|
|
44
|
+
/** Byte offset of the record_count extension: base(157) + NFT ext(33) +
|
|
45
|
+
* reserved lock byte(1). */
|
|
46
|
+
export const RECORD_COUNT_EXT_OFFSET = 191;
|
|
47
|
+
/** presence tag(1) + u32 LE count(4). */
|
|
48
|
+
export const RECORD_COUNT_EXT_LEN = 5;
|
|
49
|
+
/** One past the last known extension region (must match the program's
|
|
50
|
+
* `Handle::KNOWN_EXT_END`) — the size a fully-grown Handle account has under
|
|
51
|
+
* this program version. Layout: base(157) + NFT(33) + reserved(1) +
|
|
52
|
+
* record_count(5) + lock(9) = 205. Was 196 (pre-lock); the #7377 lock extension
|
|
53
|
+
* (see LOCK_EXT_OFFSET=196 / LOCK_EXT_LEN=9 in lock.ts) pushed it to 205, so any
|
|
54
|
+
* consumer allocating/resizing to this constant must use the current value. */
|
|
55
|
+
export const HANDLE_KNOWN_EXT_END = 205;
|
|
56
|
+
/** Anchor instruction discriminator:
|
|
57
|
+
* `sha256("global:backfill_record_count")[0..8]`. Pinned (this SDK is
|
|
58
|
+
* zero-dependency and cannot assume WebCrypto SHA-256 everywhere it runs);
|
|
59
|
+
* asserted against a re-derivation in the test suite so a typo can never
|
|
60
|
+
* silently pass. */
|
|
61
|
+
export const BACKFILL_RECORD_COUNT_DISCRIMINATOR = Uint8Array.from([
|
|
62
|
+
151, 122, 233, 167, 188, 54, 32, 110,
|
|
63
|
+
]);
|
|
64
|
+
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
65
|
+
function toBase58(v, what) {
|
|
66
|
+
if (typeof v === "string") {
|
|
67
|
+
const b = decodeBase58_32(v);
|
|
68
|
+
if (!b)
|
|
69
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
70
|
+
return encodeBase58(b);
|
|
71
|
+
}
|
|
72
|
+
if (v.length !== 32)
|
|
73
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
74
|
+
return encodeBase58(v);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Read the live-record count from a `Handle` account's raw data (exactly
|
|
78
|
+
* what an RPC `getAccountInfo` returns for the `["handle", name]` PDA).
|
|
79
|
+
*
|
|
80
|
+
* Returns the count when the extension is INITIALIZED, and `null` when it is
|
|
81
|
+
* not — see the module docs for why `null` and `0` mean different things and
|
|
82
|
+
* must be rendered differently. Mirrors the program's
|
|
83
|
+
* `Handle::read_record_count` tolerance exactly: a short account and a
|
|
84
|
+
* zeroed region are both `null`, never an error.
|
|
85
|
+
*/
|
|
86
|
+
export function readRecordCount(data) {
|
|
87
|
+
if (data.length < RECORD_COUNT_EXT_OFFSET + RECORD_COUNT_EXT_LEN)
|
|
88
|
+
return null;
|
|
89
|
+
if (data[RECORD_COUNT_EXT_OFFSET] === 0)
|
|
90
|
+
return null;
|
|
91
|
+
return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(RECORD_COUNT_EXT_OFFSET + 1, true);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Build `backfill_record_count` — admin-only: stamp a pre-#7384 handle's
|
|
95
|
+
* record_count extension with the off-chain-computed truth, moving it
|
|
96
|
+
* permanently out of the unguarded-release window. Mainnet never needs it
|
|
97
|
+
* (the mainnet registry starts fresh at cutover, counted from the first
|
|
98
|
+
* record); this is testnet migration + permanent correction tooling.
|
|
99
|
+
*/
|
|
100
|
+
export function buildBackfillRecordCountIx(p) {
|
|
101
|
+
if (!Number.isInteger(p.count) || p.count < 0 || p.count > 0xffffffff) {
|
|
102
|
+
throw new Error("count must be a u32");
|
|
103
|
+
}
|
|
104
|
+
const data = new Uint8Array(8 + 4);
|
|
105
|
+
data.set(BACKFILL_RECORD_COUNT_DISCRIMINATOR, 0);
|
|
106
|
+
new DataView(data.buffer).setUint32(8, p.count, true);
|
|
107
|
+
const keys = [
|
|
108
|
+
{ pubkey: toBase58(p.admin, "admin"), isSigner: true, isWritable: true },
|
|
109
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
|
|
110
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
111
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
112
|
+
];
|
|
113
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
114
|
+
}
|