@subly_fi/pay 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,28 +1,45 @@
1
1
  #!/usr/bin/env node
2
- // Single dispatcher bin for @subly_fi/pay so `npx -y @subly_fi/pay <subcommand>`
3
- // resolves cleanly. Subcommands map to the bundled entry points; argv is
4
- // reshaped so each entry sees its own positional args at the usual index.
2
+ import { readFileSync } from "node:fs";
5
3
  import { dirname, join } from "node:path";
6
4
  import { fileURLToPath } from "node:url";
7
-
5
+ const here = dirname(fileURLToPath(import.meta.url));
8
6
  const TARGETS = {
9
- mcp: "mcp-server.js",
10
- fetch: "pay.js",
11
- deposit: "deposit.js",
12
- withdraw: "withdraw.js",
13
- "setup-link": "setup-link.js",
14
- "setup-status": "setup-status.js"
7
+ mcp: "mcp-server.js", fetch: "pay.js", deposit: "deposit.js", withdraw: "withdraw.js",
8
+ "setup-link": "setup-link.js", "setup-status": "setup-status.js",
9
+ doctor: "doctor.js", budget: "budget.js", vaults: "vaults.js"
15
10
  };
11
+ const HELP = `Subly — x402 payments from Kamino USDC vault yield
12
+
13
+ Usage: pay <command> [arguments]
14
+ doctor Check configuration, relayer and mainnet RPC (no signing)
15
+ vaults Print the locally trusted vault catalogue (offline)
16
+ setup-link [options] Create a human owner approval link
17
+ setup-status <sessionId|URL> Read setup completion
18
+ deposit <rawUSDC> [approvalId] Deposit into the selected vault (real funds)
19
+ budget Refresh and read the selected vault's yield budget
20
+ withdraw <rawUSDC> [approvalId] Withdraw to the agent wallet (real funds)
21
+ fetch <URL> Pay a compatible x402 API within the configured cap
22
+ mcp Start the stdio MCP server
23
+ --version Print package version
16
24
 
25
+ Amounts: 1000000 raw USDC = 1 USDC. Requires Node.js 24+.
26
+ Set SUBLY_RELAYER_URL, SOLANA_RPC_URL and a wallet signer before use.
27
+ Local signer: SUBLY_DEMO_AGENT_KEYPAIR_PATH=/absolute/path/agent.json
28
+ Payment cap: SUBLY_MCP_MAX_AMOUNT_RAW_USDC (default 10000 = 0.01 USDC).
29
+ Run pay setup-link --help for policy options.
30
+ Guide: https://github.com/SublyFi/subly-payment-protocol/tree/main/packages/pay
31
+ `;
17
32
  const [sub, ...rest] = process.argv.slice(2);
18
- const target = sub === undefined ? undefined : TARGETS[sub];
19
- if (target === undefined) {
20
- console.error(
21
- "usage: pay <mcp|fetch <url>|deposit <amountRawUsdc>|withdraw <amountRawUsdc>|setup-link|setup-status <sessionId|setupUrl>>"
22
- );
23
- process.exit(1);
33
+ if (sub === "--version" || sub === "-v") {
34
+ console.log(JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8")).version);
35
+ } else if (!sub || sub === "--help" || sub === "-h" || sub === "help" || rest.includes("--help") || rest.includes("-h")) {
36
+ console.log(HELP);
37
+ if (sub === "setup-link") console.log("Policy options: --initial-deposit <rawUSDC> --approval-threshold <rawUSDC> --per-payment-cap <rawUSDC> --daily-api-cap <rawUSDC> --daily-deposit-cap <rawUSDC> --ttl-days <days>");
38
+ } else if (!Object.hasOwn(TARGETS, sub)) {
39
+ console.error(`Unknown command: ${sub}\nRun pay --help.`);
40
+ process.exitCode = 1;
41
+ } else {
42
+ process.argv = [process.argv[0], join(here, TARGETS[sub]), ...rest];
43
+ try { await import(`./${TARGETS[sub]}`); }
44
+ catch (error) { console.error(error instanceof Error ? error.message : "Command failed"); process.exitCode = 1; }
24
45
  }
25
-
26
- const here = dirname(fileURLToPath(import.meta.url));
27
- process.argv = [process.argv[0], join(here, target), ...rest];
28
- await import(`./${target}`);
package/dist/deposit.js CHANGED
@@ -1,3 +1,141 @@
1
+ // ../../src/config/vault-catalog.ts
2
+ import { readFileSync } from "node:fs";
3
+ import { z } from "zod";
4
+
5
+ // ../../src/lib/solana-address.ts
6
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
7
+ var BASE58_LOOKUP = new Map(
8
+ [...BASE58_ALPHABET].map((character, index) => [character, BigInt(index)])
9
+ );
10
+ function assertSolanaAddress(value, fieldName) {
11
+ if (value.length < 32 || value.length > 44) {
12
+ throw new Error(`${fieldName} must be a valid Solana public key`);
13
+ }
14
+ if (decodeBase58(value).length !== 32) {
15
+ throw new Error(`${fieldName} must be a valid Solana public key`);
16
+ }
17
+ return value;
18
+ }
19
+ function decodeBase58(value) {
20
+ if (value.length === 0) {
21
+ return new Uint8Array();
22
+ }
23
+ let decoded = 0n;
24
+ for (const character of value) {
25
+ const digit = BASE58_LOOKUP.get(character);
26
+ if (digit === void 0) {
27
+ return new Uint8Array();
28
+ }
29
+ decoded = decoded * 58n + digit;
30
+ }
31
+ const bytes = [];
32
+ while (decoded > 0n) {
33
+ bytes.push(Number(decoded & 0xffn));
34
+ decoded >>= 8n;
35
+ }
36
+ for (const character of value) {
37
+ if (character !== "1") {
38
+ break;
39
+ }
40
+ bytes.push(0);
41
+ }
42
+ return Uint8Array.from(bytes.reverse());
43
+ }
44
+
45
+ // ../../src/config/vault.ts
46
+ var MAINNET_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
47
+ var KAMINO_VAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
48
+ var NO_VAULT_FARM = "11111111111111111111111111111111";
49
+ var DEFAULT_VAULT_CONFIG = Object.freeze({
50
+ address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
51
+ programId: KAMINO_VAULT_PROGRAM_ID,
52
+ usdcMint: MAINNET_USDC_MINT,
53
+ shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
54
+ farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
55
+ });
56
+ function vaultConfigFromEnv(env = process.env) {
57
+ const value = (name) => env[name]?.trim() || void 0;
58
+ const vaultAddress = value("SUBLY_VAULT_ADDRESS") ?? DEFAULT_VAULT_CONFIG.address;
59
+ const customVault = vaultAddress !== DEFAULT_VAULT_CONFIG.address;
60
+ const anchor = (name, fallback) => {
61
+ const configured = value(name);
62
+ if (customVault && configured === void 0) {
63
+ throw new Error(
64
+ `${name} is required for a custom vault. Generate its settings with npm run configure:vault -- <vault-address>; use ${NO_VAULT_FARM} for a vault without a farm.`
65
+ );
66
+ }
67
+ return assertSolanaAddress(configured ?? fallback, name);
68
+ };
69
+ const usdcMint = value("SUBLY_VAULT_USDC_MINT") ?? MAINNET_USDC_MINT;
70
+ if (usdcMint !== MAINNET_USDC_MINT) {
71
+ throw new Error(
72
+ "SUBLY_VAULT_USDC_MINT must be mainnet USDC; other deposit assets are not supported"
73
+ );
74
+ }
75
+ return Object.freeze({
76
+ address: assertSolanaAddress(vaultAddress, "SUBLY_VAULT_ADDRESS"),
77
+ programId: KAMINO_VAULT_PROGRAM_ID,
78
+ usdcMint,
79
+ shareMint: anchor("SUBLY_VAULT_SHARE_MINT", DEFAULT_VAULT_CONFIG.shareMint),
80
+ farm: anchor("SUBLY_VAULT_FARM", DEFAULT_VAULT_CONFIG.farm)
81
+ });
82
+ }
83
+
84
+ // ../../src/config/vault-catalog.ts
85
+ var publicKey = z.string().refine((value) => {
86
+ try {
87
+ assertSolanaAddress(value, "vault catalog address");
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }, "Invalid Solana public key");
93
+ var catalogSchema = z.object({
94
+ version: z.literal(1),
95
+ defaultVault: publicKey,
96
+ vaults: z.array(z.object({
97
+ address: publicKey,
98
+ programId: z.literal(KAMINO_VAULT_PROGRAM_ID),
99
+ usdcMint: z.literal(MAINNET_USDC_MINT),
100
+ shareMint: publicKey,
101
+ farm: publicKey,
102
+ name: z.string().min(1).max(128).optional(),
103
+ depositsEnabled: z.boolean().optional(),
104
+ extraLookupTables: z.array(publicKey).max(16).optional()
105
+ }).strict()).min(1).max(100)
106
+ }).strict();
107
+ function parseVaultCatalog(value) {
108
+ const catalog = catalogSchema.parse(value);
109
+ const addresses = new Set(catalog.vaults.map((vault) => vault.address));
110
+ if (addresses.size !== catalog.vaults.length) throw new Error("Duplicate vault in catalog");
111
+ if (!addresses.has(catalog.defaultVault)) throw new Error("defaultVault must be in the vault catalog");
112
+ return { ...catalog, vaults: catalog.vaults.map((vault) => Object.freeze(vault)) };
113
+ }
114
+ function vaultCatalogFromEnv(env = process.env) {
115
+ const path = env.SUBLY_VAULTS_FILE?.trim();
116
+ if (!path) {
117
+ const vault = vaultConfigFromEnv(env);
118
+ return { version: 1, defaultVault: vault.address, vaults: [vault] };
119
+ }
120
+ const catalog = parseVaultCatalog(JSON.parse(readFileSync(path, "utf8")));
121
+ const selected = env.SUBLY_VAULT_ADDRESS?.trim() || catalog.defaultVault;
122
+ if (!catalog.vaults.some((vault) => vault.address === selected)) {
123
+ throw new Error("SUBLY_VAULT_ADDRESS must be in SUBLY_VAULTS_FILE");
124
+ }
125
+ return { ...catalog, defaultVault: selected };
126
+ }
127
+ function defaultCatalogVault(catalog) {
128
+ return catalog.vaults.find((vault) => vault.address === catalog.defaultVault);
129
+ }
130
+
131
+ // ../../src/config/constants.ts
132
+ var PAYMENT_SCHEME = "subly-yield-exact";
133
+ var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
134
+ var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
135
+ var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
136
+ var SUBLY_VAULT = defaultCatalogVault(vaultCatalogFromEnv());
137
+ var USDC_DECIMALS = 6;
138
+
1
139
  // ../../src/api/wallet-auth.ts
2
140
  import { createHash } from "node:crypto";
3
141
  import bs58 from "bs58";
@@ -81,22 +219,24 @@ async function ensureWalletOnboarded(params) {
81
219
  }
82
220
  };
83
221
  const wallet = params.signer.walletAddress;
222
+ const vault = params.vault ?? params.signer.vault?.address ?? SUBLY_VAULT.address;
84
223
  await post("register", "/v1/wallets/agent", {
85
224
  wallet,
225
+ vault,
86
226
  signingPolicyId: SELF_SERVE_POLICY_ID,
87
227
  signingMode: "non_interactive",
88
228
  signerValidationMode: params.signer.validationMode,
89
229
  signerProvider: params.signer.provider ?? "local-keypair",
90
230
  activateForPayments: true
91
231
  });
92
- await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
232
+ await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain", vault });
93
233
  }
94
234
 
95
235
  // ../../src/client/signer-env.ts
96
236
  import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
97
237
 
98
238
  // ../../src/solana/keys.ts
99
- import { readFileSync } from "node:fs";
239
+ import { readFileSync as readFileSync2 } from "node:fs";
100
240
  import bs582 from "bs58";
101
241
  import {
102
242
  createKeyPairSignerFromBytes
@@ -111,7 +251,7 @@ function loadSecretKeyBytes(params) {
111
251
  return bytes;
112
252
  }
113
253
  if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
114
- const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
254
+ const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
115
255
  if (!Array.isArray(raw) || raw.length !== 64) {
116
256
  throw new Error(`${label} keypair file must be a 64-byte JSON array`);
117
257
  }
@@ -235,10 +375,10 @@ function ed25519PublicKeyBytes(provider, walletAddress) {
235
375
  return bytes;
236
376
  }
237
377
  function verifiedEd25519Signature(params) {
238
- const publicKey = ed25519PublicKeyBytes(params.provider, params.walletAddress);
378
+ const publicKey2 = ed25519PublicKeyBytes(params.provider, params.walletAddress);
239
379
  const encoded = params.encodedSignature.trim();
240
380
  for (const candidate of decodeSignatureCandidates(encoded)) {
241
- if (nacl2.sign.detached.verify(params.message, candidate, publicKey)) {
381
+ if (nacl2.sign.detached.verify(params.message, candidate, publicKey2)) {
242
382
  return candidate;
243
383
  }
244
384
  }
@@ -292,8 +432,8 @@ async function requestVerifiedTransactionSignature(params) {
292
432
  `signed transaction is missing the signature for ${transport.walletAddress}`
293
433
  );
294
434
  }
295
- const publicKey = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
296
- if (!nacl2.sign.detached.verify(params.messageBytes, signature, publicKey)) {
435
+ const publicKey2 = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
436
+ if (!nacl2.sign.detached.verify(params.messageBytes, signature, publicKey2)) {
297
437
  throw new RemoteSigningError(
298
438
  transport.provider,
299
439
  "returned signature does not verify over the requested transaction"
@@ -344,38 +484,6 @@ async function providerJsonRequest(params) {
344
484
  import bs586 from "bs58";
345
485
  import { getCompiledTransactionMessageDecoder } from "@solana/kit";
346
486
 
347
- // ../../src/config/constants.ts
348
- var PAYMENT_SCHEME = "subly-yield-exact";
349
- var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
350
- var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
351
- var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
352
- var envOr = (name, fallback) => {
353
- const value = process.env[name]?.trim();
354
- return value ? value : fallback;
355
- };
356
- var SUBLY_VAULT = {
357
- name: "Subly USDC Payment Vault Alpha",
358
- address: envOr(
359
- "SUBLY_VAULT_ADDRESS",
360
- "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr"
361
- ),
362
- programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
363
- usdcMint: envOr(
364
- "SUBLY_VAULT_USDC_MINT",
365
- "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
366
- ),
367
- shareMint: envOr(
368
- "SUBLY_VAULT_SHARE_MINT",
369
- "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a"
370
- ),
371
- lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
372
- farm: envOr(
373
- "SUBLY_VAULT_FARM",
374
- "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
375
- )
376
- };
377
- var USDC_DECIMALS = 6;
378
-
379
487
  // ../../src/domain/request-binding.ts
380
488
  function computeRequestBindingHash(fields) {
381
489
  return hashStableJson({
@@ -625,16 +733,16 @@ function validatePaymentIntentTransaction(params) {
625
733
  if (intent.network !== SOLANA_MAINNET_NETWORK) {
626
734
  reject("network_mismatch", "Unsupported network");
627
735
  }
628
- if (intent.vault !== SUBLY_VAULT.address) {
736
+ if (intent.vault !== policy.vault.address) {
629
737
  reject("vault_mismatch", "Unsupported vault");
630
738
  }
631
- if (intent.shareMint !== SUBLY_VAULT.shareMint) {
739
+ if (intent.shareMint !== policy.vault.shareMint) {
632
740
  reject("share_mint_mismatch", "Unsupported share mint");
633
741
  }
634
- if (intent.farm !== SUBLY_VAULT.farm) {
742
+ if (intent.farm !== policy.vault.farm) {
635
743
  reject("farm_mismatch", "Unsupported Kamino farm");
636
744
  }
637
- if (intent.asset !== SUBLY_VAULT.usdcMint) {
745
+ if (intent.asset !== policy.vault.usdcMint) {
638
746
  reject("asset_mismatch", "Only USDC payments are supported");
639
747
  }
640
748
  if (intent.memo !== intent.paymentId) {
@@ -750,7 +858,7 @@ function validateDepositIntentTransaction(params) {
750
858
  if (new Date(intent.expiresAt).getTime() <= now) {
751
859
  reject("expired", "Deposit intent has expired");
752
860
  }
753
- assertVaultIntentTargets(intent);
861
+ assertVaultIntentTargets(intent, policy.vault);
754
862
  const decoded = decodeIntentTransaction({
755
863
  serializedTransaction: params.serializedTransaction,
756
864
  ...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
@@ -773,6 +881,9 @@ function validateDepositIntentTransaction(params) {
773
881
  case MEMO_PROGRAM_ID:
774
882
  break;
775
883
  case KVAULT_PROGRAM_ID: {
884
+ if (sawDeposit) {
885
+ reject("duplicate_deposit", "A deposit intent authorizes exactly one KVault deposit");
886
+ }
776
887
  if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
777
888
  reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
778
889
  }
@@ -823,7 +934,7 @@ function validateWithdrawalIntentTransaction(params) {
823
934
  if (new Date(intent.expiresAt).getTime() <= now) {
824
935
  reject("expired", "Withdrawal intent has expired");
825
936
  }
826
- assertVaultIntentTargets(intent);
937
+ assertVaultIntentTargets(intent, policy.vault);
827
938
  const expectedDestination = deriveAssociatedTokenAddress({
828
939
  owner: intent.wallet,
829
940
  mint: intent.asset
@@ -921,22 +1032,23 @@ function validateWithdrawalIntentTransaction(params) {
921
1032
  );
922
1033
  }
923
1034
  }
924
- function assertVaultIntentTargets(intent) {
925
- if (intent.vault !== SUBLY_VAULT.address) {
1035
+ function assertVaultIntentTargets(intent, vault) {
1036
+ if (intent.vault !== vault.address) {
926
1037
  reject("vault_mismatch", "Unsupported vault");
927
1038
  }
928
- if (intent.shareMint !== SUBLY_VAULT.shareMint) {
1039
+ if (intent.shareMint !== vault.shareMint) {
929
1040
  reject("share_mint_mismatch", "Unsupported share mint");
930
1041
  }
931
- if (intent.farm !== SUBLY_VAULT.farm) {
1042
+ if (intent.farm !== vault.farm) {
932
1043
  reject("farm_mismatch", "Unsupported Kamino farm");
933
1044
  }
934
- if (intent.asset !== SUBLY_VAULT.usdcMint) {
1045
+ if (intent.asset !== vault.usdcMint) {
935
1046
  reject("asset_mismatch", "Only USDC is supported");
936
1047
  }
937
1048
  }
938
1049
  function resolveIntentValidationPolicy(policy) {
939
1050
  const resolved = {
1051
+ vault: policy?.vault ?? SUBLY_VAULT,
940
1052
  maxComputeUnitLimit: policy?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
941
1053
  maxComputeUnitPriceMicroLamports: policy?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
942
1054
  maxTemporaryAccountLamports: policy?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
@@ -1288,10 +1400,12 @@ function readShortVec(bytes, startOffset) {
1288
1400
 
1289
1401
  // ../../src/client/agent-wallet-signer.ts
1290
1402
  var IntentValidatingAgentWalletSigner = class {
1403
+ vault;
1291
1404
  validationMode = "structured_intent_transaction";
1292
1405
  validationPolicy;
1293
1406
  constructor(validationPolicy) {
1294
- this.validationPolicy = validationPolicy;
1407
+ this.vault = Object.freeze({ ...validationPolicy?.vault ?? SUBLY_VAULT });
1408
+ this.validationPolicy = { ...validationPolicy, vault: this.vault };
1295
1409
  }
1296
1410
  async signPayment(params) {
1297
1411
  this.assertIntentWallet(params.intent.wallet);
@@ -1449,15 +1563,15 @@ async function createCircleSignerTransport(config) {
1449
1563
  const entitySecretCiphertext = async () => {
1450
1564
  if (entityPublicKey === null) {
1451
1565
  const data = await request("GET", "/v1/w3s/config/entity/publicKey");
1452
- const publicKey = data.publicKey;
1453
- if (typeof publicKey !== "string") {
1566
+ const publicKey2 = data.publicKey;
1567
+ if (typeof publicKey2 !== "string") {
1454
1568
  throw new RemoteSigningError(
1455
1569
  PROVIDER,
1456
1570
  "entity public key response has no publicKey",
1457
1571
  data
1458
1572
  );
1459
1573
  }
1460
- entityPublicKey = createPublicKey(publicKey);
1574
+ entityPublicKey = createPublicKey(publicKey2);
1461
1575
  }
1462
1576
  return publicEncrypt(
1463
1577
  {
@@ -1729,6 +1843,45 @@ async function agentWalletSignerFromEnv(env = process.env) {
1729
1843
  );
1730
1844
  }
1731
1845
 
1846
+ // ../../src/client/withdrawal-preview.ts
1847
+ var ROUNDING_RAW_USDC = 10n;
1848
+ async function assertWithdrawalPreview(input) {
1849
+ const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
1850
+ const simulation = await input.rpc.simulateTransaction(
1851
+ input.serializedTransaction,
1852
+ {
1853
+ encoding: "base64",
1854
+ commitment: "confirmed",
1855
+ sigVerify: false,
1856
+ replaceRecentBlockhash: false,
1857
+ innerInstructions: true
1858
+ }
1859
+ ).send({ abortSignal: AbortSignal.timeout(15e3) });
1860
+ if (simulation.value.err !== null) {
1861
+ throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
1862
+ }
1863
+ let received = 0n;
1864
+ for (const group of simulation.value.innerInstructions ?? []) {
1865
+ for (const instruction of group.instructions) {
1866
+ if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
1867
+ const parsed = instruction.parsed;
1868
+ if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
1869
+ const info = parsed.info;
1870
+ if (!info || info.destination !== destination && info.source !== destination) continue;
1871
+ const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
1872
+ if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
1873
+ throw new Error("Withdrawal preview returned an invalid token amount");
1874
+ }
1875
+ const amount = BigInt(raw);
1876
+ if (info.destination === destination) received += amount;
1877
+ if (info.source === destination) received -= amount;
1878
+ }
1879
+ }
1880
+ if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
1881
+ throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
1882
+ }
1883
+ }
1884
+
1732
1885
  // ../../src/client/lookup-tables.ts
1733
1886
  import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1734
1887
  import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
@@ -1785,6 +1938,8 @@ var VaultFlowClientError = class extends Error {
1785
1938
  errorDetails;
1786
1939
  };
1787
1940
  var VaultFlowClient = class {
1941
+ vault;
1942
+ rpc;
1788
1943
  baseUrl;
1789
1944
  signer;
1790
1945
  fetchImpl;
@@ -1792,6 +1947,11 @@ var VaultFlowClient = class {
1792
1947
  pollTimeoutMs;
1793
1948
  pollIntervalMs;
1794
1949
  constructor(config) {
1950
+ this.rpc = config.rpc;
1951
+ this.vault = config.vault ?? config.signer.vault ?? SUBLY_VAULT;
1952
+ if (config.signer.vault && config.signer.vault.address !== this.vault.address) {
1953
+ throw new Error("Vault flow client and signer must select the same vault");
1954
+ }
1795
1955
  this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
1796
1956
  this.signer = config.signer;
1797
1957
  this.fetchImpl = config.fetchImpl ?? fetch;
@@ -1813,6 +1973,7 @@ var VaultFlowClient = class {
1813
1973
  try {
1814
1974
  prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1815
1975
  wallet: this.signer.walletAddress,
1976
+ vault: this.vault.address,
1816
1977
  amountRawUsdc: input.amountRawUsdc.toString(),
1817
1978
  ...approvalId2 === void 0 ? {} : { approvalId: approvalId2 }
1818
1979
  });
@@ -1826,10 +1987,14 @@ var VaultFlowClient = class {
1826
1987
  }
1827
1988
  prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1828
1989
  wallet: this.signer.walletAddress,
1990
+ vault: this.vault.address,
1829
1991
  amountRawUsdc: input.amountRawUsdc.toString(),
1830
1992
  approvalId: approvalId2
1831
1993
  });
1832
1994
  }
1995
+ if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.signingIntent.amountRawUsdc !== input.amountRawUsdc.toString()) {
1996
+ throw new VaultFlowClientError("prepare", "Prepared deposit differs from the requested wallet, vault or amount");
1997
+ }
1833
1998
  const signed = await this.signer.signDeposit({
1834
1999
  intent: prepared.signingIntent,
1835
2000
  serializedTransaction: prepared.serializedTransaction,
@@ -1867,12 +2032,23 @@ var VaultFlowClient = class {
1867
2032
  "/v1/withdrawals/prepare",
1868
2033
  {
1869
2034
  wallet: this.signer.walletAddress,
2035
+ vault: this.vault.address,
1870
2036
  amountRawUsdc: input.amountRawUsdc.toString(),
1871
2037
  ...input.purpose === void 0 ? {} : { purpose: input.purpose },
1872
2038
  ...input.payment === void 0 ? {} : { payment: input.payment },
1873
2039
  ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1874
2040
  }
1875
2041
  );
2042
+ if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.requestedWithdrawRawUsdc !== input.amountRawUsdc.toString() || prepared.purpose !== (input.purpose ?? "normal") || input.purpose === "yield_realize" && prepared.signingIntent.allowFullExit) {
2043
+ throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
2044
+ }
2045
+ await assertWithdrawalPreview({
2046
+ rpc: this.rpc,
2047
+ serializedTransaction: prepared.serializedTransaction,
2048
+ wallet: this.signer.walletAddress,
2049
+ vault: this.vault,
2050
+ amountRawUsdc: input.amountRawUsdc
2051
+ });
1876
2052
  const signed = await this.signer.signWithdrawal({
1877
2053
  intent: prepared.signingIntent,
1878
2054
  serializedTransaction: prepared.serializedTransaction,
@@ -1910,12 +2086,12 @@ var VaultFlowClient = class {
1910
2086
  await this.postJson(
1911
2087
  "sync",
1912
2088
  `/v1/wallets/${this.signer.walletAddress}/sync`,
1913
- { source: "chain" }
2089
+ { source: "chain", vault: this.vault.address }
1914
2090
  );
1915
2091
  } catch {
1916
2092
  }
1917
2093
  }
1918
- const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
2094
+ const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
1919
2095
  const response = await this.fetchImpl(url, {
1920
2096
  headers: await walletAuthHeaders({
1921
2097
  signer: this.signer,
@@ -1941,8 +2117,12 @@ var VaultFlowClient = class {
1941
2117
  );
1942
2118
  }
1943
2119
  const body = parsed;
2120
+ if (body.position?.vault !== void 0 && body.position.vault !== this.vault.address) {
2121
+ throw new VaultFlowClientError("budget", "Relayer returned the budget for a different vault");
2122
+ }
1944
2123
  return {
1945
2124
  wallet: this.signer.walletAddress,
2125
+ vault: this.vault.address,
1946
2126
  principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
1947
2127
  positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
1948
2128
  grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
@@ -1960,7 +2140,7 @@ var VaultFlowClient = class {
1960
2140
  /** Wallet's approvals as the relayer sees them (optionally by status). */
1961
2141
  async listApprovals(status) {
1962
2142
  const body = await this.getJson(
1963
- `/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" : `?status=${encodeURIComponent(status)}`}`
2143
+ `/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
1964
2144
  );
1965
2145
  return body.approvals ?? [];
1966
2146
  }
@@ -1969,16 +2149,21 @@ var VaultFlowClient = class {
1969
2149
  * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
1970
2150
  */
1971
2151
  async createSetupSession(input) {
1972
- return await this.postJson(
2152
+ const session = await this.postJson(
1973
2153
  "prepare",
1974
2154
  `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
1975
2155
  {
2156
+ vault: this.vault.address,
1976
2157
  ...input.policy === void 0 ? {} : { policy: input.policy },
1977
2158
  ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
1978
2159
  ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
1979
2160
  ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1980
2161
  }
1981
2162
  );
2163
+ if (session.vault !== this.vault.address || session.wallet !== this.signer.walletAddress) {
2164
+ throw new VaultFlowClientError("prepare", "Relayer returned a setup session for a different wallet or vault");
2165
+ }
2166
+ return session;
1982
2167
  }
1983
2168
  /** Polls a setup session (public capability URL — no auth needed). */
1984
2169
  async getSetupSession(sessionId) {