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