@x1id/resolve 0.2.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -8
- package/dist/accounts.d.ts +93 -0
- package/dist/accounts.js +190 -0
- package/dist/agent.d.ts +186 -0
- package/dist/agent.js +213 -0
- package/dist/attestation.d.ts +226 -0
- package/dist/attestation.js +290 -0
- package/dist/clearRecords.d.ts +60 -0
- package/dist/clearRecords.js +69 -0
- package/dist/control.d.ts +201 -0
- package/dist/control.js +316 -0
- package/dist/delegate.d.ts +153 -0
- package/dist/delegate.js +166 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +66 -179
- package/dist/integrator.d.ts +117 -0
- package/dist/integrator.js +166 -0
- package/dist/lock.d.ts +180 -0
- package/dist/lock.js +211 -0
- package/dist/recordCount.d.ts +98 -0
- package/dist/recordCount.js +114 -0
- package/dist/recordWrite.d.ts +136 -0
- package/dist/recordWrite.js +228 -0
- package/dist/records.d.ts +130 -0
- package/dist/records.js +195 -0
- package/dist/register.d.ts +119 -0
- package/dist/register.js +183 -0
- package/dist/subname.d.ts +282 -0
- package/dist/subname.js +371 -0
- package/dist/textRecords.d.ts +259 -0
- package/dist/textRecords.js +368 -0
- package/dist/voucher.d.ts +130 -0
- package/dist/voucher.js +185 -0
- package/dist/x402.d.ts +107 -0
- package/dist/x402.js +76 -0
- package/package.json +9 -1
- package/schema/agent-manifest.json +91 -0
- package/wasm/x1_resolve_wasm.wasm +0 -0
package/dist/records.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-chain payment records (`Record` accounts) — read WITH the universal
|
|
3
|
+
* staleness rule from docs/record-trust.md structurally enforced.
|
|
4
|
+
*
|
|
5
|
+
* # Why every function here demands `registeredAt`
|
|
6
|
+
*
|
|
7
|
+
* A `Handle`'s address is `["handle", name]` — a pure function of the name.
|
|
8
|
+
* Release + re-register lands the new registration at the SAME pubkey, and
|
|
9
|
+
* `Record` PDAs (`["record", handle_pubkey, coin_type]`) hang off that pubkey,
|
|
10
|
+
* so a record a PREVIOUS owner created (possibly `verified: true` for THEIR
|
|
11
|
+
* address) is physically attached to the new owner's name with no action by
|
|
12
|
+
* anyone. The program cannot prevent this (state.rs, `Handle` doc comment);
|
|
13
|
+
* the mandatory read-side rule is:
|
|
14
|
+
*
|
|
15
|
+
* record.updated_at >= handle.registered_at
|
|
16
|
+
*
|
|
17
|
+
* A record that fails it belongs to a previous, unrelated owner and must be
|
|
18
|
+
* treated as unset/unverified — for PAYMENT resolution, not just badges.
|
|
19
|
+
* There is deliberately no way to decode or fetch a record through this
|
|
20
|
+
* module without the handle's `registered_at` in hand, mirroring
|
|
21
|
+
* app/src/lib/x1/records.ts. See docs/record-trust.md for the full argument.
|
|
22
|
+
*
|
|
23
|
+
* # `records_cleared_at` (#8139) — the SECOND, independent staleness rule
|
|
24
|
+
*
|
|
25
|
+
* `clear_records` lets an owner bulk-invalidate every record RIGHT NOW,
|
|
26
|
+
* without a transfer. The same functions therefore also demand the handle's
|
|
27
|
+
* `recordsClearedAt` (0 if never cleared, from `ParsedHandle`) and a record
|
|
28
|
+
* is `stale` when EITHER `updated_at < registeredAt` OR
|
|
29
|
+
* `updated_at < recordsClearedAt` — see `Handle::read_records_cleared_at`'s
|
|
30
|
+
* doc comment in state.rs.
|
|
31
|
+
*/
|
|
32
|
+
import { encodeBase58 } from "./base58.js";
|
|
33
|
+
import { CHAIN_COIN_TYPE } from "./types.js";
|
|
34
|
+
/** `8 + Record::INIT_SPACE` — the program allocates the full 64-byte `value`
|
|
35
|
+
* capacity, so every Record account is exactly this long:
|
|
36
|
+
* 8 + 32 + 4 + (4 + 64) + 1 + 8 + 1. */
|
|
37
|
+
export const RECORD_LEN = 122;
|
|
38
|
+
/** `sha256("account:Record")[..8]`, hex — the tag every Record account's
|
|
39
|
+
* data starts with. Pinned (this SDK is zero-dependency and cannot assume
|
|
40
|
+
* WebCrypto SHA-256 everywhere it runs); the value is asserted against a
|
|
41
|
+
* re-derivation in the test suite so a typo can never silently pass. */
|
|
42
|
+
export const RECORD_DISC = "fee975fc4ca6928b";
|
|
43
|
+
/** Byte offset of `handle` within a Record account: the 8-byte discriminator. */
|
|
44
|
+
const RECORD_HANDLE_OFFSET = 8;
|
|
45
|
+
/** Reverse of CHAIN_COIN_TYPE — which chain a stored coin_type belongs to, or
|
|
46
|
+
* null for a coin_type this SDK does not know how to render. */
|
|
47
|
+
export function chainForCoinType(coinType) {
|
|
48
|
+
for (const c of Object.keys(CHAIN_COIN_TYPE)) {
|
|
49
|
+
if (CHAIN_COIN_TYPE[c] === coinType)
|
|
50
|
+
return c;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Decode one Record account.
|
|
56
|
+
*
|
|
57
|
+
* Record account layout, after the 8-byte Anchor discriminator:
|
|
58
|
+
*
|
|
59
|
+
* handle: Pubkey(32) coin_type: u32(4) value: Vec<u8> (u32 len + bytes, max 64)
|
|
60
|
+
* verified: bool(1) updated_at: i64(8) bump: u8(1)
|
|
61
|
+
*
|
|
62
|
+
* `registeredAt` is the owning Handle's `registered_at`, decoded by the
|
|
63
|
+
* caller from the Handle account — it decides `stale` (and so `verified`).
|
|
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.
|
|
68
|
+
*
|
|
69
|
+
* Returns null for anything that is not a Record: wrong length, wrong
|
|
70
|
+
* discriminator, or a `value` length that does not fit. The Listing / Offer /
|
|
71
|
+
* Auction PDAs of the same handle also carry the handle pubkey at offset 8,
|
|
72
|
+
* so a handle-only memcmp scan DOES return them — they are rejected here by
|
|
73
|
+
* tag and size, not by luck.
|
|
74
|
+
*/
|
|
75
|
+
export function decodeRecord(raw, account, registeredAt, recordsClearedAt) {
|
|
76
|
+
if (raw.length !== RECORD_LEN)
|
|
77
|
+
return null;
|
|
78
|
+
for (let i = 0; i < 8; i++) {
|
|
79
|
+
if (raw[i] !== parseInt(RECORD_DISC.slice(i * 2, i * 2 + 2), 16))
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
83
|
+
let o = 8;
|
|
84
|
+
const handle = encodeBase58(raw.slice(o, o + 32));
|
|
85
|
+
o += 32;
|
|
86
|
+
const coinType = dv.getUint32(o, true);
|
|
87
|
+
o += 4;
|
|
88
|
+
const len = dv.getUint32(o, true);
|
|
89
|
+
o += 4;
|
|
90
|
+
// `#[max_len(64)]` — and the fixed fields after `value` must still fit.
|
|
91
|
+
if (len > 64 || o + len + 1 + 8 + 1 > raw.length)
|
|
92
|
+
return null;
|
|
93
|
+
const value = raw.slice(o, o + len);
|
|
94
|
+
o += len;
|
|
95
|
+
const verified = raw[o] === 1;
|
|
96
|
+
o += 1;
|
|
97
|
+
const updatedAt = dv.getBigInt64(o, true);
|
|
98
|
+
const stale = updatedAt < registeredAt || updatedAt < recordsClearedAt;
|
|
99
|
+
const chain = chainForCoinType(coinType);
|
|
100
|
+
return {
|
|
101
|
+
account,
|
|
102
|
+
handle,
|
|
103
|
+
coinType,
|
|
104
|
+
chain,
|
|
105
|
+
value,
|
|
106
|
+
address: valueToAddress(chain, value),
|
|
107
|
+
verified: verified && !stale,
|
|
108
|
+
updatedAt,
|
|
109
|
+
stale,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** The records the CURRENT owner actually has — `stale` ones excluded. This
|
|
113
|
+
* is the list to resolve against, display to a visitor, and count. */
|
|
114
|
+
export function liveRecords(records) {
|
|
115
|
+
return records.filter((r) => !r.stale);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Render stored record bytes for display / payment, per the conventions
|
|
119
|
+
* app/src/lib/x1/records.ts writes them with: X1/SOL are 32-byte pubkeys
|
|
120
|
+
* (base58), ETH is 20 raw bytes (0x-hex), BTC is the UTF-8 bytes of the
|
|
121
|
+
* canonical address string. Unknown coin types render as hex so a value is
|
|
122
|
+
* never silently hidden.
|
|
123
|
+
*/
|
|
124
|
+
export function valueToAddress(chain, value) {
|
|
125
|
+
const hex = () => `0x${Array.from(value, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
126
|
+
if (chain === "X1" || chain === "SOL") {
|
|
127
|
+
return value.length === 32 ? encodeBase58(value) : hex();
|
|
128
|
+
}
|
|
129
|
+
if (chain === "ETH")
|
|
130
|
+
return hex();
|
|
131
|
+
if (chain === "BTC") {
|
|
132
|
+
try {
|
|
133
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(value);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return hex();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return hex();
|
|
140
|
+
}
|
|
141
|
+
function hexToBytes(hex) {
|
|
142
|
+
const out = new Uint8Array(hex.length / 2);
|
|
143
|
+
for (let i = 0; i < out.length; i++)
|
|
144
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Fetch every Record account of a handle — one `getProgramAccounts` call,
|
|
149
|
+
* filtered by the RPC on size (122), the Record discriminator at offset 0
|
|
150
|
+
* and the handle pubkey at offset 8, then every byte re-checked locally
|
|
151
|
+
* (the node's filters are an optimisation, never the guarantee). Scoping the
|
|
152
|
+
* scan to the registry program id also IS the ownership check: a foreign
|
|
153
|
+
* account cannot appear in it.
|
|
154
|
+
*
|
|
155
|
+
* `registeredAt` is `Handle.registered_at` as decoded from the Handle
|
|
156
|
+
* account the caller already has — the staleness rule needs it, and there
|
|
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(...)`.
|
|
166
|
+
*/
|
|
167
|
+
export async function fetchRecords(rpc, programId, handleAccount, registeredAt, recordsClearedAt) {
|
|
168
|
+
const res = (await rpc("getProgramAccounts", [
|
|
169
|
+
programId,
|
|
170
|
+
{
|
|
171
|
+
encoding: "base64",
|
|
172
|
+
commitment: "confirmed",
|
|
173
|
+
filters: [
|
|
174
|
+
{ dataSize: RECORD_LEN },
|
|
175
|
+
{ memcmp: { offset: 0, bytes: encodeBase58(hexToBytes(RECORD_DISC)) } },
|
|
176
|
+
{ memcmp: { offset: RECORD_HANDLE_OFFSET, bytes: handleAccount } },
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
]));
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const a of res ?? []) {
|
|
182
|
+
const bin = atob(a.account.data[0]);
|
|
183
|
+
const raw = new Uint8Array(bin.length);
|
|
184
|
+
for (let i = 0; i < bin.length; i++)
|
|
185
|
+
raw[i] = bin.charCodeAt(i);
|
|
186
|
+
const r = decodeRecord(raw, a.pubkey, registeredAt, recordsClearedAt);
|
|
187
|
+
// Defence in depth: the memcmp filter should guarantee the handle match,
|
|
188
|
+
// but a wrong offset would silently attribute someone else's record to
|
|
189
|
+
// this handle. A malformed account is skipped, not fatal to the list.
|
|
190
|
+
if (r && r.handle === handleAccount)
|
|
191
|
+
out.push(r);
|
|
192
|
+
}
|
|
193
|
+
out.sort((x, y) => (x.chain ?? "").localeCompare(y.chain ?? "") || x.coinType - y.coinType);
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
@@ -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
|
+
}
|