@subly_fi/pay 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/doctor.js ADDED
@@ -0,0 +1,188 @@
1
+ // src/doctor.ts
2
+ import { accessSync, constants } from "node:fs";
3
+
4
+ // ../../src/config/vault-catalog.ts
5
+ import { readFileSync } from "node:fs";
6
+ import { z } from "zod";
7
+
8
+ // ../../src/lib/solana-address.ts
9
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
10
+ var BASE58_LOOKUP = new Map(
11
+ [...BASE58_ALPHABET].map((character, index) => [character, BigInt(index)])
12
+ );
13
+ function assertSolanaAddress(value, fieldName) {
14
+ if (value.length < 32 || value.length > 44) {
15
+ throw new Error(`${fieldName} must be a valid Solana public key`);
16
+ }
17
+ if (decodeBase58(value).length !== 32) {
18
+ throw new Error(`${fieldName} must be a valid Solana public key`);
19
+ }
20
+ return value;
21
+ }
22
+ function decodeBase58(value) {
23
+ if (value.length === 0) {
24
+ return new Uint8Array();
25
+ }
26
+ let decoded = 0n;
27
+ for (const character of value) {
28
+ const digit = BASE58_LOOKUP.get(character);
29
+ if (digit === void 0) {
30
+ return new Uint8Array();
31
+ }
32
+ decoded = decoded * 58n + digit;
33
+ }
34
+ const bytes = [];
35
+ while (decoded > 0n) {
36
+ bytes.push(Number(decoded & 0xffn));
37
+ decoded >>= 8n;
38
+ }
39
+ for (const character of value) {
40
+ if (character !== "1") {
41
+ break;
42
+ }
43
+ bytes.push(0);
44
+ }
45
+ return Uint8Array.from(bytes.reverse());
46
+ }
47
+
48
+ // ../../src/config/vault.ts
49
+ var MAINNET_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
50
+ var KAMINO_VAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
51
+ var NO_VAULT_FARM = "11111111111111111111111111111111";
52
+ var DEFAULT_VAULT_CONFIG = Object.freeze({
53
+ address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
54
+ programId: KAMINO_VAULT_PROGRAM_ID,
55
+ usdcMint: MAINNET_USDC_MINT,
56
+ shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
57
+ farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
58
+ });
59
+ function vaultConfigFromEnv(env2 = process.env) {
60
+ const value = (name) => env2[name]?.trim() || void 0;
61
+ const vaultAddress = value("SUBLY_VAULT_ADDRESS") ?? DEFAULT_VAULT_CONFIG.address;
62
+ const customVault = vaultAddress !== DEFAULT_VAULT_CONFIG.address;
63
+ const anchor = (name, fallback) => {
64
+ const configured = value(name);
65
+ if (customVault && configured === void 0) {
66
+ throw new Error(
67
+ `${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.`
68
+ );
69
+ }
70
+ return assertSolanaAddress(configured ?? fallback, name);
71
+ };
72
+ const usdcMint = value("SUBLY_VAULT_USDC_MINT") ?? MAINNET_USDC_MINT;
73
+ if (usdcMint !== MAINNET_USDC_MINT) {
74
+ throw new Error(
75
+ "SUBLY_VAULT_USDC_MINT must be mainnet USDC; other deposit assets are not supported"
76
+ );
77
+ }
78
+ return Object.freeze({
79
+ address: assertSolanaAddress(vaultAddress, "SUBLY_VAULT_ADDRESS"),
80
+ programId: KAMINO_VAULT_PROGRAM_ID,
81
+ usdcMint,
82
+ shareMint: anchor("SUBLY_VAULT_SHARE_MINT", DEFAULT_VAULT_CONFIG.shareMint),
83
+ farm: anchor("SUBLY_VAULT_FARM", DEFAULT_VAULT_CONFIG.farm)
84
+ });
85
+ }
86
+
87
+ // ../../src/config/vault-catalog.ts
88
+ var publicKey = z.string().refine((value) => {
89
+ try {
90
+ assertSolanaAddress(value, "vault catalog address");
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }, "Invalid Solana public key");
96
+ var catalogSchema = z.object({
97
+ version: z.literal(1),
98
+ defaultVault: publicKey,
99
+ vaults: z.array(z.object({
100
+ address: publicKey,
101
+ programId: z.literal(KAMINO_VAULT_PROGRAM_ID),
102
+ usdcMint: z.literal(MAINNET_USDC_MINT),
103
+ shareMint: publicKey,
104
+ farm: publicKey,
105
+ name: z.string().min(1).max(128).optional(),
106
+ depositsEnabled: z.boolean().optional(),
107
+ extraLookupTables: z.array(publicKey).max(16).optional()
108
+ }).strict()).min(1).max(100)
109
+ }).strict();
110
+ function parseVaultCatalog(value) {
111
+ const catalog = catalogSchema.parse(value);
112
+ const addresses = new Set(catalog.vaults.map((vault) => vault.address));
113
+ if (addresses.size !== catalog.vaults.length) throw new Error("Duplicate vault in catalog");
114
+ if (!addresses.has(catalog.defaultVault)) throw new Error("defaultVault must be in the vault catalog");
115
+ return { ...catalog, vaults: catalog.vaults.map((vault) => Object.freeze(vault)) };
116
+ }
117
+ function vaultCatalogFromEnv(env2 = process.env) {
118
+ const path = env2.SUBLY_VAULTS_FILE?.trim();
119
+ if (!path) {
120
+ const vault = vaultConfigFromEnv(env2);
121
+ return { version: 1, defaultVault: vault.address, vaults: [vault] };
122
+ }
123
+ const catalog = parseVaultCatalog(JSON.parse(readFileSync(path, "utf8")));
124
+ const selected = env2.SUBLY_VAULT_ADDRESS?.trim() || catalog.defaultVault;
125
+ if (!catalog.vaults.some((vault) => vault.address === selected)) {
126
+ throw new Error("SUBLY_VAULT_ADDRESS must be in SUBLY_VAULTS_FILE");
127
+ }
128
+ return { ...catalog, defaultVault: selected };
129
+ }
130
+ function defaultCatalogVault(catalog) {
131
+ return catalog.vaults.find((vault) => vault.address === catalog.defaultVault);
132
+ }
133
+
134
+ // src/doctor.ts
135
+ var checks = [];
136
+ async function check(name, action) {
137
+ try {
138
+ checks.push({ name, ok: true, message: await action() });
139
+ } catch (error) {
140
+ checks.push({ name, ok: false, message: error instanceof Error ? error.message : "Check failed" });
141
+ }
142
+ }
143
+ var env = process.env;
144
+ await check("node", () => {
145
+ if (Number(process.versions.node.split(".")[0]) < 24) throw new Error("Install Node.js 24 or later");
146
+ return process.versions.node;
147
+ });
148
+ await check("signer configuration", () => {
149
+ const provider = env.SUBLY_SIGNER_PROVIDER?.trim().toLowerCase() || "local";
150
+ if (provider === "local") {
151
+ if (env.SUBLY_DEMO_AGENT_KEYPAIR) return "Base58 key configured (contents not inspected)";
152
+ if (!env.SUBLY_DEMO_AGENT_KEYPAIR_PATH) throw new Error("Set SUBLY_DEMO_AGENT_KEYPAIR_PATH to an agent keypair JSON file");
153
+ try {
154
+ accessSync(env.SUBLY_DEMO_AGENT_KEYPAIR_PATH, constants.R_OK);
155
+ } catch {
156
+ throw new Error("Agent keypair file is not readable");
157
+ }
158
+ return "Agent keypair file readable (contents not inspected)";
159
+ }
160
+ const required = provider === "circle" ? ["CIRCLE_API_KEY", "CIRCLE_ENTITY_SECRET", "CIRCLE_WALLET_ID"] : provider === "privy" ? ["PRIVY_APP_ID", "PRIVY_APP_SECRET", "PRIVY_WALLET_ID"] : null;
161
+ if (!required) throw new Error("SUBLY_SIGNER_PROVIDER must be local, circle or privy");
162
+ const missing = required.filter((key) => !(env[`SUBLY_${key}`] || env[key]));
163
+ if (missing.length) throw new Error(`Missing ${missing.join(", ")}`);
164
+ return `${provider} credentials configured (provider not contacted)`;
165
+ });
166
+ await check("relayer and vault", async () => {
167
+ if (!env.SUBLY_RELAYER_URL) throw new Error("Set SUBLY_RELAYER_URL to your chosen operator's URL");
168
+ const url = new URL(env.SUBLY_RELAYER_URL);
169
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) throw new Error("Use HTTPS (HTTP is allowed only for local development)");
170
+ const catalog = vaultCatalogFromEnv();
171
+ const local = defaultCatalogVault(catalog);
172
+ const health = await fetch(`${url.toString().replace(/\/$/, "")}/readyz`, { signal: AbortSignal.timeout(1e4) });
173
+ if (!health.ok || (await health.json()).ok !== true) throw new Error("Relayer readiness check failed");
174
+ const response = await fetch(`${url.toString().replace(/\/$/, "")}/v1/vaults`, { signal: AbortSignal.timeout(1e4) });
175
+ if (!response.ok) throw new Error("Relayer vault catalogue unavailable");
176
+ const remote = (await response.json()).vaults?.find((v) => v.address === local.address);
177
+ if (!remote || ["programId", "usdcMint", "shareMint", "farm"].some((key) => remote[key] !== local[key])) throw new Error("Selected local vault trust anchors differ from the relayer; review the operator catalogue before changing configuration");
178
+ return `Ready; selected vault ${local.address} matches local trust anchors`;
179
+ });
180
+ await check("Solana RPC", async () => {
181
+ const response = await fetch(env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getGenesisHash" }), signal: AbortSignal.timeout(1e4) });
182
+ const body = await response.json();
183
+ if (!response.ok || body.result !== "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d") throw new Error("RPC is unavailable or is not Solana mainnet-beta");
184
+ return "Mainnet-beta RPC reachable";
185
+ });
186
+ var ok = checks.every((c) => c.ok);
187
+ console.log(JSON.stringify({ ok, checks, note: "Read-only diagnostics. No key was loaded, signature produced, or transaction sent. This does not verify balances, vault safety or transaction simulation support." }, null, 2));
188
+ if (!ok) process.exitCode = 1;