@x1id/resolve 0.2.0 → 0.3.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 +155 -15
- package/dist/accounts.d.ts +86 -0
- package/dist/accounts.js +183 -0
- package/dist/attestation.d.ts +214 -0
- package/dist/attestation.js +278 -0
- package/dist/control.d.ts +201 -0
- package/dist/control.js +316 -0
- package/dist/delegate.d.ts +153 -0
- package/dist/delegate.js +166 -0
- package/dist/index.d.ts +30 -2
- package/dist/index.js +70 -135
- package/dist/integrator.d.ts +117 -0
- package/dist/integrator.js +166 -0
- package/dist/lock.d.ts +180 -0
- package/dist/lock.js +211 -0
- package/dist/recordCount.d.ts +98 -0
- package/dist/recordCount.js +114 -0
- package/dist/records.d.ts +113 -0
- package/dist/records.js +178 -0
- package/dist/subname.d.ts +277 -0
- package/dist/subname.js +366 -0
- package/dist/textRecords.d.ts +248 -0
- package/dist/textRecords.js +357 -0
- package/dist/types.d.ts +12 -1
- package/dist/types.js +3 -1
- package/dist/voucher.d.ts +130 -0
- package/dist/voucher.js +185 -0
- package/package.json +2 -2
- package/wasm/x1_resolve_wasm.wasm +0 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record_count Handle extension (#7384): decode helper for the on-chain
|
|
3
|
+
* live-record counter, plus the instruction builder for the admin-only
|
|
4
|
+
* `backfill_record_count` migration path.
|
|
5
|
+
*
|
|
6
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
7
|
+
* `@solana/web3.js` import (it stays an optional peer). The builder returns
|
|
8
|
+
* the same transport-neutral {@link BuiltInstruction} `delegate.ts` uses.
|
|
9
|
+
*
|
|
10
|
+
* # The extension, mirrored from state.rs's EXTENSION OFFSET REGISTRY
|
|
11
|
+
*
|
|
12
|
+
* A `Handle` account's struct is frozen at 157 bytes (`8 + INIT_SPACE`);
|
|
13
|
+
* features append fixed-offset raw regions past it:
|
|
14
|
+
*
|
|
15
|
+
* offset size region
|
|
16
|
+
* 157 33 NFT capability extension (presence tag + mint pubkey)
|
|
17
|
+
* 190 1 RESERVED lock byte (#7377, unbuilt)
|
|
18
|
+
* 191 5 record_count: presence tag + u32 LE live-record count
|
|
19
|
+
* 196 — next free offset
|
|
20
|
+
*
|
|
21
|
+
* `create_record` increments the count (initializing the extension and
|
|
22
|
+
* growing the account on first use — which is why the record-editing
|
|
23
|
+
* create/close instructions pass the HANDLE account WRITABLE as of this
|
|
24
|
+
* feature; the account LIST is unchanged), `close_record` decrements it
|
|
25
|
+
* (saturating, only when initialized), and `release_handle` refuses while an
|
|
26
|
+
* initialized count is nonzero (`HandleHasRecords`, code 6044).
|
|
27
|
+
*
|
|
28
|
+
* # Uninitialized is NOT zero
|
|
29
|
+
*
|
|
30
|
+
* {@link readRecordCount} returns `null` for a handle whose extension is
|
|
31
|
+
* uninitialized — an account too short (a pre-#7384 handle) or a presence
|
|
32
|
+
* byte still zero (grown for the NFT extension, never counted). Such a
|
|
33
|
+
* handle keeps the OLD unguarded release behaviour until the admin stamps it
|
|
34
|
+
* via `backfill_record_count` or a post-upgrade `create_record` initializes
|
|
35
|
+
* it. A `0` return, by contrast, is an initialized, trustworthy
|
|
36
|
+
* "provably no live records". Never conflate the two: rendering `null` as
|
|
37
|
+
* "0 records" would tell an owner their release is safe when the program
|
|
38
|
+
* cannot actually vouch for that.
|
|
39
|
+
*/
|
|
40
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
41
|
+
/** `8 + Handle::INIT_SPACE` — the frozen base struct size, and the first
|
|
42
|
+
* extension offset. */
|
|
43
|
+
export const HANDLE_BASE_LEN = 157;
|
|
44
|
+
/** Byte offset of the record_count extension: base(157) + NFT ext(33) +
|
|
45
|
+
* reserved lock byte(1). */
|
|
46
|
+
export const RECORD_COUNT_EXT_OFFSET = 191;
|
|
47
|
+
/** presence tag(1) + u32 LE count(4). */
|
|
48
|
+
export const RECORD_COUNT_EXT_LEN = 5;
|
|
49
|
+
/** One past the last known extension region (must match the program's
|
|
50
|
+
* `Handle::KNOWN_EXT_END`) — the size a fully-grown Handle account has under
|
|
51
|
+
* this program version. Layout: base(157) + NFT(33) + reserved(1) +
|
|
52
|
+
* record_count(5) + lock(9) = 205. Was 196 (pre-lock); the #7377 lock extension
|
|
53
|
+
* (see LOCK_EXT_OFFSET=196 / LOCK_EXT_LEN=9 in lock.ts) pushed it to 205, so any
|
|
54
|
+
* consumer allocating/resizing to this constant must use the current value. */
|
|
55
|
+
export const HANDLE_KNOWN_EXT_END = 205;
|
|
56
|
+
/** Anchor instruction discriminator:
|
|
57
|
+
* `sha256("global:backfill_record_count")[0..8]`. Pinned (this SDK is
|
|
58
|
+
* zero-dependency and cannot assume WebCrypto SHA-256 everywhere it runs);
|
|
59
|
+
* asserted against a re-derivation in the test suite so a typo can never
|
|
60
|
+
* silently pass. */
|
|
61
|
+
export const BACKFILL_RECORD_COUNT_DISCRIMINATOR = Uint8Array.from([
|
|
62
|
+
151, 122, 233, 167, 188, 54, 32, 110,
|
|
63
|
+
]);
|
|
64
|
+
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
65
|
+
function toBase58(v, what) {
|
|
66
|
+
if (typeof v === "string") {
|
|
67
|
+
const b = decodeBase58_32(v);
|
|
68
|
+
if (!b)
|
|
69
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
70
|
+
return encodeBase58(b);
|
|
71
|
+
}
|
|
72
|
+
if (v.length !== 32)
|
|
73
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
74
|
+
return encodeBase58(v);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Read the live-record count from a `Handle` account's raw data (exactly
|
|
78
|
+
* what an RPC `getAccountInfo` returns for the `["handle", name]` PDA).
|
|
79
|
+
*
|
|
80
|
+
* Returns the count when the extension is INITIALIZED, and `null` when it is
|
|
81
|
+
* not — see the module docs for why `null` and `0` mean different things and
|
|
82
|
+
* must be rendered differently. Mirrors the program's
|
|
83
|
+
* `Handle::read_record_count` tolerance exactly: a short account and a
|
|
84
|
+
* zeroed region are both `null`, never an error.
|
|
85
|
+
*/
|
|
86
|
+
export function readRecordCount(data) {
|
|
87
|
+
if (data.length < RECORD_COUNT_EXT_OFFSET + RECORD_COUNT_EXT_LEN)
|
|
88
|
+
return null;
|
|
89
|
+
if (data[RECORD_COUNT_EXT_OFFSET] === 0)
|
|
90
|
+
return null;
|
|
91
|
+
return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(RECORD_COUNT_EXT_OFFSET + 1, true);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Build `backfill_record_count` — admin-only: stamp a pre-#7384 handle's
|
|
95
|
+
* record_count extension with the off-chain-computed truth, moving it
|
|
96
|
+
* permanently out of the unguarded-release window. Mainnet never needs it
|
|
97
|
+
* (the mainnet registry starts fresh at cutover, counted from the first
|
|
98
|
+
* record); this is testnet migration + permanent correction tooling.
|
|
99
|
+
*/
|
|
100
|
+
export function buildBackfillRecordCountIx(p) {
|
|
101
|
+
if (!Number.isInteger(p.count) || p.count < 0 || p.count > 0xffffffff) {
|
|
102
|
+
throw new Error("count must be a u32");
|
|
103
|
+
}
|
|
104
|
+
const data = new Uint8Array(8 + 4);
|
|
105
|
+
data.set(BACKFILL_RECORD_COUNT_DISCRIMINATOR, 0);
|
|
106
|
+
new DataView(data.buffer).setUint32(8, p.count, true);
|
|
107
|
+
const keys = [
|
|
108
|
+
{ pubkey: toBase58(p.admin, "admin"), isSigner: true, isWritable: true },
|
|
109
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
|
|
110
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
111
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
112
|
+
];
|
|
113
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
114
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-chain payment records (`Record` accounts) — read WITH the universal
|
|
3
|
+
* staleness rule from docs/record-trust.md structurally enforced.
|
|
4
|
+
*
|
|
5
|
+
* # Why every function here demands `registeredAt`
|
|
6
|
+
*
|
|
7
|
+
* A `Handle`'s address is `["handle", name]` — a pure function of the name.
|
|
8
|
+
* Release + re-register lands the new registration at the SAME pubkey, and
|
|
9
|
+
* `Record` PDAs (`["record", handle_pubkey, coin_type]`) hang off that pubkey,
|
|
10
|
+
* so a record a PREVIOUS owner created (possibly `verified: true` for THEIR
|
|
11
|
+
* address) is physically attached to the new owner's name with no action by
|
|
12
|
+
* anyone. The program cannot prevent this (state.rs, `Handle` doc comment);
|
|
13
|
+
* the mandatory read-side rule is:
|
|
14
|
+
*
|
|
15
|
+
* record.updated_at >= handle.registered_at
|
|
16
|
+
*
|
|
17
|
+
* A record that fails it belongs to a previous, unrelated owner and must be
|
|
18
|
+
* treated as unset/unverified — for PAYMENT resolution, not just badges.
|
|
19
|
+
* There is deliberately no way to decode or fetch a record through this
|
|
20
|
+
* module without the handle's `registered_at` in hand, mirroring
|
|
21
|
+
* app/src/lib/x1/records.ts. See docs/record-trust.md for the full argument.
|
|
22
|
+
*/
|
|
23
|
+
import { type Chain } from "./types.js";
|
|
24
|
+
import type { RpcFn } from "./accounts.js";
|
|
25
|
+
/** `8 + Record::INIT_SPACE` — the program allocates the full 64-byte `value`
|
|
26
|
+
* capacity, so every Record account is exactly this long:
|
|
27
|
+
* 8 + 32 + 4 + (4 + 64) + 1 + 8 + 1. */
|
|
28
|
+
export declare const RECORD_LEN = 122;
|
|
29
|
+
/** `sha256("account:Record")[..8]`, hex — the tag every Record account's
|
|
30
|
+
* data starts with. Pinned (this SDK is zero-dependency and cannot assume
|
|
31
|
+
* WebCrypto SHA-256 everywhere it runs); the value is asserted against a
|
|
32
|
+
* re-derivation in the test suite so a typo can never silently pass. */
|
|
33
|
+
export declare const RECORD_DISC = "fee975fc4ca6928b";
|
|
34
|
+
/** Reverse of CHAIN_COIN_TYPE — which chain a stored coin_type belongs to, or
|
|
35
|
+
* null for a coin_type this SDK does not know how to render. */
|
|
36
|
+
export declare function chainForCoinType(coinType: number): Chain | null;
|
|
37
|
+
/** A decoded per-chain payment record, staleness already judged. */
|
|
38
|
+
export interface HandleRecord {
|
|
39
|
+
/** The Record account's address, base58. */
|
|
40
|
+
readonly account: string;
|
|
41
|
+
/** The Handle account this record hangs off, base58. */
|
|
42
|
+
readonly handle: string;
|
|
43
|
+
readonly coinType: number;
|
|
44
|
+
/** Which chain `coinType` maps to, or null for an unknown coin type. */
|
|
45
|
+
readonly chain: Chain | null;
|
|
46
|
+
/** Raw bytes as stored on-chain. */
|
|
47
|
+
readonly value: Uint8Array;
|
|
48
|
+
/** Human-readable address string — see `valueToAddress`. */
|
|
49
|
+
readonly address: string;
|
|
50
|
+
/**
|
|
51
|
+
* `Record.verified` AS STORED, gated by the staleness rule: `false`
|
|
52
|
+
* whenever the record is `stale`, whatever the account says. A stale
|
|
53
|
+
* `true` is a proof a PREVIOUS owner made; it is never surfaced as a
|
|
54
|
+
* verification of the current owner's address.
|
|
55
|
+
*/
|
|
56
|
+
readonly verified: boolean;
|
|
57
|
+
/** `Record.updated_at`, unix seconds. */
|
|
58
|
+
readonly updatedAt: bigint;
|
|
59
|
+
/**
|
|
60
|
+
* The rule docs/record-trust.md mandates: this record is only the current
|
|
61
|
+
* owner's if `updated_at >= handle.registered_at`. A stale record belongs
|
|
62
|
+
* to a previous, unrelated owner of the same name: it must never be
|
|
63
|
+
* resolved as this name's address, never counted, and only ever offered
|
|
64
|
+
* to the CURRENT owner as something to remove.
|
|
65
|
+
*/
|
|
66
|
+
readonly stale: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Decode one Record account.
|
|
70
|
+
*
|
|
71
|
+
* Record account layout, after the 8-byte Anchor discriminator:
|
|
72
|
+
*
|
|
73
|
+
* handle: Pubkey(32) coin_type: u32(4) value: Vec<u8> (u32 len + bytes, max 64)
|
|
74
|
+
* verified: bool(1) updated_at: i64(8) bump: u8(1)
|
|
75
|
+
*
|
|
76
|
+
* `registeredAt` is the owning Handle's `registered_at`, decoded by the
|
|
77
|
+
* caller from the Handle account — it decides `stale` (and so `verified`).
|
|
78
|
+
* There is intentionally no overload without it.
|
|
79
|
+
*
|
|
80
|
+
* Returns null for anything that is not a Record: wrong length, wrong
|
|
81
|
+
* discriminator, or a `value` length that does not fit. The Listing / Offer /
|
|
82
|
+
* Auction PDAs of the same handle also carry the handle pubkey at offset 8,
|
|
83
|
+
* so a handle-only memcmp scan DOES return them — they are rejected here by
|
|
84
|
+
* tag and size, not by luck.
|
|
85
|
+
*/
|
|
86
|
+
export declare function decodeRecord(raw: Uint8Array, account: string, registeredAt: bigint): HandleRecord | null;
|
|
87
|
+
/** The records the CURRENT owner actually has — `stale` ones excluded. This
|
|
88
|
+
* is the list to resolve against, display to a visitor, and count. */
|
|
89
|
+
export declare function liveRecords(records: readonly HandleRecord[]): HandleRecord[];
|
|
90
|
+
/**
|
|
91
|
+
* Render stored record bytes for display / payment, per the conventions
|
|
92
|
+
* app/src/lib/x1/records.ts writes them with: X1/SOL are 32-byte pubkeys
|
|
93
|
+
* (base58), ETH is 20 raw bytes (0x-hex), BTC is the UTF-8 bytes of the
|
|
94
|
+
* canonical address string. Unknown coin types render as hex so a value is
|
|
95
|
+
* never silently hidden.
|
|
96
|
+
*/
|
|
97
|
+
export declare function valueToAddress(chain: Chain | null, value: Uint8Array): string;
|
|
98
|
+
/**
|
|
99
|
+
* Fetch every Record account of a handle — one `getProgramAccounts` call,
|
|
100
|
+
* filtered by the RPC on size (122), the Record discriminator at offset 0
|
|
101
|
+
* and the handle pubkey at offset 8, then every byte re-checked locally
|
|
102
|
+
* (the node's filters are an optimisation, never the guarantee). Scoping the
|
|
103
|
+
* scan to the registry program id also IS the ownership check: a foreign
|
|
104
|
+
* account cannot appear in it.
|
|
105
|
+
*
|
|
106
|
+
* `registeredAt` is `Handle.registered_at` as decoded from the Handle
|
|
107
|
+
* account the caller already has — the staleness rule needs it, and there
|
|
108
|
+
* is no variant of this function without it. Every record is returned,
|
|
109
|
+
* stale ones flagged (`verified` already forced false), so an owner surface
|
|
110
|
+
* can show what a previous owner left behind; anything that resolves,
|
|
111
|
+
* displays to a visitor, or counts takes `liveRecords(...)`.
|
|
112
|
+
*/
|
|
113
|
+
export declare function fetchRecords(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint): Promise<HandleRecord[]>;
|
package/dist/records.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-chain payment records (`Record` accounts) — read WITH the universal
|
|
3
|
+
* staleness rule from docs/record-trust.md structurally enforced.
|
|
4
|
+
*
|
|
5
|
+
* # Why every function here demands `registeredAt`
|
|
6
|
+
*
|
|
7
|
+
* A `Handle`'s address is `["handle", name]` — a pure function of the name.
|
|
8
|
+
* Release + re-register lands the new registration at the SAME pubkey, and
|
|
9
|
+
* `Record` PDAs (`["record", handle_pubkey, coin_type]`) hang off that pubkey,
|
|
10
|
+
* so a record a PREVIOUS owner created (possibly `verified: true` for THEIR
|
|
11
|
+
* address) is physically attached to the new owner's name with no action by
|
|
12
|
+
* anyone. The program cannot prevent this (state.rs, `Handle` doc comment);
|
|
13
|
+
* the mandatory read-side rule is:
|
|
14
|
+
*
|
|
15
|
+
* record.updated_at >= handle.registered_at
|
|
16
|
+
*
|
|
17
|
+
* A record that fails it belongs to a previous, unrelated owner and must be
|
|
18
|
+
* treated as unset/unverified — for PAYMENT resolution, not just badges.
|
|
19
|
+
* There is deliberately no way to decode or fetch a record through this
|
|
20
|
+
* module without the handle's `registered_at` in hand, mirroring
|
|
21
|
+
* app/src/lib/x1/records.ts. See docs/record-trust.md for the full argument.
|
|
22
|
+
*/
|
|
23
|
+
import { encodeBase58 } from "./base58.js";
|
|
24
|
+
import { CHAIN_COIN_TYPE } from "./types.js";
|
|
25
|
+
/** `8 + Record::INIT_SPACE` — the program allocates the full 64-byte `value`
|
|
26
|
+
* capacity, so every Record account is exactly this long:
|
|
27
|
+
* 8 + 32 + 4 + (4 + 64) + 1 + 8 + 1. */
|
|
28
|
+
export const RECORD_LEN = 122;
|
|
29
|
+
/** `sha256("account:Record")[..8]`, hex — the tag every Record account's
|
|
30
|
+
* data starts with. Pinned (this SDK is zero-dependency and cannot assume
|
|
31
|
+
* WebCrypto SHA-256 everywhere it runs); the value is asserted against a
|
|
32
|
+
* re-derivation in the test suite so a typo can never silently pass. */
|
|
33
|
+
export const RECORD_DISC = "fee975fc4ca6928b";
|
|
34
|
+
/** Byte offset of `handle` within a Record account: the 8-byte discriminator. */
|
|
35
|
+
const RECORD_HANDLE_OFFSET = 8;
|
|
36
|
+
/** Reverse of CHAIN_COIN_TYPE — which chain a stored coin_type belongs to, or
|
|
37
|
+
* null for a coin_type this SDK does not know how to render. */
|
|
38
|
+
export function chainForCoinType(coinType) {
|
|
39
|
+
for (const c of Object.keys(CHAIN_COIN_TYPE)) {
|
|
40
|
+
if (CHAIN_COIN_TYPE[c] === coinType)
|
|
41
|
+
return c;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Decode one Record account.
|
|
47
|
+
*
|
|
48
|
+
* Record account layout, after the 8-byte Anchor discriminator:
|
|
49
|
+
*
|
|
50
|
+
* handle: Pubkey(32) coin_type: u32(4) value: Vec<u8> (u32 len + bytes, max 64)
|
|
51
|
+
* verified: bool(1) updated_at: i64(8) bump: u8(1)
|
|
52
|
+
*
|
|
53
|
+
* `registeredAt` is the owning Handle's `registered_at`, decoded by the
|
|
54
|
+
* caller from the Handle account — it decides `stale` (and so `verified`).
|
|
55
|
+
* There is intentionally no overload without it.
|
|
56
|
+
*
|
|
57
|
+
* Returns null for anything that is not a Record: wrong length, wrong
|
|
58
|
+
* discriminator, or a `value` length that does not fit. The Listing / Offer /
|
|
59
|
+
* Auction PDAs of the same handle also carry the handle pubkey at offset 8,
|
|
60
|
+
* so a handle-only memcmp scan DOES return them — they are rejected here by
|
|
61
|
+
* tag and size, not by luck.
|
|
62
|
+
*/
|
|
63
|
+
export function decodeRecord(raw, account, registeredAt) {
|
|
64
|
+
if (raw.length !== RECORD_LEN)
|
|
65
|
+
return null;
|
|
66
|
+
for (let i = 0; i < 8; i++) {
|
|
67
|
+
if (raw[i] !== parseInt(RECORD_DISC.slice(i * 2, i * 2 + 2), 16))
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
71
|
+
let o = 8;
|
|
72
|
+
const handle = encodeBase58(raw.slice(o, o + 32));
|
|
73
|
+
o += 32;
|
|
74
|
+
const coinType = dv.getUint32(o, true);
|
|
75
|
+
o += 4;
|
|
76
|
+
const len = dv.getUint32(o, true);
|
|
77
|
+
o += 4;
|
|
78
|
+
// `#[max_len(64)]` — and the fixed fields after `value` must still fit.
|
|
79
|
+
if (len > 64 || o + len + 1 + 8 + 1 > raw.length)
|
|
80
|
+
return null;
|
|
81
|
+
const value = raw.slice(o, o + len);
|
|
82
|
+
o += len;
|
|
83
|
+
const verified = raw[o] === 1;
|
|
84
|
+
o += 1;
|
|
85
|
+
const updatedAt = dv.getBigInt64(o, true);
|
|
86
|
+
const stale = updatedAt < registeredAt;
|
|
87
|
+
const chain = chainForCoinType(coinType);
|
|
88
|
+
return {
|
|
89
|
+
account,
|
|
90
|
+
handle,
|
|
91
|
+
coinType,
|
|
92
|
+
chain,
|
|
93
|
+
value,
|
|
94
|
+
address: valueToAddress(chain, value),
|
|
95
|
+
verified: verified && !stale,
|
|
96
|
+
updatedAt,
|
|
97
|
+
stale,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** The records the CURRENT owner actually has — `stale` ones excluded. This
|
|
101
|
+
* is the list to resolve against, display to a visitor, and count. */
|
|
102
|
+
export function liveRecords(records) {
|
|
103
|
+
return records.filter((r) => !r.stale);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Render stored record bytes for display / payment, per the conventions
|
|
107
|
+
* app/src/lib/x1/records.ts writes them with: X1/SOL are 32-byte pubkeys
|
|
108
|
+
* (base58), ETH is 20 raw bytes (0x-hex), BTC is the UTF-8 bytes of the
|
|
109
|
+
* canonical address string. Unknown coin types render as hex so a value is
|
|
110
|
+
* never silently hidden.
|
|
111
|
+
*/
|
|
112
|
+
export function valueToAddress(chain, value) {
|
|
113
|
+
const hex = () => `0x${Array.from(value, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
114
|
+
if (chain === "X1" || chain === "SOL") {
|
|
115
|
+
return value.length === 32 ? encodeBase58(value) : hex();
|
|
116
|
+
}
|
|
117
|
+
if (chain === "ETH")
|
|
118
|
+
return hex();
|
|
119
|
+
if (chain === "BTC") {
|
|
120
|
+
try {
|
|
121
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(value);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return hex();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return hex();
|
|
128
|
+
}
|
|
129
|
+
function hexToBytes(hex) {
|
|
130
|
+
const out = new Uint8Array(hex.length / 2);
|
|
131
|
+
for (let i = 0; i < out.length; i++)
|
|
132
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Fetch every Record account of a handle — one `getProgramAccounts` call,
|
|
137
|
+
* filtered by the RPC on size (122), the Record discriminator at offset 0
|
|
138
|
+
* and the handle pubkey at offset 8, then every byte re-checked locally
|
|
139
|
+
* (the node's filters are an optimisation, never the guarantee). Scoping the
|
|
140
|
+
* scan to the registry program id also IS the ownership check: a foreign
|
|
141
|
+
* account cannot appear in it.
|
|
142
|
+
*
|
|
143
|
+
* `registeredAt` is `Handle.registered_at` as decoded from the Handle
|
|
144
|
+
* account the caller already has — the staleness rule needs it, and there
|
|
145
|
+
* is no variant of this function without it. Every record is returned,
|
|
146
|
+
* stale ones flagged (`verified` already forced false), so an owner surface
|
|
147
|
+
* can show what a previous owner left behind; anything that resolves,
|
|
148
|
+
* displays to a visitor, or counts takes `liveRecords(...)`.
|
|
149
|
+
*/
|
|
150
|
+
export async function fetchRecords(rpc, programId, handleAccount, registeredAt) {
|
|
151
|
+
const res = (await rpc("getProgramAccounts", [
|
|
152
|
+
programId,
|
|
153
|
+
{
|
|
154
|
+
encoding: "base64",
|
|
155
|
+
commitment: "confirmed",
|
|
156
|
+
filters: [
|
|
157
|
+
{ dataSize: RECORD_LEN },
|
|
158
|
+
{ memcmp: { offset: 0, bytes: encodeBase58(hexToBytes(RECORD_DISC)) } },
|
|
159
|
+
{ memcmp: { offset: RECORD_HANDLE_OFFSET, bytes: handleAccount } },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
]));
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const a of res ?? []) {
|
|
165
|
+
const bin = atob(a.account.data[0]);
|
|
166
|
+
const raw = new Uint8Array(bin.length);
|
|
167
|
+
for (let i = 0; i < bin.length; i++)
|
|
168
|
+
raw[i] = bin.charCodeAt(i);
|
|
169
|
+
const r = decodeRecord(raw, a.pubkey, registeredAt);
|
|
170
|
+
// Defence in depth: the memcmp filter should guarantee the handle match,
|
|
171
|
+
// but a wrong offset would silently attribute someone else's record to
|
|
172
|
+
// this handle. A malformed account is skipped, not fatal to the list.
|
|
173
|
+
if (r && r.handle === handleAccount)
|
|
174
|
+
out.push(r);
|
|
175
|
+
}
|
|
176
|
+
out.sort((x, y) => (x.chain ?? "").localeCompare(y.chain ?? "") || x.coinType - y.coinType);
|
|
177
|
+
return out;
|
|
178
|
+
}
|