@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 +137 -31
- package/dist/index.js +142 -6
- package/dist/wasm.d.ts +15 -0
- package/dist/wasm.js +29 -0
- package/package.json +9 -6
- package/wasm/x1_resolve_wasm.wasm +0 -0
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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…");
|
|
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
|
|
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
|
|
142
|
-
|
|
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
|
-
`
|
|
165
|
-
`
|
|
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
|
-
-
|
|
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
|
|
85
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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": {
|
|
9
|
-
|
|
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"
|
|
25
|
+
"publishConfig": { "access": "public" },
|
|
23
26
|
"engines": { "node": ">=18" },
|
|
24
27
|
"devDependencies": {
|
|
25
28
|
"typescript": "^5.6.0",
|
|
Binary file
|