@x1id/resolve 0.3.0 → 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/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/index.d.ts +6 -0
- package/dist/index.js +15 -2
- 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/x402.d.ts +107 -0
- package/dist/x402.js +76 -0
- package/package.json +45 -9
- package/schema/agent-manifest.json +91 -0
- package/wasm/x1_resolve_wasm.wasm +0 -0
package/dist/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
|
+
}
|
package/dist/subname.d.ts
CHANGED
|
@@ -259,6 +259,11 @@ export declare function fetchSubnames(rpc: RpcFn, programId: string, parentHandl
|
|
|
259
259
|
* This does NOT apply layer 1 (subname-vs-parent). Use
|
|
260
260
|
* {@link resolveSubnameRecords} for the complete, safe resolution — or gate
|
|
261
261
|
* this call on {@link subnameIsLive} yourself.
|
|
262
|
+
*
|
|
263
|
+
* `recordsClearedAt` is fixed at `0n`, not the parent handle's: `clear_records`
|
|
264
|
+
* (#8139) only ever writes the PARENT `Handle` account's own extension — a
|
|
265
|
+
* subname is a separate account with no `records_cleared_at` of its own, so
|
|
266
|
+
* the parent's clear has no bearing on records keyed by the subname's pubkey.
|
|
262
267
|
*/
|
|
263
268
|
export declare function fetchSubnameRecords(rpc: RpcFn, programId: string, subnameAccount: string, subnameCreatedAt: bigint): Promise<HandleRecord[]>;
|
|
264
269
|
/**
|
package/dist/subname.js
CHANGED
|
@@ -339,12 +339,17 @@ export async function fetchSubnames(rpc, programId, parentHandleAccount, parentR
|
|
|
339
339
|
* This does NOT apply layer 1 (subname-vs-parent). Use
|
|
340
340
|
* {@link resolveSubnameRecords} for the complete, safe resolution — or gate
|
|
341
341
|
* this call on {@link subnameIsLive} yourself.
|
|
342
|
+
*
|
|
343
|
+
* `recordsClearedAt` is fixed at `0n`, not the parent handle's: `clear_records`
|
|
344
|
+
* (#8139) only ever writes the PARENT `Handle` account's own extension — a
|
|
345
|
+
* subname is a separate account with no `records_cleared_at` of its own, so
|
|
346
|
+
* the parent's clear has no bearing on records keyed by the subname's pubkey.
|
|
342
347
|
*/
|
|
343
348
|
export async function fetchSubnameRecords(rpc, programId, subnameAccount, subnameCreatedAt) {
|
|
344
349
|
// The record filter already includes size + Record discriminator; the
|
|
345
350
|
// handle-field memcmp is the subname pubkey here. Reuse the exact scanner so
|
|
346
351
|
// subname records and handle records can never decode differently.
|
|
347
|
-
return fetchRecords(rpc, programId, subnameAccount, subnameCreatedAt);
|
|
352
|
+
return fetchRecords(rpc, programId, subnameAccount, subnameCreatedAt, 0n);
|
|
348
353
|
}
|
|
349
354
|
/**
|
|
350
355
|
* Resolve a subname's LIVE address records — the ONLY safe entry point, both
|
package/dist/textRecords.d.ts
CHANGED
|
@@ -45,6 +45,14 @@
|
|
|
45
45
|
* There is deliberately no way to decode or fetch a text record through
|
|
46
46
|
* this module without the handle's `registered_at` in hand.
|
|
47
47
|
*
|
|
48
|
+
* # `records_cleared_at` (#8139) — the SECOND, independent staleness rule
|
|
49
|
+
*
|
|
50
|
+
* `clear_records` lets an owner bulk-invalidate every record RIGHT NOW,
|
|
51
|
+
* without a transfer — text records too. The same functions therefore also
|
|
52
|
+
* demand the handle's `recordsClearedAt` (0 if never cleared, from
|
|
53
|
+
* `ParsedHandle`), and a text record is `stale` when EITHER
|
|
54
|
+
* `updated_at < registeredAt` OR `updated_at < recordsClearedAt`.
|
|
55
|
+
*
|
|
48
56
|
* # These records count (#7384)
|
|
49
57
|
*
|
|
50
58
|
* `create_text_record` increments — and `close_text_record` decrements —
|
|
@@ -140,15 +148,17 @@ export declare function textValueToString(value: Uint8Array): string | null;
|
|
|
140
148
|
* value: Vec<u8> (u32 len + bytes, max 256) updated_at: i64(8) bump: u8(1)
|
|
141
149
|
*
|
|
142
150
|
* `registeredAt` is the owning Handle's `registered_at`, decoded by the
|
|
143
|
-
* caller from the Handle account — it decides `stale`.
|
|
144
|
-
*
|
|
151
|
+
* caller from the Handle account — it decides `stale`. `recordsClearedAt` is
|
|
152
|
+
* that same Handle's `recordsClearedAt` (0 if never cleared, #8139), a
|
|
153
|
+
* SECOND independent staleness anchor. There is intentionally no overload
|
|
154
|
+
* without either (see the module docs).
|
|
145
155
|
*
|
|
146
156
|
* Returns null for anything that is not a TextRecord: wrong length, wrong
|
|
147
157
|
* discriminator, lengths that do not fit, or a key that is not valid UTF-8
|
|
148
158
|
* (the program's Borsh `String` guarantees it is, so that is not a
|
|
149
159
|
* TextRecord).
|
|
150
160
|
*/
|
|
151
|
-
export declare function decodeTextRecord(raw: Uint8Array, account: string, registeredAt: bigint): HandleTextRecord | null;
|
|
161
|
+
export declare function decodeTextRecord(raw: Uint8Array, account: string, registeredAt: bigint, recordsClearedAt: bigint): HandleTextRecord | null;
|
|
152
162
|
/** The text records the CURRENT owner actually has — `stale` ones excluded.
|
|
153
163
|
* This is the list to render on a profile and count. */
|
|
154
164
|
export declare function liveTextRecords(records: readonly HandleTextRecord[]): HandleTextRecord[];
|
|
@@ -162,12 +172,13 @@ export declare function liveTextRecords(records: readonly HandleTextRecord[]): H
|
|
|
162
172
|
*
|
|
163
173
|
* `registeredAt` is `Handle.registered_at` as decoded from the Handle
|
|
164
174
|
* account the caller already has — the staleness rule needs it, and there
|
|
165
|
-
* is no variant of this function without it.
|
|
166
|
-
*
|
|
167
|
-
*
|
|
175
|
+
* is no variant of this function without it. `recordsClearedAt` is that same
|
|
176
|
+
* Handle's `recordsClearedAt` (0 if never cleared, #8139) — pass it through.
|
|
177
|
+
* Every record is returned, stale ones flagged, so an owner surface can show
|
|
178
|
+
* what a previous owner left behind; anything that renders a profile takes
|
|
168
179
|
* {@link liveTextRecords}.
|
|
169
180
|
*/
|
|
170
|
-
export declare function fetchTextRecords(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint): Promise<HandleTextRecord[]>;
|
|
181
|
+
export declare function fetchTextRecords(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint): Promise<HandleTextRecord[]>;
|
|
171
182
|
/** The delegate-aware trailing accounts every text-record builder shares —
|
|
172
183
|
* the exact rules of `delegate.ts`'s module docs. */
|
|
173
184
|
interface RecordEditAuthorityParams {
|
package/dist/textRecords.js
CHANGED
|
@@ -45,6 +45,14 @@
|
|
|
45
45
|
* There is deliberately no way to decode or fetch a text record through
|
|
46
46
|
* this module without the handle's `registered_at` in hand.
|
|
47
47
|
*
|
|
48
|
+
* # `records_cleared_at` (#8139) — the SECOND, independent staleness rule
|
|
49
|
+
*
|
|
50
|
+
* `clear_records` lets an owner bulk-invalidate every record RIGHT NOW,
|
|
51
|
+
* without a transfer — text records too. The same functions therefore also
|
|
52
|
+
* demand the handle's `recordsClearedAt` (0 if never cleared, from
|
|
53
|
+
* `ParsedHandle`), and a text record is `stale` when EITHER
|
|
54
|
+
* `updated_at < registeredAt` OR `updated_at < recordsClearedAt`.
|
|
55
|
+
*
|
|
48
56
|
* # These records count (#7384)
|
|
49
57
|
*
|
|
50
58
|
* `create_text_record` increments — and `close_text_record` decrements —
|
|
@@ -164,15 +172,17 @@ export function textValueToString(value) {
|
|
|
164
172
|
* value: Vec<u8> (u32 len + bytes, max 256) updated_at: i64(8) bump: u8(1)
|
|
165
173
|
*
|
|
166
174
|
* `registeredAt` is the owning Handle's `registered_at`, decoded by the
|
|
167
|
-
* caller from the Handle account — it decides `stale`.
|
|
168
|
-
*
|
|
175
|
+
* caller from the Handle account — it decides `stale`. `recordsClearedAt` is
|
|
176
|
+
* that same Handle's `recordsClearedAt` (0 if never cleared, #8139), a
|
|
177
|
+
* SECOND independent staleness anchor. There is intentionally no overload
|
|
178
|
+
* without either (see the module docs).
|
|
169
179
|
*
|
|
170
180
|
* Returns null for anything that is not a TextRecord: wrong length, wrong
|
|
171
181
|
* discriminator, lengths that do not fit, or a key that is not valid UTF-8
|
|
172
182
|
* (the program's Borsh `String` guarantees it is, so that is not a
|
|
173
183
|
* TextRecord).
|
|
174
184
|
*/
|
|
175
|
-
export function decodeTextRecord(raw, account, registeredAt) {
|
|
185
|
+
export function decodeTextRecord(raw, account, registeredAt, recordsClearedAt) {
|
|
176
186
|
if (raw.length !== TEXT_RECORD_LEN)
|
|
177
187
|
return null;
|
|
178
188
|
for (let i = 0; i < 8; i++) {
|
|
@@ -207,7 +217,7 @@ export function decodeTextRecord(raw, account, registeredAt) {
|
|
|
207
217
|
value,
|
|
208
218
|
text: textValueToString(value),
|
|
209
219
|
updatedAt,
|
|
210
|
-
stale: updatedAt < registeredAt,
|
|
220
|
+
stale: updatedAt < registeredAt || updatedAt < recordsClearedAt,
|
|
211
221
|
};
|
|
212
222
|
}
|
|
213
223
|
/** The text records the CURRENT owner actually has — `stale` ones excluded.
|
|
@@ -225,12 +235,13 @@ export function liveTextRecords(records) {
|
|
|
225
235
|
*
|
|
226
236
|
* `registeredAt` is `Handle.registered_at` as decoded from the Handle
|
|
227
237
|
* account the caller already has — the staleness rule needs it, and there
|
|
228
|
-
* is no variant of this function without it.
|
|
229
|
-
*
|
|
230
|
-
*
|
|
238
|
+
* is no variant of this function without it. `recordsClearedAt` is that same
|
|
239
|
+
* Handle's `recordsClearedAt` (0 if never cleared, #8139) — pass it through.
|
|
240
|
+
* Every record is returned, stale ones flagged, so an owner surface can show
|
|
241
|
+
* what a previous owner left behind; anything that renders a profile takes
|
|
231
242
|
* {@link liveTextRecords}.
|
|
232
243
|
*/
|
|
233
|
-
export async function fetchTextRecords(rpc, programId, handleAccount, registeredAt) {
|
|
244
|
+
export async function fetchTextRecords(rpc, programId, handleAccount, registeredAt, recordsClearedAt) {
|
|
234
245
|
const res = (await rpc("getProgramAccounts", [
|
|
235
246
|
programId,
|
|
236
247
|
{
|
|
@@ -249,7 +260,7 @@ export async function fetchTextRecords(rpc, programId, handleAccount, registered
|
|
|
249
260
|
const raw = new Uint8Array(bin.length);
|
|
250
261
|
for (let i = 0; i < bin.length; i++)
|
|
251
262
|
raw[i] = bin.charCodeAt(i);
|
|
252
|
-
const decoded = decodeTextRecord(raw, a.pubkey, registeredAt);
|
|
263
|
+
const decoded = decodeTextRecord(raw, a.pubkey, registeredAt, recordsClearedAt);
|
|
253
264
|
// Defence in depth: the memcmp filter should guarantee the handle
|
|
254
265
|
// match, but a wrong offset would silently attribute someone else's
|
|
255
266
|
// record to this handle. A malformed account is skipped, not fatal.
|