@x1id/resolve 0.23.0 → 0.44.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.
Files changed (49) hide show
  1. package/README.md +97 -0
  2. package/dist/accounts.d.ts +6 -0
  3. package/dist/accounts.js +6 -1
  4. package/dist/agent.d.ts +184 -8
  5. package/dist/agent.js +351 -3
  6. package/dist/agentPolicy.d.ts +238 -0
  7. package/dist/agentPolicy.js +364 -0
  8. package/dist/attestation.d.ts +25 -0
  9. package/dist/attestation.js +25 -0
  10. package/dist/checkout.d.ts +216 -0
  11. package/dist/checkout.js +327 -0
  12. package/dist/credentials.d.ts +466 -0
  13. package/dist/credentials.js +669 -0
  14. package/dist/gate.d.ts +94 -4
  15. package/dist/gate.js +206 -7
  16. package/dist/gateClient.d.ts +12 -0
  17. package/dist/giftLink.d.ts +272 -0
  18. package/dist/giftLink.js +452 -0
  19. package/dist/index.d.ts +13 -0
  20. package/dist/index.js +32 -0
  21. package/dist/loginButton.d.ts +69 -0
  22. package/dist/loginButton.js +207 -0
  23. package/dist/logo.d.ts +1 -0
  24. package/dist/logo.js +8 -0
  25. package/dist/messaging.d.ts +439 -0
  26. package/dist/messaging.js +979 -0
  27. package/dist/nsGate.d.ts +98 -0
  28. package/dist/nsGate.js +172 -0
  29. package/dist/oidc.d.ts +272 -0
  30. package/dist/oidc.js +641 -0
  31. package/dist/pay.d.ts +513 -0
  32. package/dist/pay.js +756 -0
  33. package/dist/payment.d.ts +9 -2
  34. package/dist/payment.js +9 -2
  35. package/dist/recovery.d.ts +186 -0
  36. package/dist/recovery.js +276 -0
  37. package/dist/recoveryNft.d.ts +183 -0
  38. package/dist/recoveryNft.js +291 -0
  39. package/dist/shield.d.ts +373 -0
  40. package/dist/shield.js +695 -0
  41. package/dist/subscription.d.ts +439 -0
  42. package/dist/subscription.js +642 -0
  43. package/dist/textRecords.d.ts +52 -6
  44. package/dist/textRecords.js +80 -8
  45. package/dist/types.d.ts +3 -0
  46. package/dist/x402Receipt.d.ts +246 -0
  47. package/dist/x402Receipt.js +374 -0
  48. package/package.json +18 -1
  49. package/schema/agent-manifest.json +91 -8
@@ -0,0 +1,642 @@
1
+ /**
2
+ * X1ID Pay, phase 2: SPL pull subscriptions (docs/specs/x1id-pay-onchain.md
3
+ * §3/§4).
4
+ *
5
+ * - `Subscription` at `["sub", payer, payee, id]`: created and paid for by
6
+ * the PAYER, its terms (mint, decimals, amount, period, max charges, the
7
+ * optional `@name` binding) fixed for life. Any change = a new id.
8
+ * - The payer's delegate PDA `["sub_delegate", payer]` is the SPL delegate on
9
+ * the payer's canonical ATA. ONE per payer (an SPL account has a single
10
+ * delegate), so the SPL approval is shared by every subscription of that
11
+ * payer in a mint. The program tracks each subscription's own share in
12
+ * `allowanceLeft` (layout v2, Sentinel run-011): a charge needs and spends
13
+ * `amount` of it, so no subscription can spend another's share.
14
+ * - The payer's `SubLedger` at `["sub_ledger", payer, mint]` (Sentinel S-01)
15
+ * holds the running total of the Active shares in that mint. It is created
16
+ * on first use by `create_subscription` (payer-funded, 82 bytes) and is
17
+ * passed to every subscription instruction except close.
18
+ * - `create_subscription` records `amount × min(charges left, 12)` as the new
19
+ * subscription's `allowanceLeft`, adds it to the ledger and SETS the SPL
20
+ * approval to the new ledger total (not "current + window"), so an outside
21
+ * revoke / approve or a re-created ATA can never leave a share unbacked
22
+ * while another subscription spends it. An unlimited (or > 12-charge)
23
+ * subscription needs a top-up after 12 charges:
24
+ * {@link buildTopUpSubscriptionIx} (raises the share and the ledger, then
25
+ * re-approves the ledger total; a plain SPL `Approve` alone does not extend
26
+ * a subscription).
27
+ * - Any cancel (payer or payee) removes the subscription's `allowanceLeft`
28
+ * from the ledger. A payer cancel with `giveBack` also re-approves
29
+ * `min(current approval, remaining ledger total)` (Revoke at 0).
30
+ * - `charge_subscription` is a permissionless crank with NO arguments: it
31
+ * pulls exactly `amount` once `now >= next_due_at` (fee to the treasury as
32
+ * `pay_invoice`), then moves `next_due_at` to the next slot of the
33
+ * creation-anchored schedule after `now` ({@link subNextDueAt}).
34
+ * - SPL Token and Token-2022 only; native XNT is not pullable (use invoices).
35
+ * Token-2022 mints must carry only {@link SUB_ALLOWED_MINT_EXTENSIONS}.
36
+ *
37
+ * Routed through the program's `fallback` (`extra_ix`) like the rest of Pay:
38
+ * `sha256("global:<name>")[..8]` + raw Borsh, accounts in exactly the order of
39
+ * the program's `Accounts` structs. Hand-rolled, no web3.js.
40
+ */
41
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
42
+ // ---------------------------------------------------------------------------
43
+ // constants (mirror programs/x1-handles/src/{lib,state}.rs)
44
+ // ---------------------------------------------------------------------------
45
+ export const SUBSCRIPTION_SEED = "sub";
46
+ export const SUB_DELEGATE_SEED = "sub_delegate";
47
+ /** The payer's per-mint ledger: `["sub_ledger", payer, mint]`. */
48
+ export const SUB_LEDGER_SEED = "sub_ledger";
49
+ /** Allocated size of a `SubLedger` (8-byte discriminator + 74). */
50
+ export const SUB_LEDGER_ACCOUNT_LEN = 8 + 74;
51
+ /** Current `SubLedger.version`. */
52
+ export const SUB_LEDGER_VERSION = 1;
53
+ /** Length of a subscription id (`[u8; 16]`). */
54
+ export const SUBSCRIPTION_ID_LEN = 16;
55
+ /** Allocated size of a `Subscription` (8-byte discriminator + `INIT_SPACE`;
56
+ * 238 since layout v2 added `allowance_left`). */
57
+ export const SUBSCRIPTION_ACCOUNT_LEN = 8 + 238;
58
+ /** Current `Subscription.version`: 2 added `allowance_left`; 3 (same bytes)
59
+ * marks a subscription counted in its payer's `SubLedger`. A v2 account is
60
+ * refused by `charge_subscription` until a payer top-up adopts it. */
61
+ export const SUBSCRIPTION_VERSION = 3;
62
+ /** Shortest / longest billing period, seconds (1 hour / 5 × 366 days). */
63
+ export const SUB_PERIOD_MIN_SECS = 3600n;
64
+ export const SUB_PERIOD_MAX_SECS = 5n * 366n * 86400n;
65
+ /** Charges' worth of allowance `create_subscription` approves (and records
66
+ * as the subscription's `allowanceLeft`) at most. */
67
+ export const SUB_APPROVE_WINDOW_CHARGES = 12n;
68
+ /** Token-2022 mint extension types a subscription mint may carry. */
69
+ export const SUB_ALLOWED_MINT_EXTENSIONS = [3, 6, 12, 18, 19, 20, 21, 22, 23, 26];
70
+ /** Byte offsets inside a raw `Subscription`, for `getProgramAccounts`
71
+ * `memcmp` filters. Fields before the variable-width `handle` are fixed. */
72
+ export const SUBSCRIPTION_OFFSETS = {
73
+ version: 8,
74
+ payer: 9,
75
+ payee: 41,
76
+ id: 73,
77
+ mint: 89,
78
+ decimals: 121,
79
+ amount: 122,
80
+ periodSecs: 130,
81
+ nextDueAt: 138,
82
+ maxCharges: 146,
83
+ chargesMade: 150,
84
+ status: 154,
85
+ createdAt: 155,
86
+ lastChargedAt: 163,
87
+ handle: 171,
88
+ };
89
+ const d8 = (...b) => Uint8Array.from(b);
90
+ /** `sha256("global:create_subscription")[..8]`. */
91
+ export const CREATE_SUBSCRIPTION_DISCRIMINATOR = d8(65, 71, 10, 60, 249, 82, 197, 12);
92
+ /** `sha256("global:charge_subscription")[..8]`. */
93
+ export const CHARGE_SUBSCRIPTION_DISCRIMINATOR = d8(121, 52, 210, 23, 3, 68, 86, 194);
94
+ /** `sha256("global:cancel_subscription")[..8]`. */
95
+ export const CANCEL_SUBSCRIPTION_DISCRIMINATOR = d8(60, 139, 189, 242, 191, 208, 143, 18);
96
+ /** `sha256("global:close_subscription")[..8]`. */
97
+ export const CLOSE_SUBSCRIPTION_DISCRIMINATOR = d8(33, 214, 169, 135, 35, 127, 78, 7);
98
+ /** `sha256("global:top_up_subscription")[..8]`. */
99
+ export const TOP_UP_SUBSCRIPTION_DISCRIMINATOR = d8(145, 113, 117, 42, 174, 139, 53, 61);
100
+ /** `sha256("account:Subscription")[..8]`. */
101
+ export const SUBSCRIPTION_ACCOUNT_DISCRIMINATOR = d8(64, 7, 26, 135, 102, 132, 98, 33);
102
+ /** `sha256("account:SubLedger")[..8]`. */
103
+ export const SUB_LEDGER_ACCOUNT_DISCRIMINATOR = d8(18, 52, 119, 178, 113, 160, 89, 255);
104
+ /** Byte offsets inside a raw `SubLedger` (fixed layout). */
105
+ export const SUB_LEDGER_OFFSETS = { version: 8, payer: 9, mint: 41, total: 73, bump: 81 };
106
+ /** Program error codes added by Pay phase 2 (`RegistryError`, 6138-6150),
107
+ * plus the upgrade-5 Sentinel block at 6230-6235 (run-011 and the S-01
108
+ * ledger; 6158-6229 belong to upgrade 6). Subscriptions also reuse the Pay
109
+ * phase 1 codes for the fee config (6122), the `@name` binding (6126-6128)
110
+ * and the mint/payee/treasury accounts (6131-6133); see `PAY_ERRORS`. */
111
+ export const SUBSCRIPTION_ERRORS = {
112
+ SubscriptionAmountZero: 6138,
113
+ SubscriptionPeriodOutOfRange: 6139,
114
+ SubscriptionStartOutOfRange: 6140,
115
+ SubscriptionMintNotSupported: 6141,
116
+ SubscriptionPayerIsPayee: 6142,
117
+ SubscriptionDelegateInUse: 6143,
118
+ SubscriptionNotActive: 6144,
119
+ SubscriptionNotDue: 6145,
120
+ SubscriptionFinished: 6146,
121
+ SubscriptionAllowanceExhausted: 6147,
122
+ SubscriptionTokenAccountMismatch: 6148,
123
+ NotSubscriptionParty: 6149,
124
+ SubscriptionStillActive: 6150,
125
+ /** `payee` = the payer's own `["sub_delegate", payer]` PDA. */
126
+ SubscriptionPayeeIsDelegate: 6230,
127
+ /** `top_up_subscription` above `amount × charges left`. */
128
+ SubscriptionTopUpTooLarge: 6233,
129
+ /** Cancel without the `SubLedger` (a ledger-counted subscription or a
130
+ * give-back), or a charge of a legacy v2 subscription not yet adopted. */
131
+ SubscriptionLedgerRequired: 6234,
132
+ /** The ledger total would overflow u64 (or fall below a share). */
133
+ SubscriptionLedgerOutOfRange: 6235,
134
+ };
135
+ /** `Subscription.status` as stored (Borsh enum index). */
136
+ export const SUBSCRIPTION_STATUS = { Active: 0, Cancelled: 1 };
137
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
138
+ const ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
139
+ const TOKEN_2022_PROGRAM = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
140
+ const TE = new TextEncoder();
141
+ const U32_MAX = 0xffff_ffff;
142
+ const U64_MAX = 0xffffffffffffffffn;
143
+ const I64_MAX = 0x7fffffffffffffffn;
144
+ // ---------------------------------------------------------------------------
145
+ // encoding helpers
146
+ // ---------------------------------------------------------------------------
147
+ function toBytes32(v, what) {
148
+ if (typeof v === "string") {
149
+ const b = decodeBase58_32(v);
150
+ if (!b)
151
+ throw new Error(`${what} is not a valid base58 address`);
152
+ return b;
153
+ }
154
+ if (v.length !== 32)
155
+ throw new Error(`${what} must be exactly 32 bytes`);
156
+ return v;
157
+ }
158
+ function toBase58(v, what) {
159
+ return encodeBase58(toBytes32(v, what));
160
+ }
161
+ function key(pubkey, what, isSigner, isWritable) {
162
+ return { pubkey: toBase58(pubkey, what), isSigner, isWritable };
163
+ }
164
+ function concat(...parts) {
165
+ const out = new Uint8Array(parts.reduce((a, p) => a + p.length, 0));
166
+ let o = 0;
167
+ for (const p of parts) {
168
+ out.set(p, o);
169
+ o += p.length;
170
+ }
171
+ return out;
172
+ }
173
+ function u32le(n) {
174
+ const out = new Uint8Array(4);
175
+ new DataView(out.buffer).setUint32(0, n, true);
176
+ return out;
177
+ }
178
+ function u64le(n) {
179
+ const out = new Uint8Array(8);
180
+ new DataView(out.buffer).setBigUint64(0, n, true);
181
+ return out;
182
+ }
183
+ function i64le(n) {
184
+ const out = new Uint8Array(8);
185
+ new DataView(out.buffer).setBigInt64(0, n, true);
186
+ return out;
187
+ }
188
+ function hasPrefix(data, disc) {
189
+ if (data.length < disc.length)
190
+ return false;
191
+ for (let i = 0; i < disc.length; i++)
192
+ if (data[i] !== disc[i])
193
+ return false;
194
+ return true;
195
+ }
196
+ function checkId(id) {
197
+ if (!(id instanceof Uint8Array) || id.length !== SUBSCRIPTION_ID_LEN) {
198
+ throw new Error(`subscription id must be exactly ${SUBSCRIPTION_ID_LEN} bytes`);
199
+ }
200
+ return id;
201
+ }
202
+ function toSecs(n) {
203
+ return typeof n === "bigint" ? n : BigInt(Math.floor(n));
204
+ }
205
+ // ---------------------------------------------------------------------------
206
+ // pure helpers (mirror the program's)
207
+ // ---------------------------------------------------------------------------
208
+ /** The next due time after a charge at `now >= nextDueAt`: the first slot of
209
+ * the creation-anchored schedule strictly after `now` (the program's
210
+ * `sub_next_due_at`). One charge per period; missed periods are forfeited. */
211
+ export function subNextDueAt(nextDueAt, periodSecs, now) {
212
+ const n = toSecs(now);
213
+ if (periodSecs <= 0n)
214
+ throw new Error("periodSecs must be positive");
215
+ if (n < nextDueAt)
216
+ throw new Error("not due yet");
217
+ const missed = (n - nextDueAt) / periodSecs;
218
+ return nextDueAt + periodSecs * (missed + 1n);
219
+ }
220
+ /** `amount × min(charges left, 12)` (unlimited = the full window),
221
+ * saturating at u64::MAX: what `create_subscription` adds to the shared
222
+ * approval and sets as the subscription's `allowanceLeft`. (A payer-signed
223
+ * `cancel_subscription` gives back the stored `allowanceLeft`, not this.) */
224
+ export function subWindowAllowance(amount, maxCharges, chargesMade = 0) {
225
+ let left = SUB_APPROVE_WINDOW_CHARGES;
226
+ if (maxCharges !== 0) {
227
+ const remaining = BigInt(Math.max(0, maxCharges - chargesMade));
228
+ if (remaining < left)
229
+ left = remaining;
230
+ }
231
+ const v = amount * left;
232
+ return v > U64_MAX ? U64_MAX : v;
233
+ }
234
+ /** Whether a raw mint account (owned by `owner`) may back a subscription:
235
+ * any classic SPL mint; a Token-2022 mint only with extensions in
236
+ * {@link SUB_ALLOWED_MINT_EXTENSIONS} (the program's
237
+ * `sub_mint_extensions_allowed`). */
238
+ export function isSubscriptionMintSupported(data, owner) {
239
+ if (owner !== TOKEN_2022_PROGRAM || data.length <= 82)
240
+ return true;
241
+ if (data.length <= 165 || data[165] !== 1)
242
+ return false;
243
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
244
+ let o = 166;
245
+ while (o + 4 <= data.length) {
246
+ const t = dv.getUint16(o, true);
247
+ const len = dv.getUint16(o + 2, true);
248
+ if (t === 0)
249
+ break;
250
+ if (o + 4 + len > data.length || !SUB_ALLOWED_MINT_EXTENSIONS.includes(t))
251
+ return false;
252
+ o += 4 + len;
253
+ }
254
+ return true;
255
+ }
256
+ /** 16 fresh random bytes for a new subscription id (CSPRNG). */
257
+ export function newSubscriptionId() {
258
+ const c = globalThis.crypto;
259
+ if (!c?.getRandomValues)
260
+ throw new Error("No CSPRNG available (globalThis.crypto.getRandomValues)");
261
+ return c.getRandomValues(new Uint8Array(SUBSCRIPTION_ID_LEN));
262
+ }
263
+ // ---------------------------------------------------------------------------
264
+ // PDAs
265
+ // ---------------------------------------------------------------------------
266
+ /** A subscription: `["sub", payer, payee, id]`. */
267
+ export async function findSubscriptionPda(find, programId, payer, payee, id) {
268
+ return find([TE.encode(SUBSCRIPTION_SEED), toBytes32(payer, "payer"), toBytes32(payee, "payee"), checkId(id)], toBase58(programId, "programId"));
269
+ }
270
+ /** The payer's SPL delegate for every subscription: `["sub_delegate", payer]`. */
271
+ export async function findSubDelegatePda(find, programId, payer) {
272
+ return find([TE.encode(SUB_DELEGATE_SEED), toBytes32(payer, "payer")], toBase58(programId, "programId"));
273
+ }
274
+ /** The payer's ledger of Active subscription shares in one mint:
275
+ * `["sub_ledger", payer, mint]`. */
276
+ export async function findSubLedgerPda(find, programId, payer, mint) {
277
+ return find([TE.encode(SUB_LEDGER_SEED), toBytes32(payer, "payer"), toBytes32(mint, "mint")], toBase58(programId, "programId"));
278
+ }
279
+ const STATUS_NAMES = ["active", "cancelled"];
280
+ /**
281
+ * Decode a raw `Subscription` (discriminator included) in the Borsh order of
282
+ * state.rs: version u8 | payer | payee | id [u8;16] | mint | decimals u8 |
283
+ * amount u64 | period_secs i64 | next_due_at i64 | max_charges u32 |
284
+ * charges_made u32 | status u8 | created_at i64 | last_charged_at i64 |
285
+ * handle Option<Pubkey> | memo_hash [u8;32] | bump u8 | delegate_bump u8 |
286
+ * allowance_left u64 (version 2; a version-1 account decodes with
287
+ * `allowanceLeft = 0n`). Null when the bytes are not a `Subscription`.
288
+ */
289
+ export function decodeSubscription(data) {
290
+ const O = SUBSCRIPTION_OFFSETS;
291
+ if (!hasPrefix(data, SUBSCRIPTION_ACCOUNT_DISCRIMINATOR) || data.length < O.handle + 1)
292
+ return null;
293
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
294
+ const pk = (o) => encodeBase58(data.slice(o, o + 32));
295
+ const status = STATUS_NAMES[data[O.status]];
296
+ if (status === undefined)
297
+ return null;
298
+ const tag = data[O.handle];
299
+ if (tag > 1)
300
+ return null;
301
+ let o = O.handle + 1;
302
+ let handle = null;
303
+ if (tag === 1) {
304
+ if (data.length < o + 32)
305
+ return null;
306
+ handle = pk(o);
307
+ o += 32;
308
+ }
309
+ if (data.length < o + 32 + 2)
310
+ return null;
311
+ const version = data[O.version];
312
+ let allowanceLeft = 0n;
313
+ if (version >= 2) {
314
+ if (data.length < o + 34 + 8)
315
+ return null;
316
+ allowanceLeft = dv.getBigUint64(o + 34, true);
317
+ }
318
+ return {
319
+ version,
320
+ payer: pk(O.payer),
321
+ payee: pk(O.payee),
322
+ id: data.slice(O.id, O.id + SUBSCRIPTION_ID_LEN),
323
+ mint: pk(O.mint),
324
+ decimals: data[O.decimals],
325
+ amount: dv.getBigUint64(O.amount, true),
326
+ periodSecs: dv.getBigInt64(O.periodSecs, true),
327
+ nextDueAt: dv.getBigInt64(O.nextDueAt, true),
328
+ maxCharges: dv.getUint32(O.maxCharges, true),
329
+ chargesMade: dv.getUint32(O.chargesMade, true),
330
+ status,
331
+ createdAt: dv.getBigInt64(O.createdAt, true),
332
+ lastChargedAt: dv.getBigInt64(O.lastChargedAt, true),
333
+ handle,
334
+ memoHash: data.slice(o, o + 32),
335
+ bump: data[o + 32],
336
+ delegateBump: data[o + 33],
337
+ allowanceLeft,
338
+ };
339
+ }
340
+ /** Borsh-encode a {@link Subscription} exactly as the program stores it (the
341
+ * inverse of {@link decodeSubscription}, zero-padded to the allocation). */
342
+ export function encodeSubscription(s) {
343
+ const status = STATUS_NAMES.indexOf(s.status);
344
+ if (status < 0)
345
+ throw new Error("bad status");
346
+ if (s.memoHash.length !== 32)
347
+ throw new Error("memoHash must be exactly 32 bytes");
348
+ const body = concat(SUBSCRIPTION_ACCOUNT_DISCRIMINATOR, Uint8Array.of(s.version), toBytes32(s.payer, "payer"), toBytes32(s.payee, "payee"), checkId(s.id), toBytes32(s.mint, "mint"), Uint8Array.of(s.decimals), u64le(s.amount), i64le(s.periodSecs), i64le(s.nextDueAt), u32le(s.maxCharges), u32le(s.chargesMade), Uint8Array.of(status), i64le(s.createdAt), i64le(s.lastChargedAt), s.handle === null ? Uint8Array.of(0) : concat(Uint8Array.of(1), toBytes32(s.handle, "handle")), s.memoHash, Uint8Array.of(s.bump, s.delegateBump), s.version >= 2 ? u64le(s.allowanceLeft) : new Uint8Array(0));
349
+ const out = new Uint8Array(Math.max(SUBSCRIPTION_ACCOUNT_LEN, body.length));
350
+ out.set(body);
351
+ return out;
352
+ }
353
+ /** Decode a raw `SubLedger` (discriminator included): version u8 | payer |
354
+ * mint | total u64 | bump u8. Null when the bytes are not a `SubLedger`. */
355
+ export function decodeSubLedger(data) {
356
+ const O = SUB_LEDGER_OFFSETS;
357
+ if (!hasPrefix(data, SUB_LEDGER_ACCOUNT_DISCRIMINATOR) || data.length < SUB_LEDGER_ACCOUNT_LEN)
358
+ return null;
359
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
360
+ return {
361
+ version: data[O.version],
362
+ payer: encodeBase58(data.slice(O.payer, O.payer + 32)),
363
+ mint: encodeBase58(data.slice(O.mint, O.mint + 32)),
364
+ total: dv.getBigUint64(O.total, true),
365
+ bump: data[O.bump],
366
+ };
367
+ }
368
+ /** Borsh-encode a {@link SubLedger} exactly as the program stores it. */
369
+ export function encodeSubLedger(l) {
370
+ if (typeof l.total !== "bigint" || l.total < 0n || l.total > U64_MAX)
371
+ throw new Error("total must be a u64 bigint");
372
+ return concat(SUB_LEDGER_ACCOUNT_DISCRIMINATOR, Uint8Array.of(l.version), toBytes32(l.payer, "payer"), toBytes32(l.mint, "mint"), u64le(l.total), Uint8Array.of(l.bump));
373
+ }
374
+ /** Read and decode a payer's `SubLedger`. Null when it does not exist yet
375
+ * (no subscription in that mint so far); throws when it exists but is not
376
+ * one (or, with `programId`, is not owned by it). */
377
+ export async function fetchSubLedger(rpc, address, opts = {}) {
378
+ const addr = toBase58(address, "address");
379
+ const res = (await rpc("getAccountInfo", [addr, { encoding: "base64", commitment: opts.commitment ?? "confirmed" }]));
380
+ const v = res?.value;
381
+ if (!v)
382
+ return null;
383
+ if (opts.programId !== undefined && v.owner !== toBase58(opts.programId, "programId")) {
384
+ throw new Error("account is not owned by the registry program");
385
+ }
386
+ const bin = atob(v.data[0]);
387
+ const data = new Uint8Array(bin.length);
388
+ for (let i = 0; i < bin.length; i++)
389
+ data[i] = bin.charCodeAt(i);
390
+ const ledger = decodeSubLedger(data);
391
+ if (!ledger)
392
+ throw new Error("account is not an X1ID subscription ledger");
393
+ return { address: addr, ledger, lamports: BigInt(v.lamports) };
394
+ }
395
+ /** Whether every allowed charge has been made (`maxCharges > 0`). */
396
+ export function isSubscriptionFinished(s) {
397
+ return s.maxCharges !== 0 && s.chargesMade >= s.maxCharges;
398
+ }
399
+ /** Whether `charge_subscription` would pass its status/schedule checks at
400
+ * `nowSecs` (Active, charges left, `now >= nextDueAt`). The allowance,
401
+ * balance, binding and lockdown are checked on chain. */
402
+ export function isSubscriptionDue(s, nowSecs) {
403
+ return s.status === "active" && !isSubscriptionFinished(s) && toSecs(nowSecs) >= s.nextDueAt;
404
+ }
405
+ /** Read and decode a Subscription. Null when it does not exist; throws when
406
+ * it exists but is not one (or, with `programId`, is not owned by it). */
407
+ export async function fetchSubscription(rpc, address, opts = {}) {
408
+ const addr = toBase58(address, "address");
409
+ const res = (await rpc("getAccountInfo", [addr, { encoding: "base64", commitment: opts.commitment ?? "confirmed" }]));
410
+ const v = res?.value;
411
+ if (!v)
412
+ return null;
413
+ if (opts.programId !== undefined && v.owner !== toBase58(opts.programId, "programId")) {
414
+ throw new Error("account is not owned by the registry program");
415
+ }
416
+ const bin = atob(v.data[0]);
417
+ const data = new Uint8Array(bin.length);
418
+ for (let i = 0; i < bin.length; i++)
419
+ data[i] = bin.charCodeAt(i);
420
+ const subscription = decodeSubscription(data);
421
+ if (!subscription)
422
+ throw new Error("account is not an X1ID subscription");
423
+ return { address: addr, subscription, lamports: BigInt(v.lamports) };
424
+ }
425
+ // ---------------------------------------------------------------------------
426
+ // instruction builders
427
+ // ---------------------------------------------------------------------------
428
+ function bindingKeys(b) {
429
+ const out = [key(b.handle, "handle", false, false), key(b.payeeRecord, "payeeRecord", false, false)];
430
+ if (b.pnftTokenAccount !== undefined)
431
+ out.push(key(b.pnftTokenAccount, "pnftTokenAccount", false, false));
432
+ if (b.namespace !== undefined)
433
+ out.push(key(b.namespace, "namespace", false, false));
434
+ return out;
435
+ }
436
+ /** The Borsh arguments of `create_subscription` (after the discriminator). */
437
+ export function encodeCreateSubscriptionArgs(a) {
438
+ if (typeof a.amount !== "bigint" || a.amount <= 0n || a.amount > U64_MAX) {
439
+ throw new Error("amount must be a u64 bigint greater than zero");
440
+ }
441
+ if (typeof a.periodSecs !== "bigint" || a.periodSecs < SUB_PERIOD_MIN_SECS || a.periodSecs > SUB_PERIOD_MAX_SECS) {
442
+ throw new Error(`periodSecs must be between ${SUB_PERIOD_MIN_SECS} and ${SUB_PERIOD_MAX_SECS} seconds`);
443
+ }
444
+ if (typeof a.firstDueAt !== "bigint" || a.firstDueAt < 0n || a.firstDueAt > I64_MAX) {
445
+ throw new Error("firstDueAt must be a non-negative i64 bigint (0n = now)");
446
+ }
447
+ if (!Number.isInteger(a.maxCharges) || a.maxCharges < 0 || a.maxCharges > U32_MAX) {
448
+ throw new Error("maxCharges must be a u32 (0 = unlimited)");
449
+ }
450
+ if (a.memoHash.length !== 32)
451
+ throw new Error("memoHash must be exactly 32 bytes");
452
+ return concat(checkId(a.id), toBytes32(a.mint, "mint"), u64le(a.amount), i64le(a.periodSecs), i64le(a.firstDueAt), u32le(a.maxCharges), a.memoHash, Uint8Array.of(a.bindHandle ? 1 : 0));
453
+ }
454
+ /**
455
+ * `create_subscription(id, mint, amount, period_secs, first_due_at,
456
+ * max_charges, memo_hash, bind_handle)`. Accounts (`CreateSubscription`):
457
+ * payer(s,w), payee, config, subscription(w), sub_delegate, sub_ledger(w),
458
+ * mint, payer_token_account(w), token_program, system_program, then the
459
+ * optional handle, payee_record and the binding's remaining accounts.
460
+ *
461
+ * The program records {@link subWindowAllowance} as the subscription's own
462
+ * `allowanceLeft`, adds it to the payer's ledger and approves `subDelegate`
463
+ * on `payerTokenAccount` for EXACTLY the new ledger total (the sum of every
464
+ * Active share in the mint), so the wallet shows that delegate approval.
465
+ * Fails with `SubscriptionDelegateInUse` (6143) if the ATA has another app's
466
+ * delegate; prepend an SPL `Revoke` if the payer agrees. Refuses `payee` =
467
+ * `subDelegate` (6230: funds sent there could never move).
468
+ */
469
+ export function buildCreateSubscriptionIx(p) {
470
+ const payer = toBase58(p.payer, "payer");
471
+ const payee = toBase58(p.payee, "payee");
472
+ if (payer === payee)
473
+ throw new Error("payer and payee must differ");
474
+ if (payee === toBase58(p.subDelegate, "subDelegate")) {
475
+ throw new Error("payee cannot be the payer's subscription delegate PDA (SubscriptionPayeeIsDelegate)");
476
+ }
477
+ const keys = [
478
+ key(payer, "payer", true, true),
479
+ key(payee, "payee", false, false),
480
+ key(p.config, "config", false, false),
481
+ key(p.subscription, "subscription", false, true),
482
+ key(p.subDelegate, "subDelegate", false, false),
483
+ key(p.subLedger, "subLedger", false, true),
484
+ key(p.mint, "mint", false, false),
485
+ key(p.payerTokenAccount, "payerTokenAccount", false, true),
486
+ key(p.tokenProgram, "tokenProgram", false, false),
487
+ key(SYSTEM_PROGRAM, "systemProgram", false, false),
488
+ ];
489
+ if (p.bindHandle !== undefined)
490
+ keys.push(...bindingKeys(p.bindHandle));
491
+ return {
492
+ programId: toBase58(p.programId, "programId"),
493
+ keys,
494
+ data: concat(CREATE_SUBSCRIPTION_DISCRIMINATOR, encodeCreateSubscriptionArgs({
495
+ id: p.id,
496
+ mint: p.mint,
497
+ amount: p.amount,
498
+ periodSecs: p.periodSecs,
499
+ firstDueAt: p.firstDueAt ?? 0n,
500
+ maxCharges: p.maxCharges ?? 0,
501
+ memoHash: p.memoHash ?? new Uint8Array(32),
502
+ bindHandle: p.bindHandle !== undefined,
503
+ })),
504
+ };
505
+ }
506
+ /**
507
+ * `charge_subscription()`: no arguments; the program reads every term from
508
+ * the Subscription. Accounts (`ChargeSubscription`): cranker(s,w),
509
+ * subscription(w), config, treasury, pay_config, sub_delegate,
510
+ * sub_ledger(w), mint, payer_token_account(w), payee_token_account(w),
511
+ * treasury_token_account(w), token_program, associated_token_program,
512
+ * system_program, then the optional handle, payee_record and the binding's
513
+ * remaining accounts. After an outside revoke of the payer's approval every
514
+ * charge fails with 6147 until the payer's next create / top-up.
515
+ */
516
+ export function buildChargeSubscriptionIx(p) {
517
+ const keys = [
518
+ key(p.cranker, "cranker", true, true),
519
+ key(p.subscription, "subscription", false, true),
520
+ key(p.config, "config", false, false),
521
+ key(p.treasury, "treasury", false, false),
522
+ key(p.payConfig, "payConfig", false, false),
523
+ key(p.subDelegate, "subDelegate", false, false),
524
+ key(p.subLedger, "subLedger", false, true),
525
+ key(p.mint, "mint", false, false),
526
+ key(p.payerTokenAccount, "payerTokenAccount", false, true),
527
+ key(p.payeeTokenAccount, "payeeTokenAccount", false, true),
528
+ key(p.treasuryTokenAccount, "treasuryTokenAccount", false, true),
529
+ key(p.tokenProgram, "tokenProgram", false, false),
530
+ key(ASSOCIATED_TOKEN_PROGRAM, "associatedTokenProgram", false, false),
531
+ key(SYSTEM_PROGRAM, "systemProgram", false, false),
532
+ ];
533
+ if (p.binding !== undefined)
534
+ keys.push(...bindingKeys(p.binding));
535
+ return { programId: toBase58(p.programId, "programId"), keys, data: CHARGE_SUBSCRIPTION_DISCRIMINATOR.slice() };
536
+ }
537
+ /** `cancel_subscription()`. Accounts (`CancelSubscription`): signer(s),
538
+ * subscription(w), sub_ledger(w) (the program id as "None" when `null`),
539
+ * then the optional payer_token_account(w), sub_delegate, token_program
540
+ * (used only when the payer signs). */
541
+ export function buildCancelSubscriptionIx(p) {
542
+ if (p.subLedger === undefined)
543
+ throw new Error("subLedger is required (null only for a legacy v2 subscription)");
544
+ if (p.subLedger === null && p.giveBack !== undefined)
545
+ throw new Error("giveBack needs the subLedger");
546
+ const programId = toBase58(p.programId, "programId");
547
+ const keys = [
548
+ key(p.signer, "signer", true, false),
549
+ key(p.subscription, "subscription", false, true),
550
+ p.subLedger === null ? key(programId, "subLedger", false, false) : key(p.subLedger, "subLedger", false, true),
551
+ ];
552
+ if (p.giveBack !== undefined) {
553
+ keys.push(key(p.giveBack.payerTokenAccount, "payerTokenAccount", false, true), key(p.giveBack.subDelegate, "subDelegate", false, false), key(p.giveBack.tokenProgram, "tokenProgram", false, false));
554
+ }
555
+ return { programId, keys, data: CANCEL_SUBSCRIPTION_DISCRIMINATOR.slice() };
556
+ }
557
+ /** `close_subscription()`. Accounts (`CloseSubscription`): signer(s),
558
+ * subscription(w), payer(w). Only once Cancelled or finished. */
559
+ export function buildCloseSubscriptionIx(p) {
560
+ return {
561
+ programId: toBase58(p.programId, "programId"),
562
+ keys: [
563
+ key(p.signer, "signer", true, false),
564
+ key(p.subscription, "subscription", false, true),
565
+ key(p.payer, "payer", false, true),
566
+ ],
567
+ data: CLOSE_SUBSCRIPTION_DISCRIMINATOR.slice(),
568
+ };
569
+ }
570
+ /**
571
+ * `top_up_subscription(amount)`: raise ONE subscription's `allowanceLeft`
572
+ * and the payer's ledger total by `amount`, then approve the payer's
573
+ * delegate PDA for EXACTLY the new ledger total (also restoring the pool
574
+ * after an outside revoke). Accounts (`TopUpSubscription`): payer(s,w),
575
+ * subscription(w), config, sub_delegate, sub_ledger(w),
576
+ * payer_token_account(w), token_program, system_program. Payer only; Active
577
+ * and not finished; refused during lockdown and when the ATA has another
578
+ * app's delegate (6143).
579
+ */
580
+ export function buildTopUpSubscriptionIx(p) {
581
+ if (typeof p.amount !== "bigint" || p.amount <= 0n || p.amount > U64_MAX) {
582
+ throw new Error("amount must be a u64 bigint greater than zero");
583
+ }
584
+ return {
585
+ programId: toBase58(p.programId, "programId"),
586
+ keys: [
587
+ key(p.payer, "payer", true, true),
588
+ key(p.subscription, "subscription", false, true),
589
+ key(p.config, "config", false, false),
590
+ key(p.subDelegate, "subDelegate", false, false),
591
+ key(p.subLedger, "subLedger", false, true),
592
+ key(p.payerTokenAccount, "payerTokenAccount", false, true),
593
+ key(p.tokenProgram, "tokenProgram", false, false),
594
+ key(SYSTEM_PROGRAM, "systemProgram", false, false),
595
+ ],
596
+ data: concat(TOP_UP_SUBSCRIPTION_DISCRIMINATOR, u64le(p.amount)),
597
+ };
598
+ }
599
+ /** A plain SPL `Approve` (instruction 4, same bytes under both token
600
+ * programs) of the payer's delegate PDA: a precise re-approve, e.g. to cut
601
+ * the surplus a payee-cancelled subscription left back to the ledger total.
602
+ * It does NOT extend a subscription (each charge also needs the
603
+ * subscription's own `allowanceLeft`); use {@link buildTopUpSubscriptionIx}
604
+ * for that (it also re-approves the ledger total). */
605
+ export function buildSubscriptionAllowanceIx(p) {
606
+ if (typeof p.allowance !== "bigint" || p.allowance < 0n || p.allowance > U64_MAX) {
607
+ throw new Error("allowance must be a u64 bigint");
608
+ }
609
+ return {
610
+ programId: toBase58(p.tokenProgram, "tokenProgram"),
611
+ keys: [
612
+ key(p.payerTokenAccount, "payerTokenAccount", false, true),
613
+ key(p.subDelegate, "subDelegate", false, false),
614
+ key(p.payer, "payer", true, false),
615
+ ],
616
+ data: concat(Uint8Array.of(4), u64le(p.allowance)),
617
+ };
618
+ }
619
+ /** Plain-language sentence for a subscription error code (6138-6150,
620
+ * 6230, 6233-6235), or null (see `payErrorMessage` for the shared Pay codes). */
621
+ export function subscriptionErrorMessage(code) {
622
+ switch (code) {
623
+ case 6138: return "A subscription amount must be greater than zero.";
624
+ case 6139: return "The billing period must be between 1 hour and 5 years.";
625
+ case 6140: return "The first charge must be now or a future date within 5 years.";
626
+ case 6141: return "This token can't be used for subscriptions (its extensions could change what a charge pays).";
627
+ case 6142: return "You can't subscribe to yourself.";
628
+ case 6143: return "Your token account already has another app's spending approval. Revoke it first.";
629
+ case 6144: return "This subscription is cancelled.";
630
+ case 6145: return "The next charge isn't due yet.";
631
+ case 6146: return "This subscription has made all its charges.";
632
+ case 6147: return "This subscription's spending approval is used up or revoked. Top it up to continue.";
633
+ case 6148: return "The payer's token account doesn't match this subscription.";
634
+ case 6149: return "Only the subscriber or the merchant can do this.";
635
+ case 6150: return "This subscription is still active. Cancel it first.";
636
+ case 6230: return "A subscription can't pay to your own subscription delegate address.";
637
+ case 6233: return "That top-up is more than the remaining charges can use.";
638
+ case 6234: return "This subscription needs its subscription ledger. If it is an older subscription, top it up once to continue.";
639
+ case 6235: return "The total of your subscriptions in this token is out of range.";
640
+ default: return null;
641
+ }
642
+ }