@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 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
- memo: "invoice-42",
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,
@@ -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
- readonly value: number | bigint;
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;
@@ -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
+ }
@@ -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; a relayer may claim for a
33
- * bound recipient. Supply the integrator wallet iff the voucher carries one.
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, only at/after `expiresAt`.
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. May be a
134
- * relayer when the voucher binds a recipient. */
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, only valid at/after `expiresAt`. */
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; a relayer may claim for a
33
- * bound recipient. Supply the integrator wallet iff the voucher carries one.
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, only at/after `expiresAt`.
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, only valid at/after `expiresAt`. */
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
- return {
241
- programId: toBase58(p.programId, "programId"),
242
- keys: [
243
- { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
244
- { pubkey: toBase58(p.voucher, "voucher"), isSigner: false, isWritable: true },
245
- ],
246
- data,
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. (Fortiswap's self-hosted X1 facilitator uses the
25
- * identical id.) An earlier release used a principled `"x1:<full-genesis>"`
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
- /** UTF-8, <=256 bytes. Lets a seller correlate the on-chain payment to
76
- * an invoice without a unique deposit address per sale. */
77
- readonly memo?: string;
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. (Fortiswap's self-hosted X1 facilitator uses the
25
- * identical id.) An earlier release used a principled `"x1:<full-genesis>"`
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.memo !== undefined)
79
- extra.memo = p.memo;
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x1id/resolve",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Resolve @handles and X1NS names on X1. Never silently picks between namespaces.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",