@x1id/resolve 0.6.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/commitReveal.d.ts +187 -0
- package/dist/commitReveal.js +245 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/pnftTransfer.d.ts +151 -0
- package/dist/pnftTransfer.js +183 -0
- package/dist/wasm.d.ts +24 -0
- package/dist/wasm.js +45 -0
- package/package.json +1 -1
- package/wasm/x1_resolve_wasm.wasm +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit-reveal registration (#8134 / #8199): instruction builders and
|
|
3
|
+
* account decoding for `commit` / `register_revealed` / `cancel_commitment`
|
|
4
|
+
* — the anti-front-running path. `register` (register.ts) stays the plain
|
|
5
|
+
* single-step sibling; this module adds the two-step flow: stake out a
|
|
6
|
+
* hash, wait `Config.min_commitment_age_secs`, then reveal.
|
|
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 `["commitment", hash]` (seed constant:
|
|
11
|
+
* {@link COMMITMENT_SEED}) with your runtime's canonical
|
|
12
|
+
* `findProgramAddress`, e.g. web3.js:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* PublicKey.findProgramAddressSync(
|
|
16
|
+
* [Buffer.from(COMMITMENT_SEED), Buffer.from(hash)],
|
|
17
|
+
* programId,
|
|
18
|
+
* );
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* # The commitment hash
|
|
22
|
+
*
|
|
23
|
+
* `sha256(name_bytes ‖ owner_pubkey ‖ handle_type_byte ‖ secret[32] ‖
|
|
24
|
+
* program_id_bytes)` — the exact preimage order of the program's own
|
|
25
|
+
* `commitment_hash` (lib.rs), byte-verified against the deployed program by
|
|
26
|
+
* the E2E reveal phase (tools/scripts/e2e/journey-audit.ts). sha256, NOT
|
|
27
|
+
* keccak, for the same documented reason as `text_key_hash`: off-chain
|
|
28
|
+
* derivation is plain WebCrypto with no extra dependency — so
|
|
29
|
+
* {@link commitmentHash} is async, mirroring textRecords.ts's
|
|
30
|
+
* `hashTextKey`. The program id in the preimage is the SVM analog of
|
|
31
|
+
* ENS/ArcNS's `chainid + controller` domain separation: a commitment cannot
|
|
32
|
+
* replay against a different deployment.
|
|
33
|
+
*
|
|
34
|
+
* # Lifecycle (mirror of lib.rs)
|
|
35
|
+
*
|
|
36
|
+
* - `commit(commitment_hash)` `init`s `["commitment", hash]`, recording only
|
|
37
|
+
* `payer` + `committed_at` — the name is never on chain until reveal.
|
|
38
|
+
* Anyone may pay (the security is the secret preimage, not who pays).
|
|
39
|
+
* - `register_revealed(name, handle_type, secret)` recomputes the hash
|
|
40
|
+
* on-chain from the now-public fields; the supplied `commitment` account
|
|
41
|
+
* must sit at exactly the PDA that hash derives (`CommitmentMismatch`,
|
|
42
|
+
* 6077, otherwise). Valid only inside the window: at least
|
|
43
|
+
* `min_commitment_age_secs` after `committed_at` (`CommitmentTooNew`,
|
|
44
|
+
* 6074) and strictly before `max_commitment_age_secs` (`CommitmentTooOld`,
|
|
45
|
+
* 6075). Consuming closes the commitment — rent back. Pricing, integrator
|
|
46
|
+
* split and the `Handle` write are identical to `register` (both call the
|
|
47
|
+
* program's `register_core`); the result is a PERMANENT registration.
|
|
48
|
+
* - `cancel_commitment()` closes an unconsumed commitment, payer-only
|
|
49
|
+
* (`NotCommitmentPayer`, 6080), allowed at ANY age — the escape hatch for
|
|
50
|
+
* a lost secret or an abandoned flow, so rent is never stranded.
|
|
51
|
+
*
|
|
52
|
+
* A SECRET THAT IS LOST STRANDS THE COMMITMENT: without it the hash cannot
|
|
53
|
+
* be recomputed, so the commitment can only ever be `cancel_commitment`ed
|
|
54
|
+
* (which at least needs its address, discoverable via
|
|
55
|
+
* `getProgramAccounts` on the payer). Persist the secret client-side the
|
|
56
|
+
* moment it is generated, before `commit` is even sent.
|
|
57
|
+
*/
|
|
58
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
59
|
+
import type { RpcFn } from "./accounts.js";
|
|
60
|
+
import type { HandleTypeValue } from "./voucher.js";
|
|
61
|
+
/** Seed prefix of the commitment PDA: `["commitment", commitmentHash]`. */
|
|
62
|
+
export declare const COMMITMENT_SEED = "commitment";
|
|
63
|
+
/** Anchor instruction discriminator: `sha256("global:commit")[0..8]`. */
|
|
64
|
+
export declare const COMMIT_DISCRIMINATOR: Uint8Array;
|
|
65
|
+
/** Anchor instruction discriminator: `sha256("global:register_revealed")[0..8]`. */
|
|
66
|
+
export declare const REGISTER_REVEALED_DISCRIMINATOR: Uint8Array;
|
|
67
|
+
/** Anchor instruction discriminator: `sha256("global:cancel_commitment")[0..8]`. */
|
|
68
|
+
export declare const CANCEL_COMMITMENT_DISCRIMINATOR: Uint8Array;
|
|
69
|
+
/** Anchor account discriminator: `sha256("account:Commitment")[0..8]`. */
|
|
70
|
+
export declare const COMMITMENT_DISCRIMINATOR: Uint8Array;
|
|
71
|
+
/** `Commitment` account size: disc(8) payer(32) committed_at(8) bump(1). */
|
|
72
|
+
export declare const COMMITMENT_LEN = 49;
|
|
73
|
+
export interface CommitmentHashParams {
|
|
74
|
+
/** Canonical form (`parseName(...).canonical`), 1..=32 bytes. */
|
|
75
|
+
readonly name: string;
|
|
76
|
+
/** The owner the reveal will register the handle to — must be the SAME
|
|
77
|
+
* key later passed as `register_revealed`'s `owner`, or the reveal fails
|
|
78
|
+
* closed (`CommitmentMismatch`). */
|
|
79
|
+
readonly owner: AddressLike;
|
|
80
|
+
readonly handleType: HandleTypeValue;
|
|
81
|
+
/** 32 bytes of fresh CSPRNG output (`crypto.getRandomValues(new
|
|
82
|
+
* Uint8Array(32))`). Persist it — see the module docs. */
|
|
83
|
+
readonly secret: Uint8Array;
|
|
84
|
+
/** The registry program id — part of the preimage (domain separation). */
|
|
85
|
+
readonly programId: AddressLike;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The 32-byte commitment hash — `sha256(name ‖ owner ‖ handle_type ‖ secret
|
|
89
|
+
* ‖ program_id)`, the program's own preimage order (see the module docs).
|
|
90
|
+
* WebCrypto (`crypto.subtle`), so it is async — mirrors `hashTextKey`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function commitmentHash(p: CommitmentHashParams): Promise<Uint8Array>;
|
|
93
|
+
export interface CommitParams {
|
|
94
|
+
/** The registry program id. */
|
|
95
|
+
readonly programId: AddressLike;
|
|
96
|
+
/** Pays the commitment's rent; recorded as the only key allowed to
|
|
97
|
+
* `cancel_commitment`. Signer. */
|
|
98
|
+
readonly payer: AddressLike;
|
|
99
|
+
/** The `["commitment", commitmentHash]` PDA — derive it with your
|
|
100
|
+
* runtime's canonical `findProgramAddress` (see the module docs). */
|
|
101
|
+
readonly commitment: AddressLike;
|
|
102
|
+
/** The 32-byte hash from {@link commitmentHash}. */
|
|
103
|
+
readonly commitmentHash: Uint8Array;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build `commit` — stake out the hash. Only the hash goes on chain; the
|
|
107
|
+
* name stays private until reveal. Account order matches `Commit<'info>`
|
|
108
|
+
* (lib.rs) exactly: payer, commitment, system_program.
|
|
109
|
+
*/
|
|
110
|
+
export declare function buildCommitIx(p: CommitParams): BuiltInstruction;
|
|
111
|
+
export interface RegisterRevealedParams {
|
|
112
|
+
/** The registry program id. */
|
|
113
|
+
readonly programId: AddressLike;
|
|
114
|
+
/** Pays the registration fee and the `Handle` account's rent. Signer.
|
|
115
|
+
* Need NOT be the commitment's payer — the binding is the hash preimage,
|
|
116
|
+
* and the consumed commitment's rent refunds to this payer. */
|
|
117
|
+
readonly payer: AddressLike;
|
|
118
|
+
/** The handle's owner. Need not sign, but MUST be the key the commitment
|
|
119
|
+
* hashed (see {@link CommitmentHashParams.owner}). */
|
|
120
|
+
readonly owner: AddressLike;
|
|
121
|
+
/** The `["config"]` PDA. */
|
|
122
|
+
readonly config: AddressLike;
|
|
123
|
+
/** `Config.treasury` — read it fresh via `fetchConfigTreasury`
|
|
124
|
+
* (register.ts); it's admin-rotatable. */
|
|
125
|
+
readonly treasury: AddressLike;
|
|
126
|
+
/** The `["handle", name]` PDA being claimed — must not already exist. */
|
|
127
|
+
readonly handle: AddressLike;
|
|
128
|
+
/** The `["commitment", commitmentHash]` PDA the matching `commit`
|
|
129
|
+
* created. */
|
|
130
|
+
readonly commitment: AddressLike;
|
|
131
|
+
/** Canonical form (`parseName(...).canonical`), 1..=32 bytes. */
|
|
132
|
+
readonly name: string;
|
|
133
|
+
readonly handleType: HandleTypeValue;
|
|
134
|
+
/** The exact 32-byte secret the commitment hashed. */
|
|
135
|
+
readonly secret: Uint8Array;
|
|
136
|
+
/** OPTIONAL revenue-share pair — same contract as `register`'s (#7397):
|
|
137
|
+
* supply BOTH or NEITHER. */
|
|
138
|
+
readonly integrator?: AddressLike;
|
|
139
|
+
/** The `["integrator", integrator]` allowlist PDA. */
|
|
140
|
+
readonly integratorAllowlist?: AddressLike;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Build `register_revealed` — the reveal step. Price is computed on-chain
|
|
144
|
+
* exactly like `register`'s (see register.ts's module docs); simulate first
|
|
145
|
+
* to show the payer a price. Account order matches
|
|
146
|
+
* `RegisterRevealed<'info>` (lib.rs) exactly: payer, owner, config,
|
|
147
|
+
* treasury, handle, commitment, system_program [, integrator,
|
|
148
|
+
* integrator_allowlist].
|
|
149
|
+
*/
|
|
150
|
+
export declare function buildRegisterRevealedIx(p: RegisterRevealedParams): BuiltInstruction;
|
|
151
|
+
export interface CancelCommitmentParams {
|
|
152
|
+
/** The registry program id. */
|
|
153
|
+
readonly programId: AddressLike;
|
|
154
|
+
/** MUST be the commitment's recorded payer (`NotCommitmentPayer`, 6080,
|
|
155
|
+
* otherwise). Signer; receives the rent. */
|
|
156
|
+
readonly payer: AddressLike;
|
|
157
|
+
/** The commitment account to close. No name/secret needed — half the
|
|
158
|
+
* point of canceling is that the preimage may be lost. */
|
|
159
|
+
readonly commitment: AddressLike;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Build `cancel_commitment` — reclaim an unconsumed commitment's rent,
|
|
163
|
+
* payer-only, allowed at any age. Account order matches
|
|
164
|
+
* `CancelCommitment<'info>` (lib.rs) exactly: payer, commitment.
|
|
165
|
+
*/
|
|
166
|
+
export declare function buildCancelCommitmentIx(p: CancelCommitmentParams): BuiltInstruction;
|
|
167
|
+
/** Decoded `Commitment` account. The hash itself is never stored — the
|
|
168
|
+
* account's ADDRESS is the hash's proof (it is the PDA seed). */
|
|
169
|
+
export interface Commitment {
|
|
170
|
+
/** Who paid `commit`'s rent — the only key allowed to cancel (base58). */
|
|
171
|
+
readonly payer: string;
|
|
172
|
+
/** Unix seconds when `commit` created this account — what the
|
|
173
|
+
* min/max-age window is checked against. */
|
|
174
|
+
readonly committedAt: bigint;
|
|
175
|
+
readonly bump: number;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Decode a `Commitment` account's raw data, or null if not one (wrong
|
|
179
|
+
* discriminator or too short).
|
|
180
|
+
*/
|
|
181
|
+
export declare function decodeCommitment(data: Uint8Array): Commitment | null;
|
|
182
|
+
/**
|
|
183
|
+
* Fetch + decode one `Commitment` account, or null when it doesn't exist
|
|
184
|
+
* (never committed, already consumed by a reveal, or canceled — all leave
|
|
185
|
+
* no account) or isn't shaped like a `Commitment`.
|
|
186
|
+
*/
|
|
187
|
+
export declare function fetchCommitment(rpc: RpcFn, commitmentAccount: string): Promise<Commitment | null>;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit-reveal registration (#8134 / #8199): instruction builders and
|
|
3
|
+
* account decoding for `commit` / `register_revealed` / `cancel_commitment`
|
|
4
|
+
* — the anti-front-running path. `register` (register.ts) stays the plain
|
|
5
|
+
* single-step sibling; this module adds the two-step flow: stake out a
|
|
6
|
+
* hash, wait `Config.min_commitment_age_secs`, then reveal.
|
|
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 `["commitment", hash]` (seed constant:
|
|
11
|
+
* {@link COMMITMENT_SEED}) with your runtime's canonical
|
|
12
|
+
* `findProgramAddress`, e.g. web3.js:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* PublicKey.findProgramAddressSync(
|
|
16
|
+
* [Buffer.from(COMMITMENT_SEED), Buffer.from(hash)],
|
|
17
|
+
* programId,
|
|
18
|
+
* );
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* # The commitment hash
|
|
22
|
+
*
|
|
23
|
+
* `sha256(name_bytes ‖ owner_pubkey ‖ handle_type_byte ‖ secret[32] ‖
|
|
24
|
+
* program_id_bytes)` — the exact preimage order of the program's own
|
|
25
|
+
* `commitment_hash` (lib.rs), byte-verified against the deployed program by
|
|
26
|
+
* the E2E reveal phase (tools/scripts/e2e/journey-audit.ts). sha256, NOT
|
|
27
|
+
* keccak, for the same documented reason as `text_key_hash`: off-chain
|
|
28
|
+
* derivation is plain WebCrypto with no extra dependency — so
|
|
29
|
+
* {@link commitmentHash} is async, mirroring textRecords.ts's
|
|
30
|
+
* `hashTextKey`. The program id in the preimage is the SVM analog of
|
|
31
|
+
* ENS/ArcNS's `chainid + controller` domain separation: a commitment cannot
|
|
32
|
+
* replay against a different deployment.
|
|
33
|
+
*
|
|
34
|
+
* # Lifecycle (mirror of lib.rs)
|
|
35
|
+
*
|
|
36
|
+
* - `commit(commitment_hash)` `init`s `["commitment", hash]`, recording only
|
|
37
|
+
* `payer` + `committed_at` — the name is never on chain until reveal.
|
|
38
|
+
* Anyone may pay (the security is the secret preimage, not who pays).
|
|
39
|
+
* - `register_revealed(name, handle_type, secret)` recomputes the hash
|
|
40
|
+
* on-chain from the now-public fields; the supplied `commitment` account
|
|
41
|
+
* must sit at exactly the PDA that hash derives (`CommitmentMismatch`,
|
|
42
|
+
* 6077, otherwise). Valid only inside the window: at least
|
|
43
|
+
* `min_commitment_age_secs` after `committed_at` (`CommitmentTooNew`,
|
|
44
|
+
* 6074) and strictly before `max_commitment_age_secs` (`CommitmentTooOld`,
|
|
45
|
+
* 6075). Consuming closes the commitment — rent back. Pricing, integrator
|
|
46
|
+
* split and the `Handle` write are identical to `register` (both call the
|
|
47
|
+
* program's `register_core`); the result is a PERMANENT registration.
|
|
48
|
+
* - `cancel_commitment()` closes an unconsumed commitment, payer-only
|
|
49
|
+
* (`NotCommitmentPayer`, 6080), allowed at ANY age — the escape hatch for
|
|
50
|
+
* a lost secret or an abandoned flow, so rent is never stranded.
|
|
51
|
+
*
|
|
52
|
+
* A SECRET THAT IS LOST STRANDS THE COMMITMENT: without it the hash cannot
|
|
53
|
+
* be recomputed, so the commitment can only ever be `cancel_commitment`ed
|
|
54
|
+
* (which at least needs its address, discoverable via
|
|
55
|
+
* `getProgramAccounts` on the payer). Persist the secret client-side the
|
|
56
|
+
* moment it is generated, before `commit` is even sent.
|
|
57
|
+
*/
|
|
58
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
59
|
+
/** Seed prefix of the commitment PDA: `["commitment", commitmentHash]`. */
|
|
60
|
+
export const COMMITMENT_SEED = "commitment";
|
|
61
|
+
/** Anchor instruction discriminator: `sha256("global:commit")[0..8]`. */
|
|
62
|
+
export const COMMIT_DISCRIMINATOR = Uint8Array.from([
|
|
63
|
+
223, 140, 142, 165, 229, 208, 156, 74,
|
|
64
|
+
]);
|
|
65
|
+
/** Anchor instruction discriminator: `sha256("global:register_revealed")[0..8]`. */
|
|
66
|
+
export const REGISTER_REVEALED_DISCRIMINATOR = Uint8Array.from([
|
|
67
|
+
175, 112, 193, 148, 139, 78, 15, 86,
|
|
68
|
+
]);
|
|
69
|
+
/** Anchor instruction discriminator: `sha256("global:cancel_commitment")[0..8]`. */
|
|
70
|
+
export const CANCEL_COMMITMENT_DISCRIMINATOR = Uint8Array.from([
|
|
71
|
+
36, 39, 70, 137, 71, 179, 88, 232,
|
|
72
|
+
]);
|
|
73
|
+
/** Anchor account discriminator: `sha256("account:Commitment")[0..8]`. */
|
|
74
|
+
export const COMMITMENT_DISCRIMINATOR = Uint8Array.from([
|
|
75
|
+
61, 112, 129, 128, 24, 147, 77, 87,
|
|
76
|
+
]);
|
|
77
|
+
/** `Commitment` account size: disc(8) payer(32) committed_at(8) bump(1). */
|
|
78
|
+
export const COMMITMENT_LEN = 49;
|
|
79
|
+
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
80
|
+
function toBytes32(v, what) {
|
|
81
|
+
if (typeof v === "string") {
|
|
82
|
+
const b = decodeBase58_32(v);
|
|
83
|
+
if (!b)
|
|
84
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
85
|
+
return b;
|
|
86
|
+
}
|
|
87
|
+
if (v.length !== 32)
|
|
88
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
89
|
+
return v;
|
|
90
|
+
}
|
|
91
|
+
function toBase58(v, what) {
|
|
92
|
+
return encodeBase58(toBytes32(v, what));
|
|
93
|
+
}
|
|
94
|
+
function checkHandleType(handleType) {
|
|
95
|
+
if (!Number.isInteger(handleType) || handleType < 0 || handleType > 3) {
|
|
96
|
+
throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function checkSecret(secret) {
|
|
100
|
+
if (secret.length !== 32)
|
|
101
|
+
throw new Error("secret must be exactly 32 bytes");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The 32-byte commitment hash — `sha256(name ‖ owner ‖ handle_type ‖ secret
|
|
105
|
+
* ‖ program_id)`, the program's own preimage order (see the module docs).
|
|
106
|
+
* WebCrypto (`crypto.subtle`), so it is async — mirrors `hashTextKey`.
|
|
107
|
+
*/
|
|
108
|
+
export async function commitmentHash(p) {
|
|
109
|
+
checkHandleType(p.handleType);
|
|
110
|
+
checkSecret(p.secret);
|
|
111
|
+
const nameBytes = new TextEncoder().encode(p.name);
|
|
112
|
+
if (nameBytes.length < 1 || nameBytes.length > 32) {
|
|
113
|
+
throw new Error("name must be 1..=32 bytes (UTF-8)");
|
|
114
|
+
}
|
|
115
|
+
const owner = toBytes32(p.owner, "owner");
|
|
116
|
+
const program = toBytes32(p.programId, "programId");
|
|
117
|
+
const preimage = new Uint8Array(nameBytes.length + 32 + 1 + 32 + 32);
|
|
118
|
+
let o = 0;
|
|
119
|
+
preimage.set(nameBytes, o);
|
|
120
|
+
o += nameBytes.length;
|
|
121
|
+
preimage.set(owner, o);
|
|
122
|
+
o += 32;
|
|
123
|
+
preimage[o] = p.handleType;
|
|
124
|
+
o += 1;
|
|
125
|
+
preimage.set(p.secret, o);
|
|
126
|
+
o += 32;
|
|
127
|
+
preimage.set(program, o);
|
|
128
|
+
const digest = await crypto.subtle.digest("SHA-256", preimage);
|
|
129
|
+
return new Uint8Array(digest);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Build `commit` — stake out the hash. Only the hash goes on chain; the
|
|
133
|
+
* name stays private until reveal. Account order matches `Commit<'info>`
|
|
134
|
+
* (lib.rs) exactly: payer, commitment, system_program.
|
|
135
|
+
*/
|
|
136
|
+
export function buildCommitIx(p) {
|
|
137
|
+
if (p.commitmentHash.length !== 32) {
|
|
138
|
+
throw new Error("commitmentHash must be exactly 32 bytes (a sha256 digest)");
|
|
139
|
+
}
|
|
140
|
+
const data = new Uint8Array(8 + 32);
|
|
141
|
+
data.set(COMMIT_DISCRIMINATOR, 0);
|
|
142
|
+
data.set(p.commitmentHash, 8);
|
|
143
|
+
const keys = [
|
|
144
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
145
|
+
{ pubkey: toBase58(p.commitment, "commitment"), isSigner: false, isWritable: true },
|
|
146
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
147
|
+
];
|
|
148
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Build `register_revealed` — the reveal step. Price is computed on-chain
|
|
152
|
+
* exactly like `register`'s (see register.ts's module docs); simulate first
|
|
153
|
+
* to show the payer a price. Account order matches
|
|
154
|
+
* `RegisterRevealed<'info>` (lib.rs) exactly: payer, owner, config,
|
|
155
|
+
* treasury, handle, commitment, system_program [, integrator,
|
|
156
|
+
* integrator_allowlist].
|
|
157
|
+
*/
|
|
158
|
+
export function buildRegisterRevealedIx(p) {
|
|
159
|
+
checkHandleType(p.handleType);
|
|
160
|
+
checkSecret(p.secret);
|
|
161
|
+
const nameBytes = new TextEncoder().encode(p.name);
|
|
162
|
+
if (nameBytes.length < 1 || nameBytes.length > 32) {
|
|
163
|
+
throw new Error("name must be 1..=32 bytes (UTF-8)");
|
|
164
|
+
}
|
|
165
|
+
const data = new Uint8Array(8 + 4 + nameBytes.length + 1 + 32);
|
|
166
|
+
data.set(REGISTER_REVEALED_DISCRIMINATOR, 0);
|
|
167
|
+
new DataView(data.buffer).setUint32(8, nameBytes.length, true);
|
|
168
|
+
data.set(nameBytes, 12);
|
|
169
|
+
data[12 + nameBytes.length] = p.handleType;
|
|
170
|
+
data.set(p.secret, 12 + nameBytes.length + 1);
|
|
171
|
+
const hasIntegrator = p.integrator !== undefined || p.integratorAllowlist !== undefined;
|
|
172
|
+
if (hasIntegrator && (p.integrator === undefined || p.integratorAllowlist === undefined)) {
|
|
173
|
+
throw new Error("integrator and integratorAllowlist must both be supplied, or neither");
|
|
174
|
+
}
|
|
175
|
+
const keys = [
|
|
176
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
177
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: false, isWritable: false },
|
|
178
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
|
|
179
|
+
{ pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
|
|
180
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
181
|
+
{ pubkey: toBase58(p.commitment, "commitment"), isSigner: false, isWritable: true },
|
|
182
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
183
|
+
];
|
|
184
|
+
if (hasIntegrator) {
|
|
185
|
+
keys.push({ pubkey: toBase58(p.integrator, "integrator"), isSigner: false, isWritable: true }, {
|
|
186
|
+
pubkey: toBase58(p.integratorAllowlist, "integratorAllowlist"),
|
|
187
|
+
isSigner: false,
|
|
188
|
+
isWritable: false,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Build `cancel_commitment` — reclaim an unconsumed commitment's rent,
|
|
195
|
+
* payer-only, allowed at any age. Account order matches
|
|
196
|
+
* `CancelCommitment<'info>` (lib.rs) exactly: payer, commitment.
|
|
197
|
+
*/
|
|
198
|
+
export function buildCancelCommitmentIx(p) {
|
|
199
|
+
const data = new Uint8Array(8);
|
|
200
|
+
data.set(CANCEL_COMMITMENT_DISCRIMINATOR, 0);
|
|
201
|
+
return {
|
|
202
|
+
programId: toBase58(p.programId, "programId"),
|
|
203
|
+
keys: [
|
|
204
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
205
|
+
{ pubkey: toBase58(p.commitment, "commitment"), isSigner: false, isWritable: true },
|
|
206
|
+
],
|
|
207
|
+
data,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Decode a `Commitment` account's raw data, or null if not one (wrong
|
|
212
|
+
* discriminator or too short).
|
|
213
|
+
*/
|
|
214
|
+
export function decodeCommitment(data) {
|
|
215
|
+
if (data.length < COMMITMENT_LEN)
|
|
216
|
+
return null;
|
|
217
|
+
for (let i = 0; i < 8; i++)
|
|
218
|
+
if (data[i] !== COMMITMENT_DISCRIMINATOR[i])
|
|
219
|
+
return null;
|
|
220
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
221
|
+
return {
|
|
222
|
+
payer: encodeBase58(data.slice(8, 40)),
|
|
223
|
+
committedAt: view.getBigInt64(40, true),
|
|
224
|
+
bump: data[48],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Fetch + decode one `Commitment` account, or null when it doesn't exist
|
|
229
|
+
* (never committed, already consumed by a reveal, or canceled — all leave
|
|
230
|
+
* no account) or isn't shaped like a `Commitment`.
|
|
231
|
+
*/
|
|
232
|
+
export async function fetchCommitment(rpc, commitmentAccount) {
|
|
233
|
+
const res = (await rpc("getAccountInfo", [
|
|
234
|
+
commitmentAccount,
|
|
235
|
+
{ encoding: "base64", commitment: "confirmed" },
|
|
236
|
+
]));
|
|
237
|
+
const data = res?.value?.data?.[0];
|
|
238
|
+
if (!data)
|
|
239
|
+
return null;
|
|
240
|
+
const bin = atob(data);
|
|
241
|
+
const raw = new Uint8Array(bin.length);
|
|
242
|
+
for (let i = 0; i < bin.length; i++)
|
|
243
|
+
raw[i] = bin.charCodeAt(i);
|
|
244
|
+
return decodeCommitment(raw);
|
|
245
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export * from "./accounts.js";
|
|
|
44
44
|
export * from "./x402.js";
|
|
45
45
|
export * from "./recordWrite.js";
|
|
46
46
|
export * from "./clearRecords.js";
|
|
47
|
+
export * from "./commitReveal.js";
|
|
48
|
+
export * from "./pnftTransfer.js";
|
|
47
49
|
import { type Chain, type Resolved } from "./types.js";
|
|
48
50
|
import { WasmResolver } from "./wasm.js";
|
|
49
51
|
import { type HandleRecord } from "./records.js";
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,8 @@ export * from "./accounts.js";
|
|
|
51
51
|
export * from "./x402.js";
|
|
52
52
|
export * from "./recordWrite.js";
|
|
53
53
|
export * from "./clearRecords.js";
|
|
54
|
+
export * from "./commitReveal.js";
|
|
55
|
+
export * from "./pnftTransfer.js";
|
|
54
56
|
import { ResolveError, CHAIN_COIN_TYPE } from "./types.js";
|
|
55
57
|
import { parseName } from "./parse.js";
|
|
56
58
|
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
@@ -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
|
+
}
|
package/dist/wasm.d.ts
CHANGED
|
@@ -54,5 +54,29 @@ export declare class WasmResolver {
|
|
|
54
54
|
* holds a tokenized handle's NFT.
|
|
55
55
|
*/
|
|
56
56
|
deriveAssociatedTokenAccount(owner: Uint8Array, mint: Uint8Array): Uint8Array | null;
|
|
57
|
+
/**
|
|
58
|
+
* 32-byte Metaplex Token Metadata PDA for a 32-byte mint under a given
|
|
59
|
+
* Token Metadata program id — seeds `["metadata", program, mint]`.
|
|
60
|
+
*
|
|
61
|
+
* The program id is a parameter (staged, like `deriveHandleAccount`'s)
|
|
62
|
+
* because X1 runs its own Token Metadata deployment
|
|
63
|
+
* (`MPL_TOKEN_METADATA_PROGRAM` in pnftTransfer.ts) — the canonical
|
|
64
|
+
* Solana-mainnet id would derive addresses no X1 account lives at.
|
|
65
|
+
*/
|
|
66
|
+
deriveMetadataAccount(mint: Uint8Array, programId: Uint8Array): Uint8Array | null;
|
|
67
|
+
/**
|
|
68
|
+
* 32-byte Metaplex Master Edition PDA for a 32-byte mint under a given
|
|
69
|
+
* Token Metadata program id — seeds `["metadata", program, mint,
|
|
70
|
+
* "edition"]`. Same program-id rationale as `deriveMetadataAccount`.
|
|
71
|
+
*/
|
|
72
|
+
deriveMasterEditionAccount(mint: Uint8Array, programId: Uint8Array): Uint8Array | null;
|
|
73
|
+
/**
|
|
74
|
+
* 32-byte Metaplex `TokenRecord` PDA for a 32-byte mint and a 32-byte token
|
|
75
|
+
* ACCOUNT (the ATA — not its owner) under a given Token Metadata program id
|
|
76
|
+
* — seeds `["metadata", program, mint, "token_record", token]`,
|
|
77
|
+
* byte-for-byte `mpl_token_metadata::accounts::TokenRecord::find_pda`.
|
|
78
|
+
* Needed on BOTH sides of a `ProgrammableNonFungible` transfer.
|
|
79
|
+
*/
|
|
80
|
+
deriveTokenRecordAccount(mint: Uint8Array, token: Uint8Array, programId: Uint8Array): Uint8Array | null;
|
|
57
81
|
}
|
|
58
82
|
export {};
|
package/dist/wasm.js
CHANGED
|
@@ -101,4 +101,49 @@ export class WasmResolver {
|
|
|
101
101
|
const n = this.#write(joined);
|
|
102
102
|
return this.#x.derive_associated_token_account(n) === 1 ? this.#read() : null;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* 32-byte Metaplex Token Metadata PDA for a 32-byte mint under a given
|
|
106
|
+
* Token Metadata program id — seeds `["metadata", program, mint]`.
|
|
107
|
+
*
|
|
108
|
+
* The program id is a parameter (staged, like `deriveHandleAccount`'s)
|
|
109
|
+
* because X1 runs its own Token Metadata deployment
|
|
110
|
+
* (`MPL_TOKEN_METADATA_PROGRAM` in pnftTransfer.ts) — the canonical
|
|
111
|
+
* Solana-mainnet id would derive addresses no X1 account lives at.
|
|
112
|
+
*/
|
|
113
|
+
deriveMetadataAccount(mint, programId) {
|
|
114
|
+
if (mint.length !== 32 || programId.length !== 32)
|
|
115
|
+
return null;
|
|
116
|
+
new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
|
|
117
|
+
const n = this.#write(mint);
|
|
118
|
+
return this.#x.derive_metadata_account(n) === 1 ? this.#read() : null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 32-byte Metaplex Master Edition PDA for a 32-byte mint under a given
|
|
122
|
+
* Token Metadata program id — seeds `["metadata", program, mint,
|
|
123
|
+
* "edition"]`. Same program-id rationale as `deriveMetadataAccount`.
|
|
124
|
+
*/
|
|
125
|
+
deriveMasterEditionAccount(mint, programId) {
|
|
126
|
+
if (mint.length !== 32 || programId.length !== 32)
|
|
127
|
+
return null;
|
|
128
|
+
new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
|
|
129
|
+
const n = this.#write(mint);
|
|
130
|
+
return this.#x.derive_master_edition_account(n) === 1 ? this.#read() : null;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 32-byte Metaplex `TokenRecord` PDA for a 32-byte mint and a 32-byte token
|
|
134
|
+
* ACCOUNT (the ATA — not its owner) under a given Token Metadata program id
|
|
135
|
+
* — seeds `["metadata", program, mint, "token_record", token]`,
|
|
136
|
+
* byte-for-byte `mpl_token_metadata::accounts::TokenRecord::find_pda`.
|
|
137
|
+
* Needed on BOTH sides of a `ProgrammableNonFungible` transfer.
|
|
138
|
+
*/
|
|
139
|
+
deriveTokenRecordAccount(mint, token, programId) {
|
|
140
|
+
if (mint.length !== 32 || token.length !== 32 || programId.length !== 32)
|
|
141
|
+
return null;
|
|
142
|
+
new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
|
|
143
|
+
const joined = new Uint8Array(64);
|
|
144
|
+
joined.set(mint, 0);
|
|
145
|
+
joined.set(token, 32);
|
|
146
|
+
const n = this.#write(joined);
|
|
147
|
+
return this.#x.derive_token_record_account(n) === 1 ? this.#read() : null;
|
|
148
|
+
}
|
|
104
149
|
}
|
package/package.json
CHANGED
|
Binary file
|