diff --git a/examples/typescript/README.md b/examples/typescript/README.md index 31a6f245..1e4cc392 100644 --- a/examples/typescript/README.md +++ b/examples/typescript/README.md @@ -15,6 +15,13 @@ A full client → server → facilitator loop: | [`facilitator/basic`](facilitator/basic) | verifies + settles on-chain | 4022 | agent-wallet (EVM + TRON) | | [`servers/express`](servers/express) | sells `GET /weather` behind 402 | 4021 | none (keyless, payout address only) | | [`clients/fetch`](clients/fetch) | pays automatically via wrapped `fetch` | — | agent-wallet (EVM + TRON) | +| [`servers/mcp`](servers/mcp) | sells the `get_weather` MCP tool behind 402 | 4023 | none (keyless, payout address only) | +| [`clients/mcp`](clients/mcp) | pays automatically over MCP/SSE | — | agent-wallet (EVM + TRON) | + +The `mcp` pair is the same line over an **MCP transport** — it shares +`facilitator/basic` and `.env-exact`, swapping the HTTP server/client for an MCP +server (`pnpm dev:mcp-server`, :4023) and client (`pnpm dev:mcp-client`); `ping` +is free, `get_weather` is paid. ## GasFree scenario (TRON `exact_gasfree`) @@ -95,6 +102,7 @@ amount and strips the header. BSC uses USDC, TRON Nile uses USDT — both Permit | TRON nile | `tron:0xcd8690dc` | USDD (18) | exact **permit2** | yes — client auto-broadcasts | only the approve (TRX) | | BSC testnet | `eip155:97` | DHLU (6) | exact **eip3009** | none | **none (fully gasless)** | | BSC testnet | `eip155:97` | USDC (18) | exact **permit2 + gas-sponsoring** | yes — client signs, facilitator relays | only the approve (BNB) | +| BSC testnet | `eip155:97` | USDT (18) | exact **permit2 + gas-sponsoring** | yes — client signs, facilitator relays | only the approve (BNB) | How each token settles on-chain: @@ -106,8 +114,8 @@ How each token settles on-chain: `approve(Permit2)` (`ensureAllowance`, ~3s); the facilitator settles via `x402Permit2Proxy.settle`. TRON has no gas delegation, so that approve costs the user a little TRX. -- **BSC USDC — permit2 + gas-sponsoring:** USDC is a plain BEP-20 (no - ERC-3009 / no EIP-2612), so it needs an on-chain `approve(Permit2)`. The +- **BSC USDC/USDT — permit2 + gas-sponsoring:** both are plain BEP-20 (no + ERC-3009 / no EIP-2612), so each needs an on-chain `approve(Permit2)`. The client signs that approve **offline**; the facilitator broadcasts it bundled with settle (the `erc20ApprovalGasSponsoring` extension). This removes the separate manual approve step — but the approve's `from` is the user, so on a @@ -119,22 +127,25 @@ authorizer/fee-payer split, so the **client** broadcasts it; EVM can decouple them, so the **facilitator** relays it (leaving the door open to real sponsorship). -### Mainnet (commented-ready, real funds) +### Mainnet (real funds) -Production tokens are pre-listed as **commented** entries in the same tables, so -enabling them is config-only — no logic changes (see "Adding a chain/token" -below). All verified on-chain; sources: the official +Mainnet networks (`eip155:56`, `tron:0x2b6653dc`) are registered alongside their +testnet counterparts in the same tables — no uncommenting needed. The server +advertises both networks, and the client selects which one to pay through +`PAY_TARGETS`. The example uses one `EVM_ADDRESS` / `TRON_ADDRESS` payout address +for both the testnet and mainnet entries; configure addresses that are valid for +that shared setup, and set a reliable `EVM_RPC_URL` for BSC. ⚠️ **REAL FUNDS.** +All verified on-chain; sources: the official [Network & Token Support](https://docs.bankofai.io/x402/core-concepts/network-and-token-support/) docs. | Network | Token | Address | Dec | Method | |---|---|---|---|---| | `eip155:56` (BSC) | USDC | `0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d` | 18 | permit2 | | `eip155:56` (BSC) | USDT | `0x55d398326f99059fF775485246999027B3197955` | 18 | permit2 | -| `eip155:56` (BSC) | EPS | `0xA7f552078dcC247C2684336020c03648500C6d9F` | 18 | permit2 | | `tron:0x2b6653dc` | USDT | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` | 6 | permit2 | | `tron:0x2b6653dc` | USDD | `TXDk8mbtRbXeYuMNS83CfKPaYYT8XWv9Hz` | 18 | permit2 | -> BSC mainnet has **no ERC-3009 token** (USDC/USDT/EPS are all plain BEP-20) — so +> BSC mainnet has **no ERC-3009 token** (USDC/USDT are plain BEP-20) — so > mainnet EVM payments all go permit2 + gas-sponsored approve. DHLU (eip3009) is > BSC-testnet-only. @@ -169,11 +180,11 @@ chain reads and broadcast. ## Run Env is **split by business scenario** — one self-contained file per line, so the -three lines run side by side and you only fill in what you run: +four lines run side by side and you only fill in what you run: | Scenario | Template → fill | Loaded by | |---|---|---| -| exact (main) | `.env-exact.example` → `.env-exact` | `clients/fetch`, `servers/express`, `facilitator/basic` | +| exact (main) | `.env-exact.example` → `.env-exact` | `clients/fetch`, `servers/express`, `facilitator/basic`, `clients/mcp`, `servers/mcp` | | gasfree | `.env-gasfree.example` → `.env-gasfree` | `clients/gasfree`, `servers/gasfree`, `facilitator/gasfree` | | batch-settlement | `.env-batch.example` → `.env-batch` | `clients/batch-settlement`, `servers/batch-settlement`, `facilitator/batch-settlement` | | upto | `.env-upto.example` → `.env-upto` | `clients/upto`, `servers/upto`, `facilitator/upto` | diff --git a/examples/typescript/clients/fetch/README.md b/examples/typescript/clients/fetch/README.md index ed759e0f..b5198b38 100644 --- a/examples/typescript/clients/fetch/README.md +++ b/examples/typescript/clients/fetch/README.md @@ -8,11 +8,12 @@ Wraps `fetch` so HTTP `402 Payment Required` challenges are paid automatically. Both chains sign via `@bankofai/agent-wallet` (`resolveWallet({ network })`): -- EVM: `createClientEvmSigner(wallet, publicClient)` — adapts the wallet - (async address + `0x`-normalized signatures) and wires `readContract` for - permit2 enrichment. -- TRON: `createClientTronSigner(tronWeb, agentWallet)` — the SDK takes its - `AgentWallet` shape, so `chains/tron.ts` adapts the raw wallet inline. +- EVM: `createClientEvmSigner(wallet, { network, rpcUrl })` — adapts the wallet + (async address + `0x`-normalized signatures); the factory builds the viem + `publicClient` internally and wires `readContract` for permit2 enrichment. +- TRON: `createClientTronSigner(wallet, { network, apiKey })` — the factory + builds TronWeb internally from the network and auto-broadcasts the one-time + Permit2 approve for USDT/USDD. A chain registers only if its wallet resolves, so you can pay from EVM-only, TRON-only, or both. diff --git a/examples/typescript/facilitator/basic/README.md b/examples/typescript/facilitator/basic/README.md index 68511247..37da78f2 100644 --- a/examples/typescript/facilitator/basic/README.md +++ b/examples/typescript/facilitator/basic/README.md @@ -8,13 +8,13 @@ HTTP layer (`src/index.ts`) is **chain-agnostic** and dispatches by the payment' The signer factories are **wallet-only**: -- EVM: `createFacilitatorEvmSigner(publicClient, wallet)` -- TRON: `createFacilitatorTronSigner(tronWeb, wallet)` +- EVM: `createFacilitatorEvmSigner(wallet, { network, rpcUrl })` +- TRON: `createFacilitatorTronSigner(wallet, { network, apiKey })` The `wallet` comes from `resolveWallet({ network })` in `@bankofai/agent-wallet`, which loads the secret out-of-band (env / keystore / Privy). **This example never -reads a private key.** The viem `publicClient` / `TronWeb` carry no account — they -only do chain reads and broadcast. +reads a private key.** The factories build the viem `publicClient` / `TronWeb` +internally and they carry no account — they only do chain reads and broadcast. A chain registers only if its wallet resolves, so you can run EVM-only, TRON-only, or both. diff --git a/typescript/.changeset/tron-remove-advisory-fee.md b/typescript/.changeset/tron-remove-advisory-fee.md new file mode 100644 index 00000000..62d39f4d --- /dev/null +++ b/typescript/.changeset/tron-remove-advisory-fee.md @@ -0,0 +1,11 @@ +--- +"@bankofai/x402-tron": major +--- + +**Breaking:** Remove advisory fee configuration and metadata from the TRON +`exact` and `upto` schemes. These schemes now transfer exactly the requested +amount and no longer accept a fee configuration through +`TronFacilitatorConfig` or their facilitator constructors. + +GasFree fee handling is unchanged because those fees are enforced by the +GasFree relayer rather than advertised as advisory payment metadata. diff --git a/typescript/packages/mechanisms/tron/README.md b/typescript/packages/mechanisms/tron/README.md index c082ce5e..6931bf8d 100644 --- a/typescript/packages/mechanisms/tron/README.md +++ b/typescript/packages/mechanisms/tron/README.md @@ -18,6 +18,19 @@ This package provides the three x402 mechanism roles for TRON, mirroring `@banko It plugs into `@bankofai/x402-core` via the `tron:*` CAIP family — no core changes are required. +### Fee behavior + +The `exact` and `upto` schemes do not advertise or collect a facilitator fee: +their deployed proxies transfer exactly the payment amount. Facilitator fees are +supported only by `exact_gasfree`, where the relayer deducts the configured fee +from the GasFree wallet. + +When upgrading from the advisory-fee API, remove the `fee` property from +`TronFacilitatorConfig` and stop passing a fee configuration as the second +argument to the `exact` or `upto` facilitator scheme constructors. Pass the fee +configuration only when registering or constructing an `exact_gasfree` +facilitator. + ## Two transfer paths (and why TRON is effectively Permit2) The exact scheme supports two `assetTransferMethod`s, selected via `extra.assetTransferMethod`: @@ -37,7 +50,7 @@ The `permit2` path binds the destination with a Permit2 **witness**. For the exa Witness(address to, uint256 validAfter) ``` -The client TIP-712 type, the proxy `settle` ABI, and the on-chain `WITNESS_TYPEHASH` must stay in sync — `test/unit/permit2-digest.test.ts` enforces this. (The `upto` proxy uses a 3-field witness with a `facilitator` field; this package implements `exact` only.) +The client TIP-712 type, the proxy `settle` ABI, and the on-chain `WITNESS_TYPEHASH` must stay in sync — `test/unit/permit2-digest.test.ts` enforces this. (The `upto` proxy uses a 3-field witness with a `facilitator` field; this section describes the `exact` witness only.) ## Package exports @@ -47,6 +60,13 @@ The client TIP-712 type, the proxy `settle` ABI, and the on-chain `WITNESS_TYPEH | `@bankofai/x402-tron/exact/client` | client scheme + register + Permit2 helpers | | `@bankofai/x402-tron/exact/server` | server scheme + register | | `@bankofai/x402-tron/exact/facilitator` | facilitator scheme + register | +| `@bankofai/x402-tron/upto/client` | upto client scheme + register + Permit2 helpers | +| `@bankofai/x402-tron/upto/server` | upto server scheme + register | +| `@bankofai/x402-tron/upto/facilitator` | upto facilitator scheme + register | +| `@bankofai/x402-tron/gasfree/client` | exact_gasfree client scheme + register | +| `@bankofai/x402-tron/gasfree/server` | exact_gasfree server scheme + register | +| `@bankofai/x402-tron/gasfree/facilitator` | exact_gasfree facilitator scheme + register | +| `@bankofai/x402-tron/batch-settlement/*` | batch-settlement client/server/facilitator | ## Usage @@ -57,8 +77,8 @@ import { TronWeb } from "tronweb"; import { createClientTronSigner, createFacilitatorTronSigner, - type AgentWallet, - type FacilitatorAgentWallet, + type ClientTronWallet, + type FacilitatorTronWallet, } from "@bankofai/x402-tron"; const privateKey = process.env.TRON_PRIVATE_KEY!.replace(/^0x/, ""); @@ -66,8 +86,8 @@ const tronWeb = new TronWeb({ fullHost: "https://nile.trongrid.io" }); const address = TronWeb.address.fromPrivateKey(privateKey) as string; // Client: signs TIP-712 typed data. The private key stays in your wallet — -// the SDK only sees the AgentWallet interface, never the raw key. -const clientWallet: AgentWallet = { +// the SDK only sees the ClientTronWallet interface, never the raw key. +const clientWallet: ClientTronWallet = { getAddress: () => address, async signTypedData(args) { const sig = await tronWeb.trx._signTypedData( @@ -79,22 +99,26 @@ const clientWallet: AgentWallet = { return (sig.startsWith("0x") ? sig : `0x${sig}`) as `0x${string}`; }, }; -const clientSigner = await createClientTronSigner(tronWeb, clientWallet); +const clientSigner = await createClientTronSigner(clientWallet, { + network: "tron:0xcd8690dc", +}); // Facilitator: signs built settlement transactions for on-chain broadcast. -const facilitatorWallet: FacilitatorAgentWallet = { - address, +const facilitatorWallet: FacilitatorTronWallet = { + getAddress: () => address, async signTransaction(transaction) { return tronWeb.trx.sign(transaction, privateKey); }, }; -const facilitatorSigner = createFacilitatorTronSigner(tronWeb, facilitatorWallet); +const facilitatorSigner = await createFacilitatorTronSigner(facilitatorWallet, { + network: "tron:0xcd8690dc", +}); ``` -`createClientTronSigner` is async (it resolves the wallet address); -`createFacilitatorTronSigner` is synchronous. Both also expose -`toClientTronSigner` / `toFacilitatorTronSigner` adapters if you already have a -signing object that matches the full signer shape. +Both `createClientTronSigner` and `createFacilitatorTronSigner` are `async` +(they resolve the wallet address and build the TronWeb client from `network`). +Pass the wallet as the first argument and `{ network, rpcUrl?, apiKey? }` as +options — the factories build the TronWeb instance internally. ### Client @@ -131,10 +155,14 @@ The `permit2` path depends on three on-chain contracts. Addresses live in `src/c | Network | chainId (TIP-712) | Permit2 | x402ExactPermit2Proxy | | --- | --- | --- | --- | | `tron:0xcd8690dc` | 3448148188 | `TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h` | `TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F` | -| `tron:0x2b6653dc` | 728126428 | *(placeholder — replace)* | *(placeholder — replace)* | +| `tron:0x2b6653dc` | 728126428 | `TTJxU3P8rHycAyFY4kVtGNfmnMH4ezcuM9` | `TN49yaJmZMZoEdDCqjB4uPzQLHvYkGw95m` | | `tron:0x94a9059e` | 2494104990 | — (no Permit2 deployment) | — | -> ⚠️ **Before mainnet:** deploy your own audited Permit2 + `x402ExactPermit2Proxy` and replace the addresses in `constants.ts`. Do not reuse placeholder/testnet addresses. The proxy `PERMIT2()` immutable must equal the configured Permit2 address, and its `WITNESS_TYPEHASH` must match the 2-field witness. +> ⚠️ Mainnet uses real funds. The configured Permit2 and +> `x402ExactPermit2Proxy` addresses are the deployed mainnet contracts; verify +> them against the current release configuration before production use. The +> proxy `PERMIT2()` immutable must equal the configured Permit2 address, and its +> `WITNESS_TYPEHASH` must match the 2-field witness. ## Testing @@ -148,7 +176,7 @@ The suite is fully offline (no network, no keys): - `permit2-digest.test.ts` — reproduces the exact on-chain Permit2 digest and recovers the signer two independent ways (the facilitator verify path and a manual contract-style reconstruction), guaranteeing TIP-712 hashing matches the deployed proxy. - `gasfree-digest.test.ts` / `gasfree-flow.test.ts` — GasFree TIP-712 sign↔verify round-trip plus client/facilitator term-validation and settle flow against a mocked relayer. -- `tokens.test.ts`, `fee.test.ts`, `fee-plumbing.test.ts`, `selection.test.ts`, `signer-wallet.test.ts` — token registry, fee policy, fee advertisement plumbing, token selection / balance filtering, and the AgentWallet abstraction. +- `tokens.test.ts`, `fee.test.ts`, `selection.test.ts`, `signer-wallet.test.ts` — token registry, fee policy, token selection / balance filtering, and the ClientTronWallet abstraction. ## Notes & caveats diff --git a/typescript/packages/mechanisms/tron/src/exact/client/permit2.ts b/typescript/packages/mechanisms/tron/src/exact/client/permit2.ts index a662e2d9..d3fec566 100644 --- a/typescript/packages/mechanisms/tron/src/exact/client/permit2.ts +++ b/typescript/packages/mechanisms/tron/src/exact/client/permit2.ts @@ -6,7 +6,6 @@ import { } from "../../constants"; import { ClientTronSigner } from "../../signer"; import { ExactPermit2Payload } from "../../types"; -import { readFeeFromExtra } from "../../shared/fee"; import { createNonce, getTronChainId, normalizeAddressForSigning } from "../../utils"; /** @@ -63,13 +62,11 @@ export async function createPermit2Payload( // Ensure the one-time Permit2 allowance before signing (mirrors the Python // client). No-op when the signer can't broadcast (sign-only wallet) or when - // the allowance already covers payment + fee. TRON's mainstream tokens + // the allowance already covers the payment. TRON's mainstream tokens // (USDT/USDD) lack ERC-3009, so this approve is required on first use. - const feeAmount = readFeeFromExtra(paymentRequirements.extra)?.feeAmount ?? "0"; - const totalRequired = BigInt(paymentRequirements.amount) + BigInt(feeAmount); await signer.ensureAllowance?.({ token: paymentRequirements.asset, - amount: totalRequired, + amount: BigInt(paymentRequirements.amount), network, }); diff --git a/typescript/packages/mechanisms/tron/src/exact/facilitator/register.ts b/typescript/packages/mechanisms/tron/src/exact/facilitator/register.ts index 717bc42e..51df417f 100644 --- a/typescript/packages/mechanisms/tron/src/exact/facilitator/register.ts +++ b/typescript/packages/mechanisms/tron/src/exact/facilitator/register.ts @@ -1,7 +1,6 @@ import { x402Facilitator } from "@bankofai/x402-core/facilitator"; import { Network } from "@bankofai/x402-core/types"; import { FacilitatorTronSigner } from "../../signer"; -import { ExactTronFeeConfig } from "../../shared/fee"; import { ExactTronScheme } from "./scheme"; /** @@ -10,8 +9,6 @@ import { ExactTronScheme } from "./scheme"; export interface TronFacilitatorConfig { signer: FacilitatorTronSigner; networks: Network | Network[]; - /** Optional facilitator fee configuration (advertised via getExtra). */ - fee?: ExactTronFeeConfig; } /** @@ -25,6 +22,6 @@ export function registerExactTronScheme( facilitator: x402Facilitator, config: TronFacilitatorConfig, ): x402Facilitator { - facilitator.register(config.networks, new ExactTronScheme(config.signer, config.fee)); + facilitator.register(config.networks, new ExactTronScheme(config.signer)); return facilitator; } diff --git a/typescript/packages/mechanisms/tron/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/tron/src/exact/facilitator/scheme.ts index 040b0c24..098aa219 100644 --- a/typescript/packages/mechanisms/tron/src/exact/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/tron/src/exact/facilitator/scheme.ts @@ -9,7 +9,6 @@ import { import { FacilitatorTronSigner } from "../../signer"; import { ExactEIP3009Payload, ExactTronPayload, isPermit2Payload } from "../../types"; import { X402_PERMIT2_PROXY_ADDRESSES } from "../../constants"; -import { ExactTronFeeConfig } from "../../shared/fee"; import { verifyEIP3009, settleEIP3009 } from "./eip3009"; import { verifyPermit2, settlePermit2 } from "./permit2"; @@ -25,15 +24,8 @@ export class ExactTronScheme implements SchemeNetworkFacilitator { * Creates a new ExactTronScheme facilitator instance. * * @param signer - The TRON signer for facilitator operations - * @param feeConfig - Optional facilitator fee configuration. On the deployed - * exact/permit2 proxy the fee is advisory only (the proxy transfers exactly - * `amount` and does not split a fee); it is advertised so clients and other - * schemes (e.g. GasFree) can observe and enforce it. */ - constructor( - private readonly signer: FacilitatorTronSigner, - private readonly feeConfig: ExactTronFeeConfig = {}, - ) {} + constructor(private readonly signer: FacilitatorTronSigner) {} /** * Gets extra configuration for the facilitator. @@ -55,21 +47,6 @@ export class ExactTronScheme implements SchemeNetworkFacilitator { ...(signers.length > 0 && X402_PERMIT2_PROXY_ADDRESSES[network] ? { permit2FacilitatorAddress: signers[0] } : {}), - // Advertise the fee configuration so the server can attach per-asset fee - // terms to requirements (extra.fee) and clients/other schemes can observe - // it. Only emitted when a baseFee is configured. - ...(this.feeConfig.baseFee - ? { - feeConfig: { - feeTo: this.feeConfig.feeTo ?? signers[0], - ...(this.feeConfig.caller ? { caller: this.feeConfig.caller } : {}), - baseFee: this.feeConfig.baseFee, - ...(this.feeConfig.allowedTokens - ? { allowedTokens: this.feeConfig.allowedTokens } - : {}), - }, - } - : {}), }; } diff --git a/typescript/packages/mechanisms/tron/src/exact/server/scheme.ts b/typescript/packages/mechanisms/tron/src/exact/server/scheme.ts index 13eef7f3..8e9cabce 100644 --- a/typescript/packages/mechanisms/tron/src/exact/server/scheme.ts +++ b/typescript/packages/mechanisms/tron/src/exact/server/scheme.ts @@ -13,7 +13,6 @@ import { } from "@bankofai/x402-core/utils"; import { ExactDefaultAssetInfo, getDefaultAsset } from "../../shared/defaultAssets"; import { getDecimals, parsePrice as parseTokenPrice } from "../../shared/tokens"; -import { buildFeeInfo, type ExactTronFeeConfig } from "../../shared/fee"; /** * TRON server implementation for the Exact payment scheme. @@ -115,6 +114,10 @@ export class ExactTronScheme implements SchemeNetworkServer { ): Promise { void extensionKeys; + const extra = { ...paymentRequirements.extra }; + delete extra.fee; + const requirementsWithoutFee = { ...paymentRequirements, extra }; + const supportedMethods = supportedKind.extra?.supportedAssetTransferMethods as | string[] | undefined; @@ -128,35 +131,19 @@ export class ExactTronScheme implements SchemeNetworkServer { : undefined); if (!method) { - return Promise.resolve(paymentRequirements); + return Promise.resolve(requirementsWithoutFee); } const permit2FacilitatorAddress = (paymentRequirements.extra?.permit2FacilitatorAddress as string | undefined) ?? (supportedKind.extra?.permit2FacilitatorAddress as string | undefined); - // Attach per-asset fee terms from the facilitator's advertised fee config, - // unless a fee was already set upstream. - const feeConfig = supportedKind.extra?.feeConfig as ExactTronFeeConfig | undefined; - const existingFee = paymentRequirements.extra?.fee; - const fee = - existingFee ?? - (feeConfig - ? buildFeeInfo( - feeConfig, - supportedKind.network, - paymentRequirements.asset, - feeConfig.feeTo ?? "", - ) - : undefined); - return Promise.resolve({ - ...paymentRequirements, + ...requirementsWithoutFee, extra: { - ...paymentRequirements.extra, + ...extra, assetTransferMethod: method, ...(method === "permit2" && permit2FacilitatorAddress ? { permit2FacilitatorAddress } : {}), - ...(fee ? { fee } : {}), }, }); } diff --git a/typescript/packages/mechanisms/tron/src/index.ts b/typescript/packages/mechanisms/tron/src/index.ts index d9e5b1c0..63e643d2 100644 --- a/typescript/packages/mechanisms/tron/src/index.ts +++ b/typescript/packages/mechanisms/tron/src/index.ts @@ -76,7 +76,7 @@ export { } from "./shared/tokens"; // Fee -export { type FeeInfo, type ExactTronFeeConfig } from "./shared/fee"; +export { type FeeInfo, type ExactTronFeeConfig, type GasFreeTronFeeConfig } from "./shared/fee"; // Token selection + balance-aware selection export { diff --git a/typescript/packages/mechanisms/tron/src/shared/balance.ts b/typescript/packages/mechanisms/tron/src/shared/balance.ts index c28e27c6..54c1234d 100644 --- a/typescript/packages/mechanisms/tron/src/shared/balance.ts +++ b/typescript/packages/mechanisms/tron/src/shared/balance.ts @@ -1,6 +1,6 @@ import type { PaymentRequirements } from "@bankofai/x402-core/types"; -import { readFeeFromExtra } from "./fee"; import { CheapestTokenSelectionStrategy, type TokenSelectionStrategy } from "./tokenSelection"; +import { readFeeFromExtra } from "./fee"; /** * Balance-aware payment selection for TRON. @@ -23,10 +23,12 @@ export interface BalanceCheckable { } /** - * Filter out requirements the payer cannot afford (amount + fee). + * Filter out requirements the payer cannot afford. * * Requirements whose balance cannot be determined are kept, deferring the - * decision to createPaymentPayload. Fee is read from `extra.fee` when present. + * decision to createPaymentPayload. Only `exact_gasfree` adds its enforced + * relayer fee to the required amount; `exact` and `upto` proxies transfer + * exactly `amount` and carry no fee, so stale `extra.fee` metadata is ignored. * * @param scheme - A client scheme exposing checkBalance. * @param accepts - The candidate payment requirements. @@ -47,9 +49,11 @@ export async function filterAffordableRequirements( continue; } let needed = BigInt(req.amount); - const fee = readFeeFromExtra(req.extra); - if (fee) { - needed += BigInt(fee.feeAmount); + if (req.scheme === "exact_gasfree") { + const fee = readFeeFromExtra(req.extra); + if (fee) { + needed += BigInt(fee.feeAmount); + } } if (balance >= needed) { affordable.push(req); diff --git a/typescript/packages/mechanisms/tron/src/shared/fee.ts b/typescript/packages/mechanisms/tron/src/shared/fee.ts index 969b42d6..a4e6b6a7 100644 --- a/typescript/packages/mechanisms/tron/src/shared/fee.ts +++ b/typescript/packages/mechanisms/tron/src/shared/fee.ts @@ -7,9 +7,9 @@ import { findByAddress } from "./tokens"; * On TRON, energy (gas) for a TRC-20 settlement costs real value, so a * facilitator must recoup it from the payment token — unlike Coinbase's Base * deployment where gas is negligible and the protocol carries no fee. This - * module holds the fee data model and pure validation helpers shared by the - * `exact` scheme (advisory advertisement) and `exact_gasfree` (where the - * provider actually deducts `feeAmount` from the GasFree wallet). + * module holds the fee data model and helpers used by `exact_gasfree`, where the + * relayer actually deducts `feeAmount` from the GasFree wallet. The `exact` and + * `upto` schemes carry no fee (the proxy transfers exactly `amount`). */ /** @@ -27,11 +27,9 @@ export interface FeeInfo { /** * Facilitator-side fee configuration. * - * The fee's effect differs by scheme: on `exact` / `exact_permit` it is - * **advisory** — advertised via `getExtra` so clients and other schemes can - * observe it, but the proxy transfers exactly `amount` and does NOT split a fee. - * Only on `exact_gasfree` is it **enforced** — the relayer actually deducts - * `feeAmount` from the GasFree wallet. + * The fee is **enforced** on `exact_gasfree` — the relayer actually deducts + * `feeAmount` from the GasFree wallet. The `exact` and `upto` schemes carry no + * fee (their proxies transfer exactly `amount` and do not split a fee). */ export interface ExactTronFeeConfig { /** Address that collects the fee. Defaults to the facilitator signer address. */ @@ -50,6 +48,13 @@ export interface ExactTronFeeConfig { allowedTokens?: ReadonlyArray; } +/** + * Alias reflecting the current semantics: this config drives the enforced + * relayer fee on `exact_gasfree` only. `ExactTronFeeConfig` is retained for + * backward compatibility. + */ +export type GasFreeTronFeeConfig = ExactTronFeeConfig; + /** * Resolve the configured base fee for an asset on a network. * @@ -111,50 +116,6 @@ export function buildFeeInfo( }; } -/** Fee validation error reasons. */ -export const FEE_TOKEN_NOT_ALLOWED = "fee_token_not_allowed"; -export const FEE_UNSUPPORTED_TOKEN = "fee_unsupported_token"; -export const FEE_AMOUNT_TOO_LOW = "fee_amount_too_low"; -export const FEE_TO_MISMATCH = "fee_to_mismatch"; - -/** - * Validate a presented {@link FeeInfo} against the facilitator's config. - * - * Pure (no I/O). Returns an error reason string on the first failure, or null - * when the presented fee is acceptable. - * - * @param config - The facilitator fee configuration. - * @param network - CAIP-2 network identifier. - * @param asset - TRC-20 contract address being paid with. - * @param presented - The fee terms carried by the payment payload/requirement. - * @param normalize - Optional address normalizer for `feeTo` comparison - * (defaults to lowercase). Pass an EVM-hex normalizer when - * comparing TRON Base58 against hex. - * @returns An error reason, or null if valid. - */ -export function validateFee( - config: ExactTronFeeConfig, - network: Network, - asset: string, - presented: FeeInfo, - normalize: (addr: string) => string = a => a.toLowerCase(), -): string | null { - if (!isTokenAllowed(config, asset)) { - return FEE_TOKEN_NOT_ALLOWED; - } - const baseFee = resolveBaseFee(config, network, asset); - if (baseFee === null) { - return FEE_UNSUPPORTED_TOKEN; - } - if (BigInt(presented.feeAmount) < baseFee) { - return FEE_AMOUNT_TOO_LOW; - } - if (config.feeTo && normalize(presented.feeTo) !== normalize(config.feeTo)) { - return FEE_TO_MISMATCH; - } - return null; -} - /** * Extract a {@link FeeInfo} from a requirement/supported-kind `extra` object. * diff --git a/typescript/packages/mechanisms/tron/src/upto/client/permit2.ts b/typescript/packages/mechanisms/tron/src/upto/client/permit2.ts index 015b834f..8d2b441b 100644 --- a/typescript/packages/mechanisms/tron/src/upto/client/permit2.ts +++ b/typescript/packages/mechanisms/tron/src/upto/client/permit2.ts @@ -6,7 +6,6 @@ import { } from "../../constants"; import { ClientTronSigner } from "../../signer"; import { UptoPermit2Payload } from "../../types"; -import { readFeeFromExtra } from "../../shared/fee"; import { createNonce, getTronChainId, normalizeAddressForSigning } from "../../utils"; /** @@ -84,15 +83,13 @@ export async function createUptoPermit2Payload( // Ensure the one-time Permit2 allowance before signing (mirrors the exact // client). No-op when the signer can't broadcast (sign-only wallet) or when - // the allowance already covers the authorized maximum + fee. TRON's mainstream + // the allowance already covers the authorized maximum. TRON's mainstream // tokens (USDT/USDD) lack ERC-3009, so this approve is required on first use. // `permitted.amount` is the upto ceiling — approve at least that much, since // the facilitator may settle for any amount up to it. - const feeAmount = readFeeFromExtra(paymentRequirements.extra)?.feeAmount ?? "0"; - const totalRequired = BigInt(paymentRequirements.amount) + BigInt(feeAmount); await signer.ensureAllowance?.({ token: paymentRequirements.asset, - amount: totalRequired, + amount: BigInt(paymentRequirements.amount), network, }); diff --git a/typescript/packages/mechanisms/tron/src/upto/facilitator/scheme.ts b/typescript/packages/mechanisms/tron/src/upto/facilitator/scheme.ts index ba4fba61..a0fad4bd 100644 --- a/typescript/packages/mechanisms/tron/src/upto/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/tron/src/upto/facilitator/scheme.ts @@ -9,7 +9,6 @@ import { import { FacilitatorTronSigner } from "../../signer"; import { UptoPermit2Payload, isUptoPermit2Payload } from "../../types"; import { X402_UPTO_PERMIT2_PROXY_ADDRESSES } from "../../constants"; -import { ExactTronFeeConfig } from "../../shared/fee"; import { verifyUptoPermit2, settleUptoPermit2 } from "./permit2"; import * as errors from "./errors"; @@ -25,12 +24,8 @@ export class UptoTronScheme implements SchemeNetworkFacilitator { * Creates a new UptoTronScheme facilitator instance. * * @param signer - The TRON signer for facilitator operations - * @param feeConfig - Optional facilitator fee configuration (advertised via getExtra). */ - constructor( - private readonly signer: FacilitatorTronSigner, - private readonly feeConfig: ExactTronFeeConfig = {}, - ) {} + constructor(private readonly signer: FacilitatorTronSigner) {} /** * Returns extra metadata for the upto scheme, including the facilitator address @@ -52,18 +47,6 @@ export class UptoTronScheme implements SchemeNetworkFacilitator { assetTransferMethod: "permit2", facilitatorAddress, permit2FacilitatorAddress: facilitatorAddress, - ...(this.feeConfig.baseFee - ? { - feeConfig: { - feeTo: this.feeConfig.feeTo ?? signers[0], - ...(this.feeConfig.caller ? { caller: this.feeConfig.caller } : {}), - baseFee: this.feeConfig.baseFee, - ...(this.feeConfig.allowedTokens - ? { allowedTokens: this.feeConfig.allowedTokens } - : {}), - }, - } - : {}), }; } diff --git a/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts b/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts index cc9868fa..342174fb 100644 --- a/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts +++ b/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts @@ -13,7 +13,6 @@ import { } from "@bankofai/x402-core/utils"; import { ExactDefaultAssetInfo, getDefaultAsset } from "../../shared/defaultAssets"; import { getDecimals, parsePrice as parseTokenPrice } from "../../shared/tokens"; -import { buildFeeInfo, type ExactTronFeeConfig } from "../../shared/fee"; /** * TRON server implementation for the Upto payment scheme. @@ -100,34 +99,24 @@ export class UptoTronScheme implements SchemeNetworkServer { ): Promise { void extensionKeys; + const extra = { ...paymentRequirements.extra }; + delete extra.fee; + const requirementsWithoutFee = { ...paymentRequirements, extra }; + const facilitatorAddress = (paymentRequirements.extra?.permit2FacilitatorAddress as string | undefined) ?? (paymentRequirements.extra?.facilitatorAddress as string | undefined) ?? (supportedKind.extra?.permit2FacilitatorAddress as string | undefined) ?? (supportedKind.extra?.facilitatorAddress as string | undefined); - const feeConfig = supportedKind.extra?.feeConfig as ExactTronFeeConfig | undefined; - const existingFee = paymentRequirements.extra?.fee; - const fee = - existingFee ?? - (feeConfig - ? buildFeeInfo( - feeConfig, - supportedKind.network, - paymentRequirements.asset, - feeConfig.feeTo ?? "", - ) - : undefined); - return Promise.resolve({ - ...paymentRequirements, + ...requirementsWithoutFee, extra: { - ...paymentRequirements.extra, + ...extra, assetTransferMethod: "permit2", ...(facilitatorAddress ? { facilitatorAddress, permit2FacilitatorAddress: facilitatorAddress } : {}), - ...(fee ? { fee } : {}), }, }); } diff --git a/typescript/packages/mechanisms/tron/test/unit/advisory-fee-removal.test.ts b/typescript/packages/mechanisms/tron/test/unit/advisory-fee-removal.test.ts new file mode 100644 index 00000000..7a3d5d15 --- /dev/null +++ b/typescript/packages/mechanisms/tron/test/unit/advisory-fee-removal.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import type { PaymentRequirements } from "@bankofai/x402-core/types"; +import { ExactTronScheme } from "../../src/exact/server/scheme"; +import { UptoTronScheme } from "../../src/upto/server/scheme"; + +const NETWORK = "tron:0xcd8690dc"; + +function requirements(scheme: "exact" | "upto"): PaymentRequirements { + return { + scheme, + network: NETWORK, + asset: "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", + amount: "1000000", + payTo: "TJRyWwFs9wTFGZg3JbrVriFbNfCug5tDeC", + maxTimeoutSeconds: 60, + extra: { + fee: { feeTo: "TJRyWwFs9wTFGZg3JbrVriFbNfCug5tDeC", feeAmount: "5000" }, + }, + } as unknown as PaymentRequirements; +} + +describe("advisory fee removal", () => { + it("strips upstream fee metadata from exact requirements", async () => { + const result = await new ExactTronScheme().enhancePaymentRequirements( + requirements("exact"), + { + x402Version: 2, + scheme: "exact", + network: NETWORK, + extra: { supportedAssetTransferMethods: ["permit2"] }, + }, + [], + ); + + expect(result.extra?.fee).toBeUndefined(); + }); + + it("strips upstream fee metadata from exact requirements without a transfer method", async () => { + const result = await new ExactTronScheme().enhancePaymentRequirements( + requirements("exact"), + { x402Version: 2, scheme: "exact", network: NETWORK }, + [], + ); + + expect(result.extra?.fee).toBeUndefined(); + }); + + it("strips upstream fee metadata from upto requirements", async () => { + const result = await new UptoTronScheme().enhancePaymentRequirements( + requirements("upto"), + { x402Version: 2, scheme: "upto", network: NETWORK }, + [], + ); + + expect(result.extra?.fee).toBeUndefined(); + }); +}); diff --git a/typescript/packages/mechanisms/tron/test/unit/client-allowance.test.ts b/typescript/packages/mechanisms/tron/test/unit/client-allowance.test.ts index 4ab80330..46ee4e6a 100644 --- a/typescript/packages/mechanisms/tron/test/unit/client-allowance.test.ts +++ b/typescript/packages/mechanisms/tron/test/unit/client-allowance.test.ts @@ -172,7 +172,7 @@ describe("ClientTronSigner.ensureAllowance", () => { }); describe("createPermit2Payload — allowance injection", () => { - it("calls ensureAllowance with token, amount+fee, and network before signing", async () => { + it("calls ensureAllowance with token, amount, and network before signing", async () => { const calls: string[] = []; const signer: ClientTronSigner = { address: OWNER, @@ -194,14 +194,14 @@ describe("createPermit2Payload — allowance injection", () => { amount: "1000000", payTo: OWNER, maxTimeoutSeconds: 600, - extra: { assetTransferMethod: "permit2", fee: { feeTo: OWNER, feeAmount: "5000" } }, + extra: { assetTransferMethod: "permit2" }, }; await new ExactTronScheme(signer).createPaymentPayload(2, requirements as never); expect(signer.ensureAllowance).toHaveBeenCalledWith({ token: TOKEN, - amount: 1_005_000n, // amount + fee + amount: 1_000_000n, // amount only network: NETWORK, }); // Allowance is ensured before the witness signature is produced. diff --git a/typescript/packages/mechanisms/tron/test/unit/fee-plumbing.test.ts b/typescript/packages/mechanisms/tron/test/unit/fee-plumbing.test.ts deleted file mode 100644 index 1f99fe97..00000000 --- a/typescript/packages/mechanisms/tron/test/unit/fee-plumbing.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { ExactTronScheme as FacilitatorScheme } from "../../src/exact/facilitator/scheme"; -import { ExactTronScheme as ServerScheme } from "../../src/exact/server/scheme"; -import type { FacilitatorTronSigner } from "../../src/signer"; -import type { PaymentRequirements } from "@bankofai/x402-core/types"; - -/** - * Proves the fee plumbing: facilitator getExtra advertises feeConfig, and the - * server's enhancePaymentRequirements turns it into per-asset extra.fee (F3). - */ - -const NETWORK = "tron:0xcd8690dc"; -const USDT = "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"; -const USDD = "TGjgvdTWWrybVLaVeFqSyVqJQWjxqRYbaK"; -const SIGNER_ADDR = "TJRyWwFs9wTFGZg3JbrVriFbNfCug5tDeC"; - -function mockSigner(): FacilitatorTronSigner { - return { - getAddresses: () => [SIGNER_ADDR], - readContract: async () => 0n, - verifyTypedData: async () => true, - writeContract: async () => "0x", - waitForTransactionReceipt: async () => ({ status: "success" }), - }; -} - -function baseRequirements(asset: string): PaymentRequirements { - return { - scheme: "exact", - network: NETWORK, - asset, - amount: "1000000", - payTo: "TPayToAddress00000000000000000000000", - resource: "https://example.com/resource", - description: "", - mimeType: "", - maxTimeoutSeconds: 60, - extra: {}, - } as unknown as PaymentRequirements; -} - -describe("facilitator getExtra fee advertisement", () => { - it("omits feeConfig when no baseFee is configured", () => { - const extra = new FacilitatorScheme(mockSigner()).getExtra(NETWORK); - expect(extra?.feeConfig).toBeUndefined(); - }); - - it("advertises feeConfig with signer as default feeTo", () => { - const f = new FacilitatorScheme(mockSigner(), { baseFee: { USDT: "10000" } }); - const extra = f.getExtra(NETWORK); - expect(extra?.feeConfig).toEqual({ - feeTo: SIGNER_ADDR, - baseFee: { USDT: "10000" }, - }); - }); -}); - -describe("server enhancePaymentRequirements fee injection", () => { - const server = new ServerScheme(); - - function supportedKind(feeConfig?: Record) { - return { - x402Version: 2, - scheme: "exact", - network: NETWORK, - extra: { - supportedAssetTransferMethods: ["permit2"], - ...(feeConfig ? { feeConfig } : {}), - }, - }; - } - - it("injects extra.fee for a configured token", async () => { - const out = await server.enhancePaymentRequirements( - baseRequirements(USDT), - supportedKind({ feeTo: SIGNER_ADDR, baseFee: { USDT: "10000" } }), - [], - ); - expect(out.extra?.fee).toEqual({ feeTo: SIGNER_ADDR, feeAmount: "10000" }); - }); - - it("does not inject fee for a token absent from baseFee", async () => { - const out = await server.enhancePaymentRequirements( - baseRequirements(USDD), - supportedKind({ feeTo: SIGNER_ADDR, baseFee: { USDT: "10000" } }), - [], - ); - expect(out.extra?.fee).toBeUndefined(); - }); - - it("preserves a fee already present upstream", async () => { - const req = baseRequirements(USDT); - req.extra = { fee: { feeTo: "TPreset", feeAmount: "1" } }; - const out = await server.enhancePaymentRequirements( - req, - supportedKind({ feeTo: SIGNER_ADDR, baseFee: { USDT: "10000" } }), - [], - ); - expect(out.extra?.fee).toEqual({ feeTo: "TPreset", feeAmount: "1" }); - }); - - it("omits fee when facilitator advertises none", async () => { - const out = await server.enhancePaymentRequirements( - baseRequirements(USDT), - supportedKind(), - [], - ); - expect(out.extra?.fee).toBeUndefined(); - }); -}); diff --git a/typescript/packages/mechanisms/tron/test/unit/fee.test.ts b/typescript/packages/mechanisms/tron/test/unit/fee.test.ts index a4ce9c60..fd0c2abb 100644 --- a/typescript/packages/mechanisms/tron/test/unit/fee.test.ts +++ b/typescript/packages/mechanisms/tron/test/unit/fee.test.ts @@ -3,12 +3,7 @@ import { resolveBaseFee, isTokenAllowed, buildFeeInfo, - validateFee, readFeeFromExtra, - FEE_TOKEN_NOT_ALLOWED, - FEE_UNSUPPORTED_TOKEN, - FEE_AMOUNT_TOO_LOW, - FEE_TO_MISMATCH, type ExactTronFeeConfig, } from "../../src/shared/fee"; @@ -77,42 +72,6 @@ describe("buildFeeInfo", () => { }); }); -describe("validateFee", () => { - it("accepts a fee meeting or exceeding the base fee", () => { - expect(validateFee(config, NETWORK, USDT, { feeTo: FEE_TO, feeAmount: "10000" })).toBeNull(); - expect(validateFee(config, NETWORK, USDT, { feeTo: FEE_TO, feeAmount: "20000" })).toBeNull(); - }); - - it("rejects a fee below the base fee", () => { - expect(validateFee(config, NETWORK, USDT, { feeTo: FEE_TO, feeAmount: "9999" })).toBe( - FEE_AMOUNT_TOO_LOW, - ); - }); - - it("rejects a mismatched feeTo", () => { - expect(validateFee(config, NETWORK, USDT, { feeTo: "TWrong", feeAmount: "10000" })).toBe( - FEE_TO_MISMATCH, - ); - }); - - it("rejects disallowed tokens and unsupported tokens", () => { - const allowOnlyUsdd: ExactTronFeeConfig = { ...config, allowedTokens: [USDD] }; - expect(validateFee(allowOnlyUsdd, NETWORK, USDT, { feeTo: FEE_TO, feeAmount: "10000" })).toBe( - FEE_TOKEN_NOT_ALLOWED, - ); - expect(validateFee(config, NETWORK, "TUnknown", { feeTo: FEE_TO, feeAmount: "1" })).toBe( - FEE_UNSUPPORTED_TOKEN, - ); - }); - - it("uses a custom normalizer for feeTo comparison", () => { - const norm = (a: string) => a.toUpperCase(); - expect( - validateFee(config, NETWORK, USDT, { feeTo: FEE_TO.toLowerCase(), feeAmount: "10000" }, norm), - ).toBeNull(); - }); -}); - describe("readFeeFromExtra", () => { it("extracts a well-formed fee", () => { expect(readFeeFromExtra({ fee: { feeTo: FEE_TO, feeAmount: "100", caller: "TC" } })).toEqual({ diff --git a/typescript/packages/mechanisms/tron/test/unit/helpers.ts b/typescript/packages/mechanisms/tron/test/unit/helpers.ts index eab5089b..7d1aaf5b 100644 --- a/typescript/packages/mechanisms/tron/test/unit/helpers.ts +++ b/typescript/packages/mechanisms/tron/test/unit/helpers.ts @@ -2,14 +2,14 @@ import { TronWeb } from "tronweb"; import type { ClientTronWallet } from "../../src/signer"; /** - * Test-only helper: wrap a raw private key as an {@link AgentWallet}. + * Test-only helper: wrap a raw private key as an {@link ClientTronWallet}. * * Not part of the public SDK surface — the SDK signers are wallet-only. Used by * the offline test suite to drive signers with deterministic fixed keys. * * @param tronWeb - The TronWeb instance used for TIP-712 signing. * @param privateKey - The private key, with or without a `0x` prefix. - * @returns An AgentWallet backed by the private key. + * @returns A ClientTronWallet backed by the private key. */ export function privateKeyTronWallet(tronWeb: TronWeb, privateKey: string): ClientTronWallet { const clean = privateKey.replace(/^0x/, ""); diff --git a/typescript/packages/mechanisms/tron/test/unit/selection.test.ts b/typescript/packages/mechanisms/tron/test/unit/selection.test.ts index 0203ad51..3a6a4036 100644 --- a/typescript/packages/mechanisms/tron/test/unit/selection.test.ts +++ b/typescript/packages/mechanisms/tron/test/unit/selection.test.ts @@ -25,7 +25,7 @@ function req( extra: Record = {}, ): PaymentRequirements { return { - scheme: "exact", + scheme: "exact" as string, network: NETWORK, asset, amount, @@ -35,6 +35,17 @@ function req( } as unknown as PaymentRequirements; } +function gasfreeReq( + asset: string, + amount: string, + extra: Record = {}, +): PaymentRequirements { + return { + ...req(asset, amount, extra), + scheme: "exact_gasfree", + } as unknown as PaymentRequirements; +} + describe("CheapestTokenSelectionStrategy", () => { const strat = new CheapestTokenSelectionStrategy(); @@ -62,26 +73,45 @@ describe("createCheapestTokenSelector", () => { }); describe("filterAffordableRequirements", () => { - it("keeps only options the payer can afford (incl. fee)", async () => { + it("ignores stale extra.fee for exact scheme — only amount matters", async () => { const scheme: BalanceCheckable = { checkBalance: vi.fn(async (asset: string) => (asset === USDT ? 1_500_000n : 0n)), }; const accepts = [ - req(USDT, "1000000", { fee: { feeTo: "T", feeAmount: "100000" } }), // need 1.1 USDT, have 1.5 → ok + req(USDT, "1000000", { fee: { feeTo: "T", feeAmount: "100000" } }), // stale fee ignored; need 1.0, have 1.5 → ok req(USDD, "1000000000000000000"), // have 0 → out ]; const out = await filterAffordableRequirements(scheme, accepts); expect(out.map(r => r.asset)).toEqual([USDT]); }); - it("excludes when balance is below amount + fee", async () => { + it("keeps an exact option when balance covers amount but not stale fee", async () => { const scheme: BalanceCheckable = { checkBalance: vi.fn(async () => 1_050_000n) }; const out = await filterAffordableRequirements(scheme, [ req(USDT, "1000000", { fee: { feeTo: "T", feeAmount: "100000" } }), // need 1.1, have 1.05 ]); + // exact ignores the stale fee: 1.05 >= 1.0 → affordable + expect(out.map(r => r.asset)).toEqual([USDT]); + }); + + it("includes fee for exact_gasfree — enforced relayer fee", async () => { + const scheme: BalanceCheckable = { checkBalance: vi.fn(async () => 1_050_000n) }; + // GasFree enforces fee: need 1.1, have 1.05 → excluded + const out = await filterAffordableRequirements(scheme, [ + gasfreeReq(USDT, "1000000", { fee: { feeTo: "T", feeAmount: "100000" } }), + ]); expect(out).toEqual([]); }); + it("keeps exact_gasfree when balance covers amount + fee", async () => { + const scheme: BalanceCheckable = { checkBalance: vi.fn(async () => 1_200_000n) }; + // GasFree: need 1.1, have 1.2 → ok + const out = await filterAffordableRequirements(scheme, [ + gasfreeReq(USDT, "1000000", { fee: { feeTo: "T", feeAmount: "100000" } }), + ]); + expect(out.map(r => r.asset)).toEqual([USDT]); + }); + it("keeps a requirement when checkBalance throws", async () => { const scheme: BalanceCheckable = { checkBalance: vi.fn(async () => { diff --git a/typescript/packages/mechanisms/tron/test/unit/signer-wallet.test.ts b/typescript/packages/mechanisms/tron/test/unit/signer-wallet.test.ts index 8b361287..1fdd65f6 100644 --- a/typescript/packages/mechanisms/tron/test/unit/signer-wallet.test.ts +++ b/typescript/packages/mechanisms/tron/test/unit/signer-wallet.test.ts @@ -9,10 +9,10 @@ import { privateKeyTronWallet } from "./helpers"; vi.mock("../../src/rpc", () => ({ buildTronWeb: vi.fn() })); /** - * Offline tests for the AgentWallet abstraction (F5). + * Offline tests for the ClientTronWallet abstraction (F5). * * `createClientTronSigner` is wallet-only: the private key never enters the SDK. - * A raw key is just one AgentWallet implementation (via privateKeyTronWallet); + * A raw key is just one ClientTronWallet implementation (via privateKeyTronWallet); * arbitrary wallets (hosted, hardware) drive the signer via structural typing. */