From a5756907eb3ecaaa7fc8c1a77afdfe49a5b19970 Mon Sep 17 00:00:00 2001 From: Steven Lin Date: Thu, 17 Sep 2026 10:20:07 +0800 Subject: [PATCH 1/3] fix(x402): normalize settlement networks and classify failed receipts --- .../outbound/x402/payment-client.test.ts | 85 ++++++++++++++++++- .../adapters/outbound/x402/payment-client.ts | 28 ++++-- .../services/bai-payment-result.test.ts | 29 ++++++- .../services/bai-payment-result.ts | 7 +- 4 files changed, 131 insertions(+), 18 deletions(-) diff --git a/ts/src/adapters/outbound/x402/payment-client.test.ts b/ts/src/adapters/outbound/x402/payment-client.test.ts index e38257987..79990a5df 100644 --- a/ts/src/adapters/outbound/x402/payment-client.test.ts +++ b/ts/src/adapters/outbound/x402/payment-client.test.ts @@ -175,14 +175,15 @@ describe("X402PaymentClient", () => { }); it.each([ - [ - { success: false, errorReason: "transaction_failed", transaction: "", network: "eip155:56" }, - false, - ], [{ success: true, transaction: "0x" + "a".repeat(64), network: "eip155:56" }, true], [{ success: true, transaction: "", network: "eip155:56" }, false], [{ success: true, transaction: "0x" + "a".repeat(64), network: "eip155:8453" }, false], [{ transaction: "0x" + "a".repeat(64), network: "eip155:56" }, false], + [ + { success: false, errorReason: "insufficient_funds", transaction: "", network: "eip155:8453" }, + false, + ], + [{ success: "false", transaction: "", network: "eip155:56" }, false], ])("only marks a successful matching settlement as settled (%j)", async (header, settled) => { const client = new X402PaymentClient( resolver, @@ -207,6 +208,82 @@ it.each([ details: { retryPayment: false, settled: false, delivered: false }, }); }); +it.each([ + ["insufficient_funds", "insufficient_balance"], + ["transaction_failed", "provider_error"], + ["SECRET provider diagnostic", "provider_error"], +])("classifies a failed settlement receipt with reason %s", async (reason, code) => { + const transaction = "0x" + "a".repeat(64); + for (const status of [200, 502]) { + for (const header of ["payment-response", "x-payment-response"]) { + const client = new X402PaymentClient( + resolver, + async () => + new Response("not JSON", { + status, + headers: { + [header]: Buffer.from( + JSON.stringify({ + success: false, + errorReason: reason, + transaction, + network: "eip155:56", + }), + ).toString("base64"), + }, + }), + ); + const error = await client + .pay(scope, net, { + url: "https://example.test", + method: "GET", + headers: [], + }) + .catch((error: unknown) => error); + expect(error).toMatchObject({ + code, + details: { + phase: "settle", + httpStatus: status, + settled: false, + delivered: status === 200, + retryPayment: false, + paymentStatus: "unknown", + candidateTxHash: transaction, + ...(reason === "insufficient_funds" ? { reason } : {}), + }, + }); + expect(JSON.stringify(error)).not.toContain("SECRET"); + } + } +}); + +it("classifies a failed settlement without a transaction hash", async () => { + const client = new X402PaymentClient( + resolver, + async () => + new Response("{}", { + status: 502, + headers: { + "payment-response": Buffer.from( + JSON.stringify({ + success: false, + errorReason: "insufficient_funds", + transaction: "", + network: "eip155:56", + }), + ).toString("base64"), + }, + }), + ); + await expect( + client.pay(scope, net, { + url: "https://example.test", + method: "GET", + headers: [], + }), + ).rejects.toMatchObject({ code: "insufficient_balance" }); +}); it("bounds oversized 402 bodies before the SDK or signer handles them", async () => { let pulled = 0; const cancel = vi.fn(); diff --git a/ts/src/adapters/outbound/x402/payment-client.ts b/ts/src/adapters/outbound/x402/payment-client.ts index cd262d273..479c14f93 100644 --- a/ts/src/adapters/outbound/x402/payment-client.ts +++ b/ts/src/adapters/outbound/x402/payment-client.ts @@ -158,21 +158,31 @@ export class X402PaymentClient implements X402PaymentPort { } } if (paymentHeader && !successfulSettlement(paymentResponse, expectedNetwork)) { + const receipt = + paymentResponse && typeof paymentResponse === "object" && !Array.isArray(paymentResponse) + ? (paymentResponse as Record) + : undefined; + // A well-formed negative receipt is a settlement failure, not invalid evidence. + const failed = + receipt?.success === false && + typeof receipt.network === "string" && + sameX402Network(receipt.network, expectedNetwork ?? receipt.network) && + typeof receipt.transaction === "string" && + (receipt.errorReason === undefined || typeof receipt.errorReason === "string"); + const failure = providerPaymentError( + failed ? receipt.errorReason : undefined, + "settle", + receipt, + ); throw new TransportError( - "invalid_settlement", - "paid endpoint returned an invalid settlement receipt", + failed ? failure.code : "invalid_settlement", + failed ? failure.message : "paid endpoint returned an invalid settlement receipt", { httpStatus: response.status, settled: false, delivered: response.ok, retryPayment: false, - ...providerPaymentError( - undefined, - "settle", - paymentResponse && typeof paymentResponse === "object" - ? (paymentResponse as Record) - : undefined, - ).details, + ...failure.details, }, ); } diff --git a/ts/src/application/services/bai-payment-result.test.ts b/ts/src/application/services/bai-payment-result.test.ts index 0c0ef0603..95029842b 100644 --- a/ts/src/application/services/bai-payment-result.test.ts +++ b/ts/src/application/services/bai-payment-result.test.ts @@ -36,19 +36,44 @@ it("does not accept HTTP delivery or a legacy MCP hash as proof of settlement", ).toThrow(); expect(() => baiPaymentResult({ ...payment(), settled: false }, "eip155:56")).toThrow(); }); -it("accepts TRON settlement network notation", () => { +it.each(["tron:728126428", "tron:0x2b6653dc"])("accepts TRON settlement network %s", (network) => { expect( baiPaymentResult( { settled: true, payer: { address: "tron-payer" }, - paymentResponse: { success: true, network: "tron:0x2b6653dc", transaction: "a".repeat(64) }, + paymentResponse: { success: true, network, transaction: "a".repeat(64) }, }, "tron:728126428", ), ).toEqual({ txHash: "a".repeat(64), payer: "tron-payer" }); }); +it.each([ + "tron:3448148188", + "tron:0xcd8690dc", + "eip155:728126428", + "invalid", + undefined, + 728126428, +])("rejects mismatched or malformed TRON settlement network %j", (network) => { + expect(() => + baiPaymentResult( + { + settled: true, + payer: { address: "tron-payer" }, + paymentResponse: { success: true, network, transaction: "a".repeat(64) }, + }, + "tron:728126428", + ), + ).toThrow( + expect.objectContaining({ + code: "invalid_x402_response", + details: expect.objectContaining({ reason: "network_mismatch" }), + }), + ); +}); + it.each([ [{ payer: "0x2222222222222222222222222222222222222222" }, "payer_mismatch"], [{ network: "eip155:8453" }, "network_mismatch"], diff --git a/ts/src/application/services/bai-payment-result.ts b/ts/src/application/services/bai-payment-result.ts index a424a54ea..7fa4f53e9 100644 --- a/ts/src/application/services/bai-payment-result.ts +++ b/ts/src/application/services/bai-payment-result.ts @@ -1,19 +1,20 @@ import { tronAddressBytes, tronHexToBase58 } from "../../domain/address/index.js"; import { TransportError } from "../../domain/errors/index.js"; +import { sameX402Network } from "../../domain/x402/network-id.js"; /** Only a successful x402 settlement can be reported to B.AI as a payment. */ export function baiPaymentResult(payment: Record, network: string) { const settlement = record(payment.paymentResponse); const payer = record(payment.payer)?.address; const txHash = settlement?.transaction; - const expectedNetwork = network === "tron:728126428" ? "tron:0x2b6653dc" : network; const validHash = network.startsWith("tron:") ? /^[0-9a-fA-F]{64}$/ : /^0x[0-9a-fA-F]{64}$/; const invalid = (reason: string) => - invalidSettlement(reason, txHash, settlement?.network, expectedNetwork); + invalidSettlement(reason, txHash, settlement?.network, network); if (!settlement) throw invalid("missing_settlement"); if (payment.settled !== true || settlement.success !== true) throw invalid("settlement_unconfirmed"); - if (settlement.network !== expectedNetwork) throw invalid("network_mismatch"); + if (typeof settlement.network !== "string" || !sameX402Network(settlement.network, network)) + throw invalid("network_mismatch"); if (typeof txHash !== "string" || !validHash.test(txHash)) throw invalid("invalid_transaction_hash"); if (typeof payer !== "string" || !payer) throw invalid("missing_payer"); From 3fd11711228c4f11dbd5ffc486fbea3bf873b99c Mon Sep 17 00:00:00 2001 From: Steven Lin Date: Wed, 16 Sep 2026 16:49:54 +0800 Subject: [PATCH 2/3] ops: version update & tidy doc --- java/build.gradle | 2 +- .../java/org/tron/common/utils/Utils.java | 2 +- ts/README.md | 2 +- ts/docs/development/architecture.md | 155 -------------- ts/docs/development/bai-recharge.md | 196 ------------------ ts/docs/development/beta-release.md | 67 ------ .../development/erc8004-sdk-integration.md | 32 --- ts/docs/machine-interface.md | 2 +- ts/package-lock.json | 4 +- ts/package.json | 2 +- 10 files changed, 7 insertions(+), 457 deletions(-) delete mode 100644 ts/docs/development/architecture.md delete mode 100644 ts/docs/development/bai-recharge.md delete mode 100644 ts/docs/development/beta-release.md delete mode 100644 ts/docs/development/erc8004-sdk-integration.md diff --git a/java/build.gradle b/java/build.gradle index ccc72d8cd..36de9b725 100644 --- a/java/build.gradle +++ b/java/build.gradle @@ -19,7 +19,7 @@ plugins { } group 'Tron' -version '4.13.1' +version '4.14.0' apply plugin: 'java' apply plugin: 'com.google.protobuf' diff --git a/java/src/main/java/org/tron/common/utils/Utils.java b/java/src/main/java/org/tron/common/utils/Utils.java index 27233d309..863020856 100644 --- a/java/src/main/java/org/tron/common/utils/Utils.java +++ b/java/src/main/java/org/tron/common/utils/Utils.java @@ -136,7 +136,7 @@ public class Utils { public static final int MIN_LENGTH = 2; public static final int MAX_LENGTH = 14; - public static final String VERSION = " v4.13.1"; + public static final String VERSION = " v4.14.0"; public static final String TRANSFER_METHOD_ID = "a9059cbb"; private static SecureRandom random = new SecureRandom(); diff --git a/ts/README.md b/ts/README.md index f4c2ec194..242b1dfb3 100644 --- a/ts/README.md +++ b/ts/README.md @@ -226,7 +226,7 @@ transaction pipeline. See [the SDK integration](docs/development/erc8004-sdk-int ## B.AI usage and x402 providers -The v4.14 command names are: +The v4.14.0 command names are: | Group | Commands | | ------ | -------------------------------------------------------------------------------------------------------- | diff --git a/ts/docs/development/architecture.md b/ts/docs/development/architecture.md deleted file mode 100644 index 5252e72b2..000000000 --- a/ts/docs/development/architecture.md +++ /dev/null @@ -1,155 +0,0 @@ -# TypeScript 架構與 Feature 開發指南 - -這份文件給要在 `ts/` 新增功能的人。wallet-cli 是一個 **agent-first、多鏈、hexagonal architecture** 的 CLI:每個命令都必須可預測、可被程式呼叫,且私鑰等秘密不能穿過不安全的輸入或輸出邊界。 - -## 先記住這張圖 - -```text -argv - ↓ -bootstrap/runner.ts - ├─ migration preflight - └─ 建立 CLI、統一處理錯誤與結束碼 - ↓ -adapters/inbound/cli - 解析參數 → Zod 驗證 → 選 network/family → command binding - ↓ -application - use case / service → port - ↓ ↓ -domain adapters/outbound -純規則與型別 RPC、檔案、Ledger、外部服務 -``` - -依賴只能往內: - -```text -bootstrap → inbound/outbound adapters → application → domain -``` - -Inbound 與 outbound adapter 不能互相 import;兩者只在 `bootstrap/composition.ts` 組裝。這些規則由 `.dependency-cruiser.cjs` 強制檢查。 - -## 每一層負責什麼 - -| 目錄 | 放這裡 | 不要放這裡 | -|---|---|---| -| `src/domain/` | address、amount、wallet、derivation、fee、error 等純規則與 value types | 檔案、網路、Ledger、CLI、application import | -| `src/application/use-cases/` | 一個使用情境的編排;TRON/EVM 差異放在各自子目錄 | yargs、console、TronWeb/ethers、具體檔案系統 | -| `src/application/services/` | 多個 use case 共用的流程,例如 signer resolution、transaction pipeline | adapter 實作 | -| `src/application/ports/` | application 所需要的 I/O interface | 具體 SDK/client | -| `src/adapters/inbound/cli/` | command spec、Zod schema、argv mapping、文字 render | RPC、持久化與鏈上商業邏輯 | -| `src/adapters/outbound/` | port 的實作:chain RPC、keystore、Ledger、config、price 等 | CLI command 或 render | -| `src/bootstrap/` | dependency wiring、family 註冊、process lifecycle | domain 規則或 feature 邏輯 | - -Composition root 是 `src/bootstrap/composition.ts`。若不確定物件在哪裡建立,從這裡往回找。 - -## 一次命令如何執行 - -1. `src/index.ts` 呼叫 `bootstrap/runner.ts`。 -2. runner 先解析 global flags,執行資料 migration preflight,再建立 runtime。 -3. CLI shell 從 `CommandRegistry` 找到命令,解析 network 與 account。 -4. 命令的 Zod schema 驗證輸入;capability、family、互動與認證限制在執行前檢查。 -5. Inbound binding 呼叫 application use case;use case 只透過 port 做 I/O。 -6. formatter 將結果輸出為 text,或輸出唯一一個 `wallet-cli.result.v1` JSON envelope。 -7. 所有錯誤回到 runner,正規化成穩定 error code 與 exit code。 - -Machine-facing 行為以 [`machine-interface.md`](../machine-interface.md) 為準:stdout、stderr、JSON shape 或 exit code 的改動都視為 public API 變更。 - -## 新增 feature 時怎麼切 - -### 1. 先判斷命令類型 - -- **不接觸鏈**:使用 `CommandDefinition`,例如 wallet、config、contact。 -- **接觸鏈**:使用一份共用 `ChainSpec`,再為支援的 family 加 `FamilyBinding`。 - -`ChainSpec` 定義共同命令語意、欄位、help、capability 與 renderer;`FamilyBinding` 只補該 family 的欄位、驗證與執行方式。共用命令不要複製成 TRON/EVM 兩份 spec。參考: - -- 共用 spec 與 binding:`src/adapters/inbound/cli/commands/tx.ts` -- family wiring:`src/bootstrap/families/tron.ts`、`evm.ts` -- registry:`src/adapters/inbound/cli/registry/index.ts` - -### 2. 把邏輯放對位置 - -一般 feature 的最小垂直切片是: - -```text -commands/.ts - → application/use-cases/[family]/-service.ts - → application/ports/.ts (只有需要新 I/O 時) - → adapters/outbound/.ts (只有需要新 I/O 時) - → bootstrap/composition.ts 或 bootstrap/families/.ts -``` - -判斷原則: - -- 不需要 I/O 的規則放 `domain`。 -- 描述「要完成什麼」的流程放 application。 -- 描述「如何呼叫 RPC / SDK / filesystem」的程式放 outbound adapter。 -- CLI 層只做輸入契約、dispatch 與 presentation。 - -### 3. 交易命令必須走共用 pipeline - -由 CLI 建構與廣播的鏈上交易使用 `application/services/pipeline/TxPipeline`,不要在 command 或 use case 另寫一套流程。標準順序是: - -```text -resolve signer → build → prepare → estimate → dry/build-only - → preflight → sign → authorization → broadcast → confirm -``` - -這裡統一保證 software/Ledger signer、`--dry-run`、`--sign-only`、`--build-only`、permission、timeout 與 `--wait` 的語意。 - -### x402 協議支付的邊界 - -`x402 pay`、`x402 roundtrip` 與 `bai recharge` 使用協議支付流程:SDK 處理 challenge、付款簽名及 facilitator 結算,並非 CLI 自行建構和廣播一筆普通交易。因此不套用 `TxPipeline` 的整套 build / estimate / broadcast 流程,也不承諾其 sign-only、build-only、wait 語意。ERC-8004 寫入及一般 transfer / approve 仍必須使用 `TxPipeline`。 - -x402 必須沿用 application 的 `SignerResolver` 與 `obtainSignature`,不得自行解密 vault 或讀取私鑰。outbound adapter 負責 SDK 協定轉換;application 負責充值編排與 port。SDK 要求簽署授權交易時仍經相同 signer 邊界,不能視為一般交易 pipeline 已提供完整保障。 - -`x402 pay --dry-run` 只檢查 challenge,不簽名或付款。roundtrip 與 BAI recharge 不宣告 dry-run / sign-only / build-only,CLI 必須拒絕這些未支援的旗標。付款狀態未知時保留可用交易證據、禁止自動重付;BAI 已付款但未確認入帳時,只能查詢或對原交易補報。 - -### 4. Schema 是命令介面的單一來源 - -每個 command 的 Zod schema 同時驅動: - -- argv arity 與型別轉換 -- validation -- `--help` -- JSON Schema discovery - -因此不要另外維護參數表,也不要在 use case 才補做可由 schema 表達的輸入驗證。金額與鏈上大整數必須用 decimal string / `bigint`,不可經過 JavaScript `number`。若外部 API 強制要求 JSON number(例如 BAI),只可在 outbound adapter 的序列化邊界轉換,並檢查範圍與十進位往返一致性;application port 與付款數量保持 decimal string。 - -## 不可破壞的邊界 - -- **Secrets**:private key、mnemonic、BIP39 passphrase 不得出現在 argv、env、log 或 result,且只能由 hidden TTY 輸入;master password 只有在命令明確允許時才能走專用 stdin。是否允許互動由 command metadata 宣告。 -- **JSON contract**:JSON mode 的 stdout 只能有一個 terminal envelope;progress 與 diagnostic 走 stderr。不要直接 `console.log`。 -- **Errors**:預期錯誤使用 `domain/errors` 的 typed error。新增 error code 時同步更新 `domain/errors/codes.ts`;exit `1` 是執行失敗,exit `2` 是呼叫方式錯誤。 -- **Dry run**:標示 `broadcasts` 的命令在 `--dry-run` 下不能碰 broadcaster;不要繞過既有 guard。 -- **Network identity**:儲存與 machine output 使用 canonical network id;alias 只在選擇 network 時解析。family 是 `tron | evm`,不是 CAIP-2 namespace。 -- **Persistence**:預設 root 是 `~/.wallet-cli`,測試可用 `WALLET_CLI_HOME` 隔離。寫入沿用 `AtomicFileStore` 的 lock、atomic write 與權限檢查,不要直接覆寫 wallet files。 -- **Composition**:constructor 不應執行 command side effect;外部 I/O 發生在 use case 執行期間。 - -文件與程式碼統一使用以下核心詞彙: - -| 詞彙 | 定義 | -|---|---| -| Wallet | 一個 key source;HD wallet 可衍生多個 account | -| Account | CLI 實際選取與操作的身份,以 `accountId` 識別 | -| Family | 共用地址格式、derivation 與簽名方式的鏈族,目前是 `tron` 或 `evm` | -| Canonical network id | 儲存與 machine output 使用的永久 network id,例如 `tron:3448148188`;alias 只供 CLI 輸入解析 | -| Keystore | 可互通的 Web3 V3 單一私鑰檔案,不是 wallet-cli 的內部儲存 | -| Vault | wallet-cli 內部的加密 secret blob,可能保存 seed,不可稱為 Keystore | - -## 完成定義 - -測試與 feature 放在一起:`src/**/*.test.ts` 是單元/邊界測試,`test/**/*.test.ts` 是從 CLI process 驗證的 golden/E2E 測試。至少執行: - -```bash -cd ts -npm run depcruise -npm run typecheck -npm run lint -npm run format:check -npm test -npm run build -``` - -送出前確認:新命令可由 `--help` 與 `--json-schema` 發現、text/JSON 都有覆蓋、錯誤碼穩定、沒有 secret 洩漏,且交易功能至少覆蓋 dry-run 與失敗路徑。 diff --git a/ts/docs/development/bai-recharge.md b/ts/docs/development/bai-recharge.md deleted file mode 100644 index 0af3d874e..000000000 --- a/ts/docs/development/bai-recharge.md +++ /dev/null @@ -1,196 +0,0 @@ -# B.AI recharge - -B.AI now authenticates recharge operations with each user's personal API key. -The CLI calls B.AI directly to resolve the credit recipient, create a preorder, -and report the payment transaction. The selected wallet signs the payment. - -## Payment flow - -1. Check the local confirmation for the API key, payer wallet and mainnet. -2. Validate the amount and trusted platform destination. Resolve `--to` when it - identifies another B.AI user. -3. Create the preorder with the personal API key. -4. Call the existing `X402Service.roundtrip()` with the platform destination, - token and exact amount. It starts a temporary endpoint on `127.0.0.1` using - an automatically allocated port, pays through `X402PaymentClient`, and closes - the endpoint in `finally`. -5. The local endpoint calls the facilitator's `/verify` and `/settle` endpoints. - Wallet account selection and signing use the existing x402 signer bridge. -6. Validate the successful settlement, network, transaction hash and payer. - Call `order.reportTxHash` with the original chain, amount and credit target. - -The personal API key is sent only to B.AI business APIs. Neither the local payment -endpoint nor the facilitator receives it. `--to` selects the account receiving -credits; the on-chain recipient is always the platform address. - -The CLI no longer calls the old recharge MCP or its merchant credit endpoint. -That removes a second credit-reporting path and a second set of recharge-server -configuration. The existing x402 SDK, facilitator and `roundtrip` remain in use. -Retiring the deployed recharge server is a separate operation. - -## Wallet binding and signed message - -Binding uses the personal API key to identify the B.AI user. Before signing, -construct the message using the recharge binding template from the updated API -specification. Arbitrary test text is rejected with `WalletInvalidSignature`, -even when the signature recovers the correct wallet address locally. - -```javascript -const message = [ - "Welcome to BAI !", - `${origin} wants you to confirm wallet binding for recharge:`, - address, - "", - `Chain ID: ${chainId}`, - `Expiration Time: ${expirationTime}`, - `Nonce: ${nonce}`, -].join("\n"); -``` - -For production, `origin` is `https://chat.bankofai.io`; the specification's -`https://chat-dev.b.ai` is the development example. Use the origin of the target -B.AI deployment. Mainnet chain IDs are `728126428` (TRON), `8453` (Base), and -`56` (BNB Chain). The live test used an ISO 8601 UTC expiration five minutes ahead -and a fresh 16-byte random nonce encoded as 32 hexadecimal characters; these are -verified client choices, not documented server limits or a server-issued challenge. - -Select the wallet explicitly when signing: - -```bash -wallet-cli message sign --account --network \ - --message "$message" --password-stdin -o json -``` - -Pass the master password through stdin. Send the returned `address`, unchanged -`message`, and `signature` to `POST /trpc/lambda/wallet.bindRechargeWallet`, inside -`{"json":{...}}`, with the personal API key as Bearer authentication. Set `chain` -to `tron`, `base`, or `bnb`; `version: 2` selects TRON V2 signing and was also -accepted on both EVM chains. Never trim, reformat, or rebuild the message after -signing. Binding signatures authorize account association; this step sends no -payment transaction. - -The backend canonicalizes EVM binding responses: `chain` becomes `eth`, and the -address is lowercase. The adapter accepts that family alias for `bnb`/`base`/`eth` -and compares EVM addresses without case sensitivity, while still rejecting another -address or unrelated chain. TRON addresses remain case-sensitive. The adapter -returns the server's canonical binding; subsequent network-specific checks still -use the original `base` or `bnb` request chain. - -On 2026-09-09, three different wallets were signed with Wallet CLI and bound using -one personal API key. Every successful binding returned the same user ID. After -each binding, all three original chain/address pairs were queried through -`wallet.isRechargeBound` using that same key: - -| After binding | TRON wallet | Base wallet | BNB Chain wallet | -| --- | --- | --- | --- | -| TRON | true | false | false | -| Base | true | true | false | -| BNB Chain | true | true | true | - -This verifies those three bindings coexist on the server; it does not establish an -unlimited wallet count or prove recharge settlement. No funds were transferred. -The local `bai-binding.json` still stores only the last confirmed API-key/chain/address -fingerprint. Switching wallet or network requires configuring the same key again -for that selection to refresh local confirmation; this does not remove server -bindings. CLI credential setup checks existing bindings, rather than creating one. -`BaiRechargeClient.bind()` accepts an already signed message; there is no automatic -binding or new binding command in this change. - -## Networks and payment requirements - -| Network | Token | B.AI payment scheme | -| --- | --- | --- | -| TRON mainnet | USDT, USDD | exact / Permit2 | -| BNB Chain mainnet | USDT | exact / Permit2 | -| Base mainnet | USDC | exact / EIP-3009 | - -B.AI uses `exact`; it does not select GasFree automatically. Generic x402 commands -continue to support TRON `exact_gasfree`. The local server owns token metadata, -including Base USDC's six decimals and EIP-712 domain version `2`. - -Platform addresses live in `adapters/outbound/config/bai-builtins.ts`. Only TRON, -BNB Chain and Base are retained. The allowlist cannot be overridden by user -configuration; changing it requires a CLI release. Minimum recharge rules remain -in `domain/bai/recharge-policy.ts`. - -Roundtrip enforces token, scheme, destination and exact amount before signing. -The explicit maximum equals the requested amount. On TRON, the SDK checks Permit2 -allowance and automatically signs, broadcasts and waits for an approval when it is -insufficient and the server has not declared approval resource sponsoring. The SDK -approves the maximum uint256 amount. A failed approval stops payment; tokens that -require resetting an existing allowance to zero may still require manual handling. -When the server declares approval resource sponsoring, the signed approval is sent -in the extension instead. EVM self-funded approval fallback is not implemented. -For EVM approval sponsoring, the x402 signer bridge maps the SDK transaction -`gas` field to wallet `gasLimit`, preserving an explicit `gasLimit` when supplied. - -## Failure and verification - -The roundtrip port validates the token and decimal precision before target resolution -and preorder creation, using the same adapter rules as server startup. Classified -payment errors retain their codes and any settlement evidence through the recharge -flow. Preorder failure stops payment. An uncertain payment is never retried automatically. -Only a successful settlement with a valid hash and matching network can be reported. -Reporting failure preserves the hash, original target and `retryPayment: false`. -`bai report-recharge --chain tron|bnb|base [--amount ]` -retries reporting without creating an order, resolving a recipient, signing or -paying. It requires the original personal API key but no local wallet. For another -recipient, supply both `--to ` and `--target-id ` -from `rechargeTarget`; omit both only for self recharge. Backend verification remains -authoritative. A persistent recovery log is not implemented; retain the JSON result. - -Settlement validation failures preserve a syntactically valid hash as -`details.candidateTxHash`, with a fixed `reason`, `paymentStatus: unknown`, -`settled: false` and `retryPayment: false`. Original chain, amount and recipient are -retained by the recharge flow. A candidate is evidence for reconciliation, not a -confirmed payment: verify it before using the report-only command. - -Tests cover the local HTTP roundtrip with the installed SDK on BSC and Base, -settlement validation, endpoint cleanup, self/recipient CLI orchestration and -reporting failure. The facilitator and B.AI backend are mocked. Real settlement, -credit attribution, repeated reporting and Ledger operation still need live -integration verification. - -## API failure diagnostics - -Both B.AI API adapters decode bounded HTTP error bodies and tRPC error envelopes. -Recognized business failures return `bai_rejected` with a fixed explanatory message -and `details.reason`, `procedure`, `httpStatus`, and `retryPayment: false`. -The recognized reasons are `WalletInvalidSignature`, `UNSUPPORTED_CHAIN`, -`TX_NOT_FOUND_OR_INVALID`, `UNSUPPORTED_TOKEN`, `PAYER_MISMATCH`, `WALLET_NOT_BOUND`, -`RECHARGE_TX_TOO_OLD`, `TX_TIMESTAMP_UNAVAILABLE`, `PRICE_UNAVAILABLE`, -`RECHARGE_AMOUNT_TOO_SMALL`, and `SELF_RECHARGE_TARGET` (the documented Chinese -self-recipient error). Signature rejection explains the required message fields -and wallet selection rather than blaming the signer. - -HTTP 401/403 retain `bai_auth_failed`; 429 retains `provider_rate_limited` without -waiting for an error body. Timeouts, oversized responses, malformed JSON and -connection failures remain distinguishable. Unknown server messages are never -copied into output; callers receive the operation, HTTP status and a safe message. -Invalid local recharge request fields return `invalid_value` before HTTP. - -A report result with `success: false` retains its existing business `code` and adds -a locally defined explanation. The recharge/report-recharge result keeps the hash, -original target, `creditStatus: unconfirmed` and `retryPayment: false`. Thrown API -errors also retain their structured error envelope inside that result. These are -credit failures after payment, not permission to repeat the payment. A failure to -retrieve a price or timestamp suggests retrying reporting only. Unknown report -codes retain the bounded code and a generic reconciliation instruction. - - -## 架构边界(2026-09-10) - -`bai recharge` 使用一份 ChainSpec,并注册 TRON / EVM FamilyBinding;查询和原交易补报仍是无需链上签名的 CommandDefinition。内部订单、补报 port 和恢复信息中的 amount 使用 decimal string,只有 BAI HTTP adapter 在发送 JSON 时转换为服务端要求的 number,并拒绝不能往返保留的金额。 - -接口调整:`bai report-recharge` 的 `data.amount` 以及充值失败恢复信息中的 `amount` 统一为字符串;`bai recharge` 的顶层付款金额原本就是字符串。BAI 服务端请求仍为 number,服务端返回的原始订单字段不做类型改写。 - -x402 支付保留协议专用流程,通过共用 signer 服务签名;适用范围与不支持的交易模式见 [架构指南](architecture.md#x402-協議支付的邊界)。 - - -## 确认延迟与上报恢复 - -CLI 在支付成功后立即上报;如果 BAI 返回 `TX_NOT_FOUND_OR_INVALID` 或 `TX_TIMESTAMP_UNAVAILABLE`,等待 15、20、25 秒后分别尝试上报同一笔交易,最多 4 次请求,总上报预算 90 秒。每个 HTTP 请求仍受 `--timeout` 限制,并受剩余上报预算的取消信号约束。手动 `bai report-recharge` 使用相同恢复策略。 - -该流程只等待并重试 BAI 对原交易的核验,不轮询链上 RPC,也不会重新创建订单、解析目标或付款。认证失败、付款人不匹配、其他拒绝及网络异常不自动重试。耗尽预算后返回 `creditStatus=unconfirmed`,保留原交易、链、金额、目标用户和 `retryPayment=false`,供后续补报。 - -x402 错误新增阶段信息:`request`、`challenge`、`create_payment`、`sign`、`payment_request`、`verify`、`settle`。能够识别的 HTTP 错误保留状态码,连接错误保留白名单中的错误码;结算失败保留合法的候选交易哈希和网络,不直接回显 SDK 消息、请求内容或凭证。 diff --git a/ts/docs/development/beta-release.md b/ts/docs/development/beta-release.md deleted file mode 100644 index 8f67e6569..000000000 --- a/ts/docs/development/beta-release.md +++ /dev/null @@ -1,67 +0,0 @@ -# TypeScript CLI beta release - -Release the TypeScript npm package as `@tron-walletcli/wallet-cli@4.14.0-beta.1` with the `beta` dist-tag. Keep the stable `latest` tag unchanged. The standalone Actions workflow builds downloadable artifacts; it does not publish the npm package automatically. - -## Prepare - -Use a clean checkout of the reviewed commit and Node.js 22. The package uses an explicit public-document allowlist; internal development and API reports are excluded. Run `npm run verify:package` to check the packed files and independently installed executable. - -Run from `ts/`: - -```sh -npm version 4.14.0-beta.1 --no-git-tag-version -npm ci -npm run typecheck -npm run lint -npm run format:check -npm run depcruise -npm test -- --maxWorkers=4 -npm run build -npm run verify:package -npm pack -``` - -The CLI version comes from `package.json`. The x402 core, TRON, EVM and fetch packages are build dependencies compiled into the CLI bundle, preserving the tested SDK implementations. Root-only npm overrides are insufficient for consumer installations. - -## Validate the actual tarball - -Install the generated tarball into a fresh directory outside the repository: - -```sh -npm init -y -npm install /absolute/path/to/tron-walletcli-wallet-cli-4.14.0-beta.1.tgz -./node_modules/.bin/wallet-cli --version -./node_modules/.bin/wallet-cli --help -``` - -Require CLI version `4.14.0-beta.1`. Verify TRON `2.0.0-beta.1` and core `1.1.1-beta.1` in the build checkout with `npm ls @bankofai/x402-core @bankofai/x402-tron`; the installed CLI must not import external x402 packages. Verify the installed entry point with mocked provider challenges on Base/BSC/TRON/Nile, BAI summary and rejection of Nile recharge, and 8004 reads and dry-run/build-only transactions. - -From the build checkout, run the artifact checks against that installed executable: - -```sh -WALLET_CLI_TEST_ENTRY=/absolute/path/to/node_modules/@tron-walletcli/wallet-cli/dist/index.js \ - npx vitest run test/x402-provider-payment.test.ts test/bai-nile-compatibility.test.ts test/erc8004.test.ts test/beta-artifact-signing.test.ts --maxWorkers=4 -``` - -These checks include real signing with a temporary encrypted wallet and simulated HTTP responses, without broadcasting a transaction. - -Before claiming production readiness, separately verify real chain receipts, Permit2 allowance requirements, BAI self/recipient credit attribution and duplicate transaction reporting with the backend. Offline settlement tests simulate broadcast and receipts; they do not establish actual chain execution. Physical Ledger verification remains a separate check. - -## Publish the verified artifact - -Use an npm account with write permission for this package. Publish the exact tested tarball: - -```sh -npm whoami -npm publish /absolute/path/to/tron-walletcli-wallet-cli-4.14.0-beta.1.tgz --tag beta --access public -npm view @tron-walletcli/wallet-cli dist-tags --json -``` - -Record the source commit, tarball SHA-256 and validation results. After publishing, confirm installation from the registry: - -```sh -npm install -g @tron-walletcli/wallet-cli@4.14.0-beta.1 -wallet-cli --version -``` - -A subsequent beta must use a new version, such as `4.14.0-beta.2`. Publish standalone archives separately after the platform workflow succeeds, and mark their GitHub release as a prerelease. diff --git a/ts/docs/development/erc8004-sdk-integration.md b/ts/docs/development/erc8004-sdk-integration.md deleted file mode 100644 index 884965dd9..000000000 --- a/ts/docs/development/erc8004-sdk-integration.md +++ /dev/null @@ -1,32 +0,0 @@ -# ERC-8004 SDK integration - -The SDK supplies registry configuration and ABIs. Wallet account selection, signing, -device interaction and transaction broadcasting belong to wallet-cli. No SDK -`ExternalSigner` adapter is required or planned for this integration. - -## What is already connected - -The CLI's eight Identity commands use the SDK's network configuration and ABI -through `adapters/outbound/erc8004/sdk-registry.ts`. RPC reads and receipt reads -continue through the wallet's configured gateway, preserving the API-key header, -timeout and TRON pacing. No indexer or subgraph is required for `show`. - -Transactions continue through existing EVM/TRON ContractService and TxPipeline. -That path already supports the wallet's existing software/device signers. This -change does not disable it while waiting for a new adapter. It retains dry-run, -build-only, sign-only, permission-id, expiration and `--wait` behavior. The SDK -registry adapter never calls SDK submit methods and never broadcasts. - -`show` returns authoritative chain fields and optional registration metadata. -Metadata failures produce warnings. Registration gets a minted agentId only -from a confirmed receipt. Update/transfer preserve submitted results and add -observed current URI/owner after confirmation. Failed post-confirmation reads -never cause a second transaction. Per-Agent approvals are ERC721 approvals: -`operator` and decimal `agentId`, not a fungible `allowance`. - -## Signing boundary - -The eight identity commands continue through ContractService and TxPipeline. -They do not call SDK submission methods and do not supply private keys to the SDK. -The SDK's custom external-signer extension was removed in `1.2.0-beta.1`; -this does not remove wallet-cli's independent x402 signer bridge. diff --git a/ts/docs/machine-interface.md b/ts/docs/machine-interface.md index 98257e668..8ccdfa5ae 100644 --- a/ts/docs/machine-interface.md +++ b/ts/docs/machine-interface.md @@ -483,7 +483,7 @@ for supported reasons and recovery behavior. BAI application amounts use decimal strings. `bai report-recharge` returns `data.amount` as a string when supplied; recharge recovery/error context also carries a string amount. `bai recharge` already returns its payment amount as a string. This standardizes the new -v4.14 interface, whose earlier development build returned numbers in report/recovery fields. +v4.14.0 interface, whose earlier development build returned numbers in report/recovery fields. Raw BAI order fields retain their server-provided types. The outbound BAI API request still uses a JSON number after decimal round-trip validation; clients must not infer the CLI result type from that HTTP request format. diff --git a/ts/package-lock.json b/ts/package-lock.json index f849788e4..51983bd81 100644 --- a/ts/package-lock.json +++ b/ts/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tron-walletcli/wallet-cli", - "version": "4.14.0-beta.1", + "version": "4.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tron-walletcli/wallet-cli", - "version": "4.14.0-beta.1", + "version": "4.14.0", "license": "LGPL-3.0-or-later", "dependencies": { "@bankofai/8004-sdk": "1.2.0-beta.1", diff --git a/ts/package.json b/ts/package.json index 37d79e3be..54426d25e 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,6 +1,6 @@ { "name": "@tron-walletcli/wallet-cli", - "version": "4.14.0-beta.1", + "version": "4.14.0", "description": "Agent-first TypeScript CLI wallet for TRON — deterministic commands, JSON output, and discoverable schemas", "type": "module", "bin": { From 850f14e9cea4106c4c4970617edca08784a27bad Mon Sep 17 00:00:00 2001 From: boboliu-1010 Date: Thu, 17 Sep 2026 12:47:42 +0800 Subject: [PATCH 3/3] fix(release): remove stale references to deleted development docs --- ts/README.md | 2 +- ts/docs/machine-interface.md | 4 ++-- ts/package.json | 1 - ts/scripts/verify-package.mjs | 6 +----- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/ts/README.md b/ts/README.md index 242b1dfb3..0f47dbb6c 100644 --- a/ts/README.md +++ b/ts/README.md @@ -222,7 +222,7 @@ registered ID or re-reads URI/owner after successful confirmation; an unconfirme transaction is returned as submitted and must not be blindly retried. Registry configuration stays in the SDK; signing and broadcasting stay in the wallet -transaction pipeline. See [the SDK integration](docs/development/erc8004-sdk-integration.md). +transaction pipeline. ## B.AI usage and x402 providers diff --git a/ts/docs/machine-interface.md b/ts/docs/machine-interface.md index 8ccdfa5ae..27ed0e94b 100644 --- a/ts/docs/machine-interface.md +++ b/ts/docs/machine-interface.md @@ -474,8 +474,8 @@ raw server prose and credentials are not returned. `retryPayment: false` means that retry guidance applies to the API operation, not to sending funds again. Report-only failures retain the transaction hash and `creditStatus: unconfirmed`; the result contains a business `code` and explanatory `warning`, or an `error` -envelope for a thrown classified API failure. See `development/bai-recharge.md` -for supported reasons and recovery behavior. +envelope for a thrown classified API failure. Use `bai report-recharge` with the +original transaction details to retry reporting without creating another payment. ### BAI amount representation diff --git a/ts/package.json b/ts/package.json index 54426d25e..1e6429a0d 100644 --- a/ts/package.json +++ b/ts/package.json @@ -13,7 +13,6 @@ "docs/guide", "docs/machine-interface.md", "docs/troubleshooting.md", - "docs/development/erc8004-sdk-integration.md", "README.md", "LICENSE" ], diff --git a/ts/scripts/verify-package.mjs b/ts/scripts/verify-package.mjs index bc5cf3677..26f4d0e7a 100644 --- a/ts/scripts/verify-package.mjs +++ b/ts/scripts/verify-package.mjs @@ -17,11 +17,7 @@ try { // Never validate a stale dist left by an earlier build. call(npm, ["run", "build"]); const [packed] = JSON.parse(call(npm, ["pack", "--json", "--pack-destination", temp])); - const forbidden = packed.files.filter( - ({ path }) => - path.startsWith("docs/development/") && - path !== "docs/development/erc8004-sdk-integration.md", - ); + const forbidden = packed.files.filter(({ path }) => path.startsWith("docs/development/")); assert.equal(forbidden.length, 0, "internal development reports must not be packaged"); assert( packed.files.some(({ path }) => path === "dist/index.js"),