@aeon-ai-pay/aigateway 0.1.4 → 0.1.6

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/src/balance.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 钱包余额查询(共享模块)
2
+ * Wallet balance lookup (shared module)
3
3
  */
4
4
  import { privateKeyToAccount } from "viem/accounts";
5
5
  import { createPublicClient, http, formatUnits } from "viem";
@@ -42,8 +42,8 @@ function getClient() {
42
42
  }
43
43
 
44
44
  /**
45
- * 通过地址查询 BNB USDT 余额(不需要私钥)
46
- * @param {string} address - EVM 地址
45
+ * Query BNB and USDT balance by address (no private key required).
46
+ * @param {string} address - EVM address
47
47
  */
48
48
  export async function getBalanceByAddress(address) {
49
49
  const client = getClient();
@@ -68,7 +68,7 @@ export async function getBalanceByAddress(address) {
68
68
  }
69
69
 
70
70
  /**
71
- * 通过私钥查询钱包 BNB USDT 余额
71
+ * Query a wallet's BNB and USDT balance by private key.
72
72
  * @param {string} privateKey
73
73
  */
74
74
  export async function getWalletBalance(privateKey) {
@@ -77,9 +77,9 @@ export async function getWalletBalance(privateKey) {
77
77
  }
78
78
 
79
79
  /**
80
- * 查询 session key facilitator USDT allowance
81
- * @param {string} ownerAddress - session key 地址
82
- * @returns {bigint} 当前 allowancewei
80
+ * Query the session key's USDT allowance for the facilitator.
81
+ * @param {string} ownerAddress - session key address
82
+ * @returns {bigint} current allowance (in wei)
83
83
  */
84
84
  export async function getAllowance(ownerAddress) {
85
85
  const client = getClient();
@@ -0,0 +1,38 @@
1
+ /**
2
+ * tools-catalog client. **No caching** — every call hits the server.
3
+ *
4
+ * The server-side catalog (resources/skillboss/tools-catalog.json) is the
5
+ * single source of truth. Each invocation reads the current state, so model
6
+ * additions / schema changes take effect immediately with no stale-cache risk.
7
+ */
8
+ import axios from "axios";
9
+
10
+ /** Fetch catalog from server. Throws on network / HTTP failure. */
11
+ export async function fetchCatalog(serviceUrl) {
12
+ if (!serviceUrl) throw new Error("serviceUrl is required");
13
+ const url = `${serviceUrl}/open/api/skillBoss/tools-catalog`;
14
+ const resp = await axios.get(url, { timeout: 15_000 });
15
+ return resp.data;
16
+ }
17
+
18
+ /**
19
+ * Find a model entry in the catalog by id.
20
+ * Returns { category, model, effectiveSchema } or null.
21
+ * effectiveSchema = model.inputsOverride ?? category.defaultInputsSchema ?? null
22
+ */
23
+ export function findModel(catalog, modelId) {
24
+ if (!catalog || !Array.isArray(catalog.categories)) return null;
25
+ for (const cat of catalog.categories) {
26
+ if (!Array.isArray(cat.models)) continue;
27
+ for (const m of cat.models) {
28
+ if (m.id === modelId) {
29
+ return {
30
+ category: cat,
31
+ model: m,
32
+ effectiveSchema: m.inputsOverride || cat.defaultInputsSchema || null,
33
+ };
34
+ }
35
+ }
36
+ }
37
+ return null;
38
+ }
@@ -8,7 +8,7 @@ export async function clean() {
8
8
  const home = homedir();
9
9
  const removed = [];
10
10
 
11
- // 1. skills CLI 移除(覆盖所有工具)
11
+ // 1. Remove via the skills CLI (covers every tool)
12
12
  try {
13
13
  execFileSync("npx", ["skills", "remove", "aigateway", "-g", "-y"], {
14
14
  stdio: "inherit",
@@ -17,7 +17,7 @@ export async function clean() {
17
17
  logInfo("Removed aigateway skill via skills CLI");
18
18
  removed.push("skills");
19
19
  } catch {
20
- // skills CLI 不可用,手动清理 Claude Code
20
+ // skills CLI unavailable — clean Claude Code manually
21
21
  const skillDir = join(home, ".claude", "skills", "aigateway");
22
22
  if (existsSync(skillDir)) {
23
23
  rmSync(skillDir, { recursive: true, force: true });
@@ -26,7 +26,7 @@ export async function clean() {
26
26
  }
27
27
  }
28
28
 
29
- // 2. 卸载全局包
29
+ // 2. Uninstall the global package
30
30
  try {
31
31
  execFileSync("npm", ["uninstall", "-g", "@aeon-ai-pay/aigateway"], {
32
32
  stdio: "inherit",
@@ -38,7 +38,7 @@ export async function clean() {
38
38
  logInfo("Global package not installed, skipping uninstall");
39
39
  }
40
40
 
41
- // 3. 清理 npm 缓存
41
+ // 3. Clean npm cache
42
42
  try {
43
43
  execFileSync("npm", ["cache", "clean", "--force"], {
44
44
  stdio: "inherit",
@@ -50,7 +50,7 @@ export async function clean() {
50
50
  logError("Failed to clean npm cache, skipping");
51
51
  }
52
52
 
53
- // 4. 清理 npx 缓存
53
+ // 4. Clean npx cache
54
54
  const npxCache = join(home, ".npm", "_npx");
55
55
  if (existsSync(npxCache)) {
56
56
  rmSync(npxCache, { recursive: true, force: true });
@@ -0,0 +1,407 @@
1
+ /**
2
+ * sb-invoke: invoke any AI tool through the x402 protocol.
3
+ *
4
+ * Server endpoint: GET {serviceUrl}/open/ai/x402/skillBoss/create?body=<urlencoded-json>&appId=<merchant>
5
+ * (legacy server path name; client-side abstraction is vendor-agnostic.)
6
+ * Body shape: { "model": "<model_id>", "inputs": { /* tool-specific *​/ } }
7
+ *
8
+ * This module exposes two surfaces:
9
+ * - invoke(opts) → core logic, returns a result object. No emit.
10
+ * Reusable from any future thin wrapper.
11
+ * - sbInvokeCommand(opts) → commander action handler; runs invoke() and
12
+ * emits the universal envelope.
13
+ */
14
+ import { readFileSync, existsSync } from "node:fs";
15
+ import axios from "axios";
16
+ import { createX402Api, decodePaymentResponse, fetchPaymentRequirements } from "../x402.mjs";
17
+ import { resolve } from "../config.mjs";
18
+ import { getWalletBalance, getAllowance } from "../balance.mjs";
19
+ import {
20
+ fundSessionKey,
21
+ promptTopupAmount,
22
+ MIN_TOPUP_USDT,
23
+ TOPUP_PRESETS,
24
+ } from "../funding.mjs";
25
+ import { WalletConnectError } from "../walletconnect.mjs";
26
+ import { emitOk, emitErr, logInfo } from "../output.mjs";
27
+ import { extractOutputs, resolveOutputDir, downloadOutputs } from "../tools-download.mjs";
28
+ import { fetchCatalog, findModel } from "../catalog.mjs";
29
+ import { validateInputs } from "../inputs-validator.mjs";
30
+
31
+ /**
32
+ * Parse `--inputs` value: either a JSON literal or `@path/to/file.json`.
33
+ * Returns the parsed object on success.
34
+ * Throws { code: 'INPUTS_FILE_NOT_FOUND' | 'INVALID_INPUTS_JSON', message } on failure.
35
+ */
36
+ function parseInputs(raw) {
37
+ if (raw == null || raw === "") {
38
+ const err = new Error("Missing --inputs.");
39
+ err.code = "MISSING_INPUTS";
40
+ throw err;
41
+ }
42
+ if (typeof raw === "object") return raw;
43
+
44
+ let text = String(raw);
45
+ if (text.startsWith("@")) {
46
+ const path = text.slice(1);
47
+ if (!existsSync(path)) {
48
+ const err = new Error(`Inputs file not found: ${path}`);
49
+ err.code = "INPUTS_FILE_NOT_FOUND";
50
+ err.path = path;
51
+ throw err;
52
+ }
53
+ text = readFileSync(path, "utf-8");
54
+ }
55
+ try {
56
+ return JSON.parse(text);
57
+ } catch (e) {
58
+ const err = new Error(`Failed to parse --inputs as JSON: ${e.message}`);
59
+ err.code = "INVALID_INPUTS_JSON";
60
+ throw err;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Core invocation. Returns:
66
+ * { ok: true, data: { model, inputs, transaction, downloaded, raw, balance, paymentResponse } }
67
+ * { ok: false, code, details } — caller decides whether to emit
68
+ *
69
+ * Never calls emitOk / emitErr / process.exit directly. Suitable for reuse from
70
+ * any thin wrapper that needs to remap the envelope shape.
71
+ */
72
+ export async function invoke(opts) {
73
+ const serviceUrl = resolve(opts.serviceUrl, "AIGATEWAY_SERVICE_URL", "serviceUrl");
74
+ const privateKey = resolve(opts.privateKey, "EVM_PRIVATE_KEY", "privateKey");
75
+ const { appId, model } = opts;
76
+
77
+ if (!serviceUrl) {
78
+ return { ok: false, code: "SERVICE_URL_MISSING", details: { appId } };
79
+ }
80
+ if (!privateKey) {
81
+ return { ok: false, code: "WALLET_NOT_CONFIGURED", details: { appId } };
82
+ }
83
+ if (!model || !String(model).trim()) {
84
+ return { ok: false, code: "MISSING_MODEL", details: { appId } };
85
+ }
86
+
87
+ let inputs;
88
+ try {
89
+ inputs = parseInputs(opts.inputs);
90
+ } catch (e) {
91
+ return {
92
+ ok: false,
93
+ code: e.code || "INVALID_INPUTS_JSON",
94
+ details: { message: e.message, path: e.path, appId },
95
+ };
96
+ }
97
+
98
+ // ─── Phase 3.2 + 3.3 client-side validation (live catalog from server) ─────
99
+ // Catches model typos & missing/invalid inputs *before* any x402 round-trip.
100
+ // No cache — always fetches fresh catalog. Falls back gracefully on network
101
+ // failure (warn + skip; server still validates).
102
+ let catalog = null;
103
+ try {
104
+ catalog = await fetchCatalog(serviceUrl);
105
+ } catch (e) {
106
+ logInfo(`Warn: catalog fetch failed (${e.message}); skipping client-side validation — server will still check.`);
107
+ }
108
+
109
+ if (catalog) {
110
+ const found = findModel(catalog, model);
111
+ if (!found) {
112
+ return {
113
+ ok: false,
114
+ code: "INVALID_MODEL_ID",
115
+ details: {
116
+ message: `Model "${model}" not found in catalog. Run \`aigateway sb tools\` to see the current list.`,
117
+ model,
118
+ appId,
119
+ },
120
+ };
121
+ }
122
+
123
+ if (found.effectiveSchema) {
124
+ const { ok: validOk, errors } = validateInputs(inputs, found.effectiveSchema);
125
+ if (!validOk) {
126
+ const missingFields = errors.filter((e) => e.kind === "missing").map((e) => e.field);
127
+ const code = missingFields.length > 0 ? "MISSING_INPUTS" : "INVALID_INPUTS";
128
+ return {
129
+ ok: false,
130
+ code,
131
+ details: {
132
+ message: `Inputs validation failed for ${model}: ${errors.map((e) => `[${e.field}] ${e.message}`).join("; ")}`,
133
+ errors,
134
+ required: found.effectiveSchema.required || [],
135
+ properties: Object.keys(found.effectiveSchema.properties || {}),
136
+ category: found.category.key,
137
+ model,
138
+ appId,
139
+ },
140
+ };
141
+ }
142
+ }
143
+ }
144
+ // ─── End client-side validation ────────────────────────────────────────────
145
+
146
+ const bodyPayload = { model, inputs };
147
+ const bodyParam = encodeURIComponent(JSON.stringify(bodyPayload));
148
+ const url = `${serviceUrl}/open/ai/x402/skillBoss/create?body=${bodyParam}&appId=${encodeURIComponent(appId)}`;
149
+
150
+ logInfo(`Invoking ${model}...`);
151
+ logInfo("Fetching payment requirements...");
152
+ let paymentReq;
153
+ let requiredUsdt;
154
+ try {
155
+ paymentReq = await fetchPaymentRequirements(url);
156
+ requiredUsdt = paymentReq.amountUsdt;
157
+ logInfo(`Required: ${requiredUsdt} USDT (pay to ${paymentReq.payTo})`);
158
+ } catch (e) {
159
+ // Server may return HTTP 400 with structured { code, msg } for pricing / body errors.
160
+ // Surface that code as-is so the agent can react (e.g. MODEL_PRICING_NOT_CONFIGURED).
161
+ const serverData = e.response?.data;
162
+ const serverCode = serverData?.code || serverData?.error;
163
+ const serverMsg = serverData?.msg || serverData?.message;
164
+ if (e.response?.status === 400 && serverCode && /^[A-Z_]+$/.test(serverCode)) {
165
+ return {
166
+ ok: false,
167
+ code: serverCode,
168
+ details: { message: serverMsg || e.message, model, appId, serverStatus: 400 },
169
+ };
170
+ }
171
+ return {
172
+ ok: false,
173
+ code: "PAYMENT_FETCH_FAILED",
174
+ details: { message: `Failed to fetch payment requirements: ${e.message}`, model, appId },
175
+ };
176
+ }
177
+
178
+ // Balance / allowance / funding decision
179
+ logInfo("Checking wallet...");
180
+ let needTopup = false;
181
+ let needGas = false;
182
+ let sessionAddress;
183
+ let topupAmount = null;
184
+ let balanceInitialUsdt = null;
185
+ let balanceBeforeChargeUsdt = null;
186
+
187
+ try {
188
+ const { address, usdt, bnb, bnbRaw } = await getWalletBalance(privateKey);
189
+ sessionAddress = address;
190
+ balanceInitialUsdt = usdt;
191
+ balanceBeforeChargeUsdt = usdt;
192
+ const usdtNum = parseFloat(usdt);
193
+
194
+ logInfo(`Wallet: ${address}`);
195
+ logInfo(`Balance: ${usdt} USDT, ${bnb} BNB`);
196
+
197
+ const allowance = await getAllowance(address);
198
+ const requiredWei = BigInt(paymentReq.amountWei);
199
+ if (requiredWei === 0n) {
200
+ return {
201
+ ok: false,
202
+ code: "INVALID_PAYMENT_AMOUNT",
203
+ details: { message: "Server returned invalid payment amount (0). Please retry later.", appId },
204
+ };
205
+ }
206
+ if (allowance >= requiredWei) {
207
+ logInfo("Allowance sufficient, no approve needed.");
208
+ } else {
209
+ logInfo(`Allowance ${allowance} < required ${requiredWei}; approve needed.`);
210
+ if (bnbRaw === 0n) {
211
+ needGas = true;
212
+ logInfo("No BNB for approve gas, will request BNB transfer.");
213
+ }
214
+ }
215
+
216
+ if (usdtNum < requiredUsdt) {
217
+ needTopup = true;
218
+ const shortfall = requiredUsdt - usdtNum;
219
+ const minTopup = Math.max(MIN_TOPUP_USDT, Math.ceil(shortfall));
220
+ logInfo(`USDT insufficient: have ${usdtNum}, need ${requiredUsdt}, shortfall ${shortfall.toFixed(6)} (top-up minimum: ${minTopup} USDT)`);
221
+
222
+ if (opts.topupAmount != null && String(opts.topupAmount).trim() !== "") {
223
+ const amt = Number(opts.topupAmount);
224
+ if (!Number.isFinite(amt) || amt <= 0) {
225
+ return {
226
+ ok: false,
227
+ code: "AMOUNT_INVALID",
228
+ details: { message: `Invalid --topup-amount: ${opts.topupAmount}`, appId },
229
+ };
230
+ }
231
+ if (amt < minTopup) {
232
+ return {
233
+ ok: false,
234
+ code: "TOPUP_AMOUNT_TOO_SMALL",
235
+ details: { message: `--topup-amount ${amt} USDT is below the ${minTopup} USDT minimum for this call.`, minTopup, appId },
236
+ };
237
+ }
238
+ topupAmount = String(opts.topupAmount);
239
+ logInfo(`Using --topup-amount: ${topupAmount} USDT`);
240
+ } else if (process.stdin.isTTY) {
241
+ topupAmount = await promptTopupAmount(minTopup);
242
+ logInfo(`Selected top-up amount: ${topupAmount} USDT`);
243
+ } else {
244
+ const presets = TOPUP_PRESETS.filter((v) => v >= minTopup);
245
+ return {
246
+ ok: false,
247
+ code: "TOPUP_REQUIRED",
248
+ details: {
249
+ message: `USDT balance is below the ${minTopup} USDT minimum for this call. Choose a top-up amount and rerun with --topup-amount <usdt>.`,
250
+ minTopup,
251
+ required: requiredUsdt,
252
+ currentBalance: balanceInitialUsdt,
253
+ address: sessionAddress,
254
+ appId,
255
+ presets,
256
+ hint: `Rerun: aigateway wallet-topup --amount <usdt> --app-id ${appId}`,
257
+ },
258
+ };
259
+ }
260
+ }
261
+ } catch (e) {
262
+ return {
263
+ ok: false,
264
+ code: "BALANCE_CHECK_FAILED",
265
+ details: { message: `Balance check failed: ${e.message}`, appId },
266
+ };
267
+ }
268
+
269
+ if (needTopup || needGas) {
270
+ logInfo("Funding flow triggered...");
271
+ try {
272
+ await fundSessionKey({
273
+ sessionAddress,
274
+ usdtAmount: needTopup ? topupAmount : null,
275
+ needGas,
276
+ });
277
+ } catch (e) {
278
+ if (e instanceof WalletConnectError) {
279
+ return { ok: false, code: e.code, details: { message: e.message, address: sessionAddress, appId } };
280
+ }
281
+ return { ok: false, code: "FUNDING_FAILED", details: { message: e.message, address: sessionAddress, appId } };
282
+ }
283
+
284
+ logInfo("Re-checking wallet balance...");
285
+ try {
286
+ const { usdt, bnbRaw } = await getWalletBalance(privateKey);
287
+ balanceBeforeChargeUsdt = usdt;
288
+ const usdtNum = parseFloat(usdt);
289
+ if (needGas && bnbRaw === 0n) {
290
+ return {
291
+ ok: false,
292
+ code: "INSUFFICIENT_BNB",
293
+ details: { message: "No BNB for approve transaction after funding. Run 'aigateway wallet-gas' to add BNB manually.", address: sessionAddress, appId },
294
+ };
295
+ }
296
+ if (usdtNum < requiredUsdt) {
297
+ return {
298
+ ok: false,
299
+ code: "INSUFFICIENT_USDT",
300
+ details: { message: "Still insufficient USDT after funding.", required: `${requiredUsdt} USDT`, available: `${usdt} USDT`, address: sessionAddress, appId },
301
+ };
302
+ }
303
+ } catch (e) {
304
+ return { ok: false, code: "BALANCE_CHECK_FAILED", details: { message: `Balance re-check failed: ${e.message}`, appId } };
305
+ }
306
+ }
307
+
308
+ // Sign x402 payment & retry the request.
309
+ const { client } = createX402Api(privateKey);
310
+ logInfo(`Submitting payment & request: ${url}`);
311
+
312
+ let response;
313
+ let paymentResponse;
314
+ try {
315
+ const { x402HTTPClient } = await import("@aeon-ai-pay/core/client");
316
+ const httpClient = new x402HTTPClient(client);
317
+
318
+ const raw402 = paymentReq.raw402Response;
319
+ const getHeader = (name) => {
320
+ const value = raw402.headers[name] ?? raw402.headers[name.toLowerCase()];
321
+ return typeof value === "string" ? value : undefined;
322
+ };
323
+ const paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, raw402.data);
324
+ const paymentPayload = await client.createPaymentPayload(paymentRequired);
325
+ const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
326
+
327
+ response = await axios.get(url, {
328
+ headers: {
329
+ ...paymentHeaders,
330
+ "Access-Control-Expose-Headers": "PAYMENT-RESPONSE",
331
+ },
332
+ });
333
+ paymentResponse = decodePaymentResponse(response.headers);
334
+ } catch (error) {
335
+ return {
336
+ ok: false,
337
+ code: "PAYMENT_FAILED",
338
+ details: {
339
+ message: error.message,
340
+ status: error.response?.status,
341
+ data: error.response?.data,
342
+ appId,
343
+ },
344
+ };
345
+ }
346
+
347
+ const transaction = response.data?.transaction || paymentResponse?.txHash || null;
348
+
349
+ // Detect downloadable outputs and fetch them locally (unless --raw).
350
+ let downloaded = [];
351
+ if (!opts.raw) {
352
+ const { kind, items } = extractOutputs(response.data);
353
+ if (items.length) {
354
+ const outputDir = resolveOutputDir(opts.output, kind);
355
+ downloaded = await downloadOutputs(items, outputDir);
356
+ for (const d of downloaded) {
357
+ if (d.error) {
358
+ logInfo(`Failed to download ${d.url}: ${d.error}`);
359
+ } else {
360
+ logInfo(`Saved: ${d.localPath} (${d.format || "?"}, ${d.width || "?"}×${d.height || "?"}, ${d.sizeHuman})`);
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ // Post-payment balance probe (best effort).
367
+ let balanceAfterUsdt = null;
368
+ try {
369
+ const after = await getWalletBalance(privateKey);
370
+ balanceAfterUsdt = after.usdt;
371
+ } catch (e) {
372
+ logInfo(`Post-payment balance check failed: ${e.message}`);
373
+ }
374
+
375
+ return {
376
+ ok: true,
377
+ data: {
378
+ model,
379
+ inputs,
380
+ transaction,
381
+ downloaded,
382
+ // unwrap server envelope: { payer, transaction, data: <upstream-response> } → <upstream-response>
383
+ raw: response.data?.data ?? response.data,
384
+ paymentResponse,
385
+ balance: {
386
+ initial: balanceInitialUsdt,
387
+ before: balanceBeforeChargeUsdt,
388
+ after: balanceAfterUsdt,
389
+ charged: requiredUsdt,
390
+ topup: topupAmount,
391
+ },
392
+ },
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Commander action handler for `aigateway sb invoke`.
398
+ * Emits the universal envelope; errors are emitted via emitErr (which exits).
399
+ */
400
+ export async function sbInvokeCommand(opts) {
401
+ const result = await invoke(opts);
402
+ if (result.ok) {
403
+ emitOk("sb-invoke", result.data, { success: true, ...result.data });
404
+ return;
405
+ }
406
+ emitErr("sb-invoke", result.code, result.details);
407
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * sb-tools: fetch and display the AI tool catalog from the server.
3
+ *
4
+ * aigateway sb tools
5
+ *
6
+ * No caching — always hits the server. Server-side `tools-catalog.json` is the
7
+ * single source of truth. Stdout is the full envelope with `data` = catalog.
8
+ *
9
+ * Endpoint: GET {serviceUrl}/open/api/skillBoss/tools-catalog
10
+ * (free, no x402; price fields are stripped server-side)
11
+ */
12
+ import { resolve } from "../config.mjs";
13
+ import { emitOk, emitErr, logInfo } from "../output.mjs";
14
+ import { fetchCatalog } from "../catalog.mjs";
15
+
16
+ export async function sbTools(opts) {
17
+ const serviceUrl = resolve(opts.serviceUrl, "AIGATEWAY_SERVICE_URL", "serviceUrl");
18
+ if (!serviceUrl) {
19
+ emitErr("sb-tools", "SERVICE_URL_MISSING", {});
20
+ return;
21
+ }
22
+
23
+ logInfo("Fetching tools catalog from server...");
24
+ let data;
25
+ try {
26
+ data = await fetchCatalog(serviceUrl);
27
+ } catch (e) {
28
+ emitErr("sb-tools", "CATALOG_FETCH_FAILED", {
29
+ message: `Failed to fetch tools catalog: ${e.message}`,
30
+ url: `${serviceUrl}/open/api/skillBoss/tools-catalog`,
31
+ status: e.response?.status,
32
+ });
33
+ return;
34
+ }
35
+
36
+ emitOk("sb-tools", data, { success: true, ...data });
37
+ }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * gas 命令:通过 WalletConnect 从主钱包向本地钱包转 BNB(withdraw 时支付 gas)
2
+ * gas command: transfer BNB from the main wallet to the local wallet via WalletConnect
3
+ * (used to pay gas during withdraw).
3
4
  */
4
5
  import { loadConfig } from "../config.mjs";
5
6
  import { getBalanceByAddress } from "../balance.mjs";
@@ -1,19 +1,18 @@
1
1
  /**
2
- * wallet-init:本地 session 钱包 check / 创建 + 链上状态评估
2
+ * wallet-init: check / create the local session wallet and assess its on-chain status.
3
3
  *
4
- * 步骤:
5
- * 1. ~/.aigateway/config.json privateKey → viem.generatePrivateKey() 生成
6
- * 2. USDT / BNB 余额(除非刚 created,则跳过查询直接判定为 needsTopup)
7
- * 3. facilitator allowance(同上规则)
8
- * 4. 综合判定 needsTopup 与原因,返回完整就绪状态供 agent 决策
4
+ * Steps:
5
+ * 1. If ~/.aigateway/config.json has no privateKey → generate one with viem.generatePrivateKey().
6
+ * 2. Query USDT / BNB balance (skipped when the wallet was just created — it must be empty).
7
+ * 3. Query the facilitator allowance (same rule).
8
+ * 4. Decide needsTopup with the reason and return the full readiness state for the agent to act on.
9
9
  *
10
- * 设计意图:agent 跑完 wallet-init 一条命令就拿到决策依据:
11
- * - data.ready=true 表示钱包私钥可用
12
- * - data.needsTopup=true → 必须先 wallet-topupenvelope 里附带 presets / minTopup / reason
13
- * - data.needsTopup=false → 可直接 create-card / create-image
10
+ * Design intent: with a single wallet-init call, the agent gets every decision input it needs:
11
+ * - data.ready=true → the session private key is usable
12
+ * - data.needsTopup=true → wallet-topup must run first (the envelope includes presets / minTopup / reason)
13
+ * - data.needsTopup=false → can proceed directly to sb invoke
14
14
  */
15
15
  import { loadConfig, saveConfig } from "../config.mjs";
16
- import { MIN_AMOUNT, MAX_AMOUNT } from "../constants.mjs";
17
16
  import { getWalletBalance, getAllowance } from "../balance.mjs";
18
17
  import {
19
18
  LOW_BALANCE_THRESHOLD,
@@ -41,7 +40,7 @@ export async function initWallet(opts) {
41
40
  logInfo(`Wallet: ${config.address}`);
42
41
  }
43
42
 
44
- // 链上状态评估
43
+ // On-chain status check
45
44
  let usdt = "0";
46
45
  let bnb = "0";
47
46
  let usdtNum = 0;
@@ -50,7 +49,7 @@ export async function initWallet(opts) {
50
49
  let chainCheckError = null;
51
50
 
52
51
  if (created) {
53
- // 刚建好的钱包余额必然为 0,跳过链上查询节省 ~500ms
52
+ // A freshly created wallet is guaranteed to be empty; skip the chain query to save ~500ms.
54
53
  logInfo("Fresh wallet — skipping balance lookup (assumed empty).");
55
54
  } else {
56
55
  try {
@@ -68,18 +67,19 @@ export async function initWallet(opts) {
68
67
  }
69
68
  }
70
69
 
71
- // 决策:是否需要 topup —— 只看链上真实状态,不依赖 config.mainWallet 字段。
72
- // 之前用 !config.mainWallet 当判断条件是错的:mainWallet 只是 withdraw 默认目标地址,
73
- // 即使为 null(如用户外部转账 / 旧版本未记录),只要链上 USDT/allowance 充足就应该
74
- // 直接允许付费调用,不要强制再走一次 wallet-topup。
70
+ // Decision: needsTopup. Use only real on-chain state — do NOT depend on config.mainWallet.
71
+ // The previous logic `created || !config.mainWallet` was wrong: mainWallet is purely the default
72
+ // destination for withdraw. If USDT / allowance on-chain are sufficient — even when mainWallet
73
+ // is null (external CEX deposit / older versions that didn't record it) — paid calls should be
74
+ // allowed without forcing another wallet-topup round.
75
75
  let needsTopup = false;
76
76
  let topupReason = null;
77
77
  if (created) {
78
- // 刚生成的 session key 必然没钱,无需查链
78
+ // A freshly generated session key has no funds — no point querying the chain.
79
79
  needsTopup = true;
80
80
  topupReason = "first_time";
81
81
  } else if (!chainCheckOk) {
82
- // 链上查询失败 —— 保守标记需要 topup 由用户决定下一步
82
+ // Chain probe failed conservatively flag needsTopup so the user can decide what to do.
83
83
  needsTopup = true;
84
84
  topupReason = "chain_check_failed";
85
85
  } else if (usdtNum < LOW_BALANCE_THRESHOLD) {
@@ -102,10 +102,9 @@ export async function initWallet(opts) {
102
102
  bnb,
103
103
  allowance: allowance.toString(),
104
104
  needsTopup,
105
- topupReason, // "no_prior_funding" | "low_balance" | "no_approve" | null
105
+ topupReason, // "first_time" | "low_balance" | "no_approve" | "chain_check_failed" | null
106
106
  minTopup: MIN_TOPUP_USDT,
107
107
  presets: TOPUP_PRESETS,
108
- amountLimits: { min: MIN_AMOUNT, max: MAX_AMOUNT },
109
108
  chainCheck: chainCheckOk ? "ok" : { error: chainCheckError },
110
109
  };
111
110
  emitOk("wallet-init", data, data);