@subly_fi/pay 0.7.3 → 0.8.1

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/dist/pay.js CHANGED
@@ -236,1266 +236,1493 @@ async function ensureWalletOnboarded(params) {
236
236
  await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain", vault });
237
237
  }
238
238
 
239
- // ../../src/lib/associated-token-account.ts
239
+ // ../../src/lib/canonical-json.ts
240
+ import { createHash as createHash3 } from "node:crypto";
241
+
242
+ // ../../src/lib/hash.ts
240
243
  import { createHash as createHash2 } from "node:crypto";
241
- import bs582 from "bs58";
242
- var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
243
- var ED25519_P = (1n << 255n) - 19n;
244
- var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
245
- function deriveAssociatedTokenAddress(params) {
246
- const owner = decodePublicKey(params.owner, "owner");
247
- const mint = decodePublicKey(params.mint ?? SUBLY_VAULT.usdcMint, "mint");
248
- const tokenProgramId = decodePublicKey(
249
- params.tokenProgramId ?? SPL_TOKEN_PROGRAM_ID,
250
- "tokenProgramId"
251
- );
252
- const associatedTokenProgramId = decodePublicKey(
253
- ASSOCIATED_TOKEN_PROGRAM_ID,
254
- "associatedTokenProgramId"
255
- );
256
- for (let bump = 255; bump >= 0; bump -= 1) {
257
- const address3 = createProgramAddress(
258
- [owner, tokenProgramId, mint, Uint8Array.of(bump)],
259
- associatedTokenProgramId
260
- );
261
- if (address3 !== null) {
262
- return bs582.encode(address3);
263
- }
264
- }
265
- throw new Error("Unable to derive associated token account address");
266
- }
267
- function createProgramAddress(seeds, programId) {
268
- const hash = createHash2("sha256");
269
- for (const seed of seeds) {
270
- hash.update(seed);
271
- }
272
- hash.update(programId);
273
- hash.update(PDA_MARKER);
274
- const digest = hash.digest();
275
- return isEd25519Point(digest) ? null : new Uint8Array(digest);
244
+ var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
245
+ function sha256TaggedHex(data) {
246
+ return `sha256-${createHash2("sha256").update(data).digest("hex")}`;
276
247
  }
277
- function decodePublicKey(value, fieldName) {
278
- const decoded = bs582.decode(value);
279
- if (decoded.length !== 32) {
280
- throw new Error(`${fieldName} must be a 32-byte public key`);
248
+ function stableStringify(value) {
249
+ if (value === null) {
250
+ return "null";
281
251
  }
282
- return decoded;
283
- }
284
- function isEd25519Point(bytes) {
285
- if (bytes.length !== 32) {
286
- return false;
252
+ if (typeof value === "bigint") {
253
+ return JSON.stringify(value.toString());
287
254
  }
288
- const yBytes = Uint8Array.from(bytes);
289
- yBytes[31] = yBytes[31] & 127;
290
- const y = littleEndianToBigInt(yBytes);
291
- if (y >= ED25519_P) {
292
- return false;
255
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
256
+ return JSON.stringify(value);
293
257
  }
294
- const ySquared = mod(y * y, ED25519_P);
295
- const numerator = mod(ySquared - 1n, ED25519_P);
296
- const denominator = mod(ED25519_D * ySquared + 1n, ED25519_P);
297
- if (denominator === 0n) {
298
- return false;
258
+ if (Array.isArray(value)) {
259
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
299
260
  }
300
- const xSquared = mod(
301
- numerator * modPow(denominator, ED25519_P - 2n, ED25519_P),
302
- ED25519_P
303
- );
304
- return xSquared === 0n || modPow(xSquared, (ED25519_P - 1n) / 2n, ED25519_P) === 1n;
261
+ const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
262
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
305
263
  }
306
- function littleEndianToBigInt(bytes) {
307
- let value = 0n;
308
- for (let index = bytes.length - 1; index >= 0; index -= 1) {
309
- value = (value << 8n) + BigInt(bytes[index]);
310
- }
311
- return value;
264
+ function hashStableJson(value) {
265
+ return sha256TaggedHex(stableStringify(value));
312
266
  }
313
- function mod(value, modulus) {
314
- const result = value % modulus;
315
- return result >= 0n ? result : result + modulus;
267
+
268
+ // ../../src/lib/canonical-json.ts
269
+ function canonicalJson(value) {
270
+ return stableStringify(value);
316
271
  }
317
- function modPow(base, exponent, modulus) {
318
- let result = 1n;
319
- let nextBase = mod(base, modulus);
320
- let nextExponent = exponent;
321
- while (nextExponent > 0n) {
322
- if ((nextExponent & 1n) === 1n) {
323
- result = mod(result * nextBase, modulus);
324
- }
325
- nextBase = mod(nextBase * nextBase, modulus);
326
- nextExponent >>= 1n;
327
- }
328
- return result;
272
+ function sha256HexOf(data) {
273
+ return createHash3("sha256").update(data, "utf8").digest("hex");
274
+ }
275
+ function canonicalJsonHash(value) {
276
+ return sha256HexOf(canonicalJson(value));
329
277
  }
330
278
 
331
- // ../../src/domain/withdrawal-rounding.ts
332
- var WITHDRAWAL_ROUNDING_RAW_USDC = 10n;
333
- var YIELD_REALIZE_ROUNDING_RAW_USDC = WITHDRAWAL_ROUNDING_RAW_USDC / 2n;
334
-
335
- // ../../src/client/withdrawal-preview.ts
336
- async function assertWithdrawalPreview(input) {
337
- const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
338
- const simulation = await input.rpc.simulateTransaction(
339
- input.serializedTransaction,
340
- {
341
- encoding: "base64",
342
- commitment: "confirmed",
343
- sigVerify: false,
344
- replaceRecentBlockhash: false,
345
- innerInstructions: true
346
- }
347
- ).send({ abortSignal: AbortSignal.timeout(15e3) });
348
- if (simulation.value.err !== null) {
349
- throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
350
- }
351
- let received = 0n;
352
- for (const group of simulation.value.innerInstructions ?? []) {
353
- for (const instruction of group.instructions) {
354
- if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
355
- const parsed = instruction.parsed;
356
- if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
357
- const info = parsed.info;
358
- if (!info || info.destination !== destination && info.source !== destination) continue;
359
- const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
360
- if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
361
- throw new Error("Withdrawal preview returned an invalid token amount");
362
- }
363
- const amount = BigInt(raw);
364
- if (info.destination === destination) received += amount;
365
- if (info.source === destination) received -= amount;
366
- }
279
+ // ../../src/x402/headers.ts
280
+ import { z as z2 } from "zod";
281
+ var PAYMENT_REQUIRED_HEADER = "payment-required";
282
+ var PAYMENT_RESPONSE_HEADER = "payment-response";
283
+ var MAX_HEADER_JSON_BYTES = 16384;
284
+ var X402HeaderError = class extends Error {
285
+ reason;
286
+ constructor(reason, message) {
287
+ super(message);
288
+ this.name = "X402HeaderError";
289
+ this.reason = reason;
367
290
  }
368
- const minimum = input.purpose === "yield_realize" ? input.amountRawUsdc : input.amountRawUsdc - WITHDRAWAL_ROUNDING_RAW_USDC;
369
- if (received <= 0n || received > input.amountRawUsdc + WITHDRAWAL_ROUNDING_RAW_USDC || received < minimum) {
370
- throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
291
+ };
292
+ var sublyPaymentRequirementsSchema = z2.object({
293
+ scheme: z2.literal(PAYMENT_SCHEME),
294
+ network: z2.string().min(1),
295
+ asset: z2.string().min(32),
296
+ /** Exact seller amount in raw USDC; the scheme settles exactly this. */
297
+ amountRawUsdc: z2.string().regex(/^[1-9]\d*$/),
298
+ resource: z2.string().url(),
299
+ description: z2.string().optional(),
300
+ mimeType: z2.string().optional(),
301
+ payTo: z2.string().min(32),
302
+ maxTimeoutSeconds: z2.number().int().positive(),
303
+ extra: z2.object({
304
+ sellerRequestId: z2.string().min(1),
305
+ seller: z2.string().min(32),
306
+ sellerUsdcAta: z2.string().min(32),
307
+ vault: z2.string().min(32),
308
+ shareMint: z2.string().min(32)
309
+ })
310
+ }).loose();
311
+ var paymentRequiredSchema = z2.object({
312
+ x402Version: z2.number().int(),
313
+ accepts: z2.array(z2.unknown()),
314
+ error: z2.string().optional()
315
+ }).loose();
316
+ var sublyPaymentPayloadSchema = z2.object({
317
+ x402Version: z2.number().int(),
318
+ scheme: z2.literal(PAYMENT_SCHEME),
319
+ network: z2.string().min(1),
320
+ payload: z2.object({
321
+ paymentId: z2.string().min(1),
322
+ requestBindingHash: z2.string().min(1),
323
+ preparedMessageHash: z2.string().min(1),
324
+ serializedTransaction: z2.string().min(1).max(4096),
325
+ agentSignature: z2.string().min(1).max(128),
326
+ temporarySettlementSignature: z2.string().min(1).max(128)
327
+ })
328
+ }).loose();
329
+ function decodeX402Header(headerValue) {
330
+ if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
331
+ throw new X402HeaderError(
332
+ "header_too_large",
333
+ `x402 header exceeds ${MAX_HEADER_JSON_BYTES} encoded bytes`
334
+ );
371
335
  }
372
- }
373
-
374
- // ../../src/client/lookup-tables.ts
375
- import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
376
- import { address, getCompiledTransactionMessageDecoder } from "@solana/kit";
377
- function lookupTableAddressesForTransaction(serializedTransaction) {
378
- const wire = Buffer.from(serializedTransaction, "base64");
379
- let offset = 0;
380
- let signatureCount = 0;
381
- let shift = 0;
382
- while (offset < wire.length) {
383
- const byte = wire[offset];
384
- signatureCount |= (byte & 127) << shift;
385
- offset += 1;
386
- if ((byte & 128) === 0) {
387
- break;
388
- }
389
- shift += 7;
336
+ const json = Buffer.from(headerValue, "base64").toString("utf8");
337
+ try {
338
+ return JSON.parse(json);
339
+ } catch {
340
+ throw new X402HeaderError(
341
+ "invalid_header_encoding",
342
+ "x402 header is not base64-encoded JSON"
343
+ );
390
344
  }
391
- const messageBytes = wire.subarray(offset + signatureCount * 64);
392
- const compiled = getCompiledTransactionMessageDecoder().decode(messageBytes);
393
- const lookups = compiled.addressTableLookups ?? [];
394
- return lookups.map((lookup) => String(lookup.lookupTableAddress));
395
345
  }
396
- async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
397
- const addresses = lookupTableAddressesForTransaction(serializedTransaction);
398
- if (addresses.length === 0) {
399
- return {};
346
+ function requestBodyHashFor(body2) {
347
+ if (body2 === null || body2 === void 0 || body2.length === 0) {
348
+ return EMPTY_BODY_HASH;
400
349
  }
401
- const tables = await fetchAllMaybeAddressLookupTable(
402
- rpc2,
403
- addresses.map((value) => address(value))
350
+ return sha256TaggedHex(
351
+ typeof body2 === "string" ? Buffer.from(body2, "utf8") : Buffer.from(body2)
404
352
  );
405
- const result = {};
406
- for (const table of tables) {
407
- if (table.exists) {
408
- result[table.address] = table.data.addresses.map(String);
409
- }
410
- }
411
- return result;
412
353
  }
413
354
 
414
- // ../../src/client/vault-flows.ts
415
- var VaultFlowClientError = class extends Error {
416
- constructor(step, message, detail = null, code = null, errorDetails = null) {
417
- super(message);
418
- this.step = step;
419
- this.detail = detail;
420
- this.code = code;
421
- this.errorDetails = errorDetails;
422
- this.name = "VaultFlowClientError";
355
+ // ../../src/x402/standard-requirements.ts
356
+ import { z as z3 } from "zod";
357
+ var STANDARD_EXACT_SCHEME = "exact";
358
+ var standardExactRequirementSchema = z3.object({
359
+ scheme: z3.literal(STANDARD_EXACT_SCHEME),
360
+ /** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
361
+ network: z3.string().min(1),
362
+ /** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
363
+ asset: z3.string().min(1),
364
+ /** Exact price in the asset's atomic units, as a decimal string. */
365
+ amount: z3.string().regex(/^[1-9]\d*$/),
366
+ /** Recipient wallet; the transfer destination ATA is derived from it. */
367
+ payTo: z3.string().min(1),
368
+ maxTimeoutSeconds: z3.number().int().positive().optional(),
369
+ extra: z3.object({
370
+ /** Facilitator address that pays the tx fee (gas sponsorship). */
371
+ feePayer: z3.string().min(1).optional()
372
+ }).loose().optional()
373
+ }).loose();
374
+ var standardPaymentRequiredSchema = z3.object({
375
+ x402Version: z3.number().int(),
376
+ accepts: z3.array(z3.unknown()),
377
+ error: z3.string().optional(),
378
+ resource: z3.object({ url: z3.string().optional() }).loose().optional()
379
+ }).loose();
380
+ var StandardX402ChallengeError = class extends Error {
381
+ reason;
382
+ constructor(reason, message) {
383
+ super(message);
384
+ this.name = "StandardX402ChallengeError";
385
+ this.reason = reason;
423
386
  }
424
- step;
425
- detail;
426
- code;
427
- errorDetails;
428
387
  };
429
- var VaultFlowClient = class {
430
- vault;
431
- rpc;
432
- baseUrl;
433
- signer;
434
- fetchImpl;
435
- lookupTablesFor;
436
- pollTimeoutMs;
437
- pollIntervalMs;
438
- constructor(config) {
439
- this.rpc = config.rpc;
440
- this.vault = config.vault ?? config.signer.vault ?? SUBLY_VAULT;
441
- if (config.signer.vault && config.signer.vault.address !== this.vault.address) {
442
- throw new Error("Vault flow client and signer must select the same vault");
443
- }
444
- this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
445
- this.signer = config.signer;
446
- this.fetchImpl = config.fetchImpl ?? fetch;
447
- this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
448
- this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
449
- this.pollIntervalMs = config.pollIntervalMs ?? 2500;
388
+ function parseStandardChallenge(challenge) {
389
+ const parsed = standardPaymentRequiredSchema.safeParse(challenge);
390
+ if (!parsed.success) {
391
+ throw new StandardX402ChallengeError(
392
+ "invalid_payment_required",
393
+ "Response is not a valid x402 PaymentRequired object"
394
+ );
450
395
  }
451
- /**
452
- * Moves USDC from the agent wallet into the vault (fee sponsored). Under
453
- * depositPolicy "owner_approval_required" the relayer refuses to prepare
454
- * without an owner approval; when the caller passes none, an already
455
- * APPROVED deposit approval for this exact amount (e.g. the mandate's
456
- * initialDeposit — "one Face ID covers mandate + first deposit") is looked
457
- * up and used automatically before surfacing deposit_approval_required.
458
- */
459
- async deposit(input) {
460
- let approvalId2 = input.approvalId;
461
- let prepared;
462
- try {
463
- prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
464
- wallet: this.signer.walletAddress,
465
- vault: this.vault.address,
466
- amountRawUsdc: input.amountRawUsdc.toString(),
467
- ...approvalId2 === void 0 ? {} : { approvalId: approvalId2 }
468
- });
469
- } catch (error) {
470
- if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId2 !== void 0) {
471
- throw error;
472
- }
473
- approvalId2 = await this.findApprovedDepositApproval(input.amountRawUsdc);
474
- if (approvalId2 === void 0) {
475
- throw error;
476
- }
477
- prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
478
- wallet: this.signer.walletAddress,
479
- vault: this.vault.address,
480
- amountRawUsdc: input.amountRawUsdc.toString(),
481
- approvalId: approvalId2
482
- });
483
- }
484
- if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.signingIntent.amountRawUsdc !== input.amountRawUsdc.toString()) {
485
- throw new VaultFlowClientError("prepare", "Prepared deposit differs from the requested wallet, vault or amount");
486
- }
487
- const signed = await this.signer.signDeposit({
488
- intent: prepared.signingIntent,
489
- serializedTransaction: prepared.serializedTransaction,
490
- lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
491
- });
492
- let outcome = await this.postJson("submit", "/v1/deposits/submit", {
493
- depositId: prepared.depositId,
494
- serializedTransaction: signed.serializedTransaction,
495
- agentSignature: signed.agentSignature
496
- });
497
- if (outcome.status === "submitted") {
498
- outcome = await this.pollUntilTerminal(
499
- `/v1/deposits/${prepared.depositId}`,
500
- outcome
501
- );
396
+ const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
397
+ const requirement = standardExactRequirementSchema.safeParse(candidate);
398
+ if (!requirement.success) {
399
+ return [];
502
400
  }
503
- return {
504
- depositId: prepared.depositId,
505
- status: outcome.status,
506
- txSignature: outcome.txSignature ?? null,
507
- actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
508
- sharesMintedRaw: outcome.sharesMintedRaw ?? null,
509
- errorCode: outcome.errorCode ?? null
510
- };
511
- }
512
- /**
513
- * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
514
- * sponsored). A plain withdrawal is the exit path and MAY spend principal;
515
- * with purpose "yield_realize" the relayer refuses anything beyond the
516
- * spendable yield (the payment path, via RelayerYieldRealizer).
517
- */
518
- async withdraw(input) {
519
- const prepared = await this.postJson(
520
- "prepare",
521
- "/v1/withdrawals/prepare",
522
- {
523
- wallet: this.signer.walletAddress,
524
- vault: this.vault.address,
525
- amountRawUsdc: input.amountRawUsdc.toString(),
526
- ...input.purpose === void 0 ? {} : { purpose: input.purpose },
527
- ...input.payment === void 0 ? {} : { payment: input.payment },
528
- ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
529
- }
401
+ return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
402
+ });
403
+ return { paymentRequired: parsed.data, solanaExactRequirements };
404
+ }
405
+ function decodeStandardPaymentRequiredHeader(headerValue) {
406
+ let decoded;
407
+ try {
408
+ decoded = decodeX402Header(headerValue);
409
+ } catch (error) {
410
+ throw new StandardX402ChallengeError(
411
+ error instanceof X402HeaderError ? error.reason : "invalid_header",
412
+ "Cannot decode the payment-required header"
530
413
  );
531
- if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.requestedWithdrawRawUsdc !== input.amountRawUsdc.toString() || prepared.purpose !== (input.purpose ?? "normal") || input.purpose === "yield_realize" && prepared.signingIntent.allowFullExit) {
532
- throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
533
- }
534
- await assertWithdrawalPreview({
535
- rpc: this.rpc,
536
- serializedTransaction: prepared.serializedTransaction,
537
- wallet: this.signer.walletAddress,
538
- vault: this.vault,
539
- amountRawUsdc: input.amountRawUsdc,
540
- ...input.purpose === void 0 ? {} : { purpose: input.purpose }
541
- });
542
- const signed = await this.signer.signWithdrawal({
543
- intent: prepared.signingIntent,
544
- serializedTransaction: prepared.serializedTransaction,
545
- lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
546
- });
547
- let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
548
- withdrawalId: prepared.withdrawalId,
549
- serializedTransaction: signed.serializedTransaction,
550
- agentSignature: signed.agentSignature
551
- });
552
- if (outcome.status === "submitted") {
553
- outcome = await this.pollUntilTerminal(
554
- `/v1/withdrawals/${prepared.withdrawalId}`,
555
- outcome
556
- );
557
- }
558
- return {
559
- withdrawalId: prepared.withdrawalId,
560
- status: outcome.status,
561
- txSignature: outcome.txSignature ?? null,
562
- destinationUsdcAta: prepared.destinationUsdcAta,
563
- actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
564
- actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
565
- errorCode: outcome.errorCode ?? null
566
- };
567
414
  }
568
- /**
569
- * Reads the yield budget. Syncs the relayer's ledger from chain first (so
570
- * yield accrued since the last sync shows up); the sync is best-effort and
571
- * on failure the last-synced view is returned.
572
- */
573
- async getBudget(options = {}) {
574
- if (options.refreshFromChain !== false) {
575
- try {
576
- await this.postJson(
577
- "sync",
578
- `/v1/wallets/${this.signer.walletAddress}/sync`,
579
- { source: "chain", vault: this.vault.address }
580
- );
581
- } catch {
582
- }
583
- }
584
- const url2 = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
585
- const response = await this.fetchImpl(url2, {
586
- headers: await walletAuthHeaders({
587
- signer: this.signer,
588
- method: "GET",
589
- url: url2
590
- })
591
- });
592
- const text = await response.text();
593
- if (response.status !== 200) {
594
- throw new VaultFlowClientError(
595
- "budget",
596
- `budget endpoint returned ${response.status}: ${text}`
597
- );
598
- }
599
- let parsed;
600
- try {
601
- parsed = JSON.parse(text);
602
- } catch {
603
- throw new VaultFlowClientError(
604
- "budget",
605
- "budget endpoint returned 200 with a non-JSON body",
606
- text
607
- );
608
- }
609
- const body2 = parsed;
610
- if (body2.position?.vault !== void 0 && body2.position.vault !== this.vault.address) {
611
- throw new VaultFlowClientError("budget", "Relayer returned the budget for a different vault");
612
- }
613
- return {
614
- wallet: this.signer.walletAddress,
615
- vault: this.vault.address,
616
- principalBasisRawUsdc: body2.position?.principalBasisRawUsdc ?? "0",
617
- positionValueRawUsdc: body2.budget?.positionValueRawUsdc ?? "0",
618
- grossYieldRawUsdc: body2.budget?.grossYieldRawUsdc ?? "0",
619
- spendableYieldRawUsdc: body2.budget?.spendableYieldRawUsdc ?? "0"
620
- };
415
+ return parseStandardChallenge(decoded);
416
+ }
417
+ function selectPayableSolanaRequirement(requirements, options) {
418
+ const network = options?.network ?? SOLANA_MAINNET_NETWORK;
419
+ const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
420
+ const matchingRequirements = requirements.filter(
421
+ (candidate) => candidate.network === network && candidate.asset === usdcMint
422
+ );
423
+ if (matchingRequirements.length === 0) {
424
+ throw new StandardX402ChallengeError(
425
+ "no_payable_requirement",
426
+ `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
427
+ );
621
428
  }
622
- /** Best-effort audit link: reports the x402 payment tx a realize funded. */
623
- async reportPayment(input) {
624
- await this.postJson("submit", "/v1/payments/report", {
625
- wallet: this.signer.walletAddress,
626
- withdrawalId: input.withdrawalId,
627
- paymentTxSignature: input.paymentTxSignature
628
- });
429
+ const requirement = matchingRequirements.find(
430
+ (candidate) => candidate.extra?.feePayer !== void 0
431
+ ) ?? null;
432
+ if (requirement === null) {
433
+ throw new StandardX402ChallengeError(
434
+ "missing_svm_fee_payer",
435
+ "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
436
+ );
629
437
  }
630
- /** Wallet's approvals as the relayer sees them (optionally by status). */
631
- async listApprovals(status) {
632
- const body2 = await this.getJson(
633
- `/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
438
+ const feePayer = requirement.extra?.feePayer;
439
+ if (feePayer === void 0) {
440
+ throw new StandardX402ChallengeError(
441
+ "missing_svm_fee_payer",
442
+ "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
634
443
  );
635
- return body2.approvals ?? [];
636
444
  }
637
- /**
638
- * Creates the owner-onboarding setup link (wallet-auth pins the agreed
639
- * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
640
- */
641
- async createSetupSession(input) {
642
- const session = await this.postJson(
643
- "prepare",
644
- `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
645
- {
646
- vault: this.vault.address,
647
- ...input.policy === void 0 ? {} : { policy: input.policy },
648
- ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
649
- ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
650
- ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
651
- }
445
+ return {
446
+ requirement,
447
+ amountRawUsdc: BigInt(requirement.amount),
448
+ payTo: requirement.payTo,
449
+ feePayer
450
+ };
451
+ }
452
+ function standardRequirementMatchesSelected(candidate, selected) {
453
+ const parsed = standardExactRequirementSchema.safeParse(candidate);
454
+ return parsed.success && stableJson(parsed.data) === stableJson(selected.requirement);
455
+ }
456
+ function stableJson(value) {
457
+ return JSON.stringify(sortJson(value));
458
+ }
459
+ function sortJson(value) {
460
+ if (Array.isArray(value)) {
461
+ return value.map(sortJson);
462
+ }
463
+ if (value !== null && typeof value === "object") {
464
+ return Object.fromEntries(
465
+ Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
652
466
  );
653
- if (session.vault !== this.vault.address || session.wallet !== this.signer.walletAddress) {
654
- throw new VaultFlowClientError("prepare", "Relayer returned a setup session for a different wallet or vault");
655
- }
656
- return session;
657
467
  }
658
- /** Polls a setup session (public capability URL — no auth needed). */
659
- async getSetupSession(sessionId) {
660
- const url2 = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
661
- const response = await this.fetchImpl(url2);
662
- const text = await response.text();
663
- if (response.status !== 200) {
664
- const parsed = parseRelayerError(text);
665
- throw new VaultFlowClientError(
666
- "read",
667
- parsed.message ?? `setup session read failed with ${response.status}`,
668
- text,
669
- parsed.code,
670
- parsed.details
671
- );
672
- }
673
- return JSON.parse(text);
468
+ return value;
469
+ }
470
+
471
+ // ../../src/client/standard-x402-payer.ts
472
+ var StandardX402PayError = class extends Error {
473
+ constructor(reason, message, detail = null) {
474
+ super(message);
475
+ this.reason = reason;
476
+ this.detail = detail;
477
+ this.name = "StandardX402PayError";
674
478
  }
675
- /**
676
- * Finds an APPROVED, unconsumed deposit approval bound to exactly this
677
- * amount — the shape the mandate's initialDeposit approval has.
678
- */
679
- async findApprovedDepositApproval(amountRawUsdc) {
680
- try {
681
- const approvals = await this.listApprovals("approved");
682
- const match = approvals.find((approval) => {
683
- const binding = approval.binding;
684
- return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
685
- });
686
- return match?.approvalId;
687
- } catch {
688
- return void 0;
479
+ reason;
480
+ detail;
481
+ };
482
+ var StandardX402Payer = class {
483
+ realizer;
484
+ x402Fetch;
485
+ probeFetch;
486
+ defaultMaxAmountRawUsdc;
487
+ network;
488
+ usdcMint;
489
+ stateStore;
490
+ pending = /* @__PURE__ */ new Map();
491
+ inFlight = /* @__PURE__ */ new Map();
492
+ nowMs;
493
+ constructor(config) {
494
+ this.realizer = config.realizer;
495
+ this.x402Fetch = config.x402Fetch;
496
+ this.probeFetch = config.probeFetch ?? fetch;
497
+ this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
498
+ this.network = config.network ?? SOLANA_MAINNET_NETWORK;
499
+ this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
500
+ this.stateStore = config.stateStore ?? null;
501
+ this.nowMs = config.nowMs ?? (() => Date.now());
502
+ if (this.stateStore !== null) {
503
+ for (const record of this.stateStore.load()) {
504
+ this.pending.set(record.key, record);
505
+ }
689
506
  }
690
507
  }
691
- /**
692
- * Polls the reconciling GET endpoint until the intent leaves "submitted"
693
- * (each read looks the tx up on-chain) or the timeout elapses.
694
- */
695
- async pollUntilTerminal(path, last) {
696
- const deadline = Date.now() + this.pollTimeoutMs;
697
- let latest = last;
698
- while (Date.now() < deadline) {
699
- await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
700
- const url2 = `${this.baseUrl}${path}`;
701
- const response = await this.fetchImpl(url2, {
702
- headers: await walletAuthHeaders({
703
- signer: this.signer,
704
- method: "GET",
705
- url: url2
706
- })
707
- });
708
- if (response.status !== 200) {
709
- continue;
508
+ pay(input, realizer = this.realizer) {
509
+ const method2 = (input.method ?? "GET").toUpperCase();
510
+ const requestBodyHash = requestBodyHashFor(input.body ?? null);
511
+ const pendingKey = pendingPaymentKey({
512
+ url: input.url,
513
+ method: method2,
514
+ requestBodyHash
515
+ });
516
+ const existingFlow = this.inFlight.get(pendingKey);
517
+ if (existingFlow !== void 0) {
518
+ return existingFlow;
519
+ }
520
+ const run = async () => {
521
+ if (this.stateStore?.withExclusiveLock) {
522
+ this.pending.clear();
523
+ for (const record of this.stateStore.load()) this.pending.set(record.key, record);
524
+ }
525
+ return this.run(input, { method: method2, requestBodyHash, pendingKey }, realizer);
526
+ };
527
+ const flow = (this.stateStore?.withExclusiveLock ? this.stateStore.withExclusiveLock(run) : run()).finally(() => {
528
+ this.inFlight.delete(pendingKey);
529
+ });
530
+ this.inFlight.set(pendingKey, flow);
531
+ return flow;
532
+ }
533
+ async run(input, computed, realizer) {
534
+ const { method: method2, requestBodyHash, pendingKey } = computed;
535
+ const existingPending = this.pending.get(pendingKey);
536
+ const resuming = existingPending?.recovery !== void 0 && existingPending.status !== "external_outcome_unknown";
537
+ const requestHeadersHash = canonicalJsonHash(input.headers ?? {});
538
+ if (resuming) {
539
+ const recovery = existingPending.recovery;
540
+ if (canonicalJsonHash(recovery.context) !== canonicalJsonHash(realizer.realizationContext ?? null) || recovery.requestHeadersHash !== requestHeadersHash || existingPending.url !== input.url || existingPending.method !== method2 || existingPending.requestBodyHash !== requestBodyHash) {
541
+ throw new StandardX402PayError(
542
+ "payment_outcome_unknown",
543
+ "The pending realization belongs to a different wallet, vault, relayer or request; refusing to resume or discard it.",
544
+ publicPendingPayment(existingPending)
545
+ );
546
+ }
547
+ if (existingPending.status === "realizing" && recovery.prepared === void 0) {
548
+ throw new StandardX402PayError(
549
+ "payment_outcome_unknown",
550
+ "Realization was interrupted before its withdrawal ID was saved. Reconcile the original operation before retrying; forceNewPayment cannot discard an incomplete realization.",
551
+ publicPendingPayment(existingPending)
552
+ );
553
+ }
554
+ } else if (existingPending !== void 0) {
555
+ if (input.forceNewPayment !== true || existingPending.status === "realizing") {
556
+ throw new StandardX402PayError(
557
+ "payment_outcome_unknown",
558
+ "A previous payment or realization has an unknown outcome. Verify it before purchasing again.",
559
+ publicPendingPayment(existingPending)
560
+ );
710
561
  }
711
562
  try {
712
- latest = await response.json();
713
- } catch {
714
- continue;
563
+ this.untrack(pendingKey);
564
+ } catch (error) {
565
+ throw new StandardX402PayError(
566
+ "state_persist_failed",
567
+ "Could not clear the previous pending x402 marker before forcing a new payment",
568
+ error
569
+ );
715
570
  }
716
- if (latest.status !== "submitted") {
717
- return latest;
571
+ }
572
+ const init = {
573
+ method: method2,
574
+ ...input.body === void 0 ? {} : { body: input.body },
575
+ ...input.headers === void 0 ? {} : { headers: input.headers }
576
+ };
577
+ const probe = await this.probeFetch(input.url, init);
578
+ if (probe.status !== 402) {
579
+ if (resuming) {
580
+ throw new StandardX402PayError(
581
+ "payment_outcome_unknown",
582
+ "The seller no longer offers the original payment challenge; the saved realization is retained for reconciliation.",
583
+ publicPendingPayment(existingPending)
584
+ );
718
585
  }
586
+ return { paid: false, status: probe.status, body: await probe.text() };
719
587
  }
720
- return latest;
721
- }
722
- async postJson(step, path, body2) {
723
- const url2 = `${this.baseUrl}${path}`;
724
- const serialized = JSON.stringify(body2);
725
- const response = await this.fetchImpl(url2, {
726
- method: "POST",
727
- headers: {
728
- ...await walletAuthHeaders({
729
- signer: this.signer,
730
- method: "POST",
731
- url: url2,
732
- body: serialized
733
- }),
734
- "content-type": "application/json"
735
- },
736
- body: serialized
737
- });
738
- const text = await response.text();
739
- if (response.status !== 200) {
740
- const parsed = parseRelayerError(text);
741
- throw new VaultFlowClientError(
742
- step,
743
- parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
744
- text,
745
- parsed.code,
746
- parsed.details
588
+ const selected = await this.selectRequirement(probe);
589
+ if (resuming && (!standardRequirementMatchesSelected(existingPending.recovery.requirement, selected) || existingPending.amountRawUsdc !== selected.amountRawUsdc.toString() || existingPending.payTo !== selected.payTo || existingPending.feePayer !== selected.feePayer)) {
590
+ throw new StandardX402PayError(
591
+ "payment_outcome_unknown",
592
+ "The seller's payment challenge differs from the saved realization; refusing to fund a different payment.",
593
+ publicPendingPayment(existingPending)
747
594
  );
748
595
  }
749
- try {
750
- return JSON.parse(text);
751
- } catch {
752
- throw new VaultFlowClientError(
753
- step,
754
- `${path} returned 200 with a non-JSON body`,
755
- text
596
+ const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
597
+ if (selected.amountRawUsdc > cap) {
598
+ throw new StandardX402PayError(
599
+ "amount_exceeds_client_cap",
600
+ `the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; no new payment was attempted`,
601
+ { amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
756
602
  );
757
603
  }
758
- }
759
- async getJson(path) {
760
- const url2 = `${this.baseUrl}${path}`;
761
- const response = await this.fetchImpl(url2, {
762
- headers: await walletAuthHeaders({
763
- signer: this.signer,
764
- method: "GET",
765
- url: url2
766
- })
767
- });
768
- const text = await response.text();
769
- if (response.status !== 200) {
770
- const parsed = parseRelayerError(text);
771
- throw new VaultFlowClientError(
772
- "read",
773
- parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
774
- text,
775
- parsed.code,
776
- parsed.details
777
- );
604
+ const payment = {
605
+ payTo: selected.payTo,
606
+ amountRawUsdc: selected.amountRawUsdc.toString(),
607
+ resourceUrlHash: sha256HexOf(input.url),
608
+ method: method2
609
+ };
610
+ let pendingRecord = resuming ? existingPending : {
611
+ key: pendingKey,
612
+ url: input.url,
613
+ method: method2,
614
+ requestBodyHash,
615
+ amountRawUsdc: selected.amountRawUsdc.toString(),
616
+ payTo: selected.payTo,
617
+ feePayer: selected.feePayer,
618
+ realizedRawUsdc: "0",
619
+ realizeTxSignature: null,
620
+ status: "realizing",
621
+ createdAtMs: this.nowMs(),
622
+ updatedAtMs: this.nowMs(),
623
+ ...realizer.realizationContext === void 0 ? {} : {
624
+ recovery: {
625
+ version: 1,
626
+ context: { ...realizer.realizationContext },
627
+ requestHeadersHash,
628
+ requirement: structuredClone(selected.requirement)
629
+ }
630
+ }
631
+ };
632
+ if (!resuming) this.persistCheckpoint(pendingRecord);
633
+ let realized;
634
+ if (resuming && pendingRecord.status === "realized") {
635
+ realized = {
636
+ realizedRawUsdc: BigInt(pendingRecord.realizedRawUsdc),
637
+ txSignature: pendingRecord.realizeTxSignature,
638
+ withdrawalId: pendingRecord.recovery?.prepared?.withdrawalId ?? null
639
+ };
640
+ } else {
641
+ try {
642
+ if (resuming) {
643
+ if (realizer.resumeUsdcAvailable === void 0) {
644
+ throw new Error("This realizer cannot reconcile the saved withdrawal");
645
+ }
646
+ realized = await realizer.resumeUsdcAvailable({
647
+ amountRawUsdc: selected.amountRawUsdc,
648
+ payment,
649
+ prepared: pendingRecord.recovery.prepared
650
+ });
651
+ } else {
652
+ realized = await realizer.ensureUsdcAvailable({
653
+ amountRawUsdc: selected.amountRawUsdc,
654
+ payment,
655
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId },
656
+ onPrepared: async (prepared) => {
657
+ if (pendingRecord.recovery === void 0) {
658
+ throw new StandardX402PayError("state_persist_failed", "Cannot save a withdrawal without a pinned funding context");
659
+ }
660
+ const checkpoint = {
661
+ ...pendingRecord,
662
+ updatedAtMs: this.nowMs(),
663
+ recovery: { ...pendingRecord.recovery, prepared: structuredClone(prepared) }
664
+ };
665
+ this.persistCheckpoint(checkpoint);
666
+ pendingRecord = checkpoint;
667
+ }
668
+ });
669
+ }
670
+ } catch (error) {
671
+ if (error instanceof StandardX402PayError && error.reason === "state_persist_failed") throw error;
672
+ const failure = error;
673
+ const safeToRetry = !resuming && (failure.realizationSafeToRetry === true || failure.code === "approval_required" && pendingRecord.recovery?.prepared === void 0);
674
+ if (safeToRetry) {
675
+ try {
676
+ this.untrack(pendingKey);
677
+ } catch (persistError) {
678
+ throw new StandardX402PayError("state_persist_failed", "Could not clear the safely refused realization", persistError);
679
+ }
680
+ }
681
+ if (safeToRetry && failure.code === "approval_required") {
682
+ throw new StandardX402PayError(
683
+ "approval_required",
684
+ "This payment needs the owner's approval; nothing was paid. Ask the owner to open approveUrl, then retry the same call with approvalId.",
685
+ failure.detail ?? null
686
+ );
687
+ }
688
+ throw new StandardX402PayError(
689
+ safeToRetry ? "realize_failed" : "payment_outcome_unknown",
690
+ safeToRetry ? "Yield realization failed before submission; no payment was attempted." : "The original yield realization has not been confirmed. Its saved withdrawal must be reconciled before any new withdrawal or payment.",
691
+ { error, pendingPayment: safeToRetry ? null : publicPendingPayment(pendingRecord) }
692
+ );
693
+ }
778
694
  }
779
- try {
780
- return JSON.parse(text);
781
- } catch {
782
- throw new VaultFlowClientError(
783
- "read",
784
- `${path} returned 200 with a non-JSON body`,
785
- text
695
+ pendingRecord = {
696
+ ...pendingRecord,
697
+ status: "realized",
698
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
699
+ realizeTxSignature: realized.txSignature,
700
+ updatedAtMs: this.nowMs()
701
+ };
702
+ this.persistCheckpoint(pendingRecord);
703
+ if (realized.realizedRawUsdc < selected.amountRawUsdc) {
704
+ throw new StandardX402PayError(
705
+ "realize_underfunded",
706
+ "The confirmed withdrawal did not cover the exact API price. No external payment was attempted; reconcile the recorded withdrawal before retrying.",
707
+ { pendingPayment: publicPendingPayment(pendingRecord) }
786
708
  );
787
709
  }
788
- }
789
- };
790
- function parseRelayerError(text) {
791
- try {
792
- const parsed = JSON.parse(text);
793
- return {
794
- code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
795
- message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
796
- details: parsed.error?.details ?? null
797
- };
798
- } catch {
799
- return { code: null, message: null, details: null };
800
- }
801
- }
802
-
803
- // ../../src/client/relayer-yield-realizer.ts
804
- var REALIZE_OVERHEAD_RAW_USDC = 2500n;
805
- var RelayerRealizeError = class extends Error {
806
- constructor(code, message, detail = null) {
807
- super(message);
808
- this.code = code;
809
- this.detail = detail;
810
- this.name = "RelayerRealizeError";
811
- }
812
- code;
813
- detail;
814
- };
815
- var RelayerYieldRealizer = class {
816
- get vault() {
817
- return this.vaultFlows.vault.address;
818
- }
819
- vaultFlows;
820
- constructor(config) {
821
- this.vaultFlows = new VaultFlowClient({
822
- relayerBaseUrl: config.relayerBaseUrl,
823
- signer: config.signer,
824
- rpc: config.rpc,
825
- ...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
826
- ...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
710
+ this.persistCheckpoint({
711
+ ...pendingRecord,
712
+ status: "external_outcome_unknown",
713
+ updatedAtMs: this.nowMs()
827
714
  });
828
- }
829
- async ensureUsdcAvailable(input) {
830
- const shortfallRawUsdc = input.amountRawUsdc;
831
- await this.assertSpendableYield(shortfallRawUsdc);
832
- let outcome;
715
+ let response;
833
716
  try {
834
- outcome = await this.vaultFlows.withdraw({
835
- amountRawUsdc: shortfallRawUsdc,
836
- // The relayer refuses to prepare this withdrawal beyond the spendable
837
- // yield — the principal-protection guard the client cannot bypass.
838
- purpose: "yield_realize",
839
- // Declares what is being paid so the relayer's spending-mandate layer
840
- // can enforce caps/payee and keep the mandate → payment audit chain.
841
- ...input.payment === void 0 ? {} : { payment: input.payment },
842
- ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
843
- });
717
+ response = await this.x402Fetch(input.url, init, selected);
844
718
  } catch (error) {
845
- throw this.mapWithdrawError(error);
846
- }
847
- if (outcome.status !== "confirmed" || outcome.txSignature === null) {
848
- throw new RelayerRealizeError(
849
- "realize_not_confirmed",
850
- `yield realize withdrawal did not confirm (status=${outcome.status})`,
851
- outcome
719
+ const persistError = this.tryMarkUnknown(pendingKey, {
720
+ message: error instanceof Error ? error.message : String(error)
721
+ });
722
+ throw new StandardX402PayError(
723
+ "payment_outcome_unknown",
724
+ `the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
725
+ { error, persistError }
726
+ );
727
+ }
728
+ const bodyText = await response.text();
729
+ const receipt = readSettlementReceipt(response);
730
+ if (response.status < 200 || response.status >= 300 || receipt.status === "failed" || receipt.status === "invalid") {
731
+ const persistError = this.tryMarkUnknown(pendingKey, {
732
+ status: response.status,
733
+ body: bodyText,
734
+ receiptStatus: receipt.status
735
+ });
736
+ throw new StandardX402PayError(
737
+ "payment_outcome_unknown",
738
+ `the x402 payment attempt returned HTTP ${response.status} with a ${receipt.status} receipt; verify whether it settled before paying again`,
739
+ { status: response.status, body: bodyText, receiptStatus: receipt.status, persistError }
852
740
  );
853
741
  }
742
+ this.clearDelivered(pendingKey);
743
+ const paymentTxSignature = receipt.txSignature;
744
+ if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && realizer.reportPayment !== void 0) {
745
+ try {
746
+ await realizer.reportPayment({
747
+ withdrawalId: realized.withdrawalId,
748
+ paymentTxSignature
749
+ });
750
+ } catch (error) {
751
+ console.error(
752
+ `[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
753
+ );
754
+ }
755
+ }
854
756
  return {
855
- realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
856
- txSignature: outcome.txSignature,
857
- withdrawalId: outcome.withdrawalId
757
+ paid: true,
758
+ ...realizer.vault === void 0 ? {} : { fundingVault: realizer.vault },
759
+ status: response.status,
760
+ body: bodyText,
761
+ payment: {
762
+ amountRawUsdc: selected.amountRawUsdc.toString(),
763
+ payTo: selected.payTo,
764
+ feePayer: selected.feePayer,
765
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
766
+ realizeTxSignature: realized.txSignature,
767
+ paymentTxSignature
768
+ }
858
769
  };
859
770
  }
860
- /**
861
- * Best-effort report-back of the x402 payment tx this realize funded —
862
- * closes the relayer's mandate → realize → payment audit chain. Callers
863
- * must never let a failure here affect the payment result.
864
- */
865
- async reportPayment(input) {
866
- await this.vaultFlows.reportPayment(input);
867
- }
868
- /**
869
- * Refuses to realize more than the ledger's spendable yield (principal).
870
- * getBudget syncs the relayer's ledger from chain first (best-effort), so a
871
- * long-running client sees yield as it accrues instead of a frozen view.
872
- */
873
- async assertSpendableYield(shortfallRawUsdc) {
874
- let spendable;
771
+ /** Reads the challenge from the header (preferred) or the JSON body. */
772
+ async selectRequirement(probe) {
773
+ const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
774
+ let requirements;
875
775
  try {
876
- const budget = await this.vaultFlows.getBudget();
877
- spendable = BigInt(budget.spendableYieldRawUsdc);
776
+ if (header !== null) {
777
+ requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
778
+ } else {
779
+ requirements = parseStandardChallenge(
780
+ await probe.json()
781
+ ).solanaExactRequirements;
782
+ }
878
783
  } catch (error) {
879
- throw new RelayerRealizeError(
880
- "budget_unavailable",
881
- "could not read the spendable-yield budget",
784
+ throw new StandardX402PayError(
785
+ error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
786
+ "could not parse the x402 402 challenge",
882
787
  error
883
788
  );
884
789
  }
885
- const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
886
- if (spendable < requiredRawUsdc) {
887
- throw new RelayerRealizeError(
888
- "insufficient_yield",
889
- `spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC plus the ${REALIZE_OVERHEAD_RAW_USDC} raw fee headroom; the principal is never spent \u2014 wait for more yield`,
890
- { spendableYieldRawUsdc: spendable.toString() }
891
- );
892
- }
893
- }
894
- mapWithdrawError(error) {
895
- if (!(error instanceof VaultFlowClientError)) {
896
- return new RelayerRealizeError(
897
- "prepare_failed",
898
- `yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
790
+ try {
791
+ return selectPayableSolanaRequirement(requirements, {
792
+ network: this.network,
793
+ usdcMint: this.usdcMint
794
+ });
795
+ } catch (error) {
796
+ throw new StandardX402PayError(
797
+ "no_payable_requirement",
798
+ error instanceof Error ? error.message : String(error),
899
799
  error
900
800
  );
901
801
  }
902
- const serverCode = error.code ?? errorCodeFrom(error.detail);
903
- if (serverCode === "approval_required") {
904
- return new RelayerRealizeError(
905
- "approval_required",
906
- "this payment exceeds the owner-approval threshold; nothing was realized or paid. Ask the owner to approve, then retry with the approvalId",
907
- error.errorDetails ?? error.detail
908
- );
909
- }
910
- if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
911
- return new RelayerRealizeError(
912
- "insufficient_yield",
913
- "the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
914
- error.detail
802
+ }
803
+ persistCheckpoint(record) {
804
+ try {
805
+ this.track(record);
806
+ } catch (error) {
807
+ throw new StandardX402PayError(
808
+ "state_persist_failed",
809
+ "Could not persist payment recovery state; refusing the next financial operation.",
810
+ { error, pendingPayment: publicPendingPayment(record) }
915
811
  );
916
812
  }
917
- return new RelayerRealizeError(
918
- error.step === "submit" ? "submit_failed" : "prepare_failed",
919
- error.message,
920
- error.detail
921
- );
922
- }
923
- };
924
- function errorCodeFrom(detail) {
925
- if (typeof detail !== "string") {
926
- return null;
927
- }
928
- try {
929
- const parsed = JSON.parse(detail);
930
- return typeof parsed.error?.code === "string" ? parsed.error.code : null;
931
- } catch {
932
- return null;
933
- }
934
- }
935
-
936
- // ../../src/lib/canonical-json.ts
937
- import { createHash as createHash4 } from "node:crypto";
938
-
939
- // ../../src/lib/hash.ts
940
- import { createHash as createHash3 } from "node:crypto";
941
- var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
942
- function sha256TaggedHex(data) {
943
- return `sha256-${createHash3("sha256").update(data).digest("hex")}`;
944
- }
945
- function stableStringify(value) {
946
- if (value === null) {
947
- return "null";
948
- }
949
- if (typeof value === "bigint") {
950
- return JSON.stringify(value.toString());
951
813
  }
952
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
953
- return JSON.stringify(value);
814
+ track(record) {
815
+ const previous = this.pending.get(record.key);
816
+ this.pending.set(record.key, record);
817
+ try {
818
+ this.persist();
819
+ } catch (error) {
820
+ if (previous === void 0) {
821
+ this.pending.delete(record.key);
822
+ } else {
823
+ this.pending.set(record.key, previous);
824
+ }
825
+ throw error;
826
+ }
954
827
  }
955
- if (Array.isArray(value)) {
956
- return `[${value.map((item) => stableStringify(item)).join(",")}]`;
828
+ markUnknown(key, detail) {
829
+ const current = this.pending.get(key);
830
+ if (current === void 0) {
831
+ return;
832
+ }
833
+ const next = {
834
+ ...current,
835
+ status: "external_outcome_unknown",
836
+ updatedAtMs: this.nowMs(),
837
+ detail
838
+ };
839
+ this.pending.set(key, next);
840
+ try {
841
+ this.persist();
842
+ } catch (error) {
843
+ this.pending.set(key, current);
844
+ throw error;
845
+ }
957
846
  }
958
- const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
959
- return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
960
- }
961
- function hashStableJson(value) {
962
- return sha256TaggedHex(stableStringify(value));
963
- }
964
-
965
- // ../../src/lib/canonical-json.ts
966
- function sha256HexOf(data) {
967
- return createHash4("sha256").update(data, "utf8").digest("hex");
968
- }
969
-
970
- // ../../src/x402/headers.ts
971
- import { z as z2 } from "zod";
972
- var PAYMENT_REQUIRED_HEADER = "payment-required";
973
- var PAYMENT_RESPONSE_HEADER = "payment-response";
974
- var MAX_HEADER_JSON_BYTES = 16384;
975
- var X402HeaderError = class extends Error {
976
- reason;
977
- constructor(reason, message) {
978
- super(message);
979
- this.name = "X402HeaderError";
980
- this.reason = reason;
847
+ tryMarkUnknown(key, detail) {
848
+ try {
849
+ this.markUnknown(key, detail);
850
+ return null;
851
+ } catch (error) {
852
+ return error;
853
+ }
981
854
  }
982
- };
983
- var sublyPaymentRequirementsSchema = z2.object({
984
- scheme: z2.literal(PAYMENT_SCHEME),
985
- network: z2.string().min(1),
986
- asset: z2.string().min(32),
987
- /** Exact seller amount in raw USDC; the scheme settles exactly this. */
988
- amountRawUsdc: z2.string().regex(/^[1-9]\d*$/),
989
- resource: z2.string().url(),
990
- description: z2.string().optional(),
991
- mimeType: z2.string().optional(),
992
- payTo: z2.string().min(32),
993
- maxTimeoutSeconds: z2.number().int().positive(),
994
- extra: z2.object({
995
- sellerRequestId: z2.string().min(1),
996
- seller: z2.string().min(32),
997
- sellerUsdcAta: z2.string().min(32),
998
- vault: z2.string().min(32),
999
- shareMint: z2.string().min(32)
1000
- })
1001
- }).loose();
1002
- var paymentRequiredSchema = z2.object({
1003
- x402Version: z2.number().int(),
1004
- accepts: z2.array(z2.unknown()),
1005
- error: z2.string().optional()
1006
- }).loose();
1007
- var sublyPaymentPayloadSchema = z2.object({
1008
- x402Version: z2.number().int(),
1009
- scheme: z2.literal(PAYMENT_SCHEME),
1010
- network: z2.string().min(1),
1011
- payload: z2.object({
1012
- paymentId: z2.string().min(1),
1013
- requestBindingHash: z2.string().min(1),
1014
- preparedMessageHash: z2.string().min(1),
1015
- serializedTransaction: z2.string().min(1).max(4096),
1016
- agentSignature: z2.string().min(1).max(128),
1017
- temporarySettlementSignature: z2.string().min(1).max(128)
1018
- })
1019
- }).loose();
1020
- function decodeX402Header(headerValue) {
1021
- if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
1022
- throw new X402HeaderError(
1023
- "header_too_large",
1024
- `x402 header exceeds ${MAX_HEADER_JSON_BYTES} encoded bytes`
1025
- );
855
+ untrack(key) {
856
+ const previous = this.pending.get(key);
857
+ const existed = previous !== void 0;
858
+ this.pending.delete(key);
859
+ try {
860
+ this.persist();
861
+ } catch (error) {
862
+ if (existed) {
863
+ this.pending.set(key, previous);
864
+ }
865
+ throw error;
866
+ }
1026
867
  }
1027
- const json = Buffer.from(headerValue, "base64").toString("utf8");
1028
- try {
1029
- return JSON.parse(json);
1030
- } catch {
1031
- throw new X402HeaderError(
1032
- "invalid_header_encoding",
1033
- "x402 header is not base64-encoded JSON"
1034
- );
868
+ clearDelivered(key) {
869
+ const previous = this.pending.get(key);
870
+ this.pending.delete(key);
871
+ try {
872
+ this.persist();
873
+ } catch (error) {
874
+ if (previous !== void 0) {
875
+ this.pending.set(key, previous);
876
+ }
877
+ console.error(
878
+ `[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
879
+ );
880
+ }
881
+ }
882
+ persist() {
883
+ if (this.stateStore === null) {
884
+ return;
885
+ }
886
+ this.stateStore.save([...this.pending.values()]);
1035
887
  }
888
+ };
889
+ function publicPendingPayment(record) {
890
+ return {
891
+ url: record.url,
892
+ method: record.method,
893
+ requestBodyHash: record.requestBodyHash,
894
+ amountRawUsdc: record.amountRawUsdc,
895
+ payTo: record.payTo,
896
+ feePayer: record.feePayer,
897
+ realizedRawUsdc: record.realizedRawUsdc,
898
+ realizeTxSignature: record.realizeTxSignature,
899
+ status: record.status,
900
+ createdAtMs: record.createdAtMs,
901
+ updatedAtMs: record.updatedAtMs,
902
+ withdrawalId: record.recovery?.prepared?.withdrawalId ?? null,
903
+ fundingSource: record.recovery?.context ?? null
904
+ };
1036
905
  }
1037
- function requestBodyHashFor(body2) {
1038
- if (body2 === null || body2 === void 0 || body2.length === 0) {
1039
- return EMPTY_BODY_HASH;
906
+ function pendingPaymentKey(input) {
907
+ return `${input.method}:${input.url}:${input.requestBodyHash}`;
908
+ }
909
+ function readSettlementReceipt(response) {
910
+ const header = response.headers.get(PAYMENT_RESPONSE_HEADER) ?? response.headers.get("x-payment-response");
911
+ if (header === null) {
912
+ return { status: "absent", txSignature: null };
913
+ }
914
+ try {
915
+ const decoded = decodeX402Header(header);
916
+ if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded) || !("success" in decoded) || typeof decoded.success !== "boolean") {
917
+ return { status: "invalid", txSignature: null };
918
+ }
919
+ if (!decoded.success) return { status: "failed", txSignature: null };
920
+ const receipt = decoded;
921
+ if (typeof receipt.transaction === "string" && receipt.transaction.length > 0) {
922
+ return { status: "success", txSignature: receipt.transaction };
923
+ }
924
+ if (typeof receipt.txHash === "string" && receipt.txHash.length > 0) {
925
+ return { status: "success", txSignature: receipt.txHash };
926
+ }
927
+ return { status: "invalid", txSignature: null };
928
+ } catch {
929
+ return { status: "invalid", txSignature: null };
1040
930
  }
1041
- return sha256TaggedHex(
1042
- typeof body2 === "string" ? Buffer.from(body2, "utf8") : Buffer.from(body2)
1043
- );
1044
931
  }
1045
932
 
1046
- // ../../src/x402/standard-requirements.ts
1047
- import { z as z3 } from "zod";
1048
- var STANDARD_EXACT_SCHEME = "exact";
1049
- var standardExactRequirementSchema = z3.object({
1050
- scheme: z3.literal(STANDARD_EXACT_SCHEME),
1051
- /** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
1052
- network: z3.string().min(1),
1053
- /** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
1054
- asset: z3.string().min(1),
1055
- /** Exact price in the asset's atomic units, as a decimal string. */
1056
- amount: z3.string().regex(/^[1-9]\d*$/),
1057
- /** Recipient wallet; the transfer destination ATA is derived from it. */
1058
- payTo: z3.string().min(1),
1059
- maxTimeoutSeconds: z3.number().int().positive().optional(),
1060
- extra: z3.object({
1061
- /** Facilitator address that pays the tx fee (gas sponsorship). */
1062
- feePayer: z3.string().min(1).optional()
1063
- }).loose().optional()
1064
- }).loose();
1065
- var standardPaymentRequiredSchema = z3.object({
1066
- x402Version: z3.number().int(),
1067
- accepts: z3.array(z3.unknown()),
1068
- error: z3.string().optional(),
1069
- resource: z3.object({ url: z3.string().optional() }).loose().optional()
1070
- }).loose();
1071
- var StandardX402ChallengeError = class extends Error {
1072
- reason;
1073
- constructor(reason, message) {
1074
- super(message);
1075
- this.name = "StandardX402ChallengeError";
1076
- this.reason = reason;
1077
- }
1078
- };
1079
- function parseStandardChallenge(challenge) {
1080
- const parsed = standardPaymentRequiredSchema.safeParse(challenge);
1081
- if (!parsed.success) {
1082
- throw new StandardX402ChallengeError(
1083
- "invalid_payment_required",
1084
- "Response is not a valid x402 PaymentRequired object"
933
+ // ../../src/lib/associated-token-account.ts
934
+ import { createHash as createHash4 } from "node:crypto";
935
+ import bs582 from "bs58";
936
+ var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
937
+ var ED25519_P = (1n << 255n) - 19n;
938
+ var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
939
+ function deriveAssociatedTokenAddress(params) {
940
+ const owner = decodePublicKey(params.owner, "owner");
941
+ const mint = decodePublicKey(params.mint ?? SUBLY_VAULT.usdcMint, "mint");
942
+ const tokenProgramId = decodePublicKey(
943
+ params.tokenProgramId ?? SPL_TOKEN_PROGRAM_ID,
944
+ "tokenProgramId"
945
+ );
946
+ const associatedTokenProgramId = decodePublicKey(
947
+ ASSOCIATED_TOKEN_PROGRAM_ID,
948
+ "associatedTokenProgramId"
949
+ );
950
+ for (let bump = 255; bump >= 0; bump -= 1) {
951
+ const address3 = createProgramAddress(
952
+ [owner, tokenProgramId, mint, Uint8Array.of(bump)],
953
+ associatedTokenProgramId
1085
954
  );
1086
- }
1087
- const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
1088
- const requirement = standardExactRequirementSchema.safeParse(candidate);
1089
- if (!requirement.success) {
1090
- return [];
955
+ if (address3 !== null) {
956
+ return bs582.encode(address3);
1091
957
  }
1092
- return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
1093
- });
1094
- return { paymentRequired: parsed.data, solanaExactRequirements };
958
+ }
959
+ throw new Error("Unable to derive associated token account address");
1095
960
  }
1096
- function decodeStandardPaymentRequiredHeader(headerValue) {
1097
- let decoded;
1098
- try {
1099
- decoded = decodeX402Header(headerValue);
1100
- } catch (error) {
1101
- throw new StandardX402ChallengeError(
1102
- error instanceof X402HeaderError ? error.reason : "invalid_header",
1103
- "Cannot decode the payment-required header"
1104
- );
961
+ function createProgramAddress(seeds, programId) {
962
+ const hash = createHash4("sha256");
963
+ for (const seed of seeds) {
964
+ hash.update(seed);
1105
965
  }
1106
- return parseStandardChallenge(decoded);
966
+ hash.update(programId);
967
+ hash.update(PDA_MARKER);
968
+ const digest = hash.digest();
969
+ return isEd25519Point(digest) ? null : new Uint8Array(digest);
1107
970
  }
1108
- function selectPayableSolanaRequirement(requirements, options) {
1109
- const network = options?.network ?? SOLANA_MAINNET_NETWORK;
1110
- const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
1111
- const matchingRequirements = requirements.filter(
1112
- (candidate) => candidate.network === network && candidate.asset === usdcMint
1113
- );
1114
- if (matchingRequirements.length === 0) {
1115
- throw new StandardX402ChallengeError(
1116
- "no_payable_requirement",
1117
- `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
1118
- );
971
+ function decodePublicKey(value, fieldName) {
972
+ const decoded = bs582.decode(value);
973
+ if (decoded.length !== 32) {
974
+ throw new Error(`${fieldName} must be a 32-byte public key`);
1119
975
  }
1120
- const requirement = matchingRequirements.find(
1121
- (candidate) => candidate.extra?.feePayer !== void 0
1122
- ) ?? null;
1123
- if (requirement === null) {
1124
- throw new StandardX402ChallengeError(
1125
- "missing_svm_fee_payer",
1126
- "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
1127
- );
976
+ return decoded;
977
+ }
978
+ function isEd25519Point(bytes) {
979
+ if (bytes.length !== 32) {
980
+ return false;
1128
981
  }
1129
- const feePayer = requirement.extra?.feePayer;
1130
- if (feePayer === void 0) {
1131
- throw new StandardX402ChallengeError(
1132
- "missing_svm_fee_payer",
1133
- "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
1134
- );
982
+ const yBytes = Uint8Array.from(bytes);
983
+ yBytes[31] = yBytes[31] & 127;
984
+ const y = littleEndianToBigInt(yBytes);
985
+ if (y >= ED25519_P) {
986
+ return false;
1135
987
  }
1136
- return {
1137
- requirement,
1138
- amountRawUsdc: BigInt(requirement.amount),
1139
- payTo: requirement.payTo,
1140
- feePayer
1141
- };
988
+ const ySquared = mod(y * y, ED25519_P);
989
+ const numerator = mod(ySquared - 1n, ED25519_P);
990
+ const denominator = mod(ED25519_D * ySquared + 1n, ED25519_P);
991
+ if (denominator === 0n) {
992
+ return false;
993
+ }
994
+ const xSquared = mod(
995
+ numerator * modPow(denominator, ED25519_P - 2n, ED25519_P),
996
+ ED25519_P
997
+ );
998
+ return xSquared === 0n || modPow(xSquared, (ED25519_P - 1n) / 2n, ED25519_P) === 1n;
1142
999
  }
1143
- function standardRequirementMatchesSelected(candidate, selected) {
1144
- const parsed = standardExactRequirementSchema.safeParse(candidate);
1145
- return parsed.success && stableJson(parsed.data) === stableJson(selected.requirement);
1000
+ function littleEndianToBigInt(bytes) {
1001
+ let value = 0n;
1002
+ for (let index = bytes.length - 1; index >= 0; index -= 1) {
1003
+ value = (value << 8n) + BigInt(bytes[index]);
1004
+ }
1005
+ return value;
1006
+ }
1007
+ function mod(value, modulus) {
1008
+ const result = value % modulus;
1009
+ return result >= 0n ? result : result + modulus;
1010
+ }
1011
+ function modPow(base, exponent, modulus) {
1012
+ let result = 1n;
1013
+ let nextBase = mod(base, modulus);
1014
+ let nextExponent = exponent;
1015
+ while (nextExponent > 0n) {
1016
+ if ((nextExponent & 1n) === 1n) {
1017
+ result = mod(result * nextBase, modulus);
1018
+ }
1019
+ nextBase = mod(nextBase * nextBase, modulus);
1020
+ nextExponent >>= 1n;
1021
+ }
1022
+ return result;
1023
+ }
1024
+
1025
+ // ../../src/domain/withdrawal-rounding.ts
1026
+ var WITHDRAWAL_ROUNDING_RAW_USDC = 10n;
1027
+ var YIELD_REALIZE_ROUNDING_RAW_USDC = WITHDRAWAL_ROUNDING_RAW_USDC / 2n;
1028
+
1029
+ // ../../src/client/withdrawal-preview.ts
1030
+ async function assertWithdrawalPreview(input) {
1031
+ const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
1032
+ const simulation = await input.rpc.simulateTransaction(
1033
+ input.serializedTransaction,
1034
+ {
1035
+ encoding: "base64",
1036
+ commitment: "confirmed",
1037
+ sigVerify: false,
1038
+ replaceRecentBlockhash: false,
1039
+ innerInstructions: true
1040
+ }
1041
+ ).send({ abortSignal: AbortSignal.timeout(15e3) });
1042
+ if (simulation.value.err !== null) {
1043
+ throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
1044
+ }
1045
+ let received = 0n;
1046
+ for (const group of simulation.value.innerInstructions ?? []) {
1047
+ for (const instruction of group.instructions) {
1048
+ if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
1049
+ const parsed = instruction.parsed;
1050
+ if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
1051
+ const info = parsed.info;
1052
+ if (!info || info.destination !== destination && info.source !== destination) continue;
1053
+ const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
1054
+ if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
1055
+ throw new Error("Withdrawal preview returned an invalid token amount");
1056
+ }
1057
+ const amount = BigInt(raw);
1058
+ if (info.destination === destination) received += amount;
1059
+ if (info.source === destination) received -= amount;
1060
+ }
1061
+ }
1062
+ const minimum = input.purpose === "yield_realize" ? input.amountRawUsdc : input.amountRawUsdc - WITHDRAWAL_ROUNDING_RAW_USDC;
1063
+ if (received <= 0n || received > input.amountRawUsdc + WITHDRAWAL_ROUNDING_RAW_USDC || received < minimum) {
1064
+ throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
1065
+ }
1146
1066
  }
1147
- function stableJson(value) {
1148
- return JSON.stringify(sortJson(value));
1067
+
1068
+ // ../../src/client/lookup-tables.ts
1069
+ import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1070
+ import { address, getCompiledTransactionMessageDecoder } from "@solana/kit";
1071
+ function lookupTableAddressesForTransaction(serializedTransaction) {
1072
+ const wire = Buffer.from(serializedTransaction, "base64");
1073
+ let offset = 0;
1074
+ let signatureCount = 0;
1075
+ let shift = 0;
1076
+ while (offset < wire.length) {
1077
+ const byte = wire[offset];
1078
+ signatureCount |= (byte & 127) << shift;
1079
+ offset += 1;
1080
+ if ((byte & 128) === 0) {
1081
+ break;
1082
+ }
1083
+ shift += 7;
1084
+ }
1085
+ const messageBytes = wire.subarray(offset + signatureCount * 64);
1086
+ const compiled = getCompiledTransactionMessageDecoder().decode(messageBytes);
1087
+ const lookups = compiled.addressTableLookups ?? [];
1088
+ return lookups.map((lookup) => String(lookup.lookupTableAddress));
1149
1089
  }
1150
- function sortJson(value) {
1151
- if (Array.isArray(value)) {
1152
- return value.map(sortJson);
1090
+ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1091
+ const addresses = lookupTableAddressesForTransaction(serializedTransaction);
1092
+ if (addresses.length === 0) {
1093
+ return {};
1153
1094
  }
1154
- if (value !== null && typeof value === "object") {
1155
- return Object.fromEntries(
1156
- Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
1157
- );
1095
+ const tables = await fetchAllMaybeAddressLookupTable(
1096
+ rpc2,
1097
+ addresses.map((value) => address(value))
1098
+ );
1099
+ const result = {};
1100
+ for (const table of tables) {
1101
+ if (table.exists) {
1102
+ result[table.address] = table.data.addresses.map(String);
1103
+ }
1158
1104
  }
1159
- return value;
1105
+ return result;
1160
1106
  }
1161
1107
 
1162
- // ../../src/client/standard-x402-payer.ts
1163
- var StandardX402PayError = class extends Error {
1164
- constructor(reason, message, detail = null) {
1108
+ // ../../src/client/vault-flows.ts
1109
+ var VaultFlowClientError = class extends Error {
1110
+ constructor(step, message, detail = null, code = null, errorDetails = null) {
1165
1111
  super(message);
1166
- this.reason = reason;
1112
+ this.step = step;
1167
1113
  this.detail = detail;
1168
- this.name = "StandardX402PayError";
1114
+ this.code = code;
1115
+ this.errorDetails = errorDetails;
1116
+ this.name = "VaultFlowClientError";
1169
1117
  }
1170
- reason;
1118
+ step;
1171
1119
  detail;
1120
+ code;
1121
+ errorDetails;
1172
1122
  };
1173
- var StandardX402Payer = class {
1174
- realizer;
1175
- x402Fetch;
1176
- probeFetch;
1177
- defaultMaxAmountRawUsdc;
1178
- network;
1179
- usdcMint;
1180
- stateStore;
1181
- pending = /* @__PURE__ */ new Map();
1182
- inFlight = /* @__PURE__ */ new Map();
1183
- nowMs;
1123
+ function vaultOperationKind(intentId) {
1124
+ if (/^dep_[0-9a-f]{32}$/.test(intentId)) return "deposit";
1125
+ if (/^wdr_[0-9a-f]{32}$/.test(intentId)) return "withdrawal";
1126
+ throw new VaultFlowClientError("read", "intentId must be the original dep_ or wdr_ ID followed by 32 lowercase hexadecimal characters");
1127
+ }
1128
+ var VaultFlowClient = class {
1129
+ vault;
1130
+ rpc;
1131
+ baseUrl;
1132
+ signer;
1133
+ fetchImpl;
1134
+ lookupTablesFor;
1135
+ pollTimeoutMs;
1136
+ pollIntervalMs;
1184
1137
  constructor(config) {
1185
- this.realizer = config.realizer;
1186
- this.x402Fetch = config.x402Fetch;
1187
- this.probeFetch = config.probeFetch ?? fetch;
1188
- this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
1189
- this.network = config.network ?? SOLANA_MAINNET_NETWORK;
1190
- this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
1191
- this.stateStore = config.stateStore ?? null;
1192
- this.nowMs = config.nowMs ?? (() => Date.now());
1193
- if (this.stateStore !== null) {
1194
- for (const record of this.stateStore.load()) {
1195
- this.pending.set(record.key, record);
1196
- }
1138
+ this.rpc = config.rpc;
1139
+ this.vault = config.vault ?? config.signer.vault ?? SUBLY_VAULT;
1140
+ if (config.signer.vault && config.signer.vault.address !== this.vault.address) {
1141
+ throw new Error("Vault flow client and signer must select the same vault");
1197
1142
  }
1143
+ this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
1144
+ this.signer = config.signer;
1145
+ this.fetchImpl = config.fetchImpl ?? fetch;
1146
+ this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
1147
+ this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1148
+ this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1198
1149
  }
1199
- pay(input, realizer = this.realizer) {
1200
- const method2 = (input.method ?? "GET").toUpperCase();
1201
- const requestBodyHash = requestBodyHashFor(input.body ?? null);
1202
- const pendingKey = pendingPaymentKey({
1203
- url: input.url,
1204
- method: method2,
1205
- requestBodyHash
1150
+ /**
1151
+ * Moves USDC from the agent wallet into the vault (fee sponsored). Under
1152
+ * depositPolicy "owner_approval_required" the relayer refuses to prepare
1153
+ * without an owner approval; when the caller passes none, an already
1154
+ * APPROVED deposit approval for this exact amount (e.g. the mandate's
1155
+ * initialDeposit — "one Face ID covers mandate + first deposit") is looked
1156
+ * up and used automatically before surfacing deposit_approval_required.
1157
+ */
1158
+ async deposit(input) {
1159
+ let approvalId2 = input.approvalId;
1160
+ let prepared;
1161
+ try {
1162
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1163
+ wallet: this.signer.walletAddress,
1164
+ vault: this.vault.address,
1165
+ amountRawUsdc: input.amountRawUsdc.toString(),
1166
+ ...approvalId2 === void 0 ? {} : { approvalId: approvalId2 }
1167
+ });
1168
+ } catch (error) {
1169
+ if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId2 !== void 0) {
1170
+ throw error;
1171
+ }
1172
+ approvalId2 = await this.findApprovedDepositApproval(input.amountRawUsdc);
1173
+ if (approvalId2 === void 0) {
1174
+ throw error;
1175
+ }
1176
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1177
+ wallet: this.signer.walletAddress,
1178
+ vault: this.vault.address,
1179
+ amountRawUsdc: input.amountRawUsdc.toString(),
1180
+ approvalId: approvalId2
1181
+ });
1182
+ }
1183
+ if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.signingIntent.amountRawUsdc !== input.amountRawUsdc.toString()) {
1184
+ throw new VaultFlowClientError("prepare", "Prepared deposit differs from the requested wallet, vault or amount");
1185
+ }
1186
+ const signed = await this.signer.signDeposit({
1187
+ intent: prepared.signingIntent,
1188
+ serializedTransaction: prepared.serializedTransaction,
1189
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1206
1190
  });
1207
- const existingFlow = this.inFlight.get(pendingKey);
1208
- if (existingFlow !== void 0) {
1209
- return existingFlow;
1191
+ let outcome = await this.postJson("submit", "/v1/deposits/submit", {
1192
+ depositId: prepared.depositId,
1193
+ serializedTransaction: signed.serializedTransaction,
1194
+ agentSignature: signed.agentSignature
1195
+ });
1196
+ if (outcome.status === "submitted") {
1197
+ outcome = await this.pollUntilTerminal(
1198
+ `/v1/deposits/${prepared.depositId}`,
1199
+ outcome
1200
+ );
1210
1201
  }
1211
- const run = async () => {
1212
- if (this.stateStore?.withExclusiveLock) {
1213
- this.pending.clear();
1214
- for (const record of this.stateStore.load()) this.pending.set(record.key, record);
1215
- }
1216
- return this.run(input, { method: method2, requestBodyHash, pendingKey }, realizer);
1202
+ return {
1203
+ depositId: prepared.depositId,
1204
+ status: outcome.status,
1205
+ txSignature: outcome.txSignature ?? null,
1206
+ actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
1207
+ sharesMintedRaw: outcome.sharesMintedRaw ?? null,
1208
+ errorCode: outcome.errorCode ?? null
1217
1209
  };
1218
- const flow = (this.stateStore?.withExclusiveLock ? this.stateStore.withExclusiveLock(run) : run()).finally(() => {
1219
- this.inFlight.delete(pendingKey);
1220
- });
1221
- this.inFlight.set(pendingKey, flow);
1222
- return flow;
1223
1210
  }
1224
- async run(input, computed, realizer) {
1225
- const { method: method2, requestBodyHash, pendingKey } = computed;
1226
- const existingPending = this.pending.get(pendingKey);
1227
- if (existingPending !== void 0 && input.forceNewPayment !== true) {
1228
- throw new StandardX402PayError(
1229
- "payment_outcome_unknown",
1230
- "a previous external x402 payment for this request has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call with forceNewPayment=true.",
1231
- existingPending
1211
+ /**
1212
+ * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
1213
+ * sponsored). A plain withdrawal is the exit path and MAY spend principal;
1214
+ * with purpose "yield_realize" the relayer refuses anything beyond the
1215
+ * spendable yield (the payment path, via RelayerYieldRealizer).
1216
+ */
1217
+ async withdraw(input) {
1218
+ const prepared = await this.postJson(
1219
+ "prepare",
1220
+ "/v1/withdrawals/prepare",
1221
+ {
1222
+ wallet: this.signer.walletAddress,
1223
+ vault: this.vault.address,
1224
+ amountRawUsdc: input.amountRawUsdc.toString(),
1225
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose },
1226
+ ...input.payment === void 0 ? {} : { payment: input.payment },
1227
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1228
+ }
1229
+ );
1230
+ this.assertPreparedWithdrawal(prepared, input);
1231
+ await input.onPrepared?.(prepared);
1232
+ return this.submitPreparedWithdrawal(prepared, input);
1233
+ }
1234
+ /** Reconcile or submit the original intent; never prepare a replacement. */
1235
+ async resumeWithdrawal(prepared, input) {
1236
+ this.assertPreparedWithdrawal(prepared, input);
1237
+ const current = await this.getJson(
1238
+ `/v1/withdrawals/${encodeURIComponent(prepared.withdrawalId)}`
1239
+ );
1240
+ if (current.withdrawalId !== prepared.withdrawalId || current.wallet !== this.signer.walletAddress || current.vault !== this.vault.address || current.requestedWithdrawRawUsdc !== input.amountRawUsdc.toString() || current.purpose !== (input.purpose ?? "normal") || canonicalJsonHash(current.paymentBinding ?? null) !== canonicalJsonHash(input.payment ?? null) || current.serializedTransaction !== prepared.serializedTransaction || current.preparedMessageHash !== prepared.signingIntent.preparedMessageHash || current.destinationUsdcAta !== prepared.destinationUsdcAta) {
1241
+ throw new VaultFlowClientError("read", "Saved withdrawal differs from the original operation; refusing to resume");
1242
+ }
1243
+ if (current.status === "prepared") {
1244
+ return this.submitPreparedWithdrawal(prepared, input);
1245
+ }
1246
+ if (!["submitted", "confirmed", "failed", "failed_not_submitted", "expired", "quarantined"].includes(current.status)) {
1247
+ throw new VaultFlowClientError("read", "Relayer returned an unknown withdrawal status");
1248
+ }
1249
+ return this.withdrawalOutcome(prepared, current);
1250
+ }
1251
+ assertPreparedWithdrawal(prepared, input) {
1252
+ if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.requestedWithdrawRawUsdc !== input.amountRawUsdc.toString() || prepared.purpose !== (input.purpose ?? "normal") || input.purpose === "yield_realize" && prepared.signingIntent.allowFullExit) {
1253
+ throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
1254
+ }
1255
+ if (typeof prepared.withdrawalId !== "string" || prepared.withdrawalId.length === 0) {
1256
+ throw new VaultFlowClientError("prepare", "Prepared withdrawal has no withdrawal ID");
1257
+ }
1258
+ }
1259
+ async submitPreparedWithdrawal(prepared, input) {
1260
+ await assertWithdrawalPreview({
1261
+ rpc: this.rpc,
1262
+ serializedTransaction: prepared.serializedTransaction,
1263
+ wallet: this.signer.walletAddress,
1264
+ vault: this.vault,
1265
+ amountRawUsdc: input.amountRawUsdc,
1266
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1267
+ });
1268
+ const signed = await this.signer.signWithdrawal({
1269
+ intent: prepared.signingIntent,
1270
+ serializedTransaction: prepared.serializedTransaction,
1271
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1272
+ });
1273
+ input.onBeforeSubmit?.();
1274
+ let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
1275
+ withdrawalId: prepared.withdrawalId,
1276
+ serializedTransaction: signed.serializedTransaction,
1277
+ agentSignature: signed.agentSignature
1278
+ });
1279
+ if (outcome.status === "submitted") {
1280
+ outcome = await this.pollUntilTerminal(
1281
+ `/v1/withdrawals/${prepared.withdrawalId}`,
1282
+ outcome
1232
1283
  );
1233
1284
  }
1234
- if (existingPending !== void 0 && input.forceNewPayment === true) {
1285
+ return this.withdrawalOutcome(prepared, outcome);
1286
+ }
1287
+ withdrawalOutcome(prepared, outcome) {
1288
+ return {
1289
+ withdrawalId: prepared.withdrawalId,
1290
+ status: outcome.status,
1291
+ txSignature: outcome.txSignature ?? null,
1292
+ destinationUsdcAta: prepared.destinationUsdcAta,
1293
+ actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
1294
+ actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
1295
+ errorCode: outcome.errorCode ?? null
1296
+ };
1297
+ }
1298
+ /** Authenticated read/reconciliation only: never prepare, sign or submit a transaction. */
1299
+ async getOperationStatus(intentId) {
1300
+ const kind = vaultOperationKind(intentId);
1301
+ const raw = await this.getJson(`/v1/${kind === "deposit" ? "deposits" : "withdrawals"}/${intentId}?resubmit=false`);
1302
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
1303
+ throw new VaultFlowClientError("read", "Relayer returned an invalid operation status");
1304
+ }
1305
+ const record = raw;
1306
+ if (record[kind === "deposit" ? "depositId" : "withdrawalId"] !== intentId || record.wallet !== this.signer.walletAddress || record.vault !== this.vault.address) {
1307
+ throw new VaultFlowClientError("read", "Operation does not match the requested ID, current wallet or selected vault; use the original wallet, vault and relayer");
1308
+ }
1309
+ const requested = record[kind === "deposit" ? "amountRawUsdc" : "requestedWithdrawRawUsdc"];
1310
+ const actual = record[kind === "deposit" ? "actualDepositRawUsdc" : "actualWithdrawRawUsdc"];
1311
+ if (typeof record.status !== "string" || !["prepared", "submitted", "confirmed", "failed", "expired", "failed_not_submitted"].includes(record.status) || typeof requested !== "string" || !/^\d+$/.test(requested) || actual !== null && (typeof actual !== "string" || !/^\d+$/.test(actual)) || record.txSignature !== null && (typeof record.txSignature !== "string" || record.txSignature.length === 0) || record.errorCode !== null && typeof record.errorCode !== "string" || record.status === "confirmed" && (actual === null || record.txSignature === null)) {
1312
+ throw new VaultFlowClientError("read", "Relayer returned incomplete or invalid operation status fields");
1313
+ }
1314
+ const status = record.status;
1315
+ const nextAction = status === "confirmed" ? "done" : status === "submitted" || status === "prepared" ? "check_again" : "reconcile_with_operator";
1316
+ const message = status === "confirmed" ? `The original ${kind} is confirmed.` : status === "submitted" ? "The original transaction is still confirming. Check this same intent ID again; do not repeat the deposit or withdrawal." : status === "prepared" ? "The original intent is prepared. This status check does not submit it. Check the same ID again or ask the operator to reconcile it before starting another operation." : "The original operation ended without a confirmed result. Reconcile its intent ID and transaction with the operator before starting another operation.";
1317
+ return {
1318
+ intentId,
1319
+ kind,
1320
+ wallet: this.signer.walletAddress,
1321
+ vault: this.vault.address,
1322
+ status,
1323
+ requestedAmountRawUsdc: requested,
1324
+ actualAmountRawUsdc: actual,
1325
+ txSignature: record.txSignature,
1326
+ errorCode: record.errorCode,
1327
+ stillConfirming: status === "submitted",
1328
+ nextAction,
1329
+ message
1330
+ };
1331
+ }
1332
+ /**
1333
+ * Reads the yield budget. Syncs the relayer's ledger from chain first (so
1334
+ * yield accrued since the last sync shows up); the sync is best-effort and
1335
+ * on failure the last-synced view is returned.
1336
+ */
1337
+ async getBudget(options = {}) {
1338
+ if (options.refreshFromChain !== false) {
1235
1339
  try {
1236
- this.untrack(pendingKey);
1237
- } catch (error) {
1238
- throw new StandardX402PayError(
1239
- "state_persist_failed",
1240
- "could not clear the previous pending x402 marker before forcing a new payment",
1241
- error
1340
+ await this.postJson(
1341
+ "sync",
1342
+ `/v1/wallets/${this.signer.walletAddress}/sync`,
1343
+ { source: "chain", vault: this.vault.address }
1242
1344
  );
1345
+ } catch {
1243
1346
  }
1244
1347
  }
1245
- const init = {
1246
- method: method2,
1247
- ...input.body === void 0 ? {} : { body: input.body },
1248
- ...input.headers === void 0 ? {} : { headers: input.headers }
1249
- };
1250
- const probe = await this.probeFetch(input.url, init);
1251
- if (probe.status !== 402) {
1252
- return { paid: false, status: probe.status, body: await probe.text() };
1253
- }
1254
- const selected = await this.selectRequirement(probe);
1255
- const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1256
- if (selected.amountRawUsdc > cap) {
1257
- throw new StandardX402PayError(
1258
- "amount_exceeds_client_cap",
1259
- `the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
1260
- { amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
1348
+ const url2 = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
1349
+ const response = await this.fetchImpl(url2, {
1350
+ headers: await walletAuthHeaders({
1351
+ signer: this.signer,
1352
+ method: "GET",
1353
+ url: url2
1354
+ })
1355
+ });
1356
+ const text = await response.text();
1357
+ if (response.status !== 200) {
1358
+ throw new VaultFlowClientError(
1359
+ "budget",
1360
+ `budget endpoint returned ${response.status}: ${text}`
1261
1361
  );
1262
1362
  }
1263
- let realized;
1363
+ let parsed;
1264
1364
  try {
1265
- realized = await realizer.ensureUsdcAvailable({
1266
- amountRawUsdc: selected.amountRawUsdc,
1267
- payment: {
1268
- payTo: selected.payTo,
1269
- amountRawUsdc: selected.amountRawUsdc.toString(),
1270
- resourceUrlHash: sha256HexOf(input.url),
1271
- method: method2
1272
- },
1273
- ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1274
- });
1275
- } catch (error) {
1276
- if (error.code === "approval_required") {
1277
- throw new StandardX402PayError(
1278
- "approval_required",
1279
- "this payment exceeds the owner-approval threshold; NOTHING was paid. Ask the owner to open the approveUrl, then retry the same call with the approvalId",
1280
- error.detail ?? null
1281
- );
1282
- }
1283
- throw new StandardX402PayError(
1284
- "realize_failed",
1285
- `could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
1286
- error
1365
+ parsed = JSON.parse(text);
1366
+ } catch {
1367
+ throw new VaultFlowClientError(
1368
+ "budget",
1369
+ "budget endpoint returned 200 with a non-JSON body",
1370
+ text
1287
1371
  );
1288
1372
  }
1289
- const pendingRecord = {
1290
- key: pendingKey,
1291
- url: input.url,
1292
- method: method2,
1293
- requestBodyHash,
1294
- amountRawUsdc: selected.amountRawUsdc.toString(),
1295
- payTo: selected.payTo,
1296
- feePayer: selected.feePayer,
1297
- realizedRawUsdc: realized.realizedRawUsdc.toString(),
1298
- realizeTxSignature: realized.txSignature,
1299
- status: "realized",
1300
- createdAtMs: this.nowMs(),
1301
- updatedAtMs: this.nowMs()
1373
+ const body2 = parsed;
1374
+ if (body2.position?.vault !== void 0 && body2.position.vault !== this.vault.address) {
1375
+ throw new VaultFlowClientError("budget", "Relayer returned the budget for a different vault");
1376
+ }
1377
+ return {
1378
+ wallet: this.signer.walletAddress,
1379
+ vault: this.vault.address,
1380
+ principalBasisRawUsdc: body2.position?.principalBasisRawUsdc ?? "0",
1381
+ positionValueRawUsdc: body2.budget?.positionValueRawUsdc ?? "0",
1382
+ grossYieldRawUsdc: body2.budget?.grossYieldRawUsdc ?? "0",
1383
+ spendableYieldRawUsdc: body2.budget?.spendableYieldRawUsdc ?? "0"
1302
1384
  };
1303
- try {
1304
- this.track(pendingRecord);
1305
- } catch (error) {
1306
- throw new StandardX402PayError(
1307
- "state_persist_failed",
1308
- "could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
1309
- { error, pendingPayment: pendingRecord }
1310
- );
1385
+ }
1386
+ /** Best-effort audit link: reports the x402 payment tx a realize funded. */
1387
+ async reportPayment(input) {
1388
+ await this.postJson("submit", "/v1/payments/report", {
1389
+ wallet: this.signer.walletAddress,
1390
+ withdrawalId: input.withdrawalId,
1391
+ paymentTxSignature: input.paymentTxSignature
1392
+ });
1393
+ }
1394
+ /** Wallet's approvals as the relayer sees them (optionally by status). */
1395
+ async listApprovals(status) {
1396
+ const body2 = await this.getJson(
1397
+ `/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
1398
+ );
1399
+ return body2.approvals ?? [];
1400
+ }
1401
+ /**
1402
+ * Creates the owner-onboarding setup link (wallet-auth pins the agreed
1403
+ * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
1404
+ */
1405
+ async createSetupSession(input) {
1406
+ const session = await this.postJson(
1407
+ "prepare",
1408
+ `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
1409
+ {
1410
+ vault: this.vault.address,
1411
+ ...input.policy === void 0 ? {} : { policy: input.policy },
1412
+ ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
1413
+ ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
1414
+ ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1415
+ }
1416
+ );
1417
+ if (session.vault !== this.vault.address || session.wallet !== this.signer.walletAddress) {
1418
+ throw new VaultFlowClientError("prepare", "Relayer returned a setup session for a different wallet or vault");
1311
1419
  }
1312
- if (realized.realizedRawUsdc < selected.amountRawUsdc) {
1313
- throw new StandardX402PayError(
1314
- "realize_underfunded",
1315
- "The confirmed withdrawal did not cover the exact API price. No external payment was attempted; reconcile the recorded withdrawal before retrying.",
1316
- { pendingPayment: pendingRecord }
1420
+ return session;
1421
+ }
1422
+ /** Polls a setup session (public capability URL — no auth needed). */
1423
+ async getSetupSession(sessionId) {
1424
+ const url2 = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
1425
+ const response = await this.fetchImpl(url2);
1426
+ const text = await response.text();
1427
+ if (response.status !== 200) {
1428
+ const parsed = parseRelayerError(text);
1429
+ throw new VaultFlowClientError(
1430
+ "read",
1431
+ parsed.message ?? `setup session read failed with ${response.status}`,
1432
+ text,
1433
+ parsed.code,
1434
+ parsed.details
1317
1435
  );
1318
1436
  }
1319
- let response;
1437
+ return JSON.parse(text);
1438
+ }
1439
+ /**
1440
+ * Finds an APPROVED, unconsumed deposit approval bound to exactly this
1441
+ * amount — the shape the mandate's initialDeposit approval has.
1442
+ */
1443
+ async findApprovedDepositApproval(amountRawUsdc) {
1320
1444
  try {
1321
- response = await this.x402Fetch(input.url, init, selected);
1322
- } catch (error) {
1323
- const persistError = this.tryMarkUnknown(pendingKey, {
1324
- message: error instanceof Error ? error.message : String(error)
1445
+ const approvals = await this.listApprovals("approved");
1446
+ const match = approvals.find((approval) => {
1447
+ const binding = approval.binding;
1448
+ return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
1325
1449
  });
1326
- throw new StandardX402PayError(
1327
- "payment_outcome_unknown",
1328
- `the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
1329
- { error, persistError }
1330
- );
1450
+ return match?.approvalId;
1451
+ } catch {
1452
+ return void 0;
1331
1453
  }
1332
- const bodyText = await response.text();
1333
- if (response.status !== 200) {
1334
- const persistError = this.tryMarkUnknown(pendingKey, {
1335
- status: response.status,
1336
- body: bodyText
1454
+ }
1455
+ /**
1456
+ * Polls the reconciling GET endpoint until the intent leaves "submitted"
1457
+ * (each read looks the tx up on-chain) or the timeout elapses.
1458
+ */
1459
+ async pollUntilTerminal(path, last) {
1460
+ const deadline = Date.now() + this.pollTimeoutMs;
1461
+ let latest = last;
1462
+ while (Date.now() < deadline) {
1463
+ await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
1464
+ const url2 = `${this.baseUrl}${path}`;
1465
+ const response = await this.fetchImpl(url2, {
1466
+ headers: await walletAuthHeaders({
1467
+ signer: this.signer,
1468
+ method: "GET",
1469
+ url: url2
1470
+ })
1337
1471
  });
1338
- throw new StandardX402PayError(
1339
- "payment_outcome_unknown",
1340
- `the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
1341
- { status: response.status, body: bodyText, persistError }
1342
- );
1343
- }
1344
- this.clearDelivered(pendingKey);
1345
- const paymentTxSignature = extractSettledPaymentTxSignature(response);
1346
- if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && realizer.reportPayment !== void 0) {
1472
+ if (response.status !== 200) {
1473
+ continue;
1474
+ }
1347
1475
  try {
1348
- await realizer.reportPayment({
1349
- withdrawalId: realized.withdrawalId,
1350
- paymentTxSignature
1351
- });
1352
- } catch (error) {
1353
- console.error(
1354
- `[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
1355
- );
1476
+ latest = await response.json();
1477
+ } catch {
1478
+ continue;
1356
1479
  }
1357
- }
1358
- return {
1359
- paid: true,
1360
- ...realizer.vault === void 0 ? {} : { fundingVault: realizer.vault },
1361
- status: response.status,
1362
- body: bodyText,
1363
- payment: {
1364
- amountRawUsdc: selected.amountRawUsdc.toString(),
1365
- payTo: selected.payTo,
1366
- feePayer: selected.feePayer,
1367
- realizedRawUsdc: realized.realizedRawUsdc.toString(),
1368
- realizeTxSignature: realized.txSignature,
1369
- paymentTxSignature
1480
+ if (latest.status !== "submitted") {
1481
+ return latest;
1370
1482
  }
1371
- };
1483
+ }
1484
+ return latest;
1372
1485
  }
1373
- /** Reads the challenge from the header (preferred) or the JSON body. */
1374
- async selectRequirement(probe) {
1375
- const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
1376
- let requirements;
1377
- try {
1378
- if (header !== null) {
1379
- requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
1380
- } else {
1381
- requirements = parseStandardChallenge(
1382
- await probe.json()
1383
- ).solanaExactRequirements;
1384
- }
1385
- } catch (error) {
1386
- throw new StandardX402PayError(
1387
- error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
1388
- "could not parse the x402 402 challenge",
1389
- error
1486
+ async postJson(step, path, body2) {
1487
+ const url2 = `${this.baseUrl}${path}`;
1488
+ const serialized = JSON.stringify(body2);
1489
+ const response = await this.fetchImpl(url2, {
1490
+ method: "POST",
1491
+ headers: {
1492
+ ...await walletAuthHeaders({
1493
+ signer: this.signer,
1494
+ method: "POST",
1495
+ url: url2,
1496
+ body: serialized
1497
+ }),
1498
+ "content-type": "application/json"
1499
+ },
1500
+ body: serialized
1501
+ });
1502
+ const text = await response.text();
1503
+ if (response.status !== 200) {
1504
+ const parsed = parseRelayerError(text);
1505
+ throw new VaultFlowClientError(
1506
+ step,
1507
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1508
+ text,
1509
+ parsed.code,
1510
+ parsed.details
1390
1511
  );
1391
1512
  }
1392
1513
  try {
1393
- return selectPayableSolanaRequirement(requirements, {
1394
- network: this.network,
1395
- usdcMint: this.usdcMint
1396
- });
1397
- } catch (error) {
1398
- throw new StandardX402PayError(
1399
- "no_payable_requirement",
1400
- error instanceof Error ? error.message : String(error),
1401
- error
1514
+ return JSON.parse(text);
1515
+ } catch {
1516
+ throw new VaultFlowClientError(
1517
+ step,
1518
+ `${path} returned 200 with a non-JSON body`,
1519
+ text
1402
1520
  );
1403
1521
  }
1404
1522
  }
1405
- track(record) {
1406
- const previous = this.pending.get(record.key);
1407
- this.pending.set(record.key, record);
1523
+ async getJson(path) {
1524
+ const url2 = `${this.baseUrl}${path}`;
1525
+ const response = await this.fetchImpl(url2, {
1526
+ headers: await walletAuthHeaders({
1527
+ signer: this.signer,
1528
+ method: "GET",
1529
+ url: url2
1530
+ })
1531
+ });
1532
+ const text = await response.text();
1533
+ if (response.status !== 200) {
1534
+ const parsed = parseRelayerError(text);
1535
+ throw new VaultFlowClientError(
1536
+ "read",
1537
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1538
+ text,
1539
+ parsed.code,
1540
+ parsed.details
1541
+ );
1542
+ }
1408
1543
  try {
1409
- this.persist();
1410
- } catch (error) {
1411
- if (previous === void 0) {
1412
- this.pending.delete(record.key);
1413
- } else {
1414
- this.pending.set(record.key, previous);
1415
- }
1416
- throw error;
1544
+ return JSON.parse(text);
1545
+ } catch {
1546
+ throw new VaultFlowClientError(
1547
+ "read",
1548
+ `${path} returned 200 with a non-JSON body`,
1549
+ text
1550
+ );
1417
1551
  }
1418
1552
  }
1419
- markUnknown(key, detail) {
1420
- const current = this.pending.get(key);
1421
- if (current === void 0) {
1422
- return;
1423
- }
1424
- const next = {
1425
- ...current,
1426
- status: "external_outcome_unknown",
1427
- updatedAtMs: this.nowMs(),
1428
- detail
1553
+ };
1554
+ function parseRelayerError(text) {
1555
+ try {
1556
+ const parsed = JSON.parse(text);
1557
+ return {
1558
+ code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
1559
+ message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
1560
+ details: parsed.error?.details ?? null
1429
1561
  };
1430
- this.pending.set(key, next);
1431
- try {
1432
- this.persist();
1433
- } catch (error) {
1434
- this.pending.set(key, current);
1435
- throw error;
1436
- }
1562
+ } catch {
1563
+ return { code: null, message: null, details: null };
1437
1564
  }
1438
- tryMarkUnknown(key, detail) {
1565
+ }
1566
+
1567
+ // ../../src/client/relayer-yield-realizer.ts
1568
+ var REALIZE_OVERHEAD_RAW_USDC = 2500n;
1569
+ var RelayerRealizeError = class extends Error {
1570
+ constructor(code, message, detail = null, realizationSafeToRetry = false) {
1571
+ super(message);
1572
+ this.code = code;
1573
+ this.detail = detail;
1574
+ this.realizationSafeToRetry = realizationSafeToRetry;
1575
+ this.name = "RelayerRealizeError";
1576
+ }
1577
+ code;
1578
+ detail;
1579
+ realizationSafeToRetry;
1580
+ };
1581
+ var RelayerYieldRealizer = class {
1582
+ get vault() {
1583
+ return this.vaultFlows.vault.address;
1584
+ }
1585
+ realizationContext;
1586
+ vaultFlows;
1587
+ constructor(config) {
1588
+ this.vaultFlows = new VaultFlowClient({
1589
+ relayerBaseUrl: config.relayerBaseUrl,
1590
+ signer: config.signer,
1591
+ rpc: config.rpc,
1592
+ ...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
1593
+ ...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
1594
+ });
1595
+ this.realizationContext = Object.freeze({
1596
+ wallet: config.signer.walletAddress,
1597
+ vault: this.vaultFlows.vault.address,
1598
+ relayerBaseUrl: config.relayerBaseUrl.replace(/\/$/, "")
1599
+ });
1600
+ }
1601
+ async ensureUsdcAvailable(input) {
1602
+ const shortfallRawUsdc = input.amountRawUsdc;
1603
+ await this.assertSpendableYield(shortfallRawUsdc);
1604
+ let outcome;
1605
+ let submissionPossible = false;
1439
1606
  try {
1440
- this.markUnknown(key, detail);
1441
- return null;
1607
+ outcome = await this.vaultFlows.withdraw({
1608
+ amountRawUsdc: shortfallRawUsdc,
1609
+ // The relayer refuses to prepare this withdrawal beyond the spendable
1610
+ // yield — the principal-protection guard the client cannot bypass.
1611
+ purpose: "yield_realize",
1612
+ // Declares what is being paid so the relayer's spending-mandate layer
1613
+ // can enforce caps/payee and keep the mandate → payment audit chain.
1614
+ ...input.payment === void 0 ? {} : { payment: input.payment },
1615
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId },
1616
+ ...input.onPrepared === void 0 ? {} : { onPrepared: input.onPrepared },
1617
+ onBeforeSubmit: () => {
1618
+ submissionPossible = true;
1619
+ }
1620
+ });
1442
1621
  } catch (error) {
1443
- return error;
1622
+ if (error instanceof StandardX402PayError) throw error;
1623
+ throw this.mapWithdrawError(error, !submissionPossible);
1444
1624
  }
1625
+ return this.confirmedRealization(outcome);
1445
1626
  }
1446
- untrack(key) {
1447
- const previous = this.pending.get(key);
1448
- const existed = previous !== void 0;
1449
- this.pending.delete(key);
1450
- try {
1451
- this.persist();
1452
- } catch (error) {
1453
- if (existed) {
1454
- this.pending.set(key, previous);
1455
- }
1456
- throw error;
1627
+ async resumeUsdcAvailable(input) {
1628
+ const outcome = await this.vaultFlows.resumeWithdrawal(input.prepared, {
1629
+ amountRawUsdc: input.amountRawUsdc,
1630
+ purpose: "yield_realize",
1631
+ payment: input.payment
1632
+ });
1633
+ return this.confirmedRealization(outcome);
1634
+ }
1635
+ confirmedRealization(outcome) {
1636
+ if (outcome.status !== "confirmed" || outcome.txSignature === null) {
1637
+ throw new RelayerRealizeError(
1638
+ "realize_not_confirmed",
1639
+ `yield realize withdrawal did not confirm (status=${outcome.status})`,
1640
+ outcome
1641
+ );
1457
1642
  }
1643
+ return {
1644
+ realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
1645
+ txSignature: outcome.txSignature,
1646
+ withdrawalId: outcome.withdrawalId
1647
+ };
1458
1648
  }
1459
- clearDelivered(key) {
1460
- const previous = this.pending.get(key);
1461
- this.pending.delete(key);
1649
+ /**
1650
+ * Best-effort report-back of the x402 payment tx this realize funded —
1651
+ * closes the relayer's mandate → realize → payment audit chain. Callers
1652
+ * must never let a failure here affect the payment result.
1653
+ */
1654
+ async reportPayment(input) {
1655
+ await this.vaultFlows.reportPayment(input);
1656
+ }
1657
+ /**
1658
+ * Refuses to realize more than the ledger's spendable yield (principal).
1659
+ * getBudget syncs the relayer's ledger from chain first (best-effort), so a
1660
+ * long-running client sees yield as it accrues instead of a frozen view.
1661
+ */
1662
+ async assertSpendableYield(shortfallRawUsdc) {
1663
+ let spendable;
1462
1664
  try {
1463
- this.persist();
1665
+ const budget = await this.vaultFlows.getBudget();
1666
+ spendable = BigInt(budget.spendableYieldRawUsdc);
1464
1667
  } catch (error) {
1465
- if (previous !== void 0) {
1466
- this.pending.set(key, previous);
1467
- }
1468
- console.error(
1469
- `[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
1668
+ throw new RelayerRealizeError(
1669
+ "budget_unavailable",
1670
+ "could not read the spendable-yield budget",
1671
+ error,
1672
+ true
1673
+ );
1674
+ }
1675
+ const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
1676
+ if (spendable < requiredRawUsdc) {
1677
+ throw new RelayerRealizeError(
1678
+ "insufficient_yield",
1679
+ `spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC plus the ${REALIZE_OVERHEAD_RAW_USDC} raw fee headroom; the principal is never spent \u2014 wait for more yield`,
1680
+ { spendableYieldRawUsdc: spendable.toString() },
1681
+ true
1470
1682
  );
1471
1683
  }
1472
1684
  }
1473
- persist() {
1474
- if (this.stateStore === null) {
1475
- return;
1685
+ mapWithdrawError(error, safeToRetry) {
1686
+ if (!(error instanceof VaultFlowClientError)) {
1687
+ return new RelayerRealizeError(
1688
+ "prepare_failed",
1689
+ `yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
1690
+ error,
1691
+ safeToRetry
1692
+ );
1476
1693
  }
1477
- this.stateStore.save([...this.pending.values()]);
1694
+ const serverCode = error.code ?? errorCodeFrom(error.detail);
1695
+ if (serverCode === "approval_required") {
1696
+ return new RelayerRealizeError(
1697
+ "approval_required",
1698
+ "this payment exceeds the owner-approval threshold; nothing was realized or paid. Ask the owner to approve, then retry with the approvalId",
1699
+ error.errorDetails ?? error.detail,
1700
+ safeToRetry
1701
+ );
1702
+ }
1703
+ if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
1704
+ return new RelayerRealizeError(
1705
+ "insufficient_yield",
1706
+ "the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
1707
+ error.detail,
1708
+ safeToRetry
1709
+ );
1710
+ }
1711
+ return new RelayerRealizeError(
1712
+ error.step === "submit" ? "submit_failed" : "prepare_failed",
1713
+ error.message,
1714
+ error.detail,
1715
+ safeToRetry
1716
+ );
1478
1717
  }
1479
1718
  };
1480
- function pendingPaymentKey(input) {
1481
- return `${input.method}:${input.url}:${input.requestBodyHash}`;
1482
- }
1483
- function extractSettledPaymentTxSignature(response) {
1484
- const header = response.headers.get(PAYMENT_RESPONSE_HEADER) ?? response.headers.get("x-payment-response");
1485
- if (header === null || header.length === 0) {
1719
+ function errorCodeFrom(detail) {
1720
+ if (typeof detail !== "string") {
1486
1721
  return null;
1487
1722
  }
1488
1723
  try {
1489
- const decoded = JSON.parse(
1490
- Buffer.from(header, "base64").toString("utf8")
1491
- );
1492
- if (typeof decoded.transaction === "string" && decoded.transaction.length > 0) {
1493
- return decoded.transaction;
1494
- }
1495
- if (typeof decoded.txHash === "string" && decoded.txHash.length > 0) {
1496
- return decoded.txHash;
1497
- }
1498
- return null;
1724
+ const parsed = JSON.parse(detail);
1725
+ return typeof parsed.error?.code === "string" ? parsed.error.code : null;
1499
1726
  } catch {
1500
1727
  return null;
1501
1728
  }
@@ -2799,13 +3026,13 @@ async function createCircleSignerTransport(config) {
2799
3026
  import { createPrivateKey, createSign } from "node:crypto";
2800
3027
  var PROVIDER2 = "privy";
2801
3028
  var DEFAULT_BASE_URL2 = "https://api.privy.io";
2802
- function canonicalJson(value) {
3029
+ function canonicalJson2(value) {
2803
3030
  if (Array.isArray(value)) {
2804
- return `[${value.map(canonicalJson).join(",")}]`;
3031
+ return `[${value.map(canonicalJson2).join(",")}]`;
2805
3032
  }
2806
3033
  if (value !== null && typeof value === "object") {
2807
3034
  const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
2808
- return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
3035
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson2(v)}`).join(",")}}`;
2809
3036
  }
2810
3037
  return JSON.stringify(value);
2811
3038
  }
@@ -2834,7 +3061,7 @@ function authorizationSignature(params) {
2834
3061
  headers: { "privy-app-id": params.appId }
2835
3062
  };
2836
3063
  const signer2 = createSign("sha256");
2837
- signer2.update(canonicalJson(payload));
3064
+ signer2.update(canonicalJson2(payload));
2838
3065
  return signer2.sign(params.key).toString("base64");
2839
3066
  }
2840
3067
  async function createPrivySignerTransport(config) {
@@ -3009,7 +3236,7 @@ async function agentWalletSignerFromEnv(env = process.env) {
3009
3236
  }
3010
3237
 
3011
3238
  // ../../src/client/standard-x402-state-store.ts
3012
- import { closeSync, mkdirSync, openSync, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "node:fs";
3239
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "node:fs";
3013
3240
  import { basename, dirname, join } from "node:path";
3014
3241
  function fileStandardX402StateStore(path) {
3015
3242
  return {
@@ -3054,6 +3281,9 @@ function fileStandardX402StateStore(path) {
3054
3281
  );
3055
3282
  }
3056
3283
  }
3284
+ if (new Set(parsed.map((record) => record.key)).size !== parsed.length) {
3285
+ throw new Error(`pending payment state has duplicate request keys: ${path}`);
3286
+ }
3057
3287
  return parsed;
3058
3288
  },
3059
3289
  save(records) {
@@ -3063,8 +3293,29 @@ function fileStandardX402StateStore(path) {
3063
3293
  directory,
3064
3294
  `.${basename(path)}.${process.pid}.${Date.now()}.tmp`
3065
3295
  );
3066
- writeFileSync(tempPath, JSON.stringify(records, null, 2), { mode: 384, flag: "wx" });
3067
- renameSync(tempPath, path);
3296
+ try {
3297
+ const fd = openSync(tempPath, "wx", 384);
3298
+ try {
3299
+ writeFileSync(fd, JSON.stringify(records, null, 2));
3300
+ fsyncSync(fd);
3301
+ } finally {
3302
+ closeSync(fd);
3303
+ }
3304
+ renameSync(tempPath, path);
3305
+ if (process.platform !== "win32") {
3306
+ const directoryFd = openSync(directory, "r");
3307
+ try {
3308
+ fsyncSync(directoryFd);
3309
+ } finally {
3310
+ closeSync(directoryFd);
3311
+ }
3312
+ }
3313
+ } finally {
3314
+ try {
3315
+ unlinkSync(tempPath);
3316
+ } catch {
3317
+ }
3318
+ }
3068
3319
  }
3069
3320
  };
3070
3321
  }
@@ -3076,7 +3327,20 @@ function isPendingPaymentRecord(value) {
3076
3327
  return false;
3077
3328
  }
3078
3329
  const record = value;
3079
- return typeof record.key === "string" && typeof record.url === "string" && typeof record.method === "string" && typeof record.requestBodyHash === "string" && typeof record.amountRawUsdc === "string" && typeof record.payTo === "string" && (record.feePayer === null || typeof record.feePayer === "string") && typeof record.realizedRawUsdc === "string" && (record.realizeTxSignature === null || typeof record.realizeTxSignature === "string") && (record.status === "realized" || record.status === "external_outcome_unknown") && typeof record.createdAtMs === "number" && typeof record.updatedAtMs === "number";
3330
+ return typeof record.key === "string" && typeof record.url === "string" && typeof record.method === "string" && typeof record.requestBodyHash === "string" && typeof record.amountRawUsdc === "string" && typeof record.payTo === "string" && (record.feePayer === null || typeof record.feePayer === "string") && typeof record.realizedRawUsdc === "string" && (record.realizeTxSignature === null || typeof record.realizeTxSignature === "string") && (record.status === "realizing" || record.status === "realized" || record.status === "external_outcome_unknown") && (record.recovery === void 0 || isRecoveryRecord(record.recovery) && record.key === `${record.method}:${record.url}:${record.requestBodyHash}` && /^\d+$/.test(String(record.realizedRawUsdc)) && /^\d+$/.test(String(record.amountRawUsdc))) && typeof record.createdAtMs === "number" && typeof record.updatedAtMs === "number";
3331
+ }
3332
+ function isObject(value) {
3333
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3334
+ }
3335
+ function isRecoveryRecord(value) {
3336
+ if (!isObject(value) || value.version !== 1 || !isObject(value.context)) return false;
3337
+ const context = value.context;
3338
+ if (!["wallet", "vault", "relayerBaseUrl"].every((key) => typeof context[key] === "string" && context[key] !== "") || typeof value.requestHeadersHash !== "string" || !/^[0-9a-f]{64}$/.test(value.requestHeadersHash) || !standardExactRequirementSchema.safeParse(value.requirement).success) return false;
3339
+ if (value.prepared === void 0) return true;
3340
+ const prepared = value.prepared;
3341
+ if (!isObject(prepared) || !isObject(prepared.signingIntent) || prepared.purpose !== "yield_realize" || !["withdrawalId", "serializedTransaction", "destinationUsdcAta", "requestedWithdrawRawUsdc"].every((key) => typeof prepared[key] === "string" && prepared[key] !== "")) return false;
3342
+ const intent = prepared.signingIntent;
3343
+ return intent.allowFullExit === false && ["wallet", "vault", "farm", "shareMint", "asset", "destinationUsdcAta", "maxSharesToRedeemRaw", "feePayer", "expiresAt", "preparedMessageHash"].every((key) => typeof intent[key] === "string" && intent[key] !== "");
3080
3344
  }
3081
3345
 
3082
3346
  // ../../src/solana/rpc.ts
@@ -3153,6 +3417,13 @@ function selectExpectedRequirement(requirements, expected) {
3153
3417
  return match;
3154
3418
  }
3155
3419
 
3420
+ // ../../demo/cli-command.ts
3421
+ import { readFileSync as readFileSync4 } from "node:fs";
3422
+ var { version } = JSON.parse(
3423
+ readFileSync4(new URL("../package.json", import.meta.url), "utf8")
3424
+ );
3425
+ var PAY_COMMAND = `npx -y @subly_fi/pay@${version}`;
3426
+
3156
3427
  // src/pay.ts
3157
3428
  function fail(message) {
3158
3429
  process.stderr.write(`${message}
@@ -3161,7 +3432,7 @@ function fail(message) {
3161
3432
  }
3162
3433
  var url = process.argv[2];
3163
3434
  if (url === void 0 || !/^https?:\/\//.test(url)) {
3164
- fail("Usage: pay fetch <url> [maxAmountRawUsdc] [apr_<approvalId>]");
3435
+ fail(`Usage: ${PAY_COMMAND} fetch <url> [maxAmountRawUsdc] [apr_<approvalId>]`);
3165
3436
  }
3166
3437
  var maxAmountArg;
3167
3438
  var approvalId;
@@ -3172,7 +3443,7 @@ for (const arg of process.argv.slice(3)) {
3172
3443
  maxAmountArg = arg;
3173
3444
  } else {
3174
3445
  fail(`unrecognized argument: ${arg}
3175
- Usage: pay fetch <url> [maxAmountRawUsdc] [apr_<approvalId>]`);
3446
+ Usage: ${PAY_COMMAND} fetch <url> [maxAmountRawUsdc] [apr_<approvalId>]`);
3176
3447
  }
3177
3448
  }
3178
3449
  var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
@@ -3228,7 +3499,7 @@ try {
3228
3499
  message: error.message,
3229
3500
  detail: error.detail ?? null,
3230
3501
  ...refusalApprovalId === void 0 ? {} : {
3231
- retry: `pay fetch "${url}"${maxAmountArg === void 0 ? "" : ` ${maxAmountArg}`} ${refusalApprovalId}`
3502
+ retry: `${PAY_COMMAND} fetch "${url}"${maxAmountArg === void 0 ? "" : ` ${maxAmountArg}`} ${refusalApprovalId}`
3232
3503
  }
3233
3504
  },
3234
3505
  null,