@x1id/resolve 0.1.0 → 0.2.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
@@ -12,7 +12,7 @@ answered, and a mixed shape is refused rather than guessed.
12
12
  import { createResolver, WasmResolver } from "@x1id/resolve";
13
13
 
14
14
  const wasm = await WasmResolver.fromBytes(/* the module bytes, see below */);
15
- const x1id = createResolver({ rpcUrl: "https://rpc.mainnet.x1.xyz", wasm });
15
+ const x1id = createResolver({ rpcUrl: "https://rpc.testnet.x1.xyz", wasm });
16
16
 
17
17
  const r = await x1id.resolve("@jack");
18
18
  // { name: "jack", namespace: "handle", address: "H8Fs…", chain: "X1",
@@ -20,28 +20,28 @@ const r = await x1id.resolve("@jack");
20
20
 
21
21
  await x1id.resolve("jack.x1"); // resolves the X1NS domain instead
22
22
  await x1id.resolve("@jack.x1"); // throws ResolveError { code: "ambiguous" }
23
+ await x1id.reverse("H8Fs…"); // "nike" — the address's primary @handle, or null
23
24
  ```
24
25
 
25
- ## Why a resolver SDK at all
26
+ > **Which RPC?** The `@handle` registry is deployed on **X1 testnet** today
27
+ > (`https://rpc.testnet.x1.xyz`). X1NS domains live on **mainnet**
28
+ > (`https://rpc.mainnet.x1.xyz`). Point the resolver at the chain that holds
29
+ > the names you want; on an RPC where the registry is not deployed a handle is
30
+ > an honest `not-found`, never a fabricated address.
26
31
 
27
- X1NS shipped a great naming service — but with resolution behind their own API.
28
- A wallet that resolves through a third party's uptime **breaks the day that
29
- party loses interest**, and every pending payment breaks with it. This SDK never
30
- calls `api.x1ns.xyz` or any of ours: it **derives accounts and reads them
31
- straight off chain over RPC**. Point it at any X1 RPC and it works, ours up or
32
- not.
32
+ ## Where this code lives
33
33
 
34
- Address derivation uses a small Rust→WASM module (a real ed25519 on-curve check
35
- — the kind of thing that is subtly wrong for months when hand-rolled in JS, and
36
- this is the kind of code that loses money when it is wrong). It is verified
37
- against live mainnet accounts.
34
+ This directory (`sdk/` in the `x1-handles` monorepo) **is the source of
35
+ truth**, published to npm as **`@x1id/resolve`** (MIT). The X1ID app vendors a
36
+ byte-identical copy at `app/src/lib/x1/x1sdk/` (plus the WASM at
37
+ `app/public/x1_resolve_wasm.wasm`) — that copy is a **duplicate, not a
38
+ source**: any change here must be mirrored into `app/src/lib/x1/x1sdk`
39
+ (`test/sync.test.js` fails inside the monorepo when they drift).
38
40
 
39
41
  ## Install
40
42
 
41
43
  ```bash
42
44
  npm install @x1id/resolve
43
- # or straight from source:
44
- npm install github:fortiblox/x1id-sdk
45
45
  ```
46
46
 
47
47
  `@solana/web3.js` is an **optional** peer dependency — you only need it if you
@@ -50,9 +50,10 @@ pass web3.js types around; the SDK itself talks to RPC directly.
50
50
  ## Loading the WASM module
51
51
 
52
52
  Derivation needs the WASM module. The package ships it at
53
- `@x1id/resolve/wasm/x1_resolve_wasm.wasm`.
53
+ `@x1id/resolve/wasm/x1_resolve_wasm.wasm` (an exported subpath, so both
54
+ `require.resolve` and bundler asset imports find it).
54
55
 
55
- **Node**
56
+ **Node (ESM)**
56
57
 
57
58
  ```ts
58
59
  import { readFileSync } from "node:fs";
@@ -63,16 +64,30 @@ const require = createRequire(import.meta.url);
63
64
  const wasmPath = require.resolve("@x1id/resolve/wasm/x1_resolve_wasm.wasm");
64
65
  const wasm = await WasmResolver.fromBytes(readFileSync(wasmPath));
65
66
 
66
- const x1id = createResolver({ rpcUrl: "https://rpc.mainnet.x1.xyz", wasm });
67
+ const x1id = createResolver({ rpcUrl: "https://rpc.testnet.x1.xyz", wasm });
68
+ ```
69
+
70
+ Node ≥ 20.6 can use `import.meta.resolve` instead of `createRequire`:
71
+
72
+ ```ts
73
+ import { fileURLToPath } from "node:url";
74
+ const wasmPath = fileURLToPath(import.meta.resolve("@x1id/resolve/wasm/x1_resolve_wasm.wasm"));
67
75
  ```
68
76
 
69
77
  **Browser / bundler**
70
78
 
71
79
  ```ts
72
- import wasmUrl from "@x1id/resolve/wasm/x1_resolve_wasm.wasm?url"; // Vite
80
+ // Vite / Rollup / webpack 5 — the `?url` suffix hands you the asset URL
81
+ import wasmUrl from "@x1id/resolve/wasm/x1_resolve_wasm.wasm?url";
82
+ import { WasmResolver } from "@x1id/resolve";
83
+
73
84
  const wasm = await WasmResolver.fromBytes(await (await fetch(wasmUrl)).arrayBuffer());
74
85
  ```
75
86
 
87
+ Or copy `node_modules/@x1id/resolve/wasm/x1_resolve_wasm.wasm` into your static
88
+ directory and `fetch("/x1_resolve_wasm.wasm")` it — that is what
89
+ [x1id.io](https://x1id.io) does.
90
+
76
91
  ## The one rule: show the namespace before you send
77
92
 
78
93
  `@jack` and `jack.x1` can resolve to **different owners**. There is no API in
@@ -96,15 +111,50 @@ try {
96
111
  }
97
112
  ```
98
113
 
99
- See [`examples/recipient-field.ts`](examples/recipient-field.ts) for a fuller
100
- wallet integration.
114
+ Dispatch is on shape (`@` prefix vs known TLD suffix), never "try one, fall back
115
+ to the other". `@jack.x1` throws `ambiguous` rather than guessing. Use
116
+ `looksLikeName(input)` to decide whether to attempt resolution at all, so
117
+ pasting base58 does not surface a validation error.
118
+
119
+ ## Reads chain state, not an API
120
+
121
+ X1NS names resolve by deriving accounts and reading them over RPC. This library
122
+ never calls `api.x1ns.xyz` or any API of ours — a wallet that resolves through
123
+ a third party's uptime breaks when they lose interest. `@handle` resolution
124
+ derives the PDA (`["handle", canonical]` under `handleProgramId`) and reads the
125
+ owner directly; reverse lookup reads the registry's on-chain `Primary` pointer
126
+ (`["primary", owner]`). All reads use `confirmed` commitment.
127
+
128
+ Address derivation uses a small Rust→WASM module (a real ed25519 on-curve check
129
+ — the kind of thing that is subtly wrong for months when hand-rolled in JS, and
130
+ this is the kind of code that loses money when it is wrong). It is verified
131
+ against live accounts in `test/wasm.test.js`.
101
132
 
102
133
  ## Reverse lookup
103
134
 
104
135
  ```ts
105
- const name = await x1id.reverse("H8Fs…"); // primary name for an address, or null
136
+ const name = await x1id.reverse("H8Fs…");
137
+ // "nike" → the address's primary @handle (canonical, no "@")
138
+ // "5WL7…dMKK.x1" → no @handle primary; X1NS primary-domain record
139
+ // (account + TLD — X1NS stores no label on chain)
140
+ // null → no primary anywhere
106
141
  ```
107
142
 
143
+ A `@handle` primary is only returned when it is still trustworthy — the same
144
+ rule the X1ID API and app apply:
145
+
146
+ 1. the `Primary` pointer and the handle it names both exist and are owned by
147
+ the registry program;
148
+ 2. `set_at >= handle.registered_at` — a pointer set before the name's current
149
+ registration belongs to a previous owner of that name;
150
+ 3. the address still holds authority: an untokenized handle's `owner` is the
151
+ address, or — when the handle is tokenized — the address's associated token
152
+ account holds exactly 1 of the handle's NFT.
153
+
154
+ A pointer that fails any rule is treated as absent (falls through to X1NS, then
155
+ `null`), never as an error. Only a malformed *address* throws
156
+ (`ResolveError { code: "unrecognized" }`).
157
+
108
158
  ## Multi-chain
109
159
 
110
160
  A handle can carry addresses for several chains. Pass the one you want:
@@ -132,14 +182,39 @@ a wrong address on the wrong chain.
132
182
  | | Status |
133
183
  |---|---|
134
184
  | X1NS resolution (`.x1/.xnt/.xen`) | ✅ mainnet |
135
- | `@handle` resolution | ✅ on any RPC where the registry is deployed (testnet today; mainnet on deploy) |
136
- | Reverse lookup | ✅ |
185
+ | `@handle` resolution | ✅ on any RPC where the registry is deployed — **X1 testnet today**; mainnet on deploy |
186
+ | Reverse lookup | ✅ on-chain `@handle` primary (testnet today) with X1NS primary-domain fallback (mainnet); X1NS results are `<domainAccount>.<tld>`, not a label |
137
187
  | Per-chain ETH/BTC records | 🔜 roadmap |
138
- | Registration / transfer (write) | 🔜 roadmap |
188
+ | Registration / transfer / `set_primary` (write) | 🔜 roadmap — use [x1id.io](https://x1id.io) |
139
189
 
140
190
  The `@handle` registry program id is configurable (`handleProgramId`) and
141
- defaults to the canonical X1 deployment. On an RPC where it is not deployed you
142
- get an honest `not-found`, never a fabricated address.
191
+ defaults to the canonical X1 deployment
192
+ (`8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P`).
193
+
194
+ ## Errors
195
+
196
+ `ResolveError` carries a `code` a UI can branch on:
197
+
198
+ | code | meaning |
199
+ |---|---|
200
+ | `unrecognized` | not a handle or known domain — probably a raw address |
201
+ | `ambiguous` | both shapes at once, e.g. `@jack.x1` |
202
+ | `invalid-handle` / `invalid-domain` | right shape, invalid content |
203
+ | `not-found` | valid name, not registered |
204
+ | `no-record-for-chain` | registered, but no address for the requested chain |
205
+ | `rpc-error` | transport failure |
206
+
207
+ ## Normalization
208
+
209
+ `normalizeHandle` mirrors the Rust `handle-normalize` crate, which is the source
210
+ of truth — the on-chain registry derives PDA seeds from it. A conformance suite
211
+ runs both against generated fixtures in CI; divergence fails the build, because
212
+ a divergence would make handles registered under one normalization unreachable
213
+ under the other.
214
+
215
+ ASCII only, no Unicode or emoji. X1NS permits both; for a payment identifier
216
+ that is a homograph vector (`@аlice` with a Cyrillic `а` renders identically to
217
+ `@alice`).
143
218
 
144
219
  ## API
145
220
 
@@ -147,10 +222,10 @@ get an honest `not-found`, never a fabricated address.
147
222
  createResolver(config: ResolverConfig): Resolver
148
223
 
149
224
  interface ResolverConfig {
150
- rpcUrl: string; // any X1 RPC
225
+ rpcUrl: string; // any X1 RPC (testnet for @handles today)
151
226
  wasm: WasmResolver; // required — derivation runs in WASM, no JS fallback
152
227
  fetchImpl?: typeof fetch; // custom transport (tests, proxies)
153
- cacheTtlMs?: number; // default 30_000; 0 disables
228
+ cacheTtlMs?: number; // default 30_000; 0 disables (resolve() only)
154
229
  handleProgramId?: string; // default: canonical X1 deployment
155
230
  }
156
231
 
@@ -159,15 +234,46 @@ interface Resolver {
159
234
  reverse(address: string): Promise<string | null>;
160
235
  clearCache(): void;
161
236
  }
237
+
238
+ class WasmResolver {
239
+ static fromBytes(bytes: BufferSource): Promise<WasmResolver>;
240
+ normalizeHandle(raw: string): string | null;
241
+ deriveX1nsAccount(label: string, tld: "x1" | "xnt" | "xen"): Uint8Array | null;
242
+ derivePrimaryAccount(owner: Uint8Array): Uint8Array | null; // X1NS primary-domain record
243
+ deriveHandleAccount(canonical: string, programId: Uint8Array): Uint8Array | null; // ["handle", name]
244
+ deriveHandlePrimaryAccount(owner: Uint8Array, programId: Uint8Array): Uint8Array | null; // ["primary", owner]
245
+ deriveAssociatedTokenAccount(owner: Uint8Array, mint: Uint8Array): Uint8Array | null;
246
+ }
247
+ ```
248
+
249
+ Also exported: `parseName`, `looksLikeName`, `normalizeHandle`, `namespaceLabel`,
250
+ `encodeBase58`, `decodeBase58_32`, `ResolveError`, `CHAIN_COIN_TYPE` and the
251
+ types `Resolved`, `Namespace`, `Chain`, `Verification`, `ResolveErrorCode`.
252
+
253
+ ## Developing
254
+
255
+ ```bash
256
+ npm run build:wasm # cargo build → wasm/x1_resolve_wasm.wasm (needs the wasm32-unknown-unknown target)
257
+ npm run build # tsc → dist/
258
+ npm test # node --test; X1_LIVE=1 also runs the testnet reverse() case
162
259
  ```
163
260
 
164
- `ResolveError.code` is one of: `unrecognized`, `ambiguous`, `invalid-handle`,
165
- `invalid-domain`, `not-found`, `no-record-for-chain`, `rpc-error`.
261
+ `wasm/x1_resolve_wasm.wasm` is committed and shipped. Rebuild it whenever
262
+ `crates/x1-resolve-wasm` (or its dependencies) change; the release profile is
263
+ stripped, so a rebuild from the same source is byte-identical.
264
+
265
+ ## Publishing
266
+
267
+ Published from the (private) release mirror, not from the monorepo: sync this
268
+ directory (plus `crates/`) there, then push a `v<version>` tag that matches
269
+ `package.json` — the release workflow builds the WASM, runs the tests and
270
+ `publint`, and runs `npm publish --provenance`.
166
271
 
167
272
  ## Links
168
273
 
169
274
  - Home & docs: **[x1id.io](https://x1id.io)**
170
- - Issues: <https://github.com/fortiblox/x1id-sdk/issues>
275
+ - Developer docs: **[docs.fortiblox.com/docs/x1id](https://docs.fortiblox.com/docs/x1id)**
276
+ - Questions & issues: [x1id.io](https://x1id.io) — contact links in the footer
171
277
 
172
278
  ## License
173
279
 
package/dist/index.js CHANGED
@@ -46,6 +46,69 @@ const DEFAULT_HANDLE_PROGRAM = "8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P";
46
46
  // The owner is the address the handle resolves to on X1.
47
47
  const HANDLE_OWNER_OFFSET = 41;
48
48
  const HANDLE_MIN_LEN = HANDLE_OWNER_OFFSET + 32;
49
+ // `Handle`'s fixed base allocation (`space = 8 + INIT_SPACE`). The NFT
50
+ // extension, when present, is appended at this boundary regardless of the
51
+ // compact Borsh length of the (variable, `Option`-bearing) struct content —
52
+ // mirrors `Handle::NFT_EXT_OFFSET` in the program and `HANDLE_BASE_LEN` in
53
+ // tools/api.
54
+ const HANDLE_BASE_LEN = 8 + 149;
55
+ // `Primary` pointer account (`["primary", owner]` under the registry):
56
+ // disc(8) | owner(32) | handle(32) | set_at(i64 LE, 8) | bump(1) = 81 bytes
57
+ const PRIMARY_LEN = 81;
58
+ const PRIMARY_HANDLE_OFFSET = 40;
59
+ const PRIMARY_SET_AT_OFFSET = 72;
60
+ // SPL Token account: mint(32) | owner(32) | amount(u64 LE, 8) | ...
61
+ const TOKEN_ACCOUNT_MIN_LEN = 72;
62
+ function readI64(data, offset) {
63
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getBigInt64(offset, true);
64
+ }
65
+ function readU64(data, offset) {
66
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getBigUint64(offset, true);
67
+ }
68
+ function bytesEqual(a, b) {
69
+ if (a.length !== b.length)
70
+ return false;
71
+ for (let i = 0; i < a.length; i++)
72
+ if (a[i] !== b[i])
73
+ return false;
74
+ return true;
75
+ }
76
+ /**
77
+ * Parse the `Handle` fields the reverse rule needs. Port of `parse_handle` in
78
+ * tools/api — sequential, because `recovery` / `recovery_target` are
79
+ * `Option<Pubkey>` (Borsh: tag byte, then 32 bytes when `Some`) and shift
80
+ * `registered_at`. The NFT mint is read at the fixed base boundary, not the
81
+ * sequential position.
82
+ *
83
+ * disc(8) name(32) name_len(1) owner(32) handle_type(1)
84
+ * recovery: Option<Pubkey> recovery_initiated_at: i64
85
+ * recovery_target: Option<Pubkey> registered_at: i64 bump(1)
86
+ * [at 8+149: nft tag(1) mint(32)]
87
+ */
88
+ function parseHandleAccount(data) {
89
+ if (data.length < HANDLE_BASE_LEN)
90
+ return null;
91
+ const nameLen = Math.min(data[40], 32);
92
+ const name = new TextDecoder().decode(data.slice(8, 8 + nameLen));
93
+ const owner = data.slice(HANDLE_OWNER_OFFSET, HANDLE_OWNER_OFFSET + 32);
94
+ let pos = 73 + 1; // owner end + handle_type(1)
95
+ // recovery: Option<Pubkey>
96
+ if (pos >= data.length)
97
+ return null;
98
+ pos += 1 + (data[pos] === 1 ? 32 : 0);
99
+ pos += 8; // recovery_initiated_at
100
+ // recovery_target: Option<Pubkey>
101
+ if (pos >= data.length)
102
+ return null;
103
+ pos += 1 + (data[pos] === 1 ? 32 : 0);
104
+ if (pos + 8 > data.length)
105
+ return null;
106
+ const registeredAt = readI64(data, pos);
107
+ const nftMint = data.length >= HANDLE_BASE_LEN + 33 && data[HANDLE_BASE_LEN] === 1
108
+ ? data.slice(HANDLE_BASE_LEN + 1, HANDLE_BASE_LEN + 33)
109
+ : null;
110
+ return { name, owner, registeredAt, nftMint };
111
+ }
49
112
  export function createResolver(config) {
50
113
  const ttl = config.cacheTtlMs ?? 30_000;
51
114
  const cache = new Map();
@@ -60,6 +123,9 @@ export function createResolver(config) {
60
123
  // Explicit type so the non-null narrowing survives into the resolveHandle
61
124
  // closure below — TS widens a captured `const` back to its declared type.
62
125
  const handleProgram = decodedProgram;
126
+ // Re-encoded (not the caller's string) so a non-canonical base58 spelling of
127
+ // the same key still compares equal to the RPC's `owner` field.
128
+ const programBase58 = encodeBase58(handleProgram);
63
129
  async function rpc(method, params) {
64
130
  let res;
65
131
  try {
@@ -81,11 +147,12 @@ export function createResolver(config) {
81
147
  }
82
148
  return body.result;
83
149
  }
84
- /** Fetch raw account data, or null when the account does not exist. */
85
- async function accountData(address) {
150
+ /** Fetch raw account data plus the owning program, or null when the account
151
+ * does not exist. */
152
+ async function accountInfo(address) {
86
153
  const result = (await rpc("getAccountInfo", [
87
154
  address,
88
- { encoding: "base64" },
155
+ { encoding: "base64", commitment: "confirmed" },
89
156
  ]));
90
157
  const value = result?.value;
91
158
  if (!value)
@@ -95,7 +162,11 @@ export function createResolver(config) {
95
162
  const out = new Uint8Array(bin.length);
96
163
  for (let i = 0; i < bin.length; i++)
97
164
  out[i] = bin.charCodeAt(i);
98
- return out;
165
+ return { data: out, owner: value.owner };
166
+ }
167
+ /** Fetch raw account data, or null when the account does not exist. */
168
+ async function accountData(address) {
169
+ return (await accountInfo(address))?.data ?? null;
99
170
  }
100
171
  async function resolveX1ns(canonical, label, tld, chain, input) {
101
172
  const account = config.wasm.deriveX1nsAccount(label, tld);
@@ -183,12 +254,77 @@ export function createResolver(config) {
183
254
  cache.set(key, { value, expires: Date.now() + ttl });
184
255
  return value;
185
256
  }
186
- /** Address to its primary name, or null when none is set. */
257
+ /**
258
+ * Reverse resolution through the `@handle` registry's on-chain `Primary`
259
+ * pointer. Returns the canonical handle name (no `@`), or null when the
260
+ * address has no pointer or the pointer is no longer trustworthy.
261
+ *
262
+ * The read-side rule — identical to `tools/api` `fn reverse`, which is the
263
+ * reference — trusts a pointer only if all three hold:
264
+ * 1. the handle account it names exists and is owned by the registry;
265
+ * 2. `set_at >= handle.registered_at` — a pointer set before the handle's
266
+ * current registration belongs to a previous owner of that name;
267
+ * 3. the address still holds authority: untokenized → `handle.owner ==
268
+ * address`; tokenized → ATA(address, mint) exists, its mint/owner match
269
+ * and its amount is exactly 1.
270
+ * A pointer that fails any rule is treated as absent, never as an error: it
271
+ * is a stale artefact the owner can `clear_primary`, not a malformed chain.
272
+ */
273
+ async function reverseHandle(owner) {
274
+ const pointer = config.wasm.deriveHandlePrimaryAccount(owner, handleProgram);
275
+ if (!pointer)
276
+ return null;
277
+ const p = await accountInfo(encodeBase58(pointer));
278
+ if (!p || p.data.length < PRIMARY_LEN || p.owner !== programBase58)
279
+ return null;
280
+ const handleKey = encodeBase58(p.data.slice(PRIMARY_HANDLE_OFFSET, PRIMARY_HANDLE_OFFSET + 32));
281
+ const setAt = readI64(p.data, PRIMARY_SET_AT_OFFSET);
282
+ // rule 1: the handle is live and a registry account.
283
+ const h = await accountInfo(handleKey);
284
+ if (!h || h.owner !== programBase58)
285
+ return null;
286
+ const handle = parseHandleAccount(h.data);
287
+ if (!handle)
288
+ return null;
289
+ // rule 2: staleness — the pointer must be at least as new as the handle.
290
+ if (setAt < handle.registeredAt)
291
+ return null;
292
+ // rule 3: authority still matches.
293
+ if (handle.nftMint === null) {
294
+ if (!bytesEqual(handle.owner, owner))
295
+ return null;
296
+ }
297
+ else {
298
+ const ata = config.wasm.deriveAssociatedTokenAccount(owner, handle.nftMint);
299
+ if (!ata)
300
+ return null;
301
+ const t = await accountInfo(encodeBase58(ata));
302
+ if (!t || t.data.length < TOKEN_ACCOUNT_MIN_LEN)
303
+ return null;
304
+ const mintOk = bytesEqual(t.data.slice(0, 32), handle.nftMint);
305
+ const ownerOk = bytesEqual(t.data.slice(32, 64), owner);
306
+ const amount = readU64(t.data, 64);
307
+ if (!mintOk || !ownerOk || amount !== 1n)
308
+ return null;
309
+ }
310
+ return handle.name;
311
+ }
312
+ /**
313
+ * Address to its primary name, or null when none is set.
314
+ *
315
+ * Two sources, in order: the `@handle` registry's on-chain `Primary` pointer
316
+ * (returned as the plain canonical handle, e.g. `"nike"`), then — only when
317
+ * there is no valid handle primary — X1NS's primary-domain record, returned
318
+ * as `"<domainAccount>.<tld>"` exactly as before. The handle path never
319
+ * throws for a stale or dangling pointer; it falls through to X1NS instead.
320
+ */
187
321
  async function reverse(address) {
188
- const { decodeBase58_32 } = await import("./base58.js");
189
322
  const owner = decodeBase58_32(address);
190
323
  if (!owner)
191
324
  throw new ResolveError("unrecognized", `"${address}" is not an address`, address);
325
+ const handleName = await reverseHandle(owner);
326
+ if (handleName !== null)
327
+ return handleName;
192
328
  const record = config.wasm.derivePrimaryAccount(owner);
193
329
  if (!record)
194
330
  throw new ResolveError("unrecognized", "could not derive record", address);
package/dist/wasm.d.ts CHANGED
@@ -39,5 +39,20 @@ export declare class WasmResolver {
39
39
  * program the caller names — never a program baked into this module.
40
40
  */
41
41
  deriveHandleAccount(canonical: string, programId: Uint8Array): Uint8Array | null;
42
+ /**
43
+ * 32-byte `@handle` registry `Primary` pointer account for a 32-byte owner
44
+ * address under a given registry program id — seeds `["primary", owner]`.
45
+ * This is the on-chain `set_primary` / `clear_primary` PDA, NOT X1NS's
46
+ * primary-domain record (`derivePrimaryAccount`), which lives under the
47
+ * X1NS registrar and has a different layout.
48
+ */
49
+ deriveHandlePrimaryAccount(owner: Uint8Array, programId: Uint8Array): Uint8Array | null;
50
+ /**
51
+ * 32-byte SPL associated token account for `(owner, mint)` under the
52
+ * canonical SPL Token + Associated Token programs (fixed inside the module —
53
+ * they are the same on every SVM chain). Used to check that an address still
54
+ * holds a tokenized handle's NFT.
55
+ */
56
+ deriveAssociatedTokenAccount(owner: Uint8Array, mint: Uint8Array): Uint8Array | null;
42
57
  }
43
58
  export {};
package/dist/wasm.js CHANGED
@@ -72,4 +72,33 @@ export class WasmResolver {
72
72
  const n = this.#write(this.#enc.encode(canonical));
73
73
  return this.#x.derive_handle_account_staged(n) === 1 ? this.#read() : null;
74
74
  }
75
+ /**
76
+ * 32-byte `@handle` registry `Primary` pointer account for a 32-byte owner
77
+ * address under a given registry program id — seeds `["primary", owner]`.
78
+ * This is the on-chain `set_primary` / `clear_primary` PDA, NOT X1NS's
79
+ * primary-domain record (`derivePrimaryAccount`), which lives under the
80
+ * X1NS registrar and has a different layout.
81
+ */
82
+ deriveHandlePrimaryAccount(owner, programId) {
83
+ if (owner.length !== 32 || programId.length !== 32)
84
+ return null;
85
+ new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
86
+ const n = this.#write(owner);
87
+ return this.#x.derive_handle_primary_account_staged(n) === 1 ? this.#read() : null;
88
+ }
89
+ /**
90
+ * 32-byte SPL associated token account for `(owner, mint)` under the
91
+ * canonical SPL Token + Associated Token programs (fixed inside the module —
92
+ * they are the same on every SVM chain). Used to check that an address still
93
+ * holds a tokenized handle's NFT.
94
+ */
95
+ deriveAssociatedTokenAccount(owner, mint) {
96
+ if (owner.length !== 32 || mint.length !== 32)
97
+ return null;
98
+ const joined = new Uint8Array(64);
99
+ joined.set(owner, 0);
100
+ joined.set(mint, 32);
101
+ const n = this.#write(joined);
102
+ return this.#x.derive_associated_token_account(n) === 1 ? this.#read() : null;
103
+ }
75
104
  }
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "@x1id/resolve",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
7
7
  "types": "./dist/index.d.ts",
8
- "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
9
- "files": ["dist", "wasm"],
8
+ "exports": {
9
+ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
10
+ "./wasm/x1_resolve_wasm.wasm": "./wasm/x1_resolve_wasm.wasm",
11
+ "./wasm/*": "./wasm/*",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": ["dist", "wasm", "README.md"],
10
15
  "scripts": {
11
16
  "build:wasm": "cargo build --release -p x1-resolve-wasm --target wasm32-unknown-unknown && cp target/wasm32-unknown-unknown/release/x1_resolve_wasm.wasm wasm/",
12
17
  "build": "tsc -p tsconfig.json",
@@ -16,10 +21,8 @@
16
21
  },
17
22
  "keywords": ["x1", "x1id", "solana", "svm", "naming", "x1ns", "handles", "wallet", "resolver", "web3"],
18
23
  "homepage": "https://x1id.io",
19
- "repository": { "type": "git", "url": "git+https://github.com/fortiblox/x1id-sdk.git" },
20
- "bugs": { "url": "https://github.com/fortiblox/x1id-sdk/issues" },
21
24
  "license": "MIT",
22
- "publishConfig": { "access": "public", "provenance": true },
25
+ "publishConfig": { "access": "public" },
23
26
  "engines": { "node": ">=18" },
24
27
  "devDependencies": {
25
28
  "typescript": "^5.6.0",
Binary file