@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/CHANGELOG.md +48 -8
- package/bin/cli.mjs +23 -34
- package/docs/exit-codes.md +2 -1
- package/docs/release-process.md +9 -7
- package/package.json +1 -1
- package/scripts/postinstall.mjs +6 -6
- package/skills/aigateway/SKILL.md +369 -272
- package/src/balance.mjs +7 -7
- package/src/catalog.mjs +38 -0
- package/src/commands/clean.mjs +5 -5
- package/src/commands/sb-invoke.mjs +407 -0
- package/src/commands/sb-tools.mjs +37 -0
- package/src/commands/wallet-gas.mjs +2 -1
- package/src/commands/wallet-init.mjs +20 -21
- package/src/commands/wallet-topup.mjs +15 -15
- package/src/commands/wallet-withdraw.mjs +7 -7
- package/src/config.mjs +24 -24
- package/src/error-codes.mjs +22 -14
- package/src/funding.mjs +2 -2
- package/src/inputs-validator.mjs +125 -0
- package/src/output.mjs +19 -16
- package/src/tools-download.mjs +264 -0
- package/src/update-check.mjs +50 -47
- package/src/walletconnect.mjs +65 -63
- package/src/x402.mjs +13 -10
- package/skills/aigateway/references/check-status.md +0 -68
- package/skills/aigateway/references/create-card.md +0 -114
- package/skills/aigateway/references/store.md +0 -87
- package/src/commands/create-card-status.mjs +0 -67
- package/src/commands/create-card.mjs +0 -352
- package/src/commands/create-image.mjs +0 -428
- package/src/sanitize.mjs +0 -48
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
81
|
-
* @param {string} ownerAddress - session key
|
|
82
|
-
* @returns {bigint}
|
|
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();
|
package/src/catalog.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/commands/clean.mjs
CHANGED
|
@@ -8,7 +8,7 @@ export async function clean() {
|
|
|
8
8
|
const home = homedir();
|
|
9
9
|
const removed = [];
|
|
10
10
|
|
|
11
|
-
// 1.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
2
|
+
* wallet-init: check / create the local session wallet and assess its on-chain status.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
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
|
-
*
|
|
11
|
-
* - data.ready=true
|
|
12
|
-
* - data.needsTopup=true →
|
|
13
|
-
* - data.needsTopup=false →
|
|
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
|
-
//
|
|
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
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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, // "
|
|
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);
|