@x1id/resolve 0.1.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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/base58.d.ts +3 -0
- package/dist/base58.js +38 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +216 -0
- package/dist/parse.d.ts +37 -0
- package/dist/parse.js +92 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +25 -0
- package/dist/wasm.d.ts +43 -0
- package/dist/wasm.js +75 -0
- package/package.json +31 -0
- package/wasm/x1_resolve_wasm.wasm +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fortiblox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# @x1id/resolve
|
|
2
|
+
|
|
3
|
+
Resolve **`@handles`** and **X1NS** names (`.x1` / `.xnt` / `.xen`) to addresses on
|
|
4
|
+
[X1](https://x1.xyz) — the SDK behind [x1id.io](https://x1id.io).
|
|
5
|
+
|
|
6
|
+
Built for wallets and payment flows. One rule drives the whole design: **a name
|
|
7
|
+
is never just an address.** `@jack` and `jack.x1` are different namespaces that
|
|
8
|
+
can belong to different people, so every result tells you *which* namespace
|
|
9
|
+
answered, and a mixed shape is refused rather than guessed.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createResolver, WasmResolver } from "@x1id/resolve";
|
|
13
|
+
|
|
14
|
+
const wasm = await WasmResolver.fromBytes(/* the module bytes, see below */);
|
|
15
|
+
const x1id = createResolver({ rpcUrl: "https://rpc.mainnet.x1.xyz", wasm });
|
|
16
|
+
|
|
17
|
+
const r = await x1id.resolve("@jack");
|
|
18
|
+
// { name: "jack", namespace: "handle", address: "H8Fs…", chain: "X1",
|
|
19
|
+
// verification: "verified" }
|
|
20
|
+
|
|
21
|
+
await x1id.resolve("jack.x1"); // resolves the X1NS domain instead
|
|
22
|
+
await x1id.resolve("@jack.x1"); // throws ResolveError { code: "ambiguous" }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Why a resolver SDK at all
|
|
26
|
+
|
|
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.
|
|
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.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @x1id/resolve
|
|
43
|
+
# or straight from source:
|
|
44
|
+
npm install github:fortiblox/x1id-sdk
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`@solana/web3.js` is an **optional** peer dependency — you only need it if you
|
|
48
|
+
pass web3.js types around; the SDK itself talks to RPC directly.
|
|
49
|
+
|
|
50
|
+
## Loading the WASM module
|
|
51
|
+
|
|
52
|
+
Derivation needs the WASM module. The package ships it at
|
|
53
|
+
`@x1id/resolve/wasm/x1_resolve_wasm.wasm`.
|
|
54
|
+
|
|
55
|
+
**Node**
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { readFileSync } from "node:fs";
|
|
59
|
+
import { createRequire } from "node:module";
|
|
60
|
+
import { WasmResolver, createResolver } from "@x1id/resolve";
|
|
61
|
+
|
|
62
|
+
const require = createRequire(import.meta.url);
|
|
63
|
+
const wasmPath = require.resolve("@x1id/resolve/wasm/x1_resolve_wasm.wasm");
|
|
64
|
+
const wasm = await WasmResolver.fromBytes(readFileSync(wasmPath));
|
|
65
|
+
|
|
66
|
+
const x1id = createResolver({ rpcUrl: "https://rpc.mainnet.x1.xyz", wasm });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Browser / bundler**
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import wasmUrl from "@x1id/resolve/wasm/x1_resolve_wasm.wasm?url"; // Vite
|
|
73
|
+
const wasm = await WasmResolver.fromBytes(await (await fetch(wasmUrl)).arrayBuffer());
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## The one rule: show the namespace before you send
|
|
77
|
+
|
|
78
|
+
`@jack` and `jack.x1` can resolve to **different owners**. There is no API in
|
|
79
|
+
this SDK that returns a bare address — every `Resolved` carries its `namespace`.
|
|
80
|
+
Render it in your recipient field before the user commits a transfer.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
try {
|
|
84
|
+
const res = await x1id.resolve(userInput);
|
|
85
|
+
showRecipient({
|
|
86
|
+
address: res.address,
|
|
87
|
+
// e.g. "@jack" or "jack.x1" — the user must see which one they are paying
|
|
88
|
+
label: res.namespace === "handle" ? `@${res.name}` : res.name,
|
|
89
|
+
verified: res.verification === "verified",
|
|
90
|
+
});
|
|
91
|
+
} catch (e) {
|
|
92
|
+
if (e.code === "not-found") showHint("No name found");
|
|
93
|
+
else if (e.code === "ambiguous") showHint("Type either @jack or jack.x1, not both");
|
|
94
|
+
else if (e.code === "invalid-handle") showHint("Letters, digits and hyphens only");
|
|
95
|
+
// ResolveError.code is always a string a UI can branch on — never a bare throw
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
See [`examples/recipient-field.ts`](examples/recipient-field.ts) for a fuller
|
|
100
|
+
wallet integration.
|
|
101
|
+
|
|
102
|
+
## Reverse lookup
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const name = await x1id.reverse("H8Fs…"); // primary name for an address, or null
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Multi-chain
|
|
109
|
+
|
|
110
|
+
A handle can carry addresses for several chains. Pass the one you want:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
await x1id.resolve("@jack", { chain: "X1" }); // default
|
|
114
|
+
await x1id.resolve("@jack", { chain: "SOL" }); // X1 shares Solana's key format
|
|
115
|
+
await x1id.resolve("@jack", { chain: "ETH" }); // → no-record-for-chain (see below)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Today the SDK reads the owner's **X1/SVM** address. Per-chain records for ETH and
|
|
119
|
+
BTC live in separate on-chain accounts the resolver does not read yet, so a
|
|
120
|
+
request for those returns `no-record-for-chain` — an explicit "no record", never
|
|
121
|
+
a wrong address on the wrong chain.
|
|
122
|
+
|
|
123
|
+
## Verification
|
|
124
|
+
|
|
125
|
+
| `verification` | meaning |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `verified` | control of the address was proved (the `@handle` owner holds the registry account). |
|
|
128
|
+
| `unverified` | a record exists but ownership was not proved for that chain. **Show a warning.** |
|
|
129
|
+
|
|
130
|
+
## What works today
|
|
131
|
+
|
|
132
|
+
| | Status |
|
|
133
|
+
|---|---|
|
|
134
|
+
| 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 | ✅ |
|
|
137
|
+
| Per-chain ETH/BTC records | 🔜 roadmap |
|
|
138
|
+
| Registration / transfer (write) | 🔜 roadmap |
|
|
139
|
+
|
|
140
|
+
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.
|
|
143
|
+
|
|
144
|
+
## API
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
createResolver(config: ResolverConfig): Resolver
|
|
148
|
+
|
|
149
|
+
interface ResolverConfig {
|
|
150
|
+
rpcUrl: string; // any X1 RPC
|
|
151
|
+
wasm: WasmResolver; // required — derivation runs in WASM, no JS fallback
|
|
152
|
+
fetchImpl?: typeof fetch; // custom transport (tests, proxies)
|
|
153
|
+
cacheTtlMs?: number; // default 30_000; 0 disables
|
|
154
|
+
handleProgramId?: string; // default: canonical X1 deployment
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface Resolver {
|
|
158
|
+
resolve(input: string, opts?: { chain?: "X1" | "SOL" | "ETH" | "BTC" }): Promise<Resolved>;
|
|
159
|
+
reverse(address: string): Promise<string | null>;
|
|
160
|
+
clearCache(): void;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`ResolveError.code` is one of: `unrecognized`, `ambiguous`, `invalid-handle`,
|
|
165
|
+
`invalid-domain`, `not-found`, `no-record-for-chain`, `rpc-error`.
|
|
166
|
+
|
|
167
|
+
## Links
|
|
168
|
+
|
|
169
|
+
- Home & docs: **[x1id.io](https://x1id.io)**
|
|
170
|
+
- Issues: <https://github.com/fortiblox/x1id-sdk/issues>
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT © Fortiblox
|
package/dist/base58.d.ts
ADDED
package/dist/base58.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Base58 (Bitcoin alphabet) for 32-byte addresses. */
|
|
2
|
+
const A = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
3
|
+
export function encodeBase58(bytes) {
|
|
4
|
+
let n = 0n;
|
|
5
|
+
for (const b of bytes)
|
|
6
|
+
n = n * 256n + BigInt(b);
|
|
7
|
+
let out = "";
|
|
8
|
+
while (n > 0n) {
|
|
9
|
+
out = A[Number(n % 58n)] + out;
|
|
10
|
+
n /= 58n;
|
|
11
|
+
}
|
|
12
|
+
for (const b of bytes) {
|
|
13
|
+
if (b !== 0)
|
|
14
|
+
break;
|
|
15
|
+
out = "1" + out;
|
|
16
|
+
}
|
|
17
|
+
return out === "" ? "1" : out;
|
|
18
|
+
}
|
|
19
|
+
/** Decode to exactly 32 bytes, or null if the input is not a valid address. */
|
|
20
|
+
export function decodeBase58_32(s) {
|
|
21
|
+
if (s.length === 0 || s.length > 44)
|
|
22
|
+
return null;
|
|
23
|
+
let n = 0n;
|
|
24
|
+
for (const c of s) {
|
|
25
|
+
const i = A.indexOf(c);
|
|
26
|
+
if (i === -1)
|
|
27
|
+
return null;
|
|
28
|
+
n = n * 58n + BigInt(i);
|
|
29
|
+
}
|
|
30
|
+
if (n >= 1n << 256n)
|
|
31
|
+
return null;
|
|
32
|
+
const out = new Uint8Array(32);
|
|
33
|
+
for (let i = 31; i >= 0; i--) {
|
|
34
|
+
out[i] = Number(n & 255n);
|
|
35
|
+
n >>= 8n;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `@handles` and X1NS names on X1.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { createResolver } from "@x1id/resolve";
|
|
6
|
+
*
|
|
7
|
+
* const r = createResolver({ rpcUrl: "https://rpc.mainnet.x1.xyz" });
|
|
8
|
+
* const res = await r.resolve("@jack", { chain: "X1" });
|
|
9
|
+
* // { name: "jack", namespace: "handle", address: "...", verification: "verified" }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* # The one rule
|
|
13
|
+
*
|
|
14
|
+
* `@jack` and `jack.x1` are different namespaces that can have **different
|
|
15
|
+
* owners**. Every result carries its `namespace`, and there is no API returning
|
|
16
|
+
* a bare address. Render the namespace before letting a user send.
|
|
17
|
+
*
|
|
18
|
+
* # Reads chain, not an API
|
|
19
|
+
*
|
|
20
|
+
* X1NS names resolve by deriving accounts and reading them over RPC. We never
|
|
21
|
+
* call `api.x1ns.xyz` — a wallet that resolves through a third party's uptime
|
|
22
|
+
* breaks when they lose interest.
|
|
23
|
+
*/
|
|
24
|
+
export * from "./types.js";
|
|
25
|
+
export { normalizeHandle, parseName, looksLikeName, type ParsedName } from "./parse.js";
|
|
26
|
+
export { WasmResolver, type WasmTld } from "./wasm.js";
|
|
27
|
+
export { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
28
|
+
import { type Chain, type Resolved } from "./types.js";
|
|
29
|
+
import { WasmResolver } from "./wasm.js";
|
|
30
|
+
export interface ResolverConfig {
|
|
31
|
+
/** X1 RPC endpoint. */
|
|
32
|
+
readonly rpcUrl: string;
|
|
33
|
+
/**
|
|
34
|
+
* WASM module for account derivation. Required — derivation needs an
|
|
35
|
+
* ed25519 on-curve check and there is deliberately no TypeScript fallback,
|
|
36
|
+
* because a second implementation of that is how wrong addresses get
|
|
37
|
+
* derived silently.
|
|
38
|
+
*/
|
|
39
|
+
readonly wasm: WasmResolver;
|
|
40
|
+
/** Optional fetch override for testing or custom transport. */
|
|
41
|
+
readonly fetchImpl?: typeof fetch;
|
|
42
|
+
/** Cache TTL in ms. Default 30_000. Set 0 to disable. */
|
|
43
|
+
readonly cacheTtlMs?: number;
|
|
44
|
+
/** @handle registry program id. Defaults to the canonical X1 deployment. */
|
|
45
|
+
readonly handleProgramId?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ResolveOptions {
|
|
48
|
+
/** Which chain's address to return. Default "X1". */
|
|
49
|
+
readonly chain?: Chain;
|
|
50
|
+
}
|
|
51
|
+
export interface Resolver {
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a name to an address on a specific chain.
|
|
54
|
+
*
|
|
55
|
+
* @throws {ResolveError} with a `code` a UI can branch on — never a bare
|
|
56
|
+
* string, so a recipient field can distinguish "still typing" from "this
|
|
57
|
+
* name does not exist".
|
|
58
|
+
*/
|
|
59
|
+
resolve(input: string, opts?: ResolveOptions): Promise<Resolved>;
|
|
60
|
+
/** Reverse: address to its primary name, or null if none is set. */
|
|
61
|
+
reverse(address: string): Promise<string | null>;
|
|
62
|
+
/** Clear the resolution cache. */
|
|
63
|
+
clearCache(): void;
|
|
64
|
+
}
|
|
65
|
+
export declare function createResolver(config: ResolverConfig): Resolver;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `@handles` and X1NS names on X1.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { createResolver } from "@x1id/resolve";
|
|
6
|
+
*
|
|
7
|
+
* const r = createResolver({ rpcUrl: "https://rpc.mainnet.x1.xyz" });
|
|
8
|
+
* const res = await r.resolve("@jack", { chain: "X1" });
|
|
9
|
+
* // { name: "jack", namespace: "handle", address: "...", verification: "verified" }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* # The one rule
|
|
13
|
+
*
|
|
14
|
+
* `@jack` and `jack.x1` are different namespaces that can have **different
|
|
15
|
+
* owners**. Every result carries its `namespace`, and there is no API returning
|
|
16
|
+
* a bare address. Render the namespace before letting a user send.
|
|
17
|
+
*
|
|
18
|
+
* # Reads chain, not an API
|
|
19
|
+
*
|
|
20
|
+
* X1NS names resolve by deriving accounts and reading them over RPC. We never
|
|
21
|
+
* call `api.x1ns.xyz` — a wallet that resolves through a third party's uptime
|
|
22
|
+
* breaks when they lose interest.
|
|
23
|
+
*/
|
|
24
|
+
export * from "./types.js";
|
|
25
|
+
export { normalizeHandle, parseName, looksLikeName } from "./parse.js";
|
|
26
|
+
export { WasmResolver } from "./wasm.js";
|
|
27
|
+
export { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
28
|
+
import { ResolveError } from "./types.js";
|
|
29
|
+
import { parseName } from "./parse.js";
|
|
30
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
31
|
+
/** Root authority for each X1NS TLD — used to verify a fetched account really
|
|
32
|
+
* belongs to the TLD it claims. Without this check a caller handed an
|
|
33
|
+
* arbitrary account would read an owner straight out of it. */
|
|
34
|
+
const TLD_ROOT = Object.freeze({
|
|
35
|
+
x1: "4NG35LXbtyamuoyjarMf5f78esyWxWhSDHxqbAU5yZTk",
|
|
36
|
+
xnt: "6sHoWK6ht73Pb4y6yA7Sw8iP7DxS45gGp1fH3zWYn56V",
|
|
37
|
+
xen: "3SUwpSz33AsyJwf6B48cKZuDTswuUEdUhcXszZrFWPqo",
|
|
38
|
+
});
|
|
39
|
+
const SPL_NAME_HEADER_LEN = 96;
|
|
40
|
+
/** The @handle registry program on X1. Deployed on testnet today; the same id
|
|
41
|
+
* is used on mainnet once deployed. Override via `handleProgramId` to point at
|
|
42
|
+
* a different deployment. */
|
|
43
|
+
const DEFAULT_HANDLE_PROGRAM = "8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P";
|
|
44
|
+
// Handle account layout, mirrored from the on-chain program:
|
|
45
|
+
// discriminator(8) name(32) name_len(1) owner(32) ...
|
|
46
|
+
// The owner is the address the handle resolves to on X1.
|
|
47
|
+
const HANDLE_OWNER_OFFSET = 41;
|
|
48
|
+
const HANDLE_MIN_LEN = HANDLE_OWNER_OFFSET + 32;
|
|
49
|
+
export function createResolver(config) {
|
|
50
|
+
const ttl = config.cacheTtlMs ?? 30_000;
|
|
51
|
+
const cache = new Map();
|
|
52
|
+
const doFetch = config.fetchImpl ?? globalThis.fetch;
|
|
53
|
+
if (typeof doFetch !== "function") {
|
|
54
|
+
throw new Error("No fetch available; pass fetchImpl in ResolverConfig");
|
|
55
|
+
}
|
|
56
|
+
const decodedProgram = decodeBase58_32(config.handleProgramId ?? DEFAULT_HANDLE_PROGRAM);
|
|
57
|
+
if (!decodedProgram) {
|
|
58
|
+
throw new Error("handleProgramId is not a valid base58 address");
|
|
59
|
+
}
|
|
60
|
+
// Explicit type so the non-null narrowing survives into the resolveHandle
|
|
61
|
+
// closure below — TS widens a captured `const` back to its declared type.
|
|
62
|
+
const handleProgram = decodedProgram;
|
|
63
|
+
async function rpc(method, params) {
|
|
64
|
+
let res;
|
|
65
|
+
try {
|
|
66
|
+
res = await doFetch(config.rpcUrl, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "content-type": "application/json" },
|
|
69
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
throw new ResolveError("rpc-error", `RPC request failed: ${String(e)}`);
|
|
74
|
+
}
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
throw new ResolveError("rpc-error", `RPC returned HTTP ${res.status}`);
|
|
77
|
+
}
|
|
78
|
+
const body = (await res.json());
|
|
79
|
+
if (body.error) {
|
|
80
|
+
throw new ResolveError("rpc-error", body.error.message ?? "RPC error");
|
|
81
|
+
}
|
|
82
|
+
return body.result;
|
|
83
|
+
}
|
|
84
|
+
/** Fetch raw account data, or null when the account does not exist. */
|
|
85
|
+
async function accountData(address) {
|
|
86
|
+
const result = (await rpc("getAccountInfo", [
|
|
87
|
+
address,
|
|
88
|
+
{ encoding: "base64" },
|
|
89
|
+
]));
|
|
90
|
+
const value = result?.value;
|
|
91
|
+
if (!value)
|
|
92
|
+
return null;
|
|
93
|
+
const b64 = value.data[0];
|
|
94
|
+
const bin = atob(b64);
|
|
95
|
+
const out = new Uint8Array(bin.length);
|
|
96
|
+
for (let i = 0; i < bin.length; i++)
|
|
97
|
+
out[i] = bin.charCodeAt(i);
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
async function resolveX1ns(canonical, label, tld, chain, input) {
|
|
101
|
+
const account = config.wasm.deriveX1nsAccount(label, tld);
|
|
102
|
+
if (!account) {
|
|
103
|
+
throw new ResolveError("invalid-domain", `"${input}" is not a resolvable label`, input);
|
|
104
|
+
}
|
|
105
|
+
const data = await accountData(encodeBase58(account));
|
|
106
|
+
if (!data) {
|
|
107
|
+
throw new ResolveError("not-found", `${canonical} is not registered`, input);
|
|
108
|
+
}
|
|
109
|
+
if (data.length < SPL_NAME_HEADER_LEN) {
|
|
110
|
+
throw new ResolveError("rpc-error", `${canonical} returned a malformed account`, input);
|
|
111
|
+
}
|
|
112
|
+
// parent_name(32) || owner(32) || class(32)
|
|
113
|
+
const parent = encodeBase58(data.slice(0, 32));
|
|
114
|
+
if (parent !== TLD_ROOT[tld]) {
|
|
115
|
+
// Not a paranoid check: without it, any account could be presented as a
|
|
116
|
+
// domain and its bytes read as an owner.
|
|
117
|
+
throw new ResolveError("not-found", `${canonical} is not a .${tld} domain`, input);
|
|
118
|
+
}
|
|
119
|
+
const owner = encodeBase58(data.slice(32, 64));
|
|
120
|
+
// X1NS stores per-chain addresses in a profile record. Until that record is
|
|
121
|
+
// read, only the owner is known — and the owner is an X1/SVM address.
|
|
122
|
+
if (chain !== "X1" && chain !== "SOL") {
|
|
123
|
+
throw new ResolveError("no-record-for-chain", `${canonical} has no ${chain} record`, input);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
input,
|
|
127
|
+
name: canonical,
|
|
128
|
+
namespace: tld,
|
|
129
|
+
address: owner,
|
|
130
|
+
chain,
|
|
131
|
+
// The owner is who controls the name, which is a stronger claim than an
|
|
132
|
+
// unproved address record — but it is not a per-chain ownership proof.
|
|
133
|
+
verification: "unverified",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async function resolveHandle(canonical, chain, input) {
|
|
137
|
+
const account = config.wasm.deriveHandleAccount(canonical, handleProgram);
|
|
138
|
+
if (!account) {
|
|
139
|
+
throw new ResolveError("invalid-handle", `"${input}" is not a valid handle`, input);
|
|
140
|
+
}
|
|
141
|
+
const data = await accountData(encodeBase58(account));
|
|
142
|
+
if (!data) {
|
|
143
|
+
throw new ResolveError("not-found", `@${canonical} is not registered`, input);
|
|
144
|
+
}
|
|
145
|
+
if (data.length < HANDLE_MIN_LEN) {
|
|
146
|
+
throw new ResolveError("rpc-error", `@${canonical} returned a malformed account`, input);
|
|
147
|
+
}
|
|
148
|
+
const owner = encodeBase58(data.slice(HANDLE_OWNER_OFFSET, HANDLE_OWNER_OFFSET + 32));
|
|
149
|
+
// The owner is an X1/SVM address. Per-chain records (ETH/BTC) live in
|
|
150
|
+
// separate record accounts the resolver does not read yet, so a request for
|
|
151
|
+
// another chain is an explicit "no record" rather than a wrong address.
|
|
152
|
+
if (chain !== "X1" && chain !== "SOL") {
|
|
153
|
+
throw new ResolveError("no-record-for-chain", `@${canonical} has no ${chain} record`, input);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
input,
|
|
157
|
+
name: canonical,
|
|
158
|
+
namespace: "handle",
|
|
159
|
+
address: owner,
|
|
160
|
+
chain,
|
|
161
|
+
// The owner holds the registry account for this handle — a proved control
|
|
162
|
+
// relationship, not an unverified record.
|
|
163
|
+
verification: "verified",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function resolve(input, opts) {
|
|
167
|
+
const chain = opts?.chain ?? "X1";
|
|
168
|
+
const parsed = parseName(input); // throws with a specific code
|
|
169
|
+
const key = `${parsed.namespace}:${parsed.canonical}:${chain}`;
|
|
170
|
+
if (ttl > 0) {
|
|
171
|
+
const hit = cache.get(key);
|
|
172
|
+
if (hit && hit.expires > Date.now())
|
|
173
|
+
return hit.value;
|
|
174
|
+
}
|
|
175
|
+
let value;
|
|
176
|
+
if (parsed.namespace === "handle") {
|
|
177
|
+
value = await resolveHandle(parsed.canonical, chain, input);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
value = await resolveX1ns(parsed.canonical, parsed.label, parsed.namespace, chain, input);
|
|
181
|
+
}
|
|
182
|
+
if (ttl > 0)
|
|
183
|
+
cache.set(key, { value, expires: Date.now() + ttl });
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
/** Address to its primary name, or null when none is set. */
|
|
187
|
+
async function reverse(address) {
|
|
188
|
+
const { decodeBase58_32 } = await import("./base58.js");
|
|
189
|
+
const owner = decodeBase58_32(address);
|
|
190
|
+
if (!owner)
|
|
191
|
+
throw new ResolveError("unrecognized", `"${address}" is not an address`, address);
|
|
192
|
+
const record = config.wasm.derivePrimaryAccount(owner);
|
|
193
|
+
if (!record)
|
|
194
|
+
throw new ResolveError("unrecognized", "could not derive record", address);
|
|
195
|
+
const data = await accountData(encodeBase58(record));
|
|
196
|
+
if (!data || data.length < 41)
|
|
197
|
+
return null; // no primary set
|
|
198
|
+
// tag(1) || domain_account(32) || set_at_i64_le(8)
|
|
199
|
+
const domainAccount = encodeBase58(data.slice(1, 33));
|
|
200
|
+
const domainData = await accountData(domainAccount);
|
|
201
|
+
if (!domainData || domainData.length < SPL_NAME_HEADER_LEN)
|
|
202
|
+
return null;
|
|
203
|
+
const parent = encodeBase58(domainData.slice(0, 32));
|
|
204
|
+
const tld = Object.keys(TLD_ROOT).find((t) => TLD_ROOT[t] === parent);
|
|
205
|
+
if (!tld)
|
|
206
|
+
return null;
|
|
207
|
+
// The account stores no label, so the name cannot be reconstructed from it.
|
|
208
|
+
// Callers get the TLD and the account; resolving the label needs an index.
|
|
209
|
+
return `${domainAccount}.${tld}`;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
resolve,
|
|
213
|
+
reverse,
|
|
214
|
+
clearCache: () => cache.clear(),
|
|
215
|
+
};
|
|
216
|
+
}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Namespace } from "./types.js";
|
|
2
|
+
declare const TLDS: readonly ["x1", "xnt", "xen"];
|
|
3
|
+
export type Tld = (typeof TLDS)[number];
|
|
4
|
+
/**
|
|
5
|
+
* Canonical handle normalization.
|
|
6
|
+
*
|
|
7
|
+
* **This mirrors the Rust `handle-normalize` crate and must stay byte-identical
|
|
8
|
+
* to it.** The registry derives PDA seeds from the Rust implementation; if this
|
|
9
|
+
* one diverges, a handle registered under one normalization resolves under
|
|
10
|
+
* another — permanently unreachable, funds sent nowhere, no error raised.
|
|
11
|
+
*
|
|
12
|
+
* The Rust crate is the source of truth. This exists so a browser can give
|
|
13
|
+
* instant feedback without loading WASM; the conformance suite
|
|
14
|
+
* (`test/conformance.test.js`) asserts the two agree on every fixture.
|
|
15
|
+
*/
|
|
16
|
+
export declare function normalizeHandle(raw: string): string;
|
|
17
|
+
export interface ParsedName {
|
|
18
|
+
readonly namespace: Namespace;
|
|
19
|
+
readonly canonical: string;
|
|
20
|
+
readonly label: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Classify a name by **shape**, never by trying one namespace and falling back
|
|
24
|
+
* to the other.
|
|
25
|
+
*
|
|
26
|
+
* A fallback chain is exactly how `@jack` and `jack.x1` get conflated, and the
|
|
27
|
+
* failure mode is silent: the transfer succeeds, to the wrong person.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseName(input: string): ParsedName;
|
|
30
|
+
/**
|
|
31
|
+
* Whether input is a name at all, as opposed to a raw address.
|
|
32
|
+
*
|
|
33
|
+
* Lets a recipient field decide whether to attempt resolution, so pasting
|
|
34
|
+
* base58 does not surface a validation error.
|
|
35
|
+
*/
|
|
36
|
+
export declare function looksLikeName(input: string): boolean;
|
|
37
|
+
export {};
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ResolveError } from "./types.js";
|
|
2
|
+
const TLDS = ["x1", "xnt", "xen"];
|
|
3
|
+
const MAX_HANDLE_LEN = 32;
|
|
4
|
+
/**
|
|
5
|
+
* Canonical handle normalization.
|
|
6
|
+
*
|
|
7
|
+
* **This mirrors the Rust `handle-normalize` crate and must stay byte-identical
|
|
8
|
+
* to it.** The registry derives PDA seeds from the Rust implementation; if this
|
|
9
|
+
* one diverges, a handle registered under one normalization resolves under
|
|
10
|
+
* another — permanently unreachable, funds sent nowhere, no error raised.
|
|
11
|
+
*
|
|
12
|
+
* The Rust crate is the source of truth. This exists so a browser can give
|
|
13
|
+
* instant feedback without loading WASM; the conformance suite
|
|
14
|
+
* (`test/conformance.test.js`) asserts the two agree on every fixture.
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeHandle(raw) {
|
|
17
|
+
let s = raw.trim();
|
|
18
|
+
if (s.startsWith("@"))
|
|
19
|
+
s = s.slice(1);
|
|
20
|
+
// ASCII-only. X1NS permits Unicode and emoji; for a payment identifier that
|
|
21
|
+
// is a homograph vector, since Cyrillic "а" renders identically to ASCII "a".
|
|
22
|
+
// eslint-disable-next-line no-control-regex
|
|
23
|
+
if (!/^[\x00-\x7F]*$/.test(s)) {
|
|
24
|
+
throw new ResolveError("invalid-handle", "Handles must be ASCII only", raw);
|
|
25
|
+
}
|
|
26
|
+
if (s.length === 0)
|
|
27
|
+
throw new ResolveError("invalid-handle", "Handle is empty", raw);
|
|
28
|
+
if (s.length > MAX_HANDLE_LEN) {
|
|
29
|
+
throw new ResolveError("invalid-handle", `Handle exceeds ${MAX_HANDLE_LEN} characters`, raw);
|
|
30
|
+
}
|
|
31
|
+
const lower = s.toLowerCase();
|
|
32
|
+
if (!/^[a-z0-9-]+$/.test(lower)) {
|
|
33
|
+
throw new ResolveError("invalid-handle", "Handles allow only a-z, 0-9 and hyphen", raw);
|
|
34
|
+
}
|
|
35
|
+
if (lower.startsWith("-") || lower.endsWith("-")) {
|
|
36
|
+
throw new ResolveError("invalid-handle", "Handles cannot start or end with a hyphen", raw);
|
|
37
|
+
}
|
|
38
|
+
if (lower.includes("--")) {
|
|
39
|
+
throw new ResolveError("invalid-handle", "Handles cannot contain consecutive hyphens", raw);
|
|
40
|
+
}
|
|
41
|
+
if (/^[0-9]+$/.test(lower)) {
|
|
42
|
+
throw new ResolveError("invalid-handle", "All-digit handles are reserved", raw);
|
|
43
|
+
}
|
|
44
|
+
return lower;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Classify a name by **shape**, never by trying one namespace and falling back
|
|
48
|
+
* to the other.
|
|
49
|
+
*
|
|
50
|
+
* A fallback chain is exactly how `@jack` and `jack.x1` get conflated, and the
|
|
51
|
+
* failure mode is silent: the transfer succeeds, to the wrong person.
|
|
52
|
+
*/
|
|
53
|
+
export function parseName(input) {
|
|
54
|
+
const t = input.trim();
|
|
55
|
+
const looksHandle = t.startsWith("@");
|
|
56
|
+
const dot = t.lastIndexOf(".");
|
|
57
|
+
const suffix = dot === -1 ? "" : t.slice(dot + 1).toLowerCase();
|
|
58
|
+
const looksDomain = TLDS.includes(suffix);
|
|
59
|
+
if (looksHandle && looksDomain) {
|
|
60
|
+
throw new ResolveError("ambiguous", `"${t}" is both a handle and a domain shape`, t);
|
|
61
|
+
}
|
|
62
|
+
if (looksHandle) {
|
|
63
|
+
const canonical = normalizeHandle(t);
|
|
64
|
+
return { namespace: "handle", canonical, label: canonical };
|
|
65
|
+
}
|
|
66
|
+
if (looksDomain) {
|
|
67
|
+
const label = t.slice(0, dot).toLowerCase();
|
|
68
|
+
if (label.length === 0 || label.includes(".")) {
|
|
69
|
+
throw new ResolveError("invalid-domain", `"${t}" is not a valid domain`, t);
|
|
70
|
+
}
|
|
71
|
+
if (!/^[a-z0-9-]+$/.test(label)) {
|
|
72
|
+
// X1NS itself permits Unicode here; we refuse to resolve it rather than
|
|
73
|
+
// render a homograph as a successful result the user will act on.
|
|
74
|
+
throw new ResolveError("invalid-domain", "Domain labels must be ASCII", t);
|
|
75
|
+
}
|
|
76
|
+
return { namespace: suffix, canonical: `${label}.${suffix}`, label };
|
|
77
|
+
}
|
|
78
|
+
throw new ResolveError("unrecognized", `"${t}" is not a handle or a known domain`, t);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether input is a name at all, as opposed to a raw address.
|
|
82
|
+
*
|
|
83
|
+
* Lets a recipient field decide whether to attempt resolution, so pasting
|
|
84
|
+
* base58 does not surface a validation error.
|
|
85
|
+
*/
|
|
86
|
+
export function looksLikeName(input) {
|
|
87
|
+
const t = input.trim();
|
|
88
|
+
if (t.startsWith("@"))
|
|
89
|
+
return true;
|
|
90
|
+
const dot = t.lastIndexOf(".");
|
|
91
|
+
return dot !== -1 && TLDS.includes(t.slice(dot + 1).toLowerCase());
|
|
92
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Which naming system produced a result. */
|
|
2
|
+
export type Namespace = "handle" | "x1" | "xnt" | "xen";
|
|
3
|
+
/**
|
|
4
|
+
* Display label for a namespace. **Must be shown** next to any resolved
|
|
5
|
+
* address — see the module docs on why this is not optional.
|
|
6
|
+
*/
|
|
7
|
+
export declare function namespaceLabel(ns: Namespace): string;
|
|
8
|
+
/** Confidence in the address a name resolves to. */
|
|
9
|
+
export type Verification =
|
|
10
|
+
/** Owner proved control of this address for this chain. */
|
|
11
|
+
"verified"
|
|
12
|
+
/** Record exists but ownership was never proved. Show a warning. */
|
|
13
|
+
| "unverified";
|
|
14
|
+
/**
|
|
15
|
+
* A successfully resolved name.
|
|
16
|
+
*
|
|
17
|
+
* `namespace` is not optional and there is no variant of this type without it.
|
|
18
|
+
* `@jack` and `jack.x1` are different namespaces with different owners, so a
|
|
19
|
+
* bare address is never a complete answer.
|
|
20
|
+
*/
|
|
21
|
+
export interface Resolved {
|
|
22
|
+
/** Exactly what the user typed, for display. */
|
|
23
|
+
readonly input: string;
|
|
24
|
+
/** Canonical form of the name. */
|
|
25
|
+
readonly name: string;
|
|
26
|
+
/** Which naming system matched. Render this before allowing a send. */
|
|
27
|
+
readonly namespace: Namespace;
|
|
28
|
+
/** The address to send to, base58 for X1/SOL, 0x for EVM, bech32 for BTC. */
|
|
29
|
+
readonly address: string;
|
|
30
|
+
/** Chain this address belongs to. */
|
|
31
|
+
readonly chain: Chain;
|
|
32
|
+
/** Whether ownership of `address` was proved. */
|
|
33
|
+
readonly verification: Verification;
|
|
34
|
+
}
|
|
35
|
+
/** Chains a handle can carry a record for. SLIP-44 based. */
|
|
36
|
+
export type Chain = "X1" | "SOL" | "ETH" | "BTC";
|
|
37
|
+
export declare const CHAIN_COIN_TYPE: Readonly<Record<Chain, number>>;
|
|
38
|
+
export type ResolveErrorCode = "unrecognized" | "ambiguous" | "invalid-handle" | "invalid-domain" | "not-found" | "no-record-for-chain" | "rpc-error";
|
|
39
|
+
export declare class ResolveError extends Error {
|
|
40
|
+
readonly code: ResolveErrorCode;
|
|
41
|
+
readonly input?: string | undefined;
|
|
42
|
+
constructor(code: ResolveErrorCode, message: string, input?: string | undefined);
|
|
43
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display label for a namespace. **Must be shown** next to any resolved
|
|
3
|
+
* address — see the module docs on why this is not optional.
|
|
4
|
+
*/
|
|
5
|
+
export function namespaceLabel(ns) {
|
|
6
|
+
return ns === "handle" ? "@handle" : `.${ns}`;
|
|
7
|
+
}
|
|
8
|
+
export const CHAIN_COIN_TYPE = Object.freeze({
|
|
9
|
+
BTC: 0,
|
|
10
|
+
ETH: 60,
|
|
11
|
+
SOL: 501,
|
|
12
|
+
// X1 shares Solana's SVM and key format; it gets its own record slot so a
|
|
13
|
+
// user can point X1 and Solana at different wallets if they want to.
|
|
14
|
+
X1: 501_0000,
|
|
15
|
+
});
|
|
16
|
+
export class ResolveError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
input;
|
|
19
|
+
constructor(code, message, input) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.input = input;
|
|
23
|
+
this.name = "ResolveError";
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/wasm.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM-backed account derivation.
|
|
3
|
+
*
|
|
4
|
+
* Derivation needs `find_program_address`, which needs an ed25519 on-curve
|
|
5
|
+
* check — real curve arithmetic. Rather than hand-roll that in TypeScript
|
|
6
|
+
* (subtly wrong for months, and this one loses money when it is), the browser
|
|
7
|
+
* calls the Rust implementation that is already verified against live mainnet
|
|
8
|
+
* accounts.
|
|
9
|
+
*
|
|
10
|
+
* The module is single-threaded by construction: one shared input buffer, one
|
|
11
|
+
* shared output buffer, call-at-a-time. `WasmResolver` owns an instance and
|
|
12
|
+
* never yields between writing input and reading output, so callers cannot
|
|
13
|
+
* interleave.
|
|
14
|
+
*/
|
|
15
|
+
declare const TLD_INDEX: {
|
|
16
|
+
readonly x1: 0;
|
|
17
|
+
readonly xnt: 1;
|
|
18
|
+
readonly xen: 2;
|
|
19
|
+
};
|
|
20
|
+
export type WasmTld = keyof typeof TLD_INDEX;
|
|
21
|
+
export declare class WasmResolver {
|
|
22
|
+
#private;
|
|
23
|
+
private constructor();
|
|
24
|
+
/** Instantiate from raw module bytes. */
|
|
25
|
+
static fromBytes(bytes: BufferSource): Promise<WasmResolver>;
|
|
26
|
+
/** Canonical handle form, or null if invalid. */
|
|
27
|
+
normalizeHandle(raw: string): string | null;
|
|
28
|
+
/** 32-byte X1NS domain account, or null if the label is invalid. */
|
|
29
|
+
deriveX1nsAccount(label: string, tld: WasmTld): Uint8Array | null;
|
|
30
|
+
/** 32-byte primary-domain record account for a 32-byte owner address. */
|
|
31
|
+
derivePrimaryAccount(owner: Uint8Array): Uint8Array | null;
|
|
32
|
+
/**
|
|
33
|
+
* 32-byte `@handle` registry account (a program-derived address) for a
|
|
34
|
+
* canonical handle under a given registry program id, or null if the handle
|
|
35
|
+
* is not canonical.
|
|
36
|
+
*
|
|
37
|
+
* The program id is staged into a separate WASM buffer first, so the seed
|
|
38
|
+
* that produces the address is `["handle", canonical]` under exactly the
|
|
39
|
+
* program the caller names — never a program baked into this module.
|
|
40
|
+
*/
|
|
41
|
+
deriveHandleAccount(canonical: string, programId: Uint8Array): Uint8Array | null;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
package/dist/wasm.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM-backed account derivation.
|
|
3
|
+
*
|
|
4
|
+
* Derivation needs `find_program_address`, which needs an ed25519 on-curve
|
|
5
|
+
* check — real curve arithmetic. Rather than hand-roll that in TypeScript
|
|
6
|
+
* (subtly wrong for months, and this one loses money when it is), the browser
|
|
7
|
+
* calls the Rust implementation that is already verified against live mainnet
|
|
8
|
+
* accounts.
|
|
9
|
+
*
|
|
10
|
+
* The module is single-threaded by construction: one shared input buffer, one
|
|
11
|
+
* shared output buffer, call-at-a-time. `WasmResolver` owns an instance and
|
|
12
|
+
* never yields between writing input and reading output, so callers cannot
|
|
13
|
+
* interleave.
|
|
14
|
+
*/
|
|
15
|
+
const TLD_INDEX = { x1: 0, xnt: 1, xen: 2 };
|
|
16
|
+
export class WasmResolver {
|
|
17
|
+
#x;
|
|
18
|
+
#enc = new TextEncoder();
|
|
19
|
+
#dec = new TextDecoder();
|
|
20
|
+
constructor(exports) {
|
|
21
|
+
this.#x = exports;
|
|
22
|
+
}
|
|
23
|
+
/** Instantiate from raw module bytes. */
|
|
24
|
+
static async fromBytes(bytes) {
|
|
25
|
+
const { instance } = await WebAssembly.instantiate(bytes, {});
|
|
26
|
+
return new WasmResolver(instance.exports);
|
|
27
|
+
}
|
|
28
|
+
#write(bytes) {
|
|
29
|
+
if (bytes.length > this.#x.input_cap()) {
|
|
30
|
+
throw new RangeError(`input exceeds ${this.#x.input_cap()} bytes`);
|
|
31
|
+
}
|
|
32
|
+
// Re-read the view every call: WebAssembly.Memory can be detached by growth.
|
|
33
|
+
new Uint8Array(this.#x.memory.buffer, this.#x.input_ptr(), bytes.length).set(bytes);
|
|
34
|
+
return bytes.length;
|
|
35
|
+
}
|
|
36
|
+
#read() {
|
|
37
|
+
const len = this.#x.output_len();
|
|
38
|
+
return new Uint8Array(this.#x.memory.buffer, this.#x.output_ptr(), len).slice();
|
|
39
|
+
}
|
|
40
|
+
/** Canonical handle form, or null if invalid. */
|
|
41
|
+
normalizeHandle(raw) {
|
|
42
|
+
const n = this.#write(this.#enc.encode(raw));
|
|
43
|
+
return this.#x.normalize_handle(n) === 1 ? this.#dec.decode(this.#read()) : null;
|
|
44
|
+
}
|
|
45
|
+
/** 32-byte X1NS domain account, or null if the label is invalid. */
|
|
46
|
+
deriveX1nsAccount(label, tld) {
|
|
47
|
+
const n = this.#write(this.#enc.encode(label));
|
|
48
|
+
return this.#x.derive_x1ns_account(n, TLD_INDEX[tld]) === 1 ? this.#read() : null;
|
|
49
|
+
}
|
|
50
|
+
/** 32-byte primary-domain record account for a 32-byte owner address. */
|
|
51
|
+
derivePrimaryAccount(owner) {
|
|
52
|
+
if (owner.length !== 32)
|
|
53
|
+
return null;
|
|
54
|
+
const n = this.#write(owner);
|
|
55
|
+
return this.#x.derive_primary_account(n) === 1 ? this.#read() : null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 32-byte `@handle` registry account (a program-derived address) for a
|
|
59
|
+
* canonical handle under a given registry program id, or null if the handle
|
|
60
|
+
* is not canonical.
|
|
61
|
+
*
|
|
62
|
+
* The program id is staged into a separate WASM buffer first, so the seed
|
|
63
|
+
* that produces the address is `["handle", canonical]` under exactly the
|
|
64
|
+
* program the caller names — never a program baked into this module.
|
|
65
|
+
*/
|
|
66
|
+
deriveHandleAccount(canonical, programId) {
|
|
67
|
+
if (programId.length !== 32)
|
|
68
|
+
return null;
|
|
69
|
+
// Stage the program id into its own buffer. Re-read the view every call:
|
|
70
|
+
// WebAssembly.Memory can be detached by growth between calls.
|
|
71
|
+
new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
|
|
72
|
+
const n = this.#write(this.#enc.encode(canonical));
|
|
73
|
+
return this.#x.derive_handle_account_staged(n) === 1 ? this.#read() : null;
|
|
74
|
+
}
|
|
75
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@x1id/resolve",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Resolve @handles and X1NS names on X1. Never silently picks between namespaces.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
|
|
9
|
+
"files": ["dist", "wasm"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"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
|
+
"build": "tsc -p tsconfig.json",
|
|
13
|
+
"test": "node --test test/*.test.js",
|
|
14
|
+
"lint:package": "publint",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["x1", "x1id", "solana", "svm", "naming", "x1ns", "handles", "wallet", "resolver", "web3"],
|
|
18
|
+
"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
|
+
"license": "MIT",
|
|
22
|
+
"publishConfig": { "access": "public", "provenance": true },
|
|
23
|
+
"engines": { "node": ">=18" },
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5.6.0",
|
|
26
|
+
"@types/node": "^22.0.0",
|
|
27
|
+
"publint": "^0.2.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": { "@solana/web3.js": "^1.95.0" },
|
|
30
|
+
"peerDependenciesMeta": { "@solana/web3.js": { "optional": true } }
|
|
31
|
+
}
|
|
Binary file
|