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