@x1id/resolve 0.21.0 → 0.22.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 +3 -1
- package/dist/adminConfig.d.ts +17 -3
- package/dist/adminConfig.js +24 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/namespacePricing.d.ts +207 -0
- package/dist/namespacePricing.js +378 -0
- package/dist/sha256.d.ts +10 -0
- package/dist/sha256.js +77 -0
- package/dist/voucher.d.ts +25 -7
- package/dist/voucher.js +23 -13
- package/dist/x402.d.ts +70 -6
- package/dist/x402.js +158 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -605,8 +605,10 @@ const requirements = buildX402Requirements({
|
|
|
605
605
|
buyer: buyerAddress, // x1id v1 is self-sponsored: buyer pays their own fee, no facilitator
|
|
606
606
|
network: X1_MAINNET_NETWORK, // defaults to mainnet; a server on another cluster
|
|
607
607
|
// should pass x402NetworkForGenesis(await getGenesisHash)
|
|
608
|
-
|
|
608
|
+
reference: invoiceReference, // a unique base58 reference per invoice (the payment carries it)
|
|
609
609
|
});
|
|
610
|
+
// Buyer side for api.x1id.io: sign x402RedeemMessage(reference) with the paying
|
|
611
|
+
// wallet and send it as payload.payerSig next to payload.signature.
|
|
610
612
|
```
|
|
611
613
|
|
|
612
614
|
This package only builds the requirements object — it does not build, sign,
|
package/dist/adminConfig.d.ts
CHANGED
|
@@ -44,6 +44,18 @@ export declare const SET_LEASE_GRACE_PERIOD_DISCRIMINATOR: Uint8Array;
|
|
|
44
44
|
export declare const SET_RECOVERY_TIMELOCK_DISCRIMINATOR: Uint8Array;
|
|
45
45
|
export declare const SET_MIN_COMMITMENT_AGE_DISCRIMINATOR: Uint8Array;
|
|
46
46
|
export declare const SET_MAX_COMMITMENT_AGE_DISCRIMINATOR: Uint8Array;
|
|
47
|
+
/** `sha256("global:set_namespace_creation_fee")[0..8]` — sets
|
|
48
|
+
* `Config.namespace_creation_fee_lamports` (u64, the permissionless
|
|
49
|
+
* `create_namespace` TLD creation fee paid to treasury). */
|
|
50
|
+
export declare const SET_NAMESPACE_CREATION_FEE_DISCRIMINATOR: Uint8Array;
|
|
51
|
+
/** Byte offset of `Config.namespace_creation_fee_lamports` (u64 LE) in the
|
|
52
|
+
* Config account data, including the 8-byte Anchor discriminator — mirrors
|
|
53
|
+
* `Config::NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET` (state.rs). */
|
|
54
|
+
export declare const NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET = 436;
|
|
55
|
+
/** Decode `Config.namespace_creation_fee_lamports` from raw Config account
|
|
56
|
+
* data. Returns null for a Config too short to carry the field (pre-v6
|
|
57
|
+
* layout) — never a guessed default. */
|
|
58
|
+
export declare function decodeNamespaceCreationFeeLamports(configData: Uint8Array): bigint | null;
|
|
47
59
|
/** Map an admin config action name → its discriminator (for a relay executor
|
|
48
60
|
* that dispatches on the action string). set_admin/set_attestor intentionally
|
|
49
61
|
* absent — those are owner-gated, never dispatched here. */
|
|
@@ -54,10 +66,12 @@ export interface AdminScalarParams {
|
|
|
54
66
|
readonly config: AddressLike;
|
|
55
67
|
/** The discriminator for the specific setter (from ADMIN_SCALAR_DISCRIMINATORS). */
|
|
56
68
|
readonly discriminator: Uint8Array;
|
|
57
|
-
/** The integer arg (u64/i64 encoding is identical for a non-negative value).
|
|
58
|
-
|
|
69
|
+
/** The integer arg (u64/i64 encoding is identical for a non-negative value).
|
|
70
|
+
* A string must be plain decimal digits (lamports amounts beyond JS's safe
|
|
71
|
+
* integer range arrive from the admin queue as decimal strings). */
|
|
72
|
+
readonly value: number | bigint | string;
|
|
59
73
|
}
|
|
60
74
|
/** Build any one-integer-arg AdminOnly setter (lease rate divisor, lease grace,
|
|
61
|
-
* recovery timelock, min/max commitment age). disc(8) + value as little-endian
|
|
75
|
+
* recovery timelock, min/max commitment age, namespace creation fee). disc(8) + value as little-endian
|
|
62
76
|
* u64(8). The value must be a non-negative integer that fits u64. */
|
|
63
77
|
export declare function buildAdminScalarSetterIx(p: AdminScalarParams): BuiltInstruction;
|
package/dist/adminConfig.js
CHANGED
|
@@ -84,6 +84,22 @@ export const SET_LEASE_GRACE_PERIOD_DISCRIMINATOR = Uint8Array.from([212, 2, 110
|
|
|
84
84
|
export const SET_RECOVERY_TIMELOCK_DISCRIMINATOR = Uint8Array.from([178, 183, 148, 243, 155, 166, 79, 6]);
|
|
85
85
|
export const SET_MIN_COMMITMENT_AGE_DISCRIMINATOR = Uint8Array.from([181, 20, 114, 43, 51, 23, 47, 248]);
|
|
86
86
|
export const SET_MAX_COMMITMENT_AGE_DISCRIMINATOR = Uint8Array.from([163, 106, 196, 206, 146, 156, 79, 249]);
|
|
87
|
+
/** `sha256("global:set_namespace_creation_fee")[0..8]` — sets
|
|
88
|
+
* `Config.namespace_creation_fee_lamports` (u64, the permissionless
|
|
89
|
+
* `create_namespace` TLD creation fee paid to treasury). */
|
|
90
|
+
export const SET_NAMESPACE_CREATION_FEE_DISCRIMINATOR = Uint8Array.from([158, 152, 239, 141, 143, 5, 75, 198]);
|
|
91
|
+
/** Byte offset of `Config.namespace_creation_fee_lamports` (u64 LE) in the
|
|
92
|
+
* Config account data, including the 8-byte Anchor discriminator — mirrors
|
|
93
|
+
* `Config::NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET` (state.rs). */
|
|
94
|
+
export const NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET = 436;
|
|
95
|
+
/** Decode `Config.namespace_creation_fee_lamports` from raw Config account
|
|
96
|
+
* data. Returns null for a Config too short to carry the field (pre-v6
|
|
97
|
+
* layout) — never a guessed default. */
|
|
98
|
+
export function decodeNamespaceCreationFeeLamports(configData) {
|
|
99
|
+
if (configData.length < NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET + 8)
|
|
100
|
+
return null;
|
|
101
|
+
return new DataView(configData.buffer, configData.byteOffset, configData.byteLength).getBigUint64(NAMESPACE_CREATION_FEE_LAMPORTS_OFFSET, true);
|
|
102
|
+
}
|
|
87
103
|
/** Map an admin config action name → its discriminator (for a relay executor
|
|
88
104
|
* that dispatches on the action string). set_admin/set_attestor intentionally
|
|
89
105
|
* absent — those are owner-gated, never dispatched here. */
|
|
@@ -93,11 +109,18 @@ export const ADMIN_SCALAR_DISCRIMINATORS = {
|
|
|
93
109
|
set_recovery_timelock: SET_RECOVERY_TIMELOCK_DISCRIMINATOR,
|
|
94
110
|
set_min_commitment_age: SET_MIN_COMMITMENT_AGE_DISCRIMINATOR,
|
|
95
111
|
set_max_commitment_age: SET_MAX_COMMITMENT_AGE_DISCRIMINATOR,
|
|
112
|
+
set_namespace_creation_fee: SET_NAMESPACE_CREATION_FEE_DISCRIMINATOR,
|
|
96
113
|
};
|
|
97
114
|
/** Build any one-integer-arg AdminOnly setter (lease rate divisor, lease grace,
|
|
98
|
-
* recovery timelock, min/max commitment age). disc(8) + value as little-endian
|
|
115
|
+
* recovery timelock, min/max commitment age, namespace creation fee). disc(8) + value as little-endian
|
|
99
116
|
* u64(8). The value must be a non-negative integer that fits u64. */
|
|
100
117
|
export function buildAdminScalarSetterIx(p) {
|
|
118
|
+
if (typeof p.value === "string" && !/^\d{1,20}$/.test(p.value)) {
|
|
119
|
+
throw new Error(`value ${JSON.stringify(p.value)} is not a non-negative decimal integer`);
|
|
120
|
+
}
|
|
121
|
+
if (typeof p.value === "number" && !Number.isSafeInteger(p.value)) {
|
|
122
|
+
throw new Error(`value ${p.value} is not a safe integer`);
|
|
123
|
+
}
|
|
101
124
|
const raw = typeof p.value === "bigint" ? p.value : BigInt(p.value);
|
|
102
125
|
if (raw < 0n || raw > 0xffffffffffffffffn) {
|
|
103
126
|
throw new Error(`value ${p.value} is out of u64 range`);
|
package/dist/index.d.ts
CHANGED
|
@@ -126,4 +126,5 @@ export interface Resolver {
|
|
|
126
126
|
export declare function createResolver(config: ResolverConfig): Resolver;
|
|
127
127
|
export * from "./adminMarket.js";
|
|
128
128
|
export * from "./namespaceOverride.js";
|
|
129
|
+
export * from "./namespacePricing.js";
|
|
129
130
|
export * from "./universal.js";
|
package/dist/index.js
CHANGED
|
@@ -816,6 +816,7 @@ export function createResolver(config) {
|
|
|
816
816
|
}
|
|
817
817
|
export * from "./adminMarket.js";
|
|
818
818
|
export * from "./namespaceOverride.js";
|
|
819
|
+
export * from "./namespacePricing.js";
|
|
819
820
|
// The X1ID Universal Resolver (#8475): cross-namespace resolution (.sol via SNS,
|
|
820
821
|
// .eth via ENS) alongside the native path. Additive — the native resolver above
|
|
821
822
|
// is untouched; `createUniversalResolver` composes it with external adapters.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLD creator pricing (docs/specs/tld-creator-pricing.md): the SDK side of a
|
|
3
|
+
* TLD creator's own price table for names under their TLD.
|
|
4
|
+
*
|
|
5
|
+
* - `["ns_pricing", label]` holds a `NamespacePricing` account: a flat lamport
|
|
6
|
+
* price per length band (index = `tier_index(len)`: 0 = 1 char … 8 = 9 chars,
|
|
7
|
+
* 9 = 10+), written ONLY by the namespace creator via `set_namespace_pricing`.
|
|
8
|
+
* - `register_under_namespace_v2` (v1's accounts + the pricing PDA as the
|
|
9
|
+
* first remaining account) charges that band FLAT (no launch ramp) when
|
|
10
|
+
* the account exists, else the legacy root price (`Config.tier_lamports` ×
|
|
11
|
+
* the time/volume ramp), and refuses to charge more than the buyer's
|
|
12
|
+
* `max_price_lamports`.
|
|
13
|
+
* - The legacy `register_under_namespace` (v1) fails closed
|
|
14
|
+
* (`NamespacePricingRequiresV2`, 6117) on any TLD whose creator opted in.
|
|
15
|
+
*
|
|
16
|
+
* {@link quoteScopedName} reproduces the program's price selection exactly, so
|
|
17
|
+
* pass its `lamports` as `maxPriceLamports` to {@link buildRegisterUnderNamespaceV2Ix}.
|
|
18
|
+
*
|
|
19
|
+
* Hand-rolled like the rest of this package: no Anchor client, no
|
|
20
|
+
* `@solana/web3.js` import, no `@types/node`. PDAs are derived through an
|
|
21
|
+
* INJECTED `find_program_address` (see {@link PdaFinder}) because this package
|
|
22
|
+
* never hand-rolls the ed25519 on-curve check.
|
|
23
|
+
*/
|
|
24
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
25
|
+
import type { RpcFn } from "./accounts.js";
|
|
26
|
+
/** Seed prefix of the `["ns_pricing", label]` PDA. */
|
|
27
|
+
export declare const NAMESPACE_PRICING_SEED = "ns_pricing";
|
|
28
|
+
/** `NAMESPACE_TIER_FLOOR_LAMPORTS`: every creator tier must be >= 1 XNT. */
|
|
29
|
+
export declare const NAMESPACE_TIER_FLOOR_LAMPORTS = 1000000000n;
|
|
30
|
+
/** `NAMESPACE_TIER_MAX_LAMPORTS`: every creator tier must be <= 100,000,000 XNT. */
|
|
31
|
+
export declare const NAMESPACE_TIER_MAX_LAMPORTS: bigint;
|
|
32
|
+
/** Number of length bands (same banding as the root `Config.tier_lamports`). */
|
|
33
|
+
export declare const NAMESPACE_TIER_COUNT = 10;
|
|
34
|
+
/** `sha256("global:set_namespace_pricing")[0..8]`. */
|
|
35
|
+
export declare const SET_NAMESPACE_PRICING_DISCRIMINATOR: Uint8Array;
|
|
36
|
+
/** `sha256("global:register_under_namespace_v2")[0..8]`. */
|
|
37
|
+
export declare const REGISTER_UNDER_NAMESPACE_V2_DISCRIMINATOR: Uint8Array;
|
|
38
|
+
/** `sha256("account:NamespacePricing")[0..8]`. */
|
|
39
|
+
export declare const NAMESPACE_PRICING_ACCOUNT_DISCRIMINATOR: Uint8Array;
|
|
40
|
+
/** `Namespace::PRICING_FLAG_EXT_OFFSET` — the one-byte "creator pricing is on"
|
|
41
|
+
* marker past the Anchor-allocated `Namespace` (8 + INIT_SPACE = 107). */
|
|
42
|
+
export declare const NAMESPACE_PRICING_MARKER_OFFSET = 107;
|
|
43
|
+
/** Program error codes added by this feature (6000 + variant index). */
|
|
44
|
+
export declare const NAMESPACE_PRICING_ERRORS: {
|
|
45
|
+
readonly NamespacePricingMismatch: 6116;
|
|
46
|
+
readonly NamespacePricingRequiresV2: 6117;
|
|
47
|
+
readonly PriceExceedsMax: 6118;
|
|
48
|
+
readonly NamespaceTierBelowFloor: 6119;
|
|
49
|
+
readonly NamespaceTierAboveMax: 6120;
|
|
50
|
+
};
|
|
51
|
+
/** lib.rs `tier_index`: 1 char -> 0 … 10+ -> 9 (0 clamps to 0). */
|
|
52
|
+
export declare function scopedTierIndex(len: number): number;
|
|
53
|
+
/** lib.rs `price_now` (registration price), in bigint like the program's u128. */
|
|
54
|
+
export declare function rootPriceNow(tierLamports: bigint, launchTs: bigint, nowSecs: bigint, totalHandlesSold: bigint): bigint;
|
|
55
|
+
/** Problems with a creator tier table, or null when the program would accept
|
|
56
|
+
* it (exactly `validate_namespace_tiers`). */
|
|
57
|
+
export declare function namespaceTiersProblem(tiers: ReadonlyArray<bigint>): string | null;
|
|
58
|
+
/** `find_program_address` — injected so this package never does curve math
|
|
59
|
+
* itself. With web3.js: `(seeds, pid) => PublicKey.findProgramAddressSync(seeds, new PublicKey(pid))[0].toBase58()`. */
|
|
60
|
+
export type PdaFinder = (seeds: Uint8Array[], programId: string) => Promise<string> | string;
|
|
61
|
+
/** The seeds of `["ns_pricing", label]`. */
|
|
62
|
+
export declare function namespacePricingSeeds(label: string): Uint8Array[];
|
|
63
|
+
/** Derive the canonical `["ns_pricing", label]` PDA. */
|
|
64
|
+
export declare function deriveNamespacePricingPda(label: string, programId: AddressLike, find: PdaFinder): Promise<string>;
|
|
65
|
+
/** Decoded `NamespacePricing` account. */
|
|
66
|
+
export interface NamespacePricing {
|
|
67
|
+
readonly version: number;
|
|
68
|
+
readonly bump: number;
|
|
69
|
+
/** Lamports per band, index = {@link scopedTierIndex}(len). */
|
|
70
|
+
readonly tierLamports: readonly bigint[];
|
|
71
|
+
readonly updatedAt: bigint;
|
|
72
|
+
readonly label: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Decode a `NamespacePricing` account's raw data (discriminator included):
|
|
76
|
+
* disc(8) | version(u8) | bump(u8) | tier_lamports([u64;10]) | updated_at(i64)
|
|
77
|
+
* | label(String). Returns null when the bytes are not a `NamespacePricing`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function decodeNamespacePricing(data: Uint8Array): NamespacePricing | null;
|
|
80
|
+
/** Whether a raw `Namespace` account carries the "creator pricing is on"
|
|
81
|
+
* marker (any nonzero byte at offset 107; a shorter account = off). */
|
|
82
|
+
export declare function namespaceHasPricingMarker(namespaceData: Uint8Array): boolean;
|
|
83
|
+
export interface SetNamespacePricingParams {
|
|
84
|
+
readonly programId: AddressLike;
|
|
85
|
+
/** The namespace `creator` (signer, pays rent on first set). */
|
|
86
|
+
readonly creator: AddressLike;
|
|
87
|
+
/** `["namespace", label]`. */
|
|
88
|
+
readonly namespace: AddressLike;
|
|
89
|
+
/** `["ns_pricing", label]` ({@link deriveNamespacePricingPda}). */
|
|
90
|
+
readonly namespacePricing: AddressLike;
|
|
91
|
+
readonly label: string;
|
|
92
|
+
/** 10 lamport prices, index = {@link scopedTierIndex}(len). */
|
|
93
|
+
readonly tierLamports: ReadonlyArray<bigint>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build `set_namespace_pricing(label, tier_lamports)`. Accounts IN ORDER
|
|
97
|
+
* (`SetNamespacePricing`): creator(s,w), namespace(w), namespace_pricing(w),
|
|
98
|
+
* system_program. Validates the table client-side with the program's exact
|
|
99
|
+
* bounds so a bad table never reaches a wallet prompt.
|
|
100
|
+
*/
|
|
101
|
+
export declare function buildSetNamespacePricingIx(p: SetNamespacePricingParams): BuiltInstruction;
|
|
102
|
+
export interface RegisterUnderNamespaceV2Params {
|
|
103
|
+
readonly programId: AddressLike;
|
|
104
|
+
/** `["ns_pricing", label]` — ALWAYS the canonical PDA, whether or not the
|
|
105
|
+
* creator has set pricing (the program decides from its contents). */
|
|
106
|
+
readonly namespacePricing: AddressLike;
|
|
107
|
+
readonly payer: AddressLike;
|
|
108
|
+
readonly owner: AddressLike;
|
|
109
|
+
readonly config: AddressLike;
|
|
110
|
+
readonly treasury: AddressLike;
|
|
111
|
+
readonly namespace: AddressLike;
|
|
112
|
+
readonly creatorPayout: AddressLike;
|
|
113
|
+
/** `["handle", [label_len], label, name]`. */
|
|
114
|
+
readonly handle: AddressLike;
|
|
115
|
+
readonly mint: AddressLike;
|
|
116
|
+
readonly ownerTokenAccount: AddressLike;
|
|
117
|
+
readonly metadata: AddressLike;
|
|
118
|
+
readonly masterEdition: AddressLike;
|
|
119
|
+
readonly tokenRecord: AddressLike;
|
|
120
|
+
readonly collectionMint: AddressLike;
|
|
121
|
+
readonly collectionMetadata: AddressLike;
|
|
122
|
+
readonly collectionMasterEdition: AddressLike;
|
|
123
|
+
readonly collectionAuthority: AddressLike;
|
|
124
|
+
/** Canonical sub-name (`alice` in `alice.pepe`). */
|
|
125
|
+
readonly name: string;
|
|
126
|
+
/** 0 Human, 1 Merchant, 2 Org, 3 Agent. */
|
|
127
|
+
readonly handleType: number;
|
|
128
|
+
/** Buyer's ceiling: the tx fails (`PriceExceedsMax`) if the on-chain price
|
|
129
|
+
* is higher. Pass {@link quoteScopedName}'s `lamports`. */
|
|
130
|
+
readonly maxPriceLamports: bigint;
|
|
131
|
+
/** OPTIONAL referral pair — both or neither. */
|
|
132
|
+
readonly integrator?: AddressLike;
|
|
133
|
+
readonly integratorAllowlist?: AddressLike;
|
|
134
|
+
/** OPTIONAL `["ns_minter", label, payer]` (invite-only, non-creator). */
|
|
135
|
+
readonly minterAuthorization?: AddressLike;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Build `register_under_namespace_v2(name, handle_type, max_price_lamports)`.
|
|
139
|
+
* Accounts IN ORDER: the exact v1 `RegisterUnderNamespace` list (payer(s,w),
|
|
140
|
+
* owner, config(w), treasury(w), namespace, creator_payout(w), handle(w),
|
|
141
|
+
* mint(w), owner_token_account(w), metadata(w), master_edition(w),
|
|
142
|
+
* token_record(w), collection_mint, collection_metadata(w),
|
|
143
|
+
* collection_master_edition, collection_authority, token_metadata_program,
|
|
144
|
+
* token_program, associated_token_program, system_program,
|
|
145
|
+
* sysvar_instructions), then the three optional slots ALWAYS present
|
|
146
|
+
* (integrator, integrator_allowlist, minter_authorization — each the program
|
|
147
|
+
* id when absent, Anchor's "explicitly None"), then `namespace_pricing` as
|
|
148
|
+
* `remaining_accounts[0]`. The program fails closed if it is missing or
|
|
149
|
+
* shifted.
|
|
150
|
+
*/
|
|
151
|
+
export declare function buildRegisterUnderNamespaceV2Ix(p: RegisterUnderNamespaceV2Params): BuiltInstruction;
|
|
152
|
+
/** The program's root pricing inputs, read from `Config`. */
|
|
153
|
+
export interface RootPricingInputs {
|
|
154
|
+
readonly launchTs: bigint;
|
|
155
|
+
readonly tierLamports: readonly bigint[];
|
|
156
|
+
readonly totalHandlesSold: bigint;
|
|
157
|
+
}
|
|
158
|
+
/** Read `launch_ts`, `tier_lamports` and `total_handles_sold` from raw Config bytes. */
|
|
159
|
+
export declare function decodeRootPricingInputs(config: Uint8Array): RootPricingInputs | null;
|
|
160
|
+
/**
|
|
161
|
+
* The price `register_under_namespace_v2` charges, from already-read state —
|
|
162
|
+
* `scoped_registration_price` in lib.rs: the creator's band flat when a table
|
|
163
|
+
* exists, else the root band × the ramp.
|
|
164
|
+
*/
|
|
165
|
+
export declare function scopedRegistrationPrice(pricing: NamespacePricing | null, root: RootPricingInputs, nameLen: number, nowSecs: bigint): bigint;
|
|
166
|
+
export interface ScopedQuote {
|
|
167
|
+
/** Lamports `register_under_namespace_v2` will charge (pass as `maxPriceLamports`). */
|
|
168
|
+
readonly lamports: bigint;
|
|
169
|
+
/** `custom` = the creator's table; `fallback` = root tiers × ramp. */
|
|
170
|
+
readonly source: "custom" | "fallback";
|
|
171
|
+
readonly tierIndex: number;
|
|
172
|
+
/** The canonical `["ns_pricing", label]` PDA (pass to the v2 builder). */
|
|
173
|
+
readonly pricingAccount: string;
|
|
174
|
+
/** The creator's full table when `source === "custom"`. */
|
|
175
|
+
readonly pricing: NamespacePricing | null;
|
|
176
|
+
}
|
|
177
|
+
export interface QuoteScopedNameOptions {
|
|
178
|
+
readonly rpc: RpcFn;
|
|
179
|
+
readonly programId: AddressLike;
|
|
180
|
+
readonly find: PdaFinder;
|
|
181
|
+
/** Unix seconds; defaults to the local clock (the ramp moves per quarter, so
|
|
182
|
+
* seconds of skew never matter for a fresh quote). */
|
|
183
|
+
readonly nowSecs?: bigint;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Quote `name.label` exactly like the program prices it (pricing-or-fallback).
|
|
187
|
+
* Throws when Config is missing, or when the pricing account exists but is not
|
|
188
|
+
* a valid `NamespacePricing` owned by the program (the program would refuse
|
|
189
|
+
* too), so a caller never shows a price the chain will not honour.
|
|
190
|
+
*/
|
|
191
|
+
export declare function quoteScopedName(label: string, name: string, opts: QuoteScopedNameOptions): Promise<ScopedQuote>;
|
|
192
|
+
/**
|
|
193
|
+
* The instruction to SIMULATE (never send) to learn whether the deployed
|
|
194
|
+
* program has this feature: `set_namespace_pricing` with no accounts. An
|
|
195
|
+
* upgraded program recognizes the discriminator and fails account validation
|
|
196
|
+
* (e.g. 3005 `AccountNotEnoughKeys`); a pre-upgrade program fails with
|
|
197
|
+
* Anchor's `InstructionFallbackNotFound` (101). Interpret the simulation's
|
|
198
|
+
* `err` with {@link interpretNamespacePricingProbe}.
|
|
199
|
+
*/
|
|
200
|
+
export declare function buildNamespacePricingProbeIx(programId: AddressLike): BuiltInstruction;
|
|
201
|
+
/**
|
|
202
|
+
* Read a `simulateTransaction` result's `value.err` for the probe tx (whose
|
|
203
|
+
* probe instruction is at `probeIndex`). `true` = the program knows the
|
|
204
|
+
* instruction; `false` = it does not, or the result is not the shape we
|
|
205
|
+
* expect (fail closed: hide the feature rather than show a broken button).
|
|
206
|
+
*/
|
|
207
|
+
export declare function interpretNamespacePricingProbe(err: unknown, probeIndex: number): boolean;
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLD creator pricing (docs/specs/tld-creator-pricing.md): the SDK side of a
|
|
3
|
+
* TLD creator's own price table for names under their TLD.
|
|
4
|
+
*
|
|
5
|
+
* - `["ns_pricing", label]` holds a `NamespacePricing` account: a flat lamport
|
|
6
|
+
* price per length band (index = `tier_index(len)`: 0 = 1 char … 8 = 9 chars,
|
|
7
|
+
* 9 = 10+), written ONLY by the namespace creator via `set_namespace_pricing`.
|
|
8
|
+
* - `register_under_namespace_v2` (v1's accounts + the pricing PDA as the
|
|
9
|
+
* first remaining account) charges that band FLAT (no launch ramp) when
|
|
10
|
+
* the account exists, else the legacy root price (`Config.tier_lamports` ×
|
|
11
|
+
* the time/volume ramp), and refuses to charge more than the buyer's
|
|
12
|
+
* `max_price_lamports`.
|
|
13
|
+
* - The legacy `register_under_namespace` (v1) fails closed
|
|
14
|
+
* (`NamespacePricingRequiresV2`, 6117) on any TLD whose creator opted in.
|
|
15
|
+
*
|
|
16
|
+
* {@link quoteScopedName} reproduces the program's price selection exactly, so
|
|
17
|
+
* pass its `lamports` as `maxPriceLamports` to {@link buildRegisterUnderNamespaceV2Ix}.
|
|
18
|
+
*
|
|
19
|
+
* Hand-rolled like the rest of this package: no Anchor client, no
|
|
20
|
+
* `@solana/web3.js` import, no `@types/node`. PDAs are derived through an
|
|
21
|
+
* INJECTED `find_program_address` (see {@link PdaFinder}) because this package
|
|
22
|
+
* never hand-rolls the ed25519 on-curve check.
|
|
23
|
+
*/
|
|
24
|
+
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// constants (mirror programs/x1-handles/src/{lib,state}.rs)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
/** Seed prefix of the `["ns_pricing", label]` PDA. */
|
|
29
|
+
export const NAMESPACE_PRICING_SEED = "ns_pricing";
|
|
30
|
+
/** `NAMESPACE_TIER_FLOOR_LAMPORTS`: every creator tier must be >= 1 XNT. */
|
|
31
|
+
export const NAMESPACE_TIER_FLOOR_LAMPORTS = 1000000000n;
|
|
32
|
+
/** `NAMESPACE_TIER_MAX_LAMPORTS`: every creator tier must be <= 100,000,000 XNT. */
|
|
33
|
+
export const NAMESPACE_TIER_MAX_LAMPORTS = 100000000n * 1000000000n;
|
|
34
|
+
/** Number of length bands (same banding as the root `Config.tier_lamports`). */
|
|
35
|
+
export const NAMESPACE_TIER_COUNT = 10;
|
|
36
|
+
/** `sha256("global:set_namespace_pricing")[0..8]`. */
|
|
37
|
+
export const SET_NAMESPACE_PRICING_DISCRIMINATOR = Uint8Array.from([
|
|
38
|
+
245, 190, 192, 77, 94, 146, 50, 246,
|
|
39
|
+
]);
|
|
40
|
+
/** `sha256("global:register_under_namespace_v2")[0..8]`. */
|
|
41
|
+
export const REGISTER_UNDER_NAMESPACE_V2_DISCRIMINATOR = Uint8Array.from([
|
|
42
|
+
233, 132, 213, 179, 60, 113, 18, 247,
|
|
43
|
+
]);
|
|
44
|
+
/** `sha256("account:NamespacePricing")[0..8]`. */
|
|
45
|
+
export const NAMESPACE_PRICING_ACCOUNT_DISCRIMINATOR = Uint8Array.from([
|
|
46
|
+
197, 197, 26, 252, 150, 176, 211, 188,
|
|
47
|
+
]);
|
|
48
|
+
/** `Namespace::PRICING_FLAG_EXT_OFFSET` — the one-byte "creator pricing is on"
|
|
49
|
+
* marker past the Anchor-allocated `Namespace` (8 + INIT_SPACE = 107). */
|
|
50
|
+
export const NAMESPACE_PRICING_MARKER_OFFSET = 107;
|
|
51
|
+
/** Program error codes added by this feature (6000 + variant index). */
|
|
52
|
+
export const NAMESPACE_PRICING_ERRORS = {
|
|
53
|
+
NamespacePricingMismatch: 6116,
|
|
54
|
+
NamespacePricingRequiresV2: 6117,
|
|
55
|
+
PriceExceedsMax: 6118,
|
|
56
|
+
NamespaceTierBelowFloor: 6119,
|
|
57
|
+
NamespaceTierAboveMax: 6120,
|
|
58
|
+
};
|
|
59
|
+
/** Anchor's `InstructionFallbackNotFound`: the deployed program does not know
|
|
60
|
+
* the instruction (i.e. it predates this feature). */
|
|
61
|
+
const ANCHOR_INSTRUCTION_FALLBACK_NOT_FOUND = 101;
|
|
62
|
+
// Root pricing curve — mirrors lib.rs `ramp_bps` / `quarters_elapsed` /
|
|
63
|
+
// `compute_price` / `price_now` exactly (same constants as app/src/lib/x1/pricing.ts).
|
|
64
|
+
const INTRO_START_BPS = 5000n;
|
|
65
|
+
const INTRO_STEP_BPS = 1250n;
|
|
66
|
+
const QUARTER_SECS = 91n * 24n * 60n * 60n;
|
|
67
|
+
const VOLUME_START_BPS = 5000n;
|
|
68
|
+
const VOLUME_STEP_BPS = 1250n;
|
|
69
|
+
const VOLUME_STEP_COUNT = 500n;
|
|
70
|
+
// Config byte offsets (state.rs): disc(8) admin(32) treasury(32) launch_ts(8)
|
|
71
|
+
// tier_lamports(80) nft_tier_lamports(80) total_handles_sold(8) ...
|
|
72
|
+
const CONFIG_LAUNCH_TS_OFFSET = 72;
|
|
73
|
+
const CONFIG_TIERS_OFFSET = 80;
|
|
74
|
+
const CONFIG_TOTAL_SOLD_OFFSET = 240;
|
|
75
|
+
const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
76
|
+
const TE = new TextEncoder();
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// pure helpers
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
function toBytes32(v, what) {
|
|
81
|
+
if (typeof v === "string") {
|
|
82
|
+
const b = decodeBase58_32(v);
|
|
83
|
+
if (!b)
|
|
84
|
+
throw new Error(`${what} is not a valid base58 address`);
|
|
85
|
+
return b;
|
|
86
|
+
}
|
|
87
|
+
if (v.length !== 32)
|
|
88
|
+
throw new Error(`${what} must be exactly 32 bytes`);
|
|
89
|
+
return v;
|
|
90
|
+
}
|
|
91
|
+
function toBase58(v, what) {
|
|
92
|
+
return encodeBase58(toBytes32(v, what));
|
|
93
|
+
}
|
|
94
|
+
function key(pubkey, what, isSigner, isWritable) {
|
|
95
|
+
return { pubkey: toBase58(pubkey, what), isSigner, isWritable };
|
|
96
|
+
}
|
|
97
|
+
function borshString(s) {
|
|
98
|
+
const b = TE.encode(s);
|
|
99
|
+
const out = new Uint8Array(4 + b.length);
|
|
100
|
+
new DataView(out.buffer).setUint32(0, b.length, true);
|
|
101
|
+
out.set(b, 4);
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function u64le(n) {
|
|
105
|
+
const out = new Uint8Array(8);
|
|
106
|
+
new DataView(out.buffer).setBigUint64(0, n, true);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
function concat(...parts) {
|
|
110
|
+
const out = new Uint8Array(parts.reduce((a, p) => a + p.length, 0));
|
|
111
|
+
let o = 0;
|
|
112
|
+
for (const p of parts) {
|
|
113
|
+
out.set(p, o);
|
|
114
|
+
o += p.length;
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/** lib.rs `tier_index`: 1 char -> 0 … 10+ -> 9 (0 clamps to 0). */
|
|
119
|
+
export function scopedTierIndex(len) {
|
|
120
|
+
return Math.min(Math.max(len, 1), 10) - 1;
|
|
121
|
+
}
|
|
122
|
+
/** lib.rs `price_now` (registration price), in bigint like the program's u128. */
|
|
123
|
+
export function rootPriceNow(tierLamports, launchTs, nowSecs, totalHandlesSold) {
|
|
124
|
+
const elapsed = nowSecs > launchTs ? nowSecs - launchTs : 0n;
|
|
125
|
+
const quarters = elapsed / QUARTER_SECS;
|
|
126
|
+
const ramp = (start, step, n) => {
|
|
127
|
+
const v = start + step * n;
|
|
128
|
+
return v > 10000n ? 10000n : v;
|
|
129
|
+
};
|
|
130
|
+
const timeBps = ramp(INTRO_START_BPS, INTRO_STEP_BPS, quarters);
|
|
131
|
+
const volumeBps = ramp(VOLUME_START_BPS, VOLUME_STEP_BPS, totalHandlesSold / VOLUME_STEP_COUNT);
|
|
132
|
+
return (tierLamports * timeBps * volumeBps) / 10000n / 10000n;
|
|
133
|
+
}
|
|
134
|
+
/** Problems with a creator tier table, or null when the program would accept
|
|
135
|
+
* it (exactly `validate_namespace_tiers`). */
|
|
136
|
+
export function namespaceTiersProblem(tiers) {
|
|
137
|
+
if (tiers.length !== NAMESPACE_TIER_COUNT)
|
|
138
|
+
return `expected ${NAMESPACE_TIER_COUNT} tiers, got ${tiers.length}`;
|
|
139
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
140
|
+
const t = tiers[i];
|
|
141
|
+
if (t < NAMESPACE_TIER_FLOOR_LAMPORTS)
|
|
142
|
+
return `tier ${i} is below the 1 XNT protocol floor`;
|
|
143
|
+
if (t > NAMESPACE_TIER_MAX_LAMPORTS)
|
|
144
|
+
return `tier ${i} is above the 100,000,000 XNT protocol maximum`;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/** The seeds of `["ns_pricing", label]`. */
|
|
149
|
+
export function namespacePricingSeeds(label) {
|
|
150
|
+
return [TE.encode(NAMESPACE_PRICING_SEED), TE.encode(label)];
|
|
151
|
+
}
|
|
152
|
+
/** Derive the canonical `["ns_pricing", label]` PDA. */
|
|
153
|
+
export async function deriveNamespacePricingPda(label, programId, find) {
|
|
154
|
+
return find(namespacePricingSeeds(label), toBase58(programId, "programId"));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Decode a `NamespacePricing` account's raw data (discriminator included):
|
|
158
|
+
* disc(8) | version(u8) | bump(u8) | tier_lamports([u64;10]) | updated_at(i64)
|
|
159
|
+
* | label(String). Returns null when the bytes are not a `NamespacePricing`.
|
|
160
|
+
*/
|
|
161
|
+
export function decodeNamespacePricing(data) {
|
|
162
|
+
if (data.length < 8 + 1 + 1 + 80 + 8 + 4)
|
|
163
|
+
return null;
|
|
164
|
+
for (let i = 0; i < 8; i++) {
|
|
165
|
+
if (data[i] !== NAMESPACE_PRICING_ACCOUNT_DISCRIMINATOR[i])
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
169
|
+
const tierLamports = [];
|
|
170
|
+
for (let i = 0; i < NAMESPACE_TIER_COUNT; i++)
|
|
171
|
+
tierLamports.push(dv.getBigUint64(10 + 8 * i, true));
|
|
172
|
+
const updatedAt = dv.getBigInt64(90, true);
|
|
173
|
+
const labelLen = dv.getUint32(98, true);
|
|
174
|
+
if (data.length < 102 + labelLen)
|
|
175
|
+
return null;
|
|
176
|
+
let label;
|
|
177
|
+
try {
|
|
178
|
+
label = new TextDecoder("utf-8", { fatal: true }).decode(data.subarray(102, 102 + labelLen));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return { version: data[8], bump: data[9], tierLamports, updatedAt, label };
|
|
184
|
+
}
|
|
185
|
+
/** Whether a raw `Namespace` account carries the "creator pricing is on"
|
|
186
|
+
* marker (any nonzero byte at offset 107; a shorter account = off). */
|
|
187
|
+
export function namespaceHasPricingMarker(namespaceData) {
|
|
188
|
+
const b = namespaceData[NAMESPACE_PRICING_MARKER_OFFSET];
|
|
189
|
+
return b !== undefined && b !== 0;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Build `set_namespace_pricing(label, tier_lamports)`. Accounts IN ORDER
|
|
193
|
+
* (`SetNamespacePricing`): creator(s,w), namespace(w), namespace_pricing(w),
|
|
194
|
+
* system_program. Validates the table client-side with the program's exact
|
|
195
|
+
* bounds so a bad table never reaches a wallet prompt.
|
|
196
|
+
*/
|
|
197
|
+
export function buildSetNamespacePricingIx(p) {
|
|
198
|
+
const problem = namespaceTiersProblem(p.tierLamports);
|
|
199
|
+
if (problem)
|
|
200
|
+
throw new Error(problem);
|
|
201
|
+
return {
|
|
202
|
+
programId: toBase58(p.programId, "programId"),
|
|
203
|
+
keys: [
|
|
204
|
+
key(p.creator, "creator", true, true),
|
|
205
|
+
key(p.namespace, "namespace", false, true),
|
|
206
|
+
key(p.namespacePricing, "namespacePricing", false, true),
|
|
207
|
+
key(SYSTEM_PROGRAM, "systemProgram", false, false),
|
|
208
|
+
],
|
|
209
|
+
data: concat(SET_NAMESPACE_PRICING_DISCRIMINATOR, borshString(p.label), ...p.tierLamports.map(u64le)),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
213
|
+
const ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
214
|
+
const TOKEN_METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
|
|
215
|
+
const SYSVAR_INSTRUCTIONS = "Sysvar1nstructions1111111111111111111111111";
|
|
216
|
+
/**
|
|
217
|
+
* Build `register_under_namespace_v2(name, handle_type, max_price_lamports)`.
|
|
218
|
+
* Accounts IN ORDER: the exact v1 `RegisterUnderNamespace` list (payer(s,w),
|
|
219
|
+
* owner, config(w), treasury(w), namespace, creator_payout(w), handle(w),
|
|
220
|
+
* mint(w), owner_token_account(w), metadata(w), master_edition(w),
|
|
221
|
+
* token_record(w), collection_mint, collection_metadata(w),
|
|
222
|
+
* collection_master_edition, collection_authority, token_metadata_program,
|
|
223
|
+
* token_program, associated_token_program, system_program,
|
|
224
|
+
* sysvar_instructions), then the three optional slots ALWAYS present
|
|
225
|
+
* (integrator, integrator_allowlist, minter_authorization — each the program
|
|
226
|
+
* id when absent, Anchor's "explicitly None"), then `namespace_pricing` as
|
|
227
|
+
* `remaining_accounts[0]`. The program fails closed if it is missing or
|
|
228
|
+
* shifted.
|
|
229
|
+
*/
|
|
230
|
+
export function buildRegisterUnderNamespaceV2Ix(p) {
|
|
231
|
+
if (!Number.isInteger(p.handleType) || p.handleType < 0 || p.handleType > 3) {
|
|
232
|
+
throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
|
|
233
|
+
}
|
|
234
|
+
if (typeof p.maxPriceLamports !== "bigint" || p.maxPriceLamports < 0n || p.maxPriceLamports > 0xffffffffffffffffn) {
|
|
235
|
+
throw new Error("maxPriceLamports must be a u64 bigint");
|
|
236
|
+
}
|
|
237
|
+
if ((p.integrator === undefined) !== (p.integratorAllowlist === undefined)) {
|
|
238
|
+
throw new Error("integrator and integratorAllowlist must be supplied together");
|
|
239
|
+
}
|
|
240
|
+
const programId = toBase58(p.programId, "programId");
|
|
241
|
+
const none = () => ({ pubkey: programId, isSigner: false, isWritable: false });
|
|
242
|
+
const keys = [
|
|
243
|
+
key(p.payer, "payer", true, true),
|
|
244
|
+
key(p.owner, "owner", false, false),
|
|
245
|
+
key(p.config, "config", false, true),
|
|
246
|
+
key(p.treasury, "treasury", false, true),
|
|
247
|
+
key(p.namespace, "namespace", false, false),
|
|
248
|
+
key(p.creatorPayout, "creatorPayout", false, true),
|
|
249
|
+
key(p.handle, "handle", false, true),
|
|
250
|
+
key(p.mint, "mint", false, true),
|
|
251
|
+
key(p.ownerTokenAccount, "ownerTokenAccount", false, true),
|
|
252
|
+
key(p.metadata, "metadata", false, true),
|
|
253
|
+
key(p.masterEdition, "masterEdition", false, true),
|
|
254
|
+
key(p.tokenRecord, "tokenRecord", false, true),
|
|
255
|
+
key(p.collectionMint, "collectionMint", false, false),
|
|
256
|
+
key(p.collectionMetadata, "collectionMetadata", false, true),
|
|
257
|
+
key(p.collectionMasterEdition, "collectionMasterEdition", false, false),
|
|
258
|
+
key(p.collectionAuthority, "collectionAuthority", false, false),
|
|
259
|
+
key(TOKEN_METADATA_PROGRAM, "tokenMetadataProgram", false, false),
|
|
260
|
+
key(TOKEN_PROGRAM, "tokenProgram", false, false),
|
|
261
|
+
key(ASSOCIATED_TOKEN_PROGRAM, "associatedTokenProgram", false, false),
|
|
262
|
+
key(SYSTEM_PROGRAM, "systemProgram", false, false),
|
|
263
|
+
key(SYSVAR_INSTRUCTIONS, "sysvarInstructions", false, false),
|
|
264
|
+
p.integrator !== undefined ? key(p.integrator, "integrator", false, true) : none(),
|
|
265
|
+
p.integratorAllowlist !== undefined ? key(p.integratorAllowlist, "integratorAllowlist", false, false) : none(),
|
|
266
|
+
p.minterAuthorization !== undefined ? key(p.minterAuthorization, "minterAuthorization", false, false) : none(),
|
|
267
|
+
key(p.namespacePricing, "namespacePricing", false, false),
|
|
268
|
+
];
|
|
269
|
+
return {
|
|
270
|
+
programId,
|
|
271
|
+
keys,
|
|
272
|
+
data: concat(REGISTER_UNDER_NAMESPACE_V2_DISCRIMINATOR, borshString(p.name), Uint8Array.of(p.handleType), u64le(p.maxPriceLamports)),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** Read `launch_ts`, `tier_lamports` and `total_handles_sold` from raw Config bytes. */
|
|
276
|
+
export function decodeRootPricingInputs(config) {
|
|
277
|
+
if (config.length < CONFIG_TOTAL_SOLD_OFFSET + 8)
|
|
278
|
+
return null;
|
|
279
|
+
const dv = new DataView(config.buffer, config.byteOffset, config.byteLength);
|
|
280
|
+
const tierLamports = [];
|
|
281
|
+
for (let i = 0; i < NAMESPACE_TIER_COUNT; i++)
|
|
282
|
+
tierLamports.push(dv.getBigUint64(CONFIG_TIERS_OFFSET + 8 * i, true));
|
|
283
|
+
return {
|
|
284
|
+
launchTs: dv.getBigInt64(CONFIG_LAUNCH_TS_OFFSET, true),
|
|
285
|
+
tierLamports,
|
|
286
|
+
totalHandlesSold: dv.getBigUint64(CONFIG_TOTAL_SOLD_OFFSET, true),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The price `register_under_namespace_v2` charges, from already-read state —
|
|
291
|
+
* `scoped_registration_price` in lib.rs: the creator's band flat when a table
|
|
292
|
+
* exists, else the root band × the ramp.
|
|
293
|
+
*/
|
|
294
|
+
export function scopedRegistrationPrice(pricing, root, nameLen, nowSecs) {
|
|
295
|
+
const i = scopedTierIndex(nameLen);
|
|
296
|
+
if (pricing)
|
|
297
|
+
return pricing.tierLamports[i];
|
|
298
|
+
return rootPriceNow(root.tierLamports[i], root.launchTs, nowSecs, root.totalHandlesSold);
|
|
299
|
+
}
|
|
300
|
+
async function readAccount(rpc, address) {
|
|
301
|
+
const res = (await rpc("getAccountInfo", [address, { encoding: "base64", commitment: "confirmed" }]));
|
|
302
|
+
const v = res?.value;
|
|
303
|
+
if (!v)
|
|
304
|
+
return null;
|
|
305
|
+
const bin = atob(v.data[0]);
|
|
306
|
+
const data = new Uint8Array(bin.length);
|
|
307
|
+
for (let i = 0; i < bin.length; i++)
|
|
308
|
+
data[i] = bin.charCodeAt(i);
|
|
309
|
+
return { owner: v.owner, data };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Quote `name.label` exactly like the program prices it (pricing-or-fallback).
|
|
313
|
+
* Throws when Config is missing, or when the pricing account exists but is not
|
|
314
|
+
* a valid `NamespacePricing` owned by the program (the program would refuse
|
|
315
|
+
* too), so a caller never shows a price the chain will not honour.
|
|
316
|
+
*/
|
|
317
|
+
export async function quoteScopedName(label, name, opts) {
|
|
318
|
+
const programId = toBase58(opts.programId, "programId");
|
|
319
|
+
const [config, pricingAccount] = await Promise.all([
|
|
320
|
+
opts.find([TE.encode("config")], programId),
|
|
321
|
+
deriveNamespacePricingPda(label, programId, opts.find),
|
|
322
|
+
]);
|
|
323
|
+
const [configAcct, pricingAcct] = await Promise.all([readAccount(opts.rpc, config), readAccount(opts.rpc, pricingAccount)]);
|
|
324
|
+
if (!configAcct)
|
|
325
|
+
throw new Error("registry Config not found");
|
|
326
|
+
const root = decodeRootPricingInputs(configAcct.data);
|
|
327
|
+
if (!root)
|
|
328
|
+
throw new Error("registry Config is too short to read pricing");
|
|
329
|
+
let pricing = null;
|
|
330
|
+
if (pricingAcct && pricingAcct.owner === programId) {
|
|
331
|
+
pricing = decodeNamespacePricing(pricingAcct.data);
|
|
332
|
+
if (!pricing)
|
|
333
|
+
throw new Error(`.${label} pricing account is not a valid NamespacePricing`);
|
|
334
|
+
}
|
|
335
|
+
const nowSecs = opts.nowSecs ?? BigInt(Math.floor(Date.now() / 1000));
|
|
336
|
+
const nameLen = TE.encode(name).length;
|
|
337
|
+
return {
|
|
338
|
+
lamports: scopedRegistrationPrice(pricing, root, nameLen, nowSecs),
|
|
339
|
+
source: pricing ? "custom" : "fallback",
|
|
340
|
+
tierIndex: scopedTierIndex(nameLen),
|
|
341
|
+
pricingAccount,
|
|
342
|
+
pricing,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// upgrade probe
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
/**
|
|
349
|
+
* The instruction to SIMULATE (never send) to learn whether the deployed
|
|
350
|
+
* program has this feature: `set_namespace_pricing` with no accounts. An
|
|
351
|
+
* upgraded program recognizes the discriminator and fails account validation
|
|
352
|
+
* (e.g. 3005 `AccountNotEnoughKeys`); a pre-upgrade program fails with
|
|
353
|
+
* Anchor's `InstructionFallbackNotFound` (101). Interpret the simulation's
|
|
354
|
+
* `err` with {@link interpretNamespacePricingProbe}.
|
|
355
|
+
*/
|
|
356
|
+
export function buildNamespacePricingProbeIx(programId) {
|
|
357
|
+
return { programId: toBase58(programId, "programId"), keys: [], data: SET_NAMESPACE_PRICING_DISCRIMINATOR.slice() };
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Read a `simulateTransaction` result's `value.err` for the probe tx (whose
|
|
361
|
+
* probe instruction is at `probeIndex`). `true` = the program knows the
|
|
362
|
+
* instruction; `false` = it does not, or the result is not the shape we
|
|
363
|
+
* expect (fail closed: hide the feature rather than show a broken button).
|
|
364
|
+
*/
|
|
365
|
+
export function interpretNamespacePricingProbe(err, probeIndex) {
|
|
366
|
+
if (!err || typeof err !== "object")
|
|
367
|
+
return false;
|
|
368
|
+
const ie = err.InstructionError;
|
|
369
|
+
if (!Array.isArray(ie) || ie.length !== 2 || ie[0] !== probeIndex)
|
|
370
|
+
return false;
|
|
371
|
+
const detail = ie[1];
|
|
372
|
+
// Anchor reports both outcomes as `Custom` codes; anything else is an
|
|
373
|
+
// unexpected shape and reads as "not supported".
|
|
374
|
+
if (typeof detail === "object" && detail !== null && typeof detail.Custom === "number") {
|
|
375
|
+
return detail.Custom !== ANCHOR_INSTRUCTION_FALLBACK_NOT_FOUND;
|
|
376
|
+
}
|
|
377
|
+
return false;
|
|
378
|
+
}
|
package/dist/sha256.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal synchronous SHA-256 (FIPS 180-4). This SDK is zero-dependency and
|
|
3
|
+
* cannot assume WebCrypto is present AND synchronous (`crypto.subtle.digest`
|
|
4
|
+
* is async), so the x402 request-reference derivation (x402.ts) uses this
|
|
5
|
+
* pure implementation. Cross-checked against node:crypto in sdk/test/x402.test.js.
|
|
6
|
+
*/
|
|
7
|
+
/** SHA-256 of `data` (bytes, or a string hashed as its UTF-8 encoding). */
|
|
8
|
+
export declare function sha256(data: Uint8Array | string): Uint8Array;
|
|
9
|
+
/** Lowercase hex of `bytes`. */
|
|
10
|
+
export declare function toHex(bytes: Uint8Array): string;
|
package/dist/sha256.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal synchronous SHA-256 (FIPS 180-4). This SDK is zero-dependency and
|
|
3
|
+
* cannot assume WebCrypto is present AND synchronous (`crypto.subtle.digest`
|
|
4
|
+
* is async), so the x402 request-reference derivation (x402.ts) uses this
|
|
5
|
+
* pure implementation. Cross-checked against node:crypto in sdk/test/x402.test.js.
|
|
6
|
+
*/
|
|
7
|
+
const K = new Uint32Array([
|
|
8
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
9
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
10
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
11
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
12
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
13
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
14
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
15
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
16
|
+
]);
|
|
17
|
+
/** SHA-256 of `data` (bytes, or a string hashed as its UTF-8 encoding). */
|
|
18
|
+
export function sha256(data) {
|
|
19
|
+
const msg = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
20
|
+
const bitLen = BigInt(msg.length) * 8n;
|
|
21
|
+
const padded = new Uint8Array(Math.ceil((msg.length + 9) / 64) * 64);
|
|
22
|
+
padded.set(msg);
|
|
23
|
+
padded[msg.length] = 0x80;
|
|
24
|
+
const dv = new DataView(padded.buffer);
|
|
25
|
+
dv.setBigUint64(padded.length - 8, bitLen, false);
|
|
26
|
+
const H = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
27
|
+
const W = new Uint32Array(64);
|
|
28
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
29
|
+
for (let off = 0; off < padded.length; off += 64) {
|
|
30
|
+
for (let t = 0; t < 16; t++)
|
|
31
|
+
W[t] = dv.getUint32(off + t * 4, false);
|
|
32
|
+
for (let t = 16; t < 64; t++) {
|
|
33
|
+
const w15 = W[t - 15];
|
|
34
|
+
const w2 = W[t - 2];
|
|
35
|
+
const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
|
|
36
|
+
const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
|
|
37
|
+
W[t] = (W[t - 16] + s0 + W[t - 7] + s1) >>> 0;
|
|
38
|
+
}
|
|
39
|
+
let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
|
|
40
|
+
for (let t = 0; t < 64; t++) {
|
|
41
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
42
|
+
const ch = (e & f) ^ (~e & g);
|
|
43
|
+
const t1 = (h + S1 + ch + K[t] + W[t]) >>> 0;
|
|
44
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
45
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
46
|
+
const t2 = (S0 + maj) >>> 0;
|
|
47
|
+
h = g;
|
|
48
|
+
g = f;
|
|
49
|
+
f = e;
|
|
50
|
+
e = (d + t1) >>> 0;
|
|
51
|
+
d = c;
|
|
52
|
+
c = b;
|
|
53
|
+
b = a;
|
|
54
|
+
a = (t1 + t2) >>> 0;
|
|
55
|
+
}
|
|
56
|
+
H[0] = (H[0] + a) >>> 0;
|
|
57
|
+
H[1] = (H[1] + b) >>> 0;
|
|
58
|
+
H[2] = (H[2] + c) >>> 0;
|
|
59
|
+
H[3] = (H[3] + d) >>> 0;
|
|
60
|
+
H[4] = (H[4] + e) >>> 0;
|
|
61
|
+
H[5] = (H[5] + f) >>> 0;
|
|
62
|
+
H[6] = (H[6] + g) >>> 0;
|
|
63
|
+
H[7] = (H[7] + h) >>> 0;
|
|
64
|
+
}
|
|
65
|
+
const out = new Uint8Array(32);
|
|
66
|
+
const odv = new DataView(out.buffer);
|
|
67
|
+
for (let i = 0; i < 8; i++)
|
|
68
|
+
odv.setUint32(i * 4, H[i], false);
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** Lowercase hex of `bytes`. */
|
|
72
|
+
export function toHex(bytes) {
|
|
73
|
+
let s = "";
|
|
74
|
+
for (const b of bytes)
|
|
75
|
+
s += b.toString(16).padStart(2, "0");
|
|
76
|
+
return s;
|
|
77
|
+
}
|
package/dist/voucher.d.ts
CHANGED
|
@@ -25,18 +25,26 @@
|
|
|
25
25
|
* a bound recipient (audit S6 — open/recipient-less vouchers were removed as
|
|
26
26
|
* front-runnable; `claimSecretHash` is vestigial and rejected). An optional integrator referrer (the same trailing accounts as
|
|
27
27
|
* `register`, from `integrator.ts`) is snapshotted so the claim can split
|
|
28
|
-
* the escrow. `expiresAt` must be strictly in the future
|
|
28
|
+
* the escrow. `expiresAt` must be strictly in the future and at most
|
|
29
|
+
* `MAX_VOUCHER_DURATION_SECS` (366 days) away (audit R4-A-2).
|
|
29
30
|
* - `claim_voucher(name)` registers the handle, drawing the fee from the
|
|
30
31
|
* escrow (split integrator/treasury per the snapshot), and closes the
|
|
31
32
|
* voucher — rent back to the original payer. The `claimer` pays only the tx
|
|
32
|
-
* fee and the handle's rent, never the mint fee
|
|
33
|
-
*
|
|
33
|
+
* fee and the handle's rent, never the mint fee. This untokenized claim must
|
|
34
|
+
* be signed by the bound recipient (audit R4-A-3); a relayer uses
|
|
35
|
+
* `claim_voucher_tokenized`. Supply the integrator wallet iff the voucher
|
|
36
|
+
* carries one.
|
|
34
37
|
* - `refund_voucher(name)` returns the whole escrow (+ rent) to the payer,
|
|
35
|
-
* payer-signed,
|
|
38
|
+
* payer-signed, at/after `expiresAt`, or earlier once the name has been
|
|
39
|
+
* registered by someone else (pass `handle`, audit R4-A-2).
|
|
36
40
|
*/
|
|
37
41
|
import type { AddressLike, BuiltInstruction, InstructionKey } from "./delegate.js";
|
|
38
42
|
/** Seed prefix of the voucher PDA: `["voucher", name]`. */
|
|
39
43
|
export declare const VOUCHER_SEED = "voucher";
|
|
44
|
+
/** Longest a voucher may stay open: `create_voucher` refuses an `expiresAt`
|
|
45
|
+
* more than this many seconds ahead (`VoucherExpiryTooFar`, 6112; audit
|
|
46
|
+
* R4-A-2). Mirrors lib.rs `MAX_VOUCHER_DURATION_SECS`. */
|
|
47
|
+
export declare const MAX_VOUCHER_DURATION_SECS: number;
|
|
40
48
|
/** Anchor instruction discriminator `sha256("global:create_voucher")[0..8]`. */
|
|
41
49
|
export declare const CREATE_VOUCHER_DISCRIMINATOR: Uint8Array;
|
|
42
50
|
/** Anchor instruction discriminator `sha256("global:claim_voucher")[0..8]`. */
|
|
@@ -130,8 +138,11 @@ export interface CreateVoucherParams {
|
|
|
130
138
|
export declare function buildCreateVoucherIx(p: CreateVoucherParams): BuiltInstruction;
|
|
131
139
|
export interface ClaimVoucherParams {
|
|
132
140
|
readonly programId: AddressLike;
|
|
133
|
-
/** Pays the tx fee + the handle's rent (NOT the mint fee). Signer.
|
|
134
|
-
*
|
|
141
|
+
/** Pays the tx fee + the handle's rent (NOT the mint fee). Signer. For a
|
|
142
|
+
* bound voucher this MUST be the recipient (audit R4-A-3,
|
|
143
|
+
* `VoucherClaimNotRecipient` 6109): this untokenized claim skips the
|
|
144
|
+
* bundled pNFT, so only the recipient may opt out of it. Relayers use
|
|
145
|
+
* `buildClaimVoucherTokenizedIx`, which stays permissionless. */
|
|
135
146
|
readonly claimer: AddressLike;
|
|
136
147
|
readonly config: AddressLike;
|
|
137
148
|
/** `Config.treasury`. */
|
|
@@ -160,8 +171,15 @@ export interface RefundVoucherParams {
|
|
|
160
171
|
readonly payer: AddressLike;
|
|
161
172
|
readonly voucher: AddressLike;
|
|
162
173
|
readonly name: string;
|
|
174
|
+
/** The `["handle", name]` PDA (audit R4-A-2), appended as the trailing
|
|
175
|
+
* optional account. When that handle is already registered (someone else
|
|
176
|
+
* took the name, so the claim can never succeed) the payer may refund
|
|
177
|
+
* BEFORE `expiresAt`. Omit to send the original two-account form. */
|
|
178
|
+
readonly handle?: AddressLike;
|
|
163
179
|
}
|
|
164
|
-
/** Build `refund_voucher` — payer-signed
|
|
180
|
+
/** Build `refund_voucher` — payer-signed. Valid at/after `expiresAt`, or
|
|
181
|
+
* earlier when `handle` is passed and that name is already registered
|
|
182
|
+
* (audit R4-A-2). */
|
|
165
183
|
export declare function buildRefundVoucherIx(p: RefundVoucherParams): BuiltInstruction;
|
|
166
184
|
/** `sha256("global:claim_voucher_tokenized")[0..8]`. */
|
|
167
185
|
export declare const CLAIM_VOUCHER_TOKENIZED_DISCRIMINATOR: Uint8Array;
|
package/dist/voucher.js
CHANGED
|
@@ -25,18 +25,26 @@
|
|
|
25
25
|
* a bound recipient (audit S6 — open/recipient-less vouchers were removed as
|
|
26
26
|
* front-runnable; `claimSecretHash` is vestigial and rejected). An optional integrator referrer (the same trailing accounts as
|
|
27
27
|
* `register`, from `integrator.ts`) is snapshotted so the claim can split
|
|
28
|
-
* the escrow. `expiresAt` must be strictly in the future
|
|
28
|
+
* the escrow. `expiresAt` must be strictly in the future and at most
|
|
29
|
+
* `MAX_VOUCHER_DURATION_SECS` (366 days) away (audit R4-A-2).
|
|
29
30
|
* - `claim_voucher(name)` registers the handle, drawing the fee from the
|
|
30
31
|
* escrow (split integrator/treasury per the snapshot), and closes the
|
|
31
32
|
* voucher — rent back to the original payer. The `claimer` pays only the tx
|
|
32
|
-
* fee and the handle's rent, never the mint fee
|
|
33
|
-
*
|
|
33
|
+
* fee and the handle's rent, never the mint fee. This untokenized claim must
|
|
34
|
+
* be signed by the bound recipient (audit R4-A-3); a relayer uses
|
|
35
|
+
* `claim_voucher_tokenized`. Supply the integrator wallet iff the voucher
|
|
36
|
+
* carries one.
|
|
34
37
|
* - `refund_voucher(name)` returns the whole escrow (+ rent) to the payer,
|
|
35
|
-
* payer-signed,
|
|
38
|
+
* payer-signed, at/after `expiresAt`, or earlier once the name has been
|
|
39
|
+
* registered by someone else (pass `handle`, audit R4-A-2).
|
|
36
40
|
*/
|
|
37
41
|
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
38
42
|
/** Seed prefix of the voucher PDA: `["voucher", name]`. */
|
|
39
43
|
export const VOUCHER_SEED = "voucher";
|
|
44
|
+
/** Longest a voucher may stay open: `create_voucher` refuses an `expiresAt`
|
|
45
|
+
* more than this many seconds ahead (`VoucherExpiryTooFar`, 6112; audit
|
|
46
|
+
* R4-A-2). Mirrors lib.rs `MAX_VOUCHER_DURATION_SECS`. */
|
|
47
|
+
export const MAX_VOUCHER_DURATION_SECS = 366 * 24 * 60 * 60;
|
|
40
48
|
/** Anchor instruction discriminator `sha256("global:create_voucher")[0..8]`. */
|
|
41
49
|
export const CREATE_VOUCHER_DISCRIMINATOR = Uint8Array.from([22, 97, 32, 21, 104, 137, 188, 143]);
|
|
42
50
|
/** Anchor instruction discriminator `sha256("global:claim_voucher")[0..8]`. */
|
|
@@ -231,20 +239,22 @@ export function buildClaimVoucherIx(p) {
|
|
|
231
239
|
}
|
|
232
240
|
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
233
241
|
}
|
|
234
|
-
/** Build `refund_voucher` — payer-signed
|
|
242
|
+
/** Build `refund_voucher` — payer-signed. Valid at/after `expiresAt`, or
|
|
243
|
+
* earlier when `handle` is passed and that name is already registered
|
|
244
|
+
* (audit R4-A-2). */
|
|
235
245
|
export function buildRefundVoucherIx(p) {
|
|
236
246
|
const name = encString(p.name);
|
|
237
247
|
const data = new Uint8Array(8 + name.length);
|
|
238
248
|
data.set(REFUND_VOUCHER_DISCRIMINATOR, 0);
|
|
239
249
|
data.set(name, 8);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
};
|
|
250
|
+
const keys = [
|
|
251
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
252
|
+
{ pubkey: toBase58(p.voucher, "voucher"), isSigner: false, isWritable: true },
|
|
253
|
+
];
|
|
254
|
+
if (p.handle !== undefined) {
|
|
255
|
+
keys.push({ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false });
|
|
256
|
+
}
|
|
257
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
248
258
|
}
|
|
249
259
|
// ===================== claim_voucher_tokenized (gift NFTs, #8335) =====================
|
|
250
260
|
/** `sha256("global:claim_voucher_tokenized")[0..8]`. */
|
package/dist/x402.d.ts
CHANGED
|
@@ -21,12 +21,21 @@
|
|
|
21
21
|
* its own SVM chain, but `@x402/core`'s reference client/facilitator only route
|
|
22
22
|
* the `solana:*` scheme family — so to let ANY standard, unmodified x402 client
|
|
23
23
|
* or wallet pay an x1id agent out of the box, X1 testnet uses the CAIP-30 form
|
|
24
|
-
* keyed on X1's genesis.
|
|
25
|
-
*
|
|
24
|
+
* keyed on X1's genesis. x1id's X1 rail is standalone and self-verified: the
|
|
25
|
+
* buyer self-sponsors and broadcasts, and the seller checks the finalized
|
|
26
|
+
* transaction on chain itself; no external facilitator is involved. An
|
|
27
|
+
* earlier release used a principled `"x1:<full-genesis>"`
|
|
26
28
|
* extension; it was OSS-incompatible, so this migrated to CAIP-30 —
|
|
27
29
|
* {@link X1_LEGACY_TESTNET_NETWORK} is kept only for reading older manifests.
|
|
28
30
|
* Verified against `getGenesisHash` (full hash
|
|
29
31
|
* `C7ucgdDEhxLTpXHhWSZxavSVmaNTUJWwT5iTdeaviDho`).
|
|
32
|
+
*
|
|
33
|
+
* # HTTP headers (x402 v2 transport)
|
|
34
|
+
*
|
|
35
|
+
* api.x1id.io sends the 402 body also as `PAYMENT-REQUIRED` (base64 JSON),
|
|
36
|
+
* accepts the payment in `PAYMENT-SIGNATURE` (base64 JSON; the v1 name
|
|
37
|
+
* `X-PAYMENT` still works), and returns `PAYMENT-RESPONSE` plus the v1
|
|
38
|
+
* `X-PAYMENT-RESPONSE` on success.
|
|
30
39
|
*/
|
|
31
40
|
/** X1 testnet's x402 network id — CAIP-30 `"solana:" + first 32 chars of the
|
|
32
41
|
* genesis hash — so unmodified OSS x402 clients route to it (#8325). */
|
|
@@ -72,9 +81,23 @@ export interface X402PaymentRequirements {
|
|
|
72
81
|
* no sponsor (see docs/x402.md): this SDK always sets it to the
|
|
73
82
|
* BUYER's own address (self-sponsored, no facilitator, no custody). */
|
|
74
83
|
readonly feePayer: string;
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
|
|
84
|
+
/** Reference-key mode (api.x1id.io, R3-x402-1): a base58 pubkey derived
|
|
85
|
+
* from the request + challenge nonce + payer (see
|
|
86
|
+
* {@link deriveX402Reference}). The buyer appends it to the
|
|
87
|
+
* TransferChecked instruction as one extra READ-ONLY, NON-SIGNER account.
|
|
88
|
+
* Nobody holds a key for it. */
|
|
89
|
+
readonly reference?: string;
|
|
90
|
+
/** Hex SHA-256 of the canonical request the challenge was issued for
|
|
91
|
+
* ({@link x402RequestHash}). Informational: lets a client confirm it is
|
|
92
|
+
* about to pay for the request it will actually send. */
|
|
93
|
+
readonly requestHash?: string;
|
|
94
|
+
/** R4-D-1 payer-signed redemption (api.x1id.io): the exact UTF-8 message
|
|
95
|
+
* the bound payer signs (ed25519, `signMessage`) to redeem this challenge
|
|
96
|
+
* ({@link x402RedeemMessage}). The base58 signature goes in the payment
|
|
97
|
+
* payload as `payload.payerSig`. The on-chain payment is public once it
|
|
98
|
+
* lands; this signature is what proves the REDEEMER is the payer, so a
|
|
99
|
+
* front-runner who copies the transaction signature cannot redeem it. */
|
|
100
|
+
readonly redeemMessage?: string;
|
|
78
101
|
readonly recentBlockhash?: string;
|
|
79
102
|
readonly lastValidBlockHeight?: string;
|
|
80
103
|
};
|
|
@@ -97,6 +120,10 @@ export interface X402PaymentPayload {
|
|
|
97
120
|
};
|
|
98
121
|
}
|
|
99
122
|
export interface BuildX402RequirementsParams {
|
|
123
|
+
/** Reference-key mode: the derived reference pubkey + request hash
|
|
124
|
+
* ({@link deriveX402Reference}, {@link x402RequestHash}). */
|
|
125
|
+
readonly reference?: string;
|
|
126
|
+
readonly requestHash?: string;
|
|
100
127
|
/** The recipient's base58 address. */
|
|
101
128
|
readonly payTo: string;
|
|
102
129
|
/** The SPL/Token-2022 mint's base58 address. */
|
|
@@ -114,7 +141,6 @@ export interface BuildX402RequirementsParams {
|
|
|
114
141
|
* default. Pass {@link X1_TESTNET_NETWORK} for testnet. */
|
|
115
142
|
readonly network?: string;
|
|
116
143
|
readonly maxTimeoutSeconds?: number;
|
|
117
|
-
readonly memo?: string;
|
|
118
144
|
readonly recentBlockhash?: string;
|
|
119
145
|
readonly lastValidBlockHeight?: string;
|
|
120
146
|
}
|
|
@@ -128,3 +154,41 @@ export declare function buildX402Requirements(p: BuildX402RequirementsParams): X
|
|
|
128
154
|
/** Parse a 402 response body, returning null if it doesn't look like one
|
|
129
155
|
* (missing/wrong `x402Version`, `accepts` not an array). Never throws. */
|
|
130
156
|
export declare function parseX402Response(body: unknown): X402PaymentRequiredResponse | null;
|
|
157
|
+
/** Domain-separation tag for the reference preimage. */
|
|
158
|
+
export declare const X402_REFERENCE_DOMAIN = "x1id-x402-ref:v1|api.x1id.io/dev-api";
|
|
159
|
+
/** SHA-256("") in hex — the body hash of a GET/HEAD or empty body. */
|
|
160
|
+
export declare const X402_EMPTY_BODY_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
161
|
+
/** Canonicalise a URL path (see the section docs). */
|
|
162
|
+
export declare function canonicalizeX402Path(path: string): string;
|
|
163
|
+
/** Canonicalise a query string (with or without the leading "?"). */
|
|
164
|
+
export declare function canonicalizeX402Query(query: string): string;
|
|
165
|
+
export interface X402RequestInput {
|
|
166
|
+
/** HTTP method; uppercased. */
|
|
167
|
+
readonly method: string;
|
|
168
|
+
/** The request target: a path with optional `?query` and `#fragment`, or an
|
|
169
|
+
* absolute URL (scheme + host are dropped). */
|
|
170
|
+
readonly url: string;
|
|
171
|
+
/** The exact body bytes (a string is hashed as UTF-8). Ignored for GET/HEAD. */
|
|
172
|
+
readonly body?: Uint8Array | string | null;
|
|
173
|
+
}
|
|
174
|
+
/** The canonical request string (see the section docs). */
|
|
175
|
+
export declare function canonicalX402Request(req: X402RequestInput): string;
|
|
176
|
+
/** Hex SHA-256 of {@link canonicalX402Request} — `accepts[].extra.requestHash`. */
|
|
177
|
+
export declare function x402RequestHash(req: X402RequestInput): string;
|
|
178
|
+
/** Domain-separation tag for the payer-signed redemption message (R4-D-1). */
|
|
179
|
+
export declare const X402_REDEEM_DOMAIN = "x1id-x402-redeem:v1|api.x1id.io/dev-api";
|
|
180
|
+
/**
|
|
181
|
+
* R4-D-1: the message the bound payer signs to redeem a challenge — over its
|
|
182
|
+
* `accepts[].extra.reference`, which is single-use and already bound
|
|
183
|
+
* (server-side) to the endpoint, the exact request and the payer, so the
|
|
184
|
+
* signature cannot be replayed onto another challenge. Sent as base58 in
|
|
185
|
+
* `payload.payerSig`. The 402 also carries it verbatim as
|
|
186
|
+
* `accepts[].extra.redeemMessage`.
|
|
187
|
+
*/
|
|
188
|
+
export declare function x402RedeemMessage(reference: string): string;
|
|
189
|
+
/** Derive the base58 reference pubkey for a request + challenge nonce + payer. */
|
|
190
|
+
export declare function deriveX402Reference(p: {
|
|
191
|
+
readonly canonicalRequest: string;
|
|
192
|
+
readonly nonce: string;
|
|
193
|
+
readonly payer: string;
|
|
194
|
+
}): string;
|
package/dist/x402.js
CHANGED
|
@@ -21,13 +21,24 @@
|
|
|
21
21
|
* its own SVM chain, but `@x402/core`'s reference client/facilitator only route
|
|
22
22
|
* the `solana:*` scheme family — so to let ANY standard, unmodified x402 client
|
|
23
23
|
* or wallet pay an x1id agent out of the box, X1 testnet uses the CAIP-30 form
|
|
24
|
-
* keyed on X1's genesis.
|
|
25
|
-
*
|
|
24
|
+
* keyed on X1's genesis. x1id's X1 rail is standalone and self-verified: the
|
|
25
|
+
* buyer self-sponsors and broadcasts, and the seller checks the finalized
|
|
26
|
+
* transaction on chain itself; no external facilitator is involved. An
|
|
27
|
+
* earlier release used a principled `"x1:<full-genesis>"`
|
|
26
28
|
* extension; it was OSS-incompatible, so this migrated to CAIP-30 —
|
|
27
29
|
* {@link X1_LEGACY_TESTNET_NETWORK} is kept only for reading older manifests.
|
|
28
30
|
* Verified against `getGenesisHash` (full hash
|
|
29
31
|
* `C7ucgdDEhxLTpXHhWSZxavSVmaNTUJWwT5iTdeaviDho`).
|
|
32
|
+
*
|
|
33
|
+
* # HTTP headers (x402 v2 transport)
|
|
34
|
+
*
|
|
35
|
+
* api.x1id.io sends the 402 body also as `PAYMENT-REQUIRED` (base64 JSON),
|
|
36
|
+
* accepts the payment in `PAYMENT-SIGNATURE` (base64 JSON; the v1 name
|
|
37
|
+
* `X-PAYMENT` still works), and returns `PAYMENT-RESPONSE` plus the v1
|
|
38
|
+
* `X-PAYMENT-RESPONSE` on success.
|
|
30
39
|
*/
|
|
40
|
+
import { encodeBase58 } from "./base58.js";
|
|
41
|
+
import { sha256, toHex } from "./sha256.js";
|
|
31
42
|
/** X1 testnet's x402 network id — CAIP-30 `"solana:" + first 32 chars of the
|
|
32
43
|
* genesis hash — so unmodified OSS x402 clients route to it (#8325). */
|
|
33
44
|
export const X1_TESTNET_NETWORK = "solana:C7ucgdDEhxLTpXHhWSZxavSVmaNTUJWw";
|
|
@@ -71,12 +82,17 @@ export function buildX402Requirements(p) {
|
|
|
71
82
|
if (!/^\d+$/.test(p.amount)) {
|
|
72
83
|
throw new Error("amount must be a decimal string of smallest units (e.g. \"1000000\")");
|
|
73
84
|
}
|
|
74
|
-
if (p.memo !== undefined && new TextEncoder().encode(p.memo).length > 256) {
|
|
75
|
-
throw new Error("memo must be <=256 UTF-8 bytes");
|
|
76
|
-
}
|
|
77
85
|
const extra = { feePayer: p.buyer };
|
|
78
|
-
if (p.
|
|
79
|
-
|
|
86
|
+
if (p.reference !== undefined) {
|
|
87
|
+
if (!PUBKEY_RE.test(p.reference))
|
|
88
|
+
throw new Error("reference must be a base58 address");
|
|
89
|
+
extra.reference = p.reference;
|
|
90
|
+
}
|
|
91
|
+
if (p.requestHash !== undefined) {
|
|
92
|
+
if (!/^[0-9a-f]{64}$/.test(p.requestHash))
|
|
93
|
+
throw new Error("requestHash must be 64 lowercase hex chars");
|
|
94
|
+
extra.requestHash = p.requestHash;
|
|
95
|
+
}
|
|
80
96
|
if (p.recentBlockhash !== undefined) {
|
|
81
97
|
extra.recentBlockhash = p.recentBlockhash;
|
|
82
98
|
}
|
|
@@ -103,3 +119,138 @@ export function parseX402Response(body) {
|
|
|
103
119
|
return null;
|
|
104
120
|
return { x402Version: X402_VERSION, accepts: b.accepts };
|
|
105
121
|
}
|
|
122
|
+
// ── Request-bound reference keys (api.x1id.io R3-x402-1) ───────────────────────
|
|
123
|
+
//
|
|
124
|
+
// A 402 challenge from api.x1id.io binds the payment to the EXACT request it
|
|
125
|
+
// was issued for. The server derives a `reference` pubkey from the canonical
|
|
126
|
+
// request, the challenge nonce and the payer; the buyer appends it to the
|
|
127
|
+
// TransferChecked instruction as an extra read-only, non-signer account (SPL
|
|
128
|
+
// Token ignores extra accounts for a single-owner transfer), ~6.6k CU per
|
|
129
|
+
// payment. Nobody signs with the reference; it is not a keypair or a PDA, just
|
|
130
|
+
// 32 hash bytes read as an address.
|
|
131
|
+
//
|
|
132
|
+
// Canonical request (one UTF-8 string, "\n"-separated, no trailing newline):
|
|
133
|
+
//
|
|
134
|
+
// METHOD uppercased ("GET", "POST")
|
|
135
|
+
// path split on "/", each segment percent-decoded then re-encoded
|
|
136
|
+
// per RFC 3986 (unreserved A-Z a-z 0-9 - . _ ~ literal, every
|
|
137
|
+
// other byte %XX uppercase). No trailing-slash folding, case
|
|
138
|
+
// PRESERVED: /v1/resolve/@Alice and /v1/resolve/@alice are
|
|
139
|
+
// DIFFERENT requests (both canonicalise "@" to %40).
|
|
140
|
+
// query split on "&" (empty parts dropped), each "k=v" split on the
|
|
141
|
+
// first "=" ("k" alone == "k="), "+" read as a space, both
|
|
142
|
+
// sides percent-decoded then re-encoded as above, sorted by
|
|
143
|
+
// (key, value) on the encoded strings, duplicates KEPT, joined
|
|
144
|
+
// with "&". "" when there is no query. No fragment.
|
|
145
|
+
// bodyHash lowercase hex SHA-256 of the exact body bytes; GET/HEAD or
|
|
146
|
+
// no body = SHA-256("") = e3b0c442…b855.
|
|
147
|
+
//
|
|
148
|
+
// Scheme and host are excluded (the domain is fixed by the tag below).
|
|
149
|
+
//
|
|
150
|
+
// requestHash = hex(SHA-256(canonical))
|
|
151
|
+
// reference = base58(SHA-256(utf8(X402_REFERENCE_DOMAIN + "\n" + canonical
|
|
152
|
+
// + "\n" + nonce + "\n" + payer)))
|
|
153
|
+
/** Domain-separation tag for the reference preimage. */
|
|
154
|
+
export const X402_REFERENCE_DOMAIN = "x1id-x402-ref:v1|api.x1id.io/dev-api";
|
|
155
|
+
/** SHA-256("") in hex — the body hash of a GET/HEAD or empty body. */
|
|
156
|
+
export const X402_EMPTY_BODY_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
157
|
+
function isUnreserved(b) {
|
|
158
|
+
return ((b >= 0x41 && b <= 0x5a) || (b >= 0x61 && b <= 0x7a) || (b >= 0x30 && b <= 0x39) ||
|
|
159
|
+
b === 0x2d || b === 0x2e || b === 0x5f || b === 0x7e);
|
|
160
|
+
}
|
|
161
|
+
function hexVal(c) {
|
|
162
|
+
if (c >= 0x30 && c <= 0x39)
|
|
163
|
+
return c - 0x30;
|
|
164
|
+
if (c >= 0x41 && c <= 0x46)
|
|
165
|
+
return c - 0x37;
|
|
166
|
+
if (c >= 0x61 && c <= 0x66)
|
|
167
|
+
return c - 0x57;
|
|
168
|
+
return -1;
|
|
169
|
+
}
|
|
170
|
+
/** Percent-decode to raw bytes. A `%` not followed by two hex digits is kept
|
|
171
|
+
* literally (decoding never throws). */
|
|
172
|
+
function pctDecode(s, plusIsSpace) {
|
|
173
|
+
const raw = new TextEncoder().encode(s);
|
|
174
|
+
const out = [];
|
|
175
|
+
for (let i = 0; i < raw.length; i++) {
|
|
176
|
+
const c = raw[i];
|
|
177
|
+
if (c === 0x25 && i + 2 < raw.length) {
|
|
178
|
+
const hi = hexVal(raw[i + 1]);
|
|
179
|
+
const lo = hexVal(raw[i + 2]);
|
|
180
|
+
if (hi >= 0 && lo >= 0) {
|
|
181
|
+
out.push(hi * 16 + lo);
|
|
182
|
+
i += 2;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
out.push(plusIsSpace && c === 0x2b ? 0x20 : c);
|
|
187
|
+
}
|
|
188
|
+
return new Uint8Array(out);
|
|
189
|
+
}
|
|
190
|
+
function pctEncode(bytes) {
|
|
191
|
+
let s = "";
|
|
192
|
+
for (const b of bytes)
|
|
193
|
+
s += isUnreserved(b) ? String.fromCharCode(b) : "%" + b.toString(16).toUpperCase().padStart(2, "0");
|
|
194
|
+
return s;
|
|
195
|
+
}
|
|
196
|
+
/** Canonicalise a URL path (see the section docs). */
|
|
197
|
+
export function canonicalizeX402Path(path) {
|
|
198
|
+
const p = path === "" ? "/" : path;
|
|
199
|
+
return p
|
|
200
|
+
.split("/")
|
|
201
|
+
.map((seg) => pctEncode(pctDecode(seg, false)))
|
|
202
|
+
.join("/");
|
|
203
|
+
}
|
|
204
|
+
/** Canonicalise a query string (with or without the leading "?"). */
|
|
205
|
+
export function canonicalizeX402Query(query) {
|
|
206
|
+
const q = query.startsWith("?") ? query.slice(1) : query;
|
|
207
|
+
const pairs = [];
|
|
208
|
+
for (const part of q.split("&")) {
|
|
209
|
+
if (part === "")
|
|
210
|
+
continue;
|
|
211
|
+
const eq = part.indexOf("=");
|
|
212
|
+
const k = eq === -1 ? part : part.slice(0, eq);
|
|
213
|
+
const v = eq === -1 ? "" : part.slice(eq + 1);
|
|
214
|
+
pairs.push([pctEncode(pctDecode(k, true)), pctEncode(pctDecode(v, true))]);
|
|
215
|
+
}
|
|
216
|
+
pairs.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0));
|
|
217
|
+
return pairs.map(([k, v]) => `${k}=${v}`).join("&");
|
|
218
|
+
}
|
|
219
|
+
/** The canonical request string (see the section docs). */
|
|
220
|
+
export function canonicalX402Request(req) {
|
|
221
|
+
const method = req.method.toUpperCase();
|
|
222
|
+
let target = req.url;
|
|
223
|
+
const abs = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/?#]*/.exec(target);
|
|
224
|
+
if (abs)
|
|
225
|
+
target = target.slice(abs[0].length);
|
|
226
|
+
const hashAt = target.indexOf("#");
|
|
227
|
+
if (hashAt !== -1)
|
|
228
|
+
target = target.slice(0, hashAt);
|
|
229
|
+
const qAt = target.indexOf("?");
|
|
230
|
+
const path = qAt === -1 ? target : target.slice(0, qAt);
|
|
231
|
+
const query = qAt === -1 ? "" : target.slice(qAt + 1);
|
|
232
|
+
const noBody = method === "GET" || method === "HEAD" || req.body === undefined || req.body === null;
|
|
233
|
+
const bodyHash = noBody ? X402_EMPTY_BODY_HASH : toHex(sha256(req.body));
|
|
234
|
+
return `${method}\n${canonicalizeX402Path(path)}\n${canonicalizeX402Query(query)}\n${bodyHash}`;
|
|
235
|
+
}
|
|
236
|
+
/** Hex SHA-256 of {@link canonicalX402Request} — `accepts[].extra.requestHash`. */
|
|
237
|
+
export function x402RequestHash(req) {
|
|
238
|
+
return toHex(sha256(canonicalX402Request(req)));
|
|
239
|
+
}
|
|
240
|
+
/** Domain-separation tag for the payer-signed redemption message (R4-D-1). */
|
|
241
|
+
export const X402_REDEEM_DOMAIN = "x1id-x402-redeem:v1|api.x1id.io/dev-api";
|
|
242
|
+
/**
|
|
243
|
+
* R4-D-1: the message the bound payer signs to redeem a challenge — over its
|
|
244
|
+
* `accepts[].extra.reference`, which is single-use and already bound
|
|
245
|
+
* (server-side) to the endpoint, the exact request and the payer, so the
|
|
246
|
+
* signature cannot be replayed onto another challenge. Sent as base58 in
|
|
247
|
+
* `payload.payerSig`. The 402 also carries it verbatim as
|
|
248
|
+
* `accepts[].extra.redeemMessage`.
|
|
249
|
+
*/
|
|
250
|
+
export function x402RedeemMessage(reference) {
|
|
251
|
+
return `${X402_REDEEM_DOMAIN}\nreference:${reference}`;
|
|
252
|
+
}
|
|
253
|
+
/** Derive the base58 reference pubkey for a request + challenge nonce + payer. */
|
|
254
|
+
export function deriveX402Reference(p) {
|
|
255
|
+
return encodeBase58(sha256(`${X402_REFERENCE_DOMAIN}\n${p.canonicalRequest}\n${p.nonce}\n${p.payer}`));
|
|
256
|
+
}
|