@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/deposit.js CHANGED
@@ -1,9 +1,272 @@
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
+
139
+ // ../../src/api/wallet-auth.ts
140
+ import { createHash } from "node:crypto";
141
+ import bs58 from "bs58";
142
+ import nacl from "tweetnacl";
143
+ var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
144
+ var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
145
+ var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
146
+ function sha256Hex(data) {
147
+ return createHash("sha256").update(data, "utf8").digest("hex");
148
+ }
149
+ function walletAuthMessage(params) {
150
+ return new TextEncoder().encode(
151
+ `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
152
+ params.rawBody
153
+ )}:${params.signedAtMs}`
154
+ );
155
+ }
156
+
157
+ // ../../src/client/wallet-auth-headers.ts
158
+ async function walletAuthHeaders(params) {
159
+ const signedAtMs = String(Date.now());
160
+ const message = walletAuthMessage({
161
+ method: params.method,
162
+ path: (() => {
163
+ const url = new URL(params.url);
164
+ return url.pathname + url.search;
165
+ })(),
166
+ rawBody: params.body ?? "",
167
+ signedAtMs
168
+ });
169
+ return {
170
+ [WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
171
+ [WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
172
+ [WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
173
+ };
174
+ }
175
+
176
+ // ../../src/client/onboarding.ts
177
+ var SELF_SERVE_POLICY_ID = "self-serve";
178
+ var OnboardingError = class extends Error {
179
+ constructor(step, message, detail = null) {
180
+ super(message);
181
+ this.step = step;
182
+ this.detail = detail;
183
+ this.name = "OnboardingError";
184
+ }
185
+ step;
186
+ detail;
187
+ };
188
+ async function ensureWalletOnboarded(params) {
189
+ const fetchImpl = params.fetchImpl ?? fetch;
190
+ const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
191
+ const post = async (step, path, body) => {
192
+ const url = `${baseUrl}${path}`;
193
+ const serialized = JSON.stringify(body);
194
+ const response = await fetchImpl(url, {
195
+ method: "POST",
196
+ headers: {
197
+ ...await walletAuthHeaders({
198
+ signer: params.signer,
199
+ method: "POST",
200
+ url,
201
+ body: serialized
202
+ }),
203
+ "content-type": "application/json"
204
+ },
205
+ body: serialized
206
+ });
207
+ if (response.status !== 200) {
208
+ let detail = null;
209
+ try {
210
+ detail = await response.json();
211
+ } catch {
212
+ detail = null;
213
+ }
214
+ throw new OnboardingError(
215
+ step,
216
+ `wallet onboarding ${step} failed with ${response.status}`,
217
+ detail
218
+ );
219
+ }
220
+ };
221
+ const wallet = params.signer.walletAddress;
222
+ const vault = params.vault ?? params.signer.vault?.address ?? SUBLY_VAULT.address;
223
+ await post("register", "/v1/wallets/agent", {
224
+ wallet,
225
+ vault,
226
+ signingPolicyId: SELF_SERVE_POLICY_ID,
227
+ signingMode: "non_interactive",
228
+ signerValidationMode: params.signer.validationMode,
229
+ signerProvider: params.signer.provider ?? "local-keypair",
230
+ activateForPayments: true
231
+ });
232
+ await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain", vault });
233
+ }
234
+
235
+ // ../../src/client/signer-env.ts
236
+ import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
237
+
238
+ // ../../src/solana/keys.ts
239
+ import { readFileSync as readFileSync2 } from "node:fs";
240
+ import bs582 from "bs58";
241
+ import {
242
+ createKeyPairSignerFromBytes
243
+ } from "@solana/kit";
244
+ function loadSecretKeyBytes(params) {
245
+ const { base58Secret, jsonFilePath, label } = params;
246
+ if (base58Secret !== void 0 && base58Secret.length > 0) {
247
+ const bytes = bs582.decode(base58Secret);
248
+ if (bytes.length !== 64) {
249
+ throw new Error(`${label} base58 secret must decode to 64 bytes`);
250
+ }
251
+ return bytes;
252
+ }
253
+ if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
254
+ const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
255
+ if (!Array.isArray(raw) || raw.length !== 64) {
256
+ throw new Error(`${label} keypair file must be a 64-byte JSON array`);
257
+ }
258
+ return Uint8Array.from(raw);
259
+ }
260
+ throw new Error(`${label} keypair is not configured`);
261
+ }
262
+
1
263
  // ../../src/client/agent-wallet-signer.ts
2
264
  import { signBytes } from "@solana/kit";
3
- import bs584 from "bs58";
265
+ import bs587 from "bs58";
266
+ import nacl3 from "tweetnacl";
4
267
 
5
268
  // ../../src/solana/tx.ts
6
- import bs58 from "bs58";
269
+ import bs583 from "bs58";
7
270
  import {
8
271
  appendTransactionMessageInstructions,
9
272
  compileTransaction,
@@ -18,9 +281,9 @@ import {
18
281
  } from "@solana/kit";
19
282
 
20
283
  // ../../src/lib/hash.ts
21
- import { createHash } from "node:crypto";
284
+ import { createHash as createHash2 } from "node:crypto";
22
285
  function sha256TaggedHex(data) {
23
- return `sha256-${createHash("sha256").update(data).digest("hex")}`;
286
+ return `sha256-${createHash2("sha256").update(data).digest("hex")}`;
24
287
  }
25
288
  function stableStringify(value) {
26
289
  if (value === null) {
@@ -46,6 +309,24 @@ function hashStableJson(value) {
46
309
  function decodeSerializedTransaction(serializedBase64) {
47
310
  return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
48
311
  }
312
+ function attachExternalSignatureToTransaction(params) {
313
+ if (!(params.signer in params.transaction.signatures)) {
314
+ throw new Error(
315
+ `transaction does not expect a signature from ${params.signer}`
316
+ );
317
+ }
318
+ const transaction = Object.freeze({
319
+ ...params.transaction,
320
+ signatures: Object.freeze({
321
+ ...params.transaction.signatures,
322
+ [params.signer]: params.signature
323
+ })
324
+ });
325
+ return {
326
+ serializedBase64: getBase64EncodedWireTransaction(transaction),
327
+ transaction
328
+ };
329
+ }
49
330
  async function addSignaturesToSerializedTransaction(params) {
50
331
  const decoded = decodeSerializedTransaction(params.serializedBase64);
51
332
  const signed = await partiallySignTransaction(params.signers, decoded);
@@ -59,29 +340,150 @@ function signatureBase58ForSigner(transaction, signer2) {
59
340
  if (signature === null || signature === void 0) {
60
341
  return null;
61
342
  }
62
- return bs58.encode(signature);
343
+ return bs583.encode(signature);
344
+ }
345
+
346
+ // ../../src/client/remote-signer-transport.ts
347
+ import bs584 from "bs58";
348
+ import nacl2 from "tweetnacl";
349
+ var RemoteSigningError = class extends Error {
350
+ constructor(provider, message, detail = null) {
351
+ super(`[${provider}] ${message}`);
352
+ this.provider = provider;
353
+ this.detail = detail;
354
+ this.name = "RemoteSigningError";
355
+ }
356
+ provider;
357
+ detail;
358
+ };
359
+ function ed25519PublicKeyBytes(provider, walletAddress) {
360
+ let bytes;
361
+ try {
362
+ bytes = bs584.decode(walletAddress);
363
+ } catch {
364
+ throw new RemoteSigningError(
365
+ provider,
366
+ `wallet address ${walletAddress} is not base58`
367
+ );
368
+ }
369
+ if (bytes.length !== 32) {
370
+ throw new RemoteSigningError(
371
+ provider,
372
+ `wallet address ${walletAddress} is not a 32-byte ed25519 key`
373
+ );
374
+ }
375
+ return bytes;
376
+ }
377
+ function verifiedEd25519Signature(params) {
378
+ const publicKey2 = ed25519PublicKeyBytes(params.provider, params.walletAddress);
379
+ const encoded = params.encodedSignature.trim();
380
+ for (const candidate of decodeSignatureCandidates(encoded)) {
381
+ if (nacl2.sign.detached.verify(params.message, candidate, publicKey2)) {
382
+ return candidate;
383
+ }
384
+ }
385
+ throw new RemoteSigningError(
386
+ params.provider,
387
+ `signature did not verify for wallet ${params.walletAddress}`
388
+ );
389
+ }
390
+ function decodeSignatureCandidates(encoded) {
391
+ const candidates = [];
392
+ const hex = encoded.startsWith("0x") ? encoded.slice(2) : encoded;
393
+ if (/^[0-9a-fA-F]{128}$/.test(hex)) {
394
+ candidates.push(Uint8Array.from(Buffer.from(hex, "hex")));
395
+ }
396
+ try {
397
+ const fromBase58 = bs584.decode(encoded);
398
+ if (fromBase58.length === 64) {
399
+ candidates.push(fromBase58);
400
+ }
401
+ } catch {
402
+ }
403
+ if (/^[A-Za-z0-9+/=_-]+$/.test(encoded)) {
404
+ const fromBase64 = Uint8Array.from(
405
+ Buffer.from(encoded.replace(/-/g, "+").replace(/_/g, "/"), "base64")
406
+ );
407
+ if (fromBase64.length === 64) {
408
+ candidates.push(fromBase64);
409
+ }
410
+ }
411
+ return candidates;
412
+ }
413
+ async function requestVerifiedTransactionSignature(params) {
414
+ const { transport } = params;
415
+ const signedBase64 = await transport.signTransaction(
416
+ params.serializedTransactionBase64
417
+ );
418
+ let returned;
419
+ try {
420
+ returned = decodeSerializedTransaction(signedBase64);
421
+ } catch (error) {
422
+ throw new RemoteSigningError(
423
+ transport.provider,
424
+ "provider returned an undecodable signed transaction",
425
+ error
426
+ );
427
+ }
428
+ const signature = returned.signatures[transport.walletAddress] ?? null;
429
+ if (signature === null) {
430
+ throw new RemoteSigningError(
431
+ transport.provider,
432
+ `signed transaction is missing the signature for ${transport.walletAddress}`
433
+ );
434
+ }
435
+ const publicKey2 = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
436
+ if (!nacl2.sign.detached.verify(params.messageBytes, signature, publicKey2)) {
437
+ throw new RemoteSigningError(
438
+ transport.provider,
439
+ "returned signature does not verify over the requested transaction"
440
+ );
441
+ }
442
+ return signature;
443
+ }
444
+ async function externallySignedAgentTransaction(params) {
445
+ const original = decodeSerializedTransaction(params.serializedTransaction);
446
+ const signature = await requestVerifiedTransactionSignature({
447
+ transport: params.transport,
448
+ serializedTransactionBase64: params.serializedTransaction,
449
+ messageBytes: original.messageBytes
450
+ });
451
+ const attached = attachExternalSignatureToTransaction({
452
+ transaction: original,
453
+ signer: params.transport.walletAddress,
454
+ signature
455
+ });
456
+ return {
457
+ serializedTransaction: attached.serializedBase64,
458
+ agentSignature: bs584.encode(signature)
459
+ };
460
+ }
461
+ async function providerJsonRequest(params) {
462
+ const response = await params.fetchImpl(`${params.baseUrl}${params.path}`, {
463
+ method: params.method,
464
+ headers: { ...params.headers, "content-type": "application/json" },
465
+ ...params.body === void 0 ? {} : { body: JSON.stringify(params.body) }
466
+ });
467
+ let json = null;
468
+ try {
469
+ json = await response.json();
470
+ } catch {
471
+ json = null;
472
+ }
473
+ if (!response.ok) {
474
+ throw new RemoteSigningError(
475
+ params.provider,
476
+ `${params.method} ${params.path} failed with ${response.status}`,
477
+ json
478
+ );
479
+ }
480
+ return json;
63
481
  }
64
482
 
65
483
  // ../../src/client/transaction-intent-validator.ts
66
- import bs583 from "bs58";
484
+ import bs586 from "bs58";
67
485
  import { getCompiledTransactionMessageDecoder } from "@solana/kit";
68
486
 
69
- // ../../src/config/constants.ts
70
- var PAYMENT_SCHEME = "subly-yield-exact";
71
- var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
72
- var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
73
- var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
74
- var SUBLY_VAULT = {
75
- name: "Subly USDC Payment Vault Alpha",
76
- address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
77
- programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
78
- usdcMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
79
- shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
80
- lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
81
- farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
82
- };
83
- var USDC_DECIMALS = 6;
84
-
85
487
  // ../../src/domain/request-binding.ts
86
488
  function computeRequestBindingHash(fields) {
87
489
  return hashStableJson({
@@ -98,8 +500,8 @@ function computeRequestBindingHash(fields) {
98
500
  }
99
501
 
100
502
  // ../../src/lib/associated-token-account.ts
101
- import { createHash as createHash2 } from "node:crypto";
102
- import bs582 from "bs58";
503
+ import { createHash as createHash3 } from "node:crypto";
504
+ import bs585 from "bs58";
103
505
  var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
104
506
  var ED25519_P = (1n << 255n) - 19n;
105
507
  var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
@@ -120,13 +522,13 @@ function deriveAssociatedTokenAddress(params) {
120
522
  associatedTokenProgramId
121
523
  );
122
524
  if (address2 !== null) {
123
- return bs582.encode(address2);
525
+ return bs585.encode(address2);
124
526
  }
125
527
  }
126
528
  throw new Error("Unable to derive associated token account address");
127
529
  }
128
530
  function createProgramAddress(seeds, programId) {
129
- const hash = createHash2("sha256");
531
+ const hash = createHash3("sha256");
130
532
  for (const seed of seeds) {
131
533
  hash.update(seed);
132
534
  }
@@ -136,7 +538,7 @@ function createProgramAddress(seeds, programId) {
136
538
  return isEd25519Point(digest) ? null : new Uint8Array(digest);
137
539
  }
138
540
  function decodePublicKey(value, fieldName) {
139
- const decoded = bs582.decode(value);
541
+ const decoded = bs585.decode(value);
140
542
  if (decoded.length !== 32) {
141
543
  throw new Error(`${fieldName} must be a 32-byte public key`);
142
544
  }
@@ -331,13 +733,16 @@ function validatePaymentIntentTransaction(params) {
331
733
  if (intent.network !== SOLANA_MAINNET_NETWORK) {
332
734
  reject("network_mismatch", "Unsupported network");
333
735
  }
334
- if (intent.vault !== SUBLY_VAULT.address) {
736
+ if (intent.vault !== policy.vault.address) {
335
737
  reject("vault_mismatch", "Unsupported vault");
336
738
  }
337
- if (intent.shareMint !== SUBLY_VAULT.shareMint) {
739
+ if (intent.shareMint !== policy.vault.shareMint) {
338
740
  reject("share_mint_mismatch", "Unsupported share mint");
339
741
  }
340
- if (intent.asset !== SUBLY_VAULT.usdcMint) {
742
+ if (intent.farm !== policy.vault.farm) {
743
+ reject("farm_mismatch", "Unsupported Kamino farm");
744
+ }
745
+ if (intent.asset !== policy.vault.usdcMint) {
341
746
  reject("asset_mismatch", "Only USDC payments are supported");
342
747
  }
343
748
  if (intent.memo !== intent.paymentId) {
@@ -405,7 +810,7 @@ function validatePaymentIntentTransaction(params) {
405
810
  expectComputeBudgetPair(ixs, policy);
406
811
  expectCreateTemporaryAccount(ixs, intent, policy);
407
812
  expectInitializeTemporaryAccount(ixs, intent);
408
- consumeFarmInstructions(ixs, intent.wallet);
813
+ consumeFarmInstructions(ixs, intent);
409
814
  expectKvaultWithdraw(ixs, {
410
815
  wallet: intent.wallet,
411
816
  vault: intent.vault,
@@ -453,7 +858,7 @@ function validateDepositIntentTransaction(params) {
453
858
  if (new Date(intent.expiresAt).getTime() <= now) {
454
859
  reject("expired", "Deposit intent has expired");
455
860
  }
456
- assertVaultIntentTargets(intent);
861
+ assertVaultIntentTargets(intent, policy.vault);
457
862
  const decoded = decodeIntentTransaction({
458
863
  serializedTransaction: params.serializedTransaction,
459
864
  ...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
@@ -476,6 +881,9 @@ function validateDepositIntentTransaction(params) {
476
881
  case MEMO_PROGRAM_ID:
477
882
  break;
478
883
  case KVAULT_PROGRAM_ID: {
884
+ if (sawDeposit) {
885
+ reject("duplicate_deposit", "A deposit intent authorizes exactly one KVault deposit");
886
+ }
479
887
  if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
480
888
  reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
481
889
  }
@@ -526,7 +934,7 @@ function validateWithdrawalIntentTransaction(params) {
526
934
  if (new Date(intent.expiresAt).getTime() <= now) {
527
935
  reject("expired", "Withdrawal intent has expired");
528
936
  }
529
- assertVaultIntentTargets(intent);
937
+ assertVaultIntentTargets(intent, policy.vault);
530
938
  const expectedDestination = deriveAssociatedTokenAddress({
531
939
  owner: intent.wallet,
532
940
  mint: intent.asset
@@ -548,13 +956,27 @@ function validateWithdrawalIntentTransaction(params) {
548
956
  reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
549
957
  }
550
958
  let sawWithdraw = false;
959
+ let farmUserState = null;
960
+ let farmInstructionCount = 0;
551
961
  for (const ix of decoded.instructions) {
552
962
  switch (ix.programAddress) {
553
963
  case COMPUTE_BUDGET_PROGRAM_ID:
554
964
  validateComputeBudgetInstruction(ix, policy);
555
965
  break;
556
966
  case MEMO_PROGRAM_ID:
967
+ break;
557
968
  case KAMINO_FARMS_PROGRAM_ID:
969
+ farmInstructionCount += 1;
970
+ if (farmInstructionCount === 1) {
971
+ farmUserState = validateFarmUnstakeInstruction(ix, intent);
972
+ } else if (farmInstructionCount === 2) {
973
+ validateFarmWithdrawInstruction(ix, intent, farmUserState);
974
+ } else {
975
+ reject(
976
+ "farm_instruction_mismatch",
977
+ "Withdrawal may contain only one farm unstake and one farm withdrawal"
978
+ );
979
+ }
558
980
  break;
559
981
  case ASSOCIATED_TOKEN_PROGRAM_ID2:
560
982
  expectAtaCreateForOwner(ix, intent.wallet);
@@ -575,6 +997,12 @@ function validateWithdrawalIntentTransaction(params) {
575
997
  break;
576
998
  }
577
999
  case KVAULT_PROGRAM_ID: {
1000
+ if (sawWithdraw) {
1001
+ reject(
1002
+ "withdraw_mismatch",
1003
+ "Withdrawal may contain only one KVault withdraw instruction"
1004
+ );
1005
+ }
578
1006
  validateKvaultWithdrawInstruction(ix, {
579
1007
  wallet: intent.wallet,
580
1008
  vault: intent.vault,
@@ -597,20 +1025,30 @@ function validateWithdrawalIntentTransaction(params) {
597
1025
  if (!sawWithdraw) {
598
1026
  reject("missing_instruction", "Withdrawal transaction has no KVault withdraw");
599
1027
  }
1028
+ if (farmInstructionCount === 1) {
1029
+ reject(
1030
+ "farm_instruction_mismatch",
1031
+ "A farm unstake must be followed by a farm withdrawal"
1032
+ );
1033
+ }
600
1034
  }
601
- function assertVaultIntentTargets(intent) {
602
- if (intent.vault !== SUBLY_VAULT.address) {
1035
+ function assertVaultIntentTargets(intent, vault) {
1036
+ if (intent.vault !== vault.address) {
603
1037
  reject("vault_mismatch", "Unsupported vault");
604
1038
  }
605
- if (intent.shareMint !== SUBLY_VAULT.shareMint) {
1039
+ if (intent.shareMint !== vault.shareMint) {
606
1040
  reject("share_mint_mismatch", "Unsupported share mint");
607
1041
  }
608
- if (intent.asset !== SUBLY_VAULT.usdcMint) {
1042
+ if (intent.farm !== vault.farm) {
1043
+ reject("farm_mismatch", "Unsupported Kamino farm");
1044
+ }
1045
+ if (intent.asset !== vault.usdcMint) {
609
1046
  reject("asset_mismatch", "Only USDC is supported");
610
1047
  }
611
1048
  }
612
1049
  function resolveIntentValidationPolicy(policy) {
613
1050
  const resolved = {
1051
+ vault: policy?.vault ?? SUBLY_VAULT,
614
1052
  maxComputeUnitLimit: policy?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
615
1053
  maxComputeUnitPriceMicroLamports: policy?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
616
1054
  maxTemporaryAccountLamports: policy?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
@@ -677,7 +1115,7 @@ function expectCreateTemporaryAccount(ixs, intent, policy) {
677
1115
  }
678
1116
  const lamports = readU64LE(ix.data, 4);
679
1117
  const space = readU64LE(ix.data, 12);
680
- const owner = bs583.encode(ix.data.subarray(20, 52));
1118
+ const owner = bs586.encode(ix.data.subarray(20, 52));
681
1119
  if (space !== 165n) {
682
1120
  reject("temp_account_mismatch", "Temporary account space must be 165 bytes");
683
1121
  }
@@ -699,7 +1137,7 @@ function expectInitializeTemporaryAccount(ixs, intent) {
699
1137
  if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 18) {
700
1138
  reject("temp_account_mismatch", "Expected InitializeAccount3 instruction");
701
1139
  }
702
- const owner = bs583.encode(ix.data.subarray(1, 33));
1140
+ const owner = bs586.encode(ix.data.subarray(1, 33));
703
1141
  if (owner !== intent.wallet) {
704
1142
  reject(
705
1143
  "temp_account_mismatch",
@@ -713,15 +1151,90 @@ function expectInitializeTemporaryAccount(ixs, intent) {
713
1151
  reject("temp_account_mismatch", "Temporary account mint must be USDC");
714
1152
  }
715
1153
  }
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
- }
1154
+ function consumeFarmInstructions(ixs, intent) {
1155
+ if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
1156
+ return;
1157
+ }
1158
+ const unstake = ixs.shift();
1159
+ const userState = validateFarmUnstakeInstruction(unstake, intent);
1160
+ if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
1161
+ reject(
1162
+ "farm_instruction_mismatch",
1163
+ "A farm unstake must be followed by a farm withdrawal"
1164
+ );
1165
+ }
1166
+ const withdraw = ixs.shift();
1167
+ validateFarmWithdrawInstruction(withdraw, intent, userState);
1168
+ if (ixs[0]?.programAddress === KAMINO_FARMS_PROGRAM_ID) {
1169
+ reject(
1170
+ "farm_instruction_mismatch",
1171
+ "Payment may contain only one farm unstake and one farm withdrawal"
1172
+ );
1173
+ }
1174
+ }
1175
+ var KAMINO_FARMS_UNSTAKE_DISCRIMINATOR = Uint8Array.from([
1176
+ 90,
1177
+ 95,
1178
+ 107,
1179
+ 42,
1180
+ 205,
1181
+ 124,
1182
+ 50,
1183
+ 225
1184
+ ]);
1185
+ var KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR = Uint8Array.from([
1186
+ 36,
1187
+ 102,
1188
+ 187,
1189
+ 49,
1190
+ 220,
1191
+ 36,
1192
+ 132,
1193
+ 67
1194
+ ]);
1195
+ function validateFarmUnstakeInstruction(ix, intent) {
1196
+ if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_UNSTAKE_DISCRIMINATOR) || ix.data.length !== 24 || readU128LE(ix.data, 8) <= 0n) {
1197
+ reject(
1198
+ "farm_instruction_mismatch",
1199
+ "Expected a non-zero Kamino farm unstake instruction"
1200
+ );
1201
+ }
1202
+ if (ix.accounts.length !== 4) {
1203
+ reject(
1204
+ "farm_instruction_mismatch",
1205
+ "Farm unstake account list is not canonical"
1206
+ );
1207
+ }
1208
+ if (ix.accounts[0] !== intent.wallet) {
1209
+ reject("farm_instruction_mismatch", "Farm unstake owner must be the agent wallet");
1210
+ }
1211
+ if (ix.accounts[2] !== intent.farm) {
1212
+ reject("farm_instruction_mismatch", "Farm unstake target is not the approved farm");
1213
+ }
1214
+ return ix.accounts[1];
1215
+ }
1216
+ function validateFarmWithdrawInstruction(ix, intent, expectedUserState) {
1217
+ if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR) || ix.data.length !== 8) {
1218
+ reject(
1219
+ "farm_instruction_mismatch",
1220
+ "Expected a canonical Kamino farm withdrawal instruction"
1221
+ );
1222
+ }
1223
+ if (ix.accounts.length !== 7) {
1224
+ reject(
1225
+ "farm_instruction_mismatch",
1226
+ "Farm withdrawal account list is not canonical"
1227
+ );
1228
+ }
1229
+ const expectedSharesAta = deriveAssociatedTokenAddress({
1230
+ owner: intent.wallet,
1231
+ mint: intent.shareMint
1232
+ });
1233
+ 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) {
1234
+ reject(
1235
+ "farm_instruction_mismatch",
1236
+ "Farm withdrawal must return the approved vault shares to the agent wallet"
1237
+ );
725
1238
  }
726
1239
  }
727
1240
  function expectKvaultWithdraw(ixs, expectation) {
@@ -856,6 +1369,16 @@ function readU32LE(data, offset) {
856
1369
  }
857
1370
  return Buffer.from(data.subarray(offset, offset + 4)).readUInt32LE(0);
858
1371
  }
1372
+ function readU128LE(data, offset) {
1373
+ if (data.length < offset + 16) {
1374
+ reject("invalid_transaction_encoding", "Instruction data too short for u128");
1375
+ }
1376
+ let value = 0n;
1377
+ for (let index = 0; index < 16; index += 1) {
1378
+ value |= BigInt(data[offset + index]) << BigInt(index * 8);
1379
+ }
1380
+ return value;
1381
+ }
859
1382
  function readShortVec(bytes, startOffset) {
860
1383
  let value = 0;
861
1384
  let shift = 0;
@@ -876,55 +1399,57 @@ function readShortVec(bytes, startOffset) {
876
1399
  }
877
1400
 
878
1401
  // ../../src/client/agent-wallet-signer.ts
879
- var LocalKeypairAgentWalletSigner = class {
1402
+ var IntentValidatingAgentWalletSigner = class {
1403
+ vault;
880
1404
  validationMode = "structured_intent_transaction";
881
- keyPairSigner;
882
1405
  validationPolicy;
883
- constructor(keyPairSigner2, validationPolicy) {
884
- this.keyPairSigner = keyPairSigner2;
885
- this.validationPolicy = validationPolicy;
886
- }
887
- get walletAddress() {
888
- return this.keyPairSigner.address;
1406
+ constructor(validationPolicy) {
1407
+ this.vault = Object.freeze({ ...validationPolicy?.vault ?? SUBLY_VAULT });
1408
+ this.validationPolicy = { ...validationPolicy, vault: this.vault };
889
1409
  }
890
1410
  async signPayment(params) {
891
1411
  this.assertIntentWallet(params.intent.wallet);
892
- validatePaymentIntentTransaction({
893
- ...params,
894
- ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
895
- });
1412
+ validatePaymentIntentTransaction({ ...params, ...this.policySpread() });
896
1413
  return this.sign(params.serializedTransaction);
897
1414
  }
898
1415
  async signDeposit(params) {
899
1416
  this.assertIntentWallet(params.intent.wallet);
900
- validateDepositIntentTransaction({
901
- ...params,
902
- ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
903
- });
1417
+ validateDepositIntentTransaction({ ...params, ...this.policySpread() });
904
1418
  return this.sign(params.serializedTransaction);
905
1419
  }
906
1420
  async signWithdrawal(params) {
907
1421
  this.assertIntentWallet(params.intent.wallet);
908
- validateWithdrawalIntentTransaction({
909
- ...params,
910
- ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
911
- });
1422
+ validateWithdrawalIntentTransaction({ ...params, ...this.policySpread() });
912
1423
  return this.sign(params.serializedTransaction);
913
1424
  }
1425
+ policySpread() {
1426
+ return this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy };
1427
+ }
914
1428
  assertIntentWallet(wallet) {
915
- if (wallet !== this.keyPairSigner.address) {
1429
+ if (wallet !== this.walletAddress) {
916
1430
  throw new IntentValidationError(
917
1431
  "wallet_mismatch",
918
1432
  "Intent wallet does not match this signer's wallet"
919
1433
  );
920
1434
  }
921
1435
  }
1436
+ };
1437
+ var LocalKeypairAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
1438
+ provider = "local-keypair";
1439
+ keyPairSigner;
1440
+ constructor(keyPairSigner, validationPolicy) {
1441
+ super(validationPolicy);
1442
+ this.keyPairSigner = keyPairSigner;
1443
+ }
1444
+ get walletAddress() {
1445
+ return this.keyPairSigner.address;
1446
+ }
922
1447
  async signApiMessage(message) {
923
1448
  const signature = await signBytes(
924
1449
  this.keyPairSigner.keyPair.privateKey,
925
1450
  message
926
1451
  );
927
- return bs584.encode(signature);
1452
+ return bs587.encode(signature);
928
1453
  }
929
1454
  async sign(serializedTransaction) {
930
1455
  const { serializedBase64, transaction } = await addSignaturesToSerializedTransaction({
@@ -944,96 +1469,417 @@ var LocalKeypairAgentWalletSigner = class {
944
1469
  return { serializedTransaction: serializedBase64, agentSignature };
945
1470
  }
946
1471
  };
1472
+ var RemoteAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
1473
+ transport;
1474
+ publicKey;
1475
+ constructor(transport, validationPolicy) {
1476
+ super(validationPolicy);
1477
+ this.transport = transport;
1478
+ this.publicKey = ed25519PublicKeyBytes(
1479
+ transport.provider,
1480
+ transport.walletAddress
1481
+ );
1482
+ }
1483
+ get walletAddress() {
1484
+ return this.transport.walletAddress;
1485
+ }
1486
+ get provider() {
1487
+ return this.transport.provider;
1488
+ }
1489
+ async signApiMessage(message) {
1490
+ const signature = await this.transport.signMessage(message);
1491
+ if (!nacl3.sign.detached.verify(message, signature, this.publicKey)) {
1492
+ throw new RemoteSigningError(
1493
+ this.transport.provider,
1494
+ "message signature did not verify for the agent wallet"
1495
+ );
1496
+ }
1497
+ return bs587.encode(signature);
1498
+ }
1499
+ sign(serializedTransaction) {
1500
+ return externallySignedAgentTransaction({
1501
+ transport: this.transport,
1502
+ serializedTransaction
1503
+ });
1504
+ }
1505
+ };
947
1506
 
948
- // ../../src/api/wallet-auth.ts
949
- import { createHash as createHash3 } from "node:crypto";
950
- import bs585 from "bs58";
951
- import nacl from "tweetnacl";
952
- var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
953
- var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
954
- var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
955
- function sha256Hex(data) {
956
- return createHash3("sha256").update(data, "utf8").digest("hex");
957
- }
958
- function walletAuthMessage(params) {
959
- return new TextEncoder().encode(
960
- `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
961
- params.rawBody
962
- )}:${params.signedAtMs}`
963
- );
1507
+ // ../../src/client/signer-transports/circle.ts
1508
+ import {
1509
+ constants,
1510
+ createPublicKey,
1511
+ publicEncrypt
1512
+ } from "node:crypto";
1513
+ var PROVIDER = "circle";
1514
+ var DEFAULT_BASE_URL = "https://api.circle.com";
1515
+ async function createCircleSignerTransport(config) {
1516
+ if (!/^[0-9a-fA-F]{64}$/.test(config.entitySecret)) {
1517
+ throw new RemoteSigningError(
1518
+ PROVIDER,
1519
+ "entity secret must be 32 bytes of hex (64 hex chars)"
1520
+ );
1521
+ }
1522
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
1523
+ const fetchImpl = config.fetchImpl ?? fetch;
1524
+ const request = async (method, path, body) => {
1525
+ const json = await providerJsonRequest({
1526
+ provider: PROVIDER,
1527
+ fetchImpl,
1528
+ baseUrl,
1529
+ path,
1530
+ method,
1531
+ headers: { authorization: `Bearer ${config.apiKey}` },
1532
+ body
1533
+ });
1534
+ const data = json?.data;
1535
+ if (data === void 0) {
1536
+ throw new RemoteSigningError(
1537
+ PROVIDER,
1538
+ `${method} ${path} returned no data envelope`,
1539
+ json
1540
+ );
1541
+ }
1542
+ return data;
1543
+ };
1544
+ const walletData = await request("GET", `/v1/w3s/wallets/${config.walletId}`);
1545
+ const wallet = walletData.wallet;
1546
+ if (wallet?.address === void 0) {
1547
+ throw new RemoteSigningError(
1548
+ PROVIDER,
1549
+ `wallet ${config.walletId} has no address`,
1550
+ walletData
1551
+ );
1552
+ }
1553
+ if (wallet.blockchain !== "SOL") {
1554
+ throw new RemoteSigningError(
1555
+ PROVIDER,
1556
+ `wallet ${config.walletId} is on ${String(
1557
+ wallet.blockchain
1558
+ )}, expected SOL (Solana mainnet)`
1559
+ );
1560
+ }
1561
+ const walletAddress = wallet.address;
1562
+ let entityPublicKey = null;
1563
+ const entitySecretCiphertext = async () => {
1564
+ if (entityPublicKey === null) {
1565
+ const data = await request("GET", "/v1/w3s/config/entity/publicKey");
1566
+ const publicKey2 = data.publicKey;
1567
+ if (typeof publicKey2 !== "string") {
1568
+ throw new RemoteSigningError(
1569
+ PROVIDER,
1570
+ "entity public key response has no publicKey",
1571
+ data
1572
+ );
1573
+ }
1574
+ entityPublicKey = createPublicKey(publicKey2);
1575
+ }
1576
+ return publicEncrypt(
1577
+ {
1578
+ key: entityPublicKey,
1579
+ padding: constants.RSA_PKCS1_OAEP_PADDING,
1580
+ oaepHash: "sha256"
1581
+ },
1582
+ Buffer.from(config.entitySecret, "hex")
1583
+ ).toString("base64");
1584
+ };
1585
+ return {
1586
+ provider: PROVIDER,
1587
+ walletAddress,
1588
+ async signMessage(message) {
1589
+ const data = await request("POST", "/v1/w3s/developer/sign/message", {
1590
+ walletId: config.walletId,
1591
+ message: `0x${Buffer.from(message).toString("hex")}`,
1592
+ encodedByHex: true,
1593
+ entitySecretCiphertext: await entitySecretCiphertext()
1594
+ });
1595
+ const signature = data.signature;
1596
+ if (typeof signature !== "string") {
1597
+ throw new RemoteSigningError(
1598
+ PROVIDER,
1599
+ "sign/message returned no signature",
1600
+ data
1601
+ );
1602
+ }
1603
+ return verifiedEd25519Signature({
1604
+ provider: PROVIDER,
1605
+ encodedSignature: signature,
1606
+ message,
1607
+ walletAddress
1608
+ });
1609
+ },
1610
+ async signTransaction(serializedTransactionBase64) {
1611
+ const data = await request(
1612
+ "POST",
1613
+ "/v1/w3s/developer/sign/transaction",
1614
+ {
1615
+ walletId: config.walletId,
1616
+ rawTransaction: serializedTransactionBase64,
1617
+ entitySecretCiphertext: await entitySecretCiphertext()
1618
+ }
1619
+ );
1620
+ const signedTransaction = data.signedTransaction;
1621
+ if (typeof signedTransaction !== "string") {
1622
+ throw new RemoteSigningError(
1623
+ PROVIDER,
1624
+ "sign/transaction returned no signedTransaction",
1625
+ data
1626
+ );
1627
+ }
1628
+ return signedTransaction;
1629
+ }
1630
+ };
964
1631
  }
965
1632
 
966
- // ../../src/client/wallet-auth-headers.ts
967
- async function walletAuthHeaders(params) {
968
- const signedAtMs = String(Date.now());
969
- const message = walletAuthMessage({
1633
+ // ../../src/client/signer-transports/privy.ts
1634
+ import { createPrivateKey, createSign } from "node:crypto";
1635
+ var PROVIDER2 = "privy";
1636
+ var DEFAULT_BASE_URL2 = "https://api.privy.io";
1637
+ function canonicalJson(value) {
1638
+ if (Array.isArray(value)) {
1639
+ return `[${value.map(canonicalJson).join(",")}]`;
1640
+ }
1641
+ if (value !== null && typeof value === "object") {
1642
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1643
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
1644
+ }
1645
+ return JSON.stringify(value);
1646
+ }
1647
+ function parseAuthorizationKey(base64Pkcs8) {
1648
+ const stripped = base64Pkcs8.replace(/^wallet-auth:/, "").trim();
1649
+ try {
1650
+ return createPrivateKey({
1651
+ key: Buffer.from(stripped, "base64"),
1652
+ format: "der",
1653
+ type: "pkcs8"
1654
+ });
1655
+ } catch (error) {
1656
+ throw new RemoteSigningError(
1657
+ PROVIDER2,
1658
+ "authorization key is not a base64 PKCS#8 P-256 private key",
1659
+ error
1660
+ );
1661
+ }
1662
+ }
1663
+ function authorizationSignature(params) {
1664
+ const payload = {
1665
+ version: 1,
970
1666
  method: params.method,
971
- path: new URL(params.url).pathname,
972
- rawBody: params.body ?? "",
973
- signedAtMs
974
- });
1667
+ url: params.url,
1668
+ body: params.body,
1669
+ headers: { "privy-app-id": params.appId }
1670
+ };
1671
+ const signer2 = createSign("sha256");
1672
+ signer2.update(canonicalJson(payload));
1673
+ return signer2.sign(params.key).toString("base64");
1674
+ }
1675
+ async function createPrivySignerTransport(config) {
1676
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/$/, "");
1677
+ const fetchImpl = config.fetchImpl ?? fetch;
1678
+ const authorizationKey = config.authorizationPrivateKey === void 0 ? null : parseAuthorizationKey(config.authorizationPrivateKey);
1679
+ const baseHeaders = {
1680
+ authorization: `Basic ${Buffer.from(
1681
+ `${config.appId}:${config.appSecret}`
1682
+ ).toString("base64")}`,
1683
+ "privy-app-id": config.appId
1684
+ };
1685
+ const request = async (method, path, body) => {
1686
+ const headers = authorizationKey !== null && method !== "GET" && body !== void 0 ? {
1687
+ ...baseHeaders,
1688
+ "privy-authorization-signature": authorizationSignature({
1689
+ key: authorizationKey,
1690
+ appId: config.appId,
1691
+ method,
1692
+ url: `${baseUrl}${path}`,
1693
+ body
1694
+ })
1695
+ } : baseHeaders;
1696
+ const json = await providerJsonRequest({
1697
+ provider: PROVIDER2,
1698
+ fetchImpl,
1699
+ baseUrl,
1700
+ path,
1701
+ method,
1702
+ headers,
1703
+ body
1704
+ });
1705
+ if (json === null || typeof json !== "object") {
1706
+ throw new RemoteSigningError(
1707
+ PROVIDER2,
1708
+ `${method} ${path} returned a non-JSON body`
1709
+ );
1710
+ }
1711
+ return json;
1712
+ };
1713
+ const rpc2 = async (body) => {
1714
+ const response = await request(
1715
+ "POST",
1716
+ `/v1/wallets/${config.walletId}/rpc`,
1717
+ body
1718
+ );
1719
+ const data = response.data;
1720
+ if (data === null || typeof data !== "object") {
1721
+ throw new RemoteSigningError(PROVIDER2, "rpc returned no data", response);
1722
+ }
1723
+ return data;
1724
+ };
1725
+ const wallet = await request("GET", `/v1/wallets/${config.walletId}`);
1726
+ const walletAddress = wallet.address;
1727
+ if (typeof walletAddress !== "string") {
1728
+ throw new RemoteSigningError(
1729
+ PROVIDER2,
1730
+ `wallet ${config.walletId} has no address`,
1731
+ wallet
1732
+ );
1733
+ }
1734
+ if (wallet.chain_type !== "solana") {
1735
+ throw new RemoteSigningError(
1736
+ PROVIDER2,
1737
+ `wallet ${config.walletId} is ${String(
1738
+ wallet.chain_type
1739
+ )}, expected solana`
1740
+ );
1741
+ }
975
1742
  return {
976
- [WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
977
- [WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
978
- [WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
1743
+ provider: PROVIDER2,
1744
+ walletAddress,
1745
+ async signMessage(message) {
1746
+ const data = await rpc2({
1747
+ chain_type: "solana",
1748
+ method: "signMessage",
1749
+ params: {
1750
+ message: Buffer.from(message).toString("base64"),
1751
+ encoding: "base64"
1752
+ }
1753
+ });
1754
+ const signature = data.signature;
1755
+ if (typeof signature !== "string") {
1756
+ throw new RemoteSigningError(
1757
+ PROVIDER2,
1758
+ "signMessage returned no signature",
1759
+ data
1760
+ );
1761
+ }
1762
+ return verifiedEd25519Signature({
1763
+ provider: PROVIDER2,
1764
+ encodedSignature: signature,
1765
+ message,
1766
+ walletAddress
1767
+ });
1768
+ },
1769
+ async signTransaction(serializedTransactionBase64) {
1770
+ const data = await rpc2({
1771
+ chain_type: "solana",
1772
+ method: "signTransaction",
1773
+ params: {
1774
+ transaction: serializedTransactionBase64,
1775
+ encoding: "base64"
1776
+ }
1777
+ });
1778
+ const signedTransaction = data.signed_transaction;
1779
+ if (typeof signedTransaction !== "string") {
1780
+ throw new RemoteSigningError(
1781
+ PROVIDER2,
1782
+ "signTransaction returned no signed_transaction",
1783
+ data
1784
+ );
1785
+ }
1786
+ return signedTransaction;
1787
+ }
979
1788
  };
980
1789
  }
981
1790
 
982
- // ../../src/client/onboarding.ts
983
- var SELF_SERVE_POLICY_ID = "self-serve";
984
- var OnboardingError = class extends Error {
985
- constructor(step, message, detail = null) {
986
- super(message);
987
- this.step = step;
988
- this.detail = detail;
989
- this.name = "OnboardingError";
1791
+ // ../../src/client/signer-env.ts
1792
+ async function agentWalletSignerFromEnv(env = process.env) {
1793
+ const nonEmpty = (value) => {
1794
+ const trimmed = value?.trim();
1795
+ return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
1796
+ };
1797
+ const provider = nonEmpty(env.SUBLY_SIGNER_PROVIDER)?.toLowerCase() ?? "local";
1798
+ const pickVar = (name) => nonEmpty(env[`SUBLY_${name}`]) ?? nonEmpty(env[name]);
1799
+ const requireVar = (name) => {
1800
+ const value = pickVar(name);
1801
+ if (value === void 0) {
1802
+ throw new Error(
1803
+ `${name} (or SUBLY_${name}) is required for SUBLY_SIGNER_PROVIDER=${provider}`
1804
+ );
1805
+ }
1806
+ return value;
1807
+ };
1808
+ if (provider === "local") {
1809
+ const localSecretKey = loadSecretKeyBytes({
1810
+ base58Secret: env.SUBLY_DEMO_AGENT_KEYPAIR,
1811
+ jsonFilePath: env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1812
+ label: "SUBLY_DEMO_AGENT_KEYPAIR"
1813
+ });
1814
+ return {
1815
+ provider,
1816
+ signer: new LocalKeypairAgentWalletSigner(
1817
+ await createKeyPairSignerFromBytes2(localSecretKey)
1818
+ ),
1819
+ localSecretKey
1820
+ };
990
1821
  }
991
- step;
992
- detail;
993
- };
994
- async function ensureWalletOnboarded(params) {
995
- const fetchImpl = params.fetchImpl ?? fetch;
996
- const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
997
- const post = async (step, path, body) => {
998
- const url = `${baseUrl}${path}`;
999
- const serialized = JSON.stringify(body);
1000
- const response = await fetchImpl(url, {
1001
- method: "POST",
1002
- headers: {
1003
- ...await walletAuthHeaders({
1004
- signer: params.signer,
1005
- method: "POST",
1006
- url,
1007
- body: serialized
1008
- }),
1009
- "content-type": "application/json"
1010
- },
1011
- body: serialized
1822
+ if (provider === "circle") {
1823
+ const transport = await createCircleSignerTransport({
1824
+ apiKey: requireVar("CIRCLE_API_KEY"),
1825
+ entitySecret: requireVar("CIRCLE_ENTITY_SECRET"),
1826
+ walletId: requireVar("CIRCLE_WALLET_ID"),
1827
+ baseUrl: pickVar("CIRCLE_BASE_URL")
1012
1828
  });
1013
- if (response.status !== 200) {
1014
- let detail = null;
1015
- try {
1016
- detail = await response.json();
1017
- } catch {
1018
- detail = null;
1829
+ return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
1830
+ }
1831
+ if (provider === "privy") {
1832
+ const transport = await createPrivySignerTransport({
1833
+ appId: requireVar("PRIVY_APP_ID"),
1834
+ appSecret: requireVar("PRIVY_APP_SECRET"),
1835
+ walletId: requireVar("PRIVY_WALLET_ID"),
1836
+ authorizationPrivateKey: pickVar("PRIVY_AUTHORIZATION_KEY"),
1837
+ baseUrl: pickVar("PRIVY_BASE_URL")
1838
+ });
1839
+ return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
1840
+ }
1841
+ throw new Error(
1842
+ `unknown SUBLY_SIGNER_PROVIDER "${provider}" (expected local, circle, or privy)`
1843
+ );
1844
+ }
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");
1019
1874
  }
1020
- throw new OnboardingError(
1021
- step,
1022
- `wallet onboarding ${step} failed with ${response.status}`,
1023
- detail
1024
- );
1875
+ const amount = BigInt(raw);
1876
+ if (info.destination === destination) received += amount;
1877
+ if (info.source === destination) received -= amount;
1025
1878
  }
1026
- };
1027
- const wallet = params.signer.walletAddress;
1028
- await post("register", "/v1/wallets/agent", {
1029
- wallet,
1030
- signingPolicyId: SELF_SERVE_POLICY_ID,
1031
- signingMode: "non_interactive",
1032
- signerValidationMode: params.signer.validationMode,
1033
- signerProvider: "local-keypair",
1034
- activateForPayments: true
1035
- });
1036
- await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
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
+ }
1037
1883
  }
1038
1884
 
1039
1885
  // ../../src/client/lookup-tables.ts
@@ -1092,6 +1938,8 @@ var VaultFlowClientError = class extends Error {
1092
1938
  errorDetails;
1093
1939
  };
1094
1940
  var VaultFlowClient = class {
1941
+ vault;
1942
+ rpc;
1095
1943
  baseUrl;
1096
1944
  signer;
1097
1945
  fetchImpl;
@@ -1099,6 +1947,11 @@ var VaultFlowClient = class {
1099
1947
  pollTimeoutMs;
1100
1948
  pollIntervalMs;
1101
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
+ }
1102
1955
  this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
1103
1956
  this.signer = config.signer;
1104
1957
  this.fetchImpl = config.fetchImpl ?? fetch;
@@ -1120,6 +1973,7 @@ var VaultFlowClient = class {
1120
1973
  try {
1121
1974
  prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1122
1975
  wallet: this.signer.walletAddress,
1976
+ vault: this.vault.address,
1123
1977
  amountRawUsdc: input.amountRawUsdc.toString(),
1124
1978
  ...approvalId2 === void 0 ? {} : { approvalId: approvalId2 }
1125
1979
  });
@@ -1133,10 +1987,14 @@ var VaultFlowClient = class {
1133
1987
  }
1134
1988
  prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1135
1989
  wallet: this.signer.walletAddress,
1990
+ vault: this.vault.address,
1136
1991
  amountRawUsdc: input.amountRawUsdc.toString(),
1137
1992
  approvalId: approvalId2
1138
1993
  });
1139
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
+ }
1140
1998
  const signed = await this.signer.signDeposit({
1141
1999
  intent: prepared.signingIntent,
1142
2000
  serializedTransaction: prepared.serializedTransaction,
@@ -1174,12 +2032,23 @@ var VaultFlowClient = class {
1174
2032
  "/v1/withdrawals/prepare",
1175
2033
  {
1176
2034
  wallet: this.signer.walletAddress,
2035
+ vault: this.vault.address,
1177
2036
  amountRawUsdc: input.amountRawUsdc.toString(),
1178
2037
  ...input.purpose === void 0 ? {} : { purpose: input.purpose },
1179
2038
  ...input.payment === void 0 ? {} : { payment: input.payment },
1180
2039
  ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1181
2040
  }
1182
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
+ });
1183
2052
  const signed = await this.signer.signWithdrawal({
1184
2053
  intent: prepared.signingIntent,
1185
2054
  serializedTransaction: prepared.serializedTransaction,
@@ -1217,12 +2086,12 @@ var VaultFlowClient = class {
1217
2086
  await this.postJson(
1218
2087
  "sync",
1219
2088
  `/v1/wallets/${this.signer.walletAddress}/sync`,
1220
- { source: "chain" }
2089
+ { source: "chain", vault: this.vault.address }
1221
2090
  );
1222
2091
  } catch {
1223
2092
  }
1224
2093
  }
1225
- 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}`;
1226
2095
  const response = await this.fetchImpl(url, {
1227
2096
  headers: await walletAuthHeaders({
1228
2097
  signer: this.signer,
@@ -1248,8 +2117,12 @@ var VaultFlowClient = class {
1248
2117
  );
1249
2118
  }
1250
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
+ }
1251
2123
  return {
1252
2124
  wallet: this.signer.walletAddress,
2125
+ vault: this.vault.address,
1253
2126
  principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
1254
2127
  positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
1255
2128
  grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
@@ -1267,7 +2140,7 @@ var VaultFlowClient = class {
1267
2140
  /** Wallet's approvals as the relayer sees them (optionally by status). */
1268
2141
  async listApprovals(status) {
1269
2142
  const body = await this.getJson(
1270
- `/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)}`}`}`
1271
2144
  );
1272
2145
  return body.approvals ?? [];
1273
2146
  }
@@ -1276,16 +2149,21 @@ var VaultFlowClient = class {
1276
2149
  * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
1277
2150
  */
1278
2151
  async createSetupSession(input) {
1279
- return await this.postJson(
2152
+ const session = await this.postJson(
1280
2153
  "prepare",
1281
2154
  `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
1282
2155
  {
2156
+ vault: this.vault.address,
1283
2157
  ...input.policy === void 0 ? {} : { policy: input.policy },
1284
2158
  ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
1285
2159
  ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
1286
2160
  ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1287
2161
  }
1288
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;
1289
2167
  }
1290
2168
  /** Polls a setup session (public capability URL — no auth needed). */
1291
2169
  async getSetupSession(sessionId) {
@@ -1432,31 +2310,6 @@ function parseRelayerError(text) {
1432
2310
  }
1433
2311
  }
1434
2312
 
1435
- // ../../src/solana/keys.ts
1436
- import { readFileSync } from "node:fs";
1437
- import bs586 from "bs58";
1438
- import {
1439
- createKeyPairSignerFromBytes
1440
- } from "@solana/kit";
1441
- async function loadKeyPairSigner(params) {
1442
- const { base58Secret, jsonFilePath, label } = params;
1443
- if (base58Secret !== void 0 && base58Secret.length > 0) {
1444
- const bytes = bs586.decode(base58Secret);
1445
- if (bytes.length !== 64) {
1446
- throw new Error(`${label} base58 secret must decode to 64 bytes`);
1447
- }
1448
- return createKeyPairSignerFromBytes(bytes);
1449
- }
1450
- if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1451
- const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
1452
- if (!Array.isArray(raw) || raw.length !== 64) {
1453
- throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1454
- }
1455
- return createKeyPairSignerFromBytes(Uint8Array.from(raw));
1456
- }
1457
- throw new Error(`${label} keypair is not configured`);
1458
- }
1459
-
1460
2313
  // ../../src/solana/rpc.ts
1461
2314
  import { createSolanaRpc } from "@solana/kit";
1462
2315
  function createRpc(url) {
@@ -1487,12 +2340,7 @@ var approvalId = process.argv[3];
1487
2340
  if (approvalId !== void 0 && !/^apr_[0-9a-f]+$/i.test(approvalId)) {
1488
2341
  fail(`unrecognized argument: ${approvalId} (expected apr_<approvalId>)`);
1489
2342
  }
1490
- var keyPairSigner = await loadKeyPairSigner({
1491
- base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1492
- jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1493
- label: "SUBLY_DEMO_AGENT_KEYPAIR"
1494
- });
1495
- var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
2343
+ var { signer } = await agentWalletSignerFromEnv();
1496
2344
  var rpc = createRpc(
1497
2345
  process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com"
1498
2346
  );