@x1id/resolve 0.23.0 → 0.43.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 +71 -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
package/dist/shield.js ADDED
@@ -0,0 +1,695 @@
1
+ /**
2
+ * Shield: confidential zXNT / zUSDC.X on X1 (docs/specs/shield-confidential-tokens.md).
3
+ *
4
+ * What this module gives an integrator, with no dependencies and no proofs:
5
+ *
6
+ * - the Shield addresses, derived exactly like `programs/x1-shield`:
7
+ * z-mint `["z-mint", backing]`, `ShieldMint` `["shield-mint", z_mint]`, the
8
+ * reserve (the ShieldMint PDA's associated token account of the backing
9
+ * mint) and a holder's Shield account (their Token-2022 ATA of the z-mint);
10
+ * - the `shield` / `unshield` instruction builders (`buildShieldIx`,
11
+ * `buildUnshieldIx`);
12
+ * - decoders for a `ShieldMint` record and a Token-2022 account's
13
+ * `ConfidentialTransferAccount` extension, plus the "ready to receive
14
+ * privately" rule (spec §7);
15
+ * - {@link resolveShieldRecipient}: `@name` → the address to pay under the
16
+ * one payment rule (verified X1 record, else the current authority), its
17
+ * Shield account and whether it is ready, honouring the `x1.shield = off`
18
+ * opt-out text record and refusing reserved (treasury-held) names;
19
+ * - payment receipts (`x1receipt1…`): a strict bech32m codec, and
20
+ * {@link verifyShieldReceipt}, which finds the payment's proof data on chain
21
+ * and hands it to a commitment checker.
22
+ *
23
+ * Proof generation and the elliptic-curve check of a receipt stay in the
24
+ * separate `shield-proofs` WASM (spec §7: "so the SDK stays light"); this
25
+ * module takes that check as a function (`checkCommitments`), e.g.
26
+ * `(r, kind, data) => wasm.verifyReceiptAgainstProof(r, kind, data)`.
27
+ *
28
+ * PDAs come through an injected {@link PdaFinder}, like every other builder in
29
+ * this package: it never hand-rolls the ed25519 on-curve check.
30
+ */
31
+ import { encodeBase58, decodeBase58, decodeBase58_32 } from "./base58.js";
32
+ import { makeAccountReader, nftHolder, parseHandleAccount, } from "./accounts.js";
33
+ import { fetchRecords, liveRecords } from "./records.js";
34
+ import { fetchTextRecords, findTextRecord } from "./textRecords.js";
35
+ import { fetchConfigTreasury } from "./register.js";
36
+ import { paymentAddressFor } from "./payment.js";
37
+ import { ResolveError } from "./types.js";
38
+ // ---------------------------------------------------------------------------
39
+ // Constants
40
+ // ---------------------------------------------------------------------------
41
+ /** `programs/x1-shield` as deployed on X1 TESTNET (S1). Mainnet is not
42
+ * deployed yet (owner gate S5); a mainnet deploy may use its own id, so
43
+ * integrators should take the id from their network config. */
44
+ export const SHIELD_PROGRAM_ID_TESTNET = "7Kqs3Nu82F1NCoUgnKEBUyW2uiftRXQjc7vJ3D2Zdzxo";
45
+ export const TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
46
+ /** Classic SPL Token (p-token on X1) — the wrapped-native mint's program. */
47
+ export const SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
48
+ export const ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
49
+ export const ZK_ELGAMAL_PROOF_PROGRAM_ID = "ZkE1Gama1Proof11111111111111111111111111111";
50
+ /** Wrapped-native mint: the backing of zXNT. */
51
+ export const NATIVE_MINT = "So11111111111111111111111111111111111111112";
52
+ /** USDC.X on X1 mainnet: the backing of zUSDC.X there (spec §3). */
53
+ export const USDCX_MINT_MAINNET = "B69chRzqzDCmdB5WYB8NRu5Yv5ZA95ABiZcdzCgGm9Tq";
54
+ export const Z_MINT_SEED = "z-mint";
55
+ export const SHIELD_MINT_SEED = "shield-mint";
56
+ /** `sha256("global:shield")[0..8]`. */
57
+ export const SHIELD_DISCRIMINATOR = Uint8Array.from([220, 198, 253, 246, 231, 84, 147, 98]);
58
+ /** `sha256("global:unshield")[0..8]`. */
59
+ export const UNSHIELD_DISCRIMINATOR = Uint8Array.from([21, 228, 55, 24, 194, 10, 21, 22]);
60
+ /** `sha256("account:ShieldMint")[0..8]`. */
61
+ export const SHIELD_MINT_ACCOUNT_DISCRIMINATOR = Uint8Array.from([183, 22, 172, 202, 204, 78, 142, 136]);
62
+ /** Token-2022 caps one confidential Deposit at 2^48 − 1 base units, so one
63
+ * `shield` above that reverts (x1-shield FBL-0008): split it. */
64
+ export const MAX_SHIELD_AMOUNT = (1n << 48n) - 1n;
65
+ /** The X1ID text record a name sets to `off` to opt out of being offered for
66
+ * private payments (spec §7). */
67
+ export const SHIELD_OPT_OUT_TEXT_KEY = "x1.shield";
68
+ const TE = new TextEncoder();
69
+ function toBytes32(v, what) {
70
+ if (typeof v === "string") {
71
+ const b = decodeBase58_32(v);
72
+ if (!b)
73
+ throw new Error(`${what} is not a valid base58 address`);
74
+ return b;
75
+ }
76
+ if (v.length !== 32)
77
+ throw new Error(`${what} must be exactly 32 bytes`);
78
+ return v;
79
+ }
80
+ function toBase58(v, what) {
81
+ return encodeBase58(toBytes32(v, what));
82
+ }
83
+ function u64le(n) {
84
+ if (n < 0n || n > 0xffffffffffffffffn)
85
+ throw new Error("amount must fit in a u64");
86
+ const b = new Uint8Array(8);
87
+ new DataView(b.buffer).setBigUint64(0, n, true);
88
+ return b;
89
+ }
90
+ function readU64LE(d, o) {
91
+ return new DataView(d.buffer, d.byteOffset, d.byteLength).getBigUint64(o, true);
92
+ }
93
+ function b64ToBytes(s) {
94
+ const bin = atob(s);
95
+ const out = new Uint8Array(bin.length);
96
+ for (let i = 0; i < bin.length; i++)
97
+ out[i] = bin.charCodeAt(i);
98
+ return out;
99
+ }
100
+ // ---------------------------------------------------------------------------
101
+ // PDAs
102
+ // ---------------------------------------------------------------------------
103
+ /** The canonical z-mint of `backingMint`: `["z-mint", backing]`. */
104
+ export async function deriveZMint(find, programId, backingMint) {
105
+ return find([TE.encode(Z_MINT_SEED), toBytes32(backingMint, "backingMint")], toBase58(programId, "programId"));
106
+ }
107
+ /** A z-mint's `ShieldMint` record (its mint authority and the reserve's
108
+ * owner): `["shield-mint", z_mint]`. */
109
+ export async function deriveShieldMint(find, programId, zMint) {
110
+ return find([TE.encode(SHIELD_MINT_SEED), toBytes32(zMint, "zMint")], toBase58(programId, "programId"));
111
+ }
112
+ /** Associated token account of `owner` for `mint` under `tokenProgram`. */
113
+ export async function deriveAssociatedTokenAccount(find, owner, mint, tokenProgram) {
114
+ return find([toBytes32(owner, "owner"), toBytes32(tokenProgram, "tokenProgram"), toBytes32(mint, "mint")], ASSOCIATED_TOKEN_PROGRAM_ID);
115
+ }
116
+ /** A holder's Shield account for `zMint`: their Token-2022 ATA of it. */
117
+ export function deriveShieldAccount(find, owner, zMint) {
118
+ return deriveAssociatedTokenAccount(find, owner, zMint, TOKEN_2022_PROGRAM_ID);
119
+ }
120
+ export async function deriveShieldAccounts(find, p) {
121
+ const zMint = await deriveZMint(find, p.programId, p.backingMint);
122
+ const shieldMint = await deriveShieldMint(find, p.programId, zMint);
123
+ const reserve = await deriveAssociatedTokenAccount(find, shieldMint, p.backingMint, p.backingTokenProgram);
124
+ if (p.owner === undefined)
125
+ return { zMint, shieldMint, reserve };
126
+ const [zAccount, backingAccount] = await Promise.all([
127
+ deriveShieldAccount(find, p.owner, zMint),
128
+ deriveAssociatedTokenAccount(find, p.owner, p.backingMint, p.backingTokenProgram),
129
+ ]);
130
+ return { zMint, shieldMint, reserve, zAccount, backingAccount };
131
+ }
132
+ const k = (pubkey, what, isSigner, isWritable) => ({
133
+ pubkey: toBase58(pubkey, what),
134
+ isSigner,
135
+ isWritable,
136
+ });
137
+ /**
138
+ * `shield(amount)`: move backing into the reserve, mint exactly what arrived
139
+ * to the owner's Shield account and deposit it into its confidential PENDING
140
+ * balance. The owner's Shield account must already be configured for
141
+ * confidential balances. For zXNT, `backingAccount` is the owner's
142
+ * wrapped-native ATA, funded in the same transaction (create idempotent →
143
+ * system transfer → SyncNative → shield → CloseAccount; see
144
+ * programs/x1-shield/src/lib.rs "Client sequences").
145
+ */
146
+ export function buildShieldIx(p) {
147
+ return {
148
+ programId: toBase58(p.programId, "programId"),
149
+ keys: [
150
+ k(p.owner, "owner", true, false),
151
+ k(p.shieldMint, "shieldMint", false, true),
152
+ k(p.zMint, "zMint", false, true),
153
+ k(p.backingMint, "backingMint", false, false),
154
+ k(p.backingAccount, "backingAccount", false, true),
155
+ k(p.reserve, "reserve", false, true),
156
+ k(p.zAccount, "zAccount", false, true),
157
+ k(p.backingTokenProgram, "backingTokenProgram", false, false),
158
+ k(TOKEN_2022_PROGRAM_ID, "zTokenProgram", false, false),
159
+ ],
160
+ data: concatBytes(SHIELD_DISCRIMINATOR, u64le(p.amount)),
161
+ };
162
+ }
163
+ /**
164
+ * `unshield(amount)`: burn `amount` from the owner's PUBLIC z-balance (put
165
+ * there by a Token-2022 confidential Withdraw earlier in the same
166
+ * transaction), verify the burn, release exactly that backing to
167
+ * `backingAccount`. Note the account order differs from `shield`.
168
+ */
169
+ export function buildUnshieldIx(p) {
170
+ return {
171
+ programId: toBase58(p.programId, "programId"),
172
+ keys: [
173
+ k(p.owner, "owner", true, false),
174
+ k(p.shieldMint, "shieldMint", false, true),
175
+ k(p.zMint, "zMint", false, true),
176
+ k(p.backingMint, "backingMint", false, false),
177
+ k(p.zAccount, "zAccount", false, true),
178
+ k(p.reserve, "reserve", false, true),
179
+ k(p.backingAccount, "backingAccount", false, true),
180
+ k(p.backingTokenProgram, "backingTokenProgram", false, false),
181
+ k(TOKEN_2022_PROGRAM_ID, "zTokenProgram", false, false),
182
+ ],
183
+ data: concatBytes(UNSHIELD_DISCRIMINATOR, u64le(p.amount)),
184
+ };
185
+ }
186
+ function concatBytes(...parts) {
187
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
188
+ let o = 0;
189
+ for (const p of parts) {
190
+ out.set(p, o);
191
+ o += p.length;
192
+ }
193
+ return out;
194
+ }
195
+ /** `disc(8) z_mint(32) backing_mint(32) reserve(32) backing_token_program(32)
196
+ * decimals(1) shielded_supply(8) bump(1) z_mint_bump(1)`. Null when the bytes
197
+ * are not a ShieldMint. */
198
+ export function parseShieldMint(data) {
199
+ if (data.length < 147)
200
+ return null;
201
+ for (let i = 0; i < 8; i++)
202
+ if (data[i] !== SHIELD_MINT_ACCOUNT_DISCRIMINATOR[i])
203
+ return null;
204
+ const pk = (o) => encodeBase58(data.slice(o, o + 32));
205
+ return {
206
+ zMint: pk(8),
207
+ backingMint: pk(40),
208
+ reserve: pk(72),
209
+ backingTokenProgram: pk(104),
210
+ decimals: data[136],
211
+ shieldedSupply: readU64LE(data, 137),
212
+ };
213
+ }
214
+ /** spl-token-2022 base `Account` length; extensions follow. */
215
+ const BASE_ACCOUNT_LEN = 165;
216
+ const ACCOUNT_TYPE_ACCOUNT = 2;
217
+ /** `ExtensionType::ConfidentialTransferAccount`. */
218
+ export const EXT_CONFIDENTIAL_TRANSFER_ACCOUNT = 5;
219
+ const CT_ACCOUNT_LEN = 295;
220
+ /** Decode a Token-2022 token account and its confidential extension. Null
221
+ * when the bytes are not a token account. Pure. */
222
+ export function decodeConfidentialTokenAccount(data) {
223
+ if (data.length < BASE_ACCOUNT_LEN)
224
+ return null;
225
+ const mint = encodeBase58(data.slice(0, 32));
226
+ const owner = encodeBase58(data.slice(32, 64));
227
+ const amount = readU64LE(data, 64);
228
+ let ct = null;
229
+ if (data.length > BASE_ACCOUNT_LEN) {
230
+ if (data[BASE_ACCOUNT_LEN] !== ACCOUNT_TYPE_ACCOUNT)
231
+ return null;
232
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
233
+ let o = BASE_ACCOUNT_LEN + 1;
234
+ while (o + 4 <= data.length) {
235
+ const type = dv.getUint16(o, true);
236
+ const len = dv.getUint16(o + 2, true);
237
+ const start = o + 4;
238
+ if (type === 0 || start + len > data.length)
239
+ break; // Uninitialized / padding
240
+ if (type === EXT_CONFIDENTIAL_TRANSFER_ACCOUNT && len >= CT_ACCOUNT_LEN) {
241
+ let p = start;
242
+ const take = (n) => {
243
+ const v = data.slice(p, p + n);
244
+ p += n;
245
+ return v;
246
+ };
247
+ const approved = take(1)[0] === 1;
248
+ const elgamalPubkey = take(32);
249
+ const pendingLo = take(64);
250
+ const pendingHi = take(64);
251
+ const available = take(64);
252
+ const decryptable = take(36);
253
+ const allowConfidentialCredits = take(1)[0] === 1;
254
+ const allowNonConfidentialCredits = take(1)[0] === 1;
255
+ ct = {
256
+ approved,
257
+ elgamalPubkey,
258
+ pendingLo,
259
+ pendingHi,
260
+ available,
261
+ decryptable,
262
+ allowConfidentialCredits,
263
+ allowNonConfidentialCredits,
264
+ pendingCreditCounter: readU64LE(data, p),
265
+ maxPendingCreditCounter: readU64LE(data, p + 8),
266
+ };
267
+ }
268
+ o = start + len;
269
+ }
270
+ }
271
+ return { mint, owner, amount, ct };
272
+ }
273
+ /** True for the opt-out text record's value (`off`, any case/whitespace). */
274
+ export function isShieldOptOut(value) {
275
+ return (value ?? "").trim().toLowerCase() === "off";
276
+ }
277
+ /**
278
+ * Judge readiness to receive privately (spec §7) from the account at the
279
+ * derived Shield address: `account` is `{ data, owner }` as read from chain
280
+ * (null = no account). Pure.
281
+ */
282
+ export function shieldReadiness(account, expected, optedOut = false) {
283
+ if (optedOut)
284
+ return { kind: "opted-out" };
285
+ if (!account)
286
+ return { kind: "no-account" };
287
+ if (account.owner !== TOKEN_2022_PROGRAM_ID)
288
+ return { kind: "mismatch" };
289
+ const acct = decodeConfidentialTokenAccount(account.data);
290
+ if (!acct || acct.owner !== expected.owner || acct.mint !== expected.zMint)
291
+ return { kind: "mismatch" };
292
+ if (!acct.ct)
293
+ return { kind: "not-configured" };
294
+ if (!acct.ct.approved || !acct.ct.allowConfidentialCredits)
295
+ return { kind: "not-accepting" };
296
+ return { kind: "ready", elgamalPubkey: acct.ct.elgamalPubkey };
297
+ }
298
+ /**
299
+ * Resolve an @name for a private payment in `zMint` (spec §7):
300
+ *
301
+ * 1. the name's payment address under the one rule (`paymentAddressFor`): a
302
+ * live VERIFIED X1 record, else the current authority (`Handle.owner`, or
303
+ * the pNFT holder of a tokenized name);
304
+ * 2. a reserved name (its payment target is the registry treasury) is
305
+ * REFUSED — `ResolveError("reserved")`;
306
+ * 3. the recipient's Shield account (their Token-2022 ATA of `zMint`) is read
307
+ * from chain: `ataReady` iff it is configured for confidential balances;
308
+ * 4. a live `x1.shield = off` text record makes the name not ready
309
+ * (`readiness.kind === "opted-out"`), however its account looks.
310
+ *
311
+ * No X1ID record is needed to receive privately; readiness is chain state.
312
+ */
313
+ export async function resolveShieldRecipient(name, zMint, config) {
314
+ const reader = makeAccountReader(config.rpcUrl, config.fetchImpl);
315
+ const rpc = reader.rpc;
316
+ const zMintB58 = toBase58(zMint, "zMint");
317
+ const programBytes = toBytes32(config.handleProgramId, "handleProgramId");
318
+ const programB58 = encodeBase58(programBytes);
319
+ const canonical = config.wasm.normalizeHandle(name.trim().replace(/^@/, ""));
320
+ if (!canonical)
321
+ throw new ResolveError("invalid-handle", `"${name}" is not a valid handle`, name);
322
+ const derived = config.wasm.deriveHandleAccount(canonical, programBytes);
323
+ if (!derived)
324
+ throw new ResolveError("invalid-handle", `"${name}" is not a valid handle`, name);
325
+ const handleAccount = encodeBase58(derived);
326
+ const info = await reader.accountInfo(handleAccount);
327
+ if (!info || info.owner !== programB58)
328
+ throw new ResolveError("not-found", `@${canonical} is not registered`, name);
329
+ const handle = parseHandleAccount(info.data);
330
+ if (!handle)
331
+ throw new ResolveError("rpc-error", `@${canonical} returned a malformed account`, name);
332
+ const authority = handle.nftMint === null
333
+ ? encodeBase58(handle.owner)
334
+ : (await nftHolder(reader, config.wasm, canonical, handle.nftMint, name)).address;
335
+ const treasury = config.treasury ??
336
+ (await fetchConfigTreasury(rpc, await config.find([TE.encode("config")], programB58))) ??
337
+ null;
338
+ const reserved = () => new ResolveError("reserved", `@${canonical} is reserved by X1ID and can't receive payments`, name);
339
+ if (treasury !== null && authority === treasury)
340
+ throw reserved();
341
+ const records = liveRecords(await fetchRecords(rpc, programB58, handleAccount, handle.registeredAt, handle.recordsClearedAt, handle.holderEpoch));
342
+ const payment = paymentAddressFor(records, authority);
343
+ if (!payment)
344
+ throw new ResolveError("not-found", `@${canonical} has no payment address right now`, name);
345
+ if (treasury !== null && payment.address === treasury)
346
+ throw reserved();
347
+ // holderEpoch: a native tokenized sale must stale the seller's opt-out.
348
+ // findTextRecord: byte-exact key, live only, never a BOM look-alike.
349
+ const texts = await fetchTextRecords(rpc, programB58, handleAccount, handle.registeredAt, handle.recordsClearedAt, handle.holderEpoch);
350
+ const optedOut = isShieldOptOut(findTextRecord(texts, SHIELD_OPT_OUT_TEXT_KEY)?.text);
351
+ const zAccount = await deriveShieldAccount(config.find, payment.address, zMintB58);
352
+ const acct = await reader.accountInfo(zAccount);
353
+ const readiness = shieldReadiness(acct, { owner: payment.address, zMint: zMintB58 }, optedOut);
354
+ return {
355
+ name: canonical,
356
+ handleAccount,
357
+ address: payment.address,
358
+ basis: payment.basis,
359
+ zAccount,
360
+ ataReady: readiness.kind === "ready",
361
+ readiness,
362
+ elgamalPubkey: readiness.kind === "ready" ? readiness.elgamalPubkey : null,
363
+ };
364
+ }
365
+ // ---------------------------------------------------------------------------
366
+ // bech32m (BIP-350) — the receipt / view-key string format
367
+ // ---------------------------------------------------------------------------
368
+ const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
369
+ const BECH32M_CONST = 0x2bc830a3;
370
+ function polymod(values) {
371
+ const G = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
372
+ let chk = 1;
373
+ for (const v of values) {
374
+ const top = chk >>> 25;
375
+ chk = ((chk & 0x1ffffff) << 5) ^ v;
376
+ for (let i = 0; i < 5; i++)
377
+ if ((top >>> i) & 1)
378
+ chk ^= G[i];
379
+ }
380
+ return chk >>> 0;
381
+ }
382
+ function hrpExpand(hrp) {
383
+ const out = [];
384
+ for (let i = 0; i < hrp.length; i++)
385
+ out.push(hrp.charCodeAt(i) >> 5);
386
+ out.push(0);
387
+ for (let i = 0; i < hrp.length; i++)
388
+ out.push(hrp.charCodeAt(i) & 31);
389
+ return out;
390
+ }
391
+ function convertBits(data, from, to, pad) {
392
+ let acc = 0;
393
+ let bits = 0;
394
+ const out = [];
395
+ const maxv = (1 << to) - 1;
396
+ for (let i = 0; i < data.length; i++) {
397
+ const v = data[i];
398
+ if (v < 0 || v >> from !== 0)
399
+ return null;
400
+ acc = (acc << from) | v;
401
+ bits += from;
402
+ while (bits >= to) {
403
+ bits -= to;
404
+ out.push((acc >> bits) & maxv);
405
+ }
406
+ }
407
+ if (pad) {
408
+ if (bits > 0)
409
+ out.push((acc << (to - bits)) & maxv);
410
+ }
411
+ else if (bits >= from || ((acc << (to - bits)) & maxv) !== 0) {
412
+ return null;
413
+ }
414
+ return out;
415
+ }
416
+ /** bech32m-encode `bytes` under `hrp` (no 90-char limit: receipts are long). */
417
+ export function bech32mEncode(hrp, bytes) {
418
+ const data = convertBits(bytes, 8, 5, true);
419
+ const values = [...hrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0];
420
+ const mod = polymod(values) ^ BECH32M_CONST;
421
+ const checksum = Array.from({ length: 6 }, (_, i) => (mod >>> (5 * (5 - i))) & 31);
422
+ return `${hrp}1${[...data, ...checksum].map((d) => CHARSET[d]).join("")}`;
423
+ }
424
+ /** Strict bech32m decode: right checksum constant (not BIP-173 bech32), no
425
+ * mixed case, the expected `hrp`. Null on any failure. */
426
+ export function bech32mDecode(s, hrp) {
427
+ if (s.length > 1023)
428
+ return null;
429
+ if (s !== s.toLowerCase() && s !== s.toUpperCase())
430
+ return null;
431
+ const lower = s.toLowerCase();
432
+ const sep = lower.lastIndexOf("1");
433
+ if (sep < 1 || sep + 7 > lower.length)
434
+ return null;
435
+ if (lower.slice(0, sep) !== hrp)
436
+ return null;
437
+ const data = [];
438
+ for (const c of lower.slice(sep + 1)) {
439
+ const v = CHARSET.indexOf(c);
440
+ if (v < 0)
441
+ return null;
442
+ data.push(v);
443
+ }
444
+ if (polymod([...hrpExpand(hrp), ...data]) !== BECH32M_CONST)
445
+ return null;
446
+ const bytes = convertBits(data.slice(0, -6), 5, 8, false);
447
+ return bytes ? Uint8Array.from(bytes) : null;
448
+ }
449
+ // ---------------------------------------------------------------------------
450
+ // Payment receipts (spec §5)
451
+ // ---------------------------------------------------------------------------
452
+ export const SHIELD_RECEIPT_HRP = "x1receipt";
453
+ const RECEIPT_VERSION = 1;
454
+ const RECEIPT_LEN = 1 + 64 + 32 + 8 + 32 + 32;
455
+ export function encodeShieldReceipt(r) {
456
+ const sig = decodeBase58(r.txSignature, 64);
457
+ if (!sig)
458
+ throw new Error("txSignature must be a 64-byte base58 signature");
459
+ if (r.openingLo.length !== 32 || r.openingHi.length !== 32)
460
+ throw new Error("openings must be 32 bytes each");
461
+ return bech32mEncode(SHIELD_RECEIPT_HRP, concatBytes(Uint8Array.of(RECEIPT_VERSION), sig, toBytes32(r.mint, "mint"), u64le(r.amount), r.openingLo, r.openingHi));
462
+ }
463
+ /** Decode a receipt string. Null when it isn't a valid v1 receipt. */
464
+ export function decodeShieldReceipt(s) {
465
+ const raw = bech32mDecode(s.trim(), SHIELD_RECEIPT_HRP);
466
+ if (!raw || raw.length !== RECEIPT_LEN || raw[0] !== RECEIPT_VERSION)
467
+ return null;
468
+ return {
469
+ txSignature: encodeBase58(raw.slice(1, 65)),
470
+ mint: encodeBase58(raw.slice(65, 97)),
471
+ amount: readU64LE(raw, 97),
472
+ openingLo: raw.slice(105, 137),
473
+ openingHi: raw.slice(137, 169),
474
+ };
475
+ }
476
+ /**
477
+ * Pull a receipt out of what someone pasted or scanned: the bare string,
478
+ * with whitespace or line breaks, or a `…/receipt?r=…` link. Returns the
479
+ * normalized lowercase receipt only when it decodes; null otherwise.
480
+ */
481
+ export function extractShieldReceipt(input) {
482
+ let s = input.trim();
483
+ if (!s)
484
+ return null;
485
+ if (/^https?:\/\//i.test(s)) {
486
+ try {
487
+ s = new URL(s).searchParams.get("r") ?? "";
488
+ }
489
+ catch {
490
+ return null;
491
+ }
492
+ }
493
+ s = s.replace(/\s+/g, "");
494
+ if (s !== s.toLowerCase() && s !== s.toUpperCase())
495
+ return null;
496
+ s = s.toLowerCase();
497
+ return decodeShieldReceipt(s) ? s : null;
498
+ }
499
+ // ---------------------------------------------------------------------------
500
+ // Payment lookup + receipt verification (S-2: proofs live in the staging txs)
501
+ // ---------------------------------------------------------------------------
502
+ /** `ProofType` of the two proofs that carry a transfer's amount commitments. */
503
+ export const PROOF_TYPE_BATCHED_GROUPED_3_HANDLES_VALIDITY = 12;
504
+ export const PROOF_TYPE_BATCHED_RANGE_U128 = 7;
505
+ async function getTx(rpc, sig) {
506
+ return (await rpc("getTransaction", [
507
+ sig,
508
+ { encoding: "json", commitment: "confirmed", maxSupportedTransactionVersion: 0 },
509
+ ]));
510
+ }
511
+ function ixsOf(tx) {
512
+ const keys = tx.transaction.message.accountKeys;
513
+ return tx.transaction.message.instructions.map((ix) => ({
514
+ program: keys[ix.programIdIndex],
515
+ accounts: ix.accounts.map((i) => keys[i]),
516
+ data: decodeBase58(ix.data) ?? new Uint8Array(0),
517
+ }));
518
+ }
519
+ /** Successful transactions that touched `account` at or before `slot`. */
520
+ async function txsTouching(rpc, account, slot, exclude) {
521
+ const sigs = ((await rpc("getSignaturesForAddress", [account, { limit: 25, commitment: "confirmed" }])) ?? []);
522
+ const out = [];
523
+ for (const s of sigs) {
524
+ if (s.err || s.slot > slot || s.signature === exclude)
525
+ continue;
526
+ const t = await getTx(rpc, s.signature);
527
+ if (t && !t.meta?.err)
528
+ out.push(t);
529
+ }
530
+ return out;
531
+ }
532
+ /** Token-2022 `TransferInstructionData` (no fee): [27, 7] ‖ new decryptable
533
+ * available 36 ‖ auditor ciphertext lo 64 ‖ hi 64 ‖ equality, validity,
534
+ * range proof instruction offsets (i8 each). */
535
+ const TRANSFER_DATA_LEN = 2 + 36 + 64 + 64 + 3;
536
+ const TRANSFER_COMMITMENT_LO = 2 + 36;
537
+ const TRANSFER_COMMITMENT_HI = 2 + 36 + 64;
538
+ const TRANSFER_OFFSETS = 2 + 36 + 64 + 64;
539
+ /** Offsets inside `BatchedGroupedCiphertext3HandlesValidityProofData`:
540
+ * context = first(source) pubkey 32 ‖ second(destination) pubkey 32 ‖
541
+ * third(auditor) pubkey 32 ‖ grouped ciphertext lo 128 ‖ hi 128 (each =
542
+ * commitment 32 ‖ handle×3), then the proof. */
543
+ const VALIDITY_CONTEXT_LEN = 96 + 128 + 128;
544
+ const VALIDITY_COMMITMENT_LO = 96;
545
+ const VALIDITY_COMMITMENT_HI = 96 + 128;
546
+ function bytesEqual(a, b) {
547
+ if (a.length !== b.length)
548
+ return false;
549
+ let d = 0;
550
+ for (let i = 0; i < a.length; i++)
551
+ d |= a[i] ^ b[i];
552
+ return d === 0;
553
+ }
554
+ /** Does this validity proof carry exactly the Transfer's amount commitments?
555
+ * The payer controls the context account and can verify any number of
556
+ * other proofs into it, so a proof found by address alone proves nothing
557
+ * about this payment (Sentinel FBL-0009). */
558
+ function provesThisTransfer(proof, lo, hi) {
559
+ return (proof.length >= VALIDITY_CONTEXT_LEN &&
560
+ bytesEqual(proof.subarray(VALIDITY_COMMITMENT_LO, VALIDITY_COMMITMENT_LO + 32), lo) &&
561
+ bytesEqual(proof.subarray(VALIDITY_COMMITMENT_HI, VALIDITY_COMMITMENT_HI + 32), hi));
562
+ }
563
+ const isInlineValidity = (ix) => ix.program === ZK_ELGAMAL_PROOF_PROGRAM_ID &&
564
+ ix.data[0] === PROOF_TYPE_BATCHED_GROUPED_3_HANDLES_VALIDITY &&
565
+ ix.data.length > 1 + VALIDITY_CONTEXT_LEN;
566
+ /**
567
+ * Find a confidential payment and the proof data its amount lives in. The
568
+ * Transfer's accounts are decoded from its proof-offset bytes (an inline
569
+ * proof in the same transaction, or a context-state account plus the
570
+ * Instructions sysvar). For a context account the proof bytes come from the
571
+ * staging transaction that verified the 3-handle validity proof into it
572
+ * (crates/shield-proofs README, "S-2"), which depends on the RPC node still
573
+ * holding that transaction. Either way a proof is used only when its
574
+ * commitments equal the ones in the Transfer instruction, which Token-2022
575
+ * checked against the proof it consumed. Null when `txSignature` isn't a
576
+ * successful Token-2022 confidential transfer.
577
+ */
578
+ export async function findShieldPayment(rpc, txSignature) {
579
+ const tx = await getTx(rpc, txSignature);
580
+ if (!tx || tx.meta?.err)
581
+ return null;
582
+ const ixs = ixsOf(tx);
583
+ const at = ixs.findIndex((ix) => ix.program === TOKEN_2022_PROGRAM_ID && ix.data[0] === 27 && ix.data[1] === 7);
584
+ const transfer = ixs[at];
585
+ if (!transfer || transfer.data.length !== TRANSFER_DATA_LEN)
586
+ return null;
587
+ const off = (i) => (transfer.data[TRANSFER_OFFSETS + i] << 24) >> 24; // i8
588
+ const [eqOff, valOff, rangeOff] = [off(0), off(1), off(2)];
589
+ // source, mint, destination, [Instructions sysvar if any proof is inline],
590
+ // then a context account for each proof that isn't inline, then authority
591
+ let i = 3;
592
+ if (eqOff !== 0 || valOff !== 0 || rangeOff !== 0)
593
+ i++;
594
+ if (eqOff === 0)
595
+ i++;
596
+ const ctxVal = valOff === 0 ? transfer.accounts[i++] : undefined;
597
+ if (rangeOff === 0)
598
+ i++;
599
+ const sender = transfer.accounts[i];
600
+ const [src, mint, dst] = transfer.accounts;
601
+ if (!src || !mint || !dst || !sender || (valOff === 0 && !ctxVal))
602
+ return null;
603
+ const lo = transfer.data.slice(TRANSFER_COMMITMENT_LO, TRANSFER_COMMITMENT_LO + 32);
604
+ const hi = transfer.data.slice(TRANSFER_COMMITMENT_HI, TRANSFER_COMMITMENT_HI + 32);
605
+ let validityProof = null;
606
+ if (valOff !== 0) {
607
+ const v = ixs[at + valOff];
608
+ if (v && isInlineValidity(v) && provesThisTransfer(v.data.subarray(1), lo, hi))
609
+ validityProof = v.data.slice(1);
610
+ }
611
+ else {
612
+ for (const t of await txsTouching(rpc, ctxVal, tx.slot, txSignature)) {
613
+ const v = ixsOf(t).find((ix) => isInlineValidity(ix) && ix.accounts[0] === ctxVal && provesThisTransfer(ix.data.subarray(1), lo, hi));
614
+ if (v) {
615
+ validityProof = v.data.slice(1);
616
+ break;
617
+ }
618
+ }
619
+ }
620
+ return {
621
+ txSignature,
622
+ slot: tx.slot,
623
+ blockTime: tx.blockTime,
624
+ zMint: mint,
625
+ sender,
626
+ sourceAccount: src,
627
+ destinationAccount: dst,
628
+ amountCommitmentLo: lo,
629
+ amountCommitmentHi: hi,
630
+ validityProof,
631
+ };
632
+ }
633
+ /**
634
+ * The amount ciphertexts of a payment as seen by one party (`"source"` or
635
+ * `"destination"`): 64-byte ElGamal ciphertexts `commitment ‖ that party's
636
+ * handle` for the amount's lo (16-bit) and hi (32-bit) parts. Decrypt with
637
+ * the party's keys as `lo + hi·2^16` (shield-proofs `decryptPending(lo, hi)`
638
+ * does exactly that). Also returns the party's ElGamal key from the proof so
639
+ * a caller can check it is theirs. Null on malformed data.
640
+ */
641
+ export function paymentAmountCiphertexts(validityProof, party) {
642
+ if (validityProof.length < VALIDITY_CONTEXT_LEN)
643
+ return null;
644
+ const h = party === "source" ? 0 : 1;
645
+ const grouped = (start) => concatBytes(validityProof.slice(start, start + 32), validityProof.slice(start + 32 + 32 * h, start + 64 + 32 * h));
646
+ return {
647
+ lo: grouped(96),
648
+ hi: grouped(96 + 128),
649
+ elgamalPubkey: validityProof.slice(32 * h, 32 * h + 32),
650
+ };
651
+ }
652
+ /**
653
+ * Verify a receipt against the chain: decode it, find the payment
654
+ * ({@link findShieldPayment}), check the z-mint, and ask `checkCommitments`
655
+ * (the shield-proofs WASM's `verifyReceiptAgainstProof`) whether the
656
+ * receipt's amount and openings reproduce the commitments in the payment's
657
+ * own validity proof: the one whose commitments equal the Transfer
658
+ * instruction's (Sentinel FBL-0009). Anyone can run it; it needs no keys.
659
+ */
660
+ export async function verifyShieldReceipt(receipt, opts) {
661
+ const r = decodeShieldReceipt(receipt);
662
+ if (!r)
663
+ return { ok: false, reason: "malformed" };
664
+ const tx = await getTx(opts.rpc, r.txSignature);
665
+ if (!tx || tx.meta?.err)
666
+ return { ok: false, reason: "not-found" };
667
+ const p = await findShieldPayment(opts.rpc, r.txSignature);
668
+ if (!p)
669
+ return { ok: false, reason: "not-a-payment" };
670
+ if (p.zMint !== r.mint)
671
+ return { ok: false, reason: "wrong-token" };
672
+ if (!p.validityProof || !provesThisTransfer(p.validityProof, p.amountCommitmentLo, p.amountCommitmentHi)) {
673
+ return { ok: false, reason: "no-proof-history" };
674
+ }
675
+ let ok = false;
676
+ try {
677
+ ok = await opts.checkCommitments(receipt.trim().toLowerCase(), PROOF_TYPE_BATCHED_GROUPED_3_HANDLES_VALIDITY, p.validityProof);
678
+ }
679
+ catch {
680
+ ok = false;
681
+ }
682
+ if (!ok)
683
+ return { ok: false, reason: "mismatch" };
684
+ return {
685
+ ok: true,
686
+ amount: r.amount,
687
+ zMint: p.zMint,
688
+ txSignature: p.txSignature,
689
+ blockTime: p.blockTime,
690
+ sender: p.sender,
691
+ recipientAccount: p.destinationAccount,
692
+ };
693
+ }
694
+ /** Re-exported for callers that want to read account bytes the same way. */
695
+ export { b64ToBytes as shieldBase64ToBytes };