@x1id/resolve 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -204,6 +204,76 @@ one: `ResolveError { code: "not-found", reason: "nft-burned" }`. The registry
204
204
  account's stale `owner` field is never returned for a tokenized handle, on any
205
205
  path.
206
206
 
207
+ ## Per-platform verification ("verified by [platform]")
208
+
209
+ Beyond the per-chain `verification` above, a handle can carry **per-platform
210
+ verifications** — the blue-tick signal that a handle proved control of an X,
211
+ Discord, GitHub, Telegram account or a domain (#7145). A resolver that supplies
212
+ them (the tools/api REST `/resolve` response) attaches an optional
213
+ `verifications[]` to the `Resolved` result:
214
+
215
+ ```ts
216
+ import { isVerified, verifiedPlatforms, isVerifiedOn } from "@x1id/resolve";
217
+
218
+ // `resolved` is a Resolved carrying the resolver's `verifications[]`, e.g.
219
+ // verifications: [
220
+ // { platform: "x", value: "@michelle", verified: true, attested_at: 1730000000 },
221
+ // { platform: "github", value: "michelle", verified: true, attested_at: 1730000100 },
222
+ // { platform: "domain", value: "michelle.com", verified: true, attested_at: 1730000200 },
223
+ // ]
224
+
225
+ isVerified(resolved); // true — show the blue tick (any live signal)
226
+ verifiedPlatforms(resolved); // ["x", "github", "domain"]
227
+ isVerifiedOn(resolved, "github"); // true
228
+ isVerifiedOn(resolved, "telegram"); // false
229
+ ```
230
+
231
+ **Rendering a badge.** Light the tick when `isVerified(resolved)` is true; on
232
+ hover/focus, list `verifiedPlatforms(resolved)`, and pull each platform's
233
+ account handle from the paired `value` (`@michelle` for `x`, `michelle.com` for
234
+ `domain`). Platform slugs are `x` / `discord` / `github` / `telegram`, plus
235
+ legacy `domain` (DNS) and `social` (pre-per-platform, no attribution).
236
+
237
+ All three helpers are **absence-tolerant**: an older resolver, the SDK's own
238
+ on-chain `resolve()` (which does not populate the field — see below), or a
239
+ handle with no live verifications all yield `false` / `[]`, never a throw. The
240
+ field is optional and additive, so existing `Resolved` consumers are unaffected.
241
+
242
+ > **Honest trust caveat — read before you render.** A verification is an
243
+ > **off-chain review** (a DNS lookup, an OAuth completion, a human check) that
244
+ > x1id's **attestor key** signed off on. It is **not** a trustless proof: that
245
+ > key is admin-rotatable, so the anchor is "whoever the registry admin currently
246
+ > designates". Verifications are also **epoch-bound** — the resolver emits an
247
+ > entry only while the underlying attestation is live (`attested_at >=
248
+ > registered_at` **and** `>= records_cleared_at`), so a previous owner's proof
249
+ > never leaks through. Render it as **"verified by x1id"**, never as more.
250
+
251
+ ### Reading verification straight from chain
252
+
253
+ The SDK's own on-chain `resolve()` does **not** populate `verifications` (it
254
+ returns only what an RPC read proves). To compute the same signal from chain,
255
+ read the `Attestation` accounts directly — this applies the staleness rule
256
+ structurally:
257
+
258
+ ```ts
259
+ import {
260
+ fetchAttestations, liveAttestations, isHandleVerified,
261
+ platformSlug, textRecordKeyFor,
262
+ } from "@x1id/resolve";
263
+
264
+ const atts = await fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt);
265
+ isHandleVerified(atts); // the #7145 tick, from chain
266
+ for (const a of liveAttestations(atts)) {
267
+ platformSlug(a.kind); // "x" | "discord" | "github" | "telegram" | null (legacy)
268
+ textRecordKeyFor(a.kind); // "com.x" | ... | "website" | null — where the value lives
269
+ }
270
+ ```
271
+
272
+ `platformSlug` / `textRecordKeyFor` mirror the on-chain
273
+ `Attestation::platform_slug` and the shared contract table exactly, so an
274
+ integrator joining a live attestation to its paired text record reads the same
275
+ mapping the program, resolver and app badge use.
276
+
207
277
  ## What works today
208
278
 
209
279
  | | Status |
@@ -300,7 +370,7 @@ Also shipped, same build → sign → relay shape:
300
370
  | Typed text records (website / avatar / socials) | `buildCreateTextRecordIx`, `buildUpdateTextRecordIx`, `buildCloseTextRecordIx` |
301
371
  | Register a handle | `buildRegisterIx` (+ `fetchConfigTreasury`) |
302
372
  | Verify a non-SVM address record | `buildCreateRecordIx`, `buildVerifyRecordEthIx`, `buildVerifyRecordSvmIx`, `buildVerifyRecordBtcIx` (+ `buildRecordChallenge`, `splitEthSignature`, `splitBtcSignature`) — see [Verifying a non-SVM address record](#verifying-a-non-svm-address-record) |
303
- | Domain / social attestations | `buildCreateAttestationIx`, `buildCloseAttestationIx` (+ `fetchAttestations`, `isHandleVerified`) |
373
+ | Domain / social / per-platform attestations | `buildCreateAttestationIx` (kinds 0–5), `buildCloseAttestationIx` (+ `fetchAttestations`, `isHandleVerified`, `platformSlug`, `textRecordKeyFor`) — see [Per-platform verification](#per-platform-verification-verified-by-platform) |
304
374
  | Agent records (identity, manifest, x402 hints) | `validateManifest`, `agentVerificationLevel`, `x402AcceptsFromManifest`, `agentTextRecords` — see [Agent records](#agent-records) below |
305
375
  | Subname records / revoke | `buildRevokeSubnameIx`, `buildCreateSubnameRecordIx`, `buildUpdateSubnameRecordIx`, `buildCloseSubnameRecordIx` |
306
376
  | Voucher claim / refund | `buildClaimVoucherIx`, `buildRefundVoucherIx` |
@@ -62,14 +62,35 @@
62
62
  */
63
63
  import type { AddressLike, BuiltInstruction } from "./delegate.js";
64
64
  import type { RpcFn } from "./accounts.js";
65
+ import type { HandleVerification } from "./types.js";
65
66
  /** Seed prefix of an attestation PDA: `["attestation", handlePda, kindByte]`. */
66
67
  export declare const ATTESTATION_SEED = "attestation";
67
68
  /** Seed of the attestor-config singleton PDA: `["attestor_config"]`. */
68
69
  export declare const ATTESTOR_CONFIG_SEED = "attestor_config";
69
- /** `Attestation.kind` — domain control proven (DNS TXT challenge). */
70
+ /** `Attestation.kind` — domain control proven (DNS TXT challenge). LEGACY-safe:
71
+ * kept valid forever. Pairs with the `website` text record. */
70
72
  export declare const ATTESTATION_KIND_DNS = 0;
71
- /** `Attestation.kind` — social-account control proven. */
73
+ /** `Attestation.kind` — generic social-account control proven. LEGACY: the
74
+ * single pre-per-platform code, carrying no platform attribution. Kept valid
75
+ * forever so existing accounts are never orphaned; new attestations should use
76
+ * one of the per-platform codes below. */
72
77
  export declare const ATTESTATION_KIND_SOCIAL = 1;
78
+ /** `Attestation.kind` — X (Twitter) control proven. Pairs with text record
79
+ * `com.x`; platform slug `x`. */
80
+ export declare const ATTESTATION_KIND_X = 2;
81
+ /** `Attestation.kind` — Discord control proven. Pairs with text record
82
+ * `com.discord`; platform slug `discord`. */
83
+ export declare const ATTESTATION_KIND_DISCORD = 3;
84
+ /** `Attestation.kind` — GitHub control proven. Pairs with text record
85
+ * `com.github`; platform slug `github`. */
86
+ export declare const ATTESTATION_KIND_GITHUB = 4;
87
+ /** `Attestation.kind` — Telegram control proven. Pairs with text record
88
+ * `org.telegram`; platform slug `telegram`. */
89
+ export declare const ATTESTATION_KIND_TELEGRAM = 5;
90
+ /** One past the highest valid `kind`. `create_attestation` refuses anything
91
+ * `>=` this on-chain; the SDK's builder enforces the same bound. Mirrors
92
+ * `Attestation::KIND_COUNT` (programs/x1-handles/src/state.rs). */
93
+ export declare const ATTESTATION_KIND_COUNT = 6;
73
94
  /** Anchor account discriminator: `sha256("account:Attestation")[0..8]`.
74
95
  * Pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
75
96
  * everywhere it runs); asserted against a re-derivation in the test suite
@@ -90,19 +111,42 @@ export declare const CLOSE_ATTESTATION_DISCRIMINATOR: Uint8Array;
90
111
  export declare const ATTESTATION_LEN = 114;
91
112
  /** `AttestorConfig` account size: disc(8) + attestor(32) + bump(1). */
92
113
  export declare const ATTESTOR_CONFIG_LEN = 41;
114
+ /** Human name an attestation `kind` byte carries. */
115
+ export type AttestationKindName = "dns" | "social" | "x" | "discord" | "github" | "telegram";
93
116
  /** Which verification signal an attestation `kind` byte names, or null for a
94
- * kind this SDK does not know (future program versions may add kinds). */
95
- export declare function attestationKindName(kind: number): "dns" | "social" | null;
117
+ * kind this SDK does not know (future program versions may add kinds). For the
118
+ * per-platform kinds this returns the platform slug; the two legacy kinds keep
119
+ * their historical names (`dns`, `social`). */
120
+ export declare function attestationKindName(kind: number): AttestationKindName | null;
121
+ /**
122
+ * The canonical platform slug for a `kind`, or null for the two legacy codes
123
+ * (0 = dns, 1 = social — neither names a single platform) and for any unknown
124
+ * kind. Mirrors `Attestation::platform_slug` (programs/x1-handles/src/state.rs)
125
+ * exactly: `Some` only for the per-platform kinds 2..5.
126
+ */
127
+ export declare function platformSlug(kind: number): string | null;
128
+ /**
129
+ * The text-record key an attestation `kind` pairs with — where the actual
130
+ * account handle / domain for that platform lives — or null for the legacy
131
+ * generic-social kind (1) and any unknown kind. Mirrors the shared contract
132
+ * table: kind 0 → `website`, 2 → `com.x`, 3 → `com.discord`, 4 → `com.github`,
133
+ * 5 → `org.telegram`.
134
+ */
135
+ export declare function textRecordKeyFor(kind: number): string | null;
96
136
  /** A decoded verification attestation, staleness already judged. */
97
137
  export interface HandleAttestation {
98
138
  /** The Attestation account's address, base58. */
99
139
  readonly account: string;
100
140
  /** The Handle account this attestation is for, base58. */
101
141
  readonly handle: string;
102
- /** Raw kind byte (0 = dns, 1 = social). */
142
+ /** Raw kind byte (0 = dns, 1 = social, 2 = x, 3 = discord, 4 = github,
143
+ * 5 = telegram). */
103
144
  readonly kind: number;
104
- /** Human name for `kind`, or null for an unknown kind. */
105
- readonly kindName: "dns" | "social" | null;
145
+ /** Human name for `kind`, or null for an unknown kind. Per-platform kinds
146
+ * resolve to their platform slug; see {@link platformSlug} /
147
+ * {@link textRecordKeyFor} to map a kind to its slug and paired text-record
148
+ * key. */
149
+ readonly kindName: AttestationKindName | null;
106
150
  /** `sha256` commitment to the off-chain verdict-inputs bundle. */
107
151
  readonly evidenceHash: Uint8Array;
108
152
  /** Unix seconds the attestation was (last) stamped. */
@@ -189,7 +233,11 @@ export interface CreateAttestationParams {
189
233
  /** The `["attestation", handle, kindByte]` PDA — derive per the module
190
234
  * docs, with the SAME kind byte passed below. */
191
235
  readonly attestation: AddressLike;
192
- /** {@link ATTESTATION_KIND_DNS} or {@link ATTESTATION_KIND_SOCIAL}. */
236
+ /** One of the `ATTESTATION_KIND_*` codes (0..{@link ATTESTATION_KIND_COUNT}
237
+ * − 1): {@link ATTESTATION_KIND_DNS}, {@link ATTESTATION_KIND_SOCIAL}, or a
238
+ * per-platform kind ({@link ATTESTATION_KIND_X},
239
+ * {@link ATTESTATION_KIND_DISCORD}, {@link ATTESTATION_KIND_GITHUB},
240
+ * {@link ATTESTATION_KIND_TELEGRAM}). */
193
241
  readonly kind: number;
194
242
  /** `sha256` of the off-chain verdict-inputs bundle (32 bytes) — a
195
243
  * commitment, never the raw evidence. */
@@ -224,3 +272,31 @@ export interface CloseAttestationParams {
224
272
  * a rotated-away key's output).
225
273
  */
226
274
  export declare function buildCloseAttestationIx(p: CloseAttestationParams): BuiltInstruction;
275
+ /** The minimal shape the verification helpers read — any {@link Resolved}
276
+ * satisfies it, as does a bare `{ verifications }` an integrator assembles
277
+ * from a raw REST payload. */
278
+ export interface HasVerifications {
279
+ readonly verifications?: readonly HandleVerification[];
280
+ }
281
+ /**
282
+ * The platform slugs a handle is LIVE-verified on — e.g. `["x", "github"]` —
283
+ * de-duplicated, in first-seen order. Only entries with `verified === true`
284
+ * count. Returns `[]` when `verifications` is absent or empty.
285
+ */
286
+ export declare function verifiedPlatforms(resolved: HasVerifications): string[];
287
+ /**
288
+ * Whether a handle carries a live verification for a specific platform slug
289
+ * (`"x"`, `"discord"`, `"github"`, `"telegram"`, `"domain"`, `"social"`).
290
+ * False when `verifications` is absent.
291
+ */
292
+ export declare function isVerifiedOn(resolved: HasVerifications, platform: string): boolean;
293
+ /**
294
+ * The #7145 single-signal rule at the resolve layer: true when the handle has
295
+ * AT LEAST ONE live verification of any platform. This is the blue-tick
296
+ * condition. False when `verifications` is absent or empty.
297
+ *
298
+ * Trust caveat (see {@link HandleVerification}): a true here means x1id's
299
+ * rotatable attestor key signed off on an off-chain review that was still live
300
+ * at resolve time — render it as "verified by x1id", not as trustless proof.
301
+ */
302
+ export declare function isVerified(resolved: HasVerifications): boolean;
@@ -66,10 +66,30 @@ import { decodeBase58_32 } from "./base58.js";
66
66
  export const ATTESTATION_SEED = "attestation";
67
67
  /** Seed of the attestor-config singleton PDA: `["attestor_config"]`. */
68
68
  export const ATTESTOR_CONFIG_SEED = "attestor_config";
69
- /** `Attestation.kind` — domain control proven (DNS TXT challenge). */
69
+ /** `Attestation.kind` — domain control proven (DNS TXT challenge). LEGACY-safe:
70
+ * kept valid forever. Pairs with the `website` text record. */
70
71
  export const ATTESTATION_KIND_DNS = 0;
71
- /** `Attestation.kind` — social-account control proven. */
72
+ /** `Attestation.kind` — generic social-account control proven. LEGACY: the
73
+ * single pre-per-platform code, carrying no platform attribution. Kept valid
74
+ * forever so existing accounts are never orphaned; new attestations should use
75
+ * one of the per-platform codes below. */
72
76
  export const ATTESTATION_KIND_SOCIAL = 1;
77
+ /** `Attestation.kind` — X (Twitter) control proven. Pairs with text record
78
+ * `com.x`; platform slug `x`. */
79
+ export const ATTESTATION_KIND_X = 2;
80
+ /** `Attestation.kind` — Discord control proven. Pairs with text record
81
+ * `com.discord`; platform slug `discord`. */
82
+ export const ATTESTATION_KIND_DISCORD = 3;
83
+ /** `Attestation.kind` — GitHub control proven. Pairs with text record
84
+ * `com.github`; platform slug `github`. */
85
+ export const ATTESTATION_KIND_GITHUB = 4;
86
+ /** `Attestation.kind` — Telegram control proven. Pairs with text record
87
+ * `org.telegram`; platform slug `telegram`. */
88
+ export const ATTESTATION_KIND_TELEGRAM = 5;
89
+ /** One past the highest valid `kind`. `create_attestation` refuses anything
90
+ * `>=` this on-chain; the SDK's builder enforces the same bound. Mirrors
91
+ * `Attestation::KIND_COUNT` (programs/x1-handles/src/state.rs). */
92
+ export const ATTESTATION_KIND_COUNT = 6;
73
93
  /** Anchor account discriminator: `sha256("account:Attestation")[0..8]`.
74
94
  * Pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
75
95
  * everywhere it runs); asserted against a re-derivation in the test suite
@@ -120,13 +140,69 @@ function toBase58(v, what) {
120
140
  return encodeBase58(toBytes32(v, what));
121
141
  }
122
142
  /** Which verification signal an attestation `kind` byte names, or null for a
123
- * kind this SDK does not know (future program versions may add kinds). */
143
+ * kind this SDK does not know (future program versions may add kinds). For the
144
+ * per-platform kinds this returns the platform slug; the two legacy kinds keep
145
+ * their historical names (`dns`, `social`). */
124
146
  export function attestationKindName(kind) {
125
- if (kind === ATTESTATION_KIND_DNS)
126
- return "dns";
127
- if (kind === ATTESTATION_KIND_SOCIAL)
128
- return "social";
129
- return null;
147
+ switch (kind) {
148
+ case ATTESTATION_KIND_DNS:
149
+ return "dns";
150
+ case ATTESTATION_KIND_SOCIAL:
151
+ return "social";
152
+ case ATTESTATION_KIND_X:
153
+ return "x";
154
+ case ATTESTATION_KIND_DISCORD:
155
+ return "discord";
156
+ case ATTESTATION_KIND_GITHUB:
157
+ return "github";
158
+ case ATTESTATION_KIND_TELEGRAM:
159
+ return "telegram";
160
+ default:
161
+ return null;
162
+ }
163
+ }
164
+ /**
165
+ * The canonical platform slug for a `kind`, or null for the two legacy codes
166
+ * (0 = dns, 1 = social — neither names a single platform) and for any unknown
167
+ * kind. Mirrors `Attestation::platform_slug` (programs/x1-handles/src/state.rs)
168
+ * exactly: `Some` only for the per-platform kinds 2..5.
169
+ */
170
+ export function platformSlug(kind) {
171
+ switch (kind) {
172
+ case ATTESTATION_KIND_X:
173
+ return "x";
174
+ case ATTESTATION_KIND_DISCORD:
175
+ return "discord";
176
+ case ATTESTATION_KIND_GITHUB:
177
+ return "github";
178
+ case ATTESTATION_KIND_TELEGRAM:
179
+ return "telegram";
180
+ default:
181
+ return null;
182
+ }
183
+ }
184
+ /**
185
+ * The text-record key an attestation `kind` pairs with — where the actual
186
+ * account handle / domain for that platform lives — or null for the legacy
187
+ * generic-social kind (1) and any unknown kind. Mirrors the shared contract
188
+ * table: kind 0 → `website`, 2 → `com.x`, 3 → `com.discord`, 4 → `com.github`,
189
+ * 5 → `org.telegram`.
190
+ */
191
+ export function textRecordKeyFor(kind) {
192
+ switch (kind) {
193
+ case ATTESTATION_KIND_DNS:
194
+ return "website";
195
+ case ATTESTATION_KIND_X:
196
+ return "com.x";
197
+ case ATTESTATION_KIND_DISCORD:
198
+ return "com.discord";
199
+ case ATTESTATION_KIND_GITHUB:
200
+ return "com.github";
201
+ case ATTESTATION_KIND_TELEGRAM:
202
+ return "org.telegram";
203
+ default:
204
+ return null;
205
+ }
130
206
  }
131
207
  /**
132
208
  * Decode one Attestation account.
@@ -250,8 +326,9 @@ export function buildSetAttestorIx(p) {
250
326
  * the (handle, kind) attestation.
251
327
  */
252
328
  export function buildCreateAttestationIx(p) {
253
- if (!Number.isInteger(p.kind) || p.kind < 0 || p.kind > 1) {
254
- throw new Error("kind must be 0 (dns) or 1 (social)");
329
+ if (!Number.isInteger(p.kind) || p.kind < 0 || p.kind >= ATTESTATION_KIND_COUNT) {
330
+ throw new Error(`kind must be an integer in 0..${ATTESTATION_KIND_COUNT - 1} ` +
331
+ "(0 dns, 1 social, 2 x, 3 discord, 4 github, 5 telegram)");
255
332
  }
256
333
  if (p.evidenceHash.length !== 32) {
257
334
  throw new Error("evidenceHash must be exactly 32 bytes (a sha256 digest)");
@@ -288,3 +365,36 @@ export function buildCloseAttestationIx(p) {
288
365
  data: CLOSE_ATTESTATION_DISCRIMINATOR.slice(),
289
366
  };
290
367
  }
368
+ /**
369
+ * The platform slugs a handle is LIVE-verified on — e.g. `["x", "github"]` —
370
+ * de-duplicated, in first-seen order. Only entries with `verified === true`
371
+ * count. Returns `[]` when `verifications` is absent or empty.
372
+ */
373
+ export function verifiedPlatforms(resolved) {
374
+ const out = [];
375
+ for (const v of resolved.verifications ?? []) {
376
+ if (v && v.verified && !out.includes(v.platform))
377
+ out.push(v.platform);
378
+ }
379
+ return out;
380
+ }
381
+ /**
382
+ * Whether a handle carries a live verification for a specific platform slug
383
+ * (`"x"`, `"discord"`, `"github"`, `"telegram"`, `"domain"`, `"social"`).
384
+ * False when `verifications` is absent.
385
+ */
386
+ export function isVerifiedOn(resolved, platform) {
387
+ return (resolved.verifications ?? []).some((v) => v && v.verified && v.platform === platform);
388
+ }
389
+ /**
390
+ * The #7145 single-signal rule at the resolve layer: true when the handle has
391
+ * AT LEAST ONE live verification of any platform. This is the blue-tick
392
+ * condition. False when `verifications` is absent or empty.
393
+ *
394
+ * Trust caveat (see {@link HandleVerification}): a true here means x1id's
395
+ * rotatable attestor key signed off on an off-chain review that was still live
396
+ * at resolve time — render it as "verified by x1id", not as trustless proof.
397
+ */
398
+ export function isVerified(resolved) {
399
+ return (resolved.verifications ?? []).some((v) => v && v.verified);
400
+ }
@@ -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
@@ -44,6 +44,8 @@ export * from "./accounts.js";
44
44
  export * from "./x402.js";
45
45
  export * from "./recordWrite.js";
46
46
  export * from "./clearRecords.js";
47
+ export * from "./commitReveal.js";
48
+ export * from "./pnftTransfer.js";
47
49
  import { type Chain, type Resolved } from "./types.js";
48
50
  import { WasmResolver } from "./wasm.js";
49
51
  import { type HandleRecord } from "./records.js";
package/dist/index.js CHANGED
@@ -51,6 +51,8 @@ export * from "./accounts.js";
51
51
  export * from "./x402.js";
52
52
  export * from "./recordWrite.js";
53
53
  export * from "./clearRecords.js";
54
+ export * from "./commitReveal.js";
55
+ export * from "./pnftTransfer.js";
54
56
  import { ResolveError, CHAIN_COIN_TYPE } from "./types.js";
55
57
  import { parseName } from "./parse.js";
56
58
  import { encodeBase58, decodeBase58_32 } from "./base58.js";
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Move a handle's capability NFT — `TransferV1` for Metaplex
3
+ * ProgrammableNonFungibles (pNFTs), the standard `mint_handle_nft` mints.
4
+ *
5
+ * One import to move a handle NFT:
6
+ *
7
+ * ```ts
8
+ * import { WasmResolver, buildTransferV1Ix, deriveTransferV1Accounts } from "@x1id/resolve";
9
+ *
10
+ * const accounts = deriveTransferV1Accounts(wasm, {
11
+ * mint, // Handle.nft_mint (parseHandleAccount(...).nftMint)
12
+ * holder, // current holder — signs
13
+ * recipient, // destination wallet
14
+ * });
15
+ * const ix = buildTransferV1Ix({ ...accounts, amount: 1n });
16
+ * // adapt `ix` to your runtime (see delegate.ts's module docs for the
17
+ * // two-line web3.js adapter) and send with the holder's signature.
18
+ * ```
19
+ *
20
+ * # Why a plain SPL transfer no longer works
21
+ *
22
+ * A pNFT's token accounts are permanently FROZEN by the Token Metadata
23
+ * program (that is how it enforces programmability): `spl_token::transfer`
24
+ * fails with `AccountFrozen`. The ONLY way to move one is the Token Metadata
25
+ * program's own `TransferV1`, which thaw-moves-refreezes under the hood and
26
+ * maintains one `TokenRecord` PDA per token account on both sides.
27
+ * READ paths are unaffected — a frozen account still reports `amount 1`, so
28
+ * holder resolution (`nftHolder`, `reverse()`) is identical for both
29
+ * standards.
30
+ *
31
+ * Handles tokenized BEFORE the pNFT cutover hold plain `NonFungible`s
32
+ * (unfrozen) — those still move by plain SPL transfer. Branch on the mint's
33
+ * metadata `token_standard` (or on the source ATA's frozen state); this
34
+ * module only builds the pNFT path.
35
+ *
36
+ * # Wire format (verified against mpl-token-metadata 5.1.1, the crate the
37
+ * # workspace Cargo.lock pins — `src/generated/instructions/transfer_v1.rs`)
38
+ *
39
+ * Data: `[49, 0]` (instruction discriminator `Transfer` = 49, then
40
+ * `TransferArgs::V1` = 0) ‖ `amount: u64 LE` ‖ `authorization_data:
41
+ * Option` (`0` — this SDK never builds `AuthorizationData`; handle rule
42
+ * sets carry `rule_set: None`, so none is ever needed). 11 bytes total.
43
+ *
44
+ * Accounts, in exactly this order (17): token(w) token_owner
45
+ * destination_token(w) destination_owner mint metadata(w) edition
46
+ * token_record(w) destination_token_record(w) authority(s) payer(s,w)
47
+ * system_program sysvar_instructions spl_token_program spl_ata_program
48
+ * authorization_rules_program authorization_rules. An absent OPTIONAL slot
49
+ * is filled with the Token Metadata program id itself, read-only — Metaplex's
50
+ * "explicitly None" convention (kinobi-generated builders do the same).
51
+ *
52
+ * The destination ATA and its `TokenRecord` need not exist: `TransferV1`
53
+ * creates both (rent from `payer`) — that is why the ATA program is in the
54
+ * account list. No separate create-ATA instruction is needed.
55
+ *
56
+ * PDAs are derived through the WASM module (this package's one rule: never
57
+ * hand-roll `find_program_address`'s on-curve check in TypeScript — see
58
+ * wasm.ts). The Token Metadata program id is a parameter everywhere because
59
+ * X1 runs its own deployment ({@link MPL_TOKEN_METADATA_PROGRAM}).
60
+ */
61
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
62
+ import type { WasmResolver } from "./wasm.js";
63
+ /** X1's Token Metadata deployment — the id `mint_handle_nft` CPIs into
64
+ * (lib.rs `TOKEN_METADATA_PROGRAM_ID`, `…x1s` suffix). NOT the canonical
65
+ * Solana-mainnet id; on another chain, pass that deployment's id instead. */
66
+ export declare const MPL_TOKEN_METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
67
+ /** `MetadataInstruction::Transfer`'s Borsh enum discriminator. */
68
+ export declare const TRANSFER_IX_DISCRIMINATOR = 49;
69
+ /** `TransferArgs::V1`'s Borsh enum discriminator. */
70
+ export declare const TRANSFER_ARGS_V1 = 0;
71
+ /** Metadata `token_standard` value for a pNFT
72
+ * (`TokenStandard::ProgrammableNonFungible`, mpl 5.1.1 enum order). */
73
+ export declare const TOKEN_STANDARD_PROGRAMMABLE_NON_FUNGIBLE = 4;
74
+ /**
75
+ * Derive the `TokenRecord` PDA for `(mint, token)` — `token` is the token
76
+ * ACCOUNT (the ATA), not its owner. Byte-for-byte
77
+ * `mpl_token_metadata::accounts::TokenRecord::find_pda`; seeds
78
+ * `["metadata", program, mint, "token_record", token]` under `programId`.
79
+ */
80
+ export declare function deriveTokenRecordPda(wasm: WasmResolver, mint: AddressLike, token: AddressLike, programId?: AddressLike): string;
81
+ /** Every address {@link buildTransferV1Ix} needs, derived from just
82
+ * `(mint, holder, recipient)`. */
83
+ export interface TransferV1Accounts {
84
+ /** The holder's ATA — the account the NFT leaves. */
85
+ readonly token: string;
86
+ /** The holder. */
87
+ readonly tokenOwner: string;
88
+ /** The recipient's ATA — created by `TransferV1` itself if absent. */
89
+ readonly destinationToken: string;
90
+ readonly destinationOwner: string;
91
+ readonly mint: string;
92
+ readonly metadata: string;
93
+ readonly edition: string;
94
+ readonly tokenRecord: string;
95
+ readonly destinationTokenRecord: string;
96
+ /** The holder — pNFT self-transfers sign as owner. */
97
+ readonly authority: string;
98
+ /** Rent for the destination ATA + token record. Defaults to the holder. */
99
+ readonly payer: string;
100
+ readonly tokenMetadataProgramId: string;
101
+ }
102
+ /**
103
+ * Derive the full `TransferV1` account set for moving a handle NFT from
104
+ * `holder` to `recipient`. Pure derivation — nothing is fetched; whether the
105
+ * mint really is a pNFT is the caller's check (read the metadata
106
+ * `token_standard`, or the source ATA's frozen state).
107
+ */
108
+ export declare function deriveTransferV1Accounts(wasm: WasmResolver, p: {
109
+ readonly mint: AddressLike;
110
+ readonly holder: AddressLike;
111
+ readonly recipient: AddressLike;
112
+ /** Pays destination-side rent. Defaults to `holder`. */
113
+ readonly payer?: AddressLike;
114
+ readonly tokenMetadataProgramId?: AddressLike;
115
+ }): TransferV1Accounts;
116
+ export interface TransferV1Params {
117
+ /** Source token account (the holder's ATA). */
118
+ readonly token: AddressLike;
119
+ readonly tokenOwner: AddressLike;
120
+ /** Destination token account — need not exist yet (see module docs). */
121
+ readonly destinationToken: AddressLike;
122
+ readonly destinationOwner: AddressLike;
123
+ readonly mint: AddressLike;
124
+ readonly metadata: AddressLike;
125
+ /** Master Edition PDA. Required for a pNFT. */
126
+ readonly edition: AddressLike;
127
+ /** `TokenRecord` PDA of the SOURCE token account. Required for a pNFT. */
128
+ readonly tokenRecord: AddressLike;
129
+ /** `TokenRecord` PDA of the DESTINATION token account. Required for a
130
+ * pNFT — `TransferV1` creates the account if it does not exist. */
131
+ readonly destinationTokenRecord: AddressLike;
132
+ /** The transfer authority (holder or delegate). Signer. */
133
+ readonly authority: AddressLike;
134
+ /** Pays destination-side rent. Signer. */
135
+ readonly payer: AddressLike;
136
+ /** Defaults to 1 — handle NFTs are fixed-supply-1. */
137
+ readonly amount?: bigint;
138
+ /** OPTIONAL auth-rules pair — supply BOTH or NEITHER. Handle NFTs are
139
+ * minted with `rule_set: None`, so normally neither: the slots are then
140
+ * filled with the "explicitly None" placeholder (the program id). Plumbed
141
+ * for completeness should a rule set ever exist. */
142
+ readonly authorizationRulesProgram?: AddressLike;
143
+ readonly authorizationRules?: AddressLike;
144
+ readonly tokenMetadataProgramId?: AddressLike;
145
+ }
146
+ /**
147
+ * Build `TransferV1`. See the module docs for the verified account order and
148
+ * data serialization. `authorization_data` is always serialized `None` —
149
+ * with `rule_set: None` there is nothing to authorize against.
150
+ */
151
+ export declare function buildTransferV1Ix(p: TransferV1Params): BuiltInstruction;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Move a handle's capability NFT — `TransferV1` for Metaplex
3
+ * ProgrammableNonFungibles (pNFTs), the standard `mint_handle_nft` mints.
4
+ *
5
+ * One import to move a handle NFT:
6
+ *
7
+ * ```ts
8
+ * import { WasmResolver, buildTransferV1Ix, deriveTransferV1Accounts } from "@x1id/resolve";
9
+ *
10
+ * const accounts = deriveTransferV1Accounts(wasm, {
11
+ * mint, // Handle.nft_mint (parseHandleAccount(...).nftMint)
12
+ * holder, // current holder — signs
13
+ * recipient, // destination wallet
14
+ * });
15
+ * const ix = buildTransferV1Ix({ ...accounts, amount: 1n });
16
+ * // adapt `ix` to your runtime (see delegate.ts's module docs for the
17
+ * // two-line web3.js adapter) and send with the holder's signature.
18
+ * ```
19
+ *
20
+ * # Why a plain SPL transfer no longer works
21
+ *
22
+ * A pNFT's token accounts are permanently FROZEN by the Token Metadata
23
+ * program (that is how it enforces programmability): `spl_token::transfer`
24
+ * fails with `AccountFrozen`. The ONLY way to move one is the Token Metadata
25
+ * program's own `TransferV1`, which thaw-moves-refreezes under the hood and
26
+ * maintains one `TokenRecord` PDA per token account on both sides.
27
+ * READ paths are unaffected — a frozen account still reports `amount 1`, so
28
+ * holder resolution (`nftHolder`, `reverse()`) is identical for both
29
+ * standards.
30
+ *
31
+ * Handles tokenized BEFORE the pNFT cutover hold plain `NonFungible`s
32
+ * (unfrozen) — those still move by plain SPL transfer. Branch on the mint's
33
+ * metadata `token_standard` (or on the source ATA's frozen state); this
34
+ * module only builds the pNFT path.
35
+ *
36
+ * # Wire format (verified against mpl-token-metadata 5.1.1, the crate the
37
+ * # workspace Cargo.lock pins — `src/generated/instructions/transfer_v1.rs`)
38
+ *
39
+ * Data: `[49, 0]` (instruction discriminator `Transfer` = 49, then
40
+ * `TransferArgs::V1` = 0) ‖ `amount: u64 LE` ‖ `authorization_data:
41
+ * Option` (`0` — this SDK never builds `AuthorizationData`; handle rule
42
+ * sets carry `rule_set: None`, so none is ever needed). 11 bytes total.
43
+ *
44
+ * Accounts, in exactly this order (17): token(w) token_owner
45
+ * destination_token(w) destination_owner mint metadata(w) edition
46
+ * token_record(w) destination_token_record(w) authority(s) payer(s,w)
47
+ * system_program sysvar_instructions spl_token_program spl_ata_program
48
+ * authorization_rules_program authorization_rules. An absent OPTIONAL slot
49
+ * is filled with the Token Metadata program id itself, read-only — Metaplex's
50
+ * "explicitly None" convention (kinobi-generated builders do the same).
51
+ *
52
+ * The destination ATA and its `TokenRecord` need not exist: `TransferV1`
53
+ * creates both (rent from `payer`) — that is why the ATA program is in the
54
+ * account list. No separate create-ATA instruction is needed.
55
+ *
56
+ * PDAs are derived through the WASM module (this package's one rule: never
57
+ * hand-roll `find_program_address`'s on-curve check in TypeScript — see
58
+ * wasm.ts). The Token Metadata program id is a parameter everywhere because
59
+ * X1 runs its own deployment ({@link MPL_TOKEN_METADATA_PROGRAM}).
60
+ */
61
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
62
+ /** X1's Token Metadata deployment — the id `mint_handle_nft` CPIs into
63
+ * (lib.rs `TOKEN_METADATA_PROGRAM_ID`, `…x1s` suffix). NOT the canonical
64
+ * Solana-mainnet id; on another chain, pass that deployment's id instead. */
65
+ export const MPL_TOKEN_METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
66
+ /** `MetadataInstruction::Transfer`'s Borsh enum discriminator. */
67
+ export const TRANSFER_IX_DISCRIMINATOR = 49;
68
+ /** `TransferArgs::V1`'s Borsh enum discriminator. */
69
+ export const TRANSFER_ARGS_V1 = 0;
70
+ /** Metadata `token_standard` value for a pNFT
71
+ * (`TokenStandard::ProgrammableNonFungible`, mpl 5.1.1 enum order). */
72
+ export const TOKEN_STANDARD_PROGRAMMABLE_NON_FUNGIBLE = 4;
73
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
74
+ const SYSVAR_INSTRUCTIONS = "Sysvar1nstructions1111111111111111111111111";
75
+ const SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
76
+ const SPL_ATA_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
77
+ function toBytes32(v, what) {
78
+ if (typeof v === "string") {
79
+ const b = decodeBase58_32(v);
80
+ if (!b)
81
+ throw new Error(`${what} is not a valid base58 address`);
82
+ return b;
83
+ }
84
+ if (v.length !== 32)
85
+ throw new Error(`${what} must be exactly 32 bytes`);
86
+ return v;
87
+ }
88
+ function toBase58(v, what) {
89
+ return encodeBase58(toBytes32(v, what));
90
+ }
91
+ /**
92
+ * Derive the `TokenRecord` PDA for `(mint, token)` — `token` is the token
93
+ * ACCOUNT (the ATA), not its owner. Byte-for-byte
94
+ * `mpl_token_metadata::accounts::TokenRecord::find_pda`; seeds
95
+ * `["metadata", program, mint, "token_record", token]` under `programId`.
96
+ */
97
+ export function deriveTokenRecordPda(wasm, mint, token, programId = MPL_TOKEN_METADATA_PROGRAM) {
98
+ const out = wasm.deriveTokenRecordAccount(toBytes32(mint, "mint"), toBytes32(token, "token"), toBytes32(programId, "programId"));
99
+ if (!out)
100
+ throw new Error("token record derivation failed");
101
+ return encodeBase58(out);
102
+ }
103
+ /**
104
+ * Derive the full `TransferV1` account set for moving a handle NFT from
105
+ * `holder` to `recipient`. Pure derivation — nothing is fetched; whether the
106
+ * mint really is a pNFT is the caller's check (read the metadata
107
+ * `token_standard`, or the source ATA's frozen state).
108
+ */
109
+ export function deriveTransferV1Accounts(wasm, p) {
110
+ const mint = toBytes32(p.mint, "mint");
111
+ const holder = toBytes32(p.holder, "holder");
112
+ const recipient = toBytes32(p.recipient, "recipient");
113
+ const program = toBytes32(p.tokenMetadataProgramId ?? MPL_TOKEN_METADATA_PROGRAM, "tokenMetadataProgramId");
114
+ const sourceAta = wasm.deriveAssociatedTokenAccount(holder, mint);
115
+ const destAta = wasm.deriveAssociatedTokenAccount(recipient, mint);
116
+ const metadata = wasm.deriveMetadataAccount(mint, program);
117
+ const edition = wasm.deriveMasterEditionAccount(mint, program);
118
+ const sourceRecord = sourceAta && wasm.deriveTokenRecordAccount(mint, sourceAta, program);
119
+ const destRecord = destAta && wasm.deriveTokenRecordAccount(mint, destAta, program);
120
+ if (!sourceAta || !destAta || !metadata || !edition || !sourceRecord || !destRecord) {
121
+ throw new Error("TransferV1 account derivation failed");
122
+ }
123
+ return {
124
+ token: encodeBase58(sourceAta),
125
+ tokenOwner: encodeBase58(holder),
126
+ destinationToken: encodeBase58(destAta),
127
+ destinationOwner: encodeBase58(recipient),
128
+ mint: encodeBase58(mint),
129
+ metadata: encodeBase58(metadata),
130
+ edition: encodeBase58(edition),
131
+ tokenRecord: encodeBase58(sourceRecord),
132
+ destinationTokenRecord: encodeBase58(destRecord),
133
+ authority: encodeBase58(holder),
134
+ payer: encodeBase58(p.payer !== undefined ? toBytes32(p.payer, "payer") : holder),
135
+ tokenMetadataProgramId: encodeBase58(program),
136
+ };
137
+ }
138
+ /**
139
+ * Build `TransferV1`. See the module docs for the verified account order and
140
+ * data serialization. `authorization_data` is always serialized `None` —
141
+ * with `rule_set: None` there is nothing to authorize against.
142
+ */
143
+ export function buildTransferV1Ix(p) {
144
+ const amount = p.amount ?? 1n;
145
+ if (amount < 0n || amount > 0xffffffffffffffffn) {
146
+ throw new Error("amount must fit in a u64");
147
+ }
148
+ const hasRules = p.authorizationRulesProgram !== undefined || p.authorizationRules !== undefined;
149
+ if (hasRules && (p.authorizationRulesProgram === undefined || p.authorizationRules === undefined)) {
150
+ throw new Error("authorizationRulesProgram and authorizationRules must both be supplied, or neither");
151
+ }
152
+ const programId = toBase58(p.tokenMetadataProgramId ?? MPL_TOKEN_METADATA_PROGRAM, "tokenMetadataProgramId");
153
+ // [49, 0] ‖ amount u64 LE ‖ Option<AuthorizationData> = None (0).
154
+ const data = new Uint8Array(11);
155
+ data[0] = TRANSFER_IX_DISCRIMINATOR;
156
+ data[1] = TRANSFER_ARGS_V1;
157
+ new DataView(data.buffer).setBigUint64(2, amount, true);
158
+ data[10] = 0;
159
+ const keys = [
160
+ { pubkey: toBase58(p.token, "token"), isSigner: false, isWritable: true },
161
+ { pubkey: toBase58(p.tokenOwner, "tokenOwner"), isSigner: false, isWritable: false },
162
+ { pubkey: toBase58(p.destinationToken, "destinationToken"), isSigner: false, isWritable: true },
163
+ { pubkey: toBase58(p.destinationOwner, "destinationOwner"), isSigner: false, isWritable: false },
164
+ { pubkey: toBase58(p.mint, "mint"), isSigner: false, isWritable: false },
165
+ { pubkey: toBase58(p.metadata, "metadata"), isSigner: false, isWritable: true },
166
+ { pubkey: toBase58(p.edition, "edition"), isSigner: false, isWritable: false },
167
+ { pubkey: toBase58(p.tokenRecord, "tokenRecord"), isSigner: false, isWritable: true },
168
+ { pubkey: toBase58(p.destinationTokenRecord, "destinationTokenRecord"), isSigner: false, isWritable: true },
169
+ { pubkey: toBase58(p.authority, "authority"), isSigner: true, isWritable: false },
170
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
171
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
172
+ { pubkey: SYSVAR_INSTRUCTIONS, isSigner: false, isWritable: false },
173
+ { pubkey: SPL_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
174
+ { pubkey: SPL_ATA_PROGRAM_ID, isSigner: false, isWritable: false },
175
+ hasRules
176
+ ? { pubkey: toBase58(p.authorizationRulesProgram, "authorizationRulesProgram"), isSigner: false, isWritable: false }
177
+ : { pubkey: programId, isSigner: false, isWritable: false },
178
+ hasRules
179
+ ? { pubkey: toBase58(p.authorizationRules, "authorizationRules"), isSigner: false, isWritable: false }
180
+ : { pubkey: programId, isSigner: false, isWritable: false },
181
+ ];
182
+ return { programId, keys, data };
183
+ }
package/dist/types.d.ts CHANGED
@@ -11,6 +11,43 @@ export type Verification =
11
11
  "verified"
12
12
  /** Record exists but ownership was never proved. Show a warning. */
13
13
  | "unverified";
14
+ /**
15
+ * One per-platform verification a handle carries — the "verified by
16
+ * [platform]" signal (#7145). Shape mirrors the resolver's REST `/resolve`
17
+ * `verifications[]` entries exactly (snake_case `attested_at`), so an
18
+ * integrator can drop a decoded REST array straight onto
19
+ * {@link Resolved.verifications} with no transformation.
20
+ *
21
+ * # What this proves — and, honestly, what it does not
22
+ *
23
+ * A verification is an off-chain review (a DNS lookup, an OAuth completion, a
24
+ * human check) that x1id's **attestor key** signed off on at `attested_at`.
25
+ * It is NOT a trustless proof: the attestor key is admin-rotatable, so the
26
+ * trust anchor is "whoever the registry admin currently designates". Entries
27
+ * are also epoch-bound — the resolver emits an entry only while the underlying
28
+ * attestation is LIVE (not stale): `attested_at >= handle.registered_at` AND
29
+ * `>= handle.records_cleared_at`. Render it as "verified by x1id", never as
30
+ * more than that.
31
+ */
32
+ export interface HandleVerification {
33
+ /**
34
+ * Platform slug: `"x"`, `"discord"`, `"github"`, `"telegram"`, `"domain"`
35
+ * (legacy DNS/kind 0), or `"social"` (legacy generic/kind 1). Match this
36
+ * against {@link platformSlug}'s output when joining to an attestation kind.
37
+ */
38
+ readonly platform: string;
39
+ /**
40
+ * The paired text-record value for a live verification (e.g. `"@michelle"`
41
+ * for `x`, `"michelle.com"` for `domain`), or `null` when the text record
42
+ * is missing/stale or the platform carries none (legacy `social`).
43
+ */
44
+ readonly value: string | null;
45
+ /** Whether this verification is live. The resolver only emits live ones,
46
+ * but readers must still honour the flag. */
47
+ readonly verified: boolean;
48
+ /** Unix seconds the attestation was (last) stamped. */
49
+ readonly attested_at: number;
50
+ }
14
51
  /**
15
52
  * A successfully resolved name.
16
53
  *
@@ -31,6 +68,17 @@ export interface Resolved {
31
68
  readonly chain: Chain;
32
69
  /** Whether ownership of `address` was proved. */
33
70
  readonly verification: Verification;
71
+ /**
72
+ * Per-platform "verified by [platform]" signals for this handle (#7145),
73
+ * when the producing resolver supplies them (the tools/api REST `/resolve`
74
+ * response carries a `verifications[]` field; the SDK's own on-chain
75
+ * `resolve()` does not populate this — read attestations directly via
76
+ * {@link fetchAttestations}). **Optional and additive**: an older resolver,
77
+ * or a handle with no live verifications, omits it. Never index into it
78
+ * blindly — use {@link isVerified} / {@link verifiedPlatforms} /
79
+ * {@link isVerifiedOn}, which tolerate its absence.
80
+ */
81
+ readonly verifications?: readonly HandleVerification[];
34
82
  }
35
83
  /** Chains a handle can carry a record for. SLIP-44 based. */
36
84
  export type Chain = "X1" | "SOL" | "ETH" | "BTC";
package/dist/wasm.d.ts CHANGED
@@ -54,5 +54,29 @@ export declare class WasmResolver {
54
54
  * holds a tokenized handle's NFT.
55
55
  */
56
56
  deriveAssociatedTokenAccount(owner: Uint8Array, mint: Uint8Array): Uint8Array | null;
57
+ /**
58
+ * 32-byte Metaplex Token Metadata PDA for a 32-byte mint under a given
59
+ * Token Metadata program id — seeds `["metadata", program, mint]`.
60
+ *
61
+ * The program id is a parameter (staged, like `deriveHandleAccount`'s)
62
+ * because X1 runs its own Token Metadata deployment
63
+ * (`MPL_TOKEN_METADATA_PROGRAM` in pnftTransfer.ts) — the canonical
64
+ * Solana-mainnet id would derive addresses no X1 account lives at.
65
+ */
66
+ deriveMetadataAccount(mint: Uint8Array, programId: Uint8Array): Uint8Array | null;
67
+ /**
68
+ * 32-byte Metaplex Master Edition PDA for a 32-byte mint under a given
69
+ * Token Metadata program id — seeds `["metadata", program, mint,
70
+ * "edition"]`. Same program-id rationale as `deriveMetadataAccount`.
71
+ */
72
+ deriveMasterEditionAccount(mint: Uint8Array, programId: Uint8Array): Uint8Array | null;
73
+ /**
74
+ * 32-byte Metaplex `TokenRecord` PDA for a 32-byte mint and a 32-byte token
75
+ * ACCOUNT (the ATA — not its owner) under a given Token Metadata program id
76
+ * — seeds `["metadata", program, mint, "token_record", token]`,
77
+ * byte-for-byte `mpl_token_metadata::accounts::TokenRecord::find_pda`.
78
+ * Needed on BOTH sides of a `ProgrammableNonFungible` transfer.
79
+ */
80
+ deriveTokenRecordAccount(mint: Uint8Array, token: Uint8Array, programId: Uint8Array): Uint8Array | null;
57
81
  }
58
82
  export {};
package/dist/wasm.js CHANGED
@@ -101,4 +101,49 @@ export class WasmResolver {
101
101
  const n = this.#write(joined);
102
102
  return this.#x.derive_associated_token_account(n) === 1 ? this.#read() : null;
103
103
  }
104
+ /**
105
+ * 32-byte Metaplex Token Metadata PDA for a 32-byte mint under a given
106
+ * Token Metadata program id — seeds `["metadata", program, mint]`.
107
+ *
108
+ * The program id is a parameter (staged, like `deriveHandleAccount`'s)
109
+ * because X1 runs its own Token Metadata deployment
110
+ * (`MPL_TOKEN_METADATA_PROGRAM` in pnftTransfer.ts) — the canonical
111
+ * Solana-mainnet id would derive addresses no X1 account lives at.
112
+ */
113
+ deriveMetadataAccount(mint, programId) {
114
+ if (mint.length !== 32 || programId.length !== 32)
115
+ return null;
116
+ new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
117
+ const n = this.#write(mint);
118
+ return this.#x.derive_metadata_account(n) === 1 ? this.#read() : null;
119
+ }
120
+ /**
121
+ * 32-byte Metaplex Master Edition PDA for a 32-byte mint under a given
122
+ * Token Metadata program id — seeds `["metadata", program, mint,
123
+ * "edition"]`. Same program-id rationale as `deriveMetadataAccount`.
124
+ */
125
+ deriveMasterEditionAccount(mint, programId) {
126
+ if (mint.length !== 32 || programId.length !== 32)
127
+ return null;
128
+ new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
129
+ const n = this.#write(mint);
130
+ return this.#x.derive_master_edition_account(n) === 1 ? this.#read() : null;
131
+ }
132
+ /**
133
+ * 32-byte Metaplex `TokenRecord` PDA for a 32-byte mint and a 32-byte token
134
+ * ACCOUNT (the ATA — not its owner) under a given Token Metadata program id
135
+ * — seeds `["metadata", program, mint, "token_record", token]`,
136
+ * byte-for-byte `mpl_token_metadata::accounts::TokenRecord::find_pda`.
137
+ * Needed on BOTH sides of a `ProgrammableNonFungible` transfer.
138
+ */
139
+ deriveTokenRecordAccount(mint, token, programId) {
140
+ if (mint.length !== 32 || token.length !== 32 || programId.length !== 32)
141
+ return null;
142
+ new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
143
+ const joined = new Uint8Array(64);
144
+ joined.set(mint, 0);
145
+ joined.set(token, 32);
146
+ const n = this.#write(joined);
147
+ return this.#x.derive_token_record_account(n) === 1 ? this.#read() : null;
148
+ }
104
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x1id/resolve",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Resolve @handles and X1NS names on X1. Never silently picks between namespaces.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
Binary file