@x1id/resolve 0.3.0 → 0.7.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/dist/index.js CHANGED
@@ -38,6 +38,21 @@ export * from "./textRecords.js";
38
38
  export * from "./lock.js";
39
39
  export * from "./integrator.js";
40
40
  export * from "./voucher.js";
41
+ export * from "./agent.js";
42
+ export * from "./register.js";
43
+ // Was previously imported here for internal use only, never re-exported —
44
+ // promoted to public API 2026-09-22 because a second real package (mcp/)
45
+ // now needs `parseHandleAccount`/`RpcFn` directly rather than duplicating
46
+ // this decoder (the Handle account's `Option<Pubkey>` fields are
47
+ // variable-width Borsh — a hand-rolled offset guess here would be exactly
48
+ // the kind of drift docs/record-trust.md's "one implementation" rule
49
+ // exists to prevent).
50
+ export * from "./accounts.js";
51
+ export * from "./x402.js";
52
+ export * from "./recordWrite.js";
53
+ export * from "./clearRecords.js";
54
+ export * from "./commitReveal.js";
55
+ export * from "./pnftTransfer.js";
41
56
  import { ResolveError, CHAIN_COIN_TYPE } from "./types.js";
42
57
  import { parseName } from "./parse.js";
43
58
  import { encodeBase58, decodeBase58_32 } from "./base58.js";
@@ -136,7 +151,7 @@ export function createResolver(config) {
136
151
  // behind by a previous registration of the same name is never resolved,
137
152
  // and a stale `verified: true` is never surfaced.
138
153
  if (chain !== "X1" && chain !== "SOL") {
139
- const live = liveRecords(await fetchRecords(rpc, programBase58, pda, handle.registeredAt));
154
+ const live = liveRecords(await fetchRecords(rpc, programBase58, pda, handle.registeredAt, handle.recordsClearedAt));
140
155
  const record = live.find((r) => r.coinType === CHAIN_COIN_TYPE[chain]);
141
156
  if (!record) {
142
157
  throw new ResolveError("no-record-for-chain", `@${canonical} has no ${chain} record`, input);
@@ -166,7 +181,7 @@ export function createResolver(config) {
166
181
  throw new ResolveError("unrecognized", `per-chain records exist only for @handles, not .${parsed.namespace} domains`, input);
167
182
  }
168
183
  const { pda, handle } = await fetchHandle(parsed.canonical, input);
169
- return fetchRecords(rpc, programBase58, pda, handle.registeredAt);
184
+ return fetchRecords(rpc, programBase58, pda, handle.registeredAt, handle.recordsClearedAt);
170
185
  }
171
186
  async function resolve(input, opts) {
172
187
  const chain = opts?.chain ?? "X1";
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Move a handle's capability NFT — `TransferV1` for Metaplex
3
+ * ProgrammableNonFungibles (pNFTs), the standard `mint_handle_nft` mints.
4
+ *
5
+ * One import to move a handle NFT:
6
+ *
7
+ * ```ts
8
+ * import { WasmResolver, buildTransferV1Ix, deriveTransferV1Accounts } from "@x1id/resolve";
9
+ *
10
+ * const accounts = deriveTransferV1Accounts(wasm, {
11
+ * mint, // Handle.nft_mint (parseHandleAccount(...).nftMint)
12
+ * holder, // current holder — signs
13
+ * recipient, // destination wallet
14
+ * });
15
+ * const ix = buildTransferV1Ix({ ...accounts, amount: 1n });
16
+ * // adapt `ix` to your runtime (see delegate.ts's module docs for the
17
+ * // two-line web3.js adapter) and send with the holder's signature.
18
+ * ```
19
+ *
20
+ * # Why a plain SPL transfer no longer works
21
+ *
22
+ * A pNFT's token accounts are permanently FROZEN by the Token Metadata
23
+ * program (that is how it enforces programmability): `spl_token::transfer`
24
+ * fails with `AccountFrozen`. The ONLY way to move one is the Token Metadata
25
+ * program's own `TransferV1`, which thaw-moves-refreezes under the hood and
26
+ * maintains one `TokenRecord` PDA per token account on both sides.
27
+ * READ paths are unaffected — a frozen account still reports `amount 1`, so
28
+ * holder resolution (`nftHolder`, `reverse()`) is identical for both
29
+ * standards.
30
+ *
31
+ * Handles tokenized BEFORE the pNFT cutover hold plain `NonFungible`s
32
+ * (unfrozen) — those still move by plain SPL transfer. Branch on the mint's
33
+ * metadata `token_standard` (or on the source ATA's frozen state); this
34
+ * module only builds the pNFT path.
35
+ *
36
+ * # Wire format (verified against mpl-token-metadata 5.1.1, the crate the
37
+ * # workspace Cargo.lock pins — `src/generated/instructions/transfer_v1.rs`)
38
+ *
39
+ * Data: `[49, 0]` (instruction discriminator `Transfer` = 49, then
40
+ * `TransferArgs::V1` = 0) ‖ `amount: u64 LE` ‖ `authorization_data:
41
+ * Option` (`0` — this SDK never builds `AuthorizationData`; handle rule
42
+ * sets carry `rule_set: None`, so none is ever needed). 11 bytes total.
43
+ *
44
+ * Accounts, in exactly this order (17): token(w) token_owner
45
+ * destination_token(w) destination_owner mint metadata(w) edition
46
+ * token_record(w) destination_token_record(w) authority(s) payer(s,w)
47
+ * system_program sysvar_instructions spl_token_program spl_ata_program
48
+ * authorization_rules_program authorization_rules. An absent OPTIONAL slot
49
+ * is filled with the Token Metadata program id itself, read-only — Metaplex's
50
+ * "explicitly None" convention (kinobi-generated builders do the same).
51
+ *
52
+ * The destination ATA and its `TokenRecord` need not exist: `TransferV1`
53
+ * creates both (rent from `payer`) — that is why the ATA program is in the
54
+ * account list. No separate create-ATA instruction is needed.
55
+ *
56
+ * PDAs are derived through the WASM module (this package's one rule: never
57
+ * hand-roll `find_program_address`'s on-curve check in TypeScript — see
58
+ * wasm.ts). The Token Metadata program id is a parameter everywhere because
59
+ * X1 runs its own deployment ({@link MPL_TOKEN_METADATA_PROGRAM}).
60
+ */
61
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
62
+ import type { WasmResolver } from "./wasm.js";
63
+ /** X1's Token Metadata deployment — the id `mint_handle_nft` CPIs into
64
+ * (lib.rs `TOKEN_METADATA_PROGRAM_ID`, `…x1s` suffix). NOT the canonical
65
+ * Solana-mainnet id; on another chain, pass that deployment's id instead. */
66
+ export declare const MPL_TOKEN_METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
67
+ /** `MetadataInstruction::Transfer`'s Borsh enum discriminator. */
68
+ export declare const TRANSFER_IX_DISCRIMINATOR = 49;
69
+ /** `TransferArgs::V1`'s Borsh enum discriminator. */
70
+ export declare const TRANSFER_ARGS_V1 = 0;
71
+ /** Metadata `token_standard` value for a pNFT
72
+ * (`TokenStandard::ProgrammableNonFungible`, mpl 5.1.1 enum order). */
73
+ export declare const TOKEN_STANDARD_PROGRAMMABLE_NON_FUNGIBLE = 4;
74
+ /**
75
+ * Derive the `TokenRecord` PDA for `(mint, token)` — `token` is the token
76
+ * ACCOUNT (the ATA), not its owner. Byte-for-byte
77
+ * `mpl_token_metadata::accounts::TokenRecord::find_pda`; seeds
78
+ * `["metadata", program, mint, "token_record", token]` under `programId`.
79
+ */
80
+ export declare function deriveTokenRecordPda(wasm: WasmResolver, mint: AddressLike, token: AddressLike, programId?: AddressLike): string;
81
+ /** Every address {@link buildTransferV1Ix} needs, derived from just
82
+ * `(mint, holder, recipient)`. */
83
+ export interface TransferV1Accounts {
84
+ /** The holder's ATA — the account the NFT leaves. */
85
+ readonly token: string;
86
+ /** The holder. */
87
+ readonly tokenOwner: string;
88
+ /** The recipient's ATA — created by `TransferV1` itself if absent. */
89
+ readonly destinationToken: string;
90
+ readonly destinationOwner: string;
91
+ readonly mint: string;
92
+ readonly metadata: string;
93
+ readonly edition: string;
94
+ readonly tokenRecord: string;
95
+ readonly destinationTokenRecord: string;
96
+ /** The holder — pNFT self-transfers sign as owner. */
97
+ readonly authority: string;
98
+ /** Rent for the destination ATA + token record. Defaults to the holder. */
99
+ readonly payer: string;
100
+ readonly tokenMetadataProgramId: string;
101
+ }
102
+ /**
103
+ * Derive the full `TransferV1` account set for moving a handle NFT from
104
+ * `holder` to `recipient`. Pure derivation — nothing is fetched; whether the
105
+ * mint really is a pNFT is the caller's check (read the metadata
106
+ * `token_standard`, or the source ATA's frozen state).
107
+ */
108
+ export declare function deriveTransferV1Accounts(wasm: WasmResolver, p: {
109
+ readonly mint: AddressLike;
110
+ readonly holder: AddressLike;
111
+ readonly recipient: AddressLike;
112
+ /** Pays destination-side rent. Defaults to `holder`. */
113
+ readonly payer?: AddressLike;
114
+ readonly tokenMetadataProgramId?: AddressLike;
115
+ }): TransferV1Accounts;
116
+ export interface TransferV1Params {
117
+ /** Source token account (the holder's ATA). */
118
+ readonly token: AddressLike;
119
+ readonly tokenOwner: AddressLike;
120
+ /** Destination token account — need not exist yet (see module docs). */
121
+ readonly destinationToken: AddressLike;
122
+ readonly destinationOwner: AddressLike;
123
+ readonly mint: AddressLike;
124
+ readonly metadata: AddressLike;
125
+ /** Master Edition PDA. Required for a pNFT. */
126
+ readonly edition: AddressLike;
127
+ /** `TokenRecord` PDA of the SOURCE token account. Required for a pNFT. */
128
+ readonly tokenRecord: AddressLike;
129
+ /** `TokenRecord` PDA of the DESTINATION token account. Required for a
130
+ * pNFT — `TransferV1` creates the account if it does not exist. */
131
+ readonly destinationTokenRecord: AddressLike;
132
+ /** The transfer authority (holder or delegate). Signer. */
133
+ readonly authority: AddressLike;
134
+ /** Pays destination-side rent. Signer. */
135
+ readonly payer: AddressLike;
136
+ /** Defaults to 1 — handle NFTs are fixed-supply-1. */
137
+ readonly amount?: bigint;
138
+ /** OPTIONAL auth-rules pair — supply BOTH or NEITHER. Handle NFTs are
139
+ * minted with `rule_set: None`, so normally neither: the slots are then
140
+ * filled with the "explicitly None" placeholder (the program id). Plumbed
141
+ * for completeness should a rule set ever exist. */
142
+ readonly authorizationRulesProgram?: AddressLike;
143
+ readonly authorizationRules?: AddressLike;
144
+ readonly tokenMetadataProgramId?: AddressLike;
145
+ }
146
+ /**
147
+ * Build `TransferV1`. See the module docs for the verified account order and
148
+ * data serialization. `authorization_data` is always serialized `None` —
149
+ * with `rule_set: None` there is nothing to authorize against.
150
+ */
151
+ export declare function buildTransferV1Ix(p: TransferV1Params): BuiltInstruction;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Move a handle's capability NFT — `TransferV1` for Metaplex
3
+ * ProgrammableNonFungibles (pNFTs), the standard `mint_handle_nft` mints.
4
+ *
5
+ * One import to move a handle NFT:
6
+ *
7
+ * ```ts
8
+ * import { WasmResolver, buildTransferV1Ix, deriveTransferV1Accounts } from "@x1id/resolve";
9
+ *
10
+ * const accounts = deriveTransferV1Accounts(wasm, {
11
+ * mint, // Handle.nft_mint (parseHandleAccount(...).nftMint)
12
+ * holder, // current holder — signs
13
+ * recipient, // destination wallet
14
+ * });
15
+ * const ix = buildTransferV1Ix({ ...accounts, amount: 1n });
16
+ * // adapt `ix` to your runtime (see delegate.ts's module docs for the
17
+ * // two-line web3.js adapter) and send with the holder's signature.
18
+ * ```
19
+ *
20
+ * # Why a plain SPL transfer no longer works
21
+ *
22
+ * A pNFT's token accounts are permanently FROZEN by the Token Metadata
23
+ * program (that is how it enforces programmability): `spl_token::transfer`
24
+ * fails with `AccountFrozen`. The ONLY way to move one is the Token Metadata
25
+ * program's own `TransferV1`, which thaw-moves-refreezes under the hood and
26
+ * maintains one `TokenRecord` PDA per token account on both sides.
27
+ * READ paths are unaffected — a frozen account still reports `amount 1`, so
28
+ * holder resolution (`nftHolder`, `reverse()`) is identical for both
29
+ * standards.
30
+ *
31
+ * Handles tokenized BEFORE the pNFT cutover hold plain `NonFungible`s
32
+ * (unfrozen) — those still move by plain SPL transfer. Branch on the mint's
33
+ * metadata `token_standard` (or on the source ATA's frozen state); this
34
+ * module only builds the pNFT path.
35
+ *
36
+ * # Wire format (verified against mpl-token-metadata 5.1.1, the crate the
37
+ * # workspace Cargo.lock pins — `src/generated/instructions/transfer_v1.rs`)
38
+ *
39
+ * Data: `[49, 0]` (instruction discriminator `Transfer` = 49, then
40
+ * `TransferArgs::V1` = 0) ‖ `amount: u64 LE` ‖ `authorization_data:
41
+ * Option` (`0` — this SDK never builds `AuthorizationData`; handle rule
42
+ * sets carry `rule_set: None`, so none is ever needed). 11 bytes total.
43
+ *
44
+ * Accounts, in exactly this order (17): token(w) token_owner
45
+ * destination_token(w) destination_owner mint metadata(w) edition
46
+ * token_record(w) destination_token_record(w) authority(s) payer(s,w)
47
+ * system_program sysvar_instructions spl_token_program spl_ata_program
48
+ * authorization_rules_program authorization_rules. An absent OPTIONAL slot
49
+ * is filled with the Token Metadata program id itself, read-only — Metaplex's
50
+ * "explicitly None" convention (kinobi-generated builders do the same).
51
+ *
52
+ * The destination ATA and its `TokenRecord` need not exist: `TransferV1`
53
+ * creates both (rent from `payer`) — that is why the ATA program is in the
54
+ * account list. No separate create-ATA instruction is needed.
55
+ *
56
+ * PDAs are derived through the WASM module (this package's one rule: never
57
+ * hand-roll `find_program_address`'s on-curve check in TypeScript — see
58
+ * wasm.ts). The Token Metadata program id is a parameter everywhere because
59
+ * X1 runs its own deployment ({@link MPL_TOKEN_METADATA_PROGRAM}).
60
+ */
61
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
62
+ /** X1's Token Metadata deployment — the id `mint_handle_nft` CPIs into
63
+ * (lib.rs `TOKEN_METADATA_PROGRAM_ID`, `…x1s` suffix). NOT the canonical
64
+ * Solana-mainnet id; on another chain, pass that deployment's id instead. */
65
+ export const MPL_TOKEN_METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
66
+ /** `MetadataInstruction::Transfer`'s Borsh enum discriminator. */
67
+ export const TRANSFER_IX_DISCRIMINATOR = 49;
68
+ /** `TransferArgs::V1`'s Borsh enum discriminator. */
69
+ export const TRANSFER_ARGS_V1 = 0;
70
+ /** Metadata `token_standard` value for a pNFT
71
+ * (`TokenStandard::ProgrammableNonFungible`, mpl 5.1.1 enum order). */
72
+ export const TOKEN_STANDARD_PROGRAMMABLE_NON_FUNGIBLE = 4;
73
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
74
+ const SYSVAR_INSTRUCTIONS = "Sysvar1nstructions1111111111111111111111111";
75
+ const SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
76
+ const SPL_ATA_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
77
+ function toBytes32(v, what) {
78
+ if (typeof v === "string") {
79
+ const b = decodeBase58_32(v);
80
+ if (!b)
81
+ throw new Error(`${what} is not a valid base58 address`);
82
+ return b;
83
+ }
84
+ if (v.length !== 32)
85
+ throw new Error(`${what} must be exactly 32 bytes`);
86
+ return v;
87
+ }
88
+ function toBase58(v, what) {
89
+ return encodeBase58(toBytes32(v, what));
90
+ }
91
+ /**
92
+ * Derive the `TokenRecord` PDA for `(mint, token)` — `token` is the token
93
+ * ACCOUNT (the ATA), not its owner. Byte-for-byte
94
+ * `mpl_token_metadata::accounts::TokenRecord::find_pda`; seeds
95
+ * `["metadata", program, mint, "token_record", token]` under `programId`.
96
+ */
97
+ export function deriveTokenRecordPda(wasm, mint, token, programId = MPL_TOKEN_METADATA_PROGRAM) {
98
+ const out = wasm.deriveTokenRecordAccount(toBytes32(mint, "mint"), toBytes32(token, "token"), toBytes32(programId, "programId"));
99
+ if (!out)
100
+ throw new Error("token record derivation failed");
101
+ return encodeBase58(out);
102
+ }
103
+ /**
104
+ * Derive the full `TransferV1` account set for moving a handle NFT from
105
+ * `holder` to `recipient`. Pure derivation — nothing is fetched; whether the
106
+ * mint really is a pNFT is the caller's check (read the metadata
107
+ * `token_standard`, or the source ATA's frozen state).
108
+ */
109
+ export function deriveTransferV1Accounts(wasm, p) {
110
+ const mint = toBytes32(p.mint, "mint");
111
+ const holder = toBytes32(p.holder, "holder");
112
+ const recipient = toBytes32(p.recipient, "recipient");
113
+ const program = toBytes32(p.tokenMetadataProgramId ?? MPL_TOKEN_METADATA_PROGRAM, "tokenMetadataProgramId");
114
+ const sourceAta = wasm.deriveAssociatedTokenAccount(holder, mint);
115
+ const destAta = wasm.deriveAssociatedTokenAccount(recipient, mint);
116
+ const metadata = wasm.deriveMetadataAccount(mint, program);
117
+ const edition = wasm.deriveMasterEditionAccount(mint, program);
118
+ const sourceRecord = sourceAta && wasm.deriveTokenRecordAccount(mint, sourceAta, program);
119
+ const destRecord = destAta && wasm.deriveTokenRecordAccount(mint, destAta, program);
120
+ if (!sourceAta || !destAta || !metadata || !edition || !sourceRecord || !destRecord) {
121
+ throw new Error("TransferV1 account derivation failed");
122
+ }
123
+ return {
124
+ token: encodeBase58(sourceAta),
125
+ tokenOwner: encodeBase58(holder),
126
+ destinationToken: encodeBase58(destAta),
127
+ destinationOwner: encodeBase58(recipient),
128
+ mint: encodeBase58(mint),
129
+ metadata: encodeBase58(metadata),
130
+ edition: encodeBase58(edition),
131
+ tokenRecord: encodeBase58(sourceRecord),
132
+ destinationTokenRecord: encodeBase58(destRecord),
133
+ authority: encodeBase58(holder),
134
+ payer: encodeBase58(p.payer !== undefined ? toBytes32(p.payer, "payer") : holder),
135
+ tokenMetadataProgramId: encodeBase58(program),
136
+ };
137
+ }
138
+ /**
139
+ * Build `TransferV1`. See the module docs for the verified account order and
140
+ * data serialization. `authorization_data` is always serialized `None` —
141
+ * with `rule_set: None` there is nothing to authorize against.
142
+ */
143
+ export function buildTransferV1Ix(p) {
144
+ const amount = p.amount ?? 1n;
145
+ if (amount < 0n || amount > 0xffffffffffffffffn) {
146
+ throw new Error("amount must fit in a u64");
147
+ }
148
+ const hasRules = p.authorizationRulesProgram !== undefined || p.authorizationRules !== undefined;
149
+ if (hasRules && (p.authorizationRulesProgram === undefined || p.authorizationRules === undefined)) {
150
+ throw new Error("authorizationRulesProgram and authorizationRules must both be supplied, or neither");
151
+ }
152
+ const programId = toBase58(p.tokenMetadataProgramId ?? MPL_TOKEN_METADATA_PROGRAM, "tokenMetadataProgramId");
153
+ // [49, 0] ‖ amount u64 LE ‖ Option<AuthorizationData> = None (0).
154
+ const data = new Uint8Array(11);
155
+ data[0] = TRANSFER_IX_DISCRIMINATOR;
156
+ data[1] = TRANSFER_ARGS_V1;
157
+ new DataView(data.buffer).setBigUint64(2, amount, true);
158
+ data[10] = 0;
159
+ const keys = [
160
+ { pubkey: toBase58(p.token, "token"), isSigner: false, isWritable: true },
161
+ { pubkey: toBase58(p.tokenOwner, "tokenOwner"), isSigner: false, isWritable: false },
162
+ { pubkey: toBase58(p.destinationToken, "destinationToken"), isSigner: false, isWritable: true },
163
+ { pubkey: toBase58(p.destinationOwner, "destinationOwner"), isSigner: false, isWritable: false },
164
+ { pubkey: toBase58(p.mint, "mint"), isSigner: false, isWritable: false },
165
+ { pubkey: toBase58(p.metadata, "metadata"), isSigner: false, isWritable: true },
166
+ { pubkey: toBase58(p.edition, "edition"), isSigner: false, isWritable: false },
167
+ { pubkey: toBase58(p.tokenRecord, "tokenRecord"), isSigner: false, isWritable: true },
168
+ { pubkey: toBase58(p.destinationTokenRecord, "destinationTokenRecord"), isSigner: false, isWritable: true },
169
+ { pubkey: toBase58(p.authority, "authority"), isSigner: true, isWritable: false },
170
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
171
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
172
+ { pubkey: SYSVAR_INSTRUCTIONS, isSigner: false, isWritable: false },
173
+ { pubkey: SPL_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
174
+ { pubkey: SPL_ATA_PROGRAM_ID, isSigner: false, isWritable: false },
175
+ hasRules
176
+ ? { pubkey: toBase58(p.authorizationRulesProgram, "authorizationRulesProgram"), isSigner: false, isWritable: false }
177
+ : { pubkey: programId, isSigner: false, isWritable: false },
178
+ hasRules
179
+ ? { pubkey: toBase58(p.authorizationRules, "authorizationRules"), isSigner: false, isWritable: false }
180
+ : { pubkey: programId, isSigner: false, isWritable: false },
181
+ ];
182
+ return { programId, keys, data };
183
+ }
@@ -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 {};