@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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write builders for the address `Record` family: `create_record` and the
|
|
3
|
+
* three `verify_record_*` proof-of-control instructions (ETH/secp256k1,
|
|
4
|
+
* SVM/ed25519, BTC/BIP-137). `records.ts` was read-only until now — every
|
|
5
|
+
* other record family (text records, attestations, vouchers, subnames...)
|
|
6
|
+
* already had write builders; this was the one gap (WP #8142).
|
|
7
|
+
*
|
|
8
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
9
|
+
* `@solana/web3.js` import. PDAs are NOT derived here (see delegate.ts's
|
|
10
|
+
* module docs for why) — derive `["record", handlePda, coinTypeLE(4)]`
|
|
11
|
+
* with your runtime's canonical `findProgramAddress`.
|
|
12
|
+
*
|
|
13
|
+
* # What this SDK does and does not do for you
|
|
14
|
+
*
|
|
15
|
+
* This does NOT sign anything, derive an address from a signature, or
|
|
16
|
+
* verify a signature client-side — that's the on-chain program's job
|
|
17
|
+
* (`verify_record_eth`/`svm`/`btc` recover the signer's address from the
|
|
18
|
+
* signature itself and compare it to the record's claimed value; a client
|
|
19
|
+
* proving nothing false cannot fabricate a match). What this module DOES do:
|
|
20
|
+
*
|
|
21
|
+
* 1. {@link buildRecordChallenge} — build the exact bytes a wallet must
|
|
22
|
+
* sign, byte-identical to `handle-normalize`'s Rust `challenge()`
|
|
23
|
+
* (verified: `crates/handle-normalize/src/challenge.rs`). Unlike the
|
|
24
|
+
* off-chain control-proof flow (`control.ts`), this challenge's nonce
|
|
25
|
+
* does NOT need to be server-issued — the on-chain transaction landing
|
|
26
|
+
* IS the single-use event, so any ASCII-alphanumeric nonce (1..=64
|
|
27
|
+
* chars) the caller picks is fine.
|
|
28
|
+
* 2. {@link splitEthSignature} / {@link splitBtcSignature} — repackage what
|
|
29
|
+
* a wallet's `personal_sign` (ETH) or `signmessage` (BTC) actually
|
|
30
|
+
* returns into the exact args each verify instruction expects.
|
|
31
|
+
* 3. The four instruction builders themselves.
|
|
32
|
+
*
|
|
33
|
+
* # ETH: EIP-191 `personal_sign`, not EIP-712
|
|
34
|
+
*
|
|
35
|
+
* `verify_record_eth` (lib.rs) hashes the challenge with EIP-191's
|
|
36
|
+
* `"\x19Ethereum Signed Message:\n" + len + message` framing — the exact
|
|
37
|
+
* framing every wallet's `personal_sign` / `eth_sign` RPC method produces.
|
|
38
|
+
* There is no EIP-712 typed-data variant on-chain to build a client for.
|
|
39
|
+
*
|
|
40
|
+
* # BTC: mainnet-only, compressed P2PKH or bech32/P2WPKH only
|
|
41
|
+
*
|
|
42
|
+
* `verify_record_btc` derives the address FROM the recovered pubkey (never
|
|
43
|
+
* trusts a client-supplied address) and only accepts the header-byte ranges
|
|
44
|
+
* for compressed P2PKH (31-34) and bech32/P2WPKH (39-42) — see `btc.rs`'s
|
|
45
|
+
* module doc for exactly why uncompressed P2PKH and P2SH-segwit are refused
|
|
46
|
+
* rather than guessed. {@link splitBtcSignature} passes the header byte
|
|
47
|
+
* through unchanged; it is the wallet's job to produce one of the two
|
|
48
|
+
* supported ranges (most current wallets do).
|
|
49
|
+
*/
|
|
50
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
51
|
+
import { recordDelegateNonePlaceholder, recordDelegateSomeSlot } from "./delegate.js";
|
|
52
|
+
import { normalizeHandle } from "./parse.js";
|
|
53
|
+
import { ResolveError } from "./types.js";
|
|
54
|
+
/** Matches `control.ts`'s identically-named private helper: STRICT
|
|
55
|
+
* canonical-form check (reject, never silently transform), mirroring the
|
|
56
|
+
* Rust `is_canonical` the challenge builder must match exactly —
|
|
57
|
+
* `normalizeHandle` alone would accept `"@Alice"` by transforming it,
|
|
58
|
+
* which `handle_normalize::challenge` (Rust) does NOT: it demands
|
|
59
|
+
* already-canonical input and rejects anything else outright. */
|
|
60
|
+
function isCanonicalHandle(handle) {
|
|
61
|
+
try {
|
|
62
|
+
return normalizeHandle(handle) === handle;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export const CREATE_RECORD_DISCRIMINATOR = Uint8Array.from([
|
|
69
|
+
116, 124, 63, 58, 126, 204, 178, 10,
|
|
70
|
+
]);
|
|
71
|
+
export const VERIFY_RECORD_ETH_DISCRIMINATOR = Uint8Array.from([
|
|
72
|
+
57, 157, 98, 0, 160, 210, 94, 234,
|
|
73
|
+
]);
|
|
74
|
+
export const VERIFY_RECORD_SVM_DISCRIMINATOR = Uint8Array.from([
|
|
75
|
+
234, 151, 82, 212, 185, 112, 234, 212,
|
|
76
|
+
]);
|
|
77
|
+
export const VERIFY_RECORD_BTC_DISCRIMINATOR = Uint8Array.from([
|
|
78
|
+
54, 50, 183, 232, 122, 200, 194, 64,
|
|
79
|
+
]);
|
|
80
|
+
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
81
|
+
const NONCE_RE = /^[0-9A-Za-z]{1,64}$/;
|
|
82
|
+
function toBytes32(v, what) {
|
|
83
|
+
if (typeof v === "string") {
|
|
84
|
+
const b = decodeBase58_32(v);
|
|
85
|
+
if (!b)
|
|
86
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
87
|
+
return b;
|
|
88
|
+
}
|
|
89
|
+
if (v.length !== 32)
|
|
90
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
91
|
+
return v;
|
|
92
|
+
}
|
|
93
|
+
function toBase58(v, what) {
|
|
94
|
+
return encodeBase58(toBytes32(v, what));
|
|
95
|
+
}
|
|
96
|
+
function encVec(bytes) {
|
|
97
|
+
const out = new Uint8Array(4 + bytes.length);
|
|
98
|
+
new DataView(out.buffer).setUint32(0, bytes.length, true);
|
|
99
|
+
out.set(bytes, 4);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function encString(s) {
|
|
103
|
+
return encVec(new TextEncoder().encode(s));
|
|
104
|
+
}
|
|
105
|
+
function pushAuthorityTail(keys, programId, p) {
|
|
106
|
+
if (p.recordDelegate !== undefined) {
|
|
107
|
+
keys.push(recordDelegateSomeSlot(p.recordDelegate));
|
|
108
|
+
}
|
|
109
|
+
else if (p.holderTokenAccount !== undefined) {
|
|
110
|
+
keys.push(recordDelegateNonePlaceholder(programId));
|
|
111
|
+
}
|
|
112
|
+
if (p.holderTokenAccount !== undefined) {
|
|
113
|
+
keys.push({ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"), isSigner: false, isWritable: false });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Challenge
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
/**
|
|
120
|
+
* `x1-handles:v1:<handle>:<coinType>:<registeredAt>:<nonce>` — byte-identical
|
|
121
|
+
* to `handle_normalize::challenge` (Rust). `handle` must already be
|
|
122
|
+
* canonical (pass it through {@link import("./parse.js").normalizeHandle}
|
|
123
|
+
* first if it might have a leading `@` or mixed case).
|
|
124
|
+
*/
|
|
125
|
+
export function buildRecordChallenge(handle, coinType, registeredAt, nonce) {
|
|
126
|
+
if (!isCanonicalHandle(handle)) {
|
|
127
|
+
throw new ResolveError("invalid-handle", `"${handle}" is not a canonical handle`, handle);
|
|
128
|
+
}
|
|
129
|
+
if (!NONCE_RE.test(nonce)) {
|
|
130
|
+
throw new Error("nonce must be 1..=64 ASCII alphanumeric characters");
|
|
131
|
+
}
|
|
132
|
+
return `x1-handles:v1:${handle}:${coinType}:${registeredAt}:${nonce}`;
|
|
133
|
+
}
|
|
134
|
+
/** Split a 65-byte ETH `personal_sign` output (`r(32) || s(32) || v(1)`)
|
|
135
|
+
* into `verify_record_eth`'s `{signature: [u8;64], recoveryId: u8}`,
|
|
136
|
+
* normalizing `v`'s two common conventions (27/28, matching most wallets'
|
|
137
|
+
* raw RPC output, or already 0/1). */
|
|
138
|
+
export function splitEthSignature(sig65) {
|
|
139
|
+
if (sig65.length !== 65)
|
|
140
|
+
throw new Error("ETH signature must be exactly 65 bytes (r || s || v)");
|
|
141
|
+
const v = sig65[64];
|
|
142
|
+
const recoveryId = v >= 27 ? v - 27 : v;
|
|
143
|
+
if (recoveryId !== 0 && recoveryId !== 1) {
|
|
144
|
+
throw new Error(`unrecognized recovery id byte: ${v}`);
|
|
145
|
+
}
|
|
146
|
+
return { signature: sig65.slice(0, 64), recoveryId };
|
|
147
|
+
}
|
|
148
|
+
/** Split a 65-byte BTC BIP-137 `signmessage` output (`header(1) || r(32) ||
|
|
149
|
+
* s(32)`) into `verify_record_btc`'s `{header, signature: [u8;64]}`. The
|
|
150
|
+
* header byte is passed through unchanged — see the module docs for which
|
|
151
|
+
* ranges `verify_record_btc` accepts. */
|
|
152
|
+
export function splitBtcSignature(sig65) {
|
|
153
|
+
if (sig65.length !== 65)
|
|
154
|
+
throw new Error("BTC signature must be exactly 65 bytes (header || r || s)");
|
|
155
|
+
return { header: sig65[0], signature: sig65.slice(1) };
|
|
156
|
+
}
|
|
157
|
+
export function buildCreateRecordIx(p) {
|
|
158
|
+
if (p.value.length === 0 || p.value.length > 64) {
|
|
159
|
+
throw new Error("value must be 1..=64 bytes");
|
|
160
|
+
}
|
|
161
|
+
const coinTypeBytes = new Uint8Array(4);
|
|
162
|
+
new DataView(coinTypeBytes.buffer).setUint32(0, p.coinType, true);
|
|
163
|
+
const data = new Uint8Array(8 + 4 + 4 + p.value.length);
|
|
164
|
+
data.set(CREATE_RECORD_DISCRIMINATOR, 0);
|
|
165
|
+
data.set(coinTypeBytes, 8);
|
|
166
|
+
data.set(encVec(p.value), 12);
|
|
167
|
+
const keys = [
|
|
168
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
169
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
|
|
170
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
171
|
+
{ pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
|
|
172
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
173
|
+
];
|
|
174
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
175
|
+
}
|
|
176
|
+
export function buildVerifyRecordEthIx(p) {
|
|
177
|
+
if (p.signature.length !== 64)
|
|
178
|
+
throw new Error("signature must be exactly 64 bytes (r || s)");
|
|
179
|
+
if (p.recoveryId !== 0 && p.recoveryId !== 1)
|
|
180
|
+
throw new Error("recoveryId must be 0 or 1");
|
|
181
|
+
if (!NONCE_RE.test(p.nonce))
|
|
182
|
+
throw new Error("nonce must be 1..=64 ASCII alphanumeric characters");
|
|
183
|
+
const nonceEnc = encString(p.nonce);
|
|
184
|
+
const data = new Uint8Array(8 + nonceEnc.length + 64 + 1);
|
|
185
|
+
data.set(VERIFY_RECORD_ETH_DISCRIMINATOR, 0);
|
|
186
|
+
data.set(nonceEnc, 8);
|
|
187
|
+
data.set(p.signature, 8 + nonceEnc.length);
|
|
188
|
+
data[8 + nonceEnc.length + 64] = p.recoveryId;
|
|
189
|
+
const keys = [
|
|
190
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
|
|
191
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
|
|
192
|
+
{ pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
|
|
193
|
+
];
|
|
194
|
+
pushAuthorityTail(keys, p.programId, p);
|
|
195
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
196
|
+
}
|
|
197
|
+
export function buildVerifyRecordSvmIx(p) {
|
|
198
|
+
const keys = [
|
|
199
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
|
|
200
|
+
{ pubkey: toBase58(p.claimed, "claimed"), isSigner: true, isWritable: false },
|
|
201
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
|
|
202
|
+
{ pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
|
|
203
|
+
];
|
|
204
|
+
pushAuthorityTail(keys, p.programId, p);
|
|
205
|
+
return { programId: toBase58(p.programId, "programId"), keys, data: VERIFY_RECORD_SVM_DISCRIMINATOR.slice() };
|
|
206
|
+
}
|
|
207
|
+
export function buildVerifyRecordBtcIx(p) {
|
|
208
|
+
if (p.signature.length !== 64)
|
|
209
|
+
throw new Error("signature must be exactly 64 bytes (r || s)");
|
|
210
|
+
if (!Number.isInteger(p.header) || p.header < 0 || p.header > 255) {
|
|
211
|
+
throw new Error("header must be a single byte (0-255)");
|
|
212
|
+
}
|
|
213
|
+
if (!NONCE_RE.test(p.nonce))
|
|
214
|
+
throw new Error("nonce must be 1..=64 ASCII alphanumeric characters");
|
|
215
|
+
const nonceEnc = encString(p.nonce);
|
|
216
|
+
const data = new Uint8Array(8 + nonceEnc.length + 65);
|
|
217
|
+
data.set(VERIFY_RECORD_BTC_DISCRIMINATOR, 0);
|
|
218
|
+
data.set(nonceEnc, 8);
|
|
219
|
+
data[8 + nonceEnc.length] = p.header;
|
|
220
|
+
data.set(p.signature, 8 + nonceEnc.length + 1);
|
|
221
|
+
const keys = [
|
|
222
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
|
|
223
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
|
|
224
|
+
{ pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
|
|
225
|
+
];
|
|
226
|
+
pushAuthorityTail(keys, p.programId, p);
|
|
227
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
228
|
+
}
|
package/dist/records.d.ts
CHANGED
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
* There is deliberately no way to decode or fetch a record through this
|
|
20
20
|
* module without the handle's `registered_at` in hand, mirroring
|
|
21
21
|
* app/src/lib/x1/records.ts. See docs/record-trust.md for the full argument.
|
|
22
|
+
*
|
|
23
|
+
* # `records_cleared_at` (#8139) — the SECOND, independent staleness rule
|
|
24
|
+
*
|
|
25
|
+
* `clear_records` lets an owner bulk-invalidate every record RIGHT NOW,
|
|
26
|
+
* without a transfer. The same functions therefore also demand the handle's
|
|
27
|
+
* `recordsClearedAt` (0 if never cleared, from `ParsedHandle`) and a record
|
|
28
|
+
* is `stale` when EITHER `updated_at < registeredAt` OR
|
|
29
|
+
* `updated_at < recordsClearedAt` — see `Handle::read_records_cleared_at`'s
|
|
30
|
+
* doc comment in state.rs.
|
|
22
31
|
*/
|
|
23
32
|
import { type Chain } from "./types.js";
|
|
24
33
|
import type { RpcFn } from "./accounts.js";
|
|
@@ -75,7 +84,10 @@ export interface HandleRecord {
|
|
|
75
84
|
*
|
|
76
85
|
* `registeredAt` is the owning Handle's `registered_at`, decoded by the
|
|
77
86
|
* caller from the Handle account — it decides `stale` (and so `verified`).
|
|
78
|
-
*
|
|
87
|
+
* `recordsClearedAt` is the same Handle's `recordsClearedAt` (0 if never
|
|
88
|
+
* cleared, #8139) — a SECOND, independent staleness anchor, checked
|
|
89
|
+
* alongside (not instead of) `registeredAt`. There is intentionally no
|
|
90
|
+
* overload without either.
|
|
79
91
|
*
|
|
80
92
|
* Returns null for anything that is not a Record: wrong length, wrong
|
|
81
93
|
* discriminator, or a `value` length that does not fit. The Listing / Offer /
|
|
@@ -83,7 +95,7 @@ export interface HandleRecord {
|
|
|
83
95
|
* so a handle-only memcmp scan DOES return them — they are rejected here by
|
|
84
96
|
* tag and size, not by luck.
|
|
85
97
|
*/
|
|
86
|
-
export declare function decodeRecord(raw: Uint8Array, account: string, registeredAt: bigint): HandleRecord | null;
|
|
98
|
+
export declare function decodeRecord(raw: Uint8Array, account: string, registeredAt: bigint, recordsClearedAt: bigint): HandleRecord | null;
|
|
87
99
|
/** The records the CURRENT owner actually has — `stale` ones excluded. This
|
|
88
100
|
* is the list to resolve against, display to a visitor, and count. */
|
|
89
101
|
export declare function liveRecords(records: readonly HandleRecord[]): HandleRecord[];
|
|
@@ -105,9 +117,14 @@ export declare function valueToAddress(chain: Chain | null, value: Uint8Array):
|
|
|
105
117
|
*
|
|
106
118
|
* `registeredAt` is `Handle.registered_at` as decoded from the Handle
|
|
107
119
|
* account the caller already has — the staleness rule needs it, and there
|
|
108
|
-
* is no variant of this function without it.
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
120
|
+
* is no variant of this function without it. `recordsClearedAt` is that same
|
|
121
|
+
* Handle's `recordsClearedAt` (0 if never cleared, #8139) — pass it, not a
|
|
122
|
+
* literal 0, unless the records being fetched genuinely hang off a DIFFERENT
|
|
123
|
+
* account than the one `clear_records` could ever touch (e.g. a subname's
|
|
124
|
+
* own records, keyed by the subname's `createdAt` instead — see
|
|
125
|
+
* `subname.ts`). Every record is returned, stale ones flagged (`verified`
|
|
126
|
+
* already forced false), so an owner surface can show what a previous owner
|
|
127
|
+
* left behind; anything that resolves, displays to a visitor, or counts
|
|
128
|
+
* takes `liveRecords(...)`.
|
|
112
129
|
*/
|
|
113
|
-
export declare function fetchRecords(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint): Promise<HandleRecord[]>;
|
|
130
|
+
export declare function fetchRecords(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint): Promise<HandleRecord[]>;
|
package/dist/records.js
CHANGED
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
* There is deliberately no way to decode or fetch a record through this
|
|
20
20
|
* module without the handle's `registered_at` in hand, mirroring
|
|
21
21
|
* app/src/lib/x1/records.ts. See docs/record-trust.md for the full argument.
|
|
22
|
+
*
|
|
23
|
+
* # `records_cleared_at` (#8139) — the SECOND, independent staleness rule
|
|
24
|
+
*
|
|
25
|
+
* `clear_records` lets an owner bulk-invalidate every record RIGHT NOW,
|
|
26
|
+
* without a transfer. The same functions therefore also demand the handle's
|
|
27
|
+
* `recordsClearedAt` (0 if never cleared, from `ParsedHandle`) and a record
|
|
28
|
+
* is `stale` when EITHER `updated_at < registeredAt` OR
|
|
29
|
+
* `updated_at < recordsClearedAt` — see `Handle::read_records_cleared_at`'s
|
|
30
|
+
* doc comment in state.rs.
|
|
22
31
|
*/
|
|
23
32
|
import { encodeBase58 } from "./base58.js";
|
|
24
33
|
import { CHAIN_COIN_TYPE } from "./types.js";
|
|
@@ -52,7 +61,10 @@ export function chainForCoinType(coinType) {
|
|
|
52
61
|
*
|
|
53
62
|
* `registeredAt` is the owning Handle's `registered_at`, decoded by the
|
|
54
63
|
* caller from the Handle account — it decides `stale` (and so `verified`).
|
|
55
|
-
*
|
|
64
|
+
* `recordsClearedAt` is the same Handle's `recordsClearedAt` (0 if never
|
|
65
|
+
* cleared, #8139) — a SECOND, independent staleness anchor, checked
|
|
66
|
+
* alongside (not instead of) `registeredAt`. There is intentionally no
|
|
67
|
+
* overload without either.
|
|
56
68
|
*
|
|
57
69
|
* Returns null for anything that is not a Record: wrong length, wrong
|
|
58
70
|
* discriminator, or a `value` length that does not fit. The Listing / Offer /
|
|
@@ -60,7 +72,7 @@ export function chainForCoinType(coinType) {
|
|
|
60
72
|
* so a handle-only memcmp scan DOES return them — they are rejected here by
|
|
61
73
|
* tag and size, not by luck.
|
|
62
74
|
*/
|
|
63
|
-
export function decodeRecord(raw, account, registeredAt) {
|
|
75
|
+
export function decodeRecord(raw, account, registeredAt, recordsClearedAt) {
|
|
64
76
|
if (raw.length !== RECORD_LEN)
|
|
65
77
|
return null;
|
|
66
78
|
for (let i = 0; i < 8; i++) {
|
|
@@ -83,7 +95,7 @@ export function decodeRecord(raw, account, registeredAt) {
|
|
|
83
95
|
const verified = raw[o] === 1;
|
|
84
96
|
o += 1;
|
|
85
97
|
const updatedAt = dv.getBigInt64(o, true);
|
|
86
|
-
const stale = updatedAt < registeredAt;
|
|
98
|
+
const stale = updatedAt < registeredAt || updatedAt < recordsClearedAt;
|
|
87
99
|
const chain = chainForCoinType(coinType);
|
|
88
100
|
return {
|
|
89
101
|
account,
|
|
@@ -142,12 +154,17 @@ function hexToBytes(hex) {
|
|
|
142
154
|
*
|
|
143
155
|
* `registeredAt` is `Handle.registered_at` as decoded from the Handle
|
|
144
156
|
* account the caller already has — the staleness rule needs it, and there
|
|
145
|
-
* is no variant of this function without it.
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
157
|
+
* is no variant of this function without it. `recordsClearedAt` is that same
|
|
158
|
+
* Handle's `recordsClearedAt` (0 if never cleared, #8139) — pass it, not a
|
|
159
|
+
* literal 0, unless the records being fetched genuinely hang off a DIFFERENT
|
|
160
|
+
* account than the one `clear_records` could ever touch (e.g. a subname's
|
|
161
|
+
* own records, keyed by the subname's `createdAt` instead — see
|
|
162
|
+
* `subname.ts`). Every record is returned, stale ones flagged (`verified`
|
|
163
|
+
* already forced false), so an owner surface can show what a previous owner
|
|
164
|
+
* left behind; anything that resolves, displays to a visitor, or counts
|
|
165
|
+
* takes `liveRecords(...)`.
|
|
149
166
|
*/
|
|
150
|
-
export async function fetchRecords(rpc, programId, handleAccount, registeredAt) {
|
|
167
|
+
export async function fetchRecords(rpc, programId, handleAccount, registeredAt, recordsClearedAt) {
|
|
151
168
|
const res = (await rpc("getProgramAccounts", [
|
|
152
169
|
programId,
|
|
153
170
|
{
|
|
@@ -166,7 +183,7 @@ export async function fetchRecords(rpc, programId, handleAccount, registeredAt)
|
|
|
166
183
|
const raw = new Uint8Array(bin.length);
|
|
167
184
|
for (let i = 0; i < bin.length; i++)
|
|
168
185
|
raw[i] = bin.charCodeAt(i);
|
|
169
|
-
const r = decodeRecord(raw, a.pubkey, registeredAt);
|
|
186
|
+
const r = decodeRecord(raw, a.pubkey, registeredAt, recordsClearedAt);
|
|
170
187
|
// Defence in depth: the memcmp filter should guarantee the handle match,
|
|
171
188
|
// but a wrong offset would silently attribute someone else's record to
|
|
172
189
|
// this handle. A malformed account is skipped, not fatal to the list.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `register` — the core write instruction: pay the current ramp price and
|
|
3
|
+
* claim an unregistered `["handle", name]` PDA. Every other write path in
|
|
4
|
+
* this SDK (voucher, subname, lock, delegate, attestation, text record,
|
|
5
|
+
* integrator) already has a builder; this was the one gap.
|
|
6
|
+
*
|
|
7
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
8
|
+
* `@solana/web3.js` import. PDAs are NOT derived here (see delegate.ts's
|
|
9
|
+
* module docs for why) — derive `["config"]` / `["handle", name]` with your
|
|
10
|
+
* runtime's canonical `findProgramAddress`.
|
|
11
|
+
*
|
|
12
|
+
* `register`'s price is computed ON-CHAIN at execution time (time + volume
|
|
13
|
+
* ramp over `Config.tier_lamports`) — there is no client-supplied price or
|
|
14
|
+
* slippage parameter; the payer simply pays whatever the program charges as
|
|
15
|
+
* of the landing slot. {@link fetchConfigTreasury} reads the one `Config`
|
|
16
|
+
* field this builder needs (the treasury address every registration pays).
|
|
17
|
+
*/
|
|
18
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
19
|
+
import type { RpcFn } from "./accounts.js";
|
|
20
|
+
import type { HandleTypeValue } from "./voucher.js";
|
|
21
|
+
/** Anchor instruction discriminator: `sha256("global:register")[0..8]`. */
|
|
22
|
+
export declare const REGISTER_DISCRIMINATOR: Uint8Array;
|
|
23
|
+
/** Anchor instruction discriminator: `sha256("global:register_with_forti")[0..8]`. */
|
|
24
|
+
export declare const REGISTER_WITH_FORTI_DISCRIMINATOR: Uint8Array;
|
|
25
|
+
/**
|
|
26
|
+
* Read `Config.treasury` — the account every `register` call pays — given
|
|
27
|
+
* the `["config"]` PDA. Returns null if the account doesn't exist or isn't
|
|
28
|
+
* shaped like a `Config` (too short to hold `treasury`).
|
|
29
|
+
*/
|
|
30
|
+
export declare function fetchConfigTreasury(rpc: RpcFn, configAccount: string): Promise<string | null>;
|
|
31
|
+
export interface RegisterParams {
|
|
32
|
+
/** The registry program id. */
|
|
33
|
+
readonly programId: AddressLike;
|
|
34
|
+
/** Pays the registration fee and the `Handle` account's rent. Signer. */
|
|
35
|
+
readonly payer: AddressLike;
|
|
36
|
+
/** The handle's owner. Need not sign — a handle can be registered on
|
|
37
|
+
* someone else's behalf (gifting, merchant onboarding). */
|
|
38
|
+
readonly owner: AddressLike;
|
|
39
|
+
/** The `["config"]` PDA. */
|
|
40
|
+
readonly config: AddressLike;
|
|
41
|
+
/** `Config.treasury` — read it fresh via {@link fetchConfigTreasury}
|
|
42
|
+
* (it's admin-rotatable, so don't cache it across a session). */
|
|
43
|
+
readonly treasury: AddressLike;
|
|
44
|
+
/** The `["handle", name]` PDA being claimed — must not already exist. */
|
|
45
|
+
readonly handle: AddressLike;
|
|
46
|
+
/** Canonical form (`parseName(...).canonical`), 1..=32 bytes. */
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly handleType: HandleTypeValue;
|
|
49
|
+
/** OPTIONAL revenue-share pair (#7397) — supply BOTH or NEITHER; supplying
|
|
50
|
+
* exactly one fails on-chain (`IntegratorNotAllowed`). */
|
|
51
|
+
readonly integrator?: AddressLike;
|
|
52
|
+
/** The `["integrator", integrator]` allowlist PDA. */
|
|
53
|
+
readonly integratorAllowlist?: AddressLike;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build `register`. Price is computed on-chain (see the module docs) — this
|
|
57
|
+
* builder does not know or assert it; simulate first if you need to show the
|
|
58
|
+
* payer a price before they sign.
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildRegisterIx(p: RegisterParams): BuiltInstruction;
|
|
61
|
+
/** The `Config` fields `register_with_forti` needs beyond what
|
|
62
|
+
* {@link fetchConfigTreasury} already reads. `fortiRatePerLamport === 0n`
|
|
63
|
+
* means FORTI payment is not enabled on this deployment — the same
|
|
64
|
+
* `ZeroFortiRate` gate the program itself enforces; callers should refuse
|
|
65
|
+
* to build the instruction rather than let it fail on-chain. */
|
|
66
|
+
export interface ConfigForti {
|
|
67
|
+
readonly fortiMint: string;
|
|
68
|
+
readonly fortiTreasuryAta: string;
|
|
69
|
+
readonly veFortiProgram: string;
|
|
70
|
+
readonly fortiBuybackRecipient: string;
|
|
71
|
+
readonly fortiRatePerLamport: bigint;
|
|
72
|
+
}
|
|
73
|
+
/** Read the `Config` fields a `register_with_forti` build needs, given the
|
|
74
|
+
* `["config"]` PDA. Returns null if the account doesn't exist or isn't
|
|
75
|
+
* shaped like a post-#8136 `Config` (too short to hold these fields —
|
|
76
|
+
* e.g. a `Config` that hasn't run `migrate_config_v4` yet). */
|
|
77
|
+
export declare function fetchConfigForti(rpc: RpcFn, configAccount: string): Promise<ConfigForti | null>;
|
|
78
|
+
export interface RegisterWithFortiParams {
|
|
79
|
+
/** The registry program id. */
|
|
80
|
+
readonly programId: AddressLike;
|
|
81
|
+
/** Pays the FORTI amount and the `Handle` account's rent. Signer. */
|
|
82
|
+
readonly payer: AddressLike;
|
|
83
|
+
/** The handle's owner. Need not sign — same as {@link RegisterParams.owner}. */
|
|
84
|
+
readonly owner: AddressLike;
|
|
85
|
+
/** The `["config"]` PDA. */
|
|
86
|
+
readonly config: AddressLike;
|
|
87
|
+
/** `Config.forti_mint` — {@link fetchConfigForti}. */
|
|
88
|
+
readonly fortiMint: AddressLike;
|
|
89
|
+
/** The PAYER's associated token account for `fortiMint` — derive with your
|
|
90
|
+
* runtime's canonical ATA derivation (Token-2022; the real testnet FORTI
|
|
91
|
+
* mint is Token-2022, not classic SPL Token). */
|
|
92
|
+
readonly payerFortiAccount: AddressLike;
|
|
93
|
+
/** `Config.forti_treasury_ata` — {@link fetchConfigForti}. */
|
|
94
|
+
readonly fortiTreasuryAta: AddressLike;
|
|
95
|
+
/** `Config.forti_buyback_recipient` — {@link fetchConfigForti}. */
|
|
96
|
+
readonly fortiBuybackRecipient: AddressLike;
|
|
97
|
+
/** The payer's veFORTI lock PDA (`["lock", payer]` under
|
|
98
|
+
* `Config.veforti_program`) — may not exist (payer never locked), in
|
|
99
|
+
* which case the program treats voting power as zero. Derive with your
|
|
100
|
+
* runtime's canonical `findProgramAddress` against
|
|
101
|
+
* `fetchConfigForti(...).veFortiProgram`. */
|
|
102
|
+
readonly veFortiLock: AddressLike;
|
|
103
|
+
/** The `["handle", name]` PDA being claimed — must not already exist. */
|
|
104
|
+
readonly handle: AddressLike;
|
|
105
|
+
/** Canonical form (`parseName(...).canonical`), 1..=32 bytes. */
|
|
106
|
+
readonly name: string;
|
|
107
|
+
readonly handleType: HandleTypeValue;
|
|
108
|
+
/** The token program that owns `fortiMint` — Token-2022 on testnet today;
|
|
109
|
+
* pass whichever `fortiMint`'s own `owner` field names, never hardcode. */
|
|
110
|
+
readonly tokenProgram: AddressLike;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build `register_with_forti` (#8136) — a SIBLING payment path to
|
|
114
|
+
* `register`, not a replacement. Price is computed on-chain the same way
|
|
115
|
+
* `register`'s is (see that builder's docs) — this builder does not know or
|
|
116
|
+
* assert it; use `/quote/:name?pay_with=forti[&payer=...]` (tools/api) or
|
|
117
|
+
* simulate first if you need to show the payer a price before they sign.
|
|
118
|
+
*/
|
|
119
|
+
export declare function buildRegisterWithFortiIx(p: RegisterWithFortiParams): BuiltInstruction;
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `register` — the core write instruction: pay the current ramp price and
|
|
3
|
+
* claim an unregistered `["handle", name]` PDA. Every other write path in
|
|
4
|
+
* this SDK (voucher, subname, lock, delegate, attestation, text record,
|
|
5
|
+
* integrator) already has a builder; this was the one gap.
|
|
6
|
+
*
|
|
7
|
+
* Hand-rolled like the rest of this package — no Anchor client, no
|
|
8
|
+
* `@solana/web3.js` import. PDAs are NOT derived here (see delegate.ts's
|
|
9
|
+
* module docs for why) — derive `["config"]` / `["handle", name]` with your
|
|
10
|
+
* runtime's canonical `findProgramAddress`.
|
|
11
|
+
*
|
|
12
|
+
* `register`'s price is computed ON-CHAIN at execution time (time + volume
|
|
13
|
+
* ramp over `Config.tier_lamports`) — there is no client-supplied price or
|
|
14
|
+
* slippage parameter; the payer simply pays whatever the program charges as
|
|
15
|
+
* of the landing slot. {@link fetchConfigTreasury} reads the one `Config`
|
|
16
|
+
* field this builder needs (the treasury address every registration pays).
|
|
17
|
+
*/
|
|
18
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
19
|
+
/** Anchor instruction discriminator: `sha256("global:register")[0..8]`. */
|
|
20
|
+
export const REGISTER_DISCRIMINATOR = Uint8Array.from([
|
|
21
|
+
211, 124, 67, 15, 211, 194, 178, 240,
|
|
22
|
+
]);
|
|
23
|
+
/** Anchor instruction discriminator: `sha256("global:register_with_forti")[0..8]`. */
|
|
24
|
+
export const REGISTER_WITH_FORTI_DISCRIMINATOR = Uint8Array.from([
|
|
25
|
+
48, 73, 211, 175, 49, 236, 133, 82,
|
|
26
|
+
]);
|
|
27
|
+
/** Byte offset of `treasury` within a `Config` account (after the 8-byte
|
|
28
|
+
* discriminator: `admin: Pubkey(32)` then `treasury: Pubkey(32)` at 40). */
|
|
29
|
+
const CONFIG_TREASURY_OFFSET = 40;
|
|
30
|
+
const PUBKEY_LEN = 32;
|
|
31
|
+
function toBytes32(v, what) {
|
|
32
|
+
if (typeof v === "string") {
|
|
33
|
+
const b = decodeBase58_32(v);
|
|
34
|
+
if (!b)
|
|
35
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
36
|
+
return b;
|
|
37
|
+
}
|
|
38
|
+
if (v.length !== 32)
|
|
39
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
40
|
+
return v;
|
|
41
|
+
}
|
|
42
|
+
function toBase58(v, what) {
|
|
43
|
+
return encodeBase58(toBytes32(v, what));
|
|
44
|
+
}
|
|
45
|
+
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
46
|
+
/**
|
|
47
|
+
* Read `Config.treasury` — the account every `register` call pays — given
|
|
48
|
+
* the `["config"]` PDA. Returns null if the account doesn't exist or isn't
|
|
49
|
+
* shaped like a `Config` (too short to hold `treasury`).
|
|
50
|
+
*/
|
|
51
|
+
export async function fetchConfigTreasury(rpc, configAccount) {
|
|
52
|
+
const res = (await rpc("getAccountInfo", [
|
|
53
|
+
configAccount,
|
|
54
|
+
{ encoding: "base64", commitment: "confirmed" },
|
|
55
|
+
]));
|
|
56
|
+
const data = res?.value?.data?.[0];
|
|
57
|
+
if (!data)
|
|
58
|
+
return null;
|
|
59
|
+
const bin = atob(data);
|
|
60
|
+
if (bin.length < CONFIG_TREASURY_OFFSET + PUBKEY_LEN)
|
|
61
|
+
return null;
|
|
62
|
+
const raw = new Uint8Array(bin.length);
|
|
63
|
+
for (let i = 0; i < bin.length; i++)
|
|
64
|
+
raw[i] = bin.charCodeAt(i);
|
|
65
|
+
return encodeBase58(raw.slice(CONFIG_TREASURY_OFFSET, CONFIG_TREASURY_OFFSET + PUBKEY_LEN));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build `register`. Price is computed on-chain (see the module docs) — this
|
|
69
|
+
* builder does not know or assert it; simulate first if you need to show the
|
|
70
|
+
* payer a price before they sign.
|
|
71
|
+
*/
|
|
72
|
+
export function buildRegisterIx(p) {
|
|
73
|
+
if (!Number.isInteger(p.handleType) || p.handleType < 0 || p.handleType > 3) {
|
|
74
|
+
throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
|
|
75
|
+
}
|
|
76
|
+
const nameBytes = new TextEncoder().encode(p.name);
|
|
77
|
+
if (nameBytes.length < 1 || nameBytes.length > 32) {
|
|
78
|
+
throw new Error("name must be 1..=32 bytes (UTF-8)");
|
|
79
|
+
}
|
|
80
|
+
const data = new Uint8Array(8 + 4 + nameBytes.length + 1);
|
|
81
|
+
data.set(REGISTER_DISCRIMINATOR, 0);
|
|
82
|
+
new DataView(data.buffer).setUint32(8, nameBytes.length, true);
|
|
83
|
+
data.set(nameBytes, 12);
|
|
84
|
+
data[12 + nameBytes.length] = p.handleType;
|
|
85
|
+
const hasIntegrator = p.integrator !== undefined || p.integratorAllowlist !== undefined;
|
|
86
|
+
if (hasIntegrator && (p.integrator === undefined || p.integratorAllowlist === undefined)) {
|
|
87
|
+
throw new Error("integrator and integratorAllowlist must both be supplied, or neither");
|
|
88
|
+
}
|
|
89
|
+
const keys = [
|
|
90
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
91
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: false, isWritable: false },
|
|
92
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
|
|
93
|
+
{ pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
|
|
94
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
95
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
96
|
+
];
|
|
97
|
+
if (hasIntegrator) {
|
|
98
|
+
keys.push({ pubkey: toBase58(p.integrator, "integrator"), isSigner: false, isWritable: true }, {
|
|
99
|
+
pubkey: toBase58(p.integratorAllowlist, "integratorAllowlist"),
|
|
100
|
+
isSigner: false,
|
|
101
|
+
isWritable: false,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
105
|
+
}
|
|
106
|
+
// ============================= register_with_forti (#8136, WP #8200) =============================
|
|
107
|
+
/** Byte offsets of the `Config` fields `register_with_forti` needs, verbatim
|
|
108
|
+
* from state.rs's field order (confirmed against `migrate_config_v4`'s own
|
|
109
|
+
* write order in lib.rs) — lying after `treasury` (40) this module's own
|
|
110
|
+
* {@link fetchConfigTreasury} already reads:
|
|
111
|
+
* ...recovery_timelock_secs(i64)@256 lease_rate_divisor(u64)@264
|
|
112
|
+
* lease_grace_period_secs(i64)@272 forti_mint(32)@280
|
|
113
|
+
* forti_treasury_ata(32)@312 veforti_program(32)@344
|
|
114
|
+
* forti_buyback_recipient(32)@376 forti_rate_per_lamport(u64)@408
|
|
115
|
+
* veforti_bonus_bps(u16)@416 forti_buyback_share_bps(u16)@418 */
|
|
116
|
+
const CONFIG_FORTI_MINT_OFFSET = 280;
|
|
117
|
+
const CONFIG_FORTI_TREASURY_ATA_OFFSET = 312;
|
|
118
|
+
const CONFIG_VEFORTI_PROGRAM_OFFSET = 344;
|
|
119
|
+
const CONFIG_FORTI_BUYBACK_RECIPIENT_OFFSET = 376;
|
|
120
|
+
const CONFIG_FORTI_RATE_OFFSET = 408;
|
|
121
|
+
const CONFIG_FORTI_MIN_LEN = CONFIG_FORTI_RATE_OFFSET + 8;
|
|
122
|
+
/** Read the `Config` fields a `register_with_forti` build needs, given the
|
|
123
|
+
* `["config"]` PDA. Returns null if the account doesn't exist or isn't
|
|
124
|
+
* shaped like a post-#8136 `Config` (too short to hold these fields —
|
|
125
|
+
* e.g. a `Config` that hasn't run `migrate_config_v4` yet). */
|
|
126
|
+
export async function fetchConfigForti(rpc, configAccount) {
|
|
127
|
+
const res = (await rpc("getAccountInfo", [
|
|
128
|
+
configAccount,
|
|
129
|
+
{ encoding: "base64", commitment: "confirmed" },
|
|
130
|
+
]));
|
|
131
|
+
const data = res?.value?.data?.[0];
|
|
132
|
+
if (!data)
|
|
133
|
+
return null;
|
|
134
|
+
const bin = atob(data);
|
|
135
|
+
if (bin.length < CONFIG_FORTI_MIN_LEN)
|
|
136
|
+
return null;
|
|
137
|
+
const raw = new Uint8Array(bin.length);
|
|
138
|
+
for (let i = 0; i < bin.length; i++)
|
|
139
|
+
raw[i] = bin.charCodeAt(i);
|
|
140
|
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
141
|
+
return {
|
|
142
|
+
fortiMint: encodeBase58(raw.slice(CONFIG_FORTI_MINT_OFFSET, CONFIG_FORTI_MINT_OFFSET + PUBKEY_LEN)),
|
|
143
|
+
fortiTreasuryAta: encodeBase58(raw.slice(CONFIG_FORTI_TREASURY_ATA_OFFSET, CONFIG_FORTI_TREASURY_ATA_OFFSET + PUBKEY_LEN)),
|
|
144
|
+
veFortiProgram: encodeBase58(raw.slice(CONFIG_VEFORTI_PROGRAM_OFFSET, CONFIG_VEFORTI_PROGRAM_OFFSET + PUBKEY_LEN)),
|
|
145
|
+
fortiBuybackRecipient: encodeBase58(raw.slice(CONFIG_FORTI_BUYBACK_RECIPIENT_OFFSET, CONFIG_FORTI_BUYBACK_RECIPIENT_OFFSET + PUBKEY_LEN)),
|
|
146
|
+
fortiRatePerLamport: dv.getBigUint64(CONFIG_FORTI_RATE_OFFSET, true),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Build `register_with_forti` (#8136) — a SIBLING payment path to
|
|
151
|
+
* `register`, not a replacement. Price is computed on-chain the same way
|
|
152
|
+
* `register`'s is (see that builder's docs) — this builder does not know or
|
|
153
|
+
* assert it; use `/quote/:name?pay_with=forti[&payer=...]` (tools/api) or
|
|
154
|
+
* simulate first if you need to show the payer a price before they sign.
|
|
155
|
+
*/
|
|
156
|
+
export function buildRegisterWithFortiIx(p) {
|
|
157
|
+
if (!Number.isInteger(p.handleType) || p.handleType < 0 || p.handleType > 3) {
|
|
158
|
+
throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
|
|
159
|
+
}
|
|
160
|
+
const nameBytes = new TextEncoder().encode(p.name);
|
|
161
|
+
if (nameBytes.length < 1 || nameBytes.length > 32) {
|
|
162
|
+
throw new Error("name must be 1..=32 bytes (UTF-8)");
|
|
163
|
+
}
|
|
164
|
+
const data = new Uint8Array(8 + 4 + nameBytes.length + 1);
|
|
165
|
+
data.set(REGISTER_WITH_FORTI_DISCRIMINATOR, 0);
|
|
166
|
+
new DataView(data.buffer).setUint32(8, nameBytes.length, true);
|
|
167
|
+
data.set(nameBytes, 12);
|
|
168
|
+
data[12 + nameBytes.length] = p.handleType;
|
|
169
|
+
const keys = [
|
|
170
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
171
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: false, isWritable: false },
|
|
172
|
+
{ pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
|
|
173
|
+
{ pubkey: toBase58(p.fortiMint, "fortiMint"), isSigner: false, isWritable: false },
|
|
174
|
+
{ pubkey: toBase58(p.payerFortiAccount, "payerFortiAccount"), isSigner: false, isWritable: true },
|
|
175
|
+
{ pubkey: toBase58(p.fortiTreasuryAta, "fortiTreasuryAta"), isSigner: false, isWritable: true },
|
|
176
|
+
{ pubkey: toBase58(p.fortiBuybackRecipient, "fortiBuybackRecipient"), isSigner: false, isWritable: true },
|
|
177
|
+
{ pubkey: toBase58(p.veFortiLock, "veFortiLock"), isSigner: false, isWritable: false },
|
|
178
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
|
|
179
|
+
{ pubkey: toBase58(p.tokenProgram, "tokenProgram"), isSigner: false, isWritable: false },
|
|
180
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
181
|
+
];
|
|
182
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
183
|
+
}
|