@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/LICENSE +21 -0
- package/README.md +128 -1
- package/dist/accounts.d.ts +7 -0
- package/dist/accounts.js +8 -1
- package/dist/agent.d.ts +186 -0
- package/dist/agent.js +213 -0
- package/dist/attestation.d.ts +20 -8
- package/dist/attestation.js +22 -10
- package/dist/clearRecords.d.ts +60 -0
- package/dist/clearRecords.js +69 -0
- package/dist/commitReveal.d.ts +187 -0
- package/dist/commitReveal.js +245 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +17 -2
- package/dist/pnftTransfer.d.ts +151 -0
- package/dist/pnftTransfer.js +183 -0
- package/dist/recordWrite.d.ts +136 -0
- package/dist/recordWrite.js +228 -0
- package/dist/records.d.ts +24 -7
- package/dist/records.js +26 -9
- package/dist/register.d.ts +119 -0
- package/dist/register.js +183 -0
- package/dist/subname.d.ts +5 -0
- package/dist/subname.js +6 -1
- package/dist/textRecords.d.ts +18 -7
- package/dist/textRecords.js +20 -9
- package/dist/wasm.d.ts +24 -0
- package/dist/wasm.js +45 -0
- package/dist/x402.d.ts +107 -0
- package/dist/x402.js +76 -0
- package/package.json +45 -9
- package/schema/agent-manifest.json +91 -0
- package/wasm/x1_resolve_wasm.wasm +0 -0
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 {};
|