@subly_fi/pay 0.1.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.
@@ -0,0 +1,1916 @@
1
+ // ../../demo/mcp-server.ts
2
+ import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+
10
+ // ../../src/client/agent-wallet-signer.ts
11
+ import { signBytes } from "@solana/kit";
12
+ import bs584 from "bs58";
13
+
14
+ // ../../src/solana/tx.ts
15
+ import bs58 from "bs58";
16
+ import {
17
+ appendTransactionMessageInstructions,
18
+ compileTransaction,
19
+ compressTransactionMessageUsingAddressLookupTables,
20
+ createTransactionMessage,
21
+ getBase64EncodedWireTransaction,
22
+ getTransactionDecoder,
23
+ partiallySignTransaction,
24
+ pipe,
25
+ setTransactionMessageFeePayer,
26
+ setTransactionMessageLifetimeUsingBlockhash
27
+ } from "@solana/kit";
28
+
29
+ // ../../src/lib/hash.ts
30
+ import { createHash } from "node:crypto";
31
+ var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
32
+ function sha256TaggedHex(data) {
33
+ return `sha256-${createHash("sha256").update(data).digest("hex")}`;
34
+ }
35
+ function stableStringify(value) {
36
+ if (value === null) {
37
+ return "null";
38
+ }
39
+ if (typeof value === "bigint") {
40
+ return JSON.stringify(value.toString());
41
+ }
42
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
43
+ return JSON.stringify(value);
44
+ }
45
+ if (Array.isArray(value)) {
46
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
47
+ }
48
+ const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
49
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
50
+ }
51
+ function hashStableJson(value) {
52
+ return sha256TaggedHex(stableStringify(value));
53
+ }
54
+
55
+ // ../../src/solana/tx.ts
56
+ function decodeSerializedTransaction(serializedBase64) {
57
+ return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
58
+ }
59
+ async function addSignaturesToSerializedTransaction(params) {
60
+ const decoded = decodeSerializedTransaction(params.serializedBase64);
61
+ const signed = await partiallySignTransaction(params.signers, decoded);
62
+ return {
63
+ serializedBase64: getBase64EncodedWireTransaction(signed),
64
+ transaction: signed
65
+ };
66
+ }
67
+ function signatureBase58ForSigner(transaction, signer2) {
68
+ const signature = transaction.signatures[signer2];
69
+ if (signature === null || signature === void 0) {
70
+ return null;
71
+ }
72
+ return bs58.encode(signature);
73
+ }
74
+
75
+ // ../../src/client/transaction-intent-validator.ts
76
+ import bs583 from "bs58";
77
+ import { getCompiledTransactionMessageDecoder } from "@solana/kit";
78
+
79
+ // ../../src/config/constants.ts
80
+ var PAYMENT_SCHEME = "subly-yield-exact";
81
+ var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
82
+ var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
83
+ var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
84
+ var SUBLY_VAULT = {
85
+ name: "Subly USDC Payment Vault Alpha",
86
+ address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
87
+ programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
88
+ usdcMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
89
+ shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
90
+ lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
91
+ farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
92
+ };
93
+ var USDC_DECIMALS = 6;
94
+
95
+ // ../../src/domain/request-binding.ts
96
+ function computeRequestBindingHash(fields) {
97
+ return hashStableJson({
98
+ sellerRequestId: fields.sellerRequestId,
99
+ httpMethod: fields.httpMethod.toUpperCase(),
100
+ canonicalResourceUrl: fields.canonicalResourceUrl,
101
+ requestBodyHash: fields.requestBodyHash,
102
+ seller: fields.seller,
103
+ asset: fields.asset,
104
+ amountRawUsdc: fields.amountRawUsdc,
105
+ payTo: fields.payTo,
106
+ sellerUsdcAta: fields.sellerUsdcAta
107
+ });
108
+ }
109
+
110
+ // ../../src/lib/associated-token-account.ts
111
+ import { createHash as createHash2 } from "node:crypto";
112
+ import bs582 from "bs58";
113
+ var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
114
+ var ED25519_P = (1n << 255n) - 19n;
115
+ var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
116
+ function deriveAssociatedTokenAddress(params) {
117
+ const owner = decodePublicKey(params.owner, "owner");
118
+ const mint = decodePublicKey(params.mint ?? SUBLY_VAULT.usdcMint, "mint");
119
+ const tokenProgramId = decodePublicKey(
120
+ params.tokenProgramId ?? SPL_TOKEN_PROGRAM_ID,
121
+ "tokenProgramId"
122
+ );
123
+ const associatedTokenProgramId = decodePublicKey(
124
+ ASSOCIATED_TOKEN_PROGRAM_ID,
125
+ "associatedTokenProgramId"
126
+ );
127
+ for (let bump = 255; bump >= 0; bump -= 1) {
128
+ const address2 = createProgramAddress(
129
+ [owner, tokenProgramId, mint, Uint8Array.of(bump)],
130
+ associatedTokenProgramId
131
+ );
132
+ if (address2 !== null) {
133
+ return bs582.encode(address2);
134
+ }
135
+ }
136
+ throw new Error("Unable to derive associated token account address");
137
+ }
138
+ function createProgramAddress(seeds, programId) {
139
+ const hash = createHash2("sha256");
140
+ for (const seed of seeds) {
141
+ hash.update(seed);
142
+ }
143
+ hash.update(programId);
144
+ hash.update(PDA_MARKER);
145
+ const digest = hash.digest();
146
+ return isEd25519Point(digest) ? null : new Uint8Array(digest);
147
+ }
148
+ function decodePublicKey(value, fieldName) {
149
+ const decoded = bs582.decode(value);
150
+ if (decoded.length !== 32) {
151
+ throw new Error(`${fieldName} must be a 32-byte public key`);
152
+ }
153
+ return decoded;
154
+ }
155
+ function isEd25519Point(bytes) {
156
+ if (bytes.length !== 32) {
157
+ return false;
158
+ }
159
+ const yBytes = Uint8Array.from(bytes);
160
+ yBytes[31] = yBytes[31] & 127;
161
+ const y = littleEndianToBigInt(yBytes);
162
+ if (y >= ED25519_P) {
163
+ return false;
164
+ }
165
+ const ySquared = mod(y * y, ED25519_P);
166
+ const numerator = mod(ySquared - 1n, ED25519_P);
167
+ const denominator = mod(ED25519_D * ySquared + 1n, ED25519_P);
168
+ if (denominator === 0n) {
169
+ return false;
170
+ }
171
+ const xSquared = mod(
172
+ numerator * modPow(denominator, ED25519_P - 2n, ED25519_P),
173
+ ED25519_P
174
+ );
175
+ return xSquared === 0n || modPow(xSquared, (ED25519_P - 1n) / 2n, ED25519_P) === 1n;
176
+ }
177
+ function littleEndianToBigInt(bytes) {
178
+ let value = 0n;
179
+ for (let index = bytes.length - 1; index >= 0; index -= 1) {
180
+ value = (value << 8n) + BigInt(bytes[index]);
181
+ }
182
+ return value;
183
+ }
184
+ function mod(value, modulus) {
185
+ const result = value % modulus;
186
+ return result >= 0n ? result : result + modulus;
187
+ }
188
+ function modPow(base, exponent, modulus) {
189
+ let result = 1n;
190
+ let nextBase = mod(base, modulus);
191
+ let nextExponent = exponent;
192
+ while (nextExponent > 0n) {
193
+ if ((nextExponent & 1n) === 1n) {
194
+ result = mod(result * nextBase, modulus);
195
+ }
196
+ nextBase = mod(nextBase * nextBase, modulus);
197
+ nextExponent >>= 1n;
198
+ }
199
+ return result;
200
+ }
201
+
202
+ // ../../src/client/transaction-intent-validator.ts
203
+ var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
204
+ var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
205
+ var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
206
+ var MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
207
+ var KVAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
208
+ var KAMINO_FARMS_PROGRAM_ID = "FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr";
209
+ var KVAULT_WITHDRAW_DISCRIMINATOR = Uint8Array.from([
210
+ 183,
211
+ 18,
212
+ 70,
213
+ 156,
214
+ 148,
215
+ 109,
216
+ 161,
217
+ 34
218
+ ]);
219
+ var KVAULT_WITHDRAW_FROM_AVAILABLE_DISCRIMINATOR = Uint8Array.from([
220
+ 19,
221
+ 131,
222
+ 112,
223
+ 155,
224
+ 170,
225
+ 220,
226
+ 34,
227
+ 57
228
+ ]);
229
+ var KVAULT_DEPOSIT_DISCRIMINATOR = Uint8Array.from([
230
+ 242,
231
+ 35,
232
+ 198,
233
+ 137,
234
+ 82,
235
+ 225,
236
+ 242,
237
+ 182
238
+ ]);
239
+ var U64_MAX = 18446744073709551615n;
240
+ var MAX_TEMP_ACCOUNT_LAMPORTS = 10000000n;
241
+ var DEFAULT_MAX_COMPUTE_UNIT_LIMIT = 14e5;
242
+ var DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS = 100000n;
243
+ var IntentValidationError = class extends Error {
244
+ reason;
245
+ constructor(reason, message) {
246
+ super(message);
247
+ this.name = "IntentValidationError";
248
+ this.reason = reason;
249
+ }
250
+ };
251
+ function reject(reason, message) {
252
+ throw new IntentValidationError(reason, message);
253
+ }
254
+ function decodeIntentTransaction(params) {
255
+ const wire = Buffer.from(params.serializedTransaction, "base64");
256
+ const signatureCount = readShortVec(wire, 0);
257
+ if (signatureCount === null) {
258
+ reject("invalid_transaction_encoding", "Cannot parse signature count");
259
+ }
260
+ const messageOffset = signatureCount.nextOffset + signatureCount.value * 64;
261
+ if (messageOffset >= wire.length) {
262
+ reject("invalid_transaction_encoding", "Transaction has no message bytes");
263
+ }
264
+ const messageBytes = wire.subarray(messageOffset);
265
+ const compiled = getCompiledTransactionMessageDecoder().decode(messageBytes);
266
+ if (compiled.version !== 0) {
267
+ reject("unsupported_transaction_version", "Only v0 transactions are supported");
268
+ }
269
+ const staticAccounts = compiled.staticAccounts.map(String);
270
+ const loadedWritable = [];
271
+ const loadedReadonly = [];
272
+ const lookups = compiled.addressTableLookups ?? [];
273
+ for (const rawLookup of lookups) {
274
+ const lookup = rawLookup;
275
+ const table = params.lookupTables?.[String(lookup.lookupTableAddress)];
276
+ if (table === void 0) {
277
+ reject(
278
+ "lookup_table_unresolved",
279
+ `Transaction references unknown lookup table ${lookup.lookupTableAddress}`
280
+ );
281
+ }
282
+ const writableIndexes = lookup.writableIndexes ?? lookup.writableIndices ?? [];
283
+ const readonlyIndexes = lookup.readonlyIndexes ?? lookup.readableIndices ?? [];
284
+ for (const index of writableIndexes) {
285
+ const resolved = table[index];
286
+ if (resolved === void 0) {
287
+ reject("lookup_table_unresolved", "Lookup table index out of range");
288
+ }
289
+ loadedWritable.push(String(resolved));
290
+ }
291
+ for (const index of readonlyIndexes) {
292
+ const resolved = table[index];
293
+ if (resolved === void 0) {
294
+ reject("lookup_table_unresolved", "Lookup table index out of range");
295
+ }
296
+ loadedReadonly.push(String(resolved));
297
+ }
298
+ }
299
+ const orderedAccounts = [...staticAccounts, ...loadedWritable, ...loadedReadonly];
300
+ const instructions = compiled.instructions.map(
301
+ (instruction) => {
302
+ const programAddress = orderedAccounts[instruction.programAddressIndex];
303
+ if (programAddress === void 0) {
304
+ reject("invalid_transaction_encoding", "Program index out of range");
305
+ }
306
+ const accounts = (instruction.accountIndices ?? []).map((index) => {
307
+ const account = orderedAccounts[index];
308
+ if (account === void 0) {
309
+ reject("invalid_transaction_encoding", "Account index out of range");
310
+ }
311
+ return account;
312
+ });
313
+ return {
314
+ programAddress,
315
+ accounts,
316
+ data: instruction.data === void 0 ? new Uint8Array() : Uint8Array.from(instruction.data)
317
+ };
318
+ }
319
+ );
320
+ const feePayer = staticAccounts[0];
321
+ if (feePayer === void 0) {
322
+ reject("invalid_transaction_encoding", "Transaction has no fee payer");
323
+ }
324
+ return {
325
+ feePayer,
326
+ requiredSigners: staticAccounts.slice(0, compiled.header.numSignerAccounts),
327
+ instructions,
328
+ messageHash: sha256TaggedHex(Buffer.from(messageBytes))
329
+ };
330
+ }
331
+ function validatePaymentIntentTransaction(params) {
332
+ const { intent } = params;
333
+ const now = params.nowMs ?? Date.now();
334
+ const policy = resolveIntentValidationPolicy(params.policy);
335
+ if (new Date(intent.expiresAt).getTime() <= now) {
336
+ reject("expired", "Payment intent has expired");
337
+ }
338
+ if (intent.scheme !== PAYMENT_SCHEME) {
339
+ reject("scheme_mismatch", `scheme must be ${PAYMENT_SCHEME}`);
340
+ }
341
+ if (intent.network !== SOLANA_MAINNET_NETWORK) {
342
+ reject("network_mismatch", "Unsupported network");
343
+ }
344
+ if (intent.vault !== SUBLY_VAULT.address) {
345
+ reject("vault_mismatch", "Unsupported vault");
346
+ }
347
+ if (intent.shareMint !== SUBLY_VAULT.shareMint) {
348
+ reject("share_mint_mismatch", "Unsupported share mint");
349
+ }
350
+ if (intent.asset !== SUBLY_VAULT.usdcMint) {
351
+ reject("asset_mismatch", "Only USDC payments are supported");
352
+ }
353
+ if (intent.memo !== intent.paymentId) {
354
+ reject("memo_mismatch", "Memo must equal the paymentId");
355
+ }
356
+ const expectedBinding = computeRequestBindingHash({
357
+ sellerRequestId: intent.sellerRequestId,
358
+ httpMethod: intent.httpMethod,
359
+ canonicalResourceUrl: intent.canonicalResourceUrl,
360
+ requestBodyHash: intent.requestBodyHash,
361
+ seller: intent.seller,
362
+ asset: intent.asset,
363
+ amountRawUsdc: intent.amountRawUsdc,
364
+ payTo: intent.payTo,
365
+ sellerUsdcAta: intent.sellerUsdcAta
366
+ });
367
+ if (expectedBinding !== intent.requestBindingHash) {
368
+ reject(
369
+ "request_binding_mismatch",
370
+ "requestBindingHash does not match the request fields"
371
+ );
372
+ }
373
+ const expectedSellerAta = deriveAssociatedTokenAddress({
374
+ owner: intent.payTo,
375
+ mint: intent.asset
376
+ });
377
+ if (expectedSellerAta !== intent.sellerUsdcAta) {
378
+ reject(
379
+ "seller_ata_mismatch",
380
+ "sellerUsdcAta must be the associated USDC account for payTo"
381
+ );
382
+ }
383
+ const expectedDustAta = deriveAssociatedTokenAddress({
384
+ owner: intent.wallet,
385
+ mint: intent.asset
386
+ });
387
+ if (expectedDustAta !== intent.dustRecipientUsdcAta) {
388
+ reject(
389
+ "dust_recipient_mismatch",
390
+ "dustRecipientUsdcAta must be the agent wallet's USDC ATA"
391
+ );
392
+ }
393
+ const decoded = decodeIntentTransaction({
394
+ serializedTransaction: params.serializedTransaction,
395
+ ...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
396
+ });
397
+ if (decoded.messageHash !== intent.preparedMessageHash) {
398
+ reject("message_hash_mismatch", "Prepared message hash mismatch");
399
+ }
400
+ if (decoded.feePayer !== intent.feePayer) {
401
+ reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
402
+ }
403
+ const expectedSigners = /* @__PURE__ */ new Set([
404
+ intent.feePayer,
405
+ intent.wallet,
406
+ intent.temporarySettlementTokenAccount
407
+ ]);
408
+ if (decoded.requiredSigners.length !== expectedSigners.size || !decoded.requiredSigners.every((signer2) => expectedSigners.has(signer2))) {
409
+ reject(
410
+ "unexpected_signers",
411
+ "Transaction signers must be exactly the sponsor, the agent wallet, and the temporary settlement account"
412
+ );
413
+ }
414
+ const ixs = [...decoded.instructions];
415
+ expectComputeBudgetPair(ixs, policy);
416
+ expectCreateTemporaryAccount(ixs, intent, policy);
417
+ expectInitializeTemporaryAccount(ixs, intent);
418
+ consumeFarmInstructions(ixs, intent.wallet);
419
+ expectKvaultWithdraw(ixs, {
420
+ wallet: intent.wallet,
421
+ vault: intent.vault,
422
+ shareMint: intent.shareMint,
423
+ asset: intent.asset,
424
+ userTokenAccount: intent.temporarySettlementTokenAccount,
425
+ maxSharesToRedeemRaw: BigInt(intent.maxSharesToRedeemRaw),
426
+ allowFullExit: false
427
+ });
428
+ expectTransferChecked(ixs, {
429
+ source: intent.temporarySettlementTokenAccount,
430
+ mint: intent.asset,
431
+ destination: intent.sellerUsdcAta,
432
+ authority: intent.wallet,
433
+ amount: BigInt(intent.amountRawUsdc),
434
+ label: "seller transfer"
435
+ });
436
+ if (ixs[0] !== void 0 && ixs[0].programAddress === SPL_TOKEN_PROGRAM_ID && ixs[0].data[0] === 12) {
437
+ expectTransferChecked(ixs, {
438
+ source: intent.temporarySettlementTokenAccount,
439
+ mint: intent.asset,
440
+ destination: intent.dustRecipientUsdcAta,
441
+ authority: intent.wallet,
442
+ amount: null,
443
+ label: "dust sweep"
444
+ });
445
+ }
446
+ expectCloseAccount(ixs, {
447
+ account: intent.temporarySettlementTokenAccount,
448
+ destination: intent.feePayer,
449
+ owner: intent.wallet
450
+ });
451
+ expectMemo(ixs, intent.memo);
452
+ if (ixs.length > 0) {
453
+ reject(
454
+ "unexpected_instruction",
455
+ `Transaction contains ${ixs.length} unexpected trailing instruction(s)`
456
+ );
457
+ }
458
+ }
459
+ function validateDepositIntentTransaction(params) {
460
+ const { intent } = params;
461
+ const now = params.nowMs ?? Date.now();
462
+ const policy = resolveIntentValidationPolicy(params.policy);
463
+ if (new Date(intent.expiresAt).getTime() <= now) {
464
+ reject("expired", "Deposit intent has expired");
465
+ }
466
+ assertVaultIntentTargets(intent);
467
+ const decoded = decodeIntentTransaction({
468
+ serializedTransaction: params.serializedTransaction,
469
+ ...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
470
+ });
471
+ if (decoded.messageHash !== intent.preparedMessageHash) {
472
+ reject("message_hash_mismatch", "Prepared message hash mismatch");
473
+ }
474
+ if (decoded.feePayer !== intent.feePayer) {
475
+ reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
476
+ }
477
+ let sawDeposit = false;
478
+ for (const ix of decoded.instructions) {
479
+ switch (ix.programAddress) {
480
+ case COMPUTE_BUDGET_PROGRAM_ID:
481
+ validateComputeBudgetInstruction(ix, policy);
482
+ break;
483
+ case ASSOCIATED_TOKEN_PROGRAM_ID2:
484
+ expectAtaCreateForOwner(ix, intent.wallet);
485
+ break;
486
+ case MEMO_PROGRAM_ID:
487
+ break;
488
+ case KVAULT_PROGRAM_ID: {
489
+ if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
490
+ reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
491
+ }
492
+ const maxAmount = readU64LE(ix.data, 8);
493
+ if (maxAmount !== BigInt(intent.amountRawUsdc)) {
494
+ reject("amount_mismatch", "Deposit amount does not match the intent");
495
+ }
496
+ if (ix.accounts[0] !== intent.wallet) {
497
+ reject("wallet_mismatch", "Deposit user is not the agent wallet");
498
+ }
499
+ if (ix.accounts[1] !== intent.vault) {
500
+ reject("vault_mismatch", "Deposit vault mismatch");
501
+ }
502
+ if (ix.accounts[3] !== intent.asset) {
503
+ reject("asset_mismatch", "Deposit token mint mismatch");
504
+ }
505
+ if (ix.accounts[5] !== intent.shareMint) {
506
+ reject("share_mint_mismatch", "Deposit share mint mismatch");
507
+ }
508
+ const expectedSourceAta = deriveAssociatedTokenAddress({
509
+ owner: intent.wallet,
510
+ mint: intent.asset
511
+ });
512
+ if (ix.accounts[6] !== expectedSourceAta) {
513
+ reject(
514
+ "source_ata_mismatch",
515
+ "Deposit source must be the agent wallet's USDC ATA"
516
+ );
517
+ }
518
+ sawDeposit = true;
519
+ break;
520
+ }
521
+ default:
522
+ reject(
523
+ "unexpected_instruction",
524
+ `Unexpected program ${ix.programAddress} in deposit transaction`
525
+ );
526
+ }
527
+ }
528
+ if (!sawDeposit) {
529
+ reject("missing_instruction", "Deposit transaction has no KVault deposit");
530
+ }
531
+ }
532
+ function validateWithdrawalIntentTransaction(params) {
533
+ const { intent } = params;
534
+ const now = params.nowMs ?? Date.now();
535
+ const policy = resolveIntentValidationPolicy(params.policy);
536
+ if (new Date(intent.expiresAt).getTime() <= now) {
537
+ reject("expired", "Withdrawal intent has expired");
538
+ }
539
+ assertVaultIntentTargets(intent);
540
+ const expectedDestination = deriveAssociatedTokenAddress({
541
+ owner: intent.wallet,
542
+ mint: intent.asset
543
+ });
544
+ if (expectedDestination !== intent.destinationUsdcAta) {
545
+ reject(
546
+ "destination_mismatch",
547
+ "Withdrawal destination must be the agent wallet's USDC ATA"
548
+ );
549
+ }
550
+ const decoded = decodeIntentTransaction({
551
+ serializedTransaction: params.serializedTransaction,
552
+ ...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
553
+ });
554
+ if (decoded.messageHash !== intent.preparedMessageHash) {
555
+ reject("message_hash_mismatch", "Prepared message hash mismatch");
556
+ }
557
+ if (decoded.feePayer !== intent.feePayer) {
558
+ reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
559
+ }
560
+ let sawWithdraw = false;
561
+ for (const ix of decoded.instructions) {
562
+ switch (ix.programAddress) {
563
+ case COMPUTE_BUDGET_PROGRAM_ID:
564
+ validateComputeBudgetInstruction(ix, policy);
565
+ break;
566
+ case MEMO_PROGRAM_ID:
567
+ case KAMINO_FARMS_PROGRAM_ID:
568
+ break;
569
+ case ASSOCIATED_TOKEN_PROGRAM_ID2:
570
+ expectAtaCreateForOwner(ix, intent.wallet);
571
+ break;
572
+ case SPL_TOKEN_PROGRAM_ID: {
573
+ if (ix.data[0] !== 9) {
574
+ reject(
575
+ "unexpected_instruction",
576
+ "Only CloseAccount token instructions are allowed in withdrawals"
577
+ );
578
+ }
579
+ if (ix.accounts[1] !== intent.wallet || ix.accounts[2] !== intent.wallet) {
580
+ reject(
581
+ "unexpected_instruction",
582
+ "Withdrawal CloseAccount must pay out to the agent wallet"
583
+ );
584
+ }
585
+ break;
586
+ }
587
+ case KVAULT_PROGRAM_ID: {
588
+ validateKvaultWithdrawInstruction(ix, {
589
+ wallet: intent.wallet,
590
+ vault: intent.vault,
591
+ shareMint: intent.shareMint,
592
+ asset: intent.asset,
593
+ userTokenAccount: intent.destinationUsdcAta,
594
+ maxSharesToRedeemRaw: BigInt(intent.maxSharesToRedeemRaw),
595
+ allowFullExit: intent.allowFullExit
596
+ });
597
+ sawWithdraw = true;
598
+ break;
599
+ }
600
+ default:
601
+ reject(
602
+ "unexpected_instruction",
603
+ `Unexpected program ${ix.programAddress} in withdrawal transaction`
604
+ );
605
+ }
606
+ }
607
+ if (!sawWithdraw) {
608
+ reject("missing_instruction", "Withdrawal transaction has no KVault withdraw");
609
+ }
610
+ }
611
+ function assertVaultIntentTargets(intent) {
612
+ if (intent.vault !== SUBLY_VAULT.address) {
613
+ reject("vault_mismatch", "Unsupported vault");
614
+ }
615
+ if (intent.shareMint !== SUBLY_VAULT.shareMint) {
616
+ reject("share_mint_mismatch", "Unsupported share mint");
617
+ }
618
+ if (intent.asset !== SUBLY_VAULT.usdcMint) {
619
+ reject("asset_mismatch", "Only USDC is supported");
620
+ }
621
+ }
622
+ function resolveIntentValidationPolicy(policy) {
623
+ const resolved = {
624
+ maxComputeUnitLimit: policy?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
625
+ maxComputeUnitPriceMicroLamports: policy?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
626
+ maxTemporaryAccountLamports: policy?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
627
+ };
628
+ if (!Number.isSafeInteger(resolved.maxComputeUnitLimit) || resolved.maxComputeUnitLimit <= 0) {
629
+ reject("invalid_policy", "maxComputeUnitLimit must be a positive safe integer");
630
+ }
631
+ if (resolved.maxComputeUnitPriceMicroLamports < 0n) {
632
+ reject(
633
+ "invalid_policy",
634
+ "maxComputeUnitPriceMicroLamports must be non-negative"
635
+ );
636
+ }
637
+ if (resolved.maxTemporaryAccountLamports <= 0n) {
638
+ reject("invalid_policy", "maxTemporaryAccountLamports must be positive");
639
+ }
640
+ return resolved;
641
+ }
642
+ function expectComputeBudgetPair(ixs, policy) {
643
+ for (const discriminator of [2, 3]) {
644
+ const ix = ixs.shift();
645
+ if (ix === void 0 || ix.programAddress !== COMPUTE_BUDGET_PROGRAM_ID || ix.data[0] !== discriminator) {
646
+ reject(
647
+ "compute_budget_mismatch",
648
+ "Transaction must start with ComputeBudget limit and price instructions"
649
+ );
650
+ }
651
+ validateComputeBudgetInstruction(ix, policy);
652
+ }
653
+ }
654
+ function validateComputeBudgetInstruction(ix, policy) {
655
+ switch (ix.data[0]) {
656
+ case 2: {
657
+ const units = readU32LE(ix.data, 1);
658
+ if (units <= 0 || units > policy.maxComputeUnitLimit) {
659
+ reject(
660
+ "compute_budget_mismatch",
661
+ `Compute unit limit ${units} exceeds policy maximum ${policy.maxComputeUnitLimit}`
662
+ );
663
+ }
664
+ break;
665
+ }
666
+ case 3: {
667
+ const microLamports = readU64LE(ix.data, 1);
668
+ if (microLamports > policy.maxComputeUnitPriceMicroLamports) {
669
+ reject(
670
+ "compute_budget_mismatch",
671
+ `Compute unit price ${microLamports} exceeds policy maximum ${policy.maxComputeUnitPriceMicroLamports}`
672
+ );
673
+ }
674
+ break;
675
+ }
676
+ default:
677
+ reject("compute_budget_mismatch", "Unexpected ComputeBudget instruction");
678
+ }
679
+ }
680
+ function expectCreateTemporaryAccount(ixs, intent, policy) {
681
+ const ix = ixs.shift();
682
+ if (ix === void 0 || ix.programAddress !== SYSTEM_PROGRAM_ID) {
683
+ reject("temp_account_mismatch", "Expected System createAccount instruction");
684
+ }
685
+ if (ix.data.length < 52 || readU32LE(ix.data, 0) !== 0) {
686
+ reject("temp_account_mismatch", "Expected createAccount discriminator");
687
+ }
688
+ const lamports = readU64LE(ix.data, 4);
689
+ const space = readU64LE(ix.data, 12);
690
+ const owner = bs583.encode(ix.data.subarray(20, 52));
691
+ if (space !== 165n) {
692
+ reject("temp_account_mismatch", "Temporary account space must be 165 bytes");
693
+ }
694
+ if (owner !== SPL_TOKEN_PROGRAM_ID) {
695
+ reject("temp_account_mismatch", "Temporary account owner must be the token program");
696
+ }
697
+ if (lamports > policy.maxTemporaryAccountLamports) {
698
+ reject("temp_account_mismatch", "Temporary account rent exceeds the cap");
699
+ }
700
+ if (ix.accounts[0] !== intent.feePayer) {
701
+ reject("temp_account_mismatch", "Temporary account must be funded by the sponsor");
702
+ }
703
+ if (ix.accounts[1] !== intent.temporarySettlementTokenAccount) {
704
+ reject("temp_account_mismatch", "createAccount target is not the temporary account");
705
+ }
706
+ }
707
+ function expectInitializeTemporaryAccount(ixs, intent) {
708
+ const ix = ixs.shift();
709
+ if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 18) {
710
+ reject("temp_account_mismatch", "Expected InitializeAccount3 instruction");
711
+ }
712
+ const owner = bs583.encode(ix.data.subarray(1, 33));
713
+ if (owner !== intent.wallet) {
714
+ reject(
715
+ "temp_account_mismatch",
716
+ "Temporary account token authority must be the agent wallet"
717
+ );
718
+ }
719
+ if (ix.accounts[0] !== intent.temporarySettlementTokenAccount) {
720
+ reject("temp_account_mismatch", "InitializeAccount3 target mismatch");
721
+ }
722
+ if (ix.accounts[1] !== intent.asset) {
723
+ reject("temp_account_mismatch", "Temporary account mint must be USDC");
724
+ }
725
+ }
726
+ function consumeFarmInstructions(ixs, wallet) {
727
+ while (ixs[0] !== void 0 && ixs[0].programAddress === KAMINO_FARMS_PROGRAM_ID) {
728
+ const ix = ixs.shift();
729
+ if (!ix.accounts.includes(wallet)) {
730
+ reject(
731
+ "farm_instruction_mismatch",
732
+ "Farm unstake instruction does not reference the agent wallet"
733
+ );
734
+ }
735
+ }
736
+ }
737
+ function expectKvaultWithdraw(ixs, expectation) {
738
+ const ix = ixs.shift();
739
+ if (ix === void 0 || ix.programAddress !== KVAULT_PROGRAM_ID) {
740
+ reject("withdraw_mismatch", "Expected KVault withdraw instruction");
741
+ }
742
+ validateKvaultWithdrawInstruction(ix, expectation);
743
+ }
744
+ function validateKvaultWithdrawInstruction(ix, expectation) {
745
+ const isWithdraw = bytesStartWith(ix.data, KVAULT_WITHDRAW_DISCRIMINATOR);
746
+ const isWithdrawFromAvailable = bytesStartWith(
747
+ ix.data,
748
+ KVAULT_WITHDRAW_FROM_AVAILABLE_DISCRIMINATOR
749
+ );
750
+ if (!isWithdraw && !isWithdrawFromAvailable) {
751
+ reject("withdraw_mismatch", "Unexpected KVault instruction");
752
+ }
753
+ const sharesAmount = readU64LE(ix.data, 8);
754
+ const fullExit = sharesAmount === U64_MAX;
755
+ if (fullExit && !expectation.allowFullExit) {
756
+ reject("withdraw_mismatch", "Full-exit share burn is not allowed for this intent");
757
+ }
758
+ if (!fullExit && sharesAmount > expectation.maxSharesToRedeemRaw) {
759
+ reject(
760
+ "shares_exceed_max",
761
+ `Withdraw burns ${sharesAmount} shares which exceeds the approved maximum ${expectation.maxSharesToRedeemRaw}`
762
+ );
763
+ }
764
+ if (ix.accounts[0] !== expectation.wallet) {
765
+ reject("withdraw_mismatch", "Withdraw user is not the agent wallet");
766
+ }
767
+ if (ix.accounts[1] !== expectation.vault) {
768
+ reject("withdraw_mismatch", "Withdraw vault mismatch");
769
+ }
770
+ if (ix.accounts[5] !== expectation.userTokenAccount) {
771
+ reject(
772
+ "withdraw_mismatch",
773
+ "Withdraw token destination is not the approved account"
774
+ );
775
+ }
776
+ if (ix.accounts[6] !== expectation.asset) {
777
+ reject("withdraw_mismatch", "Withdraw token mint mismatch");
778
+ }
779
+ const expectedSharesAta = deriveAssociatedTokenAddress({
780
+ owner: expectation.wallet,
781
+ mint: expectation.shareMint
782
+ });
783
+ if (ix.accounts[7] !== expectedSharesAta) {
784
+ reject("withdraw_mismatch", "Withdraw share source must be the agent share ATA");
785
+ }
786
+ if (ix.accounts[8] !== expectation.shareMint) {
787
+ reject("withdraw_mismatch", "Withdraw share mint mismatch");
788
+ }
789
+ }
790
+ function expectTransferChecked(ixs, expectation) {
791
+ const ix = ixs.shift();
792
+ if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 12) {
793
+ reject("transfer_mismatch", `Expected TransferChecked for ${expectation.label}`);
794
+ }
795
+ const amount = readU64LE(ix.data, 1);
796
+ const decimals = ix.data[9];
797
+ if (expectation.amount !== null && amount !== expectation.amount) {
798
+ reject(
799
+ "amount_mismatch",
800
+ `${expectation.label} amount ${amount} does not match ${expectation.amount}`
801
+ );
802
+ }
803
+ if (decimals !== USDC_DECIMALS) {
804
+ reject("transfer_mismatch", `${expectation.label} has unexpected decimals`);
805
+ }
806
+ if (ix.accounts[0] !== expectation.source) {
807
+ reject("transfer_mismatch", `${expectation.label} source mismatch`);
808
+ }
809
+ if (ix.accounts[1] !== expectation.mint) {
810
+ reject("transfer_mismatch", `${expectation.label} mint mismatch`);
811
+ }
812
+ if (ix.accounts[2] !== expectation.destination) {
813
+ reject("transfer_mismatch", `${expectation.label} destination mismatch`);
814
+ }
815
+ if (ix.accounts[3] !== expectation.authority) {
816
+ reject("transfer_mismatch", `${expectation.label} authority mismatch`);
817
+ }
818
+ }
819
+ function expectCloseAccount(ixs, expectation) {
820
+ const ix = ixs.shift();
821
+ if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 9) {
822
+ reject("close_mismatch", "Expected CloseAccount instruction");
823
+ }
824
+ if (ix.accounts[0] !== expectation.account) {
825
+ reject("close_mismatch", "CloseAccount target is not the temporary account");
826
+ }
827
+ if (ix.accounts[1] !== expectation.destination) {
828
+ reject("close_mismatch", "CloseAccount rent destination must be the sponsor");
829
+ }
830
+ if (ix.accounts[2] !== expectation.owner) {
831
+ reject("close_mismatch", "CloseAccount authority must be the agent wallet");
832
+ }
833
+ }
834
+ function expectMemo(ixs, memo) {
835
+ const ix = ixs.shift();
836
+ if (ix === void 0 || ix.programAddress !== MEMO_PROGRAM_ID) {
837
+ reject("memo_mismatch", "Expected Memo instruction");
838
+ }
839
+ if (Buffer.from(ix.data).toString("utf8") !== memo) {
840
+ reject("memo_mismatch", "Memo content does not match the paymentId");
841
+ }
842
+ }
843
+ function expectAtaCreateForOwner(ix, owner) {
844
+ if (ix.accounts[2] !== owner) {
845
+ reject(
846
+ "unexpected_instruction",
847
+ "Associated token account creation for a foreign owner"
848
+ );
849
+ }
850
+ }
851
+ function bytesStartWith(data, prefix) {
852
+ if (data.length < prefix.length) {
853
+ return false;
854
+ }
855
+ return prefix.every((byte, index) => data[index] === byte);
856
+ }
857
+ function readU64LE(data, offset) {
858
+ if (data.length < offset + 8) {
859
+ reject("invalid_transaction_encoding", "Instruction data too short for u64");
860
+ }
861
+ return Buffer.from(data.subarray(offset, offset + 8)).readBigUInt64LE(0);
862
+ }
863
+ function readU32LE(data, offset) {
864
+ if (data.length < offset + 4) {
865
+ reject("invalid_transaction_encoding", "Instruction data too short for u32");
866
+ }
867
+ return Buffer.from(data.subarray(offset, offset + 4)).readUInt32LE(0);
868
+ }
869
+ function readShortVec(bytes, startOffset) {
870
+ let value = 0;
871
+ let shift = 0;
872
+ let offset = startOffset;
873
+ while (offset < bytes.length) {
874
+ const byte = bytes[offset];
875
+ value |= (byte & 127) << shift;
876
+ offset += 1;
877
+ if ((byte & 128) === 0) {
878
+ return { value, nextOffset: offset };
879
+ }
880
+ shift += 7;
881
+ if (shift > 28) {
882
+ return null;
883
+ }
884
+ }
885
+ return null;
886
+ }
887
+
888
+ // ../../src/client/agent-wallet-signer.ts
889
+ var LocalKeypairAgentWalletSigner = class {
890
+ validationMode = "structured_intent_transaction";
891
+ keyPairSigner;
892
+ validationPolicy;
893
+ constructor(keyPairSigner2, validationPolicy) {
894
+ this.keyPairSigner = keyPairSigner2;
895
+ this.validationPolicy = validationPolicy;
896
+ }
897
+ get walletAddress() {
898
+ return this.keyPairSigner.address;
899
+ }
900
+ async signPayment(params) {
901
+ this.assertIntentWallet(params.intent.wallet);
902
+ validatePaymentIntentTransaction({
903
+ ...params,
904
+ ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
905
+ });
906
+ return this.sign(params.serializedTransaction);
907
+ }
908
+ async signDeposit(params) {
909
+ this.assertIntentWallet(params.intent.wallet);
910
+ validateDepositIntentTransaction({
911
+ ...params,
912
+ ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
913
+ });
914
+ return this.sign(params.serializedTransaction);
915
+ }
916
+ async signWithdrawal(params) {
917
+ this.assertIntentWallet(params.intent.wallet);
918
+ validateWithdrawalIntentTransaction({
919
+ ...params,
920
+ ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
921
+ });
922
+ return this.sign(params.serializedTransaction);
923
+ }
924
+ assertIntentWallet(wallet) {
925
+ if (wallet !== this.keyPairSigner.address) {
926
+ throw new IntentValidationError(
927
+ "wallet_mismatch",
928
+ "Intent wallet does not match this signer's wallet"
929
+ );
930
+ }
931
+ }
932
+ async signApiMessage(message) {
933
+ const signature = await signBytes(
934
+ this.keyPairSigner.keyPair.privateKey,
935
+ message
936
+ );
937
+ return bs584.encode(signature);
938
+ }
939
+ async sign(serializedTransaction) {
940
+ const { serializedBase64, transaction } = await addSignaturesToSerializedTransaction({
941
+ serializedBase64: serializedTransaction,
942
+ signers: [this.keyPairSigner.keyPair]
943
+ });
944
+ const agentSignature = signatureBase58ForSigner(
945
+ transaction,
946
+ this.keyPairSigner.address
947
+ );
948
+ if (agentSignature === null) {
949
+ throw new IntentValidationError(
950
+ "signing_failed",
951
+ "Agent signature was not produced"
952
+ );
953
+ }
954
+ return { serializedTransaction: serializedBase64, agentSignature };
955
+ }
956
+ };
957
+
958
+ // ../../src/client/lookup-tables.ts
959
+ import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
960
+ import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
961
+ function lookupTableAddressesForTransaction(serializedTransaction) {
962
+ const wire = Buffer.from(serializedTransaction, "base64");
963
+ let offset = 0;
964
+ let signatureCount = 0;
965
+ let shift = 0;
966
+ while (offset < wire.length) {
967
+ const byte = wire[offset];
968
+ signatureCount |= (byte & 127) << shift;
969
+ offset += 1;
970
+ if ((byte & 128) === 0) {
971
+ break;
972
+ }
973
+ shift += 7;
974
+ }
975
+ const messageBytes = wire.subarray(offset + signatureCount * 64);
976
+ const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
977
+ const lookups = compiled.addressTableLookups ?? [];
978
+ return lookups.map((lookup) => String(lookup.lookupTableAddress));
979
+ }
980
+ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
981
+ const addresses = lookupTableAddressesForTransaction(serializedTransaction);
982
+ if (addresses.length === 0) {
983
+ return {};
984
+ }
985
+ const tables = await fetchAllMaybeAddressLookupTable(
986
+ rpc2,
987
+ addresses.map((value) => address(value))
988
+ );
989
+ const result = {};
990
+ for (const table of tables) {
991
+ if (table.exists) {
992
+ result[table.address] = table.data.addresses.map(String);
993
+ }
994
+ }
995
+ return result;
996
+ }
997
+
998
+ // ../../src/x402/headers.ts
999
+ import { z } from "zod";
1000
+ var PAYMENT_REQUIRED_HEADER = "payment-required";
1001
+ var PAYMENT_SIGNATURE_HEADER = "payment-signature";
1002
+ var PAYMENT_RESPONSE_HEADER = "payment-response";
1003
+ var X402_VERSION = 2;
1004
+ var MAX_HEADER_JSON_BYTES = 16384;
1005
+ var X402HeaderError = class extends Error {
1006
+ reason;
1007
+ constructor(reason, message) {
1008
+ super(message);
1009
+ this.name = "X402HeaderError";
1010
+ this.reason = reason;
1011
+ }
1012
+ };
1013
+ var sublyPaymentRequirementsSchema = z.object({
1014
+ scheme: z.literal(PAYMENT_SCHEME),
1015
+ network: z.string().min(1),
1016
+ asset: z.string().min(32),
1017
+ /** Exact seller amount in raw USDC; the scheme settles exactly this. */
1018
+ amountRawUsdc: z.string().regex(/^[1-9]\d*$/),
1019
+ resource: z.string().url(),
1020
+ description: z.string().optional(),
1021
+ mimeType: z.string().optional(),
1022
+ payTo: z.string().min(32),
1023
+ maxTimeoutSeconds: z.number().int().positive(),
1024
+ extra: z.object({
1025
+ sellerRequestId: z.string().min(1),
1026
+ seller: z.string().min(32),
1027
+ sellerUsdcAta: z.string().min(32),
1028
+ vault: z.string().min(32),
1029
+ shareMint: z.string().min(32)
1030
+ })
1031
+ }).loose();
1032
+ var paymentRequiredSchema = z.object({
1033
+ x402Version: z.number().int(),
1034
+ accepts: z.array(z.unknown()),
1035
+ error: z.string().optional()
1036
+ }).loose();
1037
+ var sublyPaymentPayloadSchema = z.object({
1038
+ x402Version: z.number().int(),
1039
+ scheme: z.literal(PAYMENT_SCHEME),
1040
+ network: z.string().min(1),
1041
+ payload: z.object({
1042
+ paymentId: z.string().min(1),
1043
+ requestBindingHash: z.string().min(1),
1044
+ preparedMessageHash: z.string().min(1),
1045
+ serializedTransaction: z.string().min(1).max(4096),
1046
+ agentSignature: z.string().min(1).max(128),
1047
+ temporarySettlementSignature: z.string().min(1).max(128)
1048
+ })
1049
+ }).loose();
1050
+ function encodeX402Header(value) {
1051
+ const json = JSON.stringify(value);
1052
+ if (Buffer.byteLength(json, "utf8") > MAX_HEADER_JSON_BYTES) {
1053
+ throw new X402HeaderError(
1054
+ "header_too_large",
1055
+ `x402 header JSON exceeds ${MAX_HEADER_JSON_BYTES} bytes`
1056
+ );
1057
+ }
1058
+ return Buffer.from(json, "utf8").toString("base64");
1059
+ }
1060
+ function decodeX402Header(headerValue) {
1061
+ if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
1062
+ throw new X402HeaderError(
1063
+ "header_too_large",
1064
+ `x402 header exceeds ${MAX_HEADER_JSON_BYTES} encoded bytes`
1065
+ );
1066
+ }
1067
+ const json = Buffer.from(headerValue, "base64").toString("utf8");
1068
+ try {
1069
+ return JSON.parse(json);
1070
+ } catch {
1071
+ throw new X402HeaderError(
1072
+ "invalid_header_encoding",
1073
+ "x402 header is not base64-encoded JSON"
1074
+ );
1075
+ }
1076
+ }
1077
+ function decodePaymentRequiredHeader(headerValue) {
1078
+ const parsed = paymentRequiredSchema.safeParse(decodeX402Header(headerValue));
1079
+ if (!parsed.success) {
1080
+ throw new X402HeaderError(
1081
+ "invalid_payment_required",
1082
+ "PAYMENT-REQUIRED header is not a valid x402 PaymentRequired object"
1083
+ );
1084
+ }
1085
+ const sublyRequirements = parsed.data.accepts.flatMap((candidate) => {
1086
+ const requirement = sublyPaymentRequirementsSchema.safeParse(candidate);
1087
+ return requirement.success ? [requirement.data] : [];
1088
+ });
1089
+ return { paymentRequired: parsed.data, sublyRequirements };
1090
+ }
1091
+ function requestBodyHashFor(body) {
1092
+ if (body === null || body === void 0 || body.length === 0) {
1093
+ return EMPTY_BODY_HASH;
1094
+ }
1095
+ return sha256TaggedHex(
1096
+ typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
1097
+ );
1098
+ }
1099
+
1100
+ // ../../src/client/paid-fetch.ts
1101
+ var PaidFetchError = class extends Error {
1102
+ constructor(reason, message, detail = null) {
1103
+ super(message);
1104
+ this.reason = reason;
1105
+ this.detail = detail;
1106
+ this.name = "PaidFetchError";
1107
+ }
1108
+ reason;
1109
+ detail;
1110
+ };
1111
+ var DEFAULT_PENDING_TTL_MS = 11e4;
1112
+ var DEFAULT_MAX_TRACKED_URLS = 1e3;
1113
+ var DEFAULT_MAX_BODY_CHARS = 2e4;
1114
+ function formatRawUsdcAmount(raw) {
1115
+ const value = BigInt(raw);
1116
+ const negative = value < 0n;
1117
+ const abs = negative ? -value : value;
1118
+ const whole = abs / 1000000n;
1119
+ const frac = (abs % 1000000n).toString().padStart(6, "0");
1120
+ return `${negative ? "-" : ""}${whole}.${frac}`;
1121
+ }
1122
+ var PaidFetchService = class {
1123
+ signatureBuilder;
1124
+ fetchImpl;
1125
+ fetchBudget;
1126
+ paymentStatusFor;
1127
+ defaultMaxAmountRawUsdc;
1128
+ pendingTtlMs;
1129
+ maxTrackedUrls;
1130
+ maxBodyChars;
1131
+ nowMs;
1132
+ stateStore;
1133
+ pending = /* @__PURE__ */ new Map();
1134
+ inFlight = /* @__PURE__ */ new Map();
1135
+ constructor(config) {
1136
+ this.signatureBuilder = config.signatureBuilder;
1137
+ this.fetchImpl = config.fetchImpl ?? fetch;
1138
+ this.fetchBudget = config.fetchBudget ?? (async () => null);
1139
+ this.paymentStatusFor = config.paymentStatusFor ?? (async () => "indeterminate");
1140
+ this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
1141
+ this.pendingTtlMs = config.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
1142
+ this.maxTrackedUrls = config.maxTrackedUrls ?? DEFAULT_MAX_TRACKED_URLS;
1143
+ this.maxBodyChars = config.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS;
1144
+ this.nowMs = config.nowMs ?? (() => Date.now());
1145
+ this.stateStore = config.stateStore ?? null;
1146
+ if (this.stateStore !== null) {
1147
+ for (const record of this.stateStore.load()) {
1148
+ const { url, ...entry } = record;
1149
+ this.pending.set(url, entry);
1150
+ }
1151
+ }
1152
+ }
1153
+ /**
1154
+ * GET the URL, paying a 402 challenge when needed. Concurrent calls for the
1155
+ * same URL share one flow and one result.
1156
+ */
1157
+ paidFetch(params) {
1158
+ const existing = this.inFlight.get(params.url);
1159
+ if (existing !== void 0) {
1160
+ return existing;
1161
+ }
1162
+ const flow = this.run(params).finally(() => {
1163
+ this.inFlight.delete(params.url);
1164
+ });
1165
+ this.inFlight.set(params.url, flow);
1166
+ return flow;
1167
+ }
1168
+ async run(params) {
1169
+ const { url } = params;
1170
+ const pending = this.pending.get(url);
1171
+ if (pending !== void 0) {
1172
+ const expired = this.nowMs() - pending.challengeAtMs > this.pendingTtlMs;
1173
+ if (pending.unresolved || expired) {
1174
+ if (params.forceNewPayment === true) {
1175
+ this.untrack(url);
1176
+ } else {
1177
+ const outcome = await this.paymentStatusFor(pending.paymentId);
1178
+ if (outcome === "not_settled") {
1179
+ this.untrack(url);
1180
+ } else if (outcome === "settled") {
1181
+ throw new PaidFetchError(
1182
+ "payment_already_settled",
1183
+ `the previous payment for this URL settled (paymentId=${pending.paymentId}) but the content delivery was lost, and the signature can no longer be retried. Calling again with forceNewPayment=true will PAY A SECOND TIME for the same resource.`,
1184
+ { paymentId: pending.paymentId }
1185
+ );
1186
+ } else {
1187
+ throw new PaidFetchError(
1188
+ "payment_outcome_unknown",
1189
+ `a previously signed payment for this URL (paymentId=${pending.paymentId}) has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call this tool with forceNewPayment=true.`,
1190
+ { paymentId: pending.paymentId }
1191
+ );
1192
+ }
1193
+ }
1194
+ } else {
1195
+ return this.deliver(url, pending, {
1196
+ retried: true,
1197
+ budgetBefore: null
1198
+ });
1199
+ }
1200
+ }
1201
+ const challengeAtMs = this.nowMs();
1202
+ const first = await this.fetchImpl(url);
1203
+ const firstText = await first.text();
1204
+ if (first.status !== 402) {
1205
+ return {
1206
+ paid: false,
1207
+ status: first.status,
1208
+ body: this.truncated(firstText)
1209
+ };
1210
+ }
1211
+ const challengeHeader = first.headers.get(PAYMENT_REQUIRED_HEADER);
1212
+ if (challengeHeader === null) {
1213
+ throw new PaidFetchError(
1214
+ "invalid_challenge",
1215
+ "402 response is missing the PAYMENT-REQUIRED header"
1216
+ );
1217
+ }
1218
+ const requirement = decodePaymentRequiredHeader(challengeHeader).sublyRequirements[0];
1219
+ if (requirement === void 0) {
1220
+ throw new PaidFetchError(
1221
+ "invalid_challenge",
1222
+ "402 challenge contains no subly-yield-exact requirement"
1223
+ );
1224
+ }
1225
+ const amountRawUsdc = BigInt(requirement.amountRawUsdc);
1226
+ const maxAmountRawUsdc = params.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1227
+ if (amountRawUsdc > maxAmountRawUsdc) {
1228
+ throw new PaidFetchError(
1229
+ "amount_exceeds_client_cap",
1230
+ `the challenge demands ${formatRawUsdcAmount(amountRawUsdc)} USDC, above this tool call's cap of ${formatRawUsdcAmount(maxAmountRawUsdc)} USDC; nothing was paid. Raise maxAmountRawUsdc only if this price is expected.`,
1231
+ {
1232
+ amountRawUsdc: requirement.amountRawUsdc,
1233
+ maxAmountRawUsdc: maxAmountRawUsdc.toString(),
1234
+ payTo: requirement.payTo
1235
+ }
1236
+ );
1237
+ }
1238
+ const budgetBefore = await this.fetchBudget();
1239
+ const { headerValue, paymentId } = await this.signatureBuilder.buildPaymentSignatureHeader({
1240
+ paymentRequiredHeader: challengeHeader,
1241
+ httpMethod: "GET",
1242
+ url
1243
+ });
1244
+ const entry = {
1245
+ headerValue,
1246
+ paymentId,
1247
+ amountUsdc: formatRawUsdcAmount(amountRawUsdc),
1248
+ payTo: requirement.payTo,
1249
+ challengeAtMs,
1250
+ unresolved: false
1251
+ };
1252
+ this.track(url, entry);
1253
+ return this.deliver(url, entry, { retried: false, budgetBefore });
1254
+ }
1255
+ /**
1256
+ * Sends the signed PAYMENT-SIGNATURE retry. The tracked entry is removed
1257
+ * only on confirmed delivery (200). A fresh 402 proves the signature can no
1258
+ * longer settle, so the entry is kept as an unresolved marker; every other
1259
+ * failure keeps it retryable so the next call reuses the same signature.
1260
+ */
1261
+ async deliver(url, entry, params) {
1262
+ let second;
1263
+ let secondText;
1264
+ try {
1265
+ second = await this.fetchImpl(url, {
1266
+ headers: { [PAYMENT_SIGNATURE_HEADER]: entry.headerValue }
1267
+ });
1268
+ secondText = await second.text();
1269
+ } catch (error) {
1270
+ throw new PaidFetchError(
1271
+ "delivery_failed_payment_pending",
1272
+ `delivery request failed in flight (${error instanceof Error ? error.message : String(error)}); the payment (paymentId=${entry.paymentId}) is already signed and may settle. Call this tool again with the same URL to retry delivery with the same payment signature \u2014 do NOT treat this as unpaid.`,
1273
+ { paymentId: entry.paymentId }
1274
+ );
1275
+ }
1276
+ if (second.status === 200) {
1277
+ this.untrack(url);
1278
+ const transaction = receiptTransaction(second.headers);
1279
+ return {
1280
+ paid: true,
1281
+ status: second.status,
1282
+ body: this.truncated(secondText),
1283
+ ...params.retried ? { retriedPendingPayment: true } : {},
1284
+ payment: {
1285
+ amountUsdc: entry.amountUsdc,
1286
+ payTo: entry.payTo,
1287
+ paymentId: entry.paymentId,
1288
+ transaction,
1289
+ solscanUrl: transaction === null ? null : `https://solscan.io/tx/${transaction}`,
1290
+ budgetBefore: params.budgetBefore,
1291
+ budgetAfter: await this.fetchBudget()
1292
+ }
1293
+ };
1294
+ }
1295
+ if (second.status === 402) {
1296
+ entry.unresolved = true;
1297
+ this.persist();
1298
+ throw new PaidFetchError(
1299
+ "payment_outcome_unknown",
1300
+ `the seller no longer accepts the signed payment (paymentId=${entry.paymentId}); it may or may not have settled. Verify the payment before purchasing again; to pay again anyway, call this tool with forceNewPayment=true.`,
1301
+ { paymentId: entry.paymentId }
1302
+ );
1303
+ }
1304
+ throw new PaidFetchError(
1305
+ "delivery_failed_payment_pending",
1306
+ `paid delivery failed with ${second.status} (paymentId=${entry.paymentId}); the payment may already have settled. Call this tool again with the same URL to retry delivery with the same payment signature \u2014 do NOT treat this as unpaid.`,
1307
+ {
1308
+ paymentId: entry.paymentId,
1309
+ status: second.status,
1310
+ body: this.truncated(secondText)
1311
+ }
1312
+ );
1313
+ }
1314
+ track(url, entry) {
1315
+ if (!this.pending.has(url) && this.pending.size >= this.maxTrackedUrls) {
1316
+ const oldest = this.pending.keys().next();
1317
+ if (!oldest.done) {
1318
+ this.pending.delete(oldest.value);
1319
+ }
1320
+ }
1321
+ this.pending.set(url, entry);
1322
+ this.persist();
1323
+ }
1324
+ untrack(url) {
1325
+ this.pending.delete(url);
1326
+ this.persist();
1327
+ }
1328
+ persist() {
1329
+ if (this.stateStore === null) {
1330
+ return;
1331
+ }
1332
+ this.stateStore.save(
1333
+ [...this.pending.entries()].map(([url, entry]) => ({ url, ...entry }))
1334
+ );
1335
+ }
1336
+ truncated(text) {
1337
+ return text.length > this.maxBodyChars ? `${text.slice(0, this.maxBodyChars)}
1338
+ ... (truncated)` : text;
1339
+ }
1340
+ };
1341
+ function receiptTransaction(headers) {
1342
+ try {
1343
+ const receiptHeader = headers.get(PAYMENT_RESPONSE_HEADER);
1344
+ if (receiptHeader === null) {
1345
+ return null;
1346
+ }
1347
+ const receipt = decodeX402Header(receiptHeader);
1348
+ return typeof receipt.transaction === "string" && receipt.transaction.length > 0 ? receipt.transaction : null;
1349
+ } catch {
1350
+ return null;
1351
+ }
1352
+ }
1353
+
1354
+ // ../../src/api/wallet-auth.ts
1355
+ import { createHash as createHash3 } from "node:crypto";
1356
+ import bs585 from "bs58";
1357
+ import nacl from "tweetnacl";
1358
+ var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
1359
+ var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
1360
+ var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
1361
+ function sha256Hex(data) {
1362
+ return createHash3("sha256").update(data, "utf8").digest("hex");
1363
+ }
1364
+ function walletAuthMessage(params) {
1365
+ return new TextEncoder().encode(
1366
+ `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
1367
+ params.rawBody
1368
+ )}:${params.signedAtMs}`
1369
+ );
1370
+ }
1371
+
1372
+ // ../../src/client/wallet-auth-headers.ts
1373
+ async function walletAuthHeaders(params) {
1374
+ const signedAtMs = String(Date.now());
1375
+ const message = walletAuthMessage({
1376
+ method: params.method,
1377
+ path: new URL(params.url).pathname,
1378
+ rawBody: params.body ?? "",
1379
+ signedAtMs
1380
+ });
1381
+ return {
1382
+ [WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
1383
+ [WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
1384
+ [WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
1385
+ };
1386
+ }
1387
+
1388
+ // ../../src/client/onboarding.ts
1389
+ var SELF_SERVE_POLICY_ID = "self-serve";
1390
+ var OnboardingError = class extends Error {
1391
+ constructor(step, message, detail = null) {
1392
+ super(message);
1393
+ this.step = step;
1394
+ this.detail = detail;
1395
+ this.name = "OnboardingError";
1396
+ }
1397
+ step;
1398
+ detail;
1399
+ };
1400
+ async function ensureWalletOnboarded(params) {
1401
+ const fetchImpl = params.fetchImpl ?? fetch;
1402
+ const baseUrl = params.facilitatorBaseUrl.replace(/\/$/, "");
1403
+ const post = async (step, path, body) => {
1404
+ const url = `${baseUrl}${path}`;
1405
+ const serialized = JSON.stringify(body);
1406
+ const response = await fetchImpl(url, {
1407
+ method: "POST",
1408
+ headers: {
1409
+ ...await walletAuthHeaders({
1410
+ signer: params.signer,
1411
+ method: "POST",
1412
+ url,
1413
+ body: serialized
1414
+ }),
1415
+ "content-type": "application/json"
1416
+ },
1417
+ body: serialized
1418
+ });
1419
+ if (response.status !== 200) {
1420
+ let detail = null;
1421
+ try {
1422
+ detail = await response.json();
1423
+ } catch {
1424
+ detail = null;
1425
+ }
1426
+ throw new OnboardingError(
1427
+ step,
1428
+ `wallet onboarding ${step} failed with ${response.status}`,
1429
+ detail
1430
+ );
1431
+ }
1432
+ };
1433
+ const wallet = params.signer.walletAddress;
1434
+ await post("register", "/v1/wallets/agent", {
1435
+ wallet,
1436
+ signingPolicyId: SELF_SERVE_POLICY_ID,
1437
+ signingMode: "non_interactive",
1438
+ signerValidationMode: params.signer.validationMode,
1439
+ signerProvider: "local-keypair",
1440
+ activateForPayments: true
1441
+ });
1442
+ await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
1443
+ }
1444
+
1445
+ // ../../src/solana/keys.ts
1446
+ import { readFileSync } from "node:fs";
1447
+ import bs586 from "bs58";
1448
+ import {
1449
+ createKeyPairSignerFromBytes
1450
+ } from "@solana/kit";
1451
+ async function loadKeyPairSigner(params) {
1452
+ const { base58Secret, jsonFilePath, label } = params;
1453
+ if (base58Secret !== void 0 && base58Secret.length > 0) {
1454
+ const bytes = bs586.decode(base58Secret);
1455
+ if (bytes.length !== 64) {
1456
+ throw new Error(`${label} base58 secret must decode to 64 bytes`);
1457
+ }
1458
+ return createKeyPairSignerFromBytes(bytes);
1459
+ }
1460
+ if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1461
+ const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
1462
+ if (!Array.isArray(raw) || raw.length !== 64) {
1463
+ throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1464
+ }
1465
+ return createKeyPairSignerFromBytes(Uint8Array.from(raw));
1466
+ }
1467
+ throw new Error(`${label} keypair is not configured`);
1468
+ }
1469
+
1470
+ // ../../src/solana/rpc.ts
1471
+ import { createSolanaRpc } from "@solana/kit";
1472
+ function createRpcFromEnv(env = process.env) {
1473
+ const url = env.SOLANA_RPC_URL;
1474
+ if (url === void 0 || url.length === 0) {
1475
+ throw new Error("SOLANA_RPC_URL must be configured");
1476
+ }
1477
+ return createSolanaRpc(url);
1478
+ }
1479
+
1480
+ // ../../src/x402/client.ts
1481
+ var X402ClientError = class extends Error {
1482
+ reason;
1483
+ detail;
1484
+ constructor(reason, message, detail) {
1485
+ super(message);
1486
+ this.name = "X402ClientError";
1487
+ this.reason = reason;
1488
+ this.detail = detail;
1489
+ }
1490
+ };
1491
+ var SublyX402Client = class {
1492
+ config;
1493
+ signer;
1494
+ lookupTablesFor;
1495
+ fetchImpl;
1496
+ constructor(config) {
1497
+ this.config = {
1498
+ facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
1499
+ network: config.network ?? SOLANA_MAINNET_NETWORK
1500
+ };
1501
+ this.signer = config.signer;
1502
+ this.lookupTablesFor = config.lookupTablesFor ?? null;
1503
+ this.fetchImpl = config.fetchImpl ?? fetch;
1504
+ }
1505
+ /**
1506
+ * Builds the PAYMENT-SIGNATURE header for a 402 challenge. The request
1507
+ * method, URL, and body must be exactly the request being retried; they are
1508
+ * bound into the payment and verified again by the seller and facilitator.
1509
+ */
1510
+ async buildPaymentSignatureHeader(input) {
1511
+ const requirement = this.selectRequirement(input.paymentRequiredHeader);
1512
+ if (requirement.resource !== input.url) {
1513
+ throw new X402ClientError(
1514
+ "resource_mismatch",
1515
+ "PAYMENT-REQUIRED resource does not match the request URL"
1516
+ );
1517
+ }
1518
+ const requestBodyHash = requestBodyHashFor(input.body);
1519
+ const prepared = await this.preparePayment({
1520
+ requirement,
1521
+ httpMethod: input.httpMethod.toUpperCase(),
1522
+ requestBodyHash
1523
+ });
1524
+ const lookupTables = this.lookupTablesFor === null ? void 0 : await this.lookupTablesFor(prepared.intentJson.serializedTransaction);
1525
+ const signed = await this.signer.signPayment({
1526
+ intent: prepared.intentJson.signingIntent,
1527
+ serializedTransaction: prepared.intentJson.serializedTransaction,
1528
+ lookupTables
1529
+ });
1530
+ const payload = {
1531
+ x402Version: X402_VERSION,
1532
+ scheme: requirement.scheme,
1533
+ network: requirement.network,
1534
+ payload: {
1535
+ paymentId: prepared.paymentId,
1536
+ requestBindingHash: prepared.requestBindingHash,
1537
+ preparedMessageHash: prepared.preparedMessageHash,
1538
+ serializedTransaction: signed.serializedTransaction,
1539
+ agentSignature: signed.agentSignature,
1540
+ temporarySettlementSignature: prepared.temporarySettlementSignature
1541
+ }
1542
+ };
1543
+ return {
1544
+ headerValue: encodeX402Header(payload),
1545
+ paymentId: prepared.paymentId
1546
+ };
1547
+ }
1548
+ /**
1549
+ * Convenience wrapper: performs the request, and on a 402 with a Subly
1550
+ * requirement pays from vault yield and retries once.
1551
+ */
1552
+ async fetchWithPayment(url, init) {
1553
+ const first = await this.fetchImpl(url, init);
1554
+ if (first.status !== 402) {
1555
+ return first;
1556
+ }
1557
+ const challenge = first.headers.get(PAYMENT_REQUIRED_HEADER);
1558
+ if (challenge === null) {
1559
+ return first;
1560
+ }
1561
+ const { headerValue } = await this.buildPaymentSignatureHeader({
1562
+ paymentRequiredHeader: challenge,
1563
+ httpMethod: init?.method ?? "GET",
1564
+ url,
1565
+ body: init?.body ?? null
1566
+ });
1567
+ return this.fetchImpl(url, {
1568
+ ...init,
1569
+ headers: {
1570
+ ...init?.headers,
1571
+ [PAYMENT_SIGNATURE_HEADER]: headerValue
1572
+ }
1573
+ });
1574
+ }
1575
+ selectRequirement(paymentRequiredHeader) {
1576
+ let sublyRequirements;
1577
+ try {
1578
+ sublyRequirements = decodePaymentRequiredHeader(
1579
+ paymentRequiredHeader
1580
+ ).sublyRequirements;
1581
+ } catch (error) {
1582
+ throw new X402ClientError(
1583
+ error instanceof X402HeaderError ? error.reason : "invalid_challenge",
1584
+ "Cannot decode the PAYMENT-REQUIRED header"
1585
+ );
1586
+ }
1587
+ const selected = sublyRequirements.find(
1588
+ (candidate) => candidate.network === this.config.network && candidate.extra.vault === SUBLY_VAULT.address && candidate.extra.shareMint === SUBLY_VAULT.shareMint && candidate.asset === SUBLY_VAULT.usdcMint
1589
+ ) ?? null;
1590
+ if (selected === null) {
1591
+ throw new X402ClientError(
1592
+ "no_supported_requirement",
1593
+ "The 402 challenge contains no supported subly-yield-exact requirement"
1594
+ );
1595
+ }
1596
+ return selected;
1597
+ }
1598
+ async preparePayment(input) {
1599
+ const { requirement } = input;
1600
+ const wallet = this.signer.walletAddress;
1601
+ const prepareUrl = `${this.config.facilitatorBaseUrl}/v1/payments/prepare`;
1602
+ const prepareBody = JSON.stringify({
1603
+ wallet,
1604
+ scheme: requirement.scheme,
1605
+ network: requirement.network,
1606
+ vault: requirement.extra.vault,
1607
+ shareMint: requirement.extra.shareMint,
1608
+ asset: requirement.asset,
1609
+ seller: requirement.extra.seller,
1610
+ sellerRequestId: requirement.extra.sellerRequestId,
1611
+ httpMethod: input.httpMethod,
1612
+ canonicalResourceUrl: requirement.resource,
1613
+ requestBodyHash: input.requestBodyHash,
1614
+ amountRawUsdc: requirement.amountRawUsdc,
1615
+ payTo: requirement.payTo,
1616
+ sellerUsdcAta: requirement.extra.sellerUsdcAta,
1617
+ dustRecipientUsdcAta: deriveAssociatedTokenAddress({
1618
+ owner: wallet,
1619
+ mint: SUBLY_VAULT.usdcMint
1620
+ })
1621
+ });
1622
+ const response = await this.fetchImpl(prepareUrl, {
1623
+ method: "POST",
1624
+ headers: {
1625
+ ...await walletAuthHeaders({
1626
+ signer: this.signer,
1627
+ method: "POST",
1628
+ url: prepareUrl,
1629
+ body: prepareBody
1630
+ }),
1631
+ "content-type": "application/json"
1632
+ },
1633
+ body: prepareBody
1634
+ });
1635
+ const body = await response.json();
1636
+ if (response.status !== 200) {
1637
+ const error = body.error;
1638
+ throw new X402ClientError(
1639
+ typeof error?.code === "string" ? error.code : "prepare_failed",
1640
+ "Payment preparation failed at the facilitator",
1641
+ body
1642
+ );
1643
+ }
1644
+ const prepared = body;
1645
+ if (typeof prepared.paymentId !== "string" || typeof prepared.preparedMessageHash !== "string" || typeof prepared.intentJson?.serializedTransaction !== "string" || typeof prepared.intentJson?.signingIntent !== "object") {
1646
+ throw new X402ClientError(
1647
+ "invalid_prepare_response",
1648
+ "Facilitator prepare response is missing required fields"
1649
+ );
1650
+ }
1651
+ return prepared;
1652
+ }
1653
+ };
1654
+
1655
+ // ../../demo/shared.ts
1656
+ function fail(message) {
1657
+ console.error(message);
1658
+ process.exit(1);
1659
+ }
1660
+ function requireEnv(name) {
1661
+ const value = process.env[name];
1662
+ if (value === void 0 || value.length === 0) {
1663
+ fail(`Missing required environment variable: ${name}`);
1664
+ }
1665
+ return value;
1666
+ }
1667
+
1668
+ // ../../demo/mcp-server.ts
1669
+ var TOOL_NAME = "fetch_with_subly_payment";
1670
+ var DEFAULT_MAX_AMOUNT_RAW_USDC = 10000n;
1671
+ requireEnv("SOLANA_RPC_URL");
1672
+ var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "http://localhost:3000";
1673
+ var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? DEFAULT_MAX_AMOUNT_RAW_USDC : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
1674
+ var keyPairSigner = await loadKeyPairSigner({
1675
+ base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1676
+ jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1677
+ label: "SUBLY_DEMO_AGENT_KEYPAIR"
1678
+ });
1679
+ var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
1680
+ var rpc = createRpcFromEnv();
1681
+ async function fetchBudget() {
1682
+ try {
1683
+ const url = `${facilitatorBaseUrl}/v1/wallets/${signer.walletAddress}/budget`;
1684
+ const response = await fetch(url, {
1685
+ headers: await walletAuthHeaders({ signer, method: "GET", url })
1686
+ });
1687
+ if (response.status !== 200) {
1688
+ return null;
1689
+ }
1690
+ const body = await response.json();
1691
+ if (body.budget === void 0) {
1692
+ return null;
1693
+ }
1694
+ return {
1695
+ positionValueUsdc: formatRawUsdcAmount(body.budget.positionValueRawUsdc),
1696
+ spendableYieldUsdc: formatRawUsdcAmount(
1697
+ body.budget.spendableYieldRawUsdc
1698
+ )
1699
+ };
1700
+ } catch {
1701
+ return null;
1702
+ }
1703
+ }
1704
+ async function paymentStatusFor(paymentId) {
1705
+ try {
1706
+ const url = `${facilitatorBaseUrl}/v1/payments/${paymentId}`;
1707
+ const response = await fetch(url, {
1708
+ headers: await walletAuthHeaders({ signer, method: "GET", url })
1709
+ });
1710
+ if (response.status !== 200) {
1711
+ return "indeterminate";
1712
+ }
1713
+ const body = await response.json();
1714
+ if (body.status === "settled") {
1715
+ return "settled";
1716
+ }
1717
+ if (body.status === "expired" || body.status === "failed" || body.status === "failed_not_submitted") {
1718
+ return "not_settled";
1719
+ }
1720
+ return "indeterminate";
1721
+ } catch {
1722
+ return "indeterminate";
1723
+ }
1724
+ }
1725
+ function fileStateStore(path) {
1726
+ return {
1727
+ load() {
1728
+ try {
1729
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
1730
+ return Array.isArray(parsed) ? parsed : [];
1731
+ } catch {
1732
+ return [];
1733
+ }
1734
+ },
1735
+ save(records) {
1736
+ try {
1737
+ writeFileSync(path, JSON.stringify(records));
1738
+ } catch (error) {
1739
+ console.error(
1740
+ `[subly-mcp] failed to persist pending payments: ${error instanceof Error ? error.message : String(error)}`
1741
+ );
1742
+ }
1743
+ }
1744
+ };
1745
+ }
1746
+ var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? "demo/env/mcp-pending-payments.json";
1747
+ var paidFetchService = new PaidFetchService({
1748
+ signatureBuilder: new SublyX402Client({
1749
+ facilitatorBaseUrl,
1750
+ signer,
1751
+ lookupTablesFor: (serializedTransaction) => fetchLookupTablesForTransaction(rpc, serializedTransaction)
1752
+ }),
1753
+ defaultMaxAmountRawUsdc,
1754
+ fetchBudget,
1755
+ paymentStatusFor,
1756
+ stateStore: fileStateStore(pendingStatePath)
1757
+ });
1758
+ var SERVER_INSTRUCTIONS = `Subly lets an agent pay for HTTP 402 (subly-yield-exact) resources from its wallet's Kamino vault YIELD \u2014 the deposited principal is never spent.
1759
+
1760
+ Before payments can succeed the operator of this server must have, once:
1761
+ 1. A Solana keypair for the agent wallet. Subly does NOT create wallets; make one with \`solana-keygen new -o agent.json\` (or export a keypair from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it. The private key never leaves that file; this server only signs locally with it.
1762
+ 2. Funded that wallet with USDC on Solana mainnet (no SOL needed \u2014 fees are sponsored) and deposited into the vault (see the project's deposit command). The vault minimum deposit is 1 USDC.
1763
+ 3. Waited for yield to accrue; a payment needs the price plus a fixed overhead (~0.0024 USDC) of spendable yield.
1764
+
1765
+ Then use fetch_with_subly_payment(url) to GET a paid resource: it pays the 402 from yield and returns the body plus an on-chain receipt. If it returns insufficient_yield, that is expected \u2014 wait for yield, do not loop. If it returns delivery_failed_payment_pending, call the SAME url again (it retries the same payment, never double-pays).`;
1766
+ var server = new Server(
1767
+ { name: "subly-payments", version: "0.1.0" },
1768
+ { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
1769
+ );
1770
+ server.setRequestHandler(ListToolsRequestSchema, () => ({
1771
+ tools: [
1772
+ {
1773
+ name: TOOL_NAME,
1774
+ description: `GET a URL, automatically paying a Subly x402 (subly-yield-exact) 402 challenge from the agent wallet's Kamino vault yield. Returns the response body; when a payment was made, also returns the settlement receipt (amount, payee, paymentId, Solscan link). Challenges above maxAmountRawUsdc (default ${defaultMaxAmountRawUsdc} raw = ${formatRawUsdcAmount(
1775
+ defaultMaxAmountRawUsdc
1776
+ )} USDC) are refused without paying. If delivery fails after the payment was signed, the signature is kept and calling again with the same URL retries the same payment instead of paying twice. Payments are refused by the facilitator when the spendable yield budget cannot cover them \u2014 the principal is never spent. Use only for URLs you intend to purchase access to.`,
1777
+ inputSchema: {
1778
+ type: "object",
1779
+ properties: {
1780
+ url: {
1781
+ type: "string",
1782
+ description: "URL to fetch (GET). Must match the seller's resource URL exactly."
1783
+ },
1784
+ maxAmountRawUsdc: {
1785
+ type: "string",
1786
+ description: 'Refuse (without paying) any challenge above this amount in raw USDC units (6 decimals, e.g. "10000" = 0.01 USDC). Defaults to the server-side cap.'
1787
+ },
1788
+ forceNewPayment: {
1789
+ type: "boolean",
1790
+ description: "Pay again even though a previous payment for this URL settled or has an unknown outcome. Only set deliberately: combined with payment_already_settled this means paying twice for the same resource. Ignored while the previous payment is still retryable (the same signature is retried instead)."
1791
+ }
1792
+ },
1793
+ required: ["url"]
1794
+ },
1795
+ annotations: {
1796
+ title: "Fetch with Subly payment",
1797
+ readOnlyHint: false,
1798
+ destructiveHint: true,
1799
+ idempotentHint: false,
1800
+ openWorldHint: true
1801
+ }
1802
+ }
1803
+ ]
1804
+ }));
1805
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1806
+ if (request.params.name !== TOOL_NAME) {
1807
+ return {
1808
+ content: [
1809
+ { type: "text", text: `unknown tool: ${request.params.name}` }
1810
+ ],
1811
+ isError: true
1812
+ };
1813
+ }
1814
+ const args = request.params.arguments ?? {};
1815
+ const url = args.url;
1816
+ if (typeof url !== "string" || url.length === 0) {
1817
+ return {
1818
+ content: [{ type: "text", text: "missing required argument: url" }],
1819
+ isError: true
1820
+ };
1821
+ }
1822
+ let maxAmountRawUsdc;
1823
+ if (args.maxAmountRawUsdc !== void 0) {
1824
+ const raw = args.maxAmountRawUsdc;
1825
+ try {
1826
+ if (typeof raw !== "string" && typeof raw !== "number") {
1827
+ throw new TypeError("not a string or number");
1828
+ }
1829
+ maxAmountRawUsdc = BigInt(raw);
1830
+ } catch {
1831
+ return {
1832
+ content: [
1833
+ {
1834
+ type: "text",
1835
+ text: "maxAmountRawUsdc must be an integer raw USDC amount"
1836
+ }
1837
+ ],
1838
+ isError: true
1839
+ };
1840
+ }
1841
+ }
1842
+ try {
1843
+ const result = await paidFetchService.paidFetch({
1844
+ url,
1845
+ ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc },
1846
+ forceNewPayment: args.forceNewPayment === true
1847
+ });
1848
+ return {
1849
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1850
+ };
1851
+ } catch (error) {
1852
+ if (error instanceof PaidFetchError) {
1853
+ return {
1854
+ content: [
1855
+ {
1856
+ type: "text",
1857
+ text: JSON.stringify(
1858
+ {
1859
+ paid: false,
1860
+ refused: true,
1861
+ reason: error.reason,
1862
+ message: error.message,
1863
+ detail: error.detail
1864
+ },
1865
+ null,
1866
+ 2
1867
+ )
1868
+ }
1869
+ ],
1870
+ isError: true
1871
+ };
1872
+ }
1873
+ if (error instanceof X402ClientError) {
1874
+ return {
1875
+ content: [
1876
+ {
1877
+ type: "text",
1878
+ text: JSON.stringify(
1879
+ {
1880
+ paid: false,
1881
+ refused: true,
1882
+ reason: error.reason,
1883
+ detail: error.detail ?? null
1884
+ },
1885
+ null,
1886
+ 2
1887
+ )
1888
+ }
1889
+ ],
1890
+ isError: true
1891
+ };
1892
+ }
1893
+ return {
1894
+ content: [
1895
+ {
1896
+ type: "text",
1897
+ text: error instanceof Error ? error.message : String(error)
1898
+ }
1899
+ ],
1900
+ isError: true
1901
+ };
1902
+ }
1903
+ });
1904
+ try {
1905
+ await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
1906
+ console.error("[subly-mcp] wallet registered and synced at the facilitator");
1907
+ } catch (error) {
1908
+ console.error(
1909
+ `[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
1910
+ );
1911
+ }
1912
+ var transport = new StdioServerTransport();
1913
+ await server.connect(transport);
1914
+ console.error(
1915
+ `[subly-mcp] ready: agent wallet ${signer.walletAddress}, facilitator ${facilitatorBaseUrl}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc)} USDC`
1916
+ );