@x1id/resolve 0.3.0 → 0.7.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.
@@ -50,6 +50,15 @@
50
50
  * documented blind spot is shared with `Handle.owner`/`Primary`/
51
51
  * `RecordDelegate`: a bearer-NFT marketplace trade bumps no epoch, so the
52
52
  * attestation keeps reading live until the attestor re-reviews or revokes.)
53
+ *
54
+ * # `records_cleared_at` (#8139) — the SECOND, independent staleness rule
55
+ *
56
+ * `clear_records` lets an owner bulk-invalidate every record RIGHT NOW,
57
+ * without a transfer — its own doc comment (lib.rs) names attestations
58
+ * explicitly alongside records/text-records as sharing this epoch rule. The
59
+ * same functions therefore also demand the handle's `recordsClearedAt` (0 if
60
+ * never cleared, from `ParsedHandle`), and an attestation is `stale` when
61
+ * EITHER `attested_at < registeredAt` OR `attested_at < recordsClearedAt`.
53
62
  */
54
63
  import { encodeBase58 } from "./base58.js";
55
64
  import { decodeBase58_32 } from "./base58.js";
@@ -123,13 +132,15 @@ export function attestationKindName(kind) {
123
132
  * Decode one Attestation account.
124
133
  *
125
134
  * `registeredAt` is the owning Handle's `registered_at`, decoded by the
126
- * caller from the Handle account — it decides `stale`. There is
127
- * intentionally no overload without it (see the module docs).
135
+ * caller from the Handle account — it decides `stale`. `recordsClearedAt` is
136
+ * that same Handle's `recordsClearedAt` (0 if never cleared, #8139), a
137
+ * SECOND independent staleness anchor. There is intentionally no overload
138
+ * without either (see the module docs).
128
139
  *
129
140
  * Returns null for anything that is not an Attestation: wrong length or
130
141
  * wrong discriminator.
131
142
  */
132
- export function decodeAttestation(raw, account, registeredAt) {
143
+ export function decodeAttestation(raw, account, registeredAt, recordsClearedAt) {
133
144
  if (raw.length !== ATTESTATION_LEN)
134
145
  return null;
135
146
  for (let i = 0; i < 8; i++) {
@@ -155,7 +166,7 @@ export function decodeAttestation(raw, account, registeredAt) {
155
166
  evidenceHash,
156
167
  attestedAt,
157
168
  attestor,
158
- stale: attestedAt < registeredAt,
169
+ stale: attestedAt < registeredAt || attestedAt < recordsClearedAt,
159
170
  };
160
171
  }
161
172
  /** The attestations that vouch for the CURRENT owner — `stale` ones
@@ -181,12 +192,13 @@ export function isHandleVerified(attestations) {
181
192
  *
182
193
  * `registeredAt` is `Handle.registered_at` as decoded from the Handle
183
194
  * account the caller already has — the staleness rule needs it, and there
184
- * is no variant of this function without it. Every attestation is returned,
185
- * stale ones flagged, so an owner surface can show what a previous
186
- * registration left behind; anything that renders a verified badge takes
187
- * {@link isHandleVerified} / {@link liveAttestations}.
195
+ * is no variant of this function without it. `recordsClearedAt` is that same
196
+ * Handle's `recordsClearedAt` (0 if never cleared, #8139) — pass it through.
197
+ * Every attestation is returned, stale ones flagged, so an owner surface can
198
+ * show what a previous registration left behind; anything that renders a
199
+ * verified badge takes {@link isHandleVerified} / {@link liveAttestations}.
188
200
  */
189
- export async function fetchAttestations(rpc, programId, handleAccount, registeredAt) {
201
+ export async function fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt) {
190
202
  const res = (await rpc("getProgramAccounts", [
191
203
  programId,
192
204
  {
@@ -205,7 +217,7 @@ export async function fetchAttestations(rpc, programId, handleAccount, registere
205
217
  const raw = new Uint8Array(bin.length);
206
218
  for (let i = 0; i < bin.length; i++)
207
219
  raw[i] = bin.charCodeAt(i);
208
- const decoded = decodeAttestation(raw, a.pubkey, registeredAt);
220
+ const decoded = decodeAttestation(raw, a.pubkey, registeredAt, recordsClearedAt);
209
221
  // Defence in depth: the memcmp filter should guarantee the handle
210
222
  // match, but a wrong offset would silently attribute someone else's
211
223
  // attestation to this handle. A malformed account is skipped, not fatal.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `clear_records` (#8139): bulk-invalidate every record/text-record/
3
+ * attestation on a handle RIGHT NOW, without a transfer, by bumping
4
+ * `Handle.records_cleared_at` to the current time. The accounts
5
+ * (`accounts.ts` `ParsedHandle.recordsClearedAt`) and read-side staleness
6
+ * rule (every decoder in `records.ts` / `textRecords.ts` / `attestation.ts`:
7
+ * `updated_at < recordsClearedAt` is stale, alongside the `registeredAt`
8
+ * epoch rule) have existed since #8139 shipped; this module is the one
9
+ * missing piece — the instruction builder to actually trigger a clear
10
+ * (WP #8212).
11
+ *
12
+ * Hand-rolled like the rest of this package — no Anchor client, no
13
+ * `@solana/web3.js` import (it stays an optional peer). The builder returns
14
+ * the same transport-neutral {@link BuiltInstruction} `delegate.ts` uses.
15
+ *
16
+ * # Authority: STRICT current-authority, no delegate
17
+ *
18
+ * `clear_records` is gated by `require_current_authority` in lib.rs — the
19
+ * SAME strict check `lock_handle`/`initiate_unlock`/`complete_unlock` use
20
+ * (see `lock.ts`'s module docs), not the record-editing delegate model
21
+ * (`require_record_edit_authority`, `recordWrite.ts`'s
22
+ * `RecordEditAuthorityParams`). A handle's active `RecordDelegate` can add,
23
+ * edit, and verify individual records, but CANNOT bulk-invalidate all of
24
+ * them — that stays an owner/NFT-holder-only action, on purpose: nuking
25
+ * every record is a much bigger blast radius than editing one. There is
26
+ * deliberately no `recordDelegate` param here.
27
+ *
28
+ * A TOKENIZED handle's current holder appends their ATA for the handle's
29
+ * NFT mint as the sole remaining account (`holderTokenAccount`), exactly
30
+ * like `lock.ts`'s `LockParams`/`UnlockParams`; omit it for an untokenized
31
+ * handle.
32
+ */
33
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
34
+ /** Anchor instruction discriminator: `sha256("global:clear_records")[0..8]`.
35
+ * Pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
36
+ * everywhere it runs); asserted against a re-derivation in the test suite
37
+ * so a typo can never silently pass. */
38
+ export declare const CLEAR_RECORDS_DISCRIMINATOR: Uint8Array;
39
+ export interface ClearRecordsParams {
40
+ /** The registry program id. */
41
+ readonly programId: AddressLike;
42
+ /** The handle's CURRENT authority (untokenized owner, or NFT holder).
43
+ * Signer, writable — also pays the one-time account grow through
44
+ * `records_cleared_at`'s extension region on a handle that has never
45
+ * been grown that far. */
46
+ readonly owner: AddressLike;
47
+ /** The `["handle", name]` PDA. */
48
+ readonly handle: AddressLike;
49
+ /** TOKENIZED handles only: the holder's associated token account for the
50
+ * handle's NFT mint, appended as the strict check's remaining-account
51
+ * proof. Omit for an untokenized handle. */
52
+ readonly holderTokenAccount?: AddressLike;
53
+ }
54
+ /**
55
+ * Build `clear_records` — bump `Handle.records_cleared_at` to now, so every
56
+ * record/text-record/attestation with an `updated_at` before this instant
57
+ * reads as stale everywhere in this SDK, without touching any of those
58
+ * accounts individually. Idempotent (re-clearing just re-stamps the clock).
59
+ */
60
+ export declare function buildClearRecordsIx(p: ClearRecordsParams): BuiltInstruction;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `clear_records` (#8139): bulk-invalidate every record/text-record/
3
+ * attestation on a handle RIGHT NOW, without a transfer, by bumping
4
+ * `Handle.records_cleared_at` to the current time. The accounts
5
+ * (`accounts.ts` `ParsedHandle.recordsClearedAt`) and read-side staleness
6
+ * rule (every decoder in `records.ts` / `textRecords.ts` / `attestation.ts`:
7
+ * `updated_at < recordsClearedAt` is stale, alongside the `registeredAt`
8
+ * epoch rule) have existed since #8139 shipped; this module is the one
9
+ * missing piece — the instruction builder to actually trigger a clear
10
+ * (WP #8212).
11
+ *
12
+ * Hand-rolled like the rest of this package — no Anchor client, no
13
+ * `@solana/web3.js` import (it stays an optional peer). The builder returns
14
+ * the same transport-neutral {@link BuiltInstruction} `delegate.ts` uses.
15
+ *
16
+ * # Authority: STRICT current-authority, no delegate
17
+ *
18
+ * `clear_records` is gated by `require_current_authority` in lib.rs — the
19
+ * SAME strict check `lock_handle`/`initiate_unlock`/`complete_unlock` use
20
+ * (see `lock.ts`'s module docs), not the record-editing delegate model
21
+ * (`require_record_edit_authority`, `recordWrite.ts`'s
22
+ * `RecordEditAuthorityParams`). A handle's active `RecordDelegate` can add,
23
+ * edit, and verify individual records, but CANNOT bulk-invalidate all of
24
+ * them — that stays an owner/NFT-holder-only action, on purpose: nuking
25
+ * every record is a much bigger blast radius than editing one. There is
26
+ * deliberately no `recordDelegate` param here.
27
+ *
28
+ * A TOKENIZED handle's current holder appends their ATA for the handle's
29
+ * NFT mint as the sole remaining account (`holderTokenAccount`), exactly
30
+ * like `lock.ts`'s `LockParams`/`UnlockParams`; omit it for an untokenized
31
+ * handle.
32
+ */
33
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
34
+ /** Anchor instruction discriminator: `sha256("global:clear_records")[0..8]`.
35
+ * Pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
36
+ * everywhere it runs); asserted against a re-derivation in the test suite
37
+ * so a typo can never silently pass. */
38
+ export const CLEAR_RECORDS_DISCRIMINATOR = Uint8Array.from([
39
+ 150, 232, 238, 45, 8, 105, 50, 153,
40
+ ]);
41
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
42
+ function toBase58(v, what) {
43
+ if (typeof v === "string") {
44
+ const b = decodeBase58_32(v);
45
+ if (!b)
46
+ throw new Error(`${what} is not a valid base58 address`);
47
+ return encodeBase58(b);
48
+ }
49
+ if (v.length !== 32)
50
+ throw new Error(`${what} must be exactly 32 bytes`);
51
+ return encodeBase58(v);
52
+ }
53
+ /**
54
+ * Build `clear_records` — bump `Handle.records_cleared_at` to now, so every
55
+ * record/text-record/attestation with an `updated_at` before this instant
56
+ * reads as stale everywhere in this SDK, without touching any of those
57
+ * accounts individually. Idempotent (re-clearing just re-stamps the clock).
58
+ */
59
+ export function buildClearRecordsIx(p) {
60
+ const keys = [
61
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: true },
62
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
63
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
64
+ ];
65
+ if (p.holderTokenAccount !== undefined) {
66
+ keys.push({ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"), isSigner: false, isWritable: false });
67
+ }
68
+ return { programId: toBase58(p.programId, "programId"), keys, data: CLEAR_RECORDS_DISCRIMINATOR.slice() };
69
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Commit-reveal registration (#8134 / #8199): instruction builders and
3
+ * account decoding for `commit` / `register_revealed` / `cancel_commitment`
4
+ * — the anti-front-running path. `register` (register.ts) stays the plain
5
+ * single-step sibling; this module adds the two-step flow: stake out a
6
+ * hash, wait `Config.min_commitment_age_secs`, then reveal.
7
+ *
8
+ * Hand-rolled like the rest of this package — no Anchor client, no
9
+ * `@solana/web3.js` import. PDAs are NOT derived here (see delegate.ts's
10
+ * module docs for why) — derive `["commitment", hash]` (seed constant:
11
+ * {@link COMMITMENT_SEED}) with your runtime's canonical
12
+ * `findProgramAddress`, e.g. web3.js:
13
+ *
14
+ * ```ts
15
+ * PublicKey.findProgramAddressSync(
16
+ * [Buffer.from(COMMITMENT_SEED), Buffer.from(hash)],
17
+ * programId,
18
+ * );
19
+ * ```
20
+ *
21
+ * # The commitment hash
22
+ *
23
+ * `sha256(name_bytes ‖ owner_pubkey ‖ handle_type_byte ‖ secret[32] ‖
24
+ * program_id_bytes)` — the exact preimage order of the program's own
25
+ * `commitment_hash` (lib.rs), byte-verified against the deployed program by
26
+ * the E2E reveal phase (tools/scripts/e2e/journey-audit.ts). sha256, NOT
27
+ * keccak, for the same documented reason as `text_key_hash`: off-chain
28
+ * derivation is plain WebCrypto with no extra dependency — so
29
+ * {@link commitmentHash} is async, mirroring textRecords.ts's
30
+ * `hashTextKey`. The program id in the preimage is the SVM analog of
31
+ * ENS/ArcNS's `chainid + controller` domain separation: a commitment cannot
32
+ * replay against a different deployment.
33
+ *
34
+ * # Lifecycle (mirror of lib.rs)
35
+ *
36
+ * - `commit(commitment_hash)` `init`s `["commitment", hash]`, recording only
37
+ * `payer` + `committed_at` — the name is never on chain until reveal.
38
+ * Anyone may pay (the security is the secret preimage, not who pays).
39
+ * - `register_revealed(name, handle_type, secret)` recomputes the hash
40
+ * on-chain from the now-public fields; the supplied `commitment` account
41
+ * must sit at exactly the PDA that hash derives (`CommitmentMismatch`,
42
+ * 6077, otherwise). Valid only inside the window: at least
43
+ * `min_commitment_age_secs` after `committed_at` (`CommitmentTooNew`,
44
+ * 6074) and strictly before `max_commitment_age_secs` (`CommitmentTooOld`,
45
+ * 6075). Consuming closes the commitment — rent back. Pricing, integrator
46
+ * split and the `Handle` write are identical to `register` (both call the
47
+ * program's `register_core`); the result is a PERMANENT registration.
48
+ * - `cancel_commitment()` closes an unconsumed commitment, payer-only
49
+ * (`NotCommitmentPayer`, 6080), allowed at ANY age — the escape hatch for
50
+ * a lost secret or an abandoned flow, so rent is never stranded.
51
+ *
52
+ * A SECRET THAT IS LOST STRANDS THE COMMITMENT: without it the hash cannot
53
+ * be recomputed, so the commitment can only ever be `cancel_commitment`ed
54
+ * (which at least needs its address, discoverable via
55
+ * `getProgramAccounts` on the payer). Persist the secret client-side the
56
+ * moment it is generated, before `commit` is even sent.
57
+ */
58
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
59
+ import type { RpcFn } from "./accounts.js";
60
+ import type { HandleTypeValue } from "./voucher.js";
61
+ /** Seed prefix of the commitment PDA: `["commitment", commitmentHash]`. */
62
+ export declare const COMMITMENT_SEED = "commitment";
63
+ /** Anchor instruction discriminator: `sha256("global:commit")[0..8]`. */
64
+ export declare const COMMIT_DISCRIMINATOR: Uint8Array;
65
+ /** Anchor instruction discriminator: `sha256("global:register_revealed")[0..8]`. */
66
+ export declare const REGISTER_REVEALED_DISCRIMINATOR: Uint8Array;
67
+ /** Anchor instruction discriminator: `sha256("global:cancel_commitment")[0..8]`. */
68
+ export declare const CANCEL_COMMITMENT_DISCRIMINATOR: Uint8Array;
69
+ /** Anchor account discriminator: `sha256("account:Commitment")[0..8]`. */
70
+ export declare const COMMITMENT_DISCRIMINATOR: Uint8Array;
71
+ /** `Commitment` account size: disc(8) payer(32) committed_at(8) bump(1). */
72
+ export declare const COMMITMENT_LEN = 49;
73
+ export interface CommitmentHashParams {
74
+ /** Canonical form (`parseName(...).canonical`), 1..=32 bytes. */
75
+ readonly name: string;
76
+ /** The owner the reveal will register the handle to — must be the SAME
77
+ * key later passed as `register_revealed`'s `owner`, or the reveal fails
78
+ * closed (`CommitmentMismatch`). */
79
+ readonly owner: AddressLike;
80
+ readonly handleType: HandleTypeValue;
81
+ /** 32 bytes of fresh CSPRNG output (`crypto.getRandomValues(new
82
+ * Uint8Array(32))`). Persist it — see the module docs. */
83
+ readonly secret: Uint8Array;
84
+ /** The registry program id — part of the preimage (domain separation). */
85
+ readonly programId: AddressLike;
86
+ }
87
+ /**
88
+ * The 32-byte commitment hash — `sha256(name ‖ owner ‖ handle_type ‖ secret
89
+ * ‖ program_id)`, the program's own preimage order (see the module docs).
90
+ * WebCrypto (`crypto.subtle`), so it is async — mirrors `hashTextKey`.
91
+ */
92
+ export declare function commitmentHash(p: CommitmentHashParams): Promise<Uint8Array>;
93
+ export interface CommitParams {
94
+ /** The registry program id. */
95
+ readonly programId: AddressLike;
96
+ /** Pays the commitment's rent; recorded as the only key allowed to
97
+ * `cancel_commitment`. Signer. */
98
+ readonly payer: AddressLike;
99
+ /** The `["commitment", commitmentHash]` PDA — derive it with your
100
+ * runtime's canonical `findProgramAddress` (see the module docs). */
101
+ readonly commitment: AddressLike;
102
+ /** The 32-byte hash from {@link commitmentHash}. */
103
+ readonly commitmentHash: Uint8Array;
104
+ }
105
+ /**
106
+ * Build `commit` — stake out the hash. Only the hash goes on chain; the
107
+ * name stays private until reveal. Account order matches `Commit<'info>`
108
+ * (lib.rs) exactly: payer, commitment, system_program.
109
+ */
110
+ export declare function buildCommitIx(p: CommitParams): BuiltInstruction;
111
+ export interface RegisterRevealedParams {
112
+ /** The registry program id. */
113
+ readonly programId: AddressLike;
114
+ /** Pays the registration fee and the `Handle` account's rent. Signer.
115
+ * Need NOT be the commitment's payer — the binding is the hash preimage,
116
+ * and the consumed commitment's rent refunds to this payer. */
117
+ readonly payer: AddressLike;
118
+ /** The handle's owner. Need not sign, but MUST be the key the commitment
119
+ * hashed (see {@link CommitmentHashParams.owner}). */
120
+ readonly owner: AddressLike;
121
+ /** The `["config"]` PDA. */
122
+ readonly config: AddressLike;
123
+ /** `Config.treasury` — read it fresh via `fetchConfigTreasury`
124
+ * (register.ts); it's admin-rotatable. */
125
+ readonly treasury: AddressLike;
126
+ /** The `["handle", name]` PDA being claimed — must not already exist. */
127
+ readonly handle: AddressLike;
128
+ /** The `["commitment", commitmentHash]` PDA the matching `commit`
129
+ * created. */
130
+ readonly commitment: AddressLike;
131
+ /** Canonical form (`parseName(...).canonical`), 1..=32 bytes. */
132
+ readonly name: string;
133
+ readonly handleType: HandleTypeValue;
134
+ /** The exact 32-byte secret the commitment hashed. */
135
+ readonly secret: Uint8Array;
136
+ /** OPTIONAL revenue-share pair — same contract as `register`'s (#7397):
137
+ * supply BOTH or NEITHER. */
138
+ readonly integrator?: AddressLike;
139
+ /** The `["integrator", integrator]` allowlist PDA. */
140
+ readonly integratorAllowlist?: AddressLike;
141
+ }
142
+ /**
143
+ * Build `register_revealed` — the reveal step. Price is computed on-chain
144
+ * exactly like `register`'s (see register.ts's module docs); simulate first
145
+ * to show the payer a price. Account order matches
146
+ * `RegisterRevealed<'info>` (lib.rs) exactly: payer, owner, config,
147
+ * treasury, handle, commitment, system_program [, integrator,
148
+ * integrator_allowlist].
149
+ */
150
+ export declare function buildRegisterRevealedIx(p: RegisterRevealedParams): BuiltInstruction;
151
+ export interface CancelCommitmentParams {
152
+ /** The registry program id. */
153
+ readonly programId: AddressLike;
154
+ /** MUST be the commitment's recorded payer (`NotCommitmentPayer`, 6080,
155
+ * otherwise). Signer; receives the rent. */
156
+ readonly payer: AddressLike;
157
+ /** The commitment account to close. No name/secret needed — half the
158
+ * point of canceling is that the preimage may be lost. */
159
+ readonly commitment: AddressLike;
160
+ }
161
+ /**
162
+ * Build `cancel_commitment` — reclaim an unconsumed commitment's rent,
163
+ * payer-only, allowed at any age. Account order matches
164
+ * `CancelCommitment<'info>` (lib.rs) exactly: payer, commitment.
165
+ */
166
+ export declare function buildCancelCommitmentIx(p: CancelCommitmentParams): BuiltInstruction;
167
+ /** Decoded `Commitment` account. The hash itself is never stored — the
168
+ * account's ADDRESS is the hash's proof (it is the PDA seed). */
169
+ export interface Commitment {
170
+ /** Who paid `commit`'s rent — the only key allowed to cancel (base58). */
171
+ readonly payer: string;
172
+ /** Unix seconds when `commit` created this account — what the
173
+ * min/max-age window is checked against. */
174
+ readonly committedAt: bigint;
175
+ readonly bump: number;
176
+ }
177
+ /**
178
+ * Decode a `Commitment` account's raw data, or null if not one (wrong
179
+ * discriminator or too short).
180
+ */
181
+ export declare function decodeCommitment(data: Uint8Array): Commitment | null;
182
+ /**
183
+ * Fetch + decode one `Commitment` account, or null when it doesn't exist
184
+ * (never committed, already consumed by a reveal, or canceled — all leave
185
+ * no account) or isn't shaped like a `Commitment`.
186
+ */
187
+ export declare function fetchCommitment(rpc: RpcFn, commitmentAccount: string): Promise<Commitment | null>;
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Commit-reveal registration (#8134 / #8199): instruction builders and
3
+ * account decoding for `commit` / `register_revealed` / `cancel_commitment`
4
+ * — the anti-front-running path. `register` (register.ts) stays the plain
5
+ * single-step sibling; this module adds the two-step flow: stake out a
6
+ * hash, wait `Config.min_commitment_age_secs`, then reveal.
7
+ *
8
+ * Hand-rolled like the rest of this package — no Anchor client, no
9
+ * `@solana/web3.js` import. PDAs are NOT derived here (see delegate.ts's
10
+ * module docs for why) — derive `["commitment", hash]` (seed constant:
11
+ * {@link COMMITMENT_SEED}) with your runtime's canonical
12
+ * `findProgramAddress`, e.g. web3.js:
13
+ *
14
+ * ```ts
15
+ * PublicKey.findProgramAddressSync(
16
+ * [Buffer.from(COMMITMENT_SEED), Buffer.from(hash)],
17
+ * programId,
18
+ * );
19
+ * ```
20
+ *
21
+ * # The commitment hash
22
+ *
23
+ * `sha256(name_bytes ‖ owner_pubkey ‖ handle_type_byte ‖ secret[32] ‖
24
+ * program_id_bytes)` — the exact preimage order of the program's own
25
+ * `commitment_hash` (lib.rs), byte-verified against the deployed program by
26
+ * the E2E reveal phase (tools/scripts/e2e/journey-audit.ts). sha256, NOT
27
+ * keccak, for the same documented reason as `text_key_hash`: off-chain
28
+ * derivation is plain WebCrypto with no extra dependency — so
29
+ * {@link commitmentHash} is async, mirroring textRecords.ts's
30
+ * `hashTextKey`. The program id in the preimage is the SVM analog of
31
+ * ENS/ArcNS's `chainid + controller` domain separation: a commitment cannot
32
+ * replay against a different deployment.
33
+ *
34
+ * # Lifecycle (mirror of lib.rs)
35
+ *
36
+ * - `commit(commitment_hash)` `init`s `["commitment", hash]`, recording only
37
+ * `payer` + `committed_at` — the name is never on chain until reveal.
38
+ * Anyone may pay (the security is the secret preimage, not who pays).
39
+ * - `register_revealed(name, handle_type, secret)` recomputes the hash
40
+ * on-chain from the now-public fields; the supplied `commitment` account
41
+ * must sit at exactly the PDA that hash derives (`CommitmentMismatch`,
42
+ * 6077, otherwise). Valid only inside the window: at least
43
+ * `min_commitment_age_secs` after `committed_at` (`CommitmentTooNew`,
44
+ * 6074) and strictly before `max_commitment_age_secs` (`CommitmentTooOld`,
45
+ * 6075). Consuming closes the commitment — rent back. Pricing, integrator
46
+ * split and the `Handle` write are identical to `register` (both call the
47
+ * program's `register_core`); the result is a PERMANENT registration.
48
+ * - `cancel_commitment()` closes an unconsumed commitment, payer-only
49
+ * (`NotCommitmentPayer`, 6080), allowed at ANY age — the escape hatch for
50
+ * a lost secret or an abandoned flow, so rent is never stranded.
51
+ *
52
+ * A SECRET THAT IS LOST STRANDS THE COMMITMENT: without it the hash cannot
53
+ * be recomputed, so the commitment can only ever be `cancel_commitment`ed
54
+ * (which at least needs its address, discoverable via
55
+ * `getProgramAccounts` on the payer). Persist the secret client-side the
56
+ * moment it is generated, before `commit` is even sent.
57
+ */
58
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
59
+ /** Seed prefix of the commitment PDA: `["commitment", commitmentHash]`. */
60
+ export const COMMITMENT_SEED = "commitment";
61
+ /** Anchor instruction discriminator: `sha256("global:commit")[0..8]`. */
62
+ export const COMMIT_DISCRIMINATOR = Uint8Array.from([
63
+ 223, 140, 142, 165, 229, 208, 156, 74,
64
+ ]);
65
+ /** Anchor instruction discriminator: `sha256("global:register_revealed")[0..8]`. */
66
+ export const REGISTER_REVEALED_DISCRIMINATOR = Uint8Array.from([
67
+ 175, 112, 193, 148, 139, 78, 15, 86,
68
+ ]);
69
+ /** Anchor instruction discriminator: `sha256("global:cancel_commitment")[0..8]`. */
70
+ export const CANCEL_COMMITMENT_DISCRIMINATOR = Uint8Array.from([
71
+ 36, 39, 70, 137, 71, 179, 88, 232,
72
+ ]);
73
+ /** Anchor account discriminator: `sha256("account:Commitment")[0..8]`. */
74
+ export const COMMITMENT_DISCRIMINATOR = Uint8Array.from([
75
+ 61, 112, 129, 128, 24, 147, 77, 87,
76
+ ]);
77
+ /** `Commitment` account size: disc(8) payer(32) committed_at(8) bump(1). */
78
+ export const COMMITMENT_LEN = 49;
79
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
80
+ function toBytes32(v, what) {
81
+ if (typeof v === "string") {
82
+ const b = decodeBase58_32(v);
83
+ if (!b)
84
+ throw new Error(`${what} is not a valid base58 address`);
85
+ return b;
86
+ }
87
+ if (v.length !== 32)
88
+ throw new Error(`${what} must be exactly 32 bytes`);
89
+ return v;
90
+ }
91
+ function toBase58(v, what) {
92
+ return encodeBase58(toBytes32(v, what));
93
+ }
94
+ function checkHandleType(handleType) {
95
+ if (!Number.isInteger(handleType) || handleType < 0 || handleType > 3) {
96
+ throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
97
+ }
98
+ }
99
+ function checkSecret(secret) {
100
+ if (secret.length !== 32)
101
+ throw new Error("secret must be exactly 32 bytes");
102
+ }
103
+ /**
104
+ * The 32-byte commitment hash — `sha256(name ‖ owner ‖ handle_type ‖ secret
105
+ * ‖ program_id)`, the program's own preimage order (see the module docs).
106
+ * WebCrypto (`crypto.subtle`), so it is async — mirrors `hashTextKey`.
107
+ */
108
+ export async function commitmentHash(p) {
109
+ checkHandleType(p.handleType);
110
+ checkSecret(p.secret);
111
+ const nameBytes = new TextEncoder().encode(p.name);
112
+ if (nameBytes.length < 1 || nameBytes.length > 32) {
113
+ throw new Error("name must be 1..=32 bytes (UTF-8)");
114
+ }
115
+ const owner = toBytes32(p.owner, "owner");
116
+ const program = toBytes32(p.programId, "programId");
117
+ const preimage = new Uint8Array(nameBytes.length + 32 + 1 + 32 + 32);
118
+ let o = 0;
119
+ preimage.set(nameBytes, o);
120
+ o += nameBytes.length;
121
+ preimage.set(owner, o);
122
+ o += 32;
123
+ preimage[o] = p.handleType;
124
+ o += 1;
125
+ preimage.set(p.secret, o);
126
+ o += 32;
127
+ preimage.set(program, o);
128
+ const digest = await crypto.subtle.digest("SHA-256", preimage);
129
+ return new Uint8Array(digest);
130
+ }
131
+ /**
132
+ * Build `commit` — stake out the hash. Only the hash goes on chain; the
133
+ * name stays private until reveal. Account order matches `Commit<'info>`
134
+ * (lib.rs) exactly: payer, commitment, system_program.
135
+ */
136
+ export function buildCommitIx(p) {
137
+ if (p.commitmentHash.length !== 32) {
138
+ throw new Error("commitmentHash must be exactly 32 bytes (a sha256 digest)");
139
+ }
140
+ const data = new Uint8Array(8 + 32);
141
+ data.set(COMMIT_DISCRIMINATOR, 0);
142
+ data.set(p.commitmentHash, 8);
143
+ const keys = [
144
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
145
+ { pubkey: toBase58(p.commitment, "commitment"), isSigner: false, isWritable: true },
146
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
147
+ ];
148
+ return { programId: toBase58(p.programId, "programId"), keys, data };
149
+ }
150
+ /**
151
+ * Build `register_revealed` — the reveal step. Price is computed on-chain
152
+ * exactly like `register`'s (see register.ts's module docs); simulate first
153
+ * to show the payer a price. Account order matches
154
+ * `RegisterRevealed<'info>` (lib.rs) exactly: payer, owner, config,
155
+ * treasury, handle, commitment, system_program [, integrator,
156
+ * integrator_allowlist].
157
+ */
158
+ export function buildRegisterRevealedIx(p) {
159
+ checkHandleType(p.handleType);
160
+ checkSecret(p.secret);
161
+ const nameBytes = new TextEncoder().encode(p.name);
162
+ if (nameBytes.length < 1 || nameBytes.length > 32) {
163
+ throw new Error("name must be 1..=32 bytes (UTF-8)");
164
+ }
165
+ const data = new Uint8Array(8 + 4 + nameBytes.length + 1 + 32);
166
+ data.set(REGISTER_REVEALED_DISCRIMINATOR, 0);
167
+ new DataView(data.buffer).setUint32(8, nameBytes.length, true);
168
+ data.set(nameBytes, 12);
169
+ data[12 + nameBytes.length] = p.handleType;
170
+ data.set(p.secret, 12 + nameBytes.length + 1);
171
+ const hasIntegrator = p.integrator !== undefined || p.integratorAllowlist !== undefined;
172
+ if (hasIntegrator && (p.integrator === undefined || p.integratorAllowlist === undefined)) {
173
+ throw new Error("integrator and integratorAllowlist must both be supplied, or neither");
174
+ }
175
+ const keys = [
176
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
177
+ { pubkey: toBase58(p.owner, "owner"), isSigner: false, isWritable: false },
178
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
179
+ { pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
180
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
181
+ { pubkey: toBase58(p.commitment, "commitment"), isSigner: false, isWritable: true },
182
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
183
+ ];
184
+ if (hasIntegrator) {
185
+ keys.push({ pubkey: toBase58(p.integrator, "integrator"), isSigner: false, isWritable: true }, {
186
+ pubkey: toBase58(p.integratorAllowlist, "integratorAllowlist"),
187
+ isSigner: false,
188
+ isWritable: false,
189
+ });
190
+ }
191
+ return { programId: toBase58(p.programId, "programId"), keys, data };
192
+ }
193
+ /**
194
+ * Build `cancel_commitment` — reclaim an unconsumed commitment's rent,
195
+ * payer-only, allowed at any age. Account order matches
196
+ * `CancelCommitment<'info>` (lib.rs) exactly: payer, commitment.
197
+ */
198
+ export function buildCancelCommitmentIx(p) {
199
+ const data = new Uint8Array(8);
200
+ data.set(CANCEL_COMMITMENT_DISCRIMINATOR, 0);
201
+ return {
202
+ programId: toBase58(p.programId, "programId"),
203
+ keys: [
204
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
205
+ { pubkey: toBase58(p.commitment, "commitment"), isSigner: false, isWritable: true },
206
+ ],
207
+ data,
208
+ };
209
+ }
210
+ /**
211
+ * Decode a `Commitment` account's raw data, or null if not one (wrong
212
+ * discriminator or too short).
213
+ */
214
+ export function decodeCommitment(data) {
215
+ if (data.length < COMMITMENT_LEN)
216
+ return null;
217
+ for (let i = 0; i < 8; i++)
218
+ if (data[i] !== COMMITMENT_DISCRIMINATOR[i])
219
+ return null;
220
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
221
+ return {
222
+ payer: encodeBase58(data.slice(8, 40)),
223
+ committedAt: view.getBigInt64(40, true),
224
+ bump: data[48],
225
+ };
226
+ }
227
+ /**
228
+ * Fetch + decode one `Commitment` account, or null when it doesn't exist
229
+ * (never committed, already consumed by a reveal, or canceled — all leave
230
+ * no account) or isn't shaped like a `Commitment`.
231
+ */
232
+ export async function fetchCommitment(rpc, commitmentAccount) {
233
+ const res = (await rpc("getAccountInfo", [
234
+ commitmentAccount,
235
+ { encoding: "base64", commitment: "confirmed" },
236
+ ]));
237
+ const data = res?.value?.data?.[0];
238
+ if (!data)
239
+ return null;
240
+ const bin = atob(data);
241
+ const raw = new Uint8Array(bin.length);
242
+ for (let i = 0; i < bin.length; i++)
243
+ raw[i] = bin.charCodeAt(i);
244
+ return decodeCommitment(raw);
245
+ }
package/dist/index.d.ts CHANGED
@@ -38,6 +38,14 @@ export * from "./textRecords.js";
38
38
  export * from "./lock.js";
39
39
  export * from "./integrator.js";
40
40
  export * from "./voucher.js";
41
+ export * from "./agent.js";
42
+ export * from "./register.js";
43
+ export * from "./accounts.js";
44
+ export * from "./x402.js";
45
+ export * from "./recordWrite.js";
46
+ export * from "./clearRecords.js";
47
+ export * from "./commitReveal.js";
48
+ export * from "./pnftTransfer.js";
41
49
  import { type Chain, type Resolved } from "./types.js";
42
50
  import { WasmResolver } from "./wasm.js";
43
51
  import { type HandleRecord } from "./records.js";