@subly_fi/pay 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -182,1389 +182,1616 @@ var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
182
182
  var SUBLY_VAULT = defaultCatalogVault(vaultCatalogFromEnv());
183
183
  var USDC_DECIMALS = 6;
184
184
 
185
- // ../../src/lib/associated-token-account.ts
185
+ // ../../src/lib/canonical-json.ts
186
+ import { createHash as createHash2 } from "node:crypto";
187
+
188
+ // ../../src/lib/hash.ts
186
189
  import { createHash } from "node:crypto";
187
- import bs58 from "bs58";
188
- var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
189
- var ED25519_P = (1n << 255n) - 19n;
190
- var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
191
- function deriveAssociatedTokenAddress(params) {
192
- const owner = decodePublicKey(params.owner, "owner");
193
- const mint = decodePublicKey(params.mint ?? SUBLY_VAULT.usdcMint, "mint");
194
- const tokenProgramId = decodePublicKey(
195
- params.tokenProgramId ?? SPL_TOKEN_PROGRAM_ID,
196
- "tokenProgramId"
197
- );
198
- const associatedTokenProgramId = decodePublicKey(
199
- ASSOCIATED_TOKEN_PROGRAM_ID,
200
- "associatedTokenProgramId"
201
- );
202
- for (let bump = 255; bump >= 0; bump -= 1) {
203
- const address3 = createProgramAddress(
204
- [owner, tokenProgramId, mint, Uint8Array.of(bump)],
205
- associatedTokenProgramId
206
- );
207
- if (address3 !== null) {
208
- return bs58.encode(address3);
209
- }
210
- }
211
- throw new Error("Unable to derive associated token account address");
212
- }
213
- function createProgramAddress(seeds, programId) {
214
- const hash = createHash("sha256");
215
- for (const seed of seeds) {
216
- hash.update(seed);
217
- }
218
- hash.update(programId);
219
- hash.update(PDA_MARKER);
220
- const digest = hash.digest();
221
- return isEd25519Point(digest) ? null : new Uint8Array(digest);
190
+ var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
191
+ function sha256TaggedHex(data) {
192
+ return `sha256-${createHash("sha256").update(data).digest("hex")}`;
222
193
  }
223
- function decodePublicKey(value, fieldName) {
224
- const decoded = bs58.decode(value);
225
- if (decoded.length !== 32) {
226
- throw new Error(`${fieldName} must be a 32-byte public key`);
194
+ function stableStringify(value) {
195
+ if (value === null) {
196
+ return "null";
227
197
  }
228
- return decoded;
229
- }
230
- function isEd25519Point(bytes) {
231
- if (bytes.length !== 32) {
232
- return false;
198
+ if (typeof value === "bigint") {
199
+ return JSON.stringify(value.toString());
233
200
  }
234
- const yBytes = Uint8Array.from(bytes);
235
- yBytes[31] = yBytes[31] & 127;
236
- const y = littleEndianToBigInt(yBytes);
237
- if (y >= ED25519_P) {
238
- return false;
201
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
202
+ return JSON.stringify(value);
239
203
  }
240
- const ySquared = mod(y * y, ED25519_P);
241
- const numerator = mod(ySquared - 1n, ED25519_P);
242
- const denominator = mod(ED25519_D * ySquared + 1n, ED25519_P);
243
- if (denominator === 0n) {
244
- return false;
204
+ if (Array.isArray(value)) {
205
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
245
206
  }
246
- const xSquared = mod(
247
- numerator * modPow(denominator, ED25519_P - 2n, ED25519_P),
248
- ED25519_P
249
- );
250
- return xSquared === 0n || modPow(xSquared, (ED25519_P - 1n) / 2n, ED25519_P) === 1n;
207
+ const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
208
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
251
209
  }
252
- function littleEndianToBigInt(bytes) {
253
- let value = 0n;
254
- for (let index = bytes.length - 1; index >= 0; index -= 1) {
255
- value = (value << 8n) + BigInt(bytes[index]);
256
- }
257
- return value;
210
+ function hashStableJson(value) {
211
+ return sha256TaggedHex(stableStringify(value));
258
212
  }
259
- function mod(value, modulus) {
260
- const result = value % modulus;
261
- return result >= 0n ? result : result + modulus;
213
+
214
+ // ../../src/lib/canonical-json.ts
215
+ function canonicalJson(value) {
216
+ return stableStringify(value);
262
217
  }
263
- function modPow(base, exponent, modulus) {
264
- let result = 1n;
265
- let nextBase = mod(base, modulus);
266
- let nextExponent = exponent;
267
- while (nextExponent > 0n) {
268
- if ((nextExponent & 1n) === 1n) {
269
- result = mod(result * nextBase, modulus);
270
- }
271
- nextBase = mod(nextBase * nextBase, modulus);
272
- nextExponent >>= 1n;
273
- }
274
- return result;
218
+ function sha256HexOf(data) {
219
+ return createHash2("sha256").update(data, "utf8").digest("hex");
220
+ }
221
+ function canonicalJsonHash(value) {
222
+ return sha256HexOf(canonicalJson(value));
275
223
  }
276
224
 
277
- // ../../src/domain/withdrawal-rounding.ts
278
- var WITHDRAWAL_ROUNDING_RAW_USDC = 10n;
279
- var YIELD_REALIZE_ROUNDING_RAW_USDC = WITHDRAWAL_ROUNDING_RAW_USDC / 2n;
280
-
281
- // ../../src/client/withdrawal-preview.ts
282
- async function assertWithdrawalPreview(input) {
283
- const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
284
- const simulation = await input.rpc.simulateTransaction(
285
- input.serializedTransaction,
286
- {
287
- encoding: "base64",
288
- commitment: "confirmed",
289
- sigVerify: false,
290
- replaceRecentBlockhash: false,
291
- innerInstructions: true
292
- }
293
- ).send({ abortSignal: AbortSignal.timeout(15e3) });
294
- if (simulation.value.err !== null) {
295
- throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
296
- }
297
- let received = 0n;
298
- for (const group of simulation.value.innerInstructions ?? []) {
299
- for (const instruction of group.instructions) {
300
- if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
301
- const parsed = instruction.parsed;
302
- if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
303
- const info = parsed.info;
304
- if (!info || info.destination !== destination && info.source !== destination) continue;
305
- const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
306
- if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
307
- throw new Error("Withdrawal preview returned an invalid token amount");
308
- }
309
- const amount = BigInt(raw);
310
- if (info.destination === destination) received += amount;
311
- if (info.source === destination) received -= amount;
312
- }
225
+ // ../../src/x402/headers.ts
226
+ import { z as z2 } from "zod";
227
+ var PAYMENT_REQUIRED_HEADER = "payment-required";
228
+ var PAYMENT_RESPONSE_HEADER = "payment-response";
229
+ var MAX_HEADER_JSON_BYTES = 16384;
230
+ var X402HeaderError = class extends Error {
231
+ reason;
232
+ constructor(reason, message) {
233
+ super(message);
234
+ this.name = "X402HeaderError";
235
+ this.reason = reason;
313
236
  }
314
- const minimum = input.purpose === "yield_realize" ? input.amountRawUsdc : input.amountRawUsdc - WITHDRAWAL_ROUNDING_RAW_USDC;
315
- if (received <= 0n || received > input.amountRawUsdc + WITHDRAWAL_ROUNDING_RAW_USDC || received < minimum) {
316
- throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
237
+ };
238
+ var sublyPaymentRequirementsSchema = z2.object({
239
+ scheme: z2.literal(PAYMENT_SCHEME),
240
+ network: z2.string().min(1),
241
+ asset: z2.string().min(32),
242
+ /** Exact seller amount in raw USDC; the scheme settles exactly this. */
243
+ amountRawUsdc: z2.string().regex(/^[1-9]\d*$/),
244
+ resource: z2.string().url(),
245
+ description: z2.string().optional(),
246
+ mimeType: z2.string().optional(),
247
+ payTo: z2.string().min(32),
248
+ maxTimeoutSeconds: z2.number().int().positive(),
249
+ extra: z2.object({
250
+ sellerRequestId: z2.string().min(1),
251
+ seller: z2.string().min(32),
252
+ sellerUsdcAta: z2.string().min(32),
253
+ vault: z2.string().min(32),
254
+ shareMint: z2.string().min(32)
255
+ })
256
+ }).loose();
257
+ var paymentRequiredSchema = z2.object({
258
+ x402Version: z2.number().int(),
259
+ accepts: z2.array(z2.unknown()),
260
+ error: z2.string().optional()
261
+ }).loose();
262
+ var sublyPaymentPayloadSchema = z2.object({
263
+ x402Version: z2.number().int(),
264
+ scheme: z2.literal(PAYMENT_SCHEME),
265
+ network: z2.string().min(1),
266
+ payload: z2.object({
267
+ paymentId: z2.string().min(1),
268
+ requestBindingHash: z2.string().min(1),
269
+ preparedMessageHash: z2.string().min(1),
270
+ serializedTransaction: z2.string().min(1).max(4096),
271
+ agentSignature: z2.string().min(1).max(128),
272
+ temporarySettlementSignature: z2.string().min(1).max(128)
273
+ })
274
+ }).loose();
275
+ function decodeX402Header(headerValue) {
276
+ if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
277
+ throw new X402HeaderError(
278
+ "header_too_large",
279
+ `x402 header exceeds ${MAX_HEADER_JSON_BYTES} encoded bytes`
280
+ );
317
281
  }
318
- }
319
-
320
- // ../../src/client/lookup-tables.ts
321
- import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
322
- import { address, getCompiledTransactionMessageDecoder } from "@solana/kit";
323
- function lookupTableAddressesForTransaction(serializedTransaction) {
324
- const wire = Buffer.from(serializedTransaction, "base64");
325
- let offset = 0;
326
- let signatureCount = 0;
327
- let shift = 0;
328
- while (offset < wire.length) {
329
- const byte = wire[offset];
330
- signatureCount |= (byte & 127) << shift;
331
- offset += 1;
332
- if ((byte & 128) === 0) {
333
- break;
334
- }
335
- shift += 7;
282
+ const json = Buffer.from(headerValue, "base64").toString("utf8");
283
+ try {
284
+ return JSON.parse(json);
285
+ } catch {
286
+ throw new X402HeaderError(
287
+ "invalid_header_encoding",
288
+ "x402 header is not base64-encoded JSON"
289
+ );
336
290
  }
337
- const messageBytes = wire.subarray(offset + signatureCount * 64);
338
- const compiled = getCompiledTransactionMessageDecoder().decode(messageBytes);
339
- const lookups = compiled.addressTableLookups ?? [];
340
- return lookups.map((lookup) => String(lookup.lookupTableAddress));
341
291
  }
342
- async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
343
- const addresses = lookupTableAddressesForTransaction(serializedTransaction);
344
- if (addresses.length === 0) {
345
- return {};
292
+ function requestBodyHashFor(body) {
293
+ if (body === null || body === void 0 || body.length === 0) {
294
+ return EMPTY_BODY_HASH;
346
295
  }
347
- const tables = await fetchAllMaybeAddressLookupTable(
348
- rpc2,
349
- addresses.map((value) => address(value))
296
+ return sha256TaggedHex(
297
+ typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
350
298
  );
351
- const result = {};
352
- for (const table of tables) {
353
- if (table.exists) {
354
- result[table.address] = table.data.addresses.map(String);
355
- }
356
- }
357
- return result;
358
299
  }
359
300
 
360
- // ../../src/api/wallet-auth.ts
361
- import { createHash as createHash2 } from "node:crypto";
362
- import bs582 from "bs58";
363
- import nacl from "tweetnacl";
364
- var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
365
- var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
366
- var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
367
- function sha256Hex(data) {
368
- return createHash2("sha256").update(data, "utf8").digest("hex");
301
+ // ../../src/x402/standard-requirements.ts
302
+ import { z as z3 } from "zod";
303
+ var STANDARD_EXACT_SCHEME = "exact";
304
+ var standardExactRequirementSchema = z3.object({
305
+ scheme: z3.literal(STANDARD_EXACT_SCHEME),
306
+ /** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
307
+ network: z3.string().min(1),
308
+ /** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
309
+ asset: z3.string().min(1),
310
+ /** Exact price in the asset's atomic units, as a decimal string. */
311
+ amount: z3.string().regex(/^[1-9]\d*$/),
312
+ /** Recipient wallet; the transfer destination ATA is derived from it. */
313
+ payTo: z3.string().min(1),
314
+ maxTimeoutSeconds: z3.number().int().positive().optional(),
315
+ extra: z3.object({
316
+ /** Facilitator address that pays the tx fee (gas sponsorship). */
317
+ feePayer: z3.string().min(1).optional()
318
+ }).loose().optional()
319
+ }).loose();
320
+ var standardPaymentRequiredSchema = z3.object({
321
+ x402Version: z3.number().int(),
322
+ accepts: z3.array(z3.unknown()),
323
+ error: z3.string().optional(),
324
+ resource: z3.object({ url: z3.string().optional() }).loose().optional()
325
+ }).loose();
326
+ var StandardX402ChallengeError = class extends Error {
327
+ reason;
328
+ constructor(reason, message) {
329
+ super(message);
330
+ this.name = "StandardX402ChallengeError";
331
+ this.reason = reason;
332
+ }
333
+ };
334
+ function parseStandardChallenge(challenge) {
335
+ const parsed = standardPaymentRequiredSchema.safeParse(challenge);
336
+ if (!parsed.success) {
337
+ throw new StandardX402ChallengeError(
338
+ "invalid_payment_required",
339
+ "Response is not a valid x402 PaymentRequired object"
340
+ );
341
+ }
342
+ const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
343
+ const requirement = standardExactRequirementSchema.safeParse(candidate);
344
+ if (!requirement.success) {
345
+ return [];
346
+ }
347
+ return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
348
+ });
349
+ return { paymentRequired: parsed.data, solanaExactRequirements };
369
350
  }
370
- function walletAuthMessage(params) {
371
- return new TextEncoder().encode(
372
- `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
373
- params.rawBody
374
- )}:${params.signedAtMs}`
375
- );
351
+ function decodeStandardPaymentRequiredHeader(headerValue) {
352
+ let decoded;
353
+ try {
354
+ decoded = decodeX402Header(headerValue);
355
+ } catch (error) {
356
+ throw new StandardX402ChallengeError(
357
+ error instanceof X402HeaderError ? error.reason : "invalid_header",
358
+ "Cannot decode the payment-required header"
359
+ );
360
+ }
361
+ return parseStandardChallenge(decoded);
376
362
  }
377
-
378
- // ../../src/client/wallet-auth-headers.ts
379
- async function walletAuthHeaders(params) {
380
- const signedAtMs = String(Date.now());
381
- const message = walletAuthMessage({
382
- method: params.method,
383
- path: (() => {
384
- const url = new URL(params.url);
385
- return url.pathname + url.search;
386
- })(),
387
- rawBody: params.body ?? "",
388
- signedAtMs
389
- });
363
+ function selectPayableSolanaRequirement(requirements, options) {
364
+ const network = options?.network ?? SOLANA_MAINNET_NETWORK;
365
+ const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
366
+ const matchingRequirements = requirements.filter(
367
+ (candidate) => candidate.network === network && candidate.asset === usdcMint
368
+ );
369
+ if (matchingRequirements.length === 0) {
370
+ throw new StandardX402ChallengeError(
371
+ "no_payable_requirement",
372
+ `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
373
+ );
374
+ }
375
+ const requirement = matchingRequirements.find(
376
+ (candidate) => candidate.extra?.feePayer !== void 0
377
+ ) ?? null;
378
+ if (requirement === null) {
379
+ throw new StandardX402ChallengeError(
380
+ "missing_svm_fee_payer",
381
+ "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
382
+ );
383
+ }
384
+ const feePayer = requirement.extra?.feePayer;
385
+ if (feePayer === void 0) {
386
+ throw new StandardX402ChallengeError(
387
+ "missing_svm_fee_payer",
388
+ "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
389
+ );
390
+ }
390
391
  return {
391
- [WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
392
- [WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
393
- [WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
392
+ requirement,
393
+ amountRawUsdc: BigInt(requirement.amount),
394
+ payTo: requirement.payTo,
395
+ feePayer
394
396
  };
395
397
  }
398
+ function standardRequirementMatchesSelected(candidate, selected2) {
399
+ const parsed = standardExactRequirementSchema.safeParse(candidate);
400
+ return parsed.success && stableJson(parsed.data) === stableJson(selected2.requirement);
401
+ }
402
+ function stableJson(value) {
403
+ return JSON.stringify(sortJson(value));
404
+ }
405
+ function sortJson(value) {
406
+ if (Array.isArray(value)) {
407
+ return value.map(sortJson);
408
+ }
409
+ if (value !== null && typeof value === "object") {
410
+ return Object.fromEntries(
411
+ Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
412
+ );
413
+ }
414
+ return value;
415
+ }
396
416
 
397
- // ../../src/client/vault-flows.ts
398
- var VaultFlowClientError = class extends Error {
399
- constructor(step, message, detail = null, code = null, errorDetails = null) {
417
+ // ../../src/client/standard-x402-payer.ts
418
+ var StandardX402PayError = class extends Error {
419
+ constructor(reason, message, detail = null) {
400
420
  super(message);
401
- this.step = step;
421
+ this.reason = reason;
402
422
  this.detail = detail;
403
- this.code = code;
404
- this.errorDetails = errorDetails;
405
- this.name = "VaultFlowClientError";
423
+ this.name = "StandardX402PayError";
406
424
  }
407
- step;
425
+ reason;
408
426
  detail;
409
- code;
410
- errorDetails;
411
427
  };
412
- var VaultFlowClient = class {
413
- vault;
414
- rpc;
415
- baseUrl;
416
- signer;
417
- fetchImpl;
418
- lookupTablesFor;
419
- pollTimeoutMs;
420
- pollIntervalMs;
428
+ var StandardX402Payer = class {
429
+ realizer;
430
+ x402Fetch;
431
+ probeFetch;
432
+ defaultMaxAmountRawUsdc;
433
+ network;
434
+ usdcMint;
435
+ stateStore;
436
+ pending = /* @__PURE__ */ new Map();
437
+ inFlight = /* @__PURE__ */ new Map();
438
+ nowMs;
421
439
  constructor(config) {
422
- this.rpc = config.rpc;
423
- this.vault = config.vault ?? config.signer.vault ?? SUBLY_VAULT;
424
- if (config.signer.vault && config.signer.vault.address !== this.vault.address) {
425
- throw new Error("Vault flow client and signer must select the same vault");
426
- }
427
- this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
428
- this.signer = config.signer;
429
- this.fetchImpl = config.fetchImpl ?? fetch;
430
- this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
431
- this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
432
- this.pollIntervalMs = config.pollIntervalMs ?? 2500;
433
- }
434
- /**
435
- * Moves USDC from the agent wallet into the vault (fee sponsored). Under
436
- * depositPolicy "owner_approval_required" the relayer refuses to prepare
437
- * without an owner approval; when the caller passes none, an already
438
- * APPROVED deposit approval for this exact amount (e.g. the mandate's
439
- * initialDeposit — "one Face ID covers mandate + first deposit") is looked
440
- * up and used automatically before surfacing deposit_approval_required.
441
- */
442
- async deposit(input) {
443
- let approvalId = input.approvalId;
444
- let prepared;
445
- try {
446
- prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
447
- wallet: this.signer.walletAddress,
448
- vault: this.vault.address,
449
- amountRawUsdc: input.amountRawUsdc.toString(),
450
- ...approvalId === void 0 ? {} : { approvalId }
451
- });
452
- } catch (error) {
453
- if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
454
- throw error;
455
- }
456
- approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
457
- if (approvalId === void 0) {
458
- throw error;
440
+ this.realizer = config.realizer;
441
+ this.x402Fetch = config.x402Fetch;
442
+ this.probeFetch = config.probeFetch ?? fetch;
443
+ this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
444
+ this.network = config.network ?? SOLANA_MAINNET_NETWORK;
445
+ this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
446
+ this.stateStore = config.stateStore ?? null;
447
+ this.nowMs = config.nowMs ?? (() => Date.now());
448
+ if (this.stateStore !== null) {
449
+ for (const record of this.stateStore.load()) {
450
+ this.pending.set(record.key, record);
459
451
  }
460
- prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
461
- wallet: this.signer.walletAddress,
462
- vault: this.vault.address,
463
- amountRawUsdc: input.amountRawUsdc.toString(),
464
- approvalId
465
- });
466
- }
467
- if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.signingIntent.amountRawUsdc !== input.amountRawUsdc.toString()) {
468
- throw new VaultFlowClientError("prepare", "Prepared deposit differs from the requested wallet, vault or amount");
469
452
  }
470
- const signed = await this.signer.signDeposit({
471
- intent: prepared.signingIntent,
472
- serializedTransaction: prepared.serializedTransaction,
473
- lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
474
- });
475
- let outcome = await this.postJson("submit", "/v1/deposits/submit", {
476
- depositId: prepared.depositId,
477
- serializedTransaction: signed.serializedTransaction,
478
- agentSignature: signed.agentSignature
453
+ }
454
+ pay(input, realizer = this.realizer) {
455
+ const method = (input.method ?? "GET").toUpperCase();
456
+ const requestBodyHash = requestBodyHashFor(input.body ?? null);
457
+ const pendingKey = pendingPaymentKey({
458
+ url: input.url,
459
+ method,
460
+ requestBodyHash
479
461
  });
480
- if (outcome.status === "submitted") {
481
- outcome = await this.pollUntilTerminal(
482
- `/v1/deposits/${prepared.depositId}`,
483
- outcome
484
- );
462
+ const existingFlow = this.inFlight.get(pendingKey);
463
+ if (existingFlow !== void 0) {
464
+ return existingFlow;
485
465
  }
486
- return {
487
- depositId: prepared.depositId,
488
- status: outcome.status,
489
- txSignature: outcome.txSignature ?? null,
490
- actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
491
- sharesMintedRaw: outcome.sharesMintedRaw ?? null,
492
- errorCode: outcome.errorCode ?? null
466
+ const run = async () => {
467
+ if (this.stateStore?.withExclusiveLock) {
468
+ this.pending.clear();
469
+ for (const record of this.stateStore.load()) this.pending.set(record.key, record);
470
+ }
471
+ return this.run(input, { method, requestBodyHash, pendingKey }, realizer);
493
472
  };
473
+ const flow = (this.stateStore?.withExclusiveLock ? this.stateStore.withExclusiveLock(run) : run()).finally(() => {
474
+ this.inFlight.delete(pendingKey);
475
+ });
476
+ this.inFlight.set(pendingKey, flow);
477
+ return flow;
494
478
  }
495
- /**
496
- * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
497
- * sponsored). A plain withdrawal is the exit path and MAY spend principal;
498
- * with purpose "yield_realize" the relayer refuses anything beyond the
499
- * spendable yield (the payment path, via RelayerYieldRealizer).
500
- */
501
- async withdraw(input) {
502
- const prepared = await this.postJson(
503
- "prepare",
504
- "/v1/withdrawals/prepare",
505
- {
506
- wallet: this.signer.walletAddress,
507
- vault: this.vault.address,
508
- amountRawUsdc: input.amountRawUsdc.toString(),
509
- ...input.purpose === void 0 ? {} : { purpose: input.purpose },
510
- ...input.payment === void 0 ? {} : { payment: input.payment },
511
- ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
479
+ async run(input, computed, realizer) {
480
+ const { method, requestBodyHash, pendingKey } = computed;
481
+ const existingPending = this.pending.get(pendingKey);
482
+ const resuming = existingPending?.recovery !== void 0 && existingPending.status !== "external_outcome_unknown";
483
+ const requestHeadersHash = canonicalJsonHash(input.headers ?? {});
484
+ if (resuming) {
485
+ const recovery = existingPending.recovery;
486
+ if (canonicalJsonHash(recovery.context) !== canonicalJsonHash(realizer.realizationContext ?? null) || recovery.requestHeadersHash !== requestHeadersHash || existingPending.url !== input.url || existingPending.method !== method || existingPending.requestBodyHash !== requestBodyHash) {
487
+ throw new StandardX402PayError(
488
+ "payment_outcome_unknown",
489
+ "The pending realization belongs to a different wallet, vault, relayer or request; refusing to resume or discard it.",
490
+ publicPendingPayment(existingPending)
491
+ );
492
+ }
493
+ if (existingPending.status === "realizing" && recovery.prepared === void 0) {
494
+ throw new StandardX402PayError(
495
+ "payment_outcome_unknown",
496
+ "Realization was interrupted before its withdrawal ID was saved. Reconcile the original operation before retrying; forceNewPayment cannot discard an incomplete realization.",
497
+ publicPendingPayment(existingPending)
498
+ );
499
+ }
500
+ } else if (existingPending !== void 0) {
501
+ if (input.forceNewPayment !== true || existingPending.status === "realizing") {
502
+ throw new StandardX402PayError(
503
+ "payment_outcome_unknown",
504
+ "A previous payment or realization has an unknown outcome. Verify it before purchasing again.",
505
+ publicPendingPayment(existingPending)
506
+ );
507
+ }
508
+ try {
509
+ this.untrack(pendingKey);
510
+ } catch (error) {
511
+ throw new StandardX402PayError(
512
+ "state_persist_failed",
513
+ "Could not clear the previous pending x402 marker before forcing a new payment",
514
+ error
515
+ );
512
516
  }
513
- );
514
- 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) {
515
- throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
516
517
  }
517
- await assertWithdrawalPreview({
518
- rpc: this.rpc,
519
- serializedTransaction: prepared.serializedTransaction,
520
- wallet: this.signer.walletAddress,
521
- vault: this.vault,
522
- amountRawUsdc: input.amountRawUsdc,
523
- ...input.purpose === void 0 ? {} : { purpose: input.purpose }
524
- });
525
- const signed = await this.signer.signWithdrawal({
526
- intent: prepared.signingIntent,
527
- serializedTransaction: prepared.serializedTransaction,
528
- lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
529
- });
530
- let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
531
- withdrawalId: prepared.withdrawalId,
532
- serializedTransaction: signed.serializedTransaction,
533
- agentSignature: signed.agentSignature
534
- });
535
- if (outcome.status === "submitted") {
536
- outcome = await this.pollUntilTerminal(
537
- `/v1/withdrawals/${prepared.withdrawalId}`,
538
- outcome
518
+ const init = {
519
+ method,
520
+ ...input.body === void 0 ? {} : { body: input.body },
521
+ ...input.headers === void 0 ? {} : { headers: input.headers }
522
+ };
523
+ const probe = await this.probeFetch(input.url, init);
524
+ if (probe.status !== 402) {
525
+ if (resuming) {
526
+ throw new StandardX402PayError(
527
+ "payment_outcome_unknown",
528
+ "The seller no longer offers the original payment challenge; the saved realization is retained for reconciliation.",
529
+ publicPendingPayment(existingPending)
530
+ );
531
+ }
532
+ return { paid: false, status: probe.status, body: await probe.text() };
533
+ }
534
+ const selected2 = await this.selectRequirement(probe);
535
+ if (resuming && (!standardRequirementMatchesSelected(existingPending.recovery.requirement, selected2) || existingPending.amountRawUsdc !== selected2.amountRawUsdc.toString() || existingPending.payTo !== selected2.payTo || existingPending.feePayer !== selected2.feePayer)) {
536
+ throw new StandardX402PayError(
537
+ "payment_outcome_unknown",
538
+ "The seller's payment challenge differs from the saved realization; refusing to fund a different payment.",
539
+ publicPendingPayment(existingPending)
539
540
  );
540
541
  }
541
- return {
542
- withdrawalId: prepared.withdrawalId,
543
- status: outcome.status,
544
- txSignature: outcome.txSignature ?? null,
545
- destinationUsdcAta: prepared.destinationUsdcAta,
546
- actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
547
- actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
548
- errorCode: outcome.errorCode ?? null
542
+ const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
543
+ if (selected2.amountRawUsdc > cap) {
544
+ throw new StandardX402PayError(
545
+ "amount_exceeds_client_cap",
546
+ `the challenge demands ${selected2.amountRawUsdc} raw USDC, above the client cap of ${cap}; no new payment was attempted`,
547
+ { amountRawUsdc: selected2.amountRawUsdc.toString(), payTo: selected2.payTo }
548
+ );
549
+ }
550
+ const payment = {
551
+ payTo: selected2.payTo,
552
+ amountRawUsdc: selected2.amountRawUsdc.toString(),
553
+ resourceUrlHash: sha256HexOf(input.url),
554
+ method
549
555
  };
550
- }
551
- /**
552
- * Reads the yield budget. Syncs the relayer's ledger from chain first (so
553
- * yield accrued since the last sync shows up); the sync is best-effort and
554
- * on failure the last-synced view is returned.
555
- */
556
- async getBudget(options = {}) {
557
- if (options.refreshFromChain !== false) {
556
+ let pendingRecord = resuming ? existingPending : {
557
+ key: pendingKey,
558
+ url: input.url,
559
+ method,
560
+ requestBodyHash,
561
+ amountRawUsdc: selected2.amountRawUsdc.toString(),
562
+ payTo: selected2.payTo,
563
+ feePayer: selected2.feePayer,
564
+ realizedRawUsdc: "0",
565
+ realizeTxSignature: null,
566
+ status: "realizing",
567
+ createdAtMs: this.nowMs(),
568
+ updatedAtMs: this.nowMs(),
569
+ ...realizer.realizationContext === void 0 ? {} : {
570
+ recovery: {
571
+ version: 1,
572
+ context: { ...realizer.realizationContext },
573
+ requestHeadersHash,
574
+ requirement: structuredClone(selected2.requirement)
575
+ }
576
+ }
577
+ };
578
+ if (!resuming) this.persistCheckpoint(pendingRecord);
579
+ let realized;
580
+ if (resuming && pendingRecord.status === "realized") {
581
+ realized = {
582
+ realizedRawUsdc: BigInt(pendingRecord.realizedRawUsdc),
583
+ txSignature: pendingRecord.realizeTxSignature,
584
+ withdrawalId: pendingRecord.recovery?.prepared?.withdrawalId ?? null
585
+ };
586
+ } else {
558
587
  try {
559
- await this.postJson(
560
- "sync",
561
- `/v1/wallets/${this.signer.walletAddress}/sync`,
562
- { source: "chain", vault: this.vault.address }
588
+ if (resuming) {
589
+ if (realizer.resumeUsdcAvailable === void 0) {
590
+ throw new Error("This realizer cannot reconcile the saved withdrawal");
591
+ }
592
+ realized = await realizer.resumeUsdcAvailable({
593
+ amountRawUsdc: selected2.amountRawUsdc,
594
+ payment,
595
+ prepared: pendingRecord.recovery.prepared
596
+ });
597
+ } else {
598
+ realized = await realizer.ensureUsdcAvailable({
599
+ amountRawUsdc: selected2.amountRawUsdc,
600
+ payment,
601
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId },
602
+ onPrepared: async (prepared) => {
603
+ if (pendingRecord.recovery === void 0) {
604
+ throw new StandardX402PayError("state_persist_failed", "Cannot save a withdrawal without a pinned funding context");
605
+ }
606
+ const checkpoint = {
607
+ ...pendingRecord,
608
+ updatedAtMs: this.nowMs(),
609
+ recovery: { ...pendingRecord.recovery, prepared: structuredClone(prepared) }
610
+ };
611
+ this.persistCheckpoint(checkpoint);
612
+ pendingRecord = checkpoint;
613
+ }
614
+ });
615
+ }
616
+ } catch (error) {
617
+ if (error instanceof StandardX402PayError && error.reason === "state_persist_failed") throw error;
618
+ const failure = error;
619
+ const safeToRetry = !resuming && (failure.realizationSafeToRetry === true || failure.code === "approval_required" && pendingRecord.recovery?.prepared === void 0);
620
+ if (safeToRetry) {
621
+ try {
622
+ this.untrack(pendingKey);
623
+ } catch (persistError) {
624
+ throw new StandardX402PayError("state_persist_failed", "Could not clear the safely refused realization", persistError);
625
+ }
626
+ }
627
+ if (safeToRetry && failure.code === "approval_required") {
628
+ throw new StandardX402PayError(
629
+ "approval_required",
630
+ "This payment needs the owner's approval; nothing was paid. Ask the owner to open approveUrl, then retry the same call with approvalId.",
631
+ failure.detail ?? null
632
+ );
633
+ }
634
+ throw new StandardX402PayError(
635
+ safeToRetry ? "realize_failed" : "payment_outcome_unknown",
636
+ 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.",
637
+ { error, pendingPayment: safeToRetry ? null : publicPendingPayment(pendingRecord) }
563
638
  );
564
- } catch {
565
639
  }
566
640
  }
567
- const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
568
- const response = await this.fetchImpl(url, {
569
- headers: await walletAuthHeaders({
570
- signer: this.signer,
571
- method: "GET",
572
- url
573
- })
574
- });
575
- const text = await response.text();
576
- if (response.status !== 200) {
577
- throw new VaultFlowClientError(
578
- "budget",
579
- `budget endpoint returned ${response.status}: ${text}`
641
+ pendingRecord = {
642
+ ...pendingRecord,
643
+ status: "realized",
644
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
645
+ realizeTxSignature: realized.txSignature,
646
+ updatedAtMs: this.nowMs()
647
+ };
648
+ this.persistCheckpoint(pendingRecord);
649
+ if (realized.realizedRawUsdc < selected2.amountRawUsdc) {
650
+ throw new StandardX402PayError(
651
+ "realize_underfunded",
652
+ "The confirmed withdrawal did not cover the exact API price. No external payment was attempted; reconcile the recorded withdrawal before retrying.",
653
+ { pendingPayment: publicPendingPayment(pendingRecord) }
580
654
  );
581
655
  }
582
- let parsed;
656
+ this.persistCheckpoint({
657
+ ...pendingRecord,
658
+ status: "external_outcome_unknown",
659
+ updatedAtMs: this.nowMs()
660
+ });
661
+ let response;
583
662
  try {
584
- parsed = JSON.parse(text);
585
- } catch {
586
- throw new VaultFlowClientError(
587
- "budget",
588
- "budget endpoint returned 200 with a non-JSON body",
589
- text
663
+ response = await this.x402Fetch(input.url, init, selected2);
664
+ } catch (error) {
665
+ const persistError = this.tryMarkUnknown(pendingKey, {
666
+ message: error instanceof Error ? error.message : String(error)
667
+ });
668
+ throw new StandardX402PayError(
669
+ "payment_outcome_unknown",
670
+ `the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
671
+ { error, persistError }
590
672
  );
591
673
  }
592
- const body = parsed;
593
- if (body.position?.vault !== void 0 && body.position.vault !== this.vault.address) {
594
- throw new VaultFlowClientError("budget", "Relayer returned the budget for a different vault");
674
+ const bodyText = await response.text();
675
+ const receipt = readSettlementReceipt(response);
676
+ if (response.status < 200 || response.status >= 300 || receipt.status === "failed" || receipt.status === "invalid") {
677
+ const persistError = this.tryMarkUnknown(pendingKey, {
678
+ status: response.status,
679
+ body: bodyText,
680
+ receiptStatus: receipt.status
681
+ });
682
+ throw new StandardX402PayError(
683
+ "payment_outcome_unknown",
684
+ `the x402 payment attempt returned HTTP ${response.status} with a ${receipt.status} receipt; verify whether it settled before paying again`,
685
+ { status: response.status, body: bodyText, receiptStatus: receipt.status, persistError }
686
+ );
687
+ }
688
+ this.clearDelivered(pendingKey);
689
+ const paymentTxSignature = receipt.txSignature;
690
+ if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && realizer.reportPayment !== void 0) {
691
+ try {
692
+ await realizer.reportPayment({
693
+ withdrawalId: realized.withdrawalId,
694
+ paymentTxSignature
695
+ });
696
+ } catch (error) {
697
+ console.error(
698
+ `[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
699
+ );
700
+ }
595
701
  }
596
702
  return {
597
- wallet: this.signer.walletAddress,
598
- vault: this.vault.address,
599
- principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
600
- positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
601
- grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
602
- spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
703
+ paid: true,
704
+ ...realizer.vault === void 0 ? {} : { fundingVault: realizer.vault },
705
+ status: response.status,
706
+ body: bodyText,
707
+ payment: {
708
+ amountRawUsdc: selected2.amountRawUsdc.toString(),
709
+ payTo: selected2.payTo,
710
+ feePayer: selected2.feePayer,
711
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
712
+ realizeTxSignature: realized.txSignature,
713
+ paymentTxSignature
714
+ }
603
715
  };
604
716
  }
605
- /** Best-effort audit link: reports the x402 payment tx a realize funded. */
606
- async reportPayment(input) {
607
- await this.postJson("submit", "/v1/payments/report", {
608
- wallet: this.signer.walletAddress,
609
- withdrawalId: input.withdrawalId,
610
- paymentTxSignature: input.paymentTxSignature
611
- });
612
- }
613
- /** Wallet's approvals as the relayer sees them (optionally by status). */
614
- async listApprovals(status) {
615
- const body = await this.getJson(
616
- `/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
617
- );
618
- return body.approvals ?? [];
619
- }
620
- /**
621
- * Creates the owner-onboarding setup link (wallet-auth pins the agreed
622
- * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
623
- */
624
- async createSetupSession(input) {
625
- const session = await this.postJson(
626
- "prepare",
627
- `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
628
- {
629
- vault: this.vault.address,
630
- ...input.policy === void 0 ? {} : { policy: input.policy },
631
- ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
632
- ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
633
- ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
717
+ /** Reads the challenge from the header (preferred) or the JSON body. */
718
+ async selectRequirement(probe) {
719
+ const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
720
+ let requirements;
721
+ try {
722
+ if (header !== null) {
723
+ requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
724
+ } else {
725
+ requirements = parseStandardChallenge(
726
+ await probe.json()
727
+ ).solanaExactRequirements;
634
728
  }
635
- );
636
- if (session.vault !== this.vault.address || session.wallet !== this.signer.walletAddress) {
637
- throw new VaultFlowClientError("prepare", "Relayer returned a setup session for a different wallet or vault");
729
+ } catch (error) {
730
+ throw new StandardX402PayError(
731
+ error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
732
+ "could not parse the x402 402 challenge",
733
+ error
734
+ );
638
735
  }
639
- return session;
640
- }
641
- /** Polls a setup session (public capability URL — no auth needed). */
642
- async getSetupSession(sessionId) {
643
- const url = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
644
- const response = await this.fetchImpl(url);
645
- const text = await response.text();
646
- if (response.status !== 200) {
647
- const parsed = parseRelayerError(text);
648
- throw new VaultFlowClientError(
649
- "read",
650
- parsed.message ?? `setup session read failed with ${response.status}`,
651
- text,
652
- parsed.code,
653
- parsed.details
736
+ try {
737
+ return selectPayableSolanaRequirement(requirements, {
738
+ network: this.network,
739
+ usdcMint: this.usdcMint
740
+ });
741
+ } catch (error) {
742
+ throw new StandardX402PayError(
743
+ "no_payable_requirement",
744
+ error instanceof Error ? error.message : String(error),
745
+ error
654
746
  );
655
747
  }
656
- return JSON.parse(text);
657
748
  }
658
- /**
659
- * Finds an APPROVED, unconsumed deposit approval bound to exactly this
660
- * amount — the shape the mandate's initialDeposit approval has.
661
- */
662
- async findApprovedDepositApproval(amountRawUsdc) {
749
+ persistCheckpoint(record) {
663
750
  try {
664
- const approvals = await this.listApprovals("approved");
665
- const match = approvals.find((approval) => {
666
- const binding = approval.binding;
667
- return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
668
- });
669
- return match?.approvalId;
670
- } catch {
671
- return void 0;
751
+ this.track(record);
752
+ } catch (error) {
753
+ throw new StandardX402PayError(
754
+ "state_persist_failed",
755
+ "Could not persist payment recovery state; refusing the next financial operation.",
756
+ { error, pendingPayment: publicPendingPayment(record) }
757
+ );
672
758
  }
673
759
  }
674
- /**
675
- * Polls the reconciling GET endpoint until the intent leaves "submitted"
676
- * (each read looks the tx up on-chain) or the timeout elapses.
677
- */
678
- async pollUntilTerminal(path, last) {
679
- const deadline = Date.now() + this.pollTimeoutMs;
680
- let latest = last;
681
- while (Date.now() < deadline) {
682
- await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
683
- const url = `${this.baseUrl}${path}`;
684
- const response = await this.fetchImpl(url, {
685
- headers: await walletAuthHeaders({
686
- signer: this.signer,
687
- method: "GET",
688
- url
689
- })
690
- });
691
- if (response.status !== 200) {
692
- continue;
693
- }
694
- try {
695
- latest = await response.json();
696
- } catch {
697
- continue;
698
- }
699
- if (latest.status !== "submitted") {
700
- return latest;
760
+ track(record) {
761
+ const previous = this.pending.get(record.key);
762
+ this.pending.set(record.key, record);
763
+ try {
764
+ this.persist();
765
+ } catch (error) {
766
+ if (previous === void 0) {
767
+ this.pending.delete(record.key);
768
+ } else {
769
+ this.pending.set(record.key, previous);
701
770
  }
771
+ throw error;
702
772
  }
703
- return latest;
704
773
  }
705
- async postJson(step, path, body) {
706
- const url = `${this.baseUrl}${path}`;
707
- const serialized = JSON.stringify(body);
708
- const response = await this.fetchImpl(url, {
709
- method: "POST",
710
- headers: {
711
- ...await walletAuthHeaders({
712
- signer: this.signer,
713
- method: "POST",
714
- url,
715
- body: serialized
716
- }),
717
- "content-type": "application/json"
718
- },
719
- body: serialized
720
- });
721
- const text = await response.text();
722
- if (response.status !== 200) {
723
- const parsed = parseRelayerError(text);
724
- throw new VaultFlowClientError(
725
- step,
726
- parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
727
- text,
728
- parsed.code,
729
- parsed.details
730
- );
774
+ markUnknown(key, detail) {
775
+ const current = this.pending.get(key);
776
+ if (current === void 0) {
777
+ return;
731
778
  }
779
+ const next = {
780
+ ...current,
781
+ status: "external_outcome_unknown",
782
+ updatedAtMs: this.nowMs(),
783
+ detail
784
+ };
785
+ this.pending.set(key, next);
732
786
  try {
733
- return JSON.parse(text);
734
- } catch {
735
- throw new VaultFlowClientError(
736
- step,
737
- `${path} returned 200 with a non-JSON body`,
738
- text
739
- );
787
+ this.persist();
788
+ } catch (error) {
789
+ this.pending.set(key, current);
790
+ throw error;
740
791
  }
741
792
  }
742
- async getJson(path) {
743
- const url = `${this.baseUrl}${path}`;
744
- const response = await this.fetchImpl(url, {
745
- headers: await walletAuthHeaders({
746
- signer: this.signer,
747
- method: "GET",
748
- url
749
- })
750
- });
751
- const text = await response.text();
752
- if (response.status !== 200) {
753
- const parsed = parseRelayerError(text);
754
- throw new VaultFlowClientError(
755
- "read",
756
- parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
757
- text,
758
- parsed.code,
759
- parsed.details
760
- );
793
+ tryMarkUnknown(key, detail) {
794
+ try {
795
+ this.markUnknown(key, detail);
796
+ return null;
797
+ } catch (error) {
798
+ return error;
761
799
  }
800
+ }
801
+ untrack(key) {
802
+ const previous = this.pending.get(key);
803
+ const existed = previous !== void 0;
804
+ this.pending.delete(key);
762
805
  try {
763
- return JSON.parse(text);
764
- } catch {
765
- throw new VaultFlowClientError(
766
- "read",
767
- `${path} returned 200 with a non-JSON body`,
768
- text
806
+ this.persist();
807
+ } catch (error) {
808
+ if (existed) {
809
+ this.pending.set(key, previous);
810
+ }
811
+ throw error;
812
+ }
813
+ }
814
+ clearDelivered(key) {
815
+ const previous = this.pending.get(key);
816
+ this.pending.delete(key);
817
+ try {
818
+ this.persist();
819
+ } catch (error) {
820
+ if (previous !== void 0) {
821
+ this.pending.set(key, previous);
822
+ }
823
+ console.error(
824
+ `[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
769
825
  );
770
826
  }
771
827
  }
828
+ persist() {
829
+ if (this.stateStore === null) {
830
+ return;
831
+ }
832
+ this.stateStore.save([...this.pending.values()]);
833
+ }
772
834
  };
773
- function parseRelayerError(text) {
835
+ function publicPendingPayment(record) {
836
+ return {
837
+ url: record.url,
838
+ method: record.method,
839
+ requestBodyHash: record.requestBodyHash,
840
+ amountRawUsdc: record.amountRawUsdc,
841
+ payTo: record.payTo,
842
+ feePayer: record.feePayer,
843
+ realizedRawUsdc: record.realizedRawUsdc,
844
+ realizeTxSignature: record.realizeTxSignature,
845
+ status: record.status,
846
+ createdAtMs: record.createdAtMs,
847
+ updatedAtMs: record.updatedAtMs,
848
+ withdrawalId: record.recovery?.prepared?.withdrawalId ?? null,
849
+ fundingSource: record.recovery?.context ?? null
850
+ };
851
+ }
852
+ function pendingPaymentKey(input) {
853
+ return `${input.method}:${input.url}:${input.requestBodyHash}`;
854
+ }
855
+ function readSettlementReceipt(response) {
856
+ const header = response.headers.get(PAYMENT_RESPONSE_HEADER) ?? response.headers.get("x-payment-response");
857
+ if (header === null) {
858
+ return { status: "absent", txSignature: null };
859
+ }
774
860
  try {
775
- const parsed = JSON.parse(text);
776
- return {
777
- code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
778
- message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
779
- details: parsed.error?.details ?? null
780
- };
861
+ const decoded = decodeX402Header(header);
862
+ if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded) || !("success" in decoded) || typeof decoded.success !== "boolean") {
863
+ return { status: "invalid", txSignature: null };
864
+ }
865
+ if (!decoded.success) return { status: "failed", txSignature: null };
866
+ const receipt = decoded;
867
+ if (typeof receipt.transaction === "string" && receipt.transaction.length > 0) {
868
+ return { status: "success", txSignature: receipt.transaction };
869
+ }
870
+ if (typeof receipt.txHash === "string" && receipt.txHash.length > 0) {
871
+ return { status: "success", txSignature: receipt.txHash };
872
+ }
873
+ return { status: "invalid", txSignature: null };
781
874
  } catch {
782
- return { code: null, message: null, details: null };
875
+ return { status: "invalid", txSignature: null };
783
876
  }
784
877
  }
785
878
 
786
- // ../../src/client/relayer-yield-realizer.ts
787
- var REALIZE_OVERHEAD_RAW_USDC = 2500n;
788
- var RelayerRealizeError = class extends Error {
789
- constructor(code, message, detail = null) {
790
- super(message);
791
- this.code = code;
792
- this.detail = detail;
793
- this.name = "RelayerRealizeError";
794
- }
795
- code;
796
- detail;
797
- };
798
- var RelayerYieldRealizer = class {
799
- get vault() {
800
- return this.vaultFlows.vault.address;
801
- }
802
- vaultFlows;
803
- constructor(config) {
804
- this.vaultFlows = new VaultFlowClient({
805
- relayerBaseUrl: config.relayerBaseUrl,
806
- signer: config.signer,
807
- rpc: config.rpc,
808
- ...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
809
- ...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
810
- });
811
- }
812
- async ensureUsdcAvailable(input) {
813
- const shortfallRawUsdc = input.amountRawUsdc;
814
- await this.assertSpendableYield(shortfallRawUsdc);
815
- let outcome;
816
- try {
817
- outcome = await this.vaultFlows.withdraw({
818
- amountRawUsdc: shortfallRawUsdc,
819
- // The relayer refuses to prepare this withdrawal beyond the spendable
820
- // yield — the principal-protection guard the client cannot bypass.
821
- purpose: "yield_realize",
822
- // Declares what is being paid so the relayer's spending-mandate layer
823
- // can enforce caps/payee and keep the mandate → payment audit chain.
824
- ...input.payment === void 0 ? {} : { payment: input.payment },
825
- ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
826
- });
827
- } catch (error) {
828
- throw this.mapWithdrawError(error);
829
- }
830
- if (outcome.status !== "confirmed" || outcome.txSignature === null) {
831
- throw new RelayerRealizeError(
832
- "realize_not_confirmed",
833
- `yield realize withdrawal did not confirm (status=${outcome.status})`,
834
- outcome
835
- );
879
+ // ../../src/lib/associated-token-account.ts
880
+ import { createHash as createHash3 } from "node:crypto";
881
+ import bs58 from "bs58";
882
+ var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
883
+ var ED25519_P = (1n << 255n) - 19n;
884
+ var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
885
+ function deriveAssociatedTokenAddress(params) {
886
+ const owner = decodePublicKey(params.owner, "owner");
887
+ const mint = decodePublicKey(params.mint ?? SUBLY_VAULT.usdcMint, "mint");
888
+ const tokenProgramId = decodePublicKey(
889
+ params.tokenProgramId ?? SPL_TOKEN_PROGRAM_ID,
890
+ "tokenProgramId"
891
+ );
892
+ const associatedTokenProgramId = decodePublicKey(
893
+ ASSOCIATED_TOKEN_PROGRAM_ID,
894
+ "associatedTokenProgramId"
895
+ );
896
+ for (let bump = 255; bump >= 0; bump -= 1) {
897
+ const address3 = createProgramAddress(
898
+ [owner, tokenProgramId, mint, Uint8Array.of(bump)],
899
+ associatedTokenProgramId
900
+ );
901
+ if (address3 !== null) {
902
+ return bs58.encode(address3);
836
903
  }
837
- return {
838
- realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
839
- txSignature: outcome.txSignature,
840
- withdrawalId: outcome.withdrawalId
841
- };
842
904
  }
843
- /**
844
- * Best-effort report-back of the x402 payment tx this realize funded —
845
- * closes the relayer's mandate → realize → payment audit chain. Callers
846
- * must never let a failure here affect the payment result.
847
- */
848
- async reportPayment(input) {
849
- await this.vaultFlows.reportPayment(input);
905
+ throw new Error("Unable to derive associated token account address");
906
+ }
907
+ function createProgramAddress(seeds, programId) {
908
+ const hash = createHash3("sha256");
909
+ for (const seed of seeds) {
910
+ hash.update(seed);
850
911
  }
851
- /**
852
- * Refuses to realize more than the ledger's spendable yield (principal).
853
- * getBudget syncs the relayer's ledger from chain first (best-effort), so a
854
- * long-running client sees yield as it accrues instead of a frozen view.
855
- */
856
- async assertSpendableYield(shortfallRawUsdc) {
857
- let spendable;
858
- try {
859
- const budget = await this.vaultFlows.getBudget();
860
- spendable = BigInt(budget.spendableYieldRawUsdc);
861
- } catch (error) {
862
- throw new RelayerRealizeError(
863
- "budget_unavailable",
864
- "could not read the spendable-yield budget",
865
- error
866
- );
867
- }
868
- const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
869
- if (spendable < requiredRawUsdc) {
870
- throw new RelayerRealizeError(
871
- "insufficient_yield",
872
- `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`,
873
- { spendableYieldRawUsdc: spendable.toString() }
874
- );
875
- }
912
+ hash.update(programId);
913
+ hash.update(PDA_MARKER);
914
+ const digest = hash.digest();
915
+ return isEd25519Point(digest) ? null : new Uint8Array(digest);
916
+ }
917
+ function decodePublicKey(value, fieldName) {
918
+ const decoded = bs58.decode(value);
919
+ if (decoded.length !== 32) {
920
+ throw new Error(`${fieldName} must be a 32-byte public key`);
876
921
  }
877
- mapWithdrawError(error) {
878
- if (!(error instanceof VaultFlowClientError)) {
879
- return new RelayerRealizeError(
880
- "prepare_failed",
881
- `yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
882
- error
883
- );
884
- }
885
- const serverCode = error.code ?? errorCodeFrom(error.detail);
886
- if (serverCode === "approval_required") {
887
- return new RelayerRealizeError(
888
- "approval_required",
889
- "this payment exceeds the owner-approval threshold; nothing was realized or paid. Ask the owner to approve, then retry with the approvalId",
890
- error.errorDetails ?? error.detail
891
- );
892
- }
893
- if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
894
- return new RelayerRealizeError(
895
- "insufficient_yield",
896
- "the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
897
- error.detail
898
- );
899
- }
900
- return new RelayerRealizeError(
901
- error.step === "submit" ? "submit_failed" : "prepare_failed",
902
- error.message,
903
- error.detail
904
- );
922
+ return decoded;
923
+ }
924
+ function isEd25519Point(bytes) {
925
+ if (bytes.length !== 32) {
926
+ return false;
905
927
  }
906
- };
907
- function errorCodeFrom(detail) {
908
- if (typeof detail !== "string") {
909
- return null;
928
+ const yBytes = Uint8Array.from(bytes);
929
+ yBytes[31] = yBytes[31] & 127;
930
+ const y = littleEndianToBigInt(yBytes);
931
+ if (y >= ED25519_P) {
932
+ return false;
910
933
  }
911
- try {
912
- const parsed = JSON.parse(detail);
913
- return typeof parsed.error?.code === "string" ? parsed.error.code : null;
914
- } catch {
915
- return null;
934
+ const ySquared = mod(y * y, ED25519_P);
935
+ const numerator = mod(ySquared - 1n, ED25519_P);
936
+ const denominator = mod(ED25519_D * ySquared + 1n, ED25519_P);
937
+ if (denominator === 0n) {
938
+ return false;
916
939
  }
940
+ const xSquared = mod(
941
+ numerator * modPow(denominator, ED25519_P - 2n, ED25519_P),
942
+ ED25519_P
943
+ );
944
+ return xSquared === 0n || modPow(xSquared, (ED25519_P - 1n) / 2n, ED25519_P) === 1n;
917
945
  }
918
-
919
- // src/mcp-server.ts
920
- import { homedir } from "node:os";
921
- import { join as join2 } from "node:path";
922
-
923
- // ../../src/client/mcp-payment-server.ts
924
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
925
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
926
- import {
927
- CallToolRequestSchema,
928
- ListToolsRequestSchema
929
- } from "@modelcontextprotocol/sdk/types.js";
930
-
931
- // ../../src/client/onboarding.ts
932
- var SELF_SERVE_POLICY_ID = "self-serve";
933
- var OnboardingError = class extends Error {
934
- constructor(step, message, detail = null) {
935
- super(message);
936
- this.step = step;
937
- this.detail = detail;
938
- this.name = "OnboardingError";
946
+ function littleEndianToBigInt(bytes) {
947
+ let value = 0n;
948
+ for (let index = bytes.length - 1; index >= 0; index -= 1) {
949
+ value = (value << 8n) + BigInt(bytes[index]);
939
950
  }
940
- step;
941
- detail;
942
- };
943
- async function ensureWalletOnboarded(params) {
944
- const fetchImpl = params.fetchImpl ?? fetch;
945
- const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
946
- const post = async (step, path, body) => {
947
- const url = `${baseUrl}${path}`;
948
- const serialized = JSON.stringify(body);
949
- const response = await fetchImpl(url, {
950
- method: "POST",
951
- headers: {
952
- ...await walletAuthHeaders({
953
- signer: params.signer,
954
- method: "POST",
955
- url,
956
- body: serialized
957
- }),
958
- "content-type": "application/json"
959
- },
960
- body: serialized
961
- });
962
- if (response.status !== 200) {
963
- let detail = null;
964
- try {
965
- detail = await response.json();
966
- } catch {
967
- detail = null;
968
- }
969
- throw new OnboardingError(
970
- step,
971
- `wallet onboarding ${step} failed with ${response.status}`,
972
- detail
973
- );
951
+ return value;
952
+ }
953
+ function mod(value, modulus) {
954
+ const result = value % modulus;
955
+ return result >= 0n ? result : result + modulus;
956
+ }
957
+ function modPow(base, exponent, modulus) {
958
+ let result = 1n;
959
+ let nextBase = mod(base, modulus);
960
+ let nextExponent = exponent;
961
+ while (nextExponent > 0n) {
962
+ if ((nextExponent & 1n) === 1n) {
963
+ result = mod(result * nextBase, modulus);
974
964
  }
975
- };
976
- const wallet = params.signer.walletAddress;
977
- const vault = params.vault ?? params.signer.vault?.address ?? SUBLY_VAULT.address;
978
- await post("register", "/v1/wallets/agent", {
979
- wallet,
980
- vault,
981
- signingPolicyId: SELF_SERVE_POLICY_ID,
982
- signingMode: "non_interactive",
983
- signerValidationMode: params.signer.validationMode,
984
- signerProvider: params.signer.provider ?? "local-keypair",
985
- activateForPayments: true
986
- });
987
- await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain", vault });
965
+ nextBase = mod(nextBase * nextBase, modulus);
966
+ nextExponent >>= 1n;
967
+ }
968
+ return result;
988
969
  }
989
970
 
990
- // ../../src/x402/headers.ts
991
- import { z as z2 } from "zod";
971
+ // ../../src/domain/withdrawal-rounding.ts
972
+ var WITHDRAWAL_ROUNDING_RAW_USDC = 10n;
973
+ var YIELD_REALIZE_ROUNDING_RAW_USDC = WITHDRAWAL_ROUNDING_RAW_USDC / 2n;
992
974
 
993
- // ../../src/lib/hash.ts
994
- import { createHash as createHash3 } from "node:crypto";
995
- var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
996
- function sha256TaggedHex(data) {
997
- return `sha256-${createHash3("sha256").update(data).digest("hex")}`;
998
- }
999
- function stableStringify(value) {
1000
- if (value === null) {
1001
- return "null";
1002
- }
1003
- if (typeof value === "bigint") {
1004
- return JSON.stringify(value.toString());
975
+ // ../../src/client/withdrawal-preview.ts
976
+ async function assertWithdrawalPreview(input) {
977
+ const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
978
+ const simulation = await input.rpc.simulateTransaction(
979
+ input.serializedTransaction,
980
+ {
981
+ encoding: "base64",
982
+ commitment: "confirmed",
983
+ sigVerify: false,
984
+ replaceRecentBlockhash: false,
985
+ innerInstructions: true
986
+ }
987
+ ).send({ abortSignal: AbortSignal.timeout(15e3) });
988
+ if (simulation.value.err !== null) {
989
+ throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
1005
990
  }
1006
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1007
- return JSON.stringify(value);
991
+ let received = 0n;
992
+ for (const group of simulation.value.innerInstructions ?? []) {
993
+ for (const instruction of group.instructions) {
994
+ if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
995
+ const parsed = instruction.parsed;
996
+ if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
997
+ const info = parsed.info;
998
+ if (!info || info.destination !== destination && info.source !== destination) continue;
999
+ const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
1000
+ if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
1001
+ throw new Error("Withdrawal preview returned an invalid token amount");
1002
+ }
1003
+ const amount = BigInt(raw);
1004
+ if (info.destination === destination) received += amount;
1005
+ if (info.source === destination) received -= amount;
1006
+ }
1008
1007
  }
1009
- if (Array.isArray(value)) {
1010
- return `[${value.map((item) => stableStringify(item)).join(",")}]`;
1008
+ const minimum = input.purpose === "yield_realize" ? input.amountRawUsdc : input.amountRawUsdc - WITHDRAWAL_ROUNDING_RAW_USDC;
1009
+ if (received <= 0n || received > input.amountRawUsdc + WITHDRAWAL_ROUNDING_RAW_USDC || received < minimum) {
1010
+ throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
1011
1011
  }
1012
- const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
1013
- return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
1014
- }
1015
- function hashStableJson(value) {
1016
- return sha256TaggedHex(stableStringify(value));
1017
1012
  }
1018
1013
 
1019
- // ../../src/x402/headers.ts
1020
- var PAYMENT_REQUIRED_HEADER = "payment-required";
1021
- var PAYMENT_RESPONSE_HEADER = "payment-response";
1022
- var MAX_HEADER_JSON_BYTES = 16384;
1023
- var X402HeaderError = class extends Error {
1024
- reason;
1025
- constructor(reason, message) {
1026
- super(message);
1027
- this.name = "X402HeaderError";
1028
- this.reason = reason;
1029
- }
1030
- };
1031
- var sublyPaymentRequirementsSchema = z2.object({
1032
- scheme: z2.literal(PAYMENT_SCHEME),
1033
- network: z2.string().min(1),
1034
- asset: z2.string().min(32),
1035
- /** Exact seller amount in raw USDC; the scheme settles exactly this. */
1036
- amountRawUsdc: z2.string().regex(/^[1-9]\d*$/),
1037
- resource: z2.string().url(),
1038
- description: z2.string().optional(),
1039
- mimeType: z2.string().optional(),
1040
- payTo: z2.string().min(32),
1041
- maxTimeoutSeconds: z2.number().int().positive(),
1042
- extra: z2.object({
1043
- sellerRequestId: z2.string().min(1),
1044
- seller: z2.string().min(32),
1045
- sellerUsdcAta: z2.string().min(32),
1046
- vault: z2.string().min(32),
1047
- shareMint: z2.string().min(32)
1048
- })
1049
- }).loose();
1050
- var paymentRequiredSchema = z2.object({
1051
- x402Version: z2.number().int(),
1052
- accepts: z2.array(z2.unknown()),
1053
- error: z2.string().optional()
1054
- }).loose();
1055
- var sublyPaymentPayloadSchema = z2.object({
1056
- x402Version: z2.number().int(),
1057
- scheme: z2.literal(PAYMENT_SCHEME),
1058
- network: z2.string().min(1),
1059
- payload: z2.object({
1060
- paymentId: z2.string().min(1),
1061
- requestBindingHash: z2.string().min(1),
1062
- preparedMessageHash: z2.string().min(1),
1063
- serializedTransaction: z2.string().min(1).max(4096),
1064
- agentSignature: z2.string().min(1).max(128),
1065
- temporarySettlementSignature: z2.string().min(1).max(128)
1066
- })
1067
- }).loose();
1068
- function decodeX402Header(headerValue) {
1069
- if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
1070
- throw new X402HeaderError(
1071
- "header_too_large",
1072
- `x402 header exceeds ${MAX_HEADER_JSON_BYTES} encoded bytes`
1073
- );
1074
- }
1075
- const json = Buffer.from(headerValue, "base64").toString("utf8");
1076
- try {
1077
- return JSON.parse(json);
1078
- } catch {
1079
- throw new X402HeaderError(
1080
- "invalid_header_encoding",
1081
- "x402 header is not base64-encoded JSON"
1082
- );
1014
+ // ../../src/client/lookup-tables.ts
1015
+ import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1016
+ import { address, getCompiledTransactionMessageDecoder } from "@solana/kit";
1017
+ function lookupTableAddressesForTransaction(serializedTransaction) {
1018
+ const wire = Buffer.from(serializedTransaction, "base64");
1019
+ let offset = 0;
1020
+ let signatureCount = 0;
1021
+ let shift = 0;
1022
+ while (offset < wire.length) {
1023
+ const byte = wire[offset];
1024
+ signatureCount |= (byte & 127) << shift;
1025
+ offset += 1;
1026
+ if ((byte & 128) === 0) {
1027
+ break;
1028
+ }
1029
+ shift += 7;
1083
1030
  }
1031
+ const messageBytes = wire.subarray(offset + signatureCount * 64);
1032
+ const compiled = getCompiledTransactionMessageDecoder().decode(messageBytes);
1033
+ const lookups = compiled.addressTableLookups ?? [];
1034
+ return lookups.map((lookup) => String(lookup.lookupTableAddress));
1084
1035
  }
1085
- function requestBodyHashFor(body) {
1086
- if (body === null || body === void 0 || body.length === 0) {
1087
- return EMPTY_BODY_HASH;
1036
+ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1037
+ const addresses = lookupTableAddressesForTransaction(serializedTransaction);
1038
+ if (addresses.length === 0) {
1039
+ return {};
1088
1040
  }
1089
- return sha256TaggedHex(
1090
- typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
1041
+ const tables = await fetchAllMaybeAddressLookupTable(
1042
+ rpc2,
1043
+ addresses.map((value) => address(value))
1091
1044
  );
1045
+ const result = {};
1046
+ for (const table of tables) {
1047
+ if (table.exists) {
1048
+ result[table.address] = table.data.addresses.map(String);
1049
+ }
1050
+ }
1051
+ return result;
1092
1052
  }
1093
1053
 
1094
- // ../../src/client/paid-fetch.ts
1095
- function formatRawUsdcAmount(raw) {
1096
- const value = BigInt(raw);
1097
- const negative = value < 0n;
1098
- const abs = negative ? -value : value;
1099
- const whole = abs / 1000000n;
1100
- const frac = (abs % 1000000n).toString().padStart(6, "0");
1101
- return `${negative ? "-" : ""}${whole}.${frac}`;
1102
- }
1103
-
1104
- // ../../src/lib/canonical-json.ts
1054
+ // ../../src/api/wallet-auth.ts
1105
1055
  import { createHash as createHash4 } from "node:crypto";
1106
- function sha256HexOf(data) {
1056
+ import bs582 from "bs58";
1057
+ import nacl from "tweetnacl";
1058
+ var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
1059
+ var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
1060
+ var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
1061
+ function sha256Hex(data) {
1107
1062
  return createHash4("sha256").update(data, "utf8").digest("hex");
1108
1063
  }
1064
+ function walletAuthMessage(params) {
1065
+ return new TextEncoder().encode(
1066
+ `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
1067
+ params.rawBody
1068
+ )}:${params.signedAtMs}`
1069
+ );
1070
+ }
1109
1071
 
1110
- // ../../src/x402/standard-requirements.ts
1111
- import { z as z3 } from "zod";
1112
- var STANDARD_EXACT_SCHEME = "exact";
1113
- var standardExactRequirementSchema = z3.object({
1114
- scheme: z3.literal(STANDARD_EXACT_SCHEME),
1115
- /** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
1116
- network: z3.string().min(1),
1117
- /** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
1118
- asset: z3.string().min(1),
1119
- /** Exact price in the asset's atomic units, as a decimal string. */
1120
- amount: z3.string().regex(/^[1-9]\d*$/),
1121
- /** Recipient wallet; the transfer destination ATA is derived from it. */
1122
- payTo: z3.string().min(1),
1123
- maxTimeoutSeconds: z3.number().int().positive().optional(),
1124
- extra: z3.object({
1125
- /** Facilitator address that pays the tx fee (gas sponsorship). */
1126
- feePayer: z3.string().min(1).optional()
1127
- }).loose().optional()
1128
- }).loose();
1129
- var standardPaymentRequiredSchema = z3.object({
1130
- x402Version: z3.number().int(),
1131
- accepts: z3.array(z3.unknown()),
1132
- error: z3.string().optional(),
1133
- resource: z3.object({ url: z3.string().optional() }).loose().optional()
1134
- }).loose();
1135
- var StandardX402ChallengeError = class extends Error {
1136
- reason;
1137
- constructor(reason, message) {
1138
- super(message);
1139
- this.name = "StandardX402ChallengeError";
1140
- this.reason = reason;
1141
- }
1142
- };
1143
- function parseStandardChallenge(challenge) {
1144
- const parsed = standardPaymentRequiredSchema.safeParse(challenge);
1145
- if (!parsed.success) {
1146
- throw new StandardX402ChallengeError(
1147
- "invalid_payment_required",
1148
- "Response is not a valid x402 PaymentRequired object"
1149
- );
1150
- }
1151
- const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
1152
- const requirement = standardExactRequirementSchema.safeParse(candidate);
1153
- if (!requirement.success) {
1154
- return [];
1155
- }
1156
- return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
1072
+ // ../../src/client/wallet-auth-headers.ts
1073
+ async function walletAuthHeaders(params) {
1074
+ const signedAtMs = String(Date.now());
1075
+ const message = walletAuthMessage({
1076
+ method: params.method,
1077
+ path: (() => {
1078
+ const url = new URL(params.url);
1079
+ return url.pathname + url.search;
1080
+ })(),
1081
+ rawBody: params.body ?? "",
1082
+ signedAtMs
1157
1083
  });
1158
- return { paymentRequired: parsed.data, solanaExactRequirements };
1159
- }
1160
- function decodeStandardPaymentRequiredHeader(headerValue) {
1161
- let decoded;
1162
- try {
1163
- decoded = decodeX402Header(headerValue);
1164
- } catch (error) {
1165
- throw new StandardX402ChallengeError(
1166
- error instanceof X402HeaderError ? error.reason : "invalid_header",
1167
- "Cannot decode the payment-required header"
1168
- );
1169
- }
1170
- return parseStandardChallenge(decoded);
1171
- }
1172
- function selectPayableSolanaRequirement(requirements, options) {
1173
- const network = options?.network ?? SOLANA_MAINNET_NETWORK;
1174
- const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
1175
- const matchingRequirements = requirements.filter(
1176
- (candidate) => candidate.network === network && candidate.asset === usdcMint
1177
- );
1178
- if (matchingRequirements.length === 0) {
1179
- throw new StandardX402ChallengeError(
1180
- "no_payable_requirement",
1181
- `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
1182
- );
1183
- }
1184
- const requirement = matchingRequirements.find(
1185
- (candidate) => candidate.extra?.feePayer !== void 0
1186
- ) ?? null;
1187
- if (requirement === null) {
1188
- throw new StandardX402ChallengeError(
1189
- "missing_svm_fee_payer",
1190
- "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
1191
- );
1192
- }
1193
- const feePayer = requirement.extra?.feePayer;
1194
- if (feePayer === void 0) {
1195
- throw new StandardX402ChallengeError(
1196
- "missing_svm_fee_payer",
1197
- "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
1198
- );
1199
- }
1200
1084
  return {
1201
- requirement,
1202
- amountRawUsdc: BigInt(requirement.amount),
1203
- payTo: requirement.payTo,
1204
- feePayer
1085
+ [WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
1086
+ [WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
1087
+ [WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
1205
1088
  };
1206
1089
  }
1207
- function standardRequirementMatchesSelected(candidate, selected2) {
1208
- const parsed = standardExactRequirementSchema.safeParse(candidate);
1209
- return parsed.success && stableJson(parsed.data) === stableJson(selected2.requirement);
1210
- }
1211
- function stableJson(value) {
1212
- return JSON.stringify(sortJson(value));
1090
+
1091
+ // ../../src/client/vault-flows.ts
1092
+ var VaultFlowClientError = class extends Error {
1093
+ constructor(step, message, detail = null, code = null, errorDetails = null) {
1094
+ super(message);
1095
+ this.step = step;
1096
+ this.detail = detail;
1097
+ this.code = code;
1098
+ this.errorDetails = errorDetails;
1099
+ this.name = "VaultFlowClientError";
1100
+ }
1101
+ step;
1102
+ detail;
1103
+ code;
1104
+ errorDetails;
1105
+ };
1106
+ function vaultOperationKind(intentId) {
1107
+ if (/^dep_[0-9a-f]{32}$/.test(intentId)) return "deposit";
1108
+ if (/^wdr_[0-9a-f]{32}$/.test(intentId)) return "withdrawal";
1109
+ throw new VaultFlowClientError("read", "intentId must be the original dep_ or wdr_ ID followed by 32 lowercase hexadecimal characters");
1213
1110
  }
1214
- function sortJson(value) {
1215
- if (Array.isArray(value)) {
1216
- return value.map(sortJson);
1111
+ var VaultFlowClient = class {
1112
+ vault;
1113
+ rpc;
1114
+ baseUrl;
1115
+ signer;
1116
+ fetchImpl;
1117
+ lookupTablesFor;
1118
+ pollTimeoutMs;
1119
+ pollIntervalMs;
1120
+ constructor(config) {
1121
+ this.rpc = config.rpc;
1122
+ this.vault = config.vault ?? config.signer.vault ?? SUBLY_VAULT;
1123
+ if (config.signer.vault && config.signer.vault.address !== this.vault.address) {
1124
+ throw new Error("Vault flow client and signer must select the same vault");
1125
+ }
1126
+ this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
1127
+ this.signer = config.signer;
1128
+ this.fetchImpl = config.fetchImpl ?? fetch;
1129
+ this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
1130
+ this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1131
+ this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1132
+ }
1133
+ /**
1134
+ * Moves USDC from the agent wallet into the vault (fee sponsored). Under
1135
+ * depositPolicy "owner_approval_required" the relayer refuses to prepare
1136
+ * without an owner approval; when the caller passes none, an already
1137
+ * APPROVED deposit approval for this exact amount (e.g. the mandate's
1138
+ * initialDeposit — "one Face ID covers mandate + first deposit") is looked
1139
+ * up and used automatically before surfacing deposit_approval_required.
1140
+ */
1141
+ async deposit(input) {
1142
+ let approvalId = input.approvalId;
1143
+ let prepared;
1144
+ try {
1145
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1146
+ wallet: this.signer.walletAddress,
1147
+ vault: this.vault.address,
1148
+ amountRawUsdc: input.amountRawUsdc.toString(),
1149
+ ...approvalId === void 0 ? {} : { approvalId }
1150
+ });
1151
+ } catch (error) {
1152
+ if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
1153
+ throw error;
1154
+ }
1155
+ approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
1156
+ if (approvalId === void 0) {
1157
+ throw error;
1158
+ }
1159
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1160
+ wallet: this.signer.walletAddress,
1161
+ vault: this.vault.address,
1162
+ amountRawUsdc: input.amountRawUsdc.toString(),
1163
+ approvalId
1164
+ });
1165
+ }
1166
+ if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.signingIntent.amountRawUsdc !== input.amountRawUsdc.toString()) {
1167
+ throw new VaultFlowClientError("prepare", "Prepared deposit differs from the requested wallet, vault or amount");
1168
+ }
1169
+ const signed = await this.signer.signDeposit({
1170
+ intent: prepared.signingIntent,
1171
+ serializedTransaction: prepared.serializedTransaction,
1172
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1173
+ });
1174
+ let outcome = await this.postJson("submit", "/v1/deposits/submit", {
1175
+ depositId: prepared.depositId,
1176
+ serializedTransaction: signed.serializedTransaction,
1177
+ agentSignature: signed.agentSignature
1178
+ });
1179
+ if (outcome.status === "submitted") {
1180
+ outcome = await this.pollUntilTerminal(
1181
+ `/v1/deposits/${prepared.depositId}`,
1182
+ outcome
1183
+ );
1184
+ }
1185
+ return {
1186
+ depositId: prepared.depositId,
1187
+ status: outcome.status,
1188
+ txSignature: outcome.txSignature ?? null,
1189
+ actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
1190
+ sharesMintedRaw: outcome.sharesMintedRaw ?? null,
1191
+ errorCode: outcome.errorCode ?? null
1192
+ };
1217
1193
  }
1218
- if (value !== null && typeof value === "object") {
1219
- return Object.fromEntries(
1220
- Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
1194
+ /**
1195
+ * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
1196
+ * sponsored). A plain withdrawal is the exit path and MAY spend principal;
1197
+ * with purpose "yield_realize" the relayer refuses anything beyond the
1198
+ * spendable yield (the payment path, via RelayerYieldRealizer).
1199
+ */
1200
+ async withdraw(input) {
1201
+ const prepared = await this.postJson(
1202
+ "prepare",
1203
+ "/v1/withdrawals/prepare",
1204
+ {
1205
+ wallet: this.signer.walletAddress,
1206
+ vault: this.vault.address,
1207
+ amountRawUsdc: input.amountRawUsdc.toString(),
1208
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose },
1209
+ ...input.payment === void 0 ? {} : { payment: input.payment },
1210
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1211
+ }
1221
1212
  );
1213
+ this.assertPreparedWithdrawal(prepared, input);
1214
+ await input.onPrepared?.(prepared);
1215
+ return this.submitPreparedWithdrawal(prepared, input);
1216
+ }
1217
+ /** Reconcile or submit the original intent; never prepare a replacement. */
1218
+ async resumeWithdrawal(prepared, input) {
1219
+ this.assertPreparedWithdrawal(prepared, input);
1220
+ const current = await this.getJson(
1221
+ `/v1/withdrawals/${encodeURIComponent(prepared.withdrawalId)}`
1222
+ );
1223
+ 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) {
1224
+ throw new VaultFlowClientError("read", "Saved withdrawal differs from the original operation; refusing to resume");
1225
+ }
1226
+ if (current.status === "prepared") {
1227
+ return this.submitPreparedWithdrawal(prepared, input);
1228
+ }
1229
+ if (!["submitted", "confirmed", "failed", "failed_not_submitted", "expired", "quarantined"].includes(current.status)) {
1230
+ throw new VaultFlowClientError("read", "Relayer returned an unknown withdrawal status");
1231
+ }
1232
+ return this.withdrawalOutcome(prepared, current);
1222
1233
  }
1223
- return value;
1224
- }
1225
-
1226
- // ../../src/client/standard-x402-payer.ts
1227
- var StandardX402PayError = class extends Error {
1228
- constructor(reason, message, detail = null) {
1229
- super(message);
1230
- this.reason = reason;
1231
- this.detail = detail;
1232
- this.name = "StandardX402PayError";
1233
- }
1234
- reason;
1235
- detail;
1236
- };
1237
- var StandardX402Payer = class {
1238
- realizer;
1239
- x402Fetch;
1240
- probeFetch;
1241
- defaultMaxAmountRawUsdc;
1242
- network;
1243
- usdcMint;
1244
- stateStore;
1245
- pending = /* @__PURE__ */ new Map();
1246
- inFlight = /* @__PURE__ */ new Map();
1247
- nowMs;
1248
- constructor(config) {
1249
- this.realizer = config.realizer;
1250
- this.x402Fetch = config.x402Fetch;
1251
- this.probeFetch = config.probeFetch ?? fetch;
1252
- this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
1253
- this.network = config.network ?? SOLANA_MAINNET_NETWORK;
1254
- this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
1255
- this.stateStore = config.stateStore ?? null;
1256
- this.nowMs = config.nowMs ?? (() => Date.now());
1257
- if (this.stateStore !== null) {
1258
- for (const record of this.stateStore.load()) {
1259
- this.pending.set(record.key, record);
1260
- }
1234
+ assertPreparedWithdrawal(prepared, input) {
1235
+ 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) {
1236
+ throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
1237
+ }
1238
+ if (typeof prepared.withdrawalId !== "string" || prepared.withdrawalId.length === 0) {
1239
+ throw new VaultFlowClientError("prepare", "Prepared withdrawal has no withdrawal ID");
1261
1240
  }
1262
1241
  }
1263
- pay(input, realizer = this.realizer) {
1264
- const method = (input.method ?? "GET").toUpperCase();
1265
- const requestBodyHash = requestBodyHashFor(input.body ?? null);
1266
- const pendingKey = pendingPaymentKey({
1267
- url: input.url,
1268
- method,
1269
- requestBodyHash
1242
+ async submitPreparedWithdrawal(prepared, input) {
1243
+ await assertWithdrawalPreview({
1244
+ rpc: this.rpc,
1245
+ serializedTransaction: prepared.serializedTransaction,
1246
+ wallet: this.signer.walletAddress,
1247
+ vault: this.vault,
1248
+ amountRawUsdc: input.amountRawUsdc,
1249
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1270
1250
  });
1271
- const existingFlow = this.inFlight.get(pendingKey);
1272
- if (existingFlow !== void 0) {
1273
- return existingFlow;
1251
+ const signed = await this.signer.signWithdrawal({
1252
+ intent: prepared.signingIntent,
1253
+ serializedTransaction: prepared.serializedTransaction,
1254
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1255
+ });
1256
+ input.onBeforeSubmit?.();
1257
+ let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
1258
+ withdrawalId: prepared.withdrawalId,
1259
+ serializedTransaction: signed.serializedTransaction,
1260
+ agentSignature: signed.agentSignature
1261
+ });
1262
+ if (outcome.status === "submitted") {
1263
+ outcome = await this.pollUntilTerminal(
1264
+ `/v1/withdrawals/${prepared.withdrawalId}`,
1265
+ outcome
1266
+ );
1274
1267
  }
1275
- const run = async () => {
1276
- if (this.stateStore?.withExclusiveLock) {
1277
- this.pending.clear();
1278
- for (const record of this.stateStore.load()) this.pending.set(record.key, record);
1279
- }
1280
- return this.run(input, { method, requestBodyHash, pendingKey }, realizer);
1268
+ return this.withdrawalOutcome(prepared, outcome);
1269
+ }
1270
+ withdrawalOutcome(prepared, outcome) {
1271
+ return {
1272
+ withdrawalId: prepared.withdrawalId,
1273
+ status: outcome.status,
1274
+ txSignature: outcome.txSignature ?? null,
1275
+ destinationUsdcAta: prepared.destinationUsdcAta,
1276
+ actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
1277
+ actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
1278
+ errorCode: outcome.errorCode ?? null
1281
1279
  };
1282
- const flow = (this.stateStore?.withExclusiveLock ? this.stateStore.withExclusiveLock(run) : run()).finally(() => {
1283
- this.inFlight.delete(pendingKey);
1284
- });
1285
- this.inFlight.set(pendingKey, flow);
1286
- return flow;
1287
1280
  }
1288
- async run(input, computed, realizer) {
1289
- const { method, requestBodyHash, pendingKey } = computed;
1290
- const existingPending = this.pending.get(pendingKey);
1291
- if (existingPending !== void 0 && input.forceNewPayment !== true) {
1292
- throw new StandardX402PayError(
1293
- "payment_outcome_unknown",
1294
- "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.",
1295
- existingPending
1296
- );
1281
+ /** Authenticated read/reconciliation only: never prepare, sign or submit a transaction. */
1282
+ async getOperationStatus(intentId) {
1283
+ const kind = vaultOperationKind(intentId);
1284
+ const raw = await this.getJson(`/v1/${kind === "deposit" ? "deposits" : "withdrawals"}/${intentId}?resubmit=false`);
1285
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
1286
+ throw new VaultFlowClientError("read", "Relayer returned an invalid operation status");
1287
+ }
1288
+ const record = raw;
1289
+ if (record[kind === "deposit" ? "depositId" : "withdrawalId"] !== intentId || record.wallet !== this.signer.walletAddress || record.vault !== this.vault.address) {
1290
+ throw new VaultFlowClientError("read", "Operation does not match the requested ID, current wallet or selected vault; use the original wallet, vault and relayer");
1297
1291
  }
1298
- if (existingPending !== void 0 && input.forceNewPayment === true) {
1292
+ const requested = record[kind === "deposit" ? "amountRawUsdc" : "requestedWithdrawRawUsdc"];
1293
+ const actual = record[kind === "deposit" ? "actualDepositRawUsdc" : "actualWithdrawRawUsdc"];
1294
+ 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)) {
1295
+ throw new VaultFlowClientError("read", "Relayer returned incomplete or invalid operation status fields");
1296
+ }
1297
+ const status = record.status;
1298
+ const nextAction = status === "confirmed" ? "done" : status === "submitted" || status === "prepared" ? "check_again" : "reconcile_with_operator";
1299
+ 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.";
1300
+ return {
1301
+ intentId,
1302
+ kind,
1303
+ wallet: this.signer.walletAddress,
1304
+ vault: this.vault.address,
1305
+ status,
1306
+ requestedAmountRawUsdc: requested,
1307
+ actualAmountRawUsdc: actual,
1308
+ txSignature: record.txSignature,
1309
+ errorCode: record.errorCode,
1310
+ stillConfirming: status === "submitted",
1311
+ nextAction,
1312
+ message
1313
+ };
1314
+ }
1315
+ /**
1316
+ * Reads the yield budget. Syncs the relayer's ledger from chain first (so
1317
+ * yield accrued since the last sync shows up); the sync is best-effort and
1318
+ * on failure the last-synced view is returned.
1319
+ */
1320
+ async getBudget(options = {}) {
1321
+ if (options.refreshFromChain !== false) {
1299
1322
  try {
1300
- this.untrack(pendingKey);
1301
- } catch (error) {
1302
- throw new StandardX402PayError(
1303
- "state_persist_failed",
1304
- "could not clear the previous pending x402 marker before forcing a new payment",
1305
- error
1323
+ await this.postJson(
1324
+ "sync",
1325
+ `/v1/wallets/${this.signer.walletAddress}/sync`,
1326
+ { source: "chain", vault: this.vault.address }
1306
1327
  );
1328
+ } catch {
1307
1329
  }
1308
1330
  }
1309
- const init = {
1310
- method,
1311
- ...input.body === void 0 ? {} : { body: input.body },
1312
- ...input.headers === void 0 ? {} : { headers: input.headers }
1331
+ const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
1332
+ const response = await this.fetchImpl(url, {
1333
+ headers: await walletAuthHeaders({
1334
+ signer: this.signer,
1335
+ method: "GET",
1336
+ url
1337
+ })
1338
+ });
1339
+ const text = await response.text();
1340
+ if (response.status !== 200) {
1341
+ throw new VaultFlowClientError(
1342
+ "budget",
1343
+ `budget endpoint returned ${response.status}: ${text}`
1344
+ );
1345
+ }
1346
+ let parsed;
1347
+ try {
1348
+ parsed = JSON.parse(text);
1349
+ } catch {
1350
+ throw new VaultFlowClientError(
1351
+ "budget",
1352
+ "budget endpoint returned 200 with a non-JSON body",
1353
+ text
1354
+ );
1355
+ }
1356
+ const body = parsed;
1357
+ if (body.position?.vault !== void 0 && body.position.vault !== this.vault.address) {
1358
+ throw new VaultFlowClientError("budget", "Relayer returned the budget for a different vault");
1359
+ }
1360
+ return {
1361
+ wallet: this.signer.walletAddress,
1362
+ vault: this.vault.address,
1363
+ principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
1364
+ positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
1365
+ grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
1366
+ spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
1313
1367
  };
1314
- const probe = await this.probeFetch(input.url, init);
1315
- if (probe.status !== 402) {
1316
- return { paid: false, status: probe.status, body: await probe.text() };
1368
+ }
1369
+ /** Best-effort audit link: reports the x402 payment tx a realize funded. */
1370
+ async reportPayment(input) {
1371
+ await this.postJson("submit", "/v1/payments/report", {
1372
+ wallet: this.signer.walletAddress,
1373
+ withdrawalId: input.withdrawalId,
1374
+ paymentTxSignature: input.paymentTxSignature
1375
+ });
1376
+ }
1377
+ /** Wallet's approvals as the relayer sees them (optionally by status). */
1378
+ async listApprovals(status) {
1379
+ const body = await this.getJson(
1380
+ `/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
1381
+ );
1382
+ return body.approvals ?? [];
1383
+ }
1384
+ /**
1385
+ * Creates the owner-onboarding setup link (wallet-auth pins the agreed
1386
+ * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
1387
+ */
1388
+ async createSetupSession(input) {
1389
+ const session = await this.postJson(
1390
+ "prepare",
1391
+ `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
1392
+ {
1393
+ vault: this.vault.address,
1394
+ ...input.policy === void 0 ? {} : { policy: input.policy },
1395
+ ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
1396
+ ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
1397
+ ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1398
+ }
1399
+ );
1400
+ if (session.vault !== this.vault.address || session.wallet !== this.signer.walletAddress) {
1401
+ throw new VaultFlowClientError("prepare", "Relayer returned a setup session for a different wallet or vault");
1317
1402
  }
1318
- const selected2 = await this.selectRequirement(probe);
1319
- const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1320
- if (selected2.amountRawUsdc > cap) {
1321
- throw new StandardX402PayError(
1322
- "amount_exceeds_client_cap",
1323
- `the challenge demands ${selected2.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
1324
- { amountRawUsdc: selected2.amountRawUsdc.toString(), payTo: selected2.payTo }
1403
+ return session;
1404
+ }
1405
+ /** Polls a setup session (public capability URL — no auth needed). */
1406
+ async getSetupSession(sessionId) {
1407
+ const url = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
1408
+ const response = await this.fetchImpl(url);
1409
+ const text = await response.text();
1410
+ if (response.status !== 200) {
1411
+ const parsed = parseRelayerError(text);
1412
+ throw new VaultFlowClientError(
1413
+ "read",
1414
+ parsed.message ?? `setup session read failed with ${response.status}`,
1415
+ text,
1416
+ parsed.code,
1417
+ parsed.details
1325
1418
  );
1326
1419
  }
1327
- let realized;
1420
+ return JSON.parse(text);
1421
+ }
1422
+ /**
1423
+ * Finds an APPROVED, unconsumed deposit approval bound to exactly this
1424
+ * amount — the shape the mandate's initialDeposit approval has.
1425
+ */
1426
+ async findApprovedDepositApproval(amountRawUsdc) {
1328
1427
  try {
1329
- realized = await realizer.ensureUsdcAvailable({
1330
- amountRawUsdc: selected2.amountRawUsdc,
1331
- payment: {
1332
- payTo: selected2.payTo,
1333
- amountRawUsdc: selected2.amountRawUsdc.toString(),
1334
- resourceUrlHash: sha256HexOf(input.url),
1335
- method
1336
- },
1337
- ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1428
+ const approvals = await this.listApprovals("approved");
1429
+ const match = approvals.find((approval) => {
1430
+ const binding = approval.binding;
1431
+ return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
1338
1432
  });
1339
- } catch (error) {
1340
- if (error.code === "approval_required") {
1341
- throw new StandardX402PayError(
1342
- "approval_required",
1343
- "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",
1344
- error.detail ?? null
1345
- );
1346
- }
1347
- throw new StandardX402PayError(
1348
- "realize_failed",
1349
- `could not realize yield to cover ${selected2.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
1350
- error
1351
- );
1433
+ return match?.approvalId;
1434
+ } catch {
1435
+ return void 0;
1352
1436
  }
1353
- const pendingRecord = {
1354
- key: pendingKey,
1355
- url: input.url,
1356
- method,
1357
- requestBodyHash,
1358
- amountRawUsdc: selected2.amountRawUsdc.toString(),
1359
- payTo: selected2.payTo,
1360
- feePayer: selected2.feePayer,
1361
- realizedRawUsdc: realized.realizedRawUsdc.toString(),
1362
- realizeTxSignature: realized.txSignature,
1363
- status: "realized",
1364
- createdAtMs: this.nowMs(),
1365
- updatedAtMs: this.nowMs()
1366
- };
1367
- try {
1368
- this.track(pendingRecord);
1369
- } catch (error) {
1370
- throw new StandardX402PayError(
1371
- "state_persist_failed",
1372
- "could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
1373
- { error, pendingPayment: pendingRecord }
1374
- );
1437
+ }
1438
+ /**
1439
+ * Polls the reconciling GET endpoint until the intent leaves "submitted"
1440
+ * (each read looks the tx up on-chain) or the timeout elapses.
1441
+ */
1442
+ async pollUntilTerminal(path, last) {
1443
+ const deadline = Date.now() + this.pollTimeoutMs;
1444
+ let latest = last;
1445
+ while (Date.now() < deadline) {
1446
+ await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
1447
+ const url = `${this.baseUrl}${path}`;
1448
+ const response = await this.fetchImpl(url, {
1449
+ headers: await walletAuthHeaders({
1450
+ signer: this.signer,
1451
+ method: "GET",
1452
+ url
1453
+ })
1454
+ });
1455
+ if (response.status !== 200) {
1456
+ continue;
1457
+ }
1458
+ try {
1459
+ latest = await response.json();
1460
+ } catch {
1461
+ continue;
1462
+ }
1463
+ if (latest.status !== "submitted") {
1464
+ return latest;
1465
+ }
1375
1466
  }
1376
- if (realized.realizedRawUsdc < selected2.amountRawUsdc) {
1377
- throw new StandardX402PayError(
1378
- "realize_underfunded",
1379
- "The confirmed withdrawal did not cover the exact API price. No external payment was attempted; reconcile the recorded withdrawal before retrying.",
1380
- { pendingPayment: pendingRecord }
1467
+ return latest;
1468
+ }
1469
+ async postJson(step, path, body) {
1470
+ const url = `${this.baseUrl}${path}`;
1471
+ const serialized = JSON.stringify(body);
1472
+ const response = await this.fetchImpl(url, {
1473
+ method: "POST",
1474
+ headers: {
1475
+ ...await walletAuthHeaders({
1476
+ signer: this.signer,
1477
+ method: "POST",
1478
+ url,
1479
+ body: serialized
1480
+ }),
1481
+ "content-type": "application/json"
1482
+ },
1483
+ body: serialized
1484
+ });
1485
+ const text = await response.text();
1486
+ if (response.status !== 200) {
1487
+ const parsed = parseRelayerError(text);
1488
+ throw new VaultFlowClientError(
1489
+ step,
1490
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1491
+ text,
1492
+ parsed.code,
1493
+ parsed.details
1381
1494
  );
1382
1495
  }
1383
- let response;
1384
1496
  try {
1385
- response = await this.x402Fetch(input.url, init, selected2);
1386
- } catch (error) {
1387
- const persistError = this.tryMarkUnknown(pendingKey, {
1388
- message: error instanceof Error ? error.message : String(error)
1389
- });
1390
- throw new StandardX402PayError(
1391
- "payment_outcome_unknown",
1392
- `the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
1393
- { error, persistError }
1497
+ return JSON.parse(text);
1498
+ } catch {
1499
+ throw new VaultFlowClientError(
1500
+ step,
1501
+ `${path} returned 200 with a non-JSON body`,
1502
+ text
1394
1503
  );
1395
1504
  }
1396
- const bodyText = await response.text();
1505
+ }
1506
+ async getJson(path) {
1507
+ const url = `${this.baseUrl}${path}`;
1508
+ const response = await this.fetchImpl(url, {
1509
+ headers: await walletAuthHeaders({
1510
+ signer: this.signer,
1511
+ method: "GET",
1512
+ url
1513
+ })
1514
+ });
1515
+ const text = await response.text();
1397
1516
  if (response.status !== 200) {
1398
- const persistError = this.tryMarkUnknown(pendingKey, {
1399
- status: response.status,
1400
- body: bodyText
1401
- });
1402
- throw new StandardX402PayError(
1403
- "payment_outcome_unknown",
1404
- `the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
1405
- { status: response.status, body: bodyText, persistError }
1517
+ const parsed = parseRelayerError(text);
1518
+ throw new VaultFlowClientError(
1519
+ "read",
1520
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1521
+ text,
1522
+ parsed.code,
1523
+ parsed.details
1406
1524
  );
1407
1525
  }
1408
- this.clearDelivered(pendingKey);
1409
- const paymentTxSignature = extractSettledPaymentTxSignature(response);
1410
- if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && realizer.reportPayment !== void 0) {
1411
- try {
1412
- await realizer.reportPayment({
1413
- withdrawalId: realized.withdrawalId,
1414
- paymentTxSignature
1415
- });
1416
- } catch (error) {
1417
- console.error(
1418
- `[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
1419
- );
1420
- }
1526
+ try {
1527
+ return JSON.parse(text);
1528
+ } catch {
1529
+ throw new VaultFlowClientError(
1530
+ "read",
1531
+ `${path} returned 200 with a non-JSON body`,
1532
+ text
1533
+ );
1421
1534
  }
1535
+ }
1536
+ };
1537
+ function parseRelayerError(text) {
1538
+ try {
1539
+ const parsed = JSON.parse(text);
1422
1540
  return {
1423
- paid: true,
1424
- ...realizer.vault === void 0 ? {} : { fundingVault: realizer.vault },
1425
- status: response.status,
1426
- body: bodyText,
1427
- payment: {
1428
- amountRawUsdc: selected2.amountRawUsdc.toString(),
1429
- payTo: selected2.payTo,
1430
- feePayer: selected2.feePayer,
1431
- realizedRawUsdc: realized.realizedRawUsdc.toString(),
1432
- realizeTxSignature: realized.txSignature,
1433
- paymentTxSignature
1434
- }
1541
+ code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
1542
+ message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
1543
+ details: parsed.error?.details ?? null
1435
1544
  };
1545
+ } catch {
1546
+ return { code: null, message: null, details: null };
1436
1547
  }
1437
- /** Reads the challenge from the header (preferred) or the JSON body. */
1438
- async selectRequirement(probe) {
1439
- const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
1440
- let requirements;
1441
- try {
1442
- if (header !== null) {
1443
- requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
1444
- } else {
1445
- requirements = parseStandardChallenge(
1446
- await probe.json()
1447
- ).solanaExactRequirements;
1448
- }
1449
- } catch (error) {
1450
- throw new StandardX402PayError(
1451
- error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
1452
- "could not parse the x402 402 challenge",
1453
- error
1454
- );
1455
- }
1456
- try {
1457
- return selectPayableSolanaRequirement(requirements, {
1458
- network: this.network,
1459
- usdcMint: this.usdcMint
1460
- });
1461
- } catch (error) {
1462
- throw new StandardX402PayError(
1463
- "no_payable_requirement",
1464
- error instanceof Error ? error.message : String(error),
1465
- error
1466
- );
1467
- }
1548
+ }
1549
+
1550
+ // ../../src/client/relayer-yield-realizer.ts
1551
+ var REALIZE_OVERHEAD_RAW_USDC = 2500n;
1552
+ var RelayerRealizeError = class extends Error {
1553
+ constructor(code, message, detail = null, realizationSafeToRetry = false) {
1554
+ super(message);
1555
+ this.code = code;
1556
+ this.detail = detail;
1557
+ this.realizationSafeToRetry = realizationSafeToRetry;
1558
+ this.name = "RelayerRealizeError";
1468
1559
  }
1469
- track(record) {
1470
- const previous = this.pending.get(record.key);
1471
- this.pending.set(record.key, record);
1560
+ code;
1561
+ detail;
1562
+ realizationSafeToRetry;
1563
+ };
1564
+ var RelayerYieldRealizer = class {
1565
+ get vault() {
1566
+ return this.vaultFlows.vault.address;
1567
+ }
1568
+ realizationContext;
1569
+ vaultFlows;
1570
+ constructor(config) {
1571
+ this.vaultFlows = new VaultFlowClient({
1572
+ relayerBaseUrl: config.relayerBaseUrl,
1573
+ signer: config.signer,
1574
+ rpc: config.rpc,
1575
+ ...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
1576
+ ...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
1577
+ });
1578
+ this.realizationContext = Object.freeze({
1579
+ wallet: config.signer.walletAddress,
1580
+ vault: this.vaultFlows.vault.address,
1581
+ relayerBaseUrl: config.relayerBaseUrl.replace(/\/$/, "")
1582
+ });
1583
+ }
1584
+ async ensureUsdcAvailable(input) {
1585
+ const shortfallRawUsdc = input.amountRawUsdc;
1586
+ await this.assertSpendableYield(shortfallRawUsdc);
1587
+ let outcome;
1588
+ let submissionPossible = false;
1472
1589
  try {
1473
- this.persist();
1590
+ outcome = await this.vaultFlows.withdraw({
1591
+ amountRawUsdc: shortfallRawUsdc,
1592
+ // The relayer refuses to prepare this withdrawal beyond the spendable
1593
+ // yield — the principal-protection guard the client cannot bypass.
1594
+ purpose: "yield_realize",
1595
+ // Declares what is being paid so the relayer's spending-mandate layer
1596
+ // can enforce caps/payee and keep the mandate → payment audit chain.
1597
+ ...input.payment === void 0 ? {} : { payment: input.payment },
1598
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId },
1599
+ ...input.onPrepared === void 0 ? {} : { onPrepared: input.onPrepared },
1600
+ onBeforeSubmit: () => {
1601
+ submissionPossible = true;
1602
+ }
1603
+ });
1474
1604
  } catch (error) {
1475
- if (previous === void 0) {
1476
- this.pending.delete(record.key);
1477
- } else {
1478
- this.pending.set(record.key, previous);
1479
- }
1480
- throw error;
1605
+ if (error instanceof StandardX402PayError) throw error;
1606
+ throw this.mapWithdrawError(error, !submissionPossible);
1481
1607
  }
1608
+ return this.confirmedRealization(outcome);
1482
1609
  }
1483
- markUnknown(key, detail) {
1484
- const current = this.pending.get(key);
1485
- if (current === void 0) {
1486
- return;
1610
+ async resumeUsdcAvailable(input) {
1611
+ const outcome = await this.vaultFlows.resumeWithdrawal(input.prepared, {
1612
+ amountRawUsdc: input.amountRawUsdc,
1613
+ purpose: "yield_realize",
1614
+ payment: input.payment
1615
+ });
1616
+ return this.confirmedRealization(outcome);
1617
+ }
1618
+ confirmedRealization(outcome) {
1619
+ if (outcome.status !== "confirmed" || outcome.txSignature === null) {
1620
+ throw new RelayerRealizeError(
1621
+ "realize_not_confirmed",
1622
+ `yield realize withdrawal did not confirm (status=${outcome.status})`,
1623
+ outcome
1624
+ );
1487
1625
  }
1488
- const next = {
1489
- ...current,
1490
- status: "external_outcome_unknown",
1491
- updatedAtMs: this.nowMs(),
1492
- detail
1626
+ return {
1627
+ realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
1628
+ txSignature: outcome.txSignature,
1629
+ withdrawalId: outcome.withdrawalId
1493
1630
  };
1494
- this.pending.set(key, next);
1495
- try {
1496
- this.persist();
1497
- } catch (error) {
1498
- this.pending.set(key, current);
1499
- throw error;
1500
- }
1501
1631
  }
1502
- tryMarkUnknown(key, detail) {
1503
- try {
1504
- this.markUnknown(key, detail);
1505
- return null;
1506
- } catch (error) {
1507
- return error;
1508
- }
1632
+ /**
1633
+ * Best-effort report-back of the x402 payment tx this realize funded —
1634
+ * closes the relayer's mandate → realize → payment audit chain. Callers
1635
+ * must never let a failure here affect the payment result.
1636
+ */
1637
+ async reportPayment(input) {
1638
+ await this.vaultFlows.reportPayment(input);
1509
1639
  }
1510
- untrack(key) {
1511
- const previous = this.pending.get(key);
1512
- const existed = previous !== void 0;
1513
- this.pending.delete(key);
1640
+ /**
1641
+ * Refuses to realize more than the ledger's spendable yield (principal).
1642
+ * getBudget syncs the relayer's ledger from chain first (best-effort), so a
1643
+ * long-running client sees yield as it accrues instead of a frozen view.
1644
+ */
1645
+ async assertSpendableYield(shortfallRawUsdc) {
1646
+ let spendable;
1514
1647
  try {
1515
- this.persist();
1648
+ const budget = await this.vaultFlows.getBudget();
1649
+ spendable = BigInt(budget.spendableYieldRawUsdc);
1516
1650
  } catch (error) {
1517
- if (existed) {
1518
- this.pending.set(key, previous);
1519
- }
1520
- throw error;
1651
+ throw new RelayerRealizeError(
1652
+ "budget_unavailable",
1653
+ "could not read the spendable-yield budget",
1654
+ error,
1655
+ true
1656
+ );
1521
1657
  }
1522
- }
1523
- clearDelivered(key) {
1524
- const previous = this.pending.get(key);
1525
- this.pending.delete(key);
1526
- try {
1527
- this.persist();
1528
- } catch (error) {
1529
- if (previous !== void 0) {
1530
- this.pending.set(key, previous);
1531
- }
1532
- console.error(
1533
- `[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
1658
+ const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
1659
+ if (spendable < requiredRawUsdc) {
1660
+ throw new RelayerRealizeError(
1661
+ "insufficient_yield",
1662
+ `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`,
1663
+ { spendableYieldRawUsdc: spendable.toString() },
1664
+ true
1534
1665
  );
1535
1666
  }
1536
1667
  }
1537
- persist() {
1538
- if (this.stateStore === null) {
1539
- return;
1668
+ mapWithdrawError(error, safeToRetry) {
1669
+ if (!(error instanceof VaultFlowClientError)) {
1670
+ return new RelayerRealizeError(
1671
+ "prepare_failed",
1672
+ `yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
1673
+ error,
1674
+ safeToRetry
1675
+ );
1540
1676
  }
1541
- this.stateStore.save([...this.pending.values()]);
1677
+ const serverCode = error.code ?? errorCodeFrom(error.detail);
1678
+ if (serverCode === "approval_required") {
1679
+ return new RelayerRealizeError(
1680
+ "approval_required",
1681
+ "this payment exceeds the owner-approval threshold; nothing was realized or paid. Ask the owner to approve, then retry with the approvalId",
1682
+ error.errorDetails ?? error.detail,
1683
+ safeToRetry
1684
+ );
1685
+ }
1686
+ if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
1687
+ return new RelayerRealizeError(
1688
+ "insufficient_yield",
1689
+ "the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
1690
+ error.detail,
1691
+ safeToRetry
1692
+ );
1693
+ }
1694
+ return new RelayerRealizeError(
1695
+ error.step === "submit" ? "submit_failed" : "prepare_failed",
1696
+ error.message,
1697
+ error.detail,
1698
+ safeToRetry
1699
+ );
1542
1700
  }
1543
1701
  };
1544
- function pendingPaymentKey(input) {
1545
- return `${input.method}:${input.url}:${input.requestBodyHash}`;
1546
- }
1547
- function extractSettledPaymentTxSignature(response) {
1548
- const header = response.headers.get(PAYMENT_RESPONSE_HEADER) ?? response.headers.get("x-payment-response");
1549
- if (header === null || header.length === 0) {
1702
+ function errorCodeFrom(detail) {
1703
+ if (typeof detail !== "string") {
1550
1704
  return null;
1551
1705
  }
1552
1706
  try {
1553
- const decoded = JSON.parse(
1554
- Buffer.from(header, "base64").toString("utf8")
1555
- );
1556
- if (typeof decoded.transaction === "string" && decoded.transaction.length > 0) {
1557
- return decoded.transaction;
1558
- }
1559
- if (typeof decoded.txHash === "string" && decoded.txHash.length > 0) {
1560
- return decoded.txHash;
1561
- }
1562
- return null;
1707
+ const parsed = JSON.parse(detail);
1708
+ return typeof parsed.error?.code === "string" ? parsed.error.code : null;
1563
1709
  } catch {
1564
1710
  return null;
1565
1711
  }
1566
1712
  }
1567
1713
 
1714
+ // src/mcp-server.ts
1715
+ import { homedir } from "node:os";
1716
+ import { join as join2 } from "node:path";
1717
+
1718
+ // ../../src/client/mcp-payment-server.ts
1719
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1720
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1721
+ import {
1722
+ CallToolRequestSchema,
1723
+ ListToolsRequestSchema
1724
+ } from "@modelcontextprotocol/sdk/types.js";
1725
+
1726
+ // ../../src/client/onboarding.ts
1727
+ var SELF_SERVE_POLICY_ID = "self-serve";
1728
+ var OnboardingError = class extends Error {
1729
+ constructor(step, message, detail = null) {
1730
+ super(message);
1731
+ this.step = step;
1732
+ this.detail = detail;
1733
+ this.name = "OnboardingError";
1734
+ }
1735
+ step;
1736
+ detail;
1737
+ };
1738
+ async function ensureWalletOnboarded(params) {
1739
+ const fetchImpl = params.fetchImpl ?? fetch;
1740
+ const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
1741
+ const post = async (step, path, body) => {
1742
+ const url = `${baseUrl}${path}`;
1743
+ const serialized = JSON.stringify(body);
1744
+ const response = await fetchImpl(url, {
1745
+ method: "POST",
1746
+ headers: {
1747
+ ...await walletAuthHeaders({
1748
+ signer: params.signer,
1749
+ method: "POST",
1750
+ url,
1751
+ body: serialized
1752
+ }),
1753
+ "content-type": "application/json"
1754
+ },
1755
+ body: serialized
1756
+ });
1757
+ if (response.status !== 200) {
1758
+ let detail = null;
1759
+ try {
1760
+ detail = await response.json();
1761
+ } catch {
1762
+ detail = null;
1763
+ }
1764
+ throw new OnboardingError(
1765
+ step,
1766
+ `wallet onboarding ${step} failed with ${response.status}`,
1767
+ detail
1768
+ );
1769
+ }
1770
+ };
1771
+ const wallet = params.signer.walletAddress;
1772
+ const vault = params.vault ?? params.signer.vault?.address ?? SUBLY_VAULT.address;
1773
+ await post("register", "/v1/wallets/agent", {
1774
+ wallet,
1775
+ vault,
1776
+ signingPolicyId: SELF_SERVE_POLICY_ID,
1777
+ signingMode: "non_interactive",
1778
+ signerValidationMode: params.signer.validationMode,
1779
+ signerProvider: params.signer.provider ?? "local-keypair",
1780
+ activateForPayments: true
1781
+ });
1782
+ await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain", vault });
1783
+ }
1784
+
1785
+ // ../../src/client/paid-fetch.ts
1786
+ function formatRawUsdcAmount(raw) {
1787
+ const value = BigInt(raw);
1788
+ const negative = value < 0n;
1789
+ const abs = negative ? -value : value;
1790
+ const whole = abs / 1000000n;
1791
+ const frac = (abs % 1000000n).toString().padStart(6, "0");
1792
+ return `${negative ? "-" : ""}${whole}.${frac}`;
1793
+ }
1794
+
1568
1795
  // ../../src/client/mcp-payment-server.ts
1569
1796
  var TOOL_NAME = "fetch_with_subly_payment";
1570
1797
  var DEPOSIT_TOOL_NAME = "deposit_to_subly_vault";
@@ -1572,6 +1799,7 @@ var WITHDRAW_TOOL_NAME = "withdraw_from_subly_vault";
1572
1799
  var BUDGET_TOOL_NAME = "get_subly_yield_budget";
1573
1800
  var SETUP_TOOL_NAME = "create_subly_setup_link";
1574
1801
  var SETUP_STATUS_TOOL_NAME = "check_subly_setup";
1802
+ var OPERATION_STATUS_TOOL_NAME = "check_subly_vault_operation";
1575
1803
  var SERVER_INSTRUCTIONS = `Subly lets an agent pay standard x402 (HTTP 402) paid APIs that offer a Solana USDC exact rail with facilitator feePayer support from its wallet's Kamino vault YIELD \u2014 the relayer limits API spending to recorded yield, and the seller needs no Subly integration. With a configured vault catalog, call list_subly_vaults and select_subly_vault(vaultAddress) for the user's choice before owner setup. All subsequent tools use that vault until changed. Selection never moves existing funds; set up a separate mandate for each vault. Never select or switch vaults automatically based on APY.
1576
1804
 
1577
1805
  One-time setup: the operator needs a Solana agent wallet. Subly does NOT create wallets; either make a local keypair with \`solana-keygen new -o agent.json\` (or export one from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it, or use a custody wallet \u2014 set SUBLY_SIGNER_PROVIDER=circle (Circle developer-controlled wallet: CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, CIRCLE_WALLET_ID) or =privy (Privy server wallet incl. agentic/owner-key wallets: PRIVY_APP_ID, PRIVY_APP_SECRET, PRIVY_WALLET_ID, plus PRIVY_AUTHORIZATION_KEY for owner-key wallets). With a local keypair the private key never leaves that file; with a custody provider it never enters this machine at all. Then fund the wallet with USDC on Solana mainnet \u2014 a funded relayer sponsors vault transaction fees, which do not require agent SOL.
@@ -1582,15 +1810,39 @@ From there the agent can do everything with these tools:
1582
1810
  1. deposit_to_subly_vault(amountRawUsdc) puts wallet USDC into the vault (the minimum depends on the selected vault) so it starts earning yield. If it returns approvalRequired, paste the approveUrl to the user and retry with the approvalId after they approve; if it returns setupRequired, run the owner onboarding above first.
1583
1811
  2. get_subly_yield_budget() shows the principal, position value, and the spendable yield a payment can use right now.
1584
1812
  3. fetch_with_subly_payment(url) GETs or POSTs a paid resource from a compatible x402 seller (e.g. Nansen): it realizes just enough yield to the agent's USDC ATA and pays the seller's Solana USDC exact challenge, returning the body plus the payment details. If it returns insufficient_yield, that is expected \u2014 yield accrues over time; wait, do not loop. If it returns approvalRequired (payment above the owner's threshold; NOTHING was paid), paste the approveUrl to the user, and once they say they approved, repeat the SAME call adding the approvalId.
1585
- 4. withdraw_from_subly_vault(amountRawUsdc) exits: moves vault funds (principal included) back to the agent wallet's USDC account. If the owner's mandate requires withdrawal approval it returns approvalRequired \u2014 same paste-approveUrl-then-retry flow as deposits.`;
1813
+ 4. withdraw_from_subly_vault(amountRawUsdc) exits: moves vault funds (principal included) back to the agent wallet's USDC account. If the owner's mandate requires withdrawal approval it returns approvalRequired \u2014 same paste-approveUrl-then-retry flow as deposits.
1814
+ 5. check_subly_vault_operation(intentId) checks the original deposit or withdrawal after a timeout. Use the returned dep_... or wdr_... ID with the same wallet, vault and relayer. It reconciles that original transaction without preparing or sending another. If still confirming, check the same ID later; do not repeat the deposit or withdrawal.`;
1586
1815
  function createMcpPaymentServer(config) {
1587
1816
  const { signer: signer2, relayerBaseUrl: relayerBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
1588
1817
  const vaultFlows = config.vaultFlows ?? null;
1818
+ const fetchOnboardingAttempts = /* @__PURE__ */ new Map();
1819
+ const fetchInFlight = /* @__PURE__ */ new Map();
1589
1820
  const server = new Server(
1590
1821
  { name: "subly-payments", version: config.serverVersion ?? "0.3.0" },
1591
1822
  { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
1592
1823
  );
1593
1824
  const vaultTools = vaultFlows === null ? [] : [
1825
+ {
1826
+ name: OPERATION_STATUS_TOOL_NAME,
1827
+ description: "Read and reconcile the original deposit or withdrawal by its dep_... or wdr_... intent ID. Use after submitted/confirmation timeout, with the same wallet, selected vault and relayer. Does not prepare, sign or send a transaction, or refresh the budget. Returns status, amounts, transaction signature and next action.",
1828
+ inputSchema: {
1829
+ type: "object",
1830
+ properties: { intentId: {
1831
+ type: "string",
1832
+ pattern: "^(dep|wdr)_[0-9a-f]{32}$",
1833
+ description: "Original depositId or withdrawalId returned by the operation."
1834
+ } },
1835
+ required: ["intentId"],
1836
+ additionalProperties: false
1837
+ },
1838
+ annotations: {
1839
+ title: "Check Subly deposit or withdrawal",
1840
+ readOnlyHint: true,
1841
+ destructiveHint: false,
1842
+ idempotentHint: true,
1843
+ openWorldHint: false
1844
+ }
1845
+ },
1594
1846
  {
1595
1847
  name: SETUP_TOOL_NAME,
1596
1848
  description: "Create the one-time owner setup link for this agent wallet's Subly spending mandate. Use during onboarding (the first deposit conversation): agree the limits and first deposit in chat, call this, and paste the returned setupUrl to the user verbatim \u2014 it expires in 10 minutes and works once. The human opens it on their phone and confirms with Face ID (passkey) or a Solana wallet signature; that single confirmation activates the mandate AND pre-approves the initial deposit. The page is confirm-only: to change values, agree in chat and create a new link.",
@@ -1823,7 +2075,7 @@ function createMcpPaymentServer(config) {
1823
2075
  ...amountField,
1824
2076
  solscanUrl,
1825
2077
  stillConfirming: true,
1826
- warning: "the transaction was broadcast but had not confirmed before the poll timeout. Do NOT submit this deposit/withdrawal again \u2014 it may still confirm and moving the funds twice is not what the user asked for. Check get_subly_yield_budget in a minute, or the solscanUrl."
2078
+ warning: "the transaction was broadcast but had not confirmed before the poll timeout. Do NOT submit this deposit/withdrawal again \u2014 it may still confirm and moving the funds twice is not what the user asked for. Call check_subly_vault_operation with the original depositId or withdrawalId, keeping the same wallet, selected vault and relayer."
1827
2079
  },
1828
2080
  true
1829
2081
  );
@@ -1855,10 +2107,11 @@ function createMcpPaymentServer(config) {
1855
2107
  DEPOSIT_TOOL_NAME,
1856
2108
  WITHDRAW_TOOL_NAME,
1857
2109
  SETUP_TOOL_NAME,
1858
- SETUP_STATUS_TOOL_NAME
2110
+ SETUP_STATUS_TOOL_NAME,
2111
+ OPERATION_STATUS_TOOL_NAME
1859
2112
  ];
1860
2113
  if (vaultFlows2 !== null && vaultToolNames.includes(request.params.name)) {
1861
- const needsChainSync = request.params.name !== SETUP_TOOL_NAME && request.params.name !== SETUP_STATUS_TOOL_NAME;
2114
+ const needsChainSync = request.params.name !== SETUP_TOOL_NAME && request.params.name !== SETUP_STATUS_TOOL_NAME && request.params.name !== OPERATION_STATUS_TOOL_NAME;
1862
2115
  if (needsChainSync) {
1863
2116
  try {
1864
2117
  await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer3 });
@@ -1866,6 +2119,16 @@ function createMcpPaymentServer(config) {
1866
2119
  }
1867
2120
  }
1868
2121
  const args2 = request.params.arguments ?? {};
2122
+ if (request.params.name === OPERATION_STATUS_TOOL_NAME) {
2123
+ if (typeof args2.intentId !== "string") {
2124
+ return textResult({ ok: false, message: "intentId is required" }, true);
2125
+ }
2126
+ try {
2127
+ return textResult(await vaultFlows2.getOperationStatus(args2.intentId));
2128
+ } catch (error) {
2129
+ return vaultFlowFailure(error);
2130
+ }
2131
+ }
1869
2132
  if (request.params.name === BUDGET_TOOL_NAME) {
1870
2133
  try {
1871
2134
  const budget = await vaultFlows2.getBudget();
@@ -2025,16 +2288,34 @@ function createMcpPaymentServer(config) {
2025
2288
  Object.entries(args.headers).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v])
2026
2289
  ) : void 0;
2027
2290
  const mergedHeaders = body === void 0 ? headers : { "content-type": "application/json", ...headers ?? {} };
2028
- try {
2029
- const result = await payer2.pay({
2030
- url,
2031
- ...method === void 0 ? {} : { method },
2032
- ...body === void 0 ? {} : { body },
2033
- ...mergedHeaders === void 0 ? {} : { headers: mergedHeaders },
2034
- ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc },
2035
- ...forceNewPayment ? { forceNewPayment } : {},
2036
- ...approvalId === void 0 ? {} : { approvalId }
2291
+ const fetchKey = `${(method ?? "GET").toUpperCase()}:${url}:${requestBodyHashFor(body ?? null)}`;
2292
+ let paymentFlow = fetchInFlight.get(fetchKey);
2293
+ if (paymentFlow === void 0) {
2294
+ paymentFlow = (async () => {
2295
+ const onboardingKey = `${signer3.walletAddress}:${vaultFlows2?.vault.address ?? signer3.vault?.address ?? "default"}`;
2296
+ let onboarding = fetchOnboardingAttempts.get(onboardingKey);
2297
+ if (onboarding === void 0) {
2298
+ onboarding = ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer3 }).catch(() => {
2299
+ });
2300
+ fetchOnboardingAttempts.set(onboardingKey, onboarding);
2301
+ }
2302
+ await onboarding;
2303
+ return payer2.pay({
2304
+ url,
2305
+ ...method === void 0 ? {} : { method },
2306
+ ...body === void 0 ? {} : { body },
2307
+ ...mergedHeaders === void 0 ? {} : { headers: mergedHeaders },
2308
+ ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc },
2309
+ ...forceNewPayment ? { forceNewPayment } : {},
2310
+ ...approvalId === void 0 ? {} : { approvalId }
2311
+ });
2312
+ })().finally(() => {
2313
+ fetchInFlight.delete(fetchKey);
2037
2314
  });
2315
+ fetchInFlight.set(fetchKey, paymentFlow);
2316
+ }
2317
+ try {
2318
+ const result = await paymentFlow;
2038
2319
  return {
2039
2320
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2040
2321
  };
@@ -2087,14 +2368,6 @@ function createMcpPaymentServer(config) {
2087
2368
  async function runMcpPaymentServer(config) {
2088
2369
  const server = createMcpPaymentServer(config);
2089
2370
  const { signer: signer2, relayerBaseUrl: relayerBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
2090
- try {
2091
- await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer2 });
2092
- console.error("[subly-mcp] wallet registered and synced at the relayer");
2093
- } catch (error) {
2094
- console.error(
2095
- `[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
2096
- );
2097
- }
2098
2371
  const transport = new StdioServerTransport();
2099
2372
  await server.connect(transport);
2100
2373
  console.error(
@@ -3400,13 +3673,13 @@ async function createCircleSignerTransport(config) {
3400
3673
  import { createPrivateKey, createSign } from "node:crypto";
3401
3674
  var PROVIDER2 = "privy";
3402
3675
  var DEFAULT_BASE_URL2 = "https://api.privy.io";
3403
- function canonicalJson(value) {
3676
+ function canonicalJson2(value) {
3404
3677
  if (Array.isArray(value)) {
3405
- return `[${value.map(canonicalJson).join(",")}]`;
3678
+ return `[${value.map(canonicalJson2).join(",")}]`;
3406
3679
  }
3407
3680
  if (value !== null && typeof value === "object") {
3408
3681
  const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
3409
- return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
3682
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson2(v)}`).join(",")}}`;
3410
3683
  }
3411
3684
  return JSON.stringify(value);
3412
3685
  }
@@ -3435,7 +3708,7 @@ function authorizationSignature(params) {
3435
3708
  headers: { "privy-app-id": params.appId }
3436
3709
  };
3437
3710
  const signer2 = createSign("sha256");
3438
- signer2.update(canonicalJson(payload));
3711
+ signer2.update(canonicalJson2(payload));
3439
3712
  return signer2.sign(params.key).toString("base64");
3440
3713
  }
3441
3714
  async function createPrivySignerTransport(config) {
@@ -3614,7 +3887,7 @@ async function signerBundleForVault(bundle2, vault) {
3614
3887
  }
3615
3888
 
3616
3889
  // ../../src/client/standard-x402-state-store.ts
3617
- import { closeSync, mkdirSync, openSync, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "node:fs";
3890
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "node:fs";
3618
3891
  import { basename, dirname, join } from "node:path";
3619
3892
  function fileStandardX402StateStore(path) {
3620
3893
  return {
@@ -3659,6 +3932,9 @@ function fileStandardX402StateStore(path) {
3659
3932
  );
3660
3933
  }
3661
3934
  }
3935
+ if (new Set(parsed.map((record) => record.key)).size !== parsed.length) {
3936
+ throw new Error(`pending payment state has duplicate request keys: ${path}`);
3937
+ }
3662
3938
  return parsed;
3663
3939
  },
3664
3940
  save(records) {
@@ -3668,8 +3944,29 @@ function fileStandardX402StateStore(path) {
3668
3944
  directory,
3669
3945
  `.${basename(path)}.${process.pid}.${Date.now()}.tmp`
3670
3946
  );
3671
- writeFileSync(tempPath, JSON.stringify(records, null, 2), { mode: 384, flag: "wx" });
3672
- renameSync(tempPath, path);
3947
+ try {
3948
+ const fd = openSync(tempPath, "wx", 384);
3949
+ try {
3950
+ writeFileSync(fd, JSON.stringify(records, null, 2));
3951
+ fsyncSync(fd);
3952
+ } finally {
3953
+ closeSync(fd);
3954
+ }
3955
+ renameSync(tempPath, path);
3956
+ if (process.platform !== "win32") {
3957
+ const directoryFd = openSync(directory, "r");
3958
+ try {
3959
+ fsyncSync(directoryFd);
3960
+ } finally {
3961
+ closeSync(directoryFd);
3962
+ }
3963
+ }
3964
+ } finally {
3965
+ try {
3966
+ unlinkSync(tempPath);
3967
+ } catch {
3968
+ }
3969
+ }
3673
3970
  }
3674
3971
  };
3675
3972
  }
@@ -3681,7 +3978,20 @@ function isPendingPaymentRecord(value) {
3681
3978
  return false;
3682
3979
  }
3683
3980
  const record = value;
3684
- 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";
3981
+ 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";
3982
+ }
3983
+ function isObject(value) {
3984
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3985
+ }
3986
+ function isRecoveryRecord(value) {
3987
+ if (!isObject(value) || value.version !== 1 || !isObject(value.context)) return false;
3988
+ const context = value.context;
3989
+ 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;
3990
+ if (value.prepared === void 0) return true;
3991
+ const prepared = value.prepared;
3992
+ if (!isObject(prepared) || !isObject(prepared.signingIntent) || prepared.purpose !== "yield_realize" || !["withdrawalId", "serializedTransaction", "destinationUsdcAta", "requestedWithdrawRawUsdc"].every((key) => typeof prepared[key] === "string" && prepared[key] !== "")) return false;
3993
+ const intent = prepared.signingIntent;
3994
+ return intent.allowFullExit === false && ["wallet", "vault", "farm", "shareMint", "asset", "destinationUsdcAta", "maxSharesToRedeemRaw", "feePayer", "expiresAt", "preparedMessageHash"].every((key) => typeof intent[key] === "string" && intent[key] !== "");
3685
3995
  }
3686
3996
 
3687
3997
  // ../../src/solana/rpc.ts