@aeon-ai-pay/aigateway 0.1.4 → 0.1.5
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 +26 -8
- package/bin/cli.mjs +3 -2
- package/package.json +1 -1
- package/scripts/postinstall.mjs +6 -6
- package/skills/aigateway/SKILL.md +9 -14
- package/src/balance.mjs +7 -7
- package/src/commands/clean.mjs +5 -5
- package/src/commands/create-card.mjs +7 -7
- package/src/commands/create-image.mjs +5 -5
- package/src/commands/wallet-gas.mjs +2 -1
- package/src/commands/wallet-init.mjs +20 -19
- package/src/commands/wallet-topup.mjs +15 -15
- package/src/commands/wallet-withdraw.mjs +7 -7
- package/src/config.mjs +6 -5
- package/src/error-codes.mjs +11 -11
- package/src/output.mjs +19 -16
- package/src/sanitize.mjs +11 -11
- package/src/update-check.mjs +11 -11
- package/src/walletconnect.mjs +65 -63
- package/src/x402.mjs +13 -10
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.5] — 2026-05-19
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Removed all Chinese-language content from source files**. All comments,
|
|
14
|
+
docstrings, log messages, and SKILL.md guidance are now English-only.
|
|
15
|
+
`normalizeWalletError` no longer matches CJK error strings from localised
|
|
16
|
+
wallet apps; those fall through to the generic `WALLET_ERROR` code.
|
|
17
|
+
- SKILL.md's "Wording Discipline" section is now language-neutral: instead
|
|
18
|
+
of listing Chinese phrasings, it instructs translators to keep the "wallet
|
|
19
|
+
top-up" verb / noun lexically distinct from the "card face value" verb /
|
|
20
|
+
noun in every target language.
|
|
21
|
+
|
|
22
|
+
### Removed
|
|
23
|
+
- `test/create-logic.test.mjs` — stale unit tests inherited from `aicard`
|
|
24
|
+
that referenced internal functions removed during the merge (e.g.
|
|
25
|
+
`inlineWalletConnectTopup`, now replaced by `funding.mjs::fundSessionKey`).
|
|
26
|
+
The file was never published to npm (`test/` is not in `files`).
|
|
27
|
+
|
|
10
28
|
## [0.1.4] — 2026-05-19
|
|
11
29
|
|
|
12
30
|
### Added
|
|
@@ -44,14 +62,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
44
62
|
## [0.1.2] — 2026-05-19
|
|
45
63
|
|
|
46
64
|
### Changed
|
|
47
|
-
- **SKILL.md wording discipline**: `wallet-topup`
|
|
48
|
-
|
|
49
|
-
**into your session wallet**"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
- **SKILL.md wording discipline**: the `wallet-topup` and `create-card` prompts
|
|
66
|
+
now enforce strict lexical separation. Step 2 (`wallet-topup`) says "load USDT
|
|
67
|
+
**into your session wallet**"; Step 3a (`create-card`) says "card
|
|
68
|
+
**face value**" / "issue a card with how much". When translating to any
|
|
69
|
+
non-English language, the verb / noun for each concept must remain distinct
|
|
70
|
+
so users cannot conflate them.
|
|
71
|
+
- New "Wording Discipline" section in SKILL.md `## Copy Constraints` to lock
|
|
72
|
+
this in.
|
|
55
73
|
|
|
56
74
|
## [0.1.1] — 2026-05-19
|
|
57
75
|
|
package/bin/cli.mjs
CHANGED
|
@@ -7,8 +7,9 @@ if (major < 25) {
|
|
|
7
7
|
process.exit(1);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
// WalletConnect v2 SDK
|
|
11
|
-
//
|
|
10
|
+
// Known WalletConnect v2 SDK quirk: the relay occasionally emits null WebSocket frames,
|
|
11
|
+
// causing `'id' in null` inside isJsonRpcPayload to throw a TypeError. It does not
|
|
12
|
+
// affect business logic, so silently ignore it.
|
|
12
13
|
process.on("uncaughtException", (err) => {
|
|
13
14
|
if (
|
|
14
15
|
err instanceof TypeError &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aeon-ai-pay/aigateway",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "AI Agents discover, invoke, and settle paid LLMs, APIs, and Skills — starting with Skill Boss. No manual key setup. No prepayment. Pay-per-call via x402 or Agent Card.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* npm install -g
|
|
4
|
+
* After `npm install -g`, automatically install the skill into every detected AI coding tool.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Prefer `npx skills add` (Vercel Labs) so the skill is registered across all tools at once.
|
|
7
|
+
* If that fails, fall back to manually copying the skill into the Claude Code skills directory.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { cpSync, existsSync, mkdirSync } from 'node:fs';
|
|
@@ -20,7 +20,7 @@ if (!existsSync(skillSrc)) {
|
|
|
20
20
|
process.exit(0);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// Try installing into every tool via the skills CLI
|
|
24
24
|
try {
|
|
25
25
|
execFileSync('npx', ['skills', 'add', skillSrc, '-g', '-y', '--copy'], {
|
|
26
26
|
stdio: 'inherit',
|
|
@@ -30,10 +30,10 @@ try {
|
|
|
30
30
|
console.log('✔ aigateway skill installed via skills CLI (all detected tools)');
|
|
31
31
|
process.exit(0);
|
|
32
32
|
} catch {
|
|
33
|
-
// skills CLI
|
|
33
|
+
// skills CLI unavailable or failed — fall back
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// Fallback:
|
|
36
|
+
// Fallback: copy manually into Claude Code
|
|
37
37
|
const dest = join(homedir(), '.claude', 'skills', 'aigateway');
|
|
38
38
|
mkdirSync(dirname(dest), { recursive: true });
|
|
39
39
|
cpSync(skillSrc, dest, { recursive: true, force: true });
|
|
@@ -16,7 +16,7 @@ description: >
|
|
|
16
16
|
emoji: "🛰️"
|
|
17
17
|
homepage: https://github.com/AEON-Project/aigateway
|
|
18
18
|
metadata:
|
|
19
|
-
version: "0.1.
|
|
19
|
+
version: "0.1.5"
|
|
20
20
|
author: AEON-Project
|
|
21
21
|
openclaw:
|
|
22
22
|
requires:
|
|
@@ -163,12 +163,10 @@ Trigger: `wallet-init` envelope reports `needsTopup: true` (any of `no_prior_fun
|
|
|
163
163
|
This is a **session-wallet top-up** in USDT (NOT card face value). Make the wording unambiguously about the wallet to avoid the user confusing it with the card face value asked in Step 3a.
|
|
164
164
|
|
|
165
165
|
- Presets: **5 / 10 / 20 / 50 USDT**. Custom amounts must be ≥ 5 USDT.
|
|
166
|
-
- Ask the user **before** running the command. **Make
|
|
166
|
+
- Ask the user **before** running the command. **Make the noun "wallet" explicit in the question** (when translating to other languages, the translated noun must also unambiguously mean *session wallet*, never *card*):
|
|
167
167
|
|
|
168
168
|
> How much USDT would you like to load **into your session wallet**? (presets: 5 / 10 / 20 / 50, or any custom amount ≥ 5)
|
|
169
169
|
|
|
170
|
-
(Suggested Chinese phrasing: "请问要往**本地钱包**充值多少 USDT?预设 5 / 10 / 20 / 50,或自定义 ≥ 5。")
|
|
171
|
-
|
|
172
170
|
- Once the user picks an amount, run:
|
|
173
171
|
|
|
174
172
|
```bash
|
|
@@ -211,13 +209,10 @@ Trigger: user wants to **buy / create / get a virtual card** *and* Step 1 envelo
|
|
|
211
209
|
|
|
212
210
|
### Amount confirmation
|
|
213
211
|
|
|
214
|
-
This is the **card face value** the user wants to issue (NOT a wallet top-up). Amount must be in `amountLimits.min ~ amountLimits.max` (from Step 1; never hardcode). If user did not specify, ask once —
|
|
212
|
+
This is the **card face value** the user wants to issue (NOT a wallet top-up). Amount must be in `amountLimits.min ~ amountLimits.max` (from Step 1; never hardcode). If the user did not specify a value, ask once — use the phrase **"card face value"** (or in any non-English language, the equivalent for *the amount loaded onto a new card*), never the verb used for *wallet top-up*:
|
|
215
213
|
|
|
216
214
|
> What card face value would you like to issue? Allowed range: ${min}~${max} USD.
|
|
217
215
|
|
|
218
|
-
(Suggested Chinese phrasing: "请问要开多少美元的卡?允许范围 $${min} ~ $${max}。"
|
|
219
|
-
**Do not** translate this as "充值多少美元" — that wording belongs to `wallet-topup` and confuses users.)
|
|
220
|
-
|
|
221
216
|
Once specified, **execute immediately**.
|
|
222
217
|
|
|
223
218
|
### Execute
|
|
@@ -460,11 +455,11 @@ Address rendering: always `0x0...{last4}` (first 3 + ellipsis + last 4 chars).
|
|
|
460
455
|
|
|
461
456
|
### Wording Discipline: "wallet top-up" vs "card face value"
|
|
462
457
|
|
|
463
|
-
These two amounts are **different concepts** asked at **different steps**. Translators must keep them lexically distinct so users
|
|
458
|
+
These two amounts are **different concepts** asked at **different steps**. Translators (to any non-English language) must keep them **lexically distinct** so users do not conflate them:
|
|
464
459
|
|
|
465
|
-
| Step | Concept | Required
|
|
466
|
-
| --- | --- | --- | --- |
|
|
467
|
-
| Step 2 (`wallet-topup`) | USDT into the **session wallet** | "load USDT **into your session wallet**" |
|
|
468
|
-
| Step 3a (`create-card`) | USD **face value** loaded onto a new card | "card **face value**" / "issue a card with how much" |
|
|
460
|
+
| Step | Concept | Required English wording | Forbidden mix-ups |
|
|
461
|
+
| --- | --- | --- | --- |
|
|
462
|
+
| Step 2 (`wallet-topup`) | USDT into the **session wallet** | "load USDT **into your session wallet**" | Never say "load onto the card" — that's Step 3a |
|
|
463
|
+
| Step 3a (`create-card`) | USD **face value** loaded onto a new card | "card **face value**" / "issue a card with how much" | Never use the same verb you used for *wallet top-up* without a strong "card" qualifier |
|
|
469
464
|
|
|
470
|
-
**Rule of thumb**: if
|
|
465
|
+
**Rule of thumb**: if a question is about the *wallet* (Step 2), the target noun must mean *session wallet*. If a question is about a *card amount* (Step 3a), prefer wording that means *face value* / *the amount loaded onto a new card*. The verb for one concept must never be reused for the other concept in the same language.
|
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/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 });
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* create-card
|
|
2
|
+
* create-card: issue a one-time virtual card by paying with USDT on BSC over the x402 protocol.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Server endpoint: GET {serviceUrl}/open/ai/x402/card/create?amount=<usd>&appId=<merchant>
|
|
5
|
+
* Flow: fetch payment requirements -> check balance + allowance
|
|
6
|
+
* -> (if balance is insufficient) top up via funding.mjs/fundSessionKey
|
|
7
|
+
* -> submit x402 EIP-712 signature -> optionally poll status
|
|
8
8
|
*/
|
|
9
9
|
import { createX402Api, decodePaymentResponse, fetchPaymentRequirements } from "../x402.mjs";
|
|
10
10
|
import { resolve } from "../config.mjs";
|
|
@@ -163,7 +163,7 @@ export async function createCard(opts) {
|
|
|
163
163
|
return;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
// Dry-run
|
|
166
|
+
// Dry-run: exit after preflight checks complete
|
|
167
167
|
if (dryRun) {
|
|
168
168
|
const preview = {
|
|
169
169
|
dryRun: true,
|
|
@@ -191,7 +191,7 @@ export async function createCard(opts) {
|
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
// WalletConnect
|
|
194
|
+
// WalletConnect top-up
|
|
195
195
|
if (needTopup || needGas) {
|
|
196
196
|
logInfo("Funding flow triggered...");
|
|
197
197
|
try {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* create-image
|
|
2
|
+
* create-image: generate an AI image via Skill Boss using the x402 protocol.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Server endpoint: GET {serviceUrl}/open/ai/x402/skillBoss/create?body=<urlencoded-json>&appId=<merchant>
|
|
5
|
+
* Flow: fetch payment requirements -> check balance + allowance
|
|
6
|
+
* -> (if balance is insufficient) top up via funding.mjs/fundSessionKey
|
|
7
|
+
* -> submit x402 EIP-712 signature -> download the image locally
|
|
8
8
|
*/
|
|
9
9
|
import { createX402Api, decodePaymentResponse, fetchPaymentRequirements } from "../x402.mjs";
|
|
10
10
|
import { resolve } from "../config.mjs";
|
|
@@ -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,16 +1,16 @@
|
|
|
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 create-card / create-image
|
|
14
14
|
*/
|
|
15
15
|
import { loadConfig, saveConfig } from "../config.mjs";
|
|
16
16
|
import { MIN_AMOUNT, MAX_AMOUNT } from "../constants.mjs";
|
|
@@ -41,7 +41,7 @@ export async function initWallet(opts) {
|
|
|
41
41
|
logInfo(`Wallet: ${config.address}`);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
//
|
|
44
|
+
// On-chain status check
|
|
45
45
|
let usdt = "0";
|
|
46
46
|
let bnb = "0";
|
|
47
47
|
let usdtNum = 0;
|
|
@@ -50,7 +50,7 @@ export async function initWallet(opts) {
|
|
|
50
50
|
let chainCheckError = null;
|
|
51
51
|
|
|
52
52
|
if (created) {
|
|
53
|
-
//
|
|
53
|
+
// A freshly created wallet is guaranteed to be empty; skip the chain query to save ~500ms.
|
|
54
54
|
logInfo("Fresh wallet — skipping balance lookup (assumed empty).");
|
|
55
55
|
} else {
|
|
56
56
|
try {
|
|
@@ -68,18 +68,19 @@ export async function initWallet(opts) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
71
|
+
// Decision: needsTopup. Use only real on-chain state — do NOT depend on config.mainWallet.
|
|
72
|
+
// The previous logic `created || !config.mainWallet` was wrong: mainWallet is purely the default
|
|
73
|
+
// destination for withdraw. If USDT / allowance on-chain are sufficient — even when mainWallet
|
|
74
|
+
// is null (external CEX deposit / older versions that didn't record it) — paid calls should be
|
|
75
|
+
// allowed without forcing another wallet-topup round.
|
|
75
76
|
let needsTopup = false;
|
|
76
77
|
let topupReason = null;
|
|
77
78
|
if (created) {
|
|
78
|
-
//
|
|
79
|
+
// A freshly generated session key has no funds — no point querying the chain.
|
|
79
80
|
needsTopup = true;
|
|
80
81
|
topupReason = "first_time";
|
|
81
82
|
} else if (!chainCheckOk) {
|
|
82
|
-
//
|
|
83
|
+
// Chain probe failed — conservatively flag needsTopup so the user can decide what to do.
|
|
83
84
|
needsTopup = true;
|
|
84
85
|
topupReason = "chain_check_failed";
|
|
85
86
|
} else if (usdtNum < LOW_BALANCE_THRESHOLD) {
|
|
@@ -102,7 +103,7 @@ export async function initWallet(opts) {
|
|
|
102
103
|
bnb,
|
|
103
104
|
allowance: allowance.toString(),
|
|
104
105
|
needsTopup,
|
|
105
|
-
topupReason, // "
|
|
106
|
+
topupReason, // "first_time" | "low_balance" | "no_approve" | "chain_check_failed" | null
|
|
106
107
|
minTopup: MIN_TOPUP_USDT,
|
|
107
108
|
presets: TOPUP_PRESETS,
|
|
108
109
|
amountLimits: { min: MIN_AMOUNT, max: MAX_AMOUNT },
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* topup
|
|
2
|
+
* wallet-topup: top up the session wallet and run the one-time facilitator approve.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
* 4. session key
|
|
12
|
-
* 5.
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Verify the session key exists (created earlier by `aigateway wallet-init`).
|
|
6
|
+
* 2. Check USDT balance + facilitator allowance.
|
|
7
|
+
* 3. If USDT < LOW_BALANCE_THRESHOLD (1 USDT) or allowance == 0, open WalletConnect to fund:
|
|
8
|
+
* - Top-up amount: TTY mode picks interactively from presets [5, 10, 20, 50] or a custom value;
|
|
9
|
+
* non-TTY mode requires `--amount <usdt>`, otherwise TOPUP_REQUIRED is emitted.
|
|
10
|
+
* - 0.0003 BNB is transferred too when an approve transaction is needed.
|
|
11
|
+
* 4. The session key broadcasts ERC20.approve(facilitator, MaxUint256) once.
|
|
12
|
+
* 5. Re-query balance / allowance and return the final state.
|
|
13
13
|
*/
|
|
14
14
|
import { resolve } from "../config.mjs";
|
|
15
15
|
import { getWalletBalance, getAllowance } from "../balance.mjs";
|
|
@@ -71,7 +71,7 @@ export async function topup(opts) {
|
|
|
71
71
|
const needApprove = allowance === 0n;
|
|
72
72
|
const needGas = needApprove && bnbRaw === 0n;
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// Already prepared: balance is sufficient and facilitator is approved
|
|
75
75
|
if (!needTopup && !needApprove) {
|
|
76
76
|
logInfo("Wallet already prepared (balance ≥ minimum, facilitator approved).");
|
|
77
77
|
const data = {
|
|
@@ -89,7 +89,7 @@ export async function topup(opts) {
|
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
//
|
|
92
|
+
// Decide the top-up amount
|
|
93
93
|
let topupAmount = null;
|
|
94
94
|
if (needTopup) {
|
|
95
95
|
if (balanceLow) {
|
|
@@ -134,7 +134,7 @@ export async function topup(opts) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
// WalletConnect
|
|
137
|
+
// WalletConnect top-up (USDT + optional BNB for approve gas)
|
|
138
138
|
if (needTopup || needGas) {
|
|
139
139
|
const willTransfer = [];
|
|
140
140
|
if (needTopup) willTransfer.push(`${topupAmount} USDT`);
|
|
@@ -161,7 +161,7 @@ export async function topup(opts) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
//
|
|
164
|
+
// Session key broadcasts the one-time facilitator approve
|
|
165
165
|
let approveTx = null;
|
|
166
166
|
if (needApprove) {
|
|
167
167
|
let postBnbRaw = bnbRaw;
|
|
@@ -193,7 +193,7 @@ export async function topup(opts) {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
//
|
|
196
|
+
// Final balance + allowance re-check
|
|
197
197
|
let finalUsdt = usdt;
|
|
198
198
|
let finalBnb = bnb;
|
|
199
199
|
let finalAllowance = allowance;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* withdraw
|
|
2
|
+
* wallet-withdraw: move the session key's funds (USDT + BNB) back to the main wallet.
|
|
3
3
|
*/
|
|
4
4
|
import { createPublicClient, createWalletClient, http, parseUnits, formatUnits, encodeFunctionData } from "viem";
|
|
5
5
|
import { privateKeyToAccount } from "viem/accounts";
|
|
@@ -54,7 +54,7 @@ export async function withdraw(opts) {
|
|
|
54
54
|
|
|
55
55
|
const isWithdrawAll = !opts.amount;
|
|
56
56
|
|
|
57
|
-
//
|
|
57
|
+
// No funds at all
|
|
58
58
|
if (balance.usdtRaw === 0n && balance.bnbRaw === 0n) {
|
|
59
59
|
emitErr("wallet-withdraw", "NO_FUNDS", { message: "No funds to withdraw.", appId });
|
|
60
60
|
return;
|
|
@@ -63,9 +63,9 @@ export async function withdraw(opts) {
|
|
|
63
63
|
let usdtTxHash = null;
|
|
64
64
|
let bnbTxHash = null;
|
|
65
65
|
|
|
66
|
-
// 1.
|
|
66
|
+
// 1. Reclaim USDT (only when USDT balance > 0)
|
|
67
67
|
if (balance.usdtRaw > 0n) {
|
|
68
|
-
// USDT
|
|
68
|
+
// USDT transfer needs BNB for gas
|
|
69
69
|
if (balance.bnbRaw === 0n) {
|
|
70
70
|
emitErr("wallet-withdraw", "INSUFFICIENT_BNB", {
|
|
71
71
|
message: "No BNB for gas. Withdraw is a normal on-chain transfer and requires BNB to pay gas.",
|
|
@@ -119,7 +119,7 @@ export async function withdraw(opts) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// 2.
|
|
122
|
+
// 2. Reclaim remaining BNB (only when withdrawing everything)
|
|
123
123
|
if (isWithdrawAll) {
|
|
124
124
|
const freshBalance = balance.usdtRaw > 0n
|
|
125
125
|
? await getBalanceByAddress(sessionAddress)
|
|
@@ -128,7 +128,7 @@ export async function withdraw(opts) {
|
|
|
128
128
|
if (freshBalance.bnbRaw > 0n) {
|
|
129
129
|
try {
|
|
130
130
|
const gasPrice = await publicClient.getGasPrice();
|
|
131
|
-
//
|
|
131
|
+
// Reserve a 20% buffer to absorb gas-price fluctuations
|
|
132
132
|
const gasCost = BNB_TRANSFER_GAS * (gasPrice * 120n / 100n);
|
|
133
133
|
const sendable = freshBalance.bnbRaw - gasCost;
|
|
134
134
|
|
|
@@ -159,7 +159,7 @@ export async function withdraw(opts) {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
//
|
|
162
|
+
// Final balance lookup
|
|
163
163
|
let finalBalance;
|
|
164
164
|
try {
|
|
165
165
|
finalBalance = await getBalanceByAddress(sessionAddress);
|
package/src/config.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Config management: ~/.aigateway/config.json
|
|
3
|
+
* Resolution priority: CLI args > env vars > config.json
|
|
4
4
|
*
|
|
5
|
-
* AEON AI Gateway
|
|
6
|
-
*
|
|
5
|
+
* AEON AI Gateway uses a single x402 service (ai-api.aeon.xyz);
|
|
6
|
+
* different capabilities (virtual card / Skill Boss calls) share the host
|
|
7
|
+
* but use distinct path prefixes.
|
|
7
8
|
*/
|
|
8
9
|
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
|
|
9
10
|
import { join } from "path";
|
|
@@ -31,7 +32,7 @@ export function saveConfig(config) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
|
-
*
|
|
35
|
+
* Resolve a value with priority: cliValue > envKey > config[configKey]
|
|
35
36
|
*/
|
|
36
37
|
export function resolve(cliValue, envKey, configKey) {
|
|
37
38
|
if (cliValue) return cliValue;
|
package/src/error-codes.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Error-code registry — single source of truth shared by the CLI and the docs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 0
|
|
6
|
-
* 1
|
|
7
|
-
* 2
|
|
8
|
-
* 3
|
|
9
|
-
* 4
|
|
4
|
+
* Exit-code semantics:
|
|
5
|
+
* 0 success
|
|
6
|
+
* 1 user error (bad argument, insufficient balance, configuration, user reject)
|
|
7
|
+
* 2 timeout (polling, WalletConnect, signature, on-chain wait)
|
|
8
|
+
* 3 service / network
|
|
9
|
+
* 4 internal error
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export const ERROR_CODES = {
|
|
13
|
-
// =====
|
|
13
|
+
// ===== User error (exit 1) =====
|
|
14
14
|
WALLET_NOT_CONFIGURED: { exit: 1, message: "Wallet not configured. Run: aigateway wallet-init" },
|
|
15
15
|
SERVICE_URL_MISSING: { exit: 1, message: "Service URL not configured." },
|
|
16
16
|
AMOUNT_INVALID: { exit: 1, message: "Invalid amount." },
|
|
@@ -25,13 +25,13 @@ export const ERROR_CODES = {
|
|
|
25
25
|
TOPUP_AMOUNT_TOO_SMALL: { exit: 1, message: "Top-up amount is below the minimum." },
|
|
26
26
|
PAYMENT_REJECTED: { exit: 1, message: "Payment approval was rejected. Please try again if you'd like to proceed." },
|
|
27
27
|
|
|
28
|
-
// =====
|
|
28
|
+
// ===== Timeout (exit 2) =====
|
|
29
29
|
PAYMENT_TIMEOUT: { exit: 2, message: "Payment approval timed out. Please try again." },
|
|
30
30
|
WC_SESSION_EXPIRED: { exit: 2, message: "WalletConnect session expired." },
|
|
31
31
|
POLL_TIMEOUT: { exit: 2, message: "Polling timed out. Card may still be provisioning." },
|
|
32
32
|
TX_TIMEOUT: { exit: 2, message: "On-chain transaction timed out." },
|
|
33
33
|
|
|
34
|
-
// =====
|
|
34
|
+
// ===== Service / network (exit 3) =====
|
|
35
35
|
SERVICE_UNAVAILABLE: { exit: 3, message: "Service unavailable or network error." },
|
|
36
36
|
PAYMENT_FETCH_FAILED: { exit: 3, message: "Failed to fetch payment requirements." },
|
|
37
37
|
BALANCE_CHECK_FAILED: { exit: 3, message: "Failed to check balance." },
|
|
@@ -44,7 +44,7 @@ export const ERROR_CODES = {
|
|
|
44
44
|
IMAGE_DOWNLOAD_FAILED: { exit: 3, message: "Image download failed." },
|
|
45
45
|
FUNDING_FAILED: { exit: 3, message: "Funding flow failed." },
|
|
46
46
|
|
|
47
|
-
// =====
|
|
47
|
+
// ===== Internal (exit 4) =====
|
|
48
48
|
INTERNAL_ERROR: { exit: 4, message: "Internal error." },
|
|
49
49
|
WALLET_ERROR: { exit: 1, message: "Wallet operation failed." },
|
|
50
50
|
};
|
package/src/output.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Unified output envelope: JSON on stdout + tiered stderr logs.
|
|
3
3
|
*
|
|
4
|
-
* stdout:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* stderr:
|
|
4
|
+
* stdout: a single line of final JSON (machine-readable)
|
|
5
|
+
* - success: { ok: true, command, version, data }
|
|
6
|
+
* - failure: { ok: false, command, version, error: { code, message, ...context } }
|
|
7
|
+
* stderr: progress logs (human-readable; agents can ignore them)
|
|
8
8
|
*
|
|
9
|
-
* --legacy-output
|
|
9
|
+
* --legacy-output mode: emit the pre-envelope shape so scripts / agents that
|
|
10
|
+
* already parse the old JSON can migrate gradually.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { readFileSync } from "fs";
|
|
@@ -30,10 +31,11 @@ export function isLegacyMode() { return LEGACY_MODE; }
|
|
|
30
31
|
export function isVerboseMode() { return VERBOSE_MODE; }
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {object}
|
|
34
|
+
* Emit a success result. Callers should let the function return naturally
|
|
35
|
+
* (do not call process.exit afterwards).
|
|
36
|
+
* @param {string} command - command name, e.g. "create-card" / "wallet-init"
|
|
37
|
+
* @param {object} data - placed under envelope.data
|
|
38
|
+
* @param {object} [legacyShape] - the legacy-mode payload; if omitted, `data` is used
|
|
37
39
|
*/
|
|
38
40
|
export function emitOk(command, data, legacyShape) {
|
|
39
41
|
if (LEGACY_MODE) {
|
|
@@ -44,10 +46,11 @@ export function emitOk(command, data, legacyShape) {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
|
-
*
|
|
49
|
+
* Emit an error and exit with the corresponding exit code.
|
|
48
50
|
* @param {string} command
|
|
49
|
-
* @param {string} code - ERROR_CODES
|
|
50
|
-
* @param {object} [details] -
|
|
51
|
+
* @param {string} code - key of ERROR_CODES
|
|
52
|
+
* @param {object} [details] - extra fields. `message` overrides the default message;
|
|
53
|
+
* `legacy` fully replaces the output in legacy mode.
|
|
51
54
|
*/
|
|
52
55
|
export function emitErr(command, code, details = {}) {
|
|
53
56
|
const info = ERROR_CODES[code] || ERROR_CODES.INTERNAL_ERROR;
|
|
@@ -69,17 +72,17 @@ export function emitErr(command, code, details = {}) {
|
|
|
69
72
|
process.exit(exit);
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
/**
|
|
75
|
+
/** Progress log (suppressed in quiet mode) */
|
|
73
76
|
export function logInfo(msg) {
|
|
74
77
|
if (!QUIET_MODE) console.error(msg);
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
/**
|
|
80
|
+
/** Verbose log (only emitted in verbose mode) */
|
|
78
81
|
export function logVerbose(msg) {
|
|
79
82
|
if (VERBOSE_MODE && !QUIET_MODE) console.error(msg);
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
/**
|
|
85
|
+
/** Error log (still emitted in quiet mode) */
|
|
83
86
|
export function logError(msg) {
|
|
84
87
|
console.error(msg);
|
|
85
88
|
}
|
package/src/sanitize.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* CLI
|
|
2
|
+
* Card output sanitisation: redact sensitive card data (truncate the full PAN to its last 4 digits, drop CVV, drop expiry).
|
|
3
|
+
* The CLI emits JSON for an agent to parse; the agent then renders the product-specific template to the user.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// Fields whose value should be replaced with the last-4 representation
|
|
7
7
|
const CARD_NUMBER_KEYS = new Set([
|
|
8
8
|
"cardnumber", "cardno",
|
|
9
9
|
]);
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Fields that must be removed entirely
|
|
12
12
|
const REMOVE_KEYS = new Set([
|
|
13
13
|
"cvv", "cvv2", "cvc", "cvc2", "securitycode",
|
|
14
14
|
"expiry", "expirydate", "expiredate", "cardexpiry",
|
|
@@ -16,10 +16,10 @@ const REMOVE_KEYS = new Set([
|
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* Recursively sanitise an object:
|
|
20
|
+
* - cardNumber / cardNo → keep only the last four digits ("•••• 3398")
|
|
21
|
+
* - cvv / securityCode → drop
|
|
22
|
+
* - expiry / expireDate → drop
|
|
23
23
|
*/
|
|
24
24
|
export function sanitizeOutput(obj) {
|
|
25
25
|
if (obj === null || obj === undefined) return obj;
|
|
@@ -30,15 +30,15 @@ export function sanitizeOutput(obj) {
|
|
|
30
30
|
for (const [key, value] of Object.entries(obj)) {
|
|
31
31
|
const normalized = key.toLowerCase().replace(/[-_]/g, "");
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// Drop sensitive fields (CVV, expiry, etc.)
|
|
34
34
|
if (REMOVE_KEYS.has(normalized)) continue;
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Card number: only keep the last 4 digits
|
|
37
37
|
if (CARD_NUMBER_KEYS.has(normalized)) {
|
|
38
38
|
if (typeof value === "string" && value.length >= 4) {
|
|
39
39
|
result[key] = "•••• " + value.slice(-4);
|
|
40
40
|
}
|
|
41
|
-
// value
|
|
41
|
+
// when value is null, do not emit this field
|
|
42
42
|
continue;
|
|
43
43
|
}
|
|
44
44
|
|
package/src/update-check.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Auto version check + silent background upgrade.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
*
|
|
4
|
+
* Strategy:
|
|
5
|
+
* 1. Synchronously poll the npm registry (`npm view`) — print a notice when a newer version is found.
|
|
6
|
+
* 2. Spawn a detached background process to run `npm install -g`.
|
|
7
|
+
* 3. After install, run postinstall.mjs (which re-installs the skill into every detected tool via the skills CLI).
|
|
8
|
+
* Does not block the main process.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { execFileSync, spawn } from "node:child_process";
|
|
@@ -13,11 +13,11 @@ import { execFileSync, spawn } from "node:child_process";
|
|
|
13
13
|
const PKG_NAME = "@aeon-ai-pay/aigateway";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Called at CLI startup: synchronous version probe + detached upgrade.
|
|
17
17
|
* @param {string} currentVersion
|
|
18
18
|
*/
|
|
19
19
|
export function checkForUpdates(currentVersion) {
|
|
20
|
-
//
|
|
20
|
+
// Synchronous fast probe (short timeout so it does not block too long)
|
|
21
21
|
let latest;
|
|
22
22
|
try {
|
|
23
23
|
latest = execFileSync("npm", ["view", PKG_NAME, "version"], {
|
|
@@ -25,15 +25,15 @@ export function checkForUpdates(currentVersion) {
|
|
|
25
25
|
stdio: ["ignore", "pipe", "ignore"],
|
|
26
26
|
}).toString().trim();
|
|
27
27
|
} catch {
|
|
28
|
-
return; //
|
|
28
|
+
return; // network unavailable — silently skip
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
if (!latest || latest === currentVersion) return;
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// A newer version exists — print a notice
|
|
34
34
|
console.error(`[update] ${PKG_NAME} ${currentVersion} → ${latest}, upgrading in background...`);
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Run the upgrade in a detached child, writing the result to a log file
|
|
37
37
|
const script = `
|
|
38
38
|
const { execFileSync } = require("child_process");
|
|
39
39
|
const { join } = require("path");
|
package/src/walletconnect.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WalletConnect v2
|
|
3
|
-
*
|
|
2
|
+
* WalletConnect v2 wrapper.
|
|
3
|
+
* Connects to the user's wallet over WalletConnect and submits transactions.
|
|
4
4
|
*/
|
|
5
5
|
import { SignClient } from "@walletconnect/sign-client";
|
|
6
6
|
import { encodeFunctionData, parseUnits } from "viem";
|
|
@@ -13,8 +13,9 @@ import { WC_CONNECT_TIMEOUT_MS, ERC20_TRANSFER_ABI, DEFAULT_WC_PROJECT_ID } from
|
|
|
13
13
|
import { loadConfig, saveConfig } from "./config.mjs";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* WalletConnect
|
|
17
|
-
*
|
|
16
|
+
* Error type thrown by the WalletConnect flow, carrying a stable error code
|
|
17
|
+
* (PAYMENT_TIMEOUT / PAYMENT_REJECTED / WALLET_ERROR). The Commands layer
|
|
18
|
+
* catches it and forwards it via emitErr; no caller needs to look at the underlying details.
|
|
18
19
|
*/
|
|
19
20
|
export class WalletConnectError extends Error {
|
|
20
21
|
constructor(code, message) {
|
|
@@ -24,7 +25,7 @@ export class WalletConnectError extends Error {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
// ==============
|
|
28
|
+
// ============== Status server (polled by the browser-side QR page) ==============
|
|
28
29
|
|
|
29
30
|
let _status = { state: "waiting_scan" };
|
|
30
31
|
let _server = null;
|
|
@@ -65,14 +66,14 @@ export function stopStatusServer() {
|
|
|
65
66
|
|
|
66
67
|
const BSC_CHAIN_ID = "eip155:56";
|
|
67
68
|
|
|
68
|
-
// QR
|
|
69
|
+
// QR-page countdown (matches the WalletConnect connection timeout)
|
|
69
70
|
const QR_EXPIRE_MS = 5 * 60 * 1000;
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
|
-
*
|
|
73
|
+
* Generate the QR-code HTML page and open it in a browser (follows the Figma "AI card v1.2" design).
|
|
73
74
|
* @param {string} uri - WalletConnect URI
|
|
74
|
-
* @param {number} statusPort -
|
|
75
|
-
* @param {string|null} amount -
|
|
75
|
+
* @param {number} statusPort - port of the local status server
|
|
76
|
+
* @param {string|null} amount - USDT amount the user has to pay (e.g. "0.66")
|
|
76
77
|
*/
|
|
77
78
|
function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB Chain(BEP20) only", gasAmount = null) {
|
|
78
79
|
const html = `<!DOCTYPE html>
|
|
@@ -165,16 +166,16 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
165
166
|
const EXPIRE_MS = ${QR_EXPIRE_MS};
|
|
166
167
|
const startTime = Date.now();
|
|
167
168
|
|
|
168
|
-
//
|
|
169
|
+
// Clock countdown icon (exported from Figma 1:63, 16x16) — uses currentColor to track `.timer` color
|
|
169
170
|
const CLOCK_SVG = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.667 8C14.667 11.68 11.68 14.667 8 14.667C4.32 14.667 1.334 11.68 1.334 8C1.334 4.32 4.32 1.333 8 1.333C11.68 1.333 14.667 4.32 14.667 8Z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M10.476 10.12L8.409 8.887C8.049 8.674 7.756 8.16 7.756 7.74V5.007" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
170
|
-
//
|
|
171
|
+
// Info / hint icon (exported from Figma 1:43, 20x20)
|
|
171
172
|
const INFO_SVG = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path opacity="0.2" d="M10 18.333C14.6 18.333 18.331 14.602 18.331 10C18.331 5.397 14.6 1.666 10 1.666C5.395 1.666 1.664 5.397 1.664 10C1.664 14.602 5.395 18.333 10 18.333Z" fill="#737A86"/><path d="M10 13.333V10" stroke="#737A86" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 6.666H10.008" stroke="#737A86" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
172
|
-
// Loading
|
|
173
|
+
// Loading spinner icon (exported from Figma 1:50 loading-02, 24x24)
|
|
173
174
|
const LOADING_SVG = '<svg class="loading-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path opacity=".7" d="M4.922 5l2.828 2.828" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".6" d="M6 12H2" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".5" d="M4.922 19.078l2.828-2.828" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".4" d="M12 18v4" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".3" d="M19.078 19.078L16.25 16.25" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".2" d="M22 12h-4" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".1" d="M19.078 5L16.25 7.828" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/><path opacity=".8" d="M12 2v4" stroke="#191B1F" stroke-width="2.5" stroke-linecap="round"/></svg>';
|
|
174
|
-
//
|
|
175
|
+
// Success checkmark
|
|
175
176
|
const CHECK_SVG = '<div class="check-icon"><svg viewBox="0 0 14 14" fill="none"><path d="M3 7l3 3 5-5" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>';
|
|
176
177
|
|
|
177
|
-
//
|
|
178
|
+
// "Expired" illustration SVG (exported from Figma)
|
|
178
179
|
const EXPIRED_SVG = \`<svg width="144" height="129" viewBox="0 0 144 129" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
179
180
|
<path opacity="0.1" d="M140.369 64.988C140.369 82.769 133.106 98.797 121.461 110.317C110.066 121.711 94.289 128.598 76.884 128.598C59.604 128.598 43.826 121.586 32.306 110.317C20.661 98.797 13.398 82.769 13.398 64.988C13.398 29.802 41.823 1.377 76.884 1.377C111.944 1.377 140.369 29.927 140.369 64.988Z" fill="#1A72F7"/>
|
|
180
181
|
<path d="M134.859 23.291C137.695 23.291 139.993 20.992 139.993 18.157C139.993 15.321 137.695 13.023 134.859 13.023C132.024 13.023 129.725 15.321 129.725 18.157C129.725 20.992 132.024 23.291 134.859 23.291Z" fill="#E8F1FE"/>
|
|
@@ -194,7 +195,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
194
195
|
<linearGradient id="pb2" x1="53.891" y1="104.557" x2="103.297" y2="105.207" gradientUnits="userSpaceOnUse"><stop stop-color="#DBDEE3" stop-opacity=".7"/><stop offset="1" stop-color="#DBDEE3"/></linearGradient>
|
|
195
196
|
</defs></svg>\`;
|
|
196
197
|
|
|
197
|
-
//
|
|
198
|
+
// "Rejected" illustration SVG (variant of the expired illustration: clock swapped for an X, orange swapped for red)
|
|
198
199
|
const REJECTED_SVG = \`<svg width="144" height="129" viewBox="0 0 144 129" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
199
200
|
<path opacity="0.1" d="M140.369 64.988C140.369 82.769 133.106 98.797 121.461 110.317C110.066 121.711 94.289 128.598 76.884 128.598C59.604 128.598 43.826 121.586 32.306 110.317C20.661 98.797 13.398 82.769 13.398 64.988C13.398 29.802 41.823 1.377 76.884 1.377C111.944 1.377 140.369 29.927 140.369 64.988Z" fill="#1A72F7"/>
|
|
200
201
|
<path d="M134.859 23.291C137.695 23.291 139.993 20.992 139.993 18.157C139.993 15.321 137.695 13.023 134.859 13.023C132.024 13.023 129.725 15.321 129.725 18.157C129.725 20.992 132.024 23.291 134.859 23.291Z" fill="#E8F1FE"/>
|
|
@@ -223,7 +224,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
223
224
|
|
|
224
225
|
function fmtAmount(v) {
|
|
225
226
|
if (v == null || v === '') return '';
|
|
226
|
-
//
|
|
227
|
+
// Render the full original amount (no rounding); trim trailing zeros and add thousands separators to the integer part
|
|
227
228
|
const s = String(v);
|
|
228
229
|
const neg = s.startsWith('-') ? '-' : '';
|
|
229
230
|
const body = neg ? s.slice(1) : s;
|
|
@@ -233,7 +234,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
233
234
|
return neg + intWithComma + (decPart ? '.' + decPart : '') + ' ' + TOKEN;
|
|
234
235
|
}
|
|
235
236
|
|
|
236
|
-
// ======
|
|
237
|
+
// ====== Page rendering helpers ======
|
|
237
238
|
|
|
238
239
|
function renderQR(data) {
|
|
239
240
|
const remaining = Math.max(0, EXPIRE_MS - (Date.now() - startTime));
|
|
@@ -250,22 +251,22 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
250
251
|
|
|
251
252
|
const expireMin = Math.ceil(remaining / 60000);
|
|
252
253
|
|
|
253
|
-
// QR wrap:
|
|
254
|
-
// canvas 200 + padding 12*2 = 224,
|
|
254
|
+
// QR wrap: rounded-rectangle path starts at 12 o'clock (top center) and goes clockwise.
|
|
255
|
+
// canvas 200 + padding 12*2 = 224, corner radius r=16, stroke offset 1px
|
|
255
256
|
const S = 224, R = 16, cx = S/2;
|
|
256
|
-
//
|
|
257
|
+
// Start at top center, draw clockwise back to the start (open path, no Z — avoids the dash wrapping around)
|
|
257
258
|
const qrPath = 'M' + cx + ',1 H' + (S-1-R) + ' A' + R + ',' + R + ' 0 0 1 ' + (S-1) + ',' + (1+R) +
|
|
258
259
|
' V' + (S-1-R) + ' A' + R + ',' + R + ' 0 0 1 ' + (S-1-R) + ',' + (S-1) +
|
|
259
260
|
' H' + (1+R) + ' A' + R + ',' + R + ' 0 0 1 1,' + (S-1-R) +
|
|
260
261
|
' V' + (1+R) + ' A' + R + ',' + R + ' 0 0 1 ' + (1+R) + ',1 H' + cx;
|
|
261
|
-
//
|
|
262
|
+
// Use pathLength=1000 so the dash math is exact (no hand-calculated drift)
|
|
262
263
|
const PL = 1000;
|
|
263
264
|
const progress = remaining / EXPIRE_MS;
|
|
264
265
|
const dashOffset = -PL * (1 - progress);
|
|
265
266
|
|
|
266
|
-
// USDT
|
|
267
|
+
// USDT icon SVG (green circle Tether glyph)
|
|
267
268
|
const USDT_ICON = '<svg class="usdt-icon" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="9" cy="9" r="9" fill="#26A17B"/><path d="M10.1 9.6c-.06 0-.33.02-.73.02-.32 0-.58-.01-.67-.02-1.32-.06-2.3-.3-2.3-.58 0-.29.98-.52 2.3-.58v.93c.09.01.36.02.68.02.38 0 .66-.01.72-.02v-.93c1.31.06 2.29.3 2.29.58 0 .28-.98.52-2.29.58zm0-.87v-.83h2.05V6.5H5.88v1.4h2.05v.83c-1.48.07-2.6.38-2.6.75 0 .37 1.12.68 2.6.75v2.69h1.07v-2.69c1.48-.07 2.59-.38 2.59-.75 0-.37-1.11-.68-2.59-.75z" fill="#fff"/></svg>';
|
|
268
|
-
// BNB
|
|
269
|
+
// BNB icon SVG (yellow circle BNB glyph)
|
|
269
270
|
const BNB_ICON = '<svg class="usdt-icon" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="9" cy="9" r="9" fill="#F3BA2F"/><path d="M9 4.5L7.2 6.3l-1.8-1.8L9 1.2l3.6 3.3-1.8 1.8L9 4.5zm-4.5 4.5L2.7 7.2 4.5 5.4l1.8 1.8L4.5 9zm4.5 4.5l-1.8-1.8-1.8 1.8L9 16.8l3.6-3.3-1.8-1.8L9 13.5zm4.5-4.5l1.8 1.8-1.8 1.8-1.8-1.8 1.8-1.8zM10.8 9L9 7.2 7.2 9 9 10.8 10.8 9z" fill="#fff"/></svg>';
|
|
270
271
|
const TOKEN_ICON = TOKEN === 'BNB' ? BNB_ICON : USDT_ICON;
|
|
271
272
|
|
|
@@ -307,7 +308,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
307
308
|
'<div class="result-sub">' + (error || 'Something went wrong.') + '<br>Please try again.</div></div>';
|
|
308
309
|
}
|
|
309
310
|
|
|
310
|
-
// ======
|
|
311
|
+
// ====== State machine ======
|
|
311
312
|
|
|
312
313
|
const FINAL = ['confirmed', 'rejected', 'failed', 'expired'];
|
|
313
314
|
let lastState = null;
|
|
@@ -339,7 +340,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
339
340
|
stopTimer();
|
|
340
341
|
} else {
|
|
341
342
|
body.innerHTML = renderQR(data);
|
|
342
|
-
//
|
|
343
|
+
// Render the QR code
|
|
343
344
|
const qrEl = document.getElementById('qr');
|
|
344
345
|
if (qrEl) {
|
|
345
346
|
new QRious({ element: qrEl, value: URI, size: 200, backgroundAlpha: 1, background: '#ffffff', foreground: '#000000', level: 'M' });
|
|
@@ -361,13 +362,13 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
361
362
|
closeTimer = setTimeout(tick, 1000);
|
|
362
363
|
}
|
|
363
364
|
|
|
364
|
-
//
|
|
365
|
+
// Initial render
|
|
365
366
|
render(null);
|
|
366
367
|
|
|
367
|
-
//
|
|
368
|
+
// Countdown refresh (update timer every second)
|
|
368
369
|
timerInterval = setInterval(() => {
|
|
369
370
|
const remaining = EXPIRE_MS - (Date.now() - startTime);
|
|
370
|
-
//
|
|
371
|
+
// Backend has entered an active state (connected / signing / tx-submitted); do not expire the page
|
|
371
372
|
const ACTIVE = ['connected', 'signing', 'tx_submitted'];
|
|
372
373
|
if (remaining <= 0 && !FINAL.includes(lastState) && !ACTIVE.includes(lastState)) {
|
|
373
374
|
lastState = 'expired';
|
|
@@ -375,7 +376,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
375
376
|
maybeAutoClose('expired');
|
|
376
377
|
return;
|
|
377
378
|
}
|
|
378
|
-
//
|
|
379
|
+
// Update the countdown label
|
|
379
380
|
const timerEl = document.querySelector('.timer');
|
|
380
381
|
if (timerEl) {
|
|
381
382
|
const expireMin = Math.ceil(Math.max(0, remaining) / 60000);
|
|
@@ -383,7 +384,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
383
384
|
const hintSpan = document.querySelector('.hint-bar span');
|
|
384
385
|
if (hintSpan) hintSpan.textContent = 'Expire in ' + expireMin + ' mins. This page will close automatically once the transfer is completed';
|
|
385
386
|
}
|
|
386
|
-
//
|
|
387
|
+
// Update the QR-border progress (negative offset → the dash recedes clockwise from 12 o'clock)
|
|
387
388
|
const progressEl = document.getElementById('qr-progress');
|
|
388
389
|
if (progressEl) {
|
|
389
390
|
const PL = 1000;
|
|
@@ -392,7 +393,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
392
393
|
}
|
|
393
394
|
}, 1000);
|
|
394
395
|
|
|
395
|
-
//
|
|
396
|
+
// Poll backend status
|
|
396
397
|
async function poll() {
|
|
397
398
|
if (stopped) return;
|
|
398
399
|
try {
|
|
@@ -426,7 +427,7 @@ function openQRInBrowser(uri, statusPort, amount, token = "USDT", network = "BNB
|
|
|
426
427
|
}
|
|
427
428
|
|
|
428
429
|
/**
|
|
429
|
-
*
|
|
430
|
+
* Initialise the WalletConnect SignClient
|
|
430
431
|
* @param {string} projectId - WalletConnect Cloud project ID
|
|
431
432
|
*/
|
|
432
433
|
export async function initSignClient(projectId) {
|
|
@@ -446,8 +447,9 @@ export async function initSignClient(projectId) {
|
|
|
446
447
|
),
|
|
447
448
|
]);
|
|
448
449
|
|
|
449
|
-
// WalletConnect relay
|
|
450
|
-
//
|
|
450
|
+
// WalletConnect relay occasionally emits null WebSocket frames, which crashes
|
|
451
|
+
// isJsonRpcPayload('id' in null). Filter null payloads at the connection layer so
|
|
452
|
+
// the error never reaches the provider / request chain.
|
|
451
453
|
try {
|
|
452
454
|
const conn = client.core.relayer.provider.connection;
|
|
453
455
|
const _origEmit = conn.emit.bind(conn);
|
|
@@ -460,7 +462,7 @@ export async function initSignClient(projectId) {
|
|
|
460
462
|
};
|
|
461
463
|
} catch {}
|
|
462
464
|
|
|
463
|
-
//
|
|
465
|
+
// Tear down stale sessions + pairings (await in parallel to ensure the relay is clean before reconnecting)
|
|
464
466
|
try {
|
|
465
467
|
const sessions = client.session.getAll();
|
|
466
468
|
if (sessions.length > 0) {
|
|
@@ -486,10 +488,10 @@ export async function initSignClient(projectId) {
|
|
|
486
488
|
}
|
|
487
489
|
|
|
488
490
|
/**
|
|
489
|
-
*
|
|
491
|
+
* Connect to a wallet: render the QR code and wait for the user to approve via scan.
|
|
490
492
|
* @param {SignClient} signClient
|
|
491
493
|
* @param {number} statusPort
|
|
492
|
-
* @param {string|null} amount -
|
|
494
|
+
* @param {string|null} amount - USDT amount to display (e.g. "0.66")
|
|
493
495
|
* @returns {{ session: object, peerAddress: string }}
|
|
494
496
|
*/
|
|
495
497
|
export async function connectWallet(signClient, statusPort, amount = null, token = "USDT", gasAmount = null) {
|
|
@@ -503,7 +505,7 @@ export async function connectWallet(signClient, statusPort, amount = null, token
|
|
|
503
505
|
},
|
|
504
506
|
});
|
|
505
507
|
|
|
506
|
-
//
|
|
508
|
+
// Generate the QR-code page (with status polling) and open it in the browser
|
|
507
509
|
openQRInBrowser(uri, statusPort, amount, token, "BNB Chain(BEP20) only", gasAmount);
|
|
508
510
|
console.error("QR code opened in browser. Scan it with your wallet app.");
|
|
509
511
|
console.error("Waiting for wallet approval...");
|
|
@@ -528,11 +530,11 @@ export async function connectWallet(signClient, statusPort, amount = null, token
|
|
|
528
530
|
}
|
|
529
531
|
|
|
530
532
|
/**
|
|
531
|
-
*
|
|
533
|
+
* Request an ERC-20 token transfer
|
|
532
534
|
* @param {SignClient} signClient
|
|
533
535
|
* @param {object} session - WalletConnect session
|
|
534
536
|
* @param {{ from: string, to: string, token: string, amount: string, decimals?: number }} params
|
|
535
|
-
* @returns {string}
|
|
537
|
+
* @returns {string} transaction hash
|
|
536
538
|
*/
|
|
537
539
|
export async function requestERC20Transfer(signClient, session, { from, to, token, amount, decimals = 18 }) {
|
|
538
540
|
const value = parseUnits(amount, decimals);
|
|
@@ -554,7 +556,7 @@ export async function requestERC20Transfer(signClient, session, { from, to, toke
|
|
|
554
556
|
from,
|
|
555
557
|
to: token,
|
|
556
558
|
data,
|
|
557
|
-
gas: "0xFDE8", // 65000 — ERC20
|
|
559
|
+
gas: "0xFDE8", // 65000 — ERC20.transfer contract call
|
|
558
560
|
},
|
|
559
561
|
],
|
|
560
562
|
},
|
|
@@ -568,11 +570,11 @@ export async function requestERC20Transfer(signClient, session, { from, to, toke
|
|
|
568
570
|
}
|
|
569
571
|
|
|
570
572
|
/**
|
|
571
|
-
*
|
|
573
|
+
* Request a native BNB transfer
|
|
572
574
|
* @param {SignClient} signClient
|
|
573
575
|
* @param {object} session
|
|
574
|
-
* @param {{ from: string, to: string, value: string }} params - value
|
|
575
|
-
* @returns {string}
|
|
576
|
+
* @param {{ from: string, to: string, value: string }} params - `value` is the BNB amount (e.g. "0.001")
|
|
577
|
+
* @returns {string} transaction hash
|
|
576
578
|
*/
|
|
577
579
|
export async function requestNativeTransfer(signClient, session, { from, to, value }) {
|
|
578
580
|
const weiValue = "0x" + parseUnits(value, 18).toString(16);
|
|
@@ -589,7 +591,7 @@ export async function requestNativeTransfer(signClient, session, { from, to, val
|
|
|
589
591
|
from,
|
|
590
592
|
to,
|
|
591
593
|
value: weiValue,
|
|
592
|
-
gas: "0x5208", // 21000 — BNB
|
|
594
|
+
gas: "0x5208", // 21000 — fixed gas for a native BNB transfer
|
|
593
595
|
},
|
|
594
596
|
],
|
|
595
597
|
},
|
|
@@ -605,11 +607,11 @@ export async function requestNativeTransfer(signClient, session, { from, to, val
|
|
|
605
607
|
const FINAL_LINGER_MS = 2000;
|
|
606
608
|
|
|
607
609
|
/**
|
|
608
|
-
*
|
|
609
|
-
* -
|
|
610
|
-
* -
|
|
611
|
-
* -
|
|
612
|
-
* -
|
|
610
|
+
* Higher-order wallet-connect helper: manages the full connection lifecycle.
|
|
611
|
+
* - Starts the status server
|
|
612
|
+
* - Waits for the user to scan and connect their wallet
|
|
613
|
+
* - Runs the caller's transaction logic
|
|
614
|
+
* - Always disconnects and cleans up, whether the body succeeded or failed
|
|
613
615
|
*
|
|
614
616
|
* @param {{ amount?: string, projectId?: string }} opts
|
|
615
617
|
* @param {(ctx: { signClient, session, peerAddress }) => Promise<void>} fn
|
|
@@ -629,7 +631,7 @@ export async function withWallet(opts, fn) {
|
|
|
629
631
|
|
|
630
632
|
await fn({ signClient, session, peerAddress });
|
|
631
633
|
|
|
632
|
-
//
|
|
634
|
+
// Success: persist mainWallet so subsequent withdraws can default to it
|
|
633
635
|
const config = loadConfig();
|
|
634
636
|
config.mainWallet = peerAddress;
|
|
635
637
|
saveConfig(config);
|
|
@@ -652,11 +654,11 @@ export async function withWallet(opts, fn) {
|
|
|
652
654
|
await new Promise((r) => setTimeout(r, FINAL_LINGER_MS));
|
|
653
655
|
stopStatusServer();
|
|
654
656
|
if (signClient) {
|
|
655
|
-
//
|
|
657
|
+
// Disconnect the current session
|
|
656
658
|
if (session) {
|
|
657
659
|
await disconnectSession(signClient, session);
|
|
658
660
|
}
|
|
659
|
-
//
|
|
661
|
+
// Disconnect every pairing so the wallet side has no lingering connection
|
|
660
662
|
try {
|
|
661
663
|
const pairings = signClient.core.pairing.pairings.getAll({ active: true });
|
|
662
664
|
await Promise.allSettled(
|
|
@@ -672,19 +674,19 @@ export async function withWallet(opts, fn) {
|
|
|
672
674
|
}
|
|
673
675
|
|
|
674
676
|
/**
|
|
675
|
-
*
|
|
677
|
+
* Normalise wallet error messages: map known Chinese / multilingual error strings to a unified English form.
|
|
676
678
|
* @param {Error} error
|
|
677
|
-
* @returns {Error}
|
|
679
|
+
* @returns {Error} the same error object with its `message` rewritten
|
|
678
680
|
*/
|
|
679
681
|
export function normalizeWalletError(error) {
|
|
680
682
|
const msg = error?.message || "";
|
|
681
683
|
const patterns = [
|
|
682
|
-
//
|
|
683
|
-
{ test:
|
|
684
|
-
//
|
|
685
|
-
{ test:
|
|
686
|
-
//
|
|
687
|
-
{ test:
|
|
684
|
+
// Rejection patterns (English error strings only — localised wallets fall through to WALLET_ERROR)
|
|
685
|
+
{ test: /User rejected|User denied|declined/i, replacement: "rejected" },
|
|
686
|
+
// Disconnect patterns
|
|
687
|
+
{ test: /disconnect.*DApp|session.*expired|session.*disconnected/i, replacement: "rejected" },
|
|
688
|
+
// Timeout patterns
|
|
689
|
+
{ test: /timed?\s*out|timeout/i, replacement: "timed out" },
|
|
688
690
|
];
|
|
689
691
|
for (const { test, replacement } of patterns) {
|
|
690
692
|
if (test.test(msg)) {
|
|
@@ -696,7 +698,7 @@ export function normalizeWalletError(error) {
|
|
|
696
698
|
}
|
|
697
699
|
|
|
698
700
|
/**
|
|
699
|
-
*
|
|
701
|
+
* Disconnect a WalletConnect session.
|
|
700
702
|
* @param {SignClient} signClient
|
|
701
703
|
* @param {object} session
|
|
702
704
|
*/
|
|
@@ -707,6 +709,6 @@ export async function disconnectSession(signClient, session) {
|
|
|
707
709
|
reason: { code: 6000, message: "Session complete" },
|
|
708
710
|
});
|
|
709
711
|
} catch {
|
|
710
|
-
//
|
|
712
|
+
// Silently ignore disconnect errors
|
|
711
713
|
}
|
|
712
714
|
}
|
package/src/x402.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* x402
|
|
2
|
+
* x402 protocol client: initialise an EVM signer and an x402Client.
|
|
3
3
|
*/
|
|
4
4
|
import { x402Client, wrapAxiosWithPayment, x402HTTPClient } from "@aeon-ai-pay/axios";
|
|
5
5
|
import { registerExactEvmScheme } from "@aeon-ai-pay/evm/exact/client";
|
|
@@ -11,8 +11,8 @@ import { BSC_RPC_URL } from "./constants.mjs";
|
|
|
11
11
|
import axios from "axios";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
* @param {`0x${string}`} privateKey - EVM
|
|
14
|
+
* Build an x402 axios client with the EVM signer pre-registered.
|
|
15
|
+
* @param {`0x${string}`} privateKey - EVM private key
|
|
16
16
|
* @returns {{ api: AxiosInstance, client: x402Client, address: string, getOrderNo: () => string|null }}
|
|
17
17
|
*/
|
|
18
18
|
export function createX402Api(privateKey) {
|
|
@@ -39,8 +39,9 @@ export function createX402Api(privateKey) {
|
|
|
39
39
|
|
|
40
40
|
const axiosInstance = axios.create();
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
42
|
+
// Register the interceptor *before* wrapAxiosWithPayment so it can
|
|
43
|
+
// capture orderNo from the 402 response body (the server returns it
|
|
44
|
+
// on the first request).
|
|
44
45
|
let capturedOrderNo = null;
|
|
45
46
|
axiosInstance.interceptors.response.use(
|
|
46
47
|
(response) => response,
|
|
@@ -63,11 +64,13 @@ export function createX402Api(privateKey) {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
67
|
+
* Send the first x402 request (unsigned) and extract the real payment requirements
|
|
68
|
+
* from the 402 response.
|
|
69
|
+
* Also keeps the raw 402 response and the original request config so the caller can
|
|
70
|
+
* sign manually later.
|
|
71
|
+
* Field names follow the x402 v2 PaymentRequirements standard: asset, payTo, amount.
|
|
69
72
|
*
|
|
70
|
-
*
|
|
73
|
+
* Supports both GET (card path) and POST (image / Skill Boss path).
|
|
71
74
|
*
|
|
72
75
|
* @param {string} url
|
|
73
76
|
* @param {{ method?: "GET"|"POST", data?: any, headers?: object }} [options]
|
|
@@ -105,7 +108,7 @@ export async function fetchPaymentRequirements(url, options = {}) {
|
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
/**
|
|
108
|
-
*
|
|
111
|
+
* Decode the PAYMENT-RESPONSE response header (x402 v2).
|
|
109
112
|
* @param {object} headers - axios response headers
|
|
110
113
|
* @returns {object|null}
|
|
111
114
|
*/
|