@subly_fi/pay 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1502 @@
1
+ // ../../src/client/agent-wallet-signer.ts
2
+ import { signBytes } from "@solana/kit";
3
+ import bs584 from "bs58";
4
+
5
+ // ../../src/solana/tx.ts
6
+ import bs58 from "bs58";
7
+ import {
8
+ appendTransactionMessageInstructions,
9
+ compileTransaction,
10
+ compressTransactionMessageUsingAddressLookupTables,
11
+ createTransactionMessage,
12
+ getBase64EncodedWireTransaction,
13
+ getTransactionDecoder,
14
+ partiallySignTransaction,
15
+ pipe,
16
+ setTransactionMessageFeePayer,
17
+ setTransactionMessageLifetimeUsingBlockhash
18
+ } from "@solana/kit";
19
+
20
+ // ../../src/lib/hash.ts
21
+ import { createHash } from "node:crypto";
22
+ function sha256TaggedHex(data) {
23
+ return `sha256-${createHash("sha256").update(data).digest("hex")}`;
24
+ }
25
+ function stableStringify(value) {
26
+ if (value === null) {
27
+ return "null";
28
+ }
29
+ if (typeof value === "bigint") {
30
+ return JSON.stringify(value.toString());
31
+ }
32
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
33
+ return JSON.stringify(value);
34
+ }
35
+ if (Array.isArray(value)) {
36
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
37
+ }
38
+ const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
39
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
40
+ }
41
+ function hashStableJson(value) {
42
+ return sha256TaggedHex(stableStringify(value));
43
+ }
44
+
45
+ // ../../src/solana/tx.ts
46
+ function decodeSerializedTransaction(serializedBase64) {
47
+ return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
48
+ }
49
+ async function addSignaturesToSerializedTransaction(params) {
50
+ const decoded = decodeSerializedTransaction(params.serializedBase64);
51
+ const signed = await partiallySignTransaction(params.signers, decoded);
52
+ return {
53
+ serializedBase64: getBase64EncodedWireTransaction(signed),
54
+ transaction: signed
55
+ };
56
+ }
57
+ function signatureBase58ForSigner(transaction, signer2) {
58
+ const signature = transaction.signatures[signer2];
59
+ if (signature === null || signature === void 0) {
60
+ return null;
61
+ }
62
+ return bs58.encode(signature);
63
+ }
64
+
65
+ // ../../src/client/transaction-intent-validator.ts
66
+ import bs583 from "bs58";
67
+ import { getCompiledTransactionMessageDecoder } from "@solana/kit";
68
+
69
+ // ../../src/config/constants.ts
70
+ var PAYMENT_SCHEME = "subly-yield-exact";
71
+ var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
72
+ var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
73
+ var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
74
+ var SUBLY_VAULT = {
75
+ name: "Subly USDC Payment Vault Alpha",
76
+ address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
77
+ programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
78
+ usdcMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
79
+ shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
80
+ lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
81
+ farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
82
+ };
83
+ var USDC_DECIMALS = 6;
84
+
85
+ // ../../src/domain/request-binding.ts
86
+ function computeRequestBindingHash(fields) {
87
+ return hashStableJson({
88
+ sellerRequestId: fields.sellerRequestId,
89
+ httpMethod: fields.httpMethod.toUpperCase(),
90
+ canonicalResourceUrl: fields.canonicalResourceUrl,
91
+ requestBodyHash: fields.requestBodyHash,
92
+ seller: fields.seller,
93
+ asset: fields.asset,
94
+ amountRawUsdc: fields.amountRawUsdc,
95
+ payTo: fields.payTo,
96
+ sellerUsdcAta: fields.sellerUsdcAta
97
+ });
98
+ }
99
+
100
+ // ../../src/lib/associated-token-account.ts
101
+ import { createHash as createHash2 } from "node:crypto";
102
+ import bs582 from "bs58";
103
+ var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
104
+ var ED25519_P = (1n << 255n) - 19n;
105
+ var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
106
+ function deriveAssociatedTokenAddress(params) {
107
+ const owner = decodePublicKey(params.owner, "owner");
108
+ const mint = decodePublicKey(params.mint ?? SUBLY_VAULT.usdcMint, "mint");
109
+ const tokenProgramId = decodePublicKey(
110
+ params.tokenProgramId ?? SPL_TOKEN_PROGRAM_ID,
111
+ "tokenProgramId"
112
+ );
113
+ const associatedTokenProgramId = decodePublicKey(
114
+ ASSOCIATED_TOKEN_PROGRAM_ID,
115
+ "associatedTokenProgramId"
116
+ );
117
+ for (let bump = 255; bump >= 0; bump -= 1) {
118
+ const address2 = createProgramAddress(
119
+ [owner, tokenProgramId, mint, Uint8Array.of(bump)],
120
+ associatedTokenProgramId
121
+ );
122
+ if (address2 !== null) {
123
+ return bs582.encode(address2);
124
+ }
125
+ }
126
+ throw new Error("Unable to derive associated token account address");
127
+ }
128
+ function createProgramAddress(seeds, programId) {
129
+ const hash = createHash2("sha256");
130
+ for (const seed of seeds) {
131
+ hash.update(seed);
132
+ }
133
+ hash.update(programId);
134
+ hash.update(PDA_MARKER);
135
+ const digest = hash.digest();
136
+ return isEd25519Point(digest) ? null : new Uint8Array(digest);
137
+ }
138
+ function decodePublicKey(value, fieldName) {
139
+ const decoded = bs582.decode(value);
140
+ if (decoded.length !== 32) {
141
+ throw new Error(`${fieldName} must be a 32-byte public key`);
142
+ }
143
+ return decoded;
144
+ }
145
+ function isEd25519Point(bytes) {
146
+ if (bytes.length !== 32) {
147
+ return false;
148
+ }
149
+ const yBytes = Uint8Array.from(bytes);
150
+ yBytes[31] = yBytes[31] & 127;
151
+ const y = littleEndianToBigInt(yBytes);
152
+ if (y >= ED25519_P) {
153
+ return false;
154
+ }
155
+ const ySquared = mod(y * y, ED25519_P);
156
+ const numerator = mod(ySquared - 1n, ED25519_P);
157
+ const denominator = mod(ED25519_D * ySquared + 1n, ED25519_P);
158
+ if (denominator === 0n) {
159
+ return false;
160
+ }
161
+ const xSquared = mod(
162
+ numerator * modPow(denominator, ED25519_P - 2n, ED25519_P),
163
+ ED25519_P
164
+ );
165
+ return xSquared === 0n || modPow(xSquared, (ED25519_P - 1n) / 2n, ED25519_P) === 1n;
166
+ }
167
+ function littleEndianToBigInt(bytes) {
168
+ let value = 0n;
169
+ for (let index = bytes.length - 1; index >= 0; index -= 1) {
170
+ value = (value << 8n) + BigInt(bytes[index]);
171
+ }
172
+ return value;
173
+ }
174
+ function mod(value, modulus) {
175
+ const result = value % modulus;
176
+ return result >= 0n ? result : result + modulus;
177
+ }
178
+ function modPow(base, exponent, modulus) {
179
+ let result = 1n;
180
+ let nextBase = mod(base, modulus);
181
+ let nextExponent = exponent;
182
+ while (nextExponent > 0n) {
183
+ if ((nextExponent & 1n) === 1n) {
184
+ result = mod(result * nextBase, modulus);
185
+ }
186
+ nextBase = mod(nextBase * nextBase, modulus);
187
+ nextExponent >>= 1n;
188
+ }
189
+ return result;
190
+ }
191
+
192
+ // ../../src/client/transaction-intent-validator.ts
193
+ var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
194
+ var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
195
+ var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
196
+ var MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
197
+ var KVAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
198
+ var KAMINO_FARMS_PROGRAM_ID = "FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr";
199
+ var KVAULT_WITHDRAW_DISCRIMINATOR = Uint8Array.from([
200
+ 183,
201
+ 18,
202
+ 70,
203
+ 156,
204
+ 148,
205
+ 109,
206
+ 161,
207
+ 34
208
+ ]);
209
+ var KVAULT_WITHDRAW_FROM_AVAILABLE_DISCRIMINATOR = Uint8Array.from([
210
+ 19,
211
+ 131,
212
+ 112,
213
+ 155,
214
+ 170,
215
+ 220,
216
+ 34,
217
+ 57
218
+ ]);
219
+ var KVAULT_DEPOSIT_DISCRIMINATOR = Uint8Array.from([
220
+ 242,
221
+ 35,
222
+ 198,
223
+ 137,
224
+ 82,
225
+ 225,
226
+ 242,
227
+ 182
228
+ ]);
229
+ var U64_MAX = 18446744073709551615n;
230
+ var MAX_TEMP_ACCOUNT_LAMPORTS = 10000000n;
231
+ var DEFAULT_MAX_COMPUTE_UNIT_LIMIT = 14e5;
232
+ var DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS = 100000n;
233
+ var IntentValidationError = class extends Error {
234
+ reason;
235
+ constructor(reason, message) {
236
+ super(message);
237
+ this.name = "IntentValidationError";
238
+ this.reason = reason;
239
+ }
240
+ };
241
+ function reject(reason, message) {
242
+ throw new IntentValidationError(reason, message);
243
+ }
244
+ function decodeIntentTransaction(params) {
245
+ const wire = Buffer.from(params.serializedTransaction, "base64");
246
+ const signatureCount = readShortVec(wire, 0);
247
+ if (signatureCount === null) {
248
+ reject("invalid_transaction_encoding", "Cannot parse signature count");
249
+ }
250
+ const messageOffset = signatureCount.nextOffset + signatureCount.value * 64;
251
+ if (messageOffset >= wire.length) {
252
+ reject("invalid_transaction_encoding", "Transaction has no message bytes");
253
+ }
254
+ const messageBytes = wire.subarray(messageOffset);
255
+ const compiled = getCompiledTransactionMessageDecoder().decode(messageBytes);
256
+ if (compiled.version !== 0) {
257
+ reject("unsupported_transaction_version", "Only v0 transactions are supported");
258
+ }
259
+ const staticAccounts = compiled.staticAccounts.map(String);
260
+ const loadedWritable = [];
261
+ const loadedReadonly = [];
262
+ const lookups = compiled.addressTableLookups ?? [];
263
+ for (const rawLookup of lookups) {
264
+ const lookup = rawLookup;
265
+ const table = params.lookupTables?.[String(lookup.lookupTableAddress)];
266
+ if (table === void 0) {
267
+ reject(
268
+ "lookup_table_unresolved",
269
+ `Transaction references unknown lookup table ${lookup.lookupTableAddress}`
270
+ );
271
+ }
272
+ const writableIndexes = lookup.writableIndexes ?? lookup.writableIndices ?? [];
273
+ const readonlyIndexes = lookup.readonlyIndexes ?? lookup.readableIndices ?? [];
274
+ for (const index of writableIndexes) {
275
+ const resolved = table[index];
276
+ if (resolved === void 0) {
277
+ reject("lookup_table_unresolved", "Lookup table index out of range");
278
+ }
279
+ loadedWritable.push(String(resolved));
280
+ }
281
+ for (const index of readonlyIndexes) {
282
+ const resolved = table[index];
283
+ if (resolved === void 0) {
284
+ reject("lookup_table_unresolved", "Lookup table index out of range");
285
+ }
286
+ loadedReadonly.push(String(resolved));
287
+ }
288
+ }
289
+ const orderedAccounts = [...staticAccounts, ...loadedWritable, ...loadedReadonly];
290
+ const instructions = compiled.instructions.map(
291
+ (instruction) => {
292
+ const programAddress = orderedAccounts[instruction.programAddressIndex];
293
+ if (programAddress === void 0) {
294
+ reject("invalid_transaction_encoding", "Program index out of range");
295
+ }
296
+ const accounts = (instruction.accountIndices ?? []).map((index) => {
297
+ const account = orderedAccounts[index];
298
+ if (account === void 0) {
299
+ reject("invalid_transaction_encoding", "Account index out of range");
300
+ }
301
+ return account;
302
+ });
303
+ return {
304
+ programAddress,
305
+ accounts,
306
+ data: instruction.data === void 0 ? new Uint8Array() : Uint8Array.from(instruction.data)
307
+ };
308
+ }
309
+ );
310
+ const feePayer = staticAccounts[0];
311
+ if (feePayer === void 0) {
312
+ reject("invalid_transaction_encoding", "Transaction has no fee payer");
313
+ }
314
+ return {
315
+ feePayer,
316
+ requiredSigners: staticAccounts.slice(0, compiled.header.numSignerAccounts),
317
+ instructions,
318
+ messageHash: sha256TaggedHex(Buffer.from(messageBytes))
319
+ };
320
+ }
321
+ function validatePaymentIntentTransaction(params) {
322
+ const { intent } = params;
323
+ const now = params.nowMs ?? Date.now();
324
+ const policy2 = resolveIntentValidationPolicy(params.policy);
325
+ if (new Date(intent.expiresAt).getTime() <= now) {
326
+ reject("expired", "Payment intent has expired");
327
+ }
328
+ if (intent.scheme !== PAYMENT_SCHEME) {
329
+ reject("scheme_mismatch", `scheme must be ${PAYMENT_SCHEME}`);
330
+ }
331
+ if (intent.network !== SOLANA_MAINNET_NETWORK) {
332
+ reject("network_mismatch", "Unsupported network");
333
+ }
334
+ if (intent.vault !== SUBLY_VAULT.address) {
335
+ reject("vault_mismatch", "Unsupported vault");
336
+ }
337
+ if (intent.shareMint !== SUBLY_VAULT.shareMint) {
338
+ reject("share_mint_mismatch", "Unsupported share mint");
339
+ }
340
+ if (intent.asset !== SUBLY_VAULT.usdcMint) {
341
+ reject("asset_mismatch", "Only USDC payments are supported");
342
+ }
343
+ if (intent.memo !== intent.paymentId) {
344
+ reject("memo_mismatch", "Memo must equal the paymentId");
345
+ }
346
+ const expectedBinding = computeRequestBindingHash({
347
+ sellerRequestId: intent.sellerRequestId,
348
+ httpMethod: intent.httpMethod,
349
+ canonicalResourceUrl: intent.canonicalResourceUrl,
350
+ requestBodyHash: intent.requestBodyHash,
351
+ seller: intent.seller,
352
+ asset: intent.asset,
353
+ amountRawUsdc: intent.amountRawUsdc,
354
+ payTo: intent.payTo,
355
+ sellerUsdcAta: intent.sellerUsdcAta
356
+ });
357
+ if (expectedBinding !== intent.requestBindingHash) {
358
+ reject(
359
+ "request_binding_mismatch",
360
+ "requestBindingHash does not match the request fields"
361
+ );
362
+ }
363
+ const expectedSellerAta = deriveAssociatedTokenAddress({
364
+ owner: intent.payTo,
365
+ mint: intent.asset
366
+ });
367
+ if (expectedSellerAta !== intent.sellerUsdcAta) {
368
+ reject(
369
+ "seller_ata_mismatch",
370
+ "sellerUsdcAta must be the associated USDC account for payTo"
371
+ );
372
+ }
373
+ const expectedDustAta = deriveAssociatedTokenAddress({
374
+ owner: intent.wallet,
375
+ mint: intent.asset
376
+ });
377
+ if (expectedDustAta !== intent.dustRecipientUsdcAta) {
378
+ reject(
379
+ "dust_recipient_mismatch",
380
+ "dustRecipientUsdcAta must be the agent wallet's USDC ATA"
381
+ );
382
+ }
383
+ const decoded = decodeIntentTransaction({
384
+ serializedTransaction: params.serializedTransaction,
385
+ ...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
386
+ });
387
+ if (decoded.messageHash !== intent.preparedMessageHash) {
388
+ reject("message_hash_mismatch", "Prepared message hash mismatch");
389
+ }
390
+ if (decoded.feePayer !== intent.feePayer) {
391
+ reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
392
+ }
393
+ const expectedSigners = /* @__PURE__ */ new Set([
394
+ intent.feePayer,
395
+ intent.wallet,
396
+ intent.temporarySettlementTokenAccount
397
+ ]);
398
+ if (decoded.requiredSigners.length !== expectedSigners.size || !decoded.requiredSigners.every((signer2) => expectedSigners.has(signer2))) {
399
+ reject(
400
+ "unexpected_signers",
401
+ "Transaction signers must be exactly the sponsor, the agent wallet, and the temporary settlement account"
402
+ );
403
+ }
404
+ const ixs = [...decoded.instructions];
405
+ expectComputeBudgetPair(ixs, policy2);
406
+ expectCreateTemporaryAccount(ixs, intent, policy2);
407
+ expectInitializeTemporaryAccount(ixs, intent);
408
+ consumeFarmInstructions(ixs, intent.wallet);
409
+ expectKvaultWithdraw(ixs, {
410
+ wallet: intent.wallet,
411
+ vault: intent.vault,
412
+ shareMint: intent.shareMint,
413
+ asset: intent.asset,
414
+ userTokenAccount: intent.temporarySettlementTokenAccount,
415
+ maxSharesToRedeemRaw: BigInt(intent.maxSharesToRedeemRaw),
416
+ allowFullExit: false
417
+ });
418
+ expectTransferChecked(ixs, {
419
+ source: intent.temporarySettlementTokenAccount,
420
+ mint: intent.asset,
421
+ destination: intent.sellerUsdcAta,
422
+ authority: intent.wallet,
423
+ amount: BigInt(intent.amountRawUsdc),
424
+ label: "seller transfer"
425
+ });
426
+ if (ixs[0] !== void 0 && ixs[0].programAddress === SPL_TOKEN_PROGRAM_ID && ixs[0].data[0] === 12) {
427
+ expectTransferChecked(ixs, {
428
+ source: intent.temporarySettlementTokenAccount,
429
+ mint: intent.asset,
430
+ destination: intent.dustRecipientUsdcAta,
431
+ authority: intent.wallet,
432
+ amount: null,
433
+ label: "dust sweep"
434
+ });
435
+ }
436
+ expectCloseAccount(ixs, {
437
+ account: intent.temporarySettlementTokenAccount,
438
+ destination: intent.feePayer,
439
+ owner: intent.wallet
440
+ });
441
+ expectMemo(ixs, intent.memo);
442
+ if (ixs.length > 0) {
443
+ reject(
444
+ "unexpected_instruction",
445
+ `Transaction contains ${ixs.length} unexpected trailing instruction(s)`
446
+ );
447
+ }
448
+ }
449
+ function validateDepositIntentTransaction(params) {
450
+ const { intent } = params;
451
+ const now = params.nowMs ?? Date.now();
452
+ const policy2 = resolveIntentValidationPolicy(params.policy);
453
+ if (new Date(intent.expiresAt).getTime() <= now) {
454
+ reject("expired", "Deposit intent has expired");
455
+ }
456
+ assertVaultIntentTargets(intent);
457
+ const decoded = decodeIntentTransaction({
458
+ serializedTransaction: params.serializedTransaction,
459
+ ...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
460
+ });
461
+ if (decoded.messageHash !== intent.preparedMessageHash) {
462
+ reject("message_hash_mismatch", "Prepared message hash mismatch");
463
+ }
464
+ if (decoded.feePayer !== intent.feePayer) {
465
+ reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
466
+ }
467
+ let sawDeposit = false;
468
+ for (const ix of decoded.instructions) {
469
+ switch (ix.programAddress) {
470
+ case COMPUTE_BUDGET_PROGRAM_ID:
471
+ validateComputeBudgetInstruction(ix, policy2);
472
+ break;
473
+ case ASSOCIATED_TOKEN_PROGRAM_ID2:
474
+ expectAtaCreateForOwner(ix, intent.wallet);
475
+ break;
476
+ case MEMO_PROGRAM_ID:
477
+ break;
478
+ case KVAULT_PROGRAM_ID: {
479
+ if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
480
+ reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
481
+ }
482
+ const maxAmount = readU64LE(ix.data, 8);
483
+ if (maxAmount !== BigInt(intent.amountRawUsdc)) {
484
+ reject("amount_mismatch", "Deposit amount does not match the intent");
485
+ }
486
+ if (ix.accounts[0] !== intent.wallet) {
487
+ reject("wallet_mismatch", "Deposit user is not the agent wallet");
488
+ }
489
+ if (ix.accounts[1] !== intent.vault) {
490
+ reject("vault_mismatch", "Deposit vault mismatch");
491
+ }
492
+ if (ix.accounts[3] !== intent.asset) {
493
+ reject("asset_mismatch", "Deposit token mint mismatch");
494
+ }
495
+ if (ix.accounts[5] !== intent.shareMint) {
496
+ reject("share_mint_mismatch", "Deposit share mint mismatch");
497
+ }
498
+ const expectedSourceAta = deriveAssociatedTokenAddress({
499
+ owner: intent.wallet,
500
+ mint: intent.asset
501
+ });
502
+ if (ix.accounts[6] !== expectedSourceAta) {
503
+ reject(
504
+ "source_ata_mismatch",
505
+ "Deposit source must be the agent wallet's USDC ATA"
506
+ );
507
+ }
508
+ sawDeposit = true;
509
+ break;
510
+ }
511
+ default:
512
+ reject(
513
+ "unexpected_instruction",
514
+ `Unexpected program ${ix.programAddress} in deposit transaction`
515
+ );
516
+ }
517
+ }
518
+ if (!sawDeposit) {
519
+ reject("missing_instruction", "Deposit transaction has no KVault deposit");
520
+ }
521
+ }
522
+ function validateWithdrawalIntentTransaction(params) {
523
+ const { intent } = params;
524
+ const now = params.nowMs ?? Date.now();
525
+ const policy2 = resolveIntentValidationPolicy(params.policy);
526
+ if (new Date(intent.expiresAt).getTime() <= now) {
527
+ reject("expired", "Withdrawal intent has expired");
528
+ }
529
+ assertVaultIntentTargets(intent);
530
+ const expectedDestination = deriveAssociatedTokenAddress({
531
+ owner: intent.wallet,
532
+ mint: intent.asset
533
+ });
534
+ if (expectedDestination !== intent.destinationUsdcAta) {
535
+ reject(
536
+ "destination_mismatch",
537
+ "Withdrawal destination must be the agent wallet's USDC ATA"
538
+ );
539
+ }
540
+ const decoded = decodeIntentTransaction({
541
+ serializedTransaction: params.serializedTransaction,
542
+ ...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
543
+ });
544
+ if (decoded.messageHash !== intent.preparedMessageHash) {
545
+ reject("message_hash_mismatch", "Prepared message hash mismatch");
546
+ }
547
+ if (decoded.feePayer !== intent.feePayer) {
548
+ reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
549
+ }
550
+ let sawWithdraw = false;
551
+ for (const ix of decoded.instructions) {
552
+ switch (ix.programAddress) {
553
+ case COMPUTE_BUDGET_PROGRAM_ID:
554
+ validateComputeBudgetInstruction(ix, policy2);
555
+ break;
556
+ case MEMO_PROGRAM_ID:
557
+ case KAMINO_FARMS_PROGRAM_ID:
558
+ break;
559
+ case ASSOCIATED_TOKEN_PROGRAM_ID2:
560
+ expectAtaCreateForOwner(ix, intent.wallet);
561
+ break;
562
+ case SPL_TOKEN_PROGRAM_ID: {
563
+ if (ix.data[0] !== 9) {
564
+ reject(
565
+ "unexpected_instruction",
566
+ "Only CloseAccount token instructions are allowed in withdrawals"
567
+ );
568
+ }
569
+ if (ix.accounts[1] !== intent.wallet || ix.accounts[2] !== intent.wallet) {
570
+ reject(
571
+ "unexpected_instruction",
572
+ "Withdrawal CloseAccount must pay out to the agent wallet"
573
+ );
574
+ }
575
+ break;
576
+ }
577
+ case KVAULT_PROGRAM_ID: {
578
+ validateKvaultWithdrawInstruction(ix, {
579
+ wallet: intent.wallet,
580
+ vault: intent.vault,
581
+ shareMint: intent.shareMint,
582
+ asset: intent.asset,
583
+ userTokenAccount: intent.destinationUsdcAta,
584
+ maxSharesToRedeemRaw: BigInt(intent.maxSharesToRedeemRaw),
585
+ allowFullExit: intent.allowFullExit
586
+ });
587
+ sawWithdraw = true;
588
+ break;
589
+ }
590
+ default:
591
+ reject(
592
+ "unexpected_instruction",
593
+ `Unexpected program ${ix.programAddress} in withdrawal transaction`
594
+ );
595
+ }
596
+ }
597
+ if (!sawWithdraw) {
598
+ reject("missing_instruction", "Withdrawal transaction has no KVault withdraw");
599
+ }
600
+ }
601
+ function assertVaultIntentTargets(intent) {
602
+ if (intent.vault !== SUBLY_VAULT.address) {
603
+ reject("vault_mismatch", "Unsupported vault");
604
+ }
605
+ if (intent.shareMint !== SUBLY_VAULT.shareMint) {
606
+ reject("share_mint_mismatch", "Unsupported share mint");
607
+ }
608
+ if (intent.asset !== SUBLY_VAULT.usdcMint) {
609
+ reject("asset_mismatch", "Only USDC is supported");
610
+ }
611
+ }
612
+ function resolveIntentValidationPolicy(policy2) {
613
+ const resolved = {
614
+ maxComputeUnitLimit: policy2?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
615
+ maxComputeUnitPriceMicroLamports: policy2?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
616
+ maxTemporaryAccountLamports: policy2?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
617
+ };
618
+ if (!Number.isSafeInteger(resolved.maxComputeUnitLimit) || resolved.maxComputeUnitLimit <= 0) {
619
+ reject("invalid_policy", "maxComputeUnitLimit must be a positive safe integer");
620
+ }
621
+ if (resolved.maxComputeUnitPriceMicroLamports < 0n) {
622
+ reject(
623
+ "invalid_policy",
624
+ "maxComputeUnitPriceMicroLamports must be non-negative"
625
+ );
626
+ }
627
+ if (resolved.maxTemporaryAccountLamports <= 0n) {
628
+ reject("invalid_policy", "maxTemporaryAccountLamports must be positive");
629
+ }
630
+ return resolved;
631
+ }
632
+ function expectComputeBudgetPair(ixs, policy2) {
633
+ for (const discriminator of [2, 3]) {
634
+ const ix = ixs.shift();
635
+ if (ix === void 0 || ix.programAddress !== COMPUTE_BUDGET_PROGRAM_ID || ix.data[0] !== discriminator) {
636
+ reject(
637
+ "compute_budget_mismatch",
638
+ "Transaction must start with ComputeBudget limit and price instructions"
639
+ );
640
+ }
641
+ validateComputeBudgetInstruction(ix, policy2);
642
+ }
643
+ }
644
+ function validateComputeBudgetInstruction(ix, policy2) {
645
+ switch (ix.data[0]) {
646
+ case 2: {
647
+ const units = readU32LE(ix.data, 1);
648
+ if (units <= 0 || units > policy2.maxComputeUnitLimit) {
649
+ reject(
650
+ "compute_budget_mismatch",
651
+ `Compute unit limit ${units} exceeds policy maximum ${policy2.maxComputeUnitLimit}`
652
+ );
653
+ }
654
+ break;
655
+ }
656
+ case 3: {
657
+ const microLamports = readU64LE(ix.data, 1);
658
+ if (microLamports > policy2.maxComputeUnitPriceMicroLamports) {
659
+ reject(
660
+ "compute_budget_mismatch",
661
+ `Compute unit price ${microLamports} exceeds policy maximum ${policy2.maxComputeUnitPriceMicroLamports}`
662
+ );
663
+ }
664
+ break;
665
+ }
666
+ default:
667
+ reject("compute_budget_mismatch", "Unexpected ComputeBudget instruction");
668
+ }
669
+ }
670
+ function expectCreateTemporaryAccount(ixs, intent, policy2) {
671
+ const ix = ixs.shift();
672
+ if (ix === void 0 || ix.programAddress !== SYSTEM_PROGRAM_ID) {
673
+ reject("temp_account_mismatch", "Expected System createAccount instruction");
674
+ }
675
+ if (ix.data.length < 52 || readU32LE(ix.data, 0) !== 0) {
676
+ reject("temp_account_mismatch", "Expected createAccount discriminator");
677
+ }
678
+ const lamports = readU64LE(ix.data, 4);
679
+ const space = readU64LE(ix.data, 12);
680
+ const owner = bs583.encode(ix.data.subarray(20, 52));
681
+ if (space !== 165n) {
682
+ reject("temp_account_mismatch", "Temporary account space must be 165 bytes");
683
+ }
684
+ if (owner !== SPL_TOKEN_PROGRAM_ID) {
685
+ reject("temp_account_mismatch", "Temporary account owner must be the token program");
686
+ }
687
+ if (lamports > policy2.maxTemporaryAccountLamports) {
688
+ reject("temp_account_mismatch", "Temporary account rent exceeds the cap");
689
+ }
690
+ if (ix.accounts[0] !== intent.feePayer) {
691
+ reject("temp_account_mismatch", "Temporary account must be funded by the sponsor");
692
+ }
693
+ if (ix.accounts[1] !== intent.temporarySettlementTokenAccount) {
694
+ reject("temp_account_mismatch", "createAccount target is not the temporary account");
695
+ }
696
+ }
697
+ function expectInitializeTemporaryAccount(ixs, intent) {
698
+ const ix = ixs.shift();
699
+ if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 18) {
700
+ reject("temp_account_mismatch", "Expected InitializeAccount3 instruction");
701
+ }
702
+ const owner = bs583.encode(ix.data.subarray(1, 33));
703
+ if (owner !== intent.wallet) {
704
+ reject(
705
+ "temp_account_mismatch",
706
+ "Temporary account token authority must be the agent wallet"
707
+ );
708
+ }
709
+ if (ix.accounts[0] !== intent.temporarySettlementTokenAccount) {
710
+ reject("temp_account_mismatch", "InitializeAccount3 target mismatch");
711
+ }
712
+ if (ix.accounts[1] !== intent.asset) {
713
+ reject("temp_account_mismatch", "Temporary account mint must be USDC");
714
+ }
715
+ }
716
+ function consumeFarmInstructions(ixs, wallet) {
717
+ while (ixs[0] !== void 0 && ixs[0].programAddress === KAMINO_FARMS_PROGRAM_ID) {
718
+ const ix = ixs.shift();
719
+ if (!ix.accounts.includes(wallet)) {
720
+ reject(
721
+ "farm_instruction_mismatch",
722
+ "Farm unstake instruction does not reference the agent wallet"
723
+ );
724
+ }
725
+ }
726
+ }
727
+ function expectKvaultWithdraw(ixs, expectation) {
728
+ const ix = ixs.shift();
729
+ if (ix === void 0 || ix.programAddress !== KVAULT_PROGRAM_ID) {
730
+ reject("withdraw_mismatch", "Expected KVault withdraw instruction");
731
+ }
732
+ validateKvaultWithdrawInstruction(ix, expectation);
733
+ }
734
+ function validateKvaultWithdrawInstruction(ix, expectation) {
735
+ const isWithdraw = bytesStartWith(ix.data, KVAULT_WITHDRAW_DISCRIMINATOR);
736
+ const isWithdrawFromAvailable = bytesStartWith(
737
+ ix.data,
738
+ KVAULT_WITHDRAW_FROM_AVAILABLE_DISCRIMINATOR
739
+ );
740
+ if (!isWithdraw && !isWithdrawFromAvailable) {
741
+ reject("withdraw_mismatch", "Unexpected KVault instruction");
742
+ }
743
+ const sharesAmount = readU64LE(ix.data, 8);
744
+ const fullExit = sharesAmount === U64_MAX;
745
+ if (fullExit && !expectation.allowFullExit) {
746
+ reject("withdraw_mismatch", "Full-exit share burn is not allowed for this intent");
747
+ }
748
+ if (!fullExit && sharesAmount > expectation.maxSharesToRedeemRaw) {
749
+ reject(
750
+ "shares_exceed_max",
751
+ `Withdraw burns ${sharesAmount} shares which exceeds the approved maximum ${expectation.maxSharesToRedeemRaw}`
752
+ );
753
+ }
754
+ if (ix.accounts[0] !== expectation.wallet) {
755
+ reject("withdraw_mismatch", "Withdraw user is not the agent wallet");
756
+ }
757
+ if (ix.accounts[1] !== expectation.vault) {
758
+ reject("withdraw_mismatch", "Withdraw vault mismatch");
759
+ }
760
+ if (ix.accounts[5] !== expectation.userTokenAccount) {
761
+ reject(
762
+ "withdraw_mismatch",
763
+ "Withdraw token destination is not the approved account"
764
+ );
765
+ }
766
+ if (ix.accounts[6] !== expectation.asset) {
767
+ reject("withdraw_mismatch", "Withdraw token mint mismatch");
768
+ }
769
+ const expectedSharesAta = deriveAssociatedTokenAddress({
770
+ owner: expectation.wallet,
771
+ mint: expectation.shareMint
772
+ });
773
+ if (ix.accounts[7] !== expectedSharesAta) {
774
+ reject("withdraw_mismatch", "Withdraw share source must be the agent share ATA");
775
+ }
776
+ if (ix.accounts[8] !== expectation.shareMint) {
777
+ reject("withdraw_mismatch", "Withdraw share mint mismatch");
778
+ }
779
+ }
780
+ function expectTransferChecked(ixs, expectation) {
781
+ const ix = ixs.shift();
782
+ if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 12) {
783
+ reject("transfer_mismatch", `Expected TransferChecked for ${expectation.label}`);
784
+ }
785
+ const amount = readU64LE(ix.data, 1);
786
+ const decimals = ix.data[9];
787
+ if (expectation.amount !== null && amount !== expectation.amount) {
788
+ reject(
789
+ "amount_mismatch",
790
+ `${expectation.label} amount ${amount} does not match ${expectation.amount}`
791
+ );
792
+ }
793
+ if (decimals !== USDC_DECIMALS) {
794
+ reject("transfer_mismatch", `${expectation.label} has unexpected decimals`);
795
+ }
796
+ if (ix.accounts[0] !== expectation.source) {
797
+ reject("transfer_mismatch", `${expectation.label} source mismatch`);
798
+ }
799
+ if (ix.accounts[1] !== expectation.mint) {
800
+ reject("transfer_mismatch", `${expectation.label} mint mismatch`);
801
+ }
802
+ if (ix.accounts[2] !== expectation.destination) {
803
+ reject("transfer_mismatch", `${expectation.label} destination mismatch`);
804
+ }
805
+ if (ix.accounts[3] !== expectation.authority) {
806
+ reject("transfer_mismatch", `${expectation.label} authority mismatch`);
807
+ }
808
+ }
809
+ function expectCloseAccount(ixs, expectation) {
810
+ const ix = ixs.shift();
811
+ if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 9) {
812
+ reject("close_mismatch", "Expected CloseAccount instruction");
813
+ }
814
+ if (ix.accounts[0] !== expectation.account) {
815
+ reject("close_mismatch", "CloseAccount target is not the temporary account");
816
+ }
817
+ if (ix.accounts[1] !== expectation.destination) {
818
+ reject("close_mismatch", "CloseAccount rent destination must be the sponsor");
819
+ }
820
+ if (ix.accounts[2] !== expectation.owner) {
821
+ reject("close_mismatch", "CloseAccount authority must be the agent wallet");
822
+ }
823
+ }
824
+ function expectMemo(ixs, memo) {
825
+ const ix = ixs.shift();
826
+ if (ix === void 0 || ix.programAddress !== MEMO_PROGRAM_ID) {
827
+ reject("memo_mismatch", "Expected Memo instruction");
828
+ }
829
+ if (Buffer.from(ix.data).toString("utf8") !== memo) {
830
+ reject("memo_mismatch", "Memo content does not match the paymentId");
831
+ }
832
+ }
833
+ function expectAtaCreateForOwner(ix, owner) {
834
+ if (ix.accounts[2] !== owner) {
835
+ reject(
836
+ "unexpected_instruction",
837
+ "Associated token account creation for a foreign owner"
838
+ );
839
+ }
840
+ }
841
+ function bytesStartWith(data, prefix) {
842
+ if (data.length < prefix.length) {
843
+ return false;
844
+ }
845
+ return prefix.every((byte, index) => data[index] === byte);
846
+ }
847
+ function readU64LE(data, offset) {
848
+ if (data.length < offset + 8) {
849
+ reject("invalid_transaction_encoding", "Instruction data too short for u64");
850
+ }
851
+ return Buffer.from(data.subarray(offset, offset + 8)).readBigUInt64LE(0);
852
+ }
853
+ function readU32LE(data, offset) {
854
+ if (data.length < offset + 4) {
855
+ reject("invalid_transaction_encoding", "Instruction data too short for u32");
856
+ }
857
+ return Buffer.from(data.subarray(offset, offset + 4)).readUInt32LE(0);
858
+ }
859
+ function readShortVec(bytes, startOffset) {
860
+ let value = 0;
861
+ let shift = 0;
862
+ let offset = startOffset;
863
+ while (offset < bytes.length) {
864
+ const byte = bytes[offset];
865
+ value |= (byte & 127) << shift;
866
+ offset += 1;
867
+ if ((byte & 128) === 0) {
868
+ return { value, nextOffset: offset };
869
+ }
870
+ shift += 7;
871
+ if (shift > 28) {
872
+ return null;
873
+ }
874
+ }
875
+ return null;
876
+ }
877
+
878
+ // ../../src/client/agent-wallet-signer.ts
879
+ var LocalKeypairAgentWalletSigner = class {
880
+ validationMode = "structured_intent_transaction";
881
+ keyPairSigner;
882
+ validationPolicy;
883
+ constructor(keyPairSigner2, validationPolicy) {
884
+ this.keyPairSigner = keyPairSigner2;
885
+ this.validationPolicy = validationPolicy;
886
+ }
887
+ get walletAddress() {
888
+ return this.keyPairSigner.address;
889
+ }
890
+ async signPayment(params) {
891
+ this.assertIntentWallet(params.intent.wallet);
892
+ validatePaymentIntentTransaction({
893
+ ...params,
894
+ ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
895
+ });
896
+ return this.sign(params.serializedTransaction);
897
+ }
898
+ async signDeposit(params) {
899
+ this.assertIntentWallet(params.intent.wallet);
900
+ validateDepositIntentTransaction({
901
+ ...params,
902
+ ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
903
+ });
904
+ return this.sign(params.serializedTransaction);
905
+ }
906
+ async signWithdrawal(params) {
907
+ this.assertIntentWallet(params.intent.wallet);
908
+ validateWithdrawalIntentTransaction({
909
+ ...params,
910
+ ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
911
+ });
912
+ return this.sign(params.serializedTransaction);
913
+ }
914
+ assertIntentWallet(wallet) {
915
+ if (wallet !== this.keyPairSigner.address) {
916
+ throw new IntentValidationError(
917
+ "wallet_mismatch",
918
+ "Intent wallet does not match this signer's wallet"
919
+ );
920
+ }
921
+ }
922
+ async signApiMessage(message) {
923
+ const signature = await signBytes(
924
+ this.keyPairSigner.keyPair.privateKey,
925
+ message
926
+ );
927
+ return bs584.encode(signature);
928
+ }
929
+ async sign(serializedTransaction) {
930
+ const { serializedBase64, transaction } = await addSignaturesToSerializedTransaction({
931
+ serializedBase64: serializedTransaction,
932
+ signers: [this.keyPairSigner.keyPair]
933
+ });
934
+ const agentSignature = signatureBase58ForSigner(
935
+ transaction,
936
+ this.keyPairSigner.address
937
+ );
938
+ if (agentSignature === null) {
939
+ throw new IntentValidationError(
940
+ "signing_failed",
941
+ "Agent signature was not produced"
942
+ );
943
+ }
944
+ return { serializedTransaction: serializedBase64, agentSignature };
945
+ }
946
+ };
947
+
948
+ // ../../src/client/lookup-tables.ts
949
+ import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
950
+ import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
951
+ function lookupTableAddressesForTransaction(serializedTransaction) {
952
+ const wire = Buffer.from(serializedTransaction, "base64");
953
+ let offset = 0;
954
+ let signatureCount = 0;
955
+ let shift = 0;
956
+ while (offset < wire.length) {
957
+ const byte = wire[offset];
958
+ signatureCount |= (byte & 127) << shift;
959
+ offset += 1;
960
+ if ((byte & 128) === 0) {
961
+ break;
962
+ }
963
+ shift += 7;
964
+ }
965
+ const messageBytes = wire.subarray(offset + signatureCount * 64);
966
+ const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
967
+ const lookups = compiled.addressTableLookups ?? [];
968
+ return lookups.map((lookup) => String(lookup.lookupTableAddress));
969
+ }
970
+ async function fetchLookupTablesForTransaction(rpc, serializedTransaction) {
971
+ const addresses = lookupTableAddressesForTransaction(serializedTransaction);
972
+ if (addresses.length === 0) {
973
+ return {};
974
+ }
975
+ const tables = await fetchAllMaybeAddressLookupTable(
976
+ rpc,
977
+ addresses.map((value) => address(value))
978
+ );
979
+ const result = {};
980
+ for (const table of tables) {
981
+ if (table.exists) {
982
+ result[table.address] = table.data.addresses.map(String);
983
+ }
984
+ }
985
+ return result;
986
+ }
987
+
988
+ // ../../src/api/wallet-auth.ts
989
+ import { createHash as createHash3 } from "node:crypto";
990
+ import bs585 from "bs58";
991
+ import nacl from "tweetnacl";
992
+ var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
993
+ var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
994
+ var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
995
+ function sha256Hex(data) {
996
+ return createHash3("sha256").update(data, "utf8").digest("hex");
997
+ }
998
+ function walletAuthMessage(params) {
999
+ return new TextEncoder().encode(
1000
+ `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
1001
+ params.rawBody
1002
+ )}:${params.signedAtMs}`
1003
+ );
1004
+ }
1005
+
1006
+ // ../../src/client/wallet-auth-headers.ts
1007
+ async function walletAuthHeaders(params) {
1008
+ const signedAtMs = String(Date.now());
1009
+ const message = walletAuthMessage({
1010
+ method: params.method,
1011
+ path: new URL(params.url).pathname,
1012
+ rawBody: params.body ?? "",
1013
+ signedAtMs
1014
+ });
1015
+ return {
1016
+ [WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
1017
+ [WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
1018
+ [WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
1019
+ };
1020
+ }
1021
+
1022
+ // ../../src/client/vault-flows.ts
1023
+ var VaultFlowClientError = class extends Error {
1024
+ constructor(step, message, detail = null, code = null, errorDetails = null) {
1025
+ super(message);
1026
+ this.step = step;
1027
+ this.detail = detail;
1028
+ this.code = code;
1029
+ this.errorDetails = errorDetails;
1030
+ this.name = "VaultFlowClientError";
1031
+ }
1032
+ step;
1033
+ detail;
1034
+ code;
1035
+ errorDetails;
1036
+ };
1037
+ var VaultFlowClient = class {
1038
+ baseUrl;
1039
+ signer;
1040
+ fetchImpl;
1041
+ lookupTablesFor;
1042
+ pollTimeoutMs;
1043
+ pollIntervalMs;
1044
+ constructor(config) {
1045
+ this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
1046
+ this.signer = config.signer;
1047
+ this.fetchImpl = config.fetchImpl ?? fetch;
1048
+ this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
1049
+ this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1050
+ this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1051
+ }
1052
+ /**
1053
+ * Moves USDC from the agent wallet into the vault (fee sponsored). Under
1054
+ * depositPolicy "owner_approval_required" the relayer refuses to prepare
1055
+ * without an owner approval; when the caller passes none, an already
1056
+ * APPROVED deposit approval for this exact amount (e.g. the mandate's
1057
+ * initialDeposit — "one Face ID covers mandate + first deposit") is looked
1058
+ * up and used automatically before surfacing deposit_approval_required.
1059
+ */
1060
+ async deposit(input) {
1061
+ let approvalId = input.approvalId;
1062
+ let prepared;
1063
+ try {
1064
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1065
+ wallet: this.signer.walletAddress,
1066
+ amountRawUsdc: input.amountRawUsdc.toString(),
1067
+ ...approvalId === void 0 ? {} : { approvalId }
1068
+ });
1069
+ } catch (error) {
1070
+ if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
1071
+ throw error;
1072
+ }
1073
+ approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
1074
+ if (approvalId === void 0) {
1075
+ throw error;
1076
+ }
1077
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1078
+ wallet: this.signer.walletAddress,
1079
+ amountRawUsdc: input.amountRawUsdc.toString(),
1080
+ approvalId
1081
+ });
1082
+ }
1083
+ const signed = await this.signer.signDeposit({
1084
+ intent: prepared.signingIntent,
1085
+ serializedTransaction: prepared.serializedTransaction,
1086
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1087
+ });
1088
+ let outcome = await this.postJson("submit", "/v1/deposits/submit", {
1089
+ depositId: prepared.depositId,
1090
+ serializedTransaction: signed.serializedTransaction,
1091
+ agentSignature: signed.agentSignature
1092
+ });
1093
+ if (outcome.status === "submitted") {
1094
+ outcome = await this.pollUntilTerminal(
1095
+ `/v1/deposits/${prepared.depositId}`,
1096
+ outcome
1097
+ );
1098
+ }
1099
+ return {
1100
+ depositId: prepared.depositId,
1101
+ status: outcome.status,
1102
+ txSignature: outcome.txSignature ?? null,
1103
+ actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
1104
+ sharesMintedRaw: outcome.sharesMintedRaw ?? null,
1105
+ errorCode: outcome.errorCode ?? null
1106
+ };
1107
+ }
1108
+ /**
1109
+ * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
1110
+ * sponsored). A plain withdrawal is the exit path and MAY spend principal;
1111
+ * with purpose "yield_realize" the relayer refuses anything beyond the
1112
+ * spendable yield (the payment path, via RelayerYieldRealizer).
1113
+ */
1114
+ async withdraw(input) {
1115
+ const prepared = await this.postJson(
1116
+ "prepare",
1117
+ "/v1/withdrawals/prepare",
1118
+ {
1119
+ wallet: this.signer.walletAddress,
1120
+ amountRawUsdc: input.amountRawUsdc.toString(),
1121
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose },
1122
+ ...input.payment === void 0 ? {} : { payment: input.payment },
1123
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1124
+ }
1125
+ );
1126
+ const signed = await this.signer.signWithdrawal({
1127
+ intent: prepared.signingIntent,
1128
+ serializedTransaction: prepared.serializedTransaction,
1129
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1130
+ });
1131
+ let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
1132
+ withdrawalId: prepared.withdrawalId,
1133
+ serializedTransaction: signed.serializedTransaction,
1134
+ agentSignature: signed.agentSignature
1135
+ });
1136
+ if (outcome.status === "submitted") {
1137
+ outcome = await this.pollUntilTerminal(
1138
+ `/v1/withdrawals/${prepared.withdrawalId}`,
1139
+ outcome
1140
+ );
1141
+ }
1142
+ return {
1143
+ withdrawalId: prepared.withdrawalId,
1144
+ status: outcome.status,
1145
+ txSignature: outcome.txSignature ?? null,
1146
+ destinationUsdcAta: prepared.destinationUsdcAta,
1147
+ actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
1148
+ actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
1149
+ errorCode: outcome.errorCode ?? null
1150
+ };
1151
+ }
1152
+ /**
1153
+ * Reads the yield budget. Syncs the relayer's ledger from chain first (so
1154
+ * yield accrued since the last sync shows up); the sync is best-effort and
1155
+ * on failure the last-synced view is returned.
1156
+ */
1157
+ async getBudget(options = {}) {
1158
+ if (options.refreshFromChain !== false) {
1159
+ try {
1160
+ await this.postJson(
1161
+ "sync",
1162
+ `/v1/wallets/${this.signer.walletAddress}/sync`,
1163
+ { source: "chain" }
1164
+ );
1165
+ } catch {
1166
+ }
1167
+ }
1168
+ const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
1169
+ const response = await this.fetchImpl(url, {
1170
+ headers: await walletAuthHeaders({
1171
+ signer: this.signer,
1172
+ method: "GET",
1173
+ url
1174
+ })
1175
+ });
1176
+ const text = await response.text();
1177
+ if (response.status !== 200) {
1178
+ throw new VaultFlowClientError(
1179
+ "budget",
1180
+ `budget endpoint returned ${response.status}: ${text}`
1181
+ );
1182
+ }
1183
+ let parsed;
1184
+ try {
1185
+ parsed = JSON.parse(text);
1186
+ } catch {
1187
+ throw new VaultFlowClientError(
1188
+ "budget",
1189
+ "budget endpoint returned 200 with a non-JSON body",
1190
+ text
1191
+ );
1192
+ }
1193
+ const body = parsed;
1194
+ return {
1195
+ wallet: this.signer.walletAddress,
1196
+ principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
1197
+ positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
1198
+ grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
1199
+ spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
1200
+ };
1201
+ }
1202
+ /** Best-effort audit link: reports the x402 payment tx a realize funded. */
1203
+ async reportPayment(input) {
1204
+ await this.postJson("submit", "/v1/payments/report", {
1205
+ wallet: this.signer.walletAddress,
1206
+ withdrawalId: input.withdrawalId,
1207
+ paymentTxSignature: input.paymentTxSignature
1208
+ });
1209
+ }
1210
+ /** Wallet's approvals as the relayer sees them (optionally by status). */
1211
+ async listApprovals(status) {
1212
+ const body = await this.getJson(
1213
+ `/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" : `?status=${encodeURIComponent(status)}`}`
1214
+ );
1215
+ return body.approvals ?? [];
1216
+ }
1217
+ /**
1218
+ * Creates the owner-onboarding setup link (wallet-auth pins the agreed
1219
+ * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
1220
+ */
1221
+ async createSetupSession(input) {
1222
+ return await this.postJson(
1223
+ "prepare",
1224
+ `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
1225
+ {
1226
+ ...input.policy === void 0 ? {} : { policy: input.policy },
1227
+ ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
1228
+ ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
1229
+ ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1230
+ }
1231
+ );
1232
+ }
1233
+ /** Polls a setup session (public capability URL — no auth needed). */
1234
+ async getSetupSession(sessionId) {
1235
+ const url = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
1236
+ const response = await this.fetchImpl(url);
1237
+ const text = await response.text();
1238
+ if (response.status !== 200) {
1239
+ const parsed = parseRelayerError(text);
1240
+ throw new VaultFlowClientError(
1241
+ "read",
1242
+ parsed.message ?? `setup session read failed with ${response.status}`,
1243
+ text,
1244
+ parsed.code,
1245
+ parsed.details
1246
+ );
1247
+ }
1248
+ return JSON.parse(text);
1249
+ }
1250
+ /**
1251
+ * Finds an APPROVED, unconsumed deposit approval bound to exactly this
1252
+ * amount — the shape the mandate's initialDeposit approval has.
1253
+ */
1254
+ async findApprovedDepositApproval(amountRawUsdc) {
1255
+ try {
1256
+ const approvals = await this.listApprovals("approved");
1257
+ const match = approvals.find((approval) => {
1258
+ const binding = approval.binding;
1259
+ return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
1260
+ });
1261
+ return match?.approvalId;
1262
+ } catch {
1263
+ return void 0;
1264
+ }
1265
+ }
1266
+ /**
1267
+ * Polls the reconciling GET endpoint until the intent leaves "submitted"
1268
+ * (each read looks the tx up on-chain) or the timeout elapses.
1269
+ */
1270
+ async pollUntilTerminal(path, last) {
1271
+ const deadline = Date.now() + this.pollTimeoutMs;
1272
+ let latest = last;
1273
+ while (Date.now() < deadline) {
1274
+ await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
1275
+ const url = `${this.baseUrl}${path}`;
1276
+ const response = await this.fetchImpl(url, {
1277
+ headers: await walletAuthHeaders({
1278
+ signer: this.signer,
1279
+ method: "GET",
1280
+ url
1281
+ })
1282
+ });
1283
+ if (response.status !== 200) {
1284
+ continue;
1285
+ }
1286
+ try {
1287
+ latest = await response.json();
1288
+ } catch {
1289
+ continue;
1290
+ }
1291
+ if (latest.status !== "submitted") {
1292
+ return latest;
1293
+ }
1294
+ }
1295
+ return latest;
1296
+ }
1297
+ async postJson(step, path, body) {
1298
+ const url = `${this.baseUrl}${path}`;
1299
+ const serialized = JSON.stringify(body);
1300
+ const response = await this.fetchImpl(url, {
1301
+ method: "POST",
1302
+ headers: {
1303
+ ...await walletAuthHeaders({
1304
+ signer: this.signer,
1305
+ method: "POST",
1306
+ url,
1307
+ body: serialized
1308
+ }),
1309
+ "content-type": "application/json"
1310
+ },
1311
+ body: serialized
1312
+ });
1313
+ const text = await response.text();
1314
+ if (response.status !== 200) {
1315
+ const parsed = parseRelayerError(text);
1316
+ throw new VaultFlowClientError(
1317
+ step,
1318
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1319
+ text,
1320
+ parsed.code,
1321
+ parsed.details
1322
+ );
1323
+ }
1324
+ try {
1325
+ return JSON.parse(text);
1326
+ } catch {
1327
+ throw new VaultFlowClientError(
1328
+ step,
1329
+ `${path} returned 200 with a non-JSON body`,
1330
+ text
1331
+ );
1332
+ }
1333
+ }
1334
+ async getJson(path) {
1335
+ const url = `${this.baseUrl}${path}`;
1336
+ const response = await this.fetchImpl(url, {
1337
+ headers: await walletAuthHeaders({
1338
+ signer: this.signer,
1339
+ method: "GET",
1340
+ url
1341
+ })
1342
+ });
1343
+ const text = await response.text();
1344
+ if (response.status !== 200) {
1345
+ const parsed = parseRelayerError(text);
1346
+ throw new VaultFlowClientError(
1347
+ "read",
1348
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1349
+ text,
1350
+ parsed.code,
1351
+ parsed.details
1352
+ );
1353
+ }
1354
+ try {
1355
+ return JSON.parse(text);
1356
+ } catch {
1357
+ throw new VaultFlowClientError(
1358
+ "read",
1359
+ `${path} returned 200 with a non-JSON body`,
1360
+ text
1361
+ );
1362
+ }
1363
+ }
1364
+ };
1365
+ function parseRelayerError(text) {
1366
+ try {
1367
+ const parsed = JSON.parse(text);
1368
+ return {
1369
+ code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
1370
+ message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
1371
+ details: parsed.error?.details ?? null
1372
+ };
1373
+ } catch {
1374
+ return { code: null, message: null, details: null };
1375
+ }
1376
+ }
1377
+
1378
+ // ../../src/solana/keys.ts
1379
+ import { readFileSync } from "node:fs";
1380
+ import bs586 from "bs58";
1381
+ import {
1382
+ createKeyPairSignerFromBytes
1383
+ } from "@solana/kit";
1384
+ async function loadKeyPairSigner(params) {
1385
+ const { base58Secret, jsonFilePath, label } = params;
1386
+ if (base58Secret !== void 0 && base58Secret.length > 0) {
1387
+ const bytes = bs586.decode(base58Secret);
1388
+ if (bytes.length !== 64) {
1389
+ throw new Error(`${label} base58 secret must decode to 64 bytes`);
1390
+ }
1391
+ return createKeyPairSignerFromBytes(bytes);
1392
+ }
1393
+ if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1394
+ const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
1395
+ if (!Array.isArray(raw) || raw.length !== 64) {
1396
+ throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1397
+ }
1398
+ return createKeyPairSignerFromBytes(Uint8Array.from(raw));
1399
+ }
1400
+ throw new Error(`${label} keypair is not configured`);
1401
+ }
1402
+
1403
+ // ../../src/solana/rpc.ts
1404
+ import { createSolanaRpc } from "@solana/kit";
1405
+ function createRpc(url) {
1406
+ return createSolanaRpc(url);
1407
+ }
1408
+
1409
+ // src/setup-link.ts
1410
+ function fail(message) {
1411
+ process.stderr.write(`${message}
1412
+ `);
1413
+ process.exit(1);
1414
+ }
1415
+ var USAGE = "Usage: pay setup-link [--initial-deposit <rawUsdc>] [--approval-threshold <rawUsdc>] [--per-payment-cap <rawUsdc>] [--daily-api-cap <rawUsdc>] [--daily-deposit-cap <rawUsdc>] [--ttl-days <days>]";
1416
+ var FLAG_TO_POLICY_KEY = {
1417
+ "--approval-threshold": "approvalThresholdRawUsdc",
1418
+ "--per-payment-cap": "perPaymentCapRawUsdc",
1419
+ "--daily-api-cap": "dailyApiSpendCapRawUsdc",
1420
+ "--daily-deposit-cap": "dailyDepositCapRawUsdc"
1421
+ };
1422
+ var policy = {};
1423
+ var initialDepositRawUsdc;
1424
+ var mandateTtlDays;
1425
+ var args = [];
1426
+ for (const raw of process.argv.slice(2)) {
1427
+ const eq = raw.startsWith("--") ? raw.indexOf("=") : -1;
1428
+ if (eq > 0) {
1429
+ args.push(raw.slice(0, eq), raw.slice(eq + 1));
1430
+ } else {
1431
+ args.push(raw);
1432
+ }
1433
+ }
1434
+ for (let i = 0; i < args.length; i += 2) {
1435
+ const flag = args[i];
1436
+ const value = args[i + 1];
1437
+ if (value === void 0) {
1438
+ fail(`missing value for ${flag}
1439
+ ${USAGE}`);
1440
+ }
1441
+ if (flag === "--initial-deposit") {
1442
+ if (!/^[1-9]\d*$/.test(value)) {
1443
+ fail(`--initial-deposit must be a positive raw USDC integer
1444
+ ${USAGE}`);
1445
+ }
1446
+ initialDepositRawUsdc = value;
1447
+ } else if (flag === "--ttl-days") {
1448
+ const days = Number(value);
1449
+ if (!Number.isInteger(days) || days <= 0) {
1450
+ fail(`--ttl-days must be a positive integer
1451
+ ${USAGE}`);
1452
+ }
1453
+ mandateTtlDays = days;
1454
+ } else if (FLAG_TO_POLICY_KEY[flag] !== void 0) {
1455
+ if (!/^[1-9]\d*$/.test(value)) {
1456
+ fail(`${flag} must be a positive raw USDC integer
1457
+ ${USAGE}`);
1458
+ }
1459
+ policy[FLAG_TO_POLICY_KEY[flag]] = value;
1460
+ } else {
1461
+ fail(`unrecognized flag: ${flag}
1462
+ ${USAGE}`);
1463
+ }
1464
+ }
1465
+ var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1466
+ var keyPairSigner = await loadKeyPairSigner({
1467
+ base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1468
+ jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1469
+ label: "SUBLY_DEMO_AGENT_KEYPAIR"
1470
+ });
1471
+ var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
1472
+ var vaultFlows = new VaultFlowClient({
1473
+ relayerBaseUrl,
1474
+ signer,
1475
+ rpc: createRpc(
1476
+ process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com"
1477
+ )
1478
+ });
1479
+ console.error(`[setup-link] agent ${signer.walletAddress} -> ${relayerBaseUrl}`);
1480
+ try {
1481
+ const created = await vaultFlows.createSetupSession({
1482
+ ...Object.keys(policy).length === 0 ? {} : { policy },
1483
+ ...mandateTtlDays === void 0 ? {} : { mandateTtlDays },
1484
+ ...initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc }
1485
+ });
1486
+ process.stdout.write(
1487
+ `${JSON.stringify(
1488
+ {
1489
+ ...created,
1490
+ instructions: `Paste setupUrl to the user verbatim (expires in 10 minutes, single-use). After they confirm on their device, check with: pay setup-status ${created.sessionId} \u2014 when completed, run the first deposit; its approval is picked up automatically.`
1491
+ },
1492
+ null,
1493
+ 2
1494
+ )}
1495
+ `
1496
+ );
1497
+ } catch (error) {
1498
+ if (error instanceof VaultFlowClientError) {
1499
+ fail(`[setup-link] failed: ${error.message}`);
1500
+ }
1501
+ throw error;
1502
+ }