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