@x1id/resolve 0.2.1 → 0.6.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.
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Write builders for the address `Record` family: `create_record` and the
3
+ * three `verify_record_*` proof-of-control instructions (ETH/secp256k1,
4
+ * SVM/ed25519, BTC/BIP-137). `records.ts` was read-only until now — every
5
+ * other record family (text records, attestations, vouchers, subnames...)
6
+ * already had write builders; this was the one gap (WP #8142).
7
+ *
8
+ * Hand-rolled like the rest of this package — no Anchor client, no
9
+ * `@solana/web3.js` import. PDAs are NOT derived here (see delegate.ts's
10
+ * module docs for why) — derive `["record", handlePda, coinTypeLE(4)]`
11
+ * with your runtime's canonical `findProgramAddress`.
12
+ *
13
+ * # What this SDK does and does not do for you
14
+ *
15
+ * This does NOT sign anything, derive an address from a signature, or
16
+ * verify a signature client-side — that's the on-chain program's job
17
+ * (`verify_record_eth`/`svm`/`btc` recover the signer's address from the
18
+ * signature itself and compare it to the record's claimed value; a client
19
+ * proving nothing false cannot fabricate a match). What this module DOES do:
20
+ *
21
+ * 1. {@link buildRecordChallenge} — build the exact bytes a wallet must
22
+ * sign, byte-identical to `handle-normalize`'s Rust `challenge()`
23
+ * (verified: `crates/handle-normalize/src/challenge.rs`). Unlike the
24
+ * off-chain control-proof flow (`control.ts`), this challenge's nonce
25
+ * does NOT need to be server-issued — the on-chain transaction landing
26
+ * IS the single-use event, so any ASCII-alphanumeric nonce (1..=64
27
+ * chars) the caller picks is fine.
28
+ * 2. {@link splitEthSignature} / {@link splitBtcSignature} — repackage what
29
+ * a wallet's `personal_sign` (ETH) or `signmessage` (BTC) actually
30
+ * returns into the exact args each verify instruction expects.
31
+ * 3. The four instruction builders themselves.
32
+ *
33
+ * # ETH: EIP-191 `personal_sign`, not EIP-712
34
+ *
35
+ * `verify_record_eth` (lib.rs) hashes the challenge with EIP-191's
36
+ * `"\x19Ethereum Signed Message:\n" + len + message` framing — the exact
37
+ * framing every wallet's `personal_sign` / `eth_sign` RPC method produces.
38
+ * There is no EIP-712 typed-data variant on-chain to build a client for.
39
+ *
40
+ * # BTC: mainnet-only, compressed P2PKH or bech32/P2WPKH only
41
+ *
42
+ * `verify_record_btc` derives the address FROM the recovered pubkey (never
43
+ * trusts a client-supplied address) and only accepts the header-byte ranges
44
+ * for compressed P2PKH (31-34) and bech32/P2WPKH (39-42) — see `btc.rs`'s
45
+ * module doc for exactly why uncompressed P2PKH and P2SH-segwit are refused
46
+ * rather than guessed. {@link splitBtcSignature} passes the header byte
47
+ * through unchanged; it is the wallet's job to produce one of the two
48
+ * supported ranges (most current wallets do).
49
+ */
50
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
51
+ export declare const CREATE_RECORD_DISCRIMINATOR: Uint8Array;
52
+ export declare const VERIFY_RECORD_ETH_DISCRIMINATOR: Uint8Array;
53
+ export declare const VERIFY_RECORD_SVM_DISCRIMINATOR: Uint8Array;
54
+ export declare const VERIFY_RECORD_BTC_DISCRIMINATOR: Uint8Array;
55
+ /** The delegate-aware trailing account every verify builder shares — same
56
+ * rule as `textRecords.ts`'s identically-named private helper (not shared
57
+ * across files by import: each write-builder module owns its own copy,
58
+ * matching this SDK's existing convention). */
59
+ interface RecordEditAuthorityParams {
60
+ readonly recordDelegate?: AddressLike;
61
+ readonly holderTokenAccount?: AddressLike;
62
+ }
63
+ /**
64
+ * `x1-handles:v1:<handle>:<coinType>:<registeredAt>:<nonce>` — byte-identical
65
+ * to `handle_normalize::challenge` (Rust). `handle` must already be
66
+ * canonical (pass it through {@link import("./parse.js").normalizeHandle}
67
+ * first if it might have a leading `@` or mixed case).
68
+ */
69
+ export declare function buildRecordChallenge(handle: string, coinType: number, registeredAt: bigint, nonce: string): string;
70
+ /** Split a 65-byte ETH `personal_sign` output (`r(32) || s(32) || v(1)`)
71
+ * into `verify_record_eth`'s `{signature: [u8;64], recoveryId: u8}`,
72
+ * normalizing `v`'s two common conventions (27/28, matching most wallets'
73
+ * raw RPC output, or already 0/1). */
74
+ export declare function splitEthSignature(sig65: Uint8Array): {
75
+ signature: Uint8Array;
76
+ recoveryId: number;
77
+ };
78
+ /** Split a 65-byte BTC BIP-137 `signmessage` output (`header(1) || r(32) ||
79
+ * s(32)`) into `verify_record_btc`'s `{header, signature: [u8;64]}`. The
80
+ * header byte is passed through unchanged — see the module docs for which
81
+ * ranges `verify_record_btc` accepts. */
82
+ export declare function splitBtcSignature(sig65: Uint8Array): {
83
+ header: number;
84
+ signature: Uint8Array;
85
+ };
86
+ export interface CreateRecordParams {
87
+ readonly programId: AddressLike;
88
+ readonly payer: AddressLike;
89
+ readonly owner: AddressLike;
90
+ readonly handle: AddressLike;
91
+ /** The `["record", handle, coinTypeLE(4)]` PDA. */
92
+ readonly record: AddressLike;
93
+ readonly coinType: number;
94
+ /** Raw address bytes (1..=64), NOT verified yet — `verified` starts
95
+ * false until a matching `verify_record_*` call succeeds. */
96
+ readonly value: Uint8Array;
97
+ }
98
+ export declare function buildCreateRecordIx(p: CreateRecordParams): BuiltInstruction;
99
+ export interface VerifyRecordEthParams extends RecordEditAuthorityParams {
100
+ readonly programId: AddressLike;
101
+ readonly owner: AddressLike;
102
+ readonly handle: AddressLike;
103
+ readonly record: AddressLike;
104
+ readonly nonce: string;
105
+ /** The 64-byte `r || s` half — use {@link splitEthSignature} on a
106
+ * wallet's raw 65-byte `personal_sign` output. */
107
+ readonly signature: Uint8Array;
108
+ readonly recoveryId: number;
109
+ }
110
+ export declare function buildVerifyRecordEthIx(p: VerifyRecordEthParams): BuiltInstruction;
111
+ export interface VerifyRecordSvmParams extends RecordEditAuthorityParams {
112
+ readonly programId: AddressLike;
113
+ readonly owner: AddressLike;
114
+ /** The address being claimed — MUST co-sign this transaction; its
115
+ * signature IS the proof, no challenge/nonce needed (X1 and Solana
116
+ * share ed25519). */
117
+ readonly claimed: AddressLike;
118
+ readonly handle: AddressLike;
119
+ readonly record: AddressLike;
120
+ }
121
+ export declare function buildVerifyRecordSvmIx(p: VerifyRecordSvmParams): BuiltInstruction;
122
+ export interface VerifyRecordBtcParams extends RecordEditAuthorityParams {
123
+ readonly programId: AddressLike;
124
+ readonly owner: AddressLike;
125
+ readonly handle: AddressLike;
126
+ readonly record: AddressLike;
127
+ readonly nonce: string;
128
+ /** BIP-137 header byte — use {@link splitBtcSignature} on a wallet's raw
129
+ * 65-byte `signmessage` output. Only 31-34 (compressed P2PKH) and 39-42
130
+ * (bech32/P2WPKH) are accepted on-chain; see the module docs. */
131
+ readonly header: number;
132
+ /** The 64-byte `r || s` half. */
133
+ readonly signature: Uint8Array;
134
+ }
135
+ export declare function buildVerifyRecordBtcIx(p: VerifyRecordBtcParams): BuiltInstruction;
136
+ export {};
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Write builders for the address `Record` family: `create_record` and the
3
+ * three `verify_record_*` proof-of-control instructions (ETH/secp256k1,
4
+ * SVM/ed25519, BTC/BIP-137). `records.ts` was read-only until now — every
5
+ * other record family (text records, attestations, vouchers, subnames...)
6
+ * already had write builders; this was the one gap (WP #8142).
7
+ *
8
+ * Hand-rolled like the rest of this package — no Anchor client, no
9
+ * `@solana/web3.js` import. PDAs are NOT derived here (see delegate.ts's
10
+ * module docs for why) — derive `["record", handlePda, coinTypeLE(4)]`
11
+ * with your runtime's canonical `findProgramAddress`.
12
+ *
13
+ * # What this SDK does and does not do for you
14
+ *
15
+ * This does NOT sign anything, derive an address from a signature, or
16
+ * verify a signature client-side — that's the on-chain program's job
17
+ * (`verify_record_eth`/`svm`/`btc` recover the signer's address from the
18
+ * signature itself and compare it to the record's claimed value; a client
19
+ * proving nothing false cannot fabricate a match). What this module DOES do:
20
+ *
21
+ * 1. {@link buildRecordChallenge} — build the exact bytes a wallet must
22
+ * sign, byte-identical to `handle-normalize`'s Rust `challenge()`
23
+ * (verified: `crates/handle-normalize/src/challenge.rs`). Unlike the
24
+ * off-chain control-proof flow (`control.ts`), this challenge's nonce
25
+ * does NOT need to be server-issued — the on-chain transaction landing
26
+ * IS the single-use event, so any ASCII-alphanumeric nonce (1..=64
27
+ * chars) the caller picks is fine.
28
+ * 2. {@link splitEthSignature} / {@link splitBtcSignature} — repackage what
29
+ * a wallet's `personal_sign` (ETH) or `signmessage` (BTC) actually
30
+ * returns into the exact args each verify instruction expects.
31
+ * 3. The four instruction builders themselves.
32
+ *
33
+ * # ETH: EIP-191 `personal_sign`, not EIP-712
34
+ *
35
+ * `verify_record_eth` (lib.rs) hashes the challenge with EIP-191's
36
+ * `"\x19Ethereum Signed Message:\n" + len + message` framing — the exact
37
+ * framing every wallet's `personal_sign` / `eth_sign` RPC method produces.
38
+ * There is no EIP-712 typed-data variant on-chain to build a client for.
39
+ *
40
+ * # BTC: mainnet-only, compressed P2PKH or bech32/P2WPKH only
41
+ *
42
+ * `verify_record_btc` derives the address FROM the recovered pubkey (never
43
+ * trusts a client-supplied address) and only accepts the header-byte ranges
44
+ * for compressed P2PKH (31-34) and bech32/P2WPKH (39-42) — see `btc.rs`'s
45
+ * module doc for exactly why uncompressed P2PKH and P2SH-segwit are refused
46
+ * rather than guessed. {@link splitBtcSignature} passes the header byte
47
+ * through unchanged; it is the wallet's job to produce one of the two
48
+ * supported ranges (most current wallets do).
49
+ */
50
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
51
+ import { recordDelegateNonePlaceholder, recordDelegateSomeSlot } from "./delegate.js";
52
+ import { normalizeHandle } from "./parse.js";
53
+ import { ResolveError } from "./types.js";
54
+ /** Matches `control.ts`'s identically-named private helper: STRICT
55
+ * canonical-form check (reject, never silently transform), mirroring the
56
+ * Rust `is_canonical` the challenge builder must match exactly —
57
+ * `normalizeHandle` alone would accept `"@Alice"` by transforming it,
58
+ * which `handle_normalize::challenge` (Rust) does NOT: it demands
59
+ * already-canonical input and rejects anything else outright. */
60
+ function isCanonicalHandle(handle) {
61
+ try {
62
+ return normalizeHandle(handle) === handle;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ export const CREATE_RECORD_DISCRIMINATOR = Uint8Array.from([
69
+ 116, 124, 63, 58, 126, 204, 178, 10,
70
+ ]);
71
+ export const VERIFY_RECORD_ETH_DISCRIMINATOR = Uint8Array.from([
72
+ 57, 157, 98, 0, 160, 210, 94, 234,
73
+ ]);
74
+ export const VERIFY_RECORD_SVM_DISCRIMINATOR = Uint8Array.from([
75
+ 234, 151, 82, 212, 185, 112, 234, 212,
76
+ ]);
77
+ export const VERIFY_RECORD_BTC_DISCRIMINATOR = Uint8Array.from([
78
+ 54, 50, 183, 232, 122, 200, 194, 64,
79
+ ]);
80
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
81
+ const NONCE_RE = /^[0-9A-Za-z]{1,64}$/;
82
+ function toBytes32(v, what) {
83
+ if (typeof v === "string") {
84
+ const b = decodeBase58_32(v);
85
+ if (!b)
86
+ throw new Error(`${what} is not a valid base58 address`);
87
+ return b;
88
+ }
89
+ if (v.length !== 32)
90
+ throw new Error(`${what} must be exactly 32 bytes`);
91
+ return v;
92
+ }
93
+ function toBase58(v, what) {
94
+ return encodeBase58(toBytes32(v, what));
95
+ }
96
+ function encVec(bytes) {
97
+ const out = new Uint8Array(4 + bytes.length);
98
+ new DataView(out.buffer).setUint32(0, bytes.length, true);
99
+ out.set(bytes, 4);
100
+ return out;
101
+ }
102
+ function encString(s) {
103
+ return encVec(new TextEncoder().encode(s));
104
+ }
105
+ function pushAuthorityTail(keys, programId, p) {
106
+ if (p.recordDelegate !== undefined) {
107
+ keys.push(recordDelegateSomeSlot(p.recordDelegate));
108
+ }
109
+ else if (p.holderTokenAccount !== undefined) {
110
+ keys.push(recordDelegateNonePlaceholder(programId));
111
+ }
112
+ if (p.holderTokenAccount !== undefined) {
113
+ keys.push({ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"), isSigner: false, isWritable: false });
114
+ }
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // Challenge
118
+ // ---------------------------------------------------------------------------
119
+ /**
120
+ * `x1-handles:v1:<handle>:<coinType>:<registeredAt>:<nonce>` — byte-identical
121
+ * to `handle_normalize::challenge` (Rust). `handle` must already be
122
+ * canonical (pass it through {@link import("./parse.js").normalizeHandle}
123
+ * first if it might have a leading `@` or mixed case).
124
+ */
125
+ export function buildRecordChallenge(handle, coinType, registeredAt, nonce) {
126
+ if (!isCanonicalHandle(handle)) {
127
+ throw new ResolveError("invalid-handle", `"${handle}" is not a canonical handle`, handle);
128
+ }
129
+ if (!NONCE_RE.test(nonce)) {
130
+ throw new Error("nonce must be 1..=64 ASCII alphanumeric characters");
131
+ }
132
+ return `x1-handles:v1:${handle}:${coinType}:${registeredAt}:${nonce}`;
133
+ }
134
+ /** Split a 65-byte ETH `personal_sign` output (`r(32) || s(32) || v(1)`)
135
+ * into `verify_record_eth`'s `{signature: [u8;64], recoveryId: u8}`,
136
+ * normalizing `v`'s two common conventions (27/28, matching most wallets'
137
+ * raw RPC output, or already 0/1). */
138
+ export function splitEthSignature(sig65) {
139
+ if (sig65.length !== 65)
140
+ throw new Error("ETH signature must be exactly 65 bytes (r || s || v)");
141
+ const v = sig65[64];
142
+ const recoveryId = v >= 27 ? v - 27 : v;
143
+ if (recoveryId !== 0 && recoveryId !== 1) {
144
+ throw new Error(`unrecognized recovery id byte: ${v}`);
145
+ }
146
+ return { signature: sig65.slice(0, 64), recoveryId };
147
+ }
148
+ /** Split a 65-byte BTC BIP-137 `signmessage` output (`header(1) || r(32) ||
149
+ * s(32)`) into `verify_record_btc`'s `{header, signature: [u8;64]}`. The
150
+ * header byte is passed through unchanged — see the module docs for which
151
+ * ranges `verify_record_btc` accepts. */
152
+ export function splitBtcSignature(sig65) {
153
+ if (sig65.length !== 65)
154
+ throw new Error("BTC signature must be exactly 65 bytes (header || r || s)");
155
+ return { header: sig65[0], signature: sig65.slice(1) };
156
+ }
157
+ export function buildCreateRecordIx(p) {
158
+ if (p.value.length === 0 || p.value.length > 64) {
159
+ throw new Error("value must be 1..=64 bytes");
160
+ }
161
+ const coinTypeBytes = new Uint8Array(4);
162
+ new DataView(coinTypeBytes.buffer).setUint32(0, p.coinType, true);
163
+ const data = new Uint8Array(8 + 4 + 4 + p.value.length);
164
+ data.set(CREATE_RECORD_DISCRIMINATOR, 0);
165
+ data.set(coinTypeBytes, 8);
166
+ data.set(encVec(p.value), 12);
167
+ const keys = [
168
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
169
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
170
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
171
+ { pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
172
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
173
+ ];
174
+ return { programId: toBase58(p.programId, "programId"), keys, data };
175
+ }
176
+ export function buildVerifyRecordEthIx(p) {
177
+ if (p.signature.length !== 64)
178
+ throw new Error("signature must be exactly 64 bytes (r || s)");
179
+ if (p.recoveryId !== 0 && p.recoveryId !== 1)
180
+ throw new Error("recoveryId must be 0 or 1");
181
+ if (!NONCE_RE.test(p.nonce))
182
+ throw new Error("nonce must be 1..=64 ASCII alphanumeric characters");
183
+ const nonceEnc = encString(p.nonce);
184
+ const data = new Uint8Array(8 + nonceEnc.length + 64 + 1);
185
+ data.set(VERIFY_RECORD_ETH_DISCRIMINATOR, 0);
186
+ data.set(nonceEnc, 8);
187
+ data.set(p.signature, 8 + nonceEnc.length);
188
+ data[8 + nonceEnc.length + 64] = p.recoveryId;
189
+ const keys = [
190
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
191
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
192
+ { pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
193
+ ];
194
+ pushAuthorityTail(keys, p.programId, p);
195
+ return { programId: toBase58(p.programId, "programId"), keys, data };
196
+ }
197
+ export function buildVerifyRecordSvmIx(p) {
198
+ const keys = [
199
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
200
+ { pubkey: toBase58(p.claimed, "claimed"), isSigner: true, isWritable: false },
201
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
202
+ { pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
203
+ ];
204
+ pushAuthorityTail(keys, p.programId, p);
205
+ return { programId: toBase58(p.programId, "programId"), keys, data: VERIFY_RECORD_SVM_DISCRIMINATOR.slice() };
206
+ }
207
+ export function buildVerifyRecordBtcIx(p) {
208
+ if (p.signature.length !== 64)
209
+ throw new Error("signature must be exactly 64 bytes (r || s)");
210
+ if (!Number.isInteger(p.header) || p.header < 0 || p.header > 255) {
211
+ throw new Error("header must be a single byte (0-255)");
212
+ }
213
+ if (!NONCE_RE.test(p.nonce))
214
+ throw new Error("nonce must be 1..=64 ASCII alphanumeric characters");
215
+ const nonceEnc = encString(p.nonce);
216
+ const data = new Uint8Array(8 + nonceEnc.length + 65);
217
+ data.set(VERIFY_RECORD_BTC_DISCRIMINATOR, 0);
218
+ data.set(nonceEnc, 8);
219
+ data[8 + nonceEnc.length] = p.header;
220
+ data.set(p.signature, 8 + nonceEnc.length + 1);
221
+ const keys = [
222
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
223
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
224
+ { pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
225
+ ];
226
+ pushAuthorityTail(keys, p.programId, p);
227
+ return { programId: toBase58(p.programId, "programId"), keys, data };
228
+ }
@@ -0,0 +1,130 @@
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
+ * # `records_cleared_at` (#8139) — the SECOND, independent staleness rule
24
+ *
25
+ * `clear_records` lets an owner bulk-invalidate every record RIGHT NOW,
26
+ * without a transfer. The same functions therefore also demand the handle's
27
+ * `recordsClearedAt` (0 if never cleared, from `ParsedHandle`) and a record
28
+ * is `stale` when EITHER `updated_at < registeredAt` OR
29
+ * `updated_at < recordsClearedAt` — see `Handle::read_records_cleared_at`'s
30
+ * doc comment in state.rs.
31
+ */
32
+ import { type Chain } from "./types.js";
33
+ import type { RpcFn } from "./accounts.js";
34
+ /** `8 + Record::INIT_SPACE` — the program allocates the full 64-byte `value`
35
+ * capacity, so every Record account is exactly this long:
36
+ * 8 + 32 + 4 + (4 + 64) + 1 + 8 + 1. */
37
+ export declare const RECORD_LEN = 122;
38
+ /** `sha256("account:Record")[..8]`, hex — the tag every Record account's
39
+ * data starts with. Pinned (this SDK is zero-dependency and cannot assume
40
+ * WebCrypto SHA-256 everywhere it runs); the value is asserted against a
41
+ * re-derivation in the test suite so a typo can never silently pass. */
42
+ export declare const RECORD_DISC = "fee975fc4ca6928b";
43
+ /** Reverse of CHAIN_COIN_TYPE — which chain a stored coin_type belongs to, or
44
+ * null for a coin_type this SDK does not know how to render. */
45
+ export declare function chainForCoinType(coinType: number): Chain | null;
46
+ /** A decoded per-chain payment record, staleness already judged. */
47
+ export interface HandleRecord {
48
+ /** The Record account's address, base58. */
49
+ readonly account: string;
50
+ /** The Handle account this record hangs off, base58. */
51
+ readonly handle: string;
52
+ readonly coinType: number;
53
+ /** Which chain `coinType` maps to, or null for an unknown coin type. */
54
+ readonly chain: Chain | null;
55
+ /** Raw bytes as stored on-chain. */
56
+ readonly value: Uint8Array;
57
+ /** Human-readable address string — see `valueToAddress`. */
58
+ readonly address: string;
59
+ /**
60
+ * `Record.verified` AS STORED, gated by the staleness rule: `false`
61
+ * whenever the record is `stale`, whatever the account says. A stale
62
+ * `true` is a proof a PREVIOUS owner made; it is never surfaced as a
63
+ * verification of the current owner's address.
64
+ */
65
+ readonly verified: boolean;
66
+ /** `Record.updated_at`, unix seconds. */
67
+ readonly updatedAt: bigint;
68
+ /**
69
+ * The rule docs/record-trust.md mandates: this record is only the current
70
+ * owner's if `updated_at >= handle.registered_at`. A stale record belongs
71
+ * to a previous, unrelated owner of the same name: it must never be
72
+ * resolved as this name's address, never counted, and only ever offered
73
+ * to the CURRENT owner as something to remove.
74
+ */
75
+ readonly stale: boolean;
76
+ }
77
+ /**
78
+ * Decode one Record account.
79
+ *
80
+ * Record account layout, after the 8-byte Anchor discriminator:
81
+ *
82
+ * handle: Pubkey(32) coin_type: u32(4) value: Vec<u8> (u32 len + bytes, max 64)
83
+ * verified: bool(1) updated_at: i64(8) bump: u8(1)
84
+ *
85
+ * `registeredAt` is the owning Handle's `registered_at`, decoded by the
86
+ * caller from the Handle account — it decides `stale` (and so `verified`).
87
+ * `recordsClearedAt` is the same Handle's `recordsClearedAt` (0 if never
88
+ * cleared, #8139) — a SECOND, independent staleness anchor, checked
89
+ * alongside (not instead of) `registeredAt`. There is intentionally no
90
+ * overload without either.
91
+ *
92
+ * Returns null for anything that is not a Record: wrong length, wrong
93
+ * discriminator, or a `value` length that does not fit. The Listing / Offer /
94
+ * Auction PDAs of the same handle also carry the handle pubkey at offset 8,
95
+ * so a handle-only memcmp scan DOES return them — they are rejected here by
96
+ * tag and size, not by luck.
97
+ */
98
+ export declare function decodeRecord(raw: Uint8Array, account: string, registeredAt: bigint, recordsClearedAt: bigint): HandleRecord | null;
99
+ /** The records the CURRENT owner actually has — `stale` ones excluded. This
100
+ * is the list to resolve against, display to a visitor, and count. */
101
+ export declare function liveRecords(records: readonly HandleRecord[]): HandleRecord[];
102
+ /**
103
+ * Render stored record bytes for display / payment, per the conventions
104
+ * app/src/lib/x1/records.ts writes them with: X1/SOL are 32-byte pubkeys
105
+ * (base58), ETH is 20 raw bytes (0x-hex), BTC is the UTF-8 bytes of the
106
+ * canonical address string. Unknown coin types render as hex so a value is
107
+ * never silently hidden.
108
+ */
109
+ export declare function valueToAddress(chain: Chain | null, value: Uint8Array): string;
110
+ /**
111
+ * Fetch every Record account of a handle — one `getProgramAccounts` call,
112
+ * filtered by the RPC on size (122), the Record discriminator at offset 0
113
+ * and the handle pubkey at offset 8, then every byte re-checked locally
114
+ * (the node's filters are an optimisation, never the guarantee). Scoping the
115
+ * scan to the registry program id also IS the ownership check: a foreign
116
+ * account cannot appear in it.
117
+ *
118
+ * `registeredAt` is `Handle.registered_at` as decoded from the Handle
119
+ * account the caller already has — the staleness rule needs it, and there
120
+ * is no variant of this function without it. `recordsClearedAt` is that same
121
+ * Handle's `recordsClearedAt` (0 if never cleared, #8139) — pass it, not a
122
+ * literal 0, unless the records being fetched genuinely hang off a DIFFERENT
123
+ * account than the one `clear_records` could ever touch (e.g. a subname's
124
+ * own records, keyed by the subname's `createdAt` instead — see
125
+ * `subname.ts`). Every record is returned, stale ones flagged (`verified`
126
+ * already forced false), so an owner surface can show what a previous owner
127
+ * left behind; anything that resolves, displays to a visitor, or counts
128
+ * takes `liveRecords(...)`.
129
+ */
130
+ export declare function fetchRecords(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint): Promise<HandleRecord[]>;