@subly_fi/pay 0.6.2 → 0.7.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.
@@ -26,6 +26,144 @@ function loadSecretKeyBytes(params) {
26
26
  throw new Error(`${label} keypair is not configured`);
27
27
  }
28
28
 
29
+ // ../../src/config/vault-catalog.ts
30
+ import { readFileSync as readFileSync2 } from "node:fs";
31
+ import { z } from "zod";
32
+
33
+ // ../../src/lib/solana-address.ts
34
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
35
+ var BASE58_LOOKUP = new Map(
36
+ [...BASE58_ALPHABET].map((character, index) => [character, BigInt(index)])
37
+ );
38
+ function assertSolanaAddress(value, fieldName) {
39
+ if (value.length < 32 || value.length > 44) {
40
+ throw new Error(`${fieldName} must be a valid Solana public key`);
41
+ }
42
+ if (decodeBase58(value).length !== 32) {
43
+ throw new Error(`${fieldName} must be a valid Solana public key`);
44
+ }
45
+ return value;
46
+ }
47
+ function decodeBase58(value) {
48
+ if (value.length === 0) {
49
+ return new Uint8Array();
50
+ }
51
+ let decoded = 0n;
52
+ for (const character of value) {
53
+ const digit = BASE58_LOOKUP.get(character);
54
+ if (digit === void 0) {
55
+ return new Uint8Array();
56
+ }
57
+ decoded = decoded * 58n + digit;
58
+ }
59
+ const bytes = [];
60
+ while (decoded > 0n) {
61
+ bytes.push(Number(decoded & 0xffn));
62
+ decoded >>= 8n;
63
+ }
64
+ for (const character of value) {
65
+ if (character !== "1") {
66
+ break;
67
+ }
68
+ bytes.push(0);
69
+ }
70
+ return Uint8Array.from(bytes.reverse());
71
+ }
72
+
73
+ // ../../src/config/vault.ts
74
+ var MAINNET_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
75
+ var KAMINO_VAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
76
+ var NO_VAULT_FARM = "11111111111111111111111111111111";
77
+ var DEFAULT_VAULT_CONFIG = Object.freeze({
78
+ address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
79
+ programId: KAMINO_VAULT_PROGRAM_ID,
80
+ usdcMint: MAINNET_USDC_MINT,
81
+ shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
82
+ farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
83
+ });
84
+ function vaultConfigFromEnv(env = process.env) {
85
+ const value = (name) => env[name]?.trim() || void 0;
86
+ const vaultAddress = value("SUBLY_VAULT_ADDRESS") ?? DEFAULT_VAULT_CONFIG.address;
87
+ const customVault = vaultAddress !== DEFAULT_VAULT_CONFIG.address;
88
+ const anchor = (name, fallback) => {
89
+ const configured = value(name);
90
+ if (customVault && configured === void 0) {
91
+ throw new Error(
92
+ `${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.`
93
+ );
94
+ }
95
+ return assertSolanaAddress(configured ?? fallback, name);
96
+ };
97
+ const usdcMint = value("SUBLY_VAULT_USDC_MINT") ?? MAINNET_USDC_MINT;
98
+ if (usdcMint !== MAINNET_USDC_MINT) {
99
+ throw new Error(
100
+ "SUBLY_VAULT_USDC_MINT must be mainnet USDC; other deposit assets are not supported"
101
+ );
102
+ }
103
+ return Object.freeze({
104
+ address: assertSolanaAddress(vaultAddress, "SUBLY_VAULT_ADDRESS"),
105
+ programId: KAMINO_VAULT_PROGRAM_ID,
106
+ usdcMint,
107
+ shareMint: anchor("SUBLY_VAULT_SHARE_MINT", DEFAULT_VAULT_CONFIG.shareMint),
108
+ farm: anchor("SUBLY_VAULT_FARM", DEFAULT_VAULT_CONFIG.farm)
109
+ });
110
+ }
111
+
112
+ // ../../src/config/vault-catalog.ts
113
+ var publicKey = z.string().refine((value) => {
114
+ try {
115
+ assertSolanaAddress(value, "vault catalog address");
116
+ return true;
117
+ } catch {
118
+ return false;
119
+ }
120
+ }, "Invalid Solana public key");
121
+ var catalogSchema = z.object({
122
+ version: z.literal(1),
123
+ defaultVault: publicKey,
124
+ vaults: z.array(z.object({
125
+ address: publicKey,
126
+ programId: z.literal(KAMINO_VAULT_PROGRAM_ID),
127
+ usdcMint: z.literal(MAINNET_USDC_MINT),
128
+ shareMint: publicKey,
129
+ farm: publicKey,
130
+ name: z.string().min(1).max(128).optional(),
131
+ depositsEnabled: z.boolean().optional(),
132
+ extraLookupTables: z.array(publicKey).max(16).optional()
133
+ }).strict()).min(1).max(100)
134
+ }).strict();
135
+ function parseVaultCatalog(value) {
136
+ const catalog = catalogSchema.parse(value);
137
+ const addresses = new Set(catalog.vaults.map((vault) => vault.address));
138
+ if (addresses.size !== catalog.vaults.length) throw new Error("Duplicate vault in catalog");
139
+ if (!addresses.has(catalog.defaultVault)) throw new Error("defaultVault must be in the vault catalog");
140
+ return { ...catalog, vaults: catalog.vaults.map((vault) => Object.freeze(vault)) };
141
+ }
142
+ function vaultCatalogFromEnv(env = process.env) {
143
+ const path = env.SUBLY_VAULTS_FILE?.trim();
144
+ if (!path) {
145
+ const vault = vaultConfigFromEnv(env);
146
+ return { version: 1, defaultVault: vault.address, vaults: [vault] };
147
+ }
148
+ const catalog = parseVaultCatalog(JSON.parse(readFileSync2(path, "utf8")));
149
+ const selected = env.SUBLY_VAULT_ADDRESS?.trim() || catalog.defaultVault;
150
+ if (!catalog.vaults.some((vault) => vault.address === selected)) {
151
+ throw new Error("SUBLY_VAULT_ADDRESS must be in SUBLY_VAULTS_FILE");
152
+ }
153
+ return { ...catalog, defaultVault: selected };
154
+ }
155
+ function defaultCatalogVault(catalog) {
156
+ return catalog.vaults.find((vault) => vault.address === catalog.defaultVault);
157
+ }
158
+
159
+ // ../../src/config/constants.ts
160
+ var PAYMENT_SCHEME = "subly-yield-exact";
161
+ var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
162
+ var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
163
+ var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
164
+ var SUBLY_VAULT = defaultCatalogVault(vaultCatalogFromEnv());
165
+ var USDC_DECIMALS = 6;
166
+
29
167
  // ../../src/client/agent-wallet-signer.ts
30
168
  import { signBytes } from "@solana/kit";
31
169
  import bs586 from "bs58";
@@ -141,10 +279,10 @@ function ed25519PublicKeyBytes(provider, walletAddress) {
141
279
  return bytes;
142
280
  }
143
281
  function verifiedEd25519Signature(params) {
144
- const publicKey = ed25519PublicKeyBytes(params.provider, params.walletAddress);
282
+ const publicKey2 = ed25519PublicKeyBytes(params.provider, params.walletAddress);
145
283
  const encoded = params.encodedSignature.trim();
146
284
  for (const candidate of decodeSignatureCandidates(encoded)) {
147
- if (nacl.sign.detached.verify(params.message, candidate, publicKey)) {
285
+ if (nacl.sign.detached.verify(params.message, candidate, publicKey2)) {
148
286
  return candidate;
149
287
  }
150
288
  }
@@ -198,8 +336,8 @@ async function requestVerifiedTransactionSignature(params) {
198
336
  `signed transaction is missing the signature for ${transport.walletAddress}`
199
337
  );
200
338
  }
201
- const publicKey = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
202
- if (!nacl.sign.detached.verify(params.messageBytes, signature, publicKey)) {
339
+ const publicKey2 = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
340
+ if (!nacl.sign.detached.verify(params.messageBytes, signature, publicKey2)) {
203
341
  throw new RemoteSigningError(
204
342
  transport.provider,
205
343
  "returned signature does not verify over the requested transaction"
@@ -250,38 +388,6 @@ async function providerJsonRequest(params) {
250
388
  import bs585 from "bs58";
251
389
  import { getCompiledTransactionMessageDecoder } from "@solana/kit";
252
390
 
253
- // ../../src/config/constants.ts
254
- var PAYMENT_SCHEME = "subly-yield-exact";
255
- var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
256
- var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
257
- var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
258
- var envOr = (name, fallback) => {
259
- const value = process.env[name]?.trim();
260
- return value ? value : fallback;
261
- };
262
- var SUBLY_VAULT = {
263
- name: "Subly USDC Payment Vault Alpha",
264
- address: envOr(
265
- "SUBLY_VAULT_ADDRESS",
266
- "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr"
267
- ),
268
- programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
269
- usdcMint: envOr(
270
- "SUBLY_VAULT_USDC_MINT",
271
- "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
272
- ),
273
- shareMint: envOr(
274
- "SUBLY_VAULT_SHARE_MINT",
275
- "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a"
276
- ),
277
- lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
278
- farm: envOr(
279
- "SUBLY_VAULT_FARM",
280
- "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
281
- )
282
- };
283
- var USDC_DECIMALS = 6;
284
-
285
391
  // ../../src/domain/request-binding.ts
286
392
  function computeRequestBindingHash(fields) {
287
393
  return hashStableJson({
@@ -531,16 +637,16 @@ function validatePaymentIntentTransaction(params) {
531
637
  if (intent.network !== SOLANA_MAINNET_NETWORK) {
532
638
  reject("network_mismatch", "Unsupported network");
533
639
  }
534
- if (intent.vault !== SUBLY_VAULT.address) {
640
+ if (intent.vault !== policy2.vault.address) {
535
641
  reject("vault_mismatch", "Unsupported vault");
536
642
  }
537
- if (intent.shareMint !== SUBLY_VAULT.shareMint) {
643
+ if (intent.shareMint !== policy2.vault.shareMint) {
538
644
  reject("share_mint_mismatch", "Unsupported share mint");
539
645
  }
540
- if (intent.farm !== SUBLY_VAULT.farm) {
646
+ if (intent.farm !== policy2.vault.farm) {
541
647
  reject("farm_mismatch", "Unsupported Kamino farm");
542
648
  }
543
- if (intent.asset !== SUBLY_VAULT.usdcMint) {
649
+ if (intent.asset !== policy2.vault.usdcMint) {
544
650
  reject("asset_mismatch", "Only USDC payments are supported");
545
651
  }
546
652
  if (intent.memo !== intent.paymentId) {
@@ -656,7 +762,7 @@ function validateDepositIntentTransaction(params) {
656
762
  if (new Date(intent.expiresAt).getTime() <= now) {
657
763
  reject("expired", "Deposit intent has expired");
658
764
  }
659
- assertVaultIntentTargets(intent);
765
+ assertVaultIntentTargets(intent, policy2.vault);
660
766
  const decoded = decodeIntentTransaction({
661
767
  serializedTransaction: params.serializedTransaction,
662
768
  ...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
@@ -679,6 +785,9 @@ function validateDepositIntentTransaction(params) {
679
785
  case MEMO_PROGRAM_ID:
680
786
  break;
681
787
  case KVAULT_PROGRAM_ID: {
788
+ if (sawDeposit) {
789
+ reject("duplicate_deposit", "A deposit intent authorizes exactly one KVault deposit");
790
+ }
682
791
  if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
683
792
  reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
684
793
  }
@@ -729,7 +838,7 @@ function validateWithdrawalIntentTransaction(params) {
729
838
  if (new Date(intent.expiresAt).getTime() <= now) {
730
839
  reject("expired", "Withdrawal intent has expired");
731
840
  }
732
- assertVaultIntentTargets(intent);
841
+ assertVaultIntentTargets(intent, policy2.vault);
733
842
  const expectedDestination = deriveAssociatedTokenAddress({
734
843
  owner: intent.wallet,
735
844
  mint: intent.asset
@@ -827,22 +936,23 @@ function validateWithdrawalIntentTransaction(params) {
827
936
  );
828
937
  }
829
938
  }
830
- function assertVaultIntentTargets(intent) {
831
- if (intent.vault !== SUBLY_VAULT.address) {
939
+ function assertVaultIntentTargets(intent, vault) {
940
+ if (intent.vault !== vault.address) {
832
941
  reject("vault_mismatch", "Unsupported vault");
833
942
  }
834
- if (intent.shareMint !== SUBLY_VAULT.shareMint) {
943
+ if (intent.shareMint !== vault.shareMint) {
835
944
  reject("share_mint_mismatch", "Unsupported share mint");
836
945
  }
837
- if (intent.farm !== SUBLY_VAULT.farm) {
946
+ if (intent.farm !== vault.farm) {
838
947
  reject("farm_mismatch", "Unsupported Kamino farm");
839
948
  }
840
- if (intent.asset !== SUBLY_VAULT.usdcMint) {
949
+ if (intent.asset !== vault.usdcMint) {
841
950
  reject("asset_mismatch", "Only USDC is supported");
842
951
  }
843
952
  }
844
953
  function resolveIntentValidationPolicy(policy2) {
845
954
  const resolved = {
955
+ vault: policy2?.vault ?? SUBLY_VAULT,
846
956
  maxComputeUnitLimit: policy2?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
847
957
  maxComputeUnitPriceMicroLamports: policy2?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
848
958
  maxTemporaryAccountLamports: policy2?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
@@ -1194,10 +1304,12 @@ function readShortVec(bytes, startOffset) {
1194
1304
 
1195
1305
  // ../../src/client/agent-wallet-signer.ts
1196
1306
  var IntentValidatingAgentWalletSigner = class {
1307
+ vault;
1197
1308
  validationMode = "structured_intent_transaction";
1198
1309
  validationPolicy;
1199
1310
  constructor(validationPolicy) {
1200
- this.validationPolicy = validationPolicy;
1311
+ this.vault = Object.freeze({ ...validationPolicy?.vault ?? SUBLY_VAULT });
1312
+ this.validationPolicy = { ...validationPolicy, vault: this.vault };
1201
1313
  }
1202
1314
  async signPayment(params) {
1203
1315
  this.assertIntentWallet(params.intent.wallet);
@@ -1355,15 +1467,15 @@ async function createCircleSignerTransport(config) {
1355
1467
  const entitySecretCiphertext = async () => {
1356
1468
  if (entityPublicKey === null) {
1357
1469
  const data = await request("GET", "/v1/w3s/config/entity/publicKey");
1358
- const publicKey = data.publicKey;
1359
- if (typeof publicKey !== "string") {
1470
+ const publicKey2 = data.publicKey;
1471
+ if (typeof publicKey2 !== "string") {
1360
1472
  throw new RemoteSigningError(
1361
1473
  PROVIDER,
1362
1474
  "entity public key response has no publicKey",
1363
1475
  data
1364
1476
  );
1365
1477
  }
1366
- entityPublicKey = createPublicKey(publicKey);
1478
+ entityPublicKey = createPublicKey(publicKey2);
1367
1479
  }
1368
1480
  return publicEncrypt(
1369
1481
  {
@@ -1635,6 +1747,45 @@ async function agentWalletSignerFromEnv(env = process.env) {
1635
1747
  );
1636
1748
  }
1637
1749
 
1750
+ // ../../src/client/withdrawal-preview.ts
1751
+ var ROUNDING_RAW_USDC = 10n;
1752
+ async function assertWithdrawalPreview(input) {
1753
+ const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
1754
+ const simulation = await input.rpc.simulateTransaction(
1755
+ input.serializedTransaction,
1756
+ {
1757
+ encoding: "base64",
1758
+ commitment: "confirmed",
1759
+ sigVerify: false,
1760
+ replaceRecentBlockhash: false,
1761
+ innerInstructions: true
1762
+ }
1763
+ ).send({ abortSignal: AbortSignal.timeout(15e3) });
1764
+ if (simulation.value.err !== null) {
1765
+ throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
1766
+ }
1767
+ let received = 0n;
1768
+ for (const group of simulation.value.innerInstructions ?? []) {
1769
+ for (const instruction of group.instructions) {
1770
+ if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
1771
+ const parsed = instruction.parsed;
1772
+ if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
1773
+ const info = parsed.info;
1774
+ if (!info || info.destination !== destination && info.source !== destination) continue;
1775
+ const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
1776
+ if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
1777
+ throw new Error("Withdrawal preview returned an invalid token amount");
1778
+ }
1779
+ const amount = BigInt(raw);
1780
+ if (info.destination === destination) received += amount;
1781
+ if (info.source === destination) received -= amount;
1782
+ }
1783
+ }
1784
+ if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
1785
+ throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
1786
+ }
1787
+ }
1788
+
1638
1789
  // ../../src/client/lookup-tables.ts
1639
1790
  import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1640
1791
  import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
@@ -1728,6 +1879,8 @@ var VaultFlowClientError = class extends Error {
1728
1879
  errorDetails;
1729
1880
  };
1730
1881
  var VaultFlowClient = class {
1882
+ vault;
1883
+ rpc;
1731
1884
  baseUrl;
1732
1885
  signer;
1733
1886
  fetchImpl;
@@ -1735,6 +1888,11 @@ var VaultFlowClient = class {
1735
1888
  pollTimeoutMs;
1736
1889
  pollIntervalMs;
1737
1890
  constructor(config) {
1891
+ this.rpc = config.rpc;
1892
+ this.vault = config.vault ?? config.signer.vault ?? SUBLY_VAULT;
1893
+ if (config.signer.vault && config.signer.vault.address !== this.vault.address) {
1894
+ throw new Error("Vault flow client and signer must select the same vault");
1895
+ }
1738
1896
  this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
1739
1897
  this.signer = config.signer;
1740
1898
  this.fetchImpl = config.fetchImpl ?? fetch;
@@ -1756,6 +1914,7 @@ var VaultFlowClient = class {
1756
1914
  try {
1757
1915
  prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1758
1916
  wallet: this.signer.walletAddress,
1917
+ vault: this.vault.address,
1759
1918
  amountRawUsdc: input.amountRawUsdc.toString(),
1760
1919
  ...approvalId === void 0 ? {} : { approvalId }
1761
1920
  });
@@ -1769,10 +1928,14 @@ var VaultFlowClient = class {
1769
1928
  }
1770
1929
  prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1771
1930
  wallet: this.signer.walletAddress,
1931
+ vault: this.vault.address,
1772
1932
  amountRawUsdc: input.amountRawUsdc.toString(),
1773
1933
  approvalId
1774
1934
  });
1775
1935
  }
1936
+ if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.signingIntent.amountRawUsdc !== input.amountRawUsdc.toString()) {
1937
+ throw new VaultFlowClientError("prepare", "Prepared deposit differs from the requested wallet, vault or amount");
1938
+ }
1776
1939
  const signed = await this.signer.signDeposit({
1777
1940
  intent: prepared.signingIntent,
1778
1941
  serializedTransaction: prepared.serializedTransaction,
@@ -1810,12 +1973,23 @@ var VaultFlowClient = class {
1810
1973
  "/v1/withdrawals/prepare",
1811
1974
  {
1812
1975
  wallet: this.signer.walletAddress,
1976
+ vault: this.vault.address,
1813
1977
  amountRawUsdc: input.amountRawUsdc.toString(),
1814
1978
  ...input.purpose === void 0 ? {} : { purpose: input.purpose },
1815
1979
  ...input.payment === void 0 ? {} : { payment: input.payment },
1816
1980
  ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1817
1981
  }
1818
1982
  );
1983
+ 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) {
1984
+ throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
1985
+ }
1986
+ await assertWithdrawalPreview({
1987
+ rpc: this.rpc,
1988
+ serializedTransaction: prepared.serializedTransaction,
1989
+ wallet: this.signer.walletAddress,
1990
+ vault: this.vault,
1991
+ amountRawUsdc: input.amountRawUsdc
1992
+ });
1819
1993
  const signed = await this.signer.signWithdrawal({
1820
1994
  intent: prepared.signingIntent,
1821
1995
  serializedTransaction: prepared.serializedTransaction,
@@ -1853,12 +2027,12 @@ var VaultFlowClient = class {
1853
2027
  await this.postJson(
1854
2028
  "sync",
1855
2029
  `/v1/wallets/${this.signer.walletAddress}/sync`,
1856
- { source: "chain" }
2030
+ { source: "chain", vault: this.vault.address }
1857
2031
  );
1858
2032
  } catch {
1859
2033
  }
1860
2034
  }
1861
- const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
2035
+ const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
1862
2036
  const response = await this.fetchImpl(url, {
1863
2037
  headers: await walletAuthHeaders({
1864
2038
  signer: this.signer,
@@ -1884,8 +2058,12 @@ var VaultFlowClient = class {
1884
2058
  );
1885
2059
  }
1886
2060
  const body = parsed;
2061
+ if (body.position?.vault !== void 0 && body.position.vault !== this.vault.address) {
2062
+ throw new VaultFlowClientError("budget", "Relayer returned the budget for a different vault");
2063
+ }
1887
2064
  return {
1888
2065
  wallet: this.signer.walletAddress,
2066
+ vault: this.vault.address,
1889
2067
  principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
1890
2068
  positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
1891
2069
  grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
@@ -1903,7 +2081,7 @@ var VaultFlowClient = class {
1903
2081
  /** Wallet's approvals as the relayer sees them (optionally by status). */
1904
2082
  async listApprovals(status) {
1905
2083
  const body = await this.getJson(
1906
- `/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" : `?status=${encodeURIComponent(status)}`}`
2084
+ `/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
1907
2085
  );
1908
2086
  return body.approvals ?? [];
1909
2087
  }
@@ -1912,16 +2090,21 @@ var VaultFlowClient = class {
1912
2090
  * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
1913
2091
  */
1914
2092
  async createSetupSession(input) {
1915
- return await this.postJson(
2093
+ const session = await this.postJson(
1916
2094
  "prepare",
1917
2095
  `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
1918
2096
  {
2097
+ vault: this.vault.address,
1919
2098
  ...input.policy === void 0 ? {} : { policy: input.policy },
1920
2099
  ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
1921
2100
  ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
1922
2101
  ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1923
2102
  }
1924
2103
  );
2104
+ if (session.vault !== this.vault.address || session.wallet !== this.signer.walletAddress) {
2105
+ throw new VaultFlowClientError("prepare", "Relayer returned a setup session for a different wallet or vault");
2106
+ }
2107
+ return session;
1925
2108
  }
1926
2109
  /** Polls a setup session (public capability URL — no auth needed). */
1927
2110
  async getSetupSession(sessionId) {
package/dist/vaults.js ADDED
@@ -0,0 +1,129 @@
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
+
128
+ // src/vaults.ts
129
+ console.log(JSON.stringify(vaultCatalogFromEnv(), null, 2));