@aeon-ai-pay/aigateway 0.1.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/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/bin/cli.mjs +155 -0
- package/docs/env-vars.md +73 -0
- package/docs/exit-codes.md +65 -0
- package/docs/ide-setup.md +60 -0
- package/docs/output-schema.md +188 -0
- package/docs/recipes/cron-issue-cards.md +69 -0
- package/docs/recipes/error-recovery.md +53 -0
- package/docs/recipes/integrate-in-agent.md +108 -0
- package/docs/recipes/merchant-integration.md +243 -0
- package/docs/release-process.md +98 -0
- package/docs/troubleshooting.md +200 -0
- package/package.json +58 -0
- package/scripts/postinstall.mjs +40 -0
- package/skills/aigateway/SKILL.md +370 -0
- package/skills/aigateway/references/check-status.md +68 -0
- package/skills/aigateway/references/create-card.md +114 -0
- package/skills/aigateway/references/store.md +87 -0
- package/skills/aigateway/references/x402-protocol.md +143 -0
- package/src/balance.mjs +92 -0
- package/src/commands/clean.mjs +65 -0
- package/src/commands/create-card-status.mjs +67 -0
- package/src/commands/create-card.mjs +333 -0
- package/src/commands/create-image.mjs +428 -0
- package/src/commands/wallet-balance.mjs +47 -0
- package/src/commands/wallet-gas.mjs +99 -0
- package/src/commands/wallet-init.mjs +42 -0
- package/src/commands/wallet-topup.mjs +221 -0
- package/src/commands/wallet-withdraw.mjs +183 -0
- package/src/config.mjs +50 -0
- package/src/constants.mjs +22 -0
- package/src/error-codes.mjs +50 -0
- package/src/funding.mjs +216 -0
- package/src/output.mjs +85 -0
- package/src/sanitize.mjs +48 -0
- package/src/update-check.mjs +69 -0
- package/src/walletconnect.mjs +712 -0
- package/src/x402.mjs +120 -0
- package/templates/cline/.clinerules +53 -0
- package/templates/codex/AGENTS.md +56 -0
- package/templates/cursor/.cursor/rules/aigateway.mdc +60 -0
- package/templates/windsurf/.windsurfrules +48 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* create-image:通过 x402 调用 Skill Boss 生成 AI 图像
|
|
3
|
+
*
|
|
4
|
+
* 服务端路径:GET {serviceUrl}/open/ai/x402/skillBoss/create?body=<urlencoded-json>&appId=<merchant>
|
|
5
|
+
* 流程:fetch payment requirements → balance + allowance 检查
|
|
6
|
+
* → (余额不足时)走 funding.mjs/fundSessionKey 充值
|
|
7
|
+
* → x402 EIP-712 签名提交 → 下载图片到本地
|
|
8
|
+
*/
|
|
9
|
+
import { createX402Api, decodePaymentResponse, fetchPaymentRequirements } from "../x402.mjs";
|
|
10
|
+
import { resolve } from "../config.mjs";
|
|
11
|
+
import { getWalletBalance, getAllowance } from "../balance.mjs";
|
|
12
|
+
import axios from "axios";
|
|
13
|
+
import {
|
|
14
|
+
fundSessionKey,
|
|
15
|
+
promptTopupAmount,
|
|
16
|
+
MIN_TOPUP_USDT,
|
|
17
|
+
TOPUP_PRESETS,
|
|
18
|
+
} from "../funding.mjs";
|
|
19
|
+
import { WalletConnectError } from "../walletconnect.mjs";
|
|
20
|
+
import { emitOk, emitErr, logInfo } from "../output.mjs";
|
|
21
|
+
import { mkdirSync, createWriteStream, existsSync, unlinkSync, openSync, readSync, closeSync, statSync } from "node:fs";
|
|
22
|
+
import { join, basename, extname } from "node:path";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { URL } from "node:url";
|
|
25
|
+
import { get as httpsGet } from "node:https";
|
|
26
|
+
import { get as httpGet } from "node:http";
|
|
27
|
+
|
|
28
|
+
const DEFAULT_IMAGE_DIR = join(homedir(), "aigateway-images");
|
|
29
|
+
const DEFAULT_MODEL = "replicate/black-forest-labs/flux-schnell";
|
|
30
|
+
|
|
31
|
+
export async function createImage(opts) {
|
|
32
|
+
logInfo("Generating image...");
|
|
33
|
+
const serviceUrl = resolve(opts.serviceUrl, "AIGATEWAY_SERVICE_URL", "serviceUrl");
|
|
34
|
+
const privateKey = resolve(opts.privateKey, "EVM_PRIVATE_KEY", "privateKey");
|
|
35
|
+
const { prompt, appId } = opts;
|
|
36
|
+
const aspectRatio = opts.aspectRatio || "16:9";
|
|
37
|
+
const outputFormat = (opts.outputFormat || "png").toLowerCase();
|
|
38
|
+
const model = opts.model || DEFAULT_MODEL;
|
|
39
|
+
|
|
40
|
+
if (!serviceUrl) {
|
|
41
|
+
emitErr("create-image", "SERVICE_URL_MISSING", {
|
|
42
|
+
message: "Missing service URL. Set env AIGATEWAY_SERVICE_URL if you need to override the built-in default.",
|
|
43
|
+
appId,
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!privateKey) {
|
|
48
|
+
emitErr("create-image", "WALLET_NOT_CONFIGURED", { appId });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!prompt || !prompt.trim()) {
|
|
52
|
+
emitErr("create-image", "MISSING_PROMPT", { appId });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const bodyPayload = {
|
|
57
|
+
model,
|
|
58
|
+
inputs: {
|
|
59
|
+
prompt,
|
|
60
|
+
aspect_ratio: aspectRatio,
|
|
61
|
+
output_format: outputFormat,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const bodyParam = encodeURIComponent(JSON.stringify(bodyPayload));
|
|
65
|
+
const url = `${serviceUrl}/open/ai/x402/skillBoss/create?body=${bodyParam}&appId=${encodeURIComponent(appId)}`;
|
|
66
|
+
|
|
67
|
+
logInfo("Fetching payment requirements...");
|
|
68
|
+
let requiredUsdt;
|
|
69
|
+
let paymentReq;
|
|
70
|
+
try {
|
|
71
|
+
paymentReq = await fetchPaymentRequirements(url);
|
|
72
|
+
requiredUsdt = paymentReq.amountUsdt;
|
|
73
|
+
logInfo(`Required: ${requiredUsdt} USDT (pay to ${paymentReq.payTo})`);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
emitErr("create-image", "PAYMENT_FETCH_FAILED", {
|
|
76
|
+
message: `Failed to fetch payment requirements: ${e.message}`,
|
|
77
|
+
appId,
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
logInfo("Checking wallet...");
|
|
83
|
+
let needTopup = false;
|
|
84
|
+
let needGas = false;
|
|
85
|
+
let sessionAddress;
|
|
86
|
+
let topupAmount = null;
|
|
87
|
+
let balanceInitialUsdt = null;
|
|
88
|
+
let balanceBeforeChargeUsdt = null;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const { address, usdt, bnb, bnbRaw } = await getWalletBalance(privateKey);
|
|
92
|
+
sessionAddress = address;
|
|
93
|
+
balanceInitialUsdt = usdt;
|
|
94
|
+
balanceBeforeChargeUsdt = usdt;
|
|
95
|
+
const usdtNum = parseFloat(usdt);
|
|
96
|
+
|
|
97
|
+
logInfo(`Wallet: ${address}`);
|
|
98
|
+
logInfo(`Balance: ${usdt} USDT, ${bnb} BNB`);
|
|
99
|
+
|
|
100
|
+
const allowance = await getAllowance(address);
|
|
101
|
+
const requiredWei = BigInt(paymentReq.amountWei);
|
|
102
|
+
if (requiredWei === 0n) {
|
|
103
|
+
emitErr("create-image", "INVALID_PAYMENT_AMOUNT", {
|
|
104
|
+
message: "Server returned invalid payment amount (0). Please retry later.",
|
|
105
|
+
appId,
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (allowance >= requiredWei) {
|
|
110
|
+
logInfo("Allowance sufficient, no approve needed.");
|
|
111
|
+
} else {
|
|
112
|
+
logInfo(`Allowance ${allowance} < required ${requiredWei}; approve needed.`);
|
|
113
|
+
if (bnbRaw === 0n) {
|
|
114
|
+
needGas = true;
|
|
115
|
+
logInfo("No BNB for approve gas, will request BNB transfer.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (usdtNum < requiredUsdt) {
|
|
120
|
+
needTopup = true;
|
|
121
|
+
const shortfall = requiredUsdt - usdtNum;
|
|
122
|
+
const minTopup = Math.max(MIN_TOPUP_USDT, Math.ceil(shortfall));
|
|
123
|
+
logInfo(`USDT insufficient: have ${usdtNum}, need ${requiredUsdt}, shortfall ${shortfall.toFixed(6)} (top-up minimum: ${minTopup} USDT)`);
|
|
124
|
+
|
|
125
|
+
if (opts.topupAmount != null && String(opts.topupAmount).trim() !== "") {
|
|
126
|
+
const amt = Number(opts.topupAmount);
|
|
127
|
+
if (!Number.isFinite(amt) || amt <= 0) {
|
|
128
|
+
emitErr("create-image", "AMOUNT_INVALID", {
|
|
129
|
+
message: `Invalid --topup-amount: ${opts.topupAmount}`,
|
|
130
|
+
appId,
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (amt < minTopup) {
|
|
135
|
+
emitErr("create-image", "TOPUP_AMOUNT_TOO_SMALL", {
|
|
136
|
+
message: `--topup-amount ${amt} USDT is below the ${minTopup} USDT minimum for this call.`,
|
|
137
|
+
minTopup,
|
|
138
|
+
appId,
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
topupAmount = String(opts.topupAmount);
|
|
143
|
+
logInfo(`Using --topup-amount: ${topupAmount} USDT`);
|
|
144
|
+
} else if (process.stdin.isTTY) {
|
|
145
|
+
topupAmount = await promptTopupAmount(minTopup);
|
|
146
|
+
logInfo(`Selected top-up amount: ${topupAmount} USDT`);
|
|
147
|
+
} else {
|
|
148
|
+
const presets = TOPUP_PRESETS.filter((v) => v >= minTopup);
|
|
149
|
+
emitErr("create-image", "TOPUP_REQUIRED", {
|
|
150
|
+
message: `USDT balance is below the ${minTopup} USDT minimum for this call. Choose a top-up amount and rerun with --topup-amount <usdt>.`,
|
|
151
|
+
minTopup,
|
|
152
|
+
required: requiredUsdt,
|
|
153
|
+
currentBalance: balanceInitialUsdt,
|
|
154
|
+
address: sessionAddress,
|
|
155
|
+
appId,
|
|
156
|
+
presets,
|
|
157
|
+
hint: `Rerun: aigateway wallet-topup --amount <usdt> --app-id ${appId}`,
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
emitErr("create-image", "BALANCE_CHECK_FAILED", {
|
|
164
|
+
message: `Balance check failed: ${e.message}`,
|
|
165
|
+
appId,
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (needTopup || needGas) {
|
|
171
|
+
logInfo("Funding flow triggered...");
|
|
172
|
+
try {
|
|
173
|
+
await fundSessionKey({
|
|
174
|
+
sessionAddress,
|
|
175
|
+
usdtAmount: needTopup ? topupAmount : null,
|
|
176
|
+
needGas,
|
|
177
|
+
});
|
|
178
|
+
} catch (e) {
|
|
179
|
+
if (e instanceof WalletConnectError) {
|
|
180
|
+
emitErr("create-image", e.code, { message: e.message, address: sessionAddress, appId });
|
|
181
|
+
} else {
|
|
182
|
+
emitErr("create-image", "FUNDING_FAILED", { message: e.message, address: sessionAddress, appId });
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
logInfo("Re-checking wallet balance...");
|
|
188
|
+
try {
|
|
189
|
+
const { usdt, bnbRaw } = await getWalletBalance(privateKey);
|
|
190
|
+
balanceBeforeChargeUsdt = usdt;
|
|
191
|
+
const usdtNum = parseFloat(usdt);
|
|
192
|
+
if (needGas && bnbRaw === 0n) {
|
|
193
|
+
emitErr("create-image", "INSUFFICIENT_BNB", {
|
|
194
|
+
message: "No BNB for approve transaction after funding. Run 'aigateway wallet-gas' to add BNB manually.",
|
|
195
|
+
address: sessionAddress,
|
|
196
|
+
appId,
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (usdtNum < requiredUsdt) {
|
|
201
|
+
emitErr("create-image", "INSUFFICIENT_USDT", {
|
|
202
|
+
message: "Still insufficient USDT after funding.",
|
|
203
|
+
required: `${requiredUsdt} USDT`,
|
|
204
|
+
available: `${usdt} USDT`,
|
|
205
|
+
address: sessionAddress,
|
|
206
|
+
appId,
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
emitErr("create-image", "BALANCE_CHECK_FAILED", {
|
|
212
|
+
message: `Balance re-check failed: ${e.message}`,
|
|
213
|
+
appId,
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const { client } = createX402Api(privateKey);
|
|
220
|
+
logInfo(`Submitting payment & request: ${url}`);
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const { x402HTTPClient } = await import("@aeon-ai-pay/core/client");
|
|
224
|
+
const httpClient = new x402HTTPClient(client);
|
|
225
|
+
|
|
226
|
+
const raw402 = paymentReq.raw402Response;
|
|
227
|
+
const getHeader = (name) => {
|
|
228
|
+
const value = raw402.headers[name] ?? raw402.headers[name.toLowerCase()];
|
|
229
|
+
return typeof value === "string" ? value : undefined;
|
|
230
|
+
};
|
|
231
|
+
const paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, raw402.data);
|
|
232
|
+
const paymentPayload = await client.createPaymentPayload(paymentRequired);
|
|
233
|
+
const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
234
|
+
|
|
235
|
+
const response = await axios.get(url, {
|
|
236
|
+
headers: {
|
|
237
|
+
...paymentHeaders,
|
|
238
|
+
"Access-Control-Expose-Headers": "PAYMENT-RESPONSE",
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
const paymentResponse = decodePaymentResponse(response.headers);
|
|
242
|
+
|
|
243
|
+
const transaction = response.data?.transaction || paymentResponse?.txHash || null;
|
|
244
|
+
const images = Array.isArray(response.data?.data?.images) ? response.data.data.images : [];
|
|
245
|
+
|
|
246
|
+
const outputDir = opts.output || DEFAULT_IMAGE_DIR;
|
|
247
|
+
const downloaded = [];
|
|
248
|
+
if (images.length > 0) {
|
|
249
|
+
mkdirSync(outputDir, { recursive: true });
|
|
250
|
+
for (const img of images) {
|
|
251
|
+
const imgUrl = img?.url;
|
|
252
|
+
if (!imgUrl) continue;
|
|
253
|
+
try {
|
|
254
|
+
const localPath = await downloadImage(imgUrl, outputDir);
|
|
255
|
+
const meta = readImageMeta(localPath);
|
|
256
|
+
downloaded.push({
|
|
257
|
+
url: imgUrl,
|
|
258
|
+
localPath,
|
|
259
|
+
format: meta.format,
|
|
260
|
+
width: meta.width,
|
|
261
|
+
height: meta.height,
|
|
262
|
+
sizeBytes: meta.sizeBytes,
|
|
263
|
+
sizeHuman: meta.sizeHuman,
|
|
264
|
+
});
|
|
265
|
+
logInfo(`Saved: ${localPath} (${meta.format || "?"}, ${meta.width || "?"}×${meta.height || "?"}, ${meta.sizeHuman})`);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
logInfo(`Failed to download ${imgUrl}: ${e.message}`);
|
|
268
|
+
downloaded.push({ url: imgUrl, error: e.message });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let balanceAfterUsdt = null;
|
|
274
|
+
try {
|
|
275
|
+
const after = await getWalletBalance(privateKey);
|
|
276
|
+
balanceAfterUsdt = after.usdt;
|
|
277
|
+
} catch (e) {
|
|
278
|
+
logInfo(`Post-payment balance check failed: ${e.message}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const result = {
|
|
282
|
+
appId,
|
|
283
|
+
prompt,
|
|
284
|
+
aspectRatio,
|
|
285
|
+
outputFormat,
|
|
286
|
+
model,
|
|
287
|
+
transaction,
|
|
288
|
+
images: downloaded,
|
|
289
|
+
balance: {
|
|
290
|
+
initial: balanceInitialUsdt,
|
|
291
|
+
before: balanceBeforeChargeUsdt,
|
|
292
|
+
after: balanceAfterUsdt,
|
|
293
|
+
charged: requiredUsdt,
|
|
294
|
+
topup: topupAmount,
|
|
295
|
+
},
|
|
296
|
+
data: response.data,
|
|
297
|
+
paymentResponse,
|
|
298
|
+
};
|
|
299
|
+
emitOk("create-image", result, { success: true, ...result });
|
|
300
|
+
} catch (error) {
|
|
301
|
+
emitErr("create-image", "PAYMENT_FAILED", {
|
|
302
|
+
message: error.message,
|
|
303
|
+
status: error.response?.status,
|
|
304
|
+
data: error.response?.data,
|
|
305
|
+
appId,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function readImageMeta(filePath) {
|
|
311
|
+
const sizeBytes = statSync(filePath).size;
|
|
312
|
+
const sizeHuman = humanSize(sizeBytes);
|
|
313
|
+
let format = null, width = null, height = null;
|
|
314
|
+
const fd = openSync(filePath, "r");
|
|
315
|
+
try {
|
|
316
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
317
|
+
const len = readSync(fd, buf, 0, buf.length, 0);
|
|
318
|
+
if (len >= 24 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4E && buf[3] === 0x47) {
|
|
319
|
+
format = "png";
|
|
320
|
+
width = buf.readUInt32BE(16);
|
|
321
|
+
height = buf.readUInt32BE(20);
|
|
322
|
+
} else if (len >= 4 && buf[0] === 0xFF && buf[1] === 0xD8 && buf[2] === 0xFF) {
|
|
323
|
+
format = "jpeg";
|
|
324
|
+
let i = 2;
|
|
325
|
+
while (i + 9 < len) {
|
|
326
|
+
if (buf[i] !== 0xFF) { i++; continue; }
|
|
327
|
+
while (i < len && buf[i] === 0xFF) i++;
|
|
328
|
+
const marker = buf[i];
|
|
329
|
+
i++;
|
|
330
|
+
if (marker === 0xD8 || marker === 0xD9) continue;
|
|
331
|
+
const segLen = buf.readUInt16BE(i);
|
|
332
|
+
if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) {
|
|
333
|
+
height = buf.readUInt16BE(i + 3);
|
|
334
|
+
width = buf.readUInt16BE(i + 5);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
i += segLen;
|
|
338
|
+
}
|
|
339
|
+
} else if (len >= 30 && buf.slice(0, 4).toString("ascii") === "RIFF" && buf.slice(8, 12).toString("ascii") === "WEBP") {
|
|
340
|
+
format = "webp";
|
|
341
|
+
const fourCC = buf.slice(12, 16).toString("ascii");
|
|
342
|
+
if (fourCC === "VP8 ") {
|
|
343
|
+
width = buf.readUInt16LE(26) & 0x3FFF;
|
|
344
|
+
height = buf.readUInt16LE(28) & 0x3FFF;
|
|
345
|
+
} else if (fourCC === "VP8L") {
|
|
346
|
+
const b0 = buf[21], b1 = buf[22], b2 = buf[23], b3 = buf[24];
|
|
347
|
+
width = ((b1 & 0x3F) << 8 | b0) + 1;
|
|
348
|
+
height = ((b3 & 0x0F) << 10 | b2 << 2 | (b1 & 0xC0) >> 6) + 1;
|
|
349
|
+
} else if (fourCC === "VP8X") {
|
|
350
|
+
width = (buf[24] | (buf[25] << 8) | (buf[26] << 16)) + 1;
|
|
351
|
+
height = (buf[27] | (buf[28] << 8) | (buf[29] << 16)) + 1;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} catch {
|
|
355
|
+
// leave null on parse failure
|
|
356
|
+
} finally {
|
|
357
|
+
closeSync(fd);
|
|
358
|
+
}
|
|
359
|
+
return { format, width, height, sizeBytes, sizeHuman };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function humanSize(bytes) {
|
|
363
|
+
if (!Number.isFinite(bytes)) return "?";
|
|
364
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
365
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
366
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
367
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function downloadImage(imgUrl, outputDir, { maxRedirects = 5, timeoutMs = 60_000 } = {}) {
|
|
371
|
+
let filename;
|
|
372
|
+
try {
|
|
373
|
+
filename = basename(new URL(imgUrl).pathname) || `image-${Date.now()}.png`;
|
|
374
|
+
} catch {
|
|
375
|
+
filename = `image-${Date.now()}.png`;
|
|
376
|
+
}
|
|
377
|
+
if (!extname(filename)) filename += ".png";
|
|
378
|
+
|
|
379
|
+
let target = join(outputDir, filename);
|
|
380
|
+
if (existsSync(target)) {
|
|
381
|
+
const ext = extname(filename);
|
|
382
|
+
const stem = filename.slice(0, filename.length - ext.length);
|
|
383
|
+
let i = 1;
|
|
384
|
+
while (existsSync(join(outputDir, `${stem}-${i}${ext}`))) i++;
|
|
385
|
+
target = join(outputDir, `${stem}-${i}${ext}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return new Promise((resolve, reject) => {
|
|
389
|
+
const fetchOnce = (currentUrl, redirectsLeft) => {
|
|
390
|
+
let parsed;
|
|
391
|
+
try {
|
|
392
|
+
parsed = new URL(currentUrl);
|
|
393
|
+
} catch (e) {
|
|
394
|
+
return reject(new Error(`Invalid URL: ${currentUrl}`));
|
|
395
|
+
}
|
|
396
|
+
const httpModule = parsed.protocol === "http:" ? httpGet : httpsGet;
|
|
397
|
+
const req = httpModule(currentUrl, { timeout: timeoutMs }, (res) => {
|
|
398
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
399
|
+
res.resume();
|
|
400
|
+
if (redirectsLeft <= 0) return reject(new Error("Too many redirects"));
|
|
401
|
+
const nextUrl = new URL(res.headers.location, currentUrl).toString();
|
|
402
|
+
return fetchOnce(nextUrl, redirectsLeft - 1);
|
|
403
|
+
}
|
|
404
|
+
if (res.statusCode !== 200) {
|
|
405
|
+
res.resume();
|
|
406
|
+
return reject(new Error(`HTTP ${res.statusCode} from ${currentUrl}`));
|
|
407
|
+
}
|
|
408
|
+
const file = createWriteStream(target);
|
|
409
|
+
res.pipe(file);
|
|
410
|
+
file.on("finish", () => file.close(() => resolve(target)));
|
|
411
|
+
file.on("error", (err) => {
|
|
412
|
+
try { unlinkSync(target); } catch {}
|
|
413
|
+
reject(err);
|
|
414
|
+
});
|
|
415
|
+
res.on("error", (err) => {
|
|
416
|
+
file.destroy();
|
|
417
|
+
try { unlinkSync(target); } catch {}
|
|
418
|
+
reject(err);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
req.on("error", reject);
|
|
422
|
+
req.on("timeout", () => {
|
|
423
|
+
req.destroy(new Error(`Download timed out after ${timeoutMs}ms`));
|
|
424
|
+
});
|
|
425
|
+
};
|
|
426
|
+
fetchOnce(imgUrl, maxRedirects);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { resolve, loadConfig } from "../config.mjs";
|
|
2
|
+
import { getWalletBalance, getBalanceByAddress } from "../balance.mjs";
|
|
3
|
+
import { emitOk, emitErr, logInfo } from "../output.mjs";
|
|
4
|
+
|
|
5
|
+
export async function wallet(opts) {
|
|
6
|
+
const privateKey = resolve(opts.privateKey, "EVM_PRIVATE_KEY", "privateKey");
|
|
7
|
+
const { appId } = opts;
|
|
8
|
+
|
|
9
|
+
if (!privateKey) {
|
|
10
|
+
emitErr("wallet-balance", "WALLET_NOT_CONFIGURED", { appId });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
const { address, usdt, bnb, usdtRaw } = await getWalletBalance(privateKey);
|
|
17
|
+
|
|
18
|
+
const result = {
|
|
19
|
+
appId,
|
|
20
|
+
mode: config.mode || "private-key",
|
|
21
|
+
address,
|
|
22
|
+
usdt,
|
|
23
|
+
bnb,
|
|
24
|
+
network: "BSC Mainnet (Chain ID: 56)",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (config.mainWallet) {
|
|
28
|
+
try {
|
|
29
|
+
const mainBal = await getBalanceByAddress(config.mainWallet);
|
|
30
|
+
result.mainWallet = {
|
|
31
|
+
address: config.mainWallet,
|
|
32
|
+
usdt: mainBal.usdt,
|
|
33
|
+
};
|
|
34
|
+
} catch {
|
|
35
|
+
result.mainWallet = { address: config.mainWallet, error: "Failed to query balance" };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
emitOk("wallet-balance", result, result);
|
|
40
|
+
|
|
41
|
+
if (usdtRaw === 0n) {
|
|
42
|
+
logInfo("Warning: No USDT balance. Run 'aigateway wallet-topup --amount <usdt>' to add funds.");
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
emitErr("wallet-balance", "BALANCE_CHECK_FAILED", { message: error.message, appId });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gas 命令:通过 WalletConnect 从主钱包向本地钱包转 BNB(withdraw 时支付 gas)
|
|
3
|
+
*/
|
|
4
|
+
import { loadConfig } from "../config.mjs";
|
|
5
|
+
import { getBalanceByAddress } from "../balance.mjs";
|
|
6
|
+
import {
|
|
7
|
+
withWallet,
|
|
8
|
+
requestNativeTransfer,
|
|
9
|
+
setStatus,
|
|
10
|
+
WalletConnectError,
|
|
11
|
+
} from "../walletconnect.mjs";
|
|
12
|
+
import { BSC_RPC_URL } from "../constants.mjs";
|
|
13
|
+
import { emitOk, emitErr, logInfo } from "../output.mjs";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_GAS_AMOUNT = "0.001";
|
|
16
|
+
|
|
17
|
+
export async function gas(opts) {
|
|
18
|
+
const config = loadConfig();
|
|
19
|
+
const { appId } = opts;
|
|
20
|
+
|
|
21
|
+
if (!config.privateKey || !config.address) {
|
|
22
|
+
emitErr("wallet-gas", "WALLET_NOT_CONFIGURED", {
|
|
23
|
+
message: "No local wallet found. Run 'aigateway wallet-init' first to auto-create one.",
|
|
24
|
+
appId,
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const amount = opts.amount || DEFAULT_GAS_AMOUNT;
|
|
30
|
+
const sessionAddress = config.address;
|
|
31
|
+
logInfo(`Local wallet: ${sessionAddress}`);
|
|
32
|
+
logInfo(`App ID: ${appId}`);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const bal = await getBalanceByAddress(sessionAddress);
|
|
36
|
+
logInfo(`Current balance: ${bal.bnb} BNB`);
|
|
37
|
+
} catch {}
|
|
38
|
+
|
|
39
|
+
let bnbTxHash = null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await withWallet({ amount, token: "BNB" }, async ({ signClient, session, peerAddress }) => {
|
|
43
|
+
const { createPublicClient, http } = await import("viem");
|
|
44
|
+
const { bsc } = await import("viem/chains");
|
|
45
|
+
const publicClient = createPublicClient({
|
|
46
|
+
chain: bsc,
|
|
47
|
+
transport: http(BSC_RPC_URL, { timeout: 15000, retryCount: 2 }),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
setStatus("signing", { amount, token: "BNB", to: sessionAddress });
|
|
51
|
+
logInfo(`\nRequesting BNB transfer: ${amount} BNB → ${sessionAddress}`);
|
|
52
|
+
logInfo("Please confirm the transaction in your wallet app...");
|
|
53
|
+
|
|
54
|
+
bnbTxHash = await requestNativeTransfer(signClient, session, {
|
|
55
|
+
from: peerAddress,
|
|
56
|
+
to: sessionAddress,
|
|
57
|
+
value: amount,
|
|
58
|
+
});
|
|
59
|
+
setStatus("tx_submitted", { txHash: bnbTxHash, amount, token: "BNB" });
|
|
60
|
+
logInfo(`BNB transfer submitted: ${bnbTxHash}`);
|
|
61
|
+
logInfo("Waiting for confirmation...");
|
|
62
|
+
|
|
63
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
64
|
+
hash: bnbTxHash,
|
|
65
|
+
timeout: 60_000,
|
|
66
|
+
});
|
|
67
|
+
if (receipt.status !== "success") {
|
|
68
|
+
throw new Error("BNB transfer transaction reverted");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setStatus("confirmed", { txHash: bnbTxHash, amount, token: "BNB" });
|
|
72
|
+
logInfo("BNB transfer confirmed.");
|
|
73
|
+
});
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (e instanceof WalletConnectError) {
|
|
76
|
+
emitErr("wallet-gas", e.code, { message: e.message, appId });
|
|
77
|
+
} else {
|
|
78
|
+
emitErr("wallet-gas", "INTERNAL_ERROR", { message: e.message, appId });
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let finalBalance;
|
|
84
|
+
try {
|
|
85
|
+
finalBalance = await getBalanceByAddress(sessionAddress);
|
|
86
|
+
} catch {
|
|
87
|
+
finalBalance = { bnb: "unknown" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const data = {
|
|
91
|
+
appId,
|
|
92
|
+
localWallet: {
|
|
93
|
+
address: sessionAddress,
|
|
94
|
+
bnb: finalBalance.bnb,
|
|
95
|
+
},
|
|
96
|
+
transaction: bnbTxHash,
|
|
97
|
+
};
|
|
98
|
+
emitOk("wallet-gas", data, { success: true, ...data });
|
|
99
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init-wallet:本地 session 钱包 check / 创建
|
|
3
|
+
*
|
|
4
|
+
* 纯本地操作 —— 不上链、不扫码、不花钱。
|
|
5
|
+
* - 如果 ~/.aigateway/config.json 没有 privateKey → 用 viem.generatePrivateKey() 生成一对
|
|
6
|
+
* - 返回当前钱包就绪状态(ready / created / address / amountLimits / ...)
|
|
7
|
+
*
|
|
8
|
+
* Agent 任何入口的第一步都应该先跑这个确认环境就绪。再之后才是 topup(充值)/ create-* 等需要钱的命令。
|
|
9
|
+
*/
|
|
10
|
+
import { loadConfig, saveConfig } from "../config.mjs";
|
|
11
|
+
import { MIN_AMOUNT, MAX_AMOUNT } from "../constants.mjs";
|
|
12
|
+
import { emitOk } from "../output.mjs";
|
|
13
|
+
|
|
14
|
+
export async function initWallet(opts) {
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
const { appId } = opts;
|
|
17
|
+
let created = false;
|
|
18
|
+
|
|
19
|
+
if (!config.privateKey) {
|
|
20
|
+
const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
|
|
21
|
+
const newKey = generatePrivateKey();
|
|
22
|
+
const account = privateKeyToAccount(newKey);
|
|
23
|
+
config.privateKey = newKey;
|
|
24
|
+
config.address = account.address;
|
|
25
|
+
config.mode = "private-key";
|
|
26
|
+
created = true;
|
|
27
|
+
saveConfig(config);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ready = !!(config.serviceUrl && config.privateKey);
|
|
31
|
+
const data = {
|
|
32
|
+
ready,
|
|
33
|
+
created,
|
|
34
|
+
appId,
|
|
35
|
+
mode: config.mode || null,
|
|
36
|
+
address: config.address || null,
|
|
37
|
+
mainWallet: config.mainWallet || null,
|
|
38
|
+
serviceUrl: config.serviceUrl || null,
|
|
39
|
+
amountLimits: { min: MIN_AMOUNT, max: MAX_AMOUNT },
|
|
40
|
+
};
|
|
41
|
+
emitOk("wallet-init", data, data);
|
|
42
|
+
}
|