@subly_fi/pay 0.6.1 → 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/withdraw.js CHANGED
@@ -1,9 +1,176 @@
1
+ // ../../src/client/signer-env.ts
2
+ import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
3
+
4
+ // ../../src/solana/keys.ts
5
+ import { readFileSync } from "node:fs";
6
+ import bs58 from "bs58";
7
+ import {
8
+ createKeyPairSignerFromBytes
9
+ } from "@solana/kit";
10
+ function loadSecretKeyBytes(params) {
11
+ const { base58Secret, jsonFilePath, label } = params;
12
+ if (base58Secret !== void 0 && base58Secret.length > 0) {
13
+ const bytes = bs58.decode(base58Secret);
14
+ if (bytes.length !== 64) {
15
+ throw new Error(`${label} base58 secret must decode to 64 bytes`);
16
+ }
17
+ return bytes;
18
+ }
19
+ if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
20
+ const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
21
+ if (!Array.isArray(raw) || raw.length !== 64) {
22
+ throw new Error(`${label} keypair file must be a 64-byte JSON array`);
23
+ }
24
+ return Uint8Array.from(raw);
25
+ }
26
+ throw new Error(`${label} keypair is not configured`);
27
+ }
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
+
1
167
  // ../../src/client/agent-wallet-signer.ts
2
168
  import { signBytes } from "@solana/kit";
3
- import bs584 from "bs58";
169
+ import bs586 from "bs58";
170
+ import nacl2 from "tweetnacl";
4
171
 
5
172
  // ../../src/solana/tx.ts
6
- import bs58 from "bs58";
173
+ import bs582 from "bs58";
7
174
  import {
8
175
  appendTransactionMessageInstructions,
9
176
  compileTransaction,
@@ -46,6 +213,24 @@ function hashStableJson(value) {
46
213
  function decodeSerializedTransaction(serializedBase64) {
47
214
  return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
48
215
  }
216
+ function attachExternalSignatureToTransaction(params) {
217
+ if (!(params.signer in params.transaction.signatures)) {
218
+ throw new Error(
219
+ `transaction does not expect a signature from ${params.signer}`
220
+ );
221
+ }
222
+ const transaction = Object.freeze({
223
+ ...params.transaction,
224
+ signatures: Object.freeze({
225
+ ...params.transaction.signatures,
226
+ [params.signer]: params.signature
227
+ })
228
+ });
229
+ return {
230
+ serializedBase64: getBase64EncodedWireTransaction(transaction),
231
+ transaction
232
+ };
233
+ }
49
234
  async function addSignaturesToSerializedTransaction(params) {
50
235
  const decoded = decodeSerializedTransaction(params.serializedBase64);
51
236
  const signed = await partiallySignTransaction(params.signers, decoded);
@@ -59,28 +244,149 @@ function signatureBase58ForSigner(transaction, signer2) {
59
244
  if (signature === null || signature === void 0) {
60
245
  return null;
61
246
  }
62
- return bs58.encode(signature);
247
+ return bs582.encode(signature);
63
248
  }
64
249
 
65
- // ../../src/client/transaction-intent-validator.ts
250
+ // ../../src/client/remote-signer-transport.ts
66
251
  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"
252
+ import nacl from "tweetnacl";
253
+ var RemoteSigningError = class extends Error {
254
+ constructor(provider, message, detail = null) {
255
+ super(`[${provider}] ${message}`);
256
+ this.provider = provider;
257
+ this.detail = detail;
258
+ this.name = "RemoteSigningError";
259
+ }
260
+ provider;
261
+ detail;
82
262
  };
83
- var USDC_DECIMALS = 6;
263
+ function ed25519PublicKeyBytes(provider, walletAddress) {
264
+ let bytes;
265
+ try {
266
+ bytes = bs583.decode(walletAddress);
267
+ } catch {
268
+ throw new RemoteSigningError(
269
+ provider,
270
+ `wallet address ${walletAddress} is not base58`
271
+ );
272
+ }
273
+ if (bytes.length !== 32) {
274
+ throw new RemoteSigningError(
275
+ provider,
276
+ `wallet address ${walletAddress} is not a 32-byte ed25519 key`
277
+ );
278
+ }
279
+ return bytes;
280
+ }
281
+ function verifiedEd25519Signature(params) {
282
+ const publicKey2 = ed25519PublicKeyBytes(params.provider, params.walletAddress);
283
+ const encoded = params.encodedSignature.trim();
284
+ for (const candidate of decodeSignatureCandidates(encoded)) {
285
+ if (nacl.sign.detached.verify(params.message, candidate, publicKey2)) {
286
+ return candidate;
287
+ }
288
+ }
289
+ throw new RemoteSigningError(
290
+ params.provider,
291
+ `signature did not verify for wallet ${params.walletAddress}`
292
+ );
293
+ }
294
+ function decodeSignatureCandidates(encoded) {
295
+ const candidates = [];
296
+ const hex = encoded.startsWith("0x") ? encoded.slice(2) : encoded;
297
+ if (/^[0-9a-fA-F]{128}$/.test(hex)) {
298
+ candidates.push(Uint8Array.from(Buffer.from(hex, "hex")));
299
+ }
300
+ try {
301
+ const fromBase58 = bs583.decode(encoded);
302
+ if (fromBase58.length === 64) {
303
+ candidates.push(fromBase58);
304
+ }
305
+ } catch {
306
+ }
307
+ if (/^[A-Za-z0-9+/=_-]+$/.test(encoded)) {
308
+ const fromBase64 = Uint8Array.from(
309
+ Buffer.from(encoded.replace(/-/g, "+").replace(/_/g, "/"), "base64")
310
+ );
311
+ if (fromBase64.length === 64) {
312
+ candidates.push(fromBase64);
313
+ }
314
+ }
315
+ return candidates;
316
+ }
317
+ async function requestVerifiedTransactionSignature(params) {
318
+ const { transport } = params;
319
+ const signedBase64 = await transport.signTransaction(
320
+ params.serializedTransactionBase64
321
+ );
322
+ let returned;
323
+ try {
324
+ returned = decodeSerializedTransaction(signedBase64);
325
+ } catch (error) {
326
+ throw new RemoteSigningError(
327
+ transport.provider,
328
+ "provider returned an undecodable signed transaction",
329
+ error
330
+ );
331
+ }
332
+ const signature = returned.signatures[transport.walletAddress] ?? null;
333
+ if (signature === null) {
334
+ throw new RemoteSigningError(
335
+ transport.provider,
336
+ `signed transaction is missing the signature for ${transport.walletAddress}`
337
+ );
338
+ }
339
+ const publicKey2 = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
340
+ if (!nacl.sign.detached.verify(params.messageBytes, signature, publicKey2)) {
341
+ throw new RemoteSigningError(
342
+ transport.provider,
343
+ "returned signature does not verify over the requested transaction"
344
+ );
345
+ }
346
+ return signature;
347
+ }
348
+ async function externallySignedAgentTransaction(params) {
349
+ const original = decodeSerializedTransaction(params.serializedTransaction);
350
+ const signature = await requestVerifiedTransactionSignature({
351
+ transport: params.transport,
352
+ serializedTransactionBase64: params.serializedTransaction,
353
+ messageBytes: original.messageBytes
354
+ });
355
+ const attached = attachExternalSignatureToTransaction({
356
+ transaction: original,
357
+ signer: params.transport.walletAddress,
358
+ signature
359
+ });
360
+ return {
361
+ serializedTransaction: attached.serializedBase64,
362
+ agentSignature: bs583.encode(signature)
363
+ };
364
+ }
365
+ async function providerJsonRequest(params) {
366
+ const response = await params.fetchImpl(`${params.baseUrl}${params.path}`, {
367
+ method: params.method,
368
+ headers: { ...params.headers, "content-type": "application/json" },
369
+ ...params.body === void 0 ? {} : { body: JSON.stringify(params.body) }
370
+ });
371
+ let json = null;
372
+ try {
373
+ json = await response.json();
374
+ } catch {
375
+ json = null;
376
+ }
377
+ if (!response.ok) {
378
+ throw new RemoteSigningError(
379
+ params.provider,
380
+ `${params.method} ${params.path} failed with ${response.status}`,
381
+ json
382
+ );
383
+ }
384
+ return json;
385
+ }
386
+
387
+ // ../../src/client/transaction-intent-validator.ts
388
+ import bs585 from "bs58";
389
+ import { getCompiledTransactionMessageDecoder } from "@solana/kit";
84
390
 
85
391
  // ../../src/domain/request-binding.ts
86
392
  function computeRequestBindingHash(fields) {
@@ -99,7 +405,7 @@ function computeRequestBindingHash(fields) {
99
405
 
100
406
  // ../../src/lib/associated-token-account.ts
101
407
  import { createHash as createHash2 } from "node:crypto";
102
- import bs582 from "bs58";
408
+ import bs584 from "bs58";
103
409
  var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
104
410
  var ED25519_P = (1n << 255n) - 19n;
105
411
  var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
@@ -120,7 +426,7 @@ function deriveAssociatedTokenAddress(params) {
120
426
  associatedTokenProgramId
121
427
  );
122
428
  if (address2 !== null) {
123
- return bs582.encode(address2);
429
+ return bs584.encode(address2);
124
430
  }
125
431
  }
126
432
  throw new Error("Unable to derive associated token account address");
@@ -136,7 +442,7 @@ function createProgramAddress(seeds, programId) {
136
442
  return isEd25519Point(digest) ? null : new Uint8Array(digest);
137
443
  }
138
444
  function decodePublicKey(value, fieldName) {
139
- const decoded = bs582.decode(value);
445
+ const decoded = bs584.decode(value);
140
446
  if (decoded.length !== 32) {
141
447
  throw new Error(`${fieldName} must be a 32-byte public key`);
142
448
  }
@@ -331,13 +637,16 @@ function validatePaymentIntentTransaction(params) {
331
637
  if (intent.network !== SOLANA_MAINNET_NETWORK) {
332
638
  reject("network_mismatch", "Unsupported network");
333
639
  }
334
- if (intent.vault !== SUBLY_VAULT.address) {
640
+ if (intent.vault !== policy.vault.address) {
335
641
  reject("vault_mismatch", "Unsupported vault");
336
642
  }
337
- if (intent.shareMint !== SUBLY_VAULT.shareMint) {
643
+ if (intent.shareMint !== policy.vault.shareMint) {
338
644
  reject("share_mint_mismatch", "Unsupported share mint");
339
645
  }
340
- if (intent.asset !== SUBLY_VAULT.usdcMint) {
646
+ if (intent.farm !== policy.vault.farm) {
647
+ reject("farm_mismatch", "Unsupported Kamino farm");
648
+ }
649
+ if (intent.asset !== policy.vault.usdcMint) {
341
650
  reject("asset_mismatch", "Only USDC payments are supported");
342
651
  }
343
652
  if (intent.memo !== intent.paymentId) {
@@ -405,7 +714,7 @@ function validatePaymentIntentTransaction(params) {
405
714
  expectComputeBudgetPair(ixs, policy);
406
715
  expectCreateTemporaryAccount(ixs, intent, policy);
407
716
  expectInitializeTemporaryAccount(ixs, intent);
408
- consumeFarmInstructions(ixs, intent.wallet);
717
+ consumeFarmInstructions(ixs, intent);
409
718
  expectKvaultWithdraw(ixs, {
410
719
  wallet: intent.wallet,
411
720
  vault: intent.vault,
@@ -453,7 +762,7 @@ function validateDepositIntentTransaction(params) {
453
762
  if (new Date(intent.expiresAt).getTime() <= now) {
454
763
  reject("expired", "Deposit intent has expired");
455
764
  }
456
- assertVaultIntentTargets(intent);
765
+ assertVaultIntentTargets(intent, policy.vault);
457
766
  const decoded = decodeIntentTransaction({
458
767
  serializedTransaction: params.serializedTransaction,
459
768
  ...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
@@ -476,6 +785,9 @@ function validateDepositIntentTransaction(params) {
476
785
  case MEMO_PROGRAM_ID:
477
786
  break;
478
787
  case KVAULT_PROGRAM_ID: {
788
+ if (sawDeposit) {
789
+ reject("duplicate_deposit", "A deposit intent authorizes exactly one KVault deposit");
790
+ }
479
791
  if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
480
792
  reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
481
793
  }
@@ -526,7 +838,7 @@ function validateWithdrawalIntentTransaction(params) {
526
838
  if (new Date(intent.expiresAt).getTime() <= now) {
527
839
  reject("expired", "Withdrawal intent has expired");
528
840
  }
529
- assertVaultIntentTargets(intent);
841
+ assertVaultIntentTargets(intent, policy.vault);
530
842
  const expectedDestination = deriveAssociatedTokenAddress({
531
843
  owner: intent.wallet,
532
844
  mint: intent.asset
@@ -548,13 +860,27 @@ function validateWithdrawalIntentTransaction(params) {
548
860
  reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
549
861
  }
550
862
  let sawWithdraw = false;
863
+ let farmUserState = null;
864
+ let farmInstructionCount = 0;
551
865
  for (const ix of decoded.instructions) {
552
866
  switch (ix.programAddress) {
553
867
  case COMPUTE_BUDGET_PROGRAM_ID:
554
868
  validateComputeBudgetInstruction(ix, policy);
555
869
  break;
556
870
  case MEMO_PROGRAM_ID:
871
+ break;
557
872
  case KAMINO_FARMS_PROGRAM_ID:
873
+ farmInstructionCount += 1;
874
+ if (farmInstructionCount === 1) {
875
+ farmUserState = validateFarmUnstakeInstruction(ix, intent);
876
+ } else if (farmInstructionCount === 2) {
877
+ validateFarmWithdrawInstruction(ix, intent, farmUserState);
878
+ } else {
879
+ reject(
880
+ "farm_instruction_mismatch",
881
+ "Withdrawal may contain only one farm unstake and one farm withdrawal"
882
+ );
883
+ }
558
884
  break;
559
885
  case ASSOCIATED_TOKEN_PROGRAM_ID2:
560
886
  expectAtaCreateForOwner(ix, intent.wallet);
@@ -575,6 +901,12 @@ function validateWithdrawalIntentTransaction(params) {
575
901
  break;
576
902
  }
577
903
  case KVAULT_PROGRAM_ID: {
904
+ if (sawWithdraw) {
905
+ reject(
906
+ "withdraw_mismatch",
907
+ "Withdrawal may contain only one KVault withdraw instruction"
908
+ );
909
+ }
578
910
  validateKvaultWithdrawInstruction(ix, {
579
911
  wallet: intent.wallet,
580
912
  vault: intent.vault,
@@ -597,20 +929,30 @@ function validateWithdrawalIntentTransaction(params) {
597
929
  if (!sawWithdraw) {
598
930
  reject("missing_instruction", "Withdrawal transaction has no KVault withdraw");
599
931
  }
932
+ if (farmInstructionCount === 1) {
933
+ reject(
934
+ "farm_instruction_mismatch",
935
+ "A farm unstake must be followed by a farm withdrawal"
936
+ );
937
+ }
600
938
  }
601
- function assertVaultIntentTargets(intent) {
602
- if (intent.vault !== SUBLY_VAULT.address) {
939
+ function assertVaultIntentTargets(intent, vault) {
940
+ if (intent.vault !== vault.address) {
603
941
  reject("vault_mismatch", "Unsupported vault");
604
942
  }
605
- if (intent.shareMint !== SUBLY_VAULT.shareMint) {
943
+ if (intent.shareMint !== vault.shareMint) {
606
944
  reject("share_mint_mismatch", "Unsupported share mint");
607
945
  }
608
- if (intent.asset !== SUBLY_VAULT.usdcMint) {
946
+ if (intent.farm !== vault.farm) {
947
+ reject("farm_mismatch", "Unsupported Kamino farm");
948
+ }
949
+ if (intent.asset !== vault.usdcMint) {
609
950
  reject("asset_mismatch", "Only USDC is supported");
610
951
  }
611
952
  }
612
953
  function resolveIntentValidationPolicy(policy) {
613
954
  const resolved = {
955
+ vault: policy?.vault ?? SUBLY_VAULT,
614
956
  maxComputeUnitLimit: policy?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
615
957
  maxComputeUnitPriceMicroLamports: policy?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
616
958
  maxTemporaryAccountLamports: policy?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
@@ -677,7 +1019,7 @@ function expectCreateTemporaryAccount(ixs, intent, policy) {
677
1019
  }
678
1020
  const lamports = readU64LE(ix.data, 4);
679
1021
  const space = readU64LE(ix.data, 12);
680
- const owner = bs583.encode(ix.data.subarray(20, 52));
1022
+ const owner = bs585.encode(ix.data.subarray(20, 52));
681
1023
  if (space !== 165n) {
682
1024
  reject("temp_account_mismatch", "Temporary account space must be 165 bytes");
683
1025
  }
@@ -699,7 +1041,7 @@ function expectInitializeTemporaryAccount(ixs, intent) {
699
1041
  if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 18) {
700
1042
  reject("temp_account_mismatch", "Expected InitializeAccount3 instruction");
701
1043
  }
702
- const owner = bs583.encode(ix.data.subarray(1, 33));
1044
+ const owner = bs585.encode(ix.data.subarray(1, 33));
703
1045
  if (owner !== intent.wallet) {
704
1046
  reject(
705
1047
  "temp_account_mismatch",
@@ -713,15 +1055,90 @@ function expectInitializeTemporaryAccount(ixs, intent) {
713
1055
  reject("temp_account_mismatch", "Temporary account mint must be USDC");
714
1056
  }
715
1057
  }
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
- }
1058
+ function consumeFarmInstructions(ixs, intent) {
1059
+ if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
1060
+ return;
1061
+ }
1062
+ const unstake = ixs.shift();
1063
+ const userState = validateFarmUnstakeInstruction(unstake, intent);
1064
+ if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
1065
+ reject(
1066
+ "farm_instruction_mismatch",
1067
+ "A farm unstake must be followed by a farm withdrawal"
1068
+ );
1069
+ }
1070
+ const withdraw = ixs.shift();
1071
+ validateFarmWithdrawInstruction(withdraw, intent, userState);
1072
+ if (ixs[0]?.programAddress === KAMINO_FARMS_PROGRAM_ID) {
1073
+ reject(
1074
+ "farm_instruction_mismatch",
1075
+ "Payment may contain only one farm unstake and one farm withdrawal"
1076
+ );
1077
+ }
1078
+ }
1079
+ var KAMINO_FARMS_UNSTAKE_DISCRIMINATOR = Uint8Array.from([
1080
+ 90,
1081
+ 95,
1082
+ 107,
1083
+ 42,
1084
+ 205,
1085
+ 124,
1086
+ 50,
1087
+ 225
1088
+ ]);
1089
+ var KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR = Uint8Array.from([
1090
+ 36,
1091
+ 102,
1092
+ 187,
1093
+ 49,
1094
+ 220,
1095
+ 36,
1096
+ 132,
1097
+ 67
1098
+ ]);
1099
+ function validateFarmUnstakeInstruction(ix, intent) {
1100
+ if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_UNSTAKE_DISCRIMINATOR) || ix.data.length !== 24 || readU128LE(ix.data, 8) <= 0n) {
1101
+ reject(
1102
+ "farm_instruction_mismatch",
1103
+ "Expected a non-zero Kamino farm unstake instruction"
1104
+ );
1105
+ }
1106
+ if (ix.accounts.length !== 4) {
1107
+ reject(
1108
+ "farm_instruction_mismatch",
1109
+ "Farm unstake account list is not canonical"
1110
+ );
1111
+ }
1112
+ if (ix.accounts[0] !== intent.wallet) {
1113
+ reject("farm_instruction_mismatch", "Farm unstake owner must be the agent wallet");
1114
+ }
1115
+ if (ix.accounts[2] !== intent.farm) {
1116
+ reject("farm_instruction_mismatch", "Farm unstake target is not the approved farm");
1117
+ }
1118
+ return ix.accounts[1];
1119
+ }
1120
+ function validateFarmWithdrawInstruction(ix, intent, expectedUserState) {
1121
+ if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR) || ix.data.length !== 8) {
1122
+ reject(
1123
+ "farm_instruction_mismatch",
1124
+ "Expected a canonical Kamino farm withdrawal instruction"
1125
+ );
1126
+ }
1127
+ if (ix.accounts.length !== 7) {
1128
+ reject(
1129
+ "farm_instruction_mismatch",
1130
+ "Farm withdrawal account list is not canonical"
1131
+ );
1132
+ }
1133
+ const expectedSharesAta = deriveAssociatedTokenAddress({
1134
+ owner: intent.wallet,
1135
+ mint: intent.shareMint
1136
+ });
1137
+ if (ix.accounts[0] !== intent.wallet || ix.accounts[1] !== expectedUserState || ix.accounts[2] !== intent.farm || ix.accounts[3] !== expectedSharesAta || ix.accounts[6] !== SPL_TOKEN_PROGRAM_ID) {
1138
+ reject(
1139
+ "farm_instruction_mismatch",
1140
+ "Farm withdrawal must return the approved vault shares to the agent wallet"
1141
+ );
725
1142
  }
726
1143
  }
727
1144
  function expectKvaultWithdraw(ixs, expectation) {
@@ -856,6 +1273,16 @@ function readU32LE(data, offset) {
856
1273
  }
857
1274
  return Buffer.from(data.subarray(offset, offset + 4)).readUInt32LE(0);
858
1275
  }
1276
+ function readU128LE(data, offset) {
1277
+ if (data.length < offset + 16) {
1278
+ reject("invalid_transaction_encoding", "Instruction data too short for u128");
1279
+ }
1280
+ let value = 0n;
1281
+ for (let index = 0; index < 16; index += 1) {
1282
+ value |= BigInt(data[offset + index]) << BigInt(index * 8);
1283
+ }
1284
+ return value;
1285
+ }
859
1286
  function readShortVec(bytes, startOffset) {
860
1287
  let value = 0;
861
1288
  let shift = 0;
@@ -876,55 +1303,57 @@ function readShortVec(bytes, startOffset) {
876
1303
  }
877
1304
 
878
1305
  // ../../src/client/agent-wallet-signer.ts
879
- var LocalKeypairAgentWalletSigner = class {
1306
+ var IntentValidatingAgentWalletSigner = class {
1307
+ vault;
880
1308
  validationMode = "structured_intent_transaction";
881
- keyPairSigner;
882
1309
  validationPolicy;
883
- constructor(keyPairSigner2, validationPolicy) {
884
- this.keyPairSigner = keyPairSigner2;
885
- this.validationPolicy = validationPolicy;
886
- }
887
- get walletAddress() {
888
- return this.keyPairSigner.address;
1310
+ constructor(validationPolicy) {
1311
+ this.vault = Object.freeze({ ...validationPolicy?.vault ?? SUBLY_VAULT });
1312
+ this.validationPolicy = { ...validationPolicy, vault: this.vault };
889
1313
  }
890
1314
  async signPayment(params) {
891
1315
  this.assertIntentWallet(params.intent.wallet);
892
- validatePaymentIntentTransaction({
893
- ...params,
894
- ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
895
- });
1316
+ validatePaymentIntentTransaction({ ...params, ...this.policySpread() });
896
1317
  return this.sign(params.serializedTransaction);
897
1318
  }
898
1319
  async signDeposit(params) {
899
1320
  this.assertIntentWallet(params.intent.wallet);
900
- validateDepositIntentTransaction({
901
- ...params,
902
- ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
903
- });
1321
+ validateDepositIntentTransaction({ ...params, ...this.policySpread() });
904
1322
  return this.sign(params.serializedTransaction);
905
1323
  }
906
1324
  async signWithdrawal(params) {
907
1325
  this.assertIntentWallet(params.intent.wallet);
908
- validateWithdrawalIntentTransaction({
909
- ...params,
910
- ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
911
- });
1326
+ validateWithdrawalIntentTransaction({ ...params, ...this.policySpread() });
912
1327
  return this.sign(params.serializedTransaction);
913
1328
  }
1329
+ policySpread() {
1330
+ return this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy };
1331
+ }
914
1332
  assertIntentWallet(wallet) {
915
- if (wallet !== this.keyPairSigner.address) {
1333
+ if (wallet !== this.walletAddress) {
916
1334
  throw new IntentValidationError(
917
1335
  "wallet_mismatch",
918
1336
  "Intent wallet does not match this signer's wallet"
919
1337
  );
920
1338
  }
921
1339
  }
1340
+ };
1341
+ var LocalKeypairAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
1342
+ provider = "local-keypair";
1343
+ keyPairSigner;
1344
+ constructor(keyPairSigner, validationPolicy) {
1345
+ super(validationPolicy);
1346
+ this.keyPairSigner = keyPairSigner;
1347
+ }
1348
+ get walletAddress() {
1349
+ return this.keyPairSigner.address;
1350
+ }
922
1351
  async signApiMessage(message) {
923
1352
  const signature = await signBytes(
924
1353
  this.keyPairSigner.keyPair.privateKey,
925
1354
  message
926
1355
  );
927
- return bs584.encode(signature);
1356
+ return bs586.encode(signature);
928
1357
  }
929
1358
  async sign(serializedTransaction) {
930
1359
  const { serializedBase64, transaction } = await addSignaturesToSerializedTransaction({
@@ -944,6 +1373,418 @@ var LocalKeypairAgentWalletSigner = class {
944
1373
  return { serializedTransaction: serializedBase64, agentSignature };
945
1374
  }
946
1375
  };
1376
+ var RemoteAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
1377
+ transport;
1378
+ publicKey;
1379
+ constructor(transport, validationPolicy) {
1380
+ super(validationPolicy);
1381
+ this.transport = transport;
1382
+ this.publicKey = ed25519PublicKeyBytes(
1383
+ transport.provider,
1384
+ transport.walletAddress
1385
+ );
1386
+ }
1387
+ get walletAddress() {
1388
+ return this.transport.walletAddress;
1389
+ }
1390
+ get provider() {
1391
+ return this.transport.provider;
1392
+ }
1393
+ async signApiMessage(message) {
1394
+ const signature = await this.transport.signMessage(message);
1395
+ if (!nacl2.sign.detached.verify(message, signature, this.publicKey)) {
1396
+ throw new RemoteSigningError(
1397
+ this.transport.provider,
1398
+ "message signature did not verify for the agent wallet"
1399
+ );
1400
+ }
1401
+ return bs586.encode(signature);
1402
+ }
1403
+ sign(serializedTransaction) {
1404
+ return externallySignedAgentTransaction({
1405
+ transport: this.transport,
1406
+ serializedTransaction
1407
+ });
1408
+ }
1409
+ };
1410
+
1411
+ // ../../src/client/signer-transports/circle.ts
1412
+ import {
1413
+ constants,
1414
+ createPublicKey,
1415
+ publicEncrypt
1416
+ } from "node:crypto";
1417
+ var PROVIDER = "circle";
1418
+ var DEFAULT_BASE_URL = "https://api.circle.com";
1419
+ async function createCircleSignerTransport(config) {
1420
+ if (!/^[0-9a-fA-F]{64}$/.test(config.entitySecret)) {
1421
+ throw new RemoteSigningError(
1422
+ PROVIDER,
1423
+ "entity secret must be 32 bytes of hex (64 hex chars)"
1424
+ );
1425
+ }
1426
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
1427
+ const fetchImpl = config.fetchImpl ?? fetch;
1428
+ const request = async (method, path, body) => {
1429
+ const json = await providerJsonRequest({
1430
+ provider: PROVIDER,
1431
+ fetchImpl,
1432
+ baseUrl,
1433
+ path,
1434
+ method,
1435
+ headers: { authorization: `Bearer ${config.apiKey}` },
1436
+ body
1437
+ });
1438
+ const data = json?.data;
1439
+ if (data === void 0) {
1440
+ throw new RemoteSigningError(
1441
+ PROVIDER,
1442
+ `${method} ${path} returned no data envelope`,
1443
+ json
1444
+ );
1445
+ }
1446
+ return data;
1447
+ };
1448
+ const walletData = await request("GET", `/v1/w3s/wallets/${config.walletId}`);
1449
+ const wallet = walletData.wallet;
1450
+ if (wallet?.address === void 0) {
1451
+ throw new RemoteSigningError(
1452
+ PROVIDER,
1453
+ `wallet ${config.walletId} has no address`,
1454
+ walletData
1455
+ );
1456
+ }
1457
+ if (wallet.blockchain !== "SOL") {
1458
+ throw new RemoteSigningError(
1459
+ PROVIDER,
1460
+ `wallet ${config.walletId} is on ${String(
1461
+ wallet.blockchain
1462
+ )}, expected SOL (Solana mainnet)`
1463
+ );
1464
+ }
1465
+ const walletAddress = wallet.address;
1466
+ let entityPublicKey = null;
1467
+ const entitySecretCiphertext = async () => {
1468
+ if (entityPublicKey === null) {
1469
+ const data = await request("GET", "/v1/w3s/config/entity/publicKey");
1470
+ const publicKey2 = data.publicKey;
1471
+ if (typeof publicKey2 !== "string") {
1472
+ throw new RemoteSigningError(
1473
+ PROVIDER,
1474
+ "entity public key response has no publicKey",
1475
+ data
1476
+ );
1477
+ }
1478
+ entityPublicKey = createPublicKey(publicKey2);
1479
+ }
1480
+ return publicEncrypt(
1481
+ {
1482
+ key: entityPublicKey,
1483
+ padding: constants.RSA_PKCS1_OAEP_PADDING,
1484
+ oaepHash: "sha256"
1485
+ },
1486
+ Buffer.from(config.entitySecret, "hex")
1487
+ ).toString("base64");
1488
+ };
1489
+ return {
1490
+ provider: PROVIDER,
1491
+ walletAddress,
1492
+ async signMessage(message) {
1493
+ const data = await request("POST", "/v1/w3s/developer/sign/message", {
1494
+ walletId: config.walletId,
1495
+ message: `0x${Buffer.from(message).toString("hex")}`,
1496
+ encodedByHex: true,
1497
+ entitySecretCiphertext: await entitySecretCiphertext()
1498
+ });
1499
+ const signature = data.signature;
1500
+ if (typeof signature !== "string") {
1501
+ throw new RemoteSigningError(
1502
+ PROVIDER,
1503
+ "sign/message returned no signature",
1504
+ data
1505
+ );
1506
+ }
1507
+ return verifiedEd25519Signature({
1508
+ provider: PROVIDER,
1509
+ encodedSignature: signature,
1510
+ message,
1511
+ walletAddress
1512
+ });
1513
+ },
1514
+ async signTransaction(serializedTransactionBase64) {
1515
+ const data = await request(
1516
+ "POST",
1517
+ "/v1/w3s/developer/sign/transaction",
1518
+ {
1519
+ walletId: config.walletId,
1520
+ rawTransaction: serializedTransactionBase64,
1521
+ entitySecretCiphertext: await entitySecretCiphertext()
1522
+ }
1523
+ );
1524
+ const signedTransaction = data.signedTransaction;
1525
+ if (typeof signedTransaction !== "string") {
1526
+ throw new RemoteSigningError(
1527
+ PROVIDER,
1528
+ "sign/transaction returned no signedTransaction",
1529
+ data
1530
+ );
1531
+ }
1532
+ return signedTransaction;
1533
+ }
1534
+ };
1535
+ }
1536
+
1537
+ // ../../src/client/signer-transports/privy.ts
1538
+ import { createPrivateKey, createSign } from "node:crypto";
1539
+ var PROVIDER2 = "privy";
1540
+ var DEFAULT_BASE_URL2 = "https://api.privy.io";
1541
+ function canonicalJson(value) {
1542
+ if (Array.isArray(value)) {
1543
+ return `[${value.map(canonicalJson).join(",")}]`;
1544
+ }
1545
+ if (value !== null && typeof value === "object") {
1546
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1547
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
1548
+ }
1549
+ return JSON.stringify(value);
1550
+ }
1551
+ function parseAuthorizationKey(base64Pkcs8) {
1552
+ const stripped = base64Pkcs8.replace(/^wallet-auth:/, "").trim();
1553
+ try {
1554
+ return createPrivateKey({
1555
+ key: Buffer.from(stripped, "base64"),
1556
+ format: "der",
1557
+ type: "pkcs8"
1558
+ });
1559
+ } catch (error) {
1560
+ throw new RemoteSigningError(
1561
+ PROVIDER2,
1562
+ "authorization key is not a base64 PKCS#8 P-256 private key",
1563
+ error
1564
+ );
1565
+ }
1566
+ }
1567
+ function authorizationSignature(params) {
1568
+ const payload = {
1569
+ version: 1,
1570
+ method: params.method,
1571
+ url: params.url,
1572
+ body: params.body,
1573
+ headers: { "privy-app-id": params.appId }
1574
+ };
1575
+ const signer2 = createSign("sha256");
1576
+ signer2.update(canonicalJson(payload));
1577
+ return signer2.sign(params.key).toString("base64");
1578
+ }
1579
+ async function createPrivySignerTransport(config) {
1580
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/$/, "");
1581
+ const fetchImpl = config.fetchImpl ?? fetch;
1582
+ const authorizationKey = config.authorizationPrivateKey === void 0 ? null : parseAuthorizationKey(config.authorizationPrivateKey);
1583
+ const baseHeaders = {
1584
+ authorization: `Basic ${Buffer.from(
1585
+ `${config.appId}:${config.appSecret}`
1586
+ ).toString("base64")}`,
1587
+ "privy-app-id": config.appId
1588
+ };
1589
+ const request = async (method, path, body) => {
1590
+ const headers = authorizationKey !== null && method !== "GET" && body !== void 0 ? {
1591
+ ...baseHeaders,
1592
+ "privy-authorization-signature": authorizationSignature({
1593
+ key: authorizationKey,
1594
+ appId: config.appId,
1595
+ method,
1596
+ url: `${baseUrl}${path}`,
1597
+ body
1598
+ })
1599
+ } : baseHeaders;
1600
+ const json = await providerJsonRequest({
1601
+ provider: PROVIDER2,
1602
+ fetchImpl,
1603
+ baseUrl,
1604
+ path,
1605
+ method,
1606
+ headers,
1607
+ body
1608
+ });
1609
+ if (json === null || typeof json !== "object") {
1610
+ throw new RemoteSigningError(
1611
+ PROVIDER2,
1612
+ `${method} ${path} returned a non-JSON body`
1613
+ );
1614
+ }
1615
+ return json;
1616
+ };
1617
+ const rpc2 = async (body) => {
1618
+ const response = await request(
1619
+ "POST",
1620
+ `/v1/wallets/${config.walletId}/rpc`,
1621
+ body
1622
+ );
1623
+ const data = response.data;
1624
+ if (data === null || typeof data !== "object") {
1625
+ throw new RemoteSigningError(PROVIDER2, "rpc returned no data", response);
1626
+ }
1627
+ return data;
1628
+ };
1629
+ const wallet = await request("GET", `/v1/wallets/${config.walletId}`);
1630
+ const walletAddress = wallet.address;
1631
+ if (typeof walletAddress !== "string") {
1632
+ throw new RemoteSigningError(
1633
+ PROVIDER2,
1634
+ `wallet ${config.walletId} has no address`,
1635
+ wallet
1636
+ );
1637
+ }
1638
+ if (wallet.chain_type !== "solana") {
1639
+ throw new RemoteSigningError(
1640
+ PROVIDER2,
1641
+ `wallet ${config.walletId} is ${String(
1642
+ wallet.chain_type
1643
+ )}, expected solana`
1644
+ );
1645
+ }
1646
+ return {
1647
+ provider: PROVIDER2,
1648
+ walletAddress,
1649
+ async signMessage(message) {
1650
+ const data = await rpc2({
1651
+ chain_type: "solana",
1652
+ method: "signMessage",
1653
+ params: {
1654
+ message: Buffer.from(message).toString("base64"),
1655
+ encoding: "base64"
1656
+ }
1657
+ });
1658
+ const signature = data.signature;
1659
+ if (typeof signature !== "string") {
1660
+ throw new RemoteSigningError(
1661
+ PROVIDER2,
1662
+ "signMessage returned no signature",
1663
+ data
1664
+ );
1665
+ }
1666
+ return verifiedEd25519Signature({
1667
+ provider: PROVIDER2,
1668
+ encodedSignature: signature,
1669
+ message,
1670
+ walletAddress
1671
+ });
1672
+ },
1673
+ async signTransaction(serializedTransactionBase64) {
1674
+ const data = await rpc2({
1675
+ chain_type: "solana",
1676
+ method: "signTransaction",
1677
+ params: {
1678
+ transaction: serializedTransactionBase64,
1679
+ encoding: "base64"
1680
+ }
1681
+ });
1682
+ const signedTransaction = data.signed_transaction;
1683
+ if (typeof signedTransaction !== "string") {
1684
+ throw new RemoteSigningError(
1685
+ PROVIDER2,
1686
+ "signTransaction returned no signed_transaction",
1687
+ data
1688
+ );
1689
+ }
1690
+ return signedTransaction;
1691
+ }
1692
+ };
1693
+ }
1694
+
1695
+ // ../../src/client/signer-env.ts
1696
+ async function agentWalletSignerFromEnv(env = process.env) {
1697
+ const nonEmpty = (value) => {
1698
+ const trimmed = value?.trim();
1699
+ return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
1700
+ };
1701
+ const provider = nonEmpty(env.SUBLY_SIGNER_PROVIDER)?.toLowerCase() ?? "local";
1702
+ const pickVar = (name) => nonEmpty(env[`SUBLY_${name}`]) ?? nonEmpty(env[name]);
1703
+ const requireVar = (name) => {
1704
+ const value = pickVar(name);
1705
+ if (value === void 0) {
1706
+ throw new Error(
1707
+ `${name} (or SUBLY_${name}) is required for SUBLY_SIGNER_PROVIDER=${provider}`
1708
+ );
1709
+ }
1710
+ return value;
1711
+ };
1712
+ if (provider === "local") {
1713
+ const localSecretKey = loadSecretKeyBytes({
1714
+ base58Secret: env.SUBLY_DEMO_AGENT_KEYPAIR,
1715
+ jsonFilePath: env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1716
+ label: "SUBLY_DEMO_AGENT_KEYPAIR"
1717
+ });
1718
+ return {
1719
+ provider,
1720
+ signer: new LocalKeypairAgentWalletSigner(
1721
+ await createKeyPairSignerFromBytes2(localSecretKey)
1722
+ ),
1723
+ localSecretKey
1724
+ };
1725
+ }
1726
+ if (provider === "circle") {
1727
+ const transport = await createCircleSignerTransport({
1728
+ apiKey: requireVar("CIRCLE_API_KEY"),
1729
+ entitySecret: requireVar("CIRCLE_ENTITY_SECRET"),
1730
+ walletId: requireVar("CIRCLE_WALLET_ID"),
1731
+ baseUrl: pickVar("CIRCLE_BASE_URL")
1732
+ });
1733
+ return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
1734
+ }
1735
+ if (provider === "privy") {
1736
+ const transport = await createPrivySignerTransport({
1737
+ appId: requireVar("PRIVY_APP_ID"),
1738
+ appSecret: requireVar("PRIVY_APP_SECRET"),
1739
+ walletId: requireVar("PRIVY_WALLET_ID"),
1740
+ authorizationPrivateKey: pickVar("PRIVY_AUTHORIZATION_KEY"),
1741
+ baseUrl: pickVar("PRIVY_BASE_URL")
1742
+ });
1743
+ return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
1744
+ }
1745
+ throw new Error(
1746
+ `unknown SUBLY_SIGNER_PROVIDER "${provider}" (expected local, circle, or privy)`
1747
+ );
1748
+ }
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
+ }
947
1788
 
948
1789
  // ../../src/client/lookup-tables.ts
949
1790
  import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
@@ -987,8 +1828,8 @@ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
987
1828
 
988
1829
  // ../../src/api/wallet-auth.ts
989
1830
  import { createHash as createHash3 } from "node:crypto";
990
- import bs585 from "bs58";
991
- import nacl from "tweetnacl";
1831
+ import bs587 from "bs58";
1832
+ import nacl3 from "tweetnacl";
992
1833
  var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
993
1834
  var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
994
1835
  var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
@@ -1008,7 +1849,10 @@ async function walletAuthHeaders(params) {
1008
1849
  const signedAtMs = String(Date.now());
1009
1850
  const message = walletAuthMessage({
1010
1851
  method: params.method,
1011
- path: new URL(params.url).pathname,
1852
+ path: (() => {
1853
+ const url = new URL(params.url);
1854
+ return url.pathname + url.search;
1855
+ })(),
1012
1856
  rawBody: params.body ?? "",
1013
1857
  signedAtMs
1014
1858
  });
@@ -1035,6 +1879,8 @@ var VaultFlowClientError = class extends Error {
1035
1879
  errorDetails;
1036
1880
  };
1037
1881
  var VaultFlowClient = class {
1882
+ vault;
1883
+ rpc;
1038
1884
  baseUrl;
1039
1885
  signer;
1040
1886
  fetchImpl;
@@ -1042,6 +1888,11 @@ var VaultFlowClient = class {
1042
1888
  pollTimeoutMs;
1043
1889
  pollIntervalMs;
1044
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
+ }
1045
1896
  this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
1046
1897
  this.signer = config.signer;
1047
1898
  this.fetchImpl = config.fetchImpl ?? fetch;
@@ -1063,6 +1914,7 @@ var VaultFlowClient = class {
1063
1914
  try {
1064
1915
  prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1065
1916
  wallet: this.signer.walletAddress,
1917
+ vault: this.vault.address,
1066
1918
  amountRawUsdc: input.amountRawUsdc.toString(),
1067
1919
  ...approvalId2 === void 0 ? {} : { approvalId: approvalId2 }
1068
1920
  });
@@ -1076,10 +1928,14 @@ var VaultFlowClient = class {
1076
1928
  }
1077
1929
  prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1078
1930
  wallet: this.signer.walletAddress,
1931
+ vault: this.vault.address,
1079
1932
  amountRawUsdc: input.amountRawUsdc.toString(),
1080
1933
  approvalId: approvalId2
1081
1934
  });
1082
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
+ }
1083
1939
  const signed = await this.signer.signDeposit({
1084
1940
  intent: prepared.signingIntent,
1085
1941
  serializedTransaction: prepared.serializedTransaction,
@@ -1117,12 +1973,23 @@ var VaultFlowClient = class {
1117
1973
  "/v1/withdrawals/prepare",
1118
1974
  {
1119
1975
  wallet: this.signer.walletAddress,
1976
+ vault: this.vault.address,
1120
1977
  amountRawUsdc: input.amountRawUsdc.toString(),
1121
1978
  ...input.purpose === void 0 ? {} : { purpose: input.purpose },
1122
1979
  ...input.payment === void 0 ? {} : { payment: input.payment },
1123
1980
  ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1124
1981
  }
1125
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
+ });
1126
1993
  const signed = await this.signer.signWithdrawal({
1127
1994
  intent: prepared.signingIntent,
1128
1995
  serializedTransaction: prepared.serializedTransaction,
@@ -1160,12 +2027,12 @@ var VaultFlowClient = class {
1160
2027
  await this.postJson(
1161
2028
  "sync",
1162
2029
  `/v1/wallets/${this.signer.walletAddress}/sync`,
1163
- { source: "chain" }
2030
+ { source: "chain", vault: this.vault.address }
1164
2031
  );
1165
2032
  } catch {
1166
2033
  }
1167
2034
  }
1168
- 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}`;
1169
2036
  const response = await this.fetchImpl(url, {
1170
2037
  headers: await walletAuthHeaders({
1171
2038
  signer: this.signer,
@@ -1191,8 +2058,12 @@ var VaultFlowClient = class {
1191
2058
  );
1192
2059
  }
1193
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
+ }
1194
2064
  return {
1195
2065
  wallet: this.signer.walletAddress,
2066
+ vault: this.vault.address,
1196
2067
  principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
1197
2068
  positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
1198
2069
  grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
@@ -1210,7 +2081,7 @@ var VaultFlowClient = class {
1210
2081
  /** Wallet's approvals as the relayer sees them (optionally by status). */
1211
2082
  async listApprovals(status) {
1212
2083
  const body = await this.getJson(
1213
- `/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)}`}`}`
1214
2085
  );
1215
2086
  return body.approvals ?? [];
1216
2087
  }
@@ -1219,16 +2090,21 @@ var VaultFlowClient = class {
1219
2090
  * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
1220
2091
  */
1221
2092
  async createSetupSession(input) {
1222
- return await this.postJson(
2093
+ const session = await this.postJson(
1223
2094
  "prepare",
1224
2095
  `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
1225
2096
  {
2097
+ vault: this.vault.address,
1226
2098
  ...input.policy === void 0 ? {} : { policy: input.policy },
1227
2099
  ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
1228
2100
  ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
1229
2101
  ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1230
2102
  }
1231
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;
1232
2108
  }
1233
2109
  /** Polls a setup session (public capability URL — no auth needed). */
1234
2110
  async getSetupSession(sessionId) {
@@ -1375,31 +2251,6 @@ function parseRelayerError(text) {
1375
2251
  }
1376
2252
  }
1377
2253
 
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
2254
  // ../../src/solana/rpc.ts
1404
2255
  import { createSolanaRpc } from "@solana/kit";
1405
2256
  function createRpc(url) {
@@ -1430,12 +2281,7 @@ var approvalId = process.argv[3];
1430
2281
  if (approvalId !== void 0 && !/^apr_[0-9a-f]+$/i.test(approvalId)) {
1431
2282
  fail(`unrecognized argument: ${approvalId} (expected apr_<approvalId>)`);
1432
2283
  }
1433
- var keyPairSigner = await loadKeyPairSigner({
1434
- base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1435
- jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1436
- label: "SUBLY_DEMO_AGENT_KEYPAIR"
1437
- });
1438
- var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
2284
+ var { signer } = await agentWalletSignerFromEnv();
1439
2285
  var rpc = createRpc(
1440
2286
  process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com"
1441
2287
  );