From e2aa6fef465fdfedef562672701e06403dc05b2a Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Fri, 3 Jul 2026 11:14:53 +0800 Subject: [PATCH 1/9] chore(tron): remove advisory fee from exact and upto schemes The `exact` and `upto` schemes advertised a facilitator fee via `getExtra` and injected `extra.fee` into payment requirements, but the fee was never signed and the on-chain Permit2 proxy only transferred `amount` (no split). This dead "advisory" fee created confusion and an unused allowance cushion without any actual fee payment. Removed all fee plumbing from both schemes (client allowance, server injection, facilitator advertisement, register config). The GasFree scheme keeps its fee handling intact (signed + enforced via TIP-712). Cleaned up the now-dead `validateFee` helper and its error constants in `shared/fee.ts`. - exact/client/permit2.ts: ensureAllowance uses amount only - exact/server/scheme.ts: drop buildFeeInfo injection - exact/facilitator/{scheme,register}.ts: drop feeConfig param + getExtra ad - upto/{client,server,facilitator}: mirror exact changes - shared/fee.ts: remove validateFee + FEE_ constants, update docs - tests: drop fee-plumbing.test.ts, trim validateFee tests, fix allowance test --- .../tron/src/exact/client/permit2.ts | 7 +- .../tron/src/exact/facilitator/register.ts | 5 +- .../tron/src/exact/facilitator/scheme.ts | 21 ---- .../tron/src/exact/server/scheme.ts | 17 --- .../mechanisms/tron/src/shared/fee.ts | 57 +-------- .../tron/src/upto/client/permit2.ts | 7 +- .../tron/src/upto/facilitator/scheme.ts | 15 --- .../mechanisms/tron/src/upto/server/scheme.ts | 15 --- .../tron/test/unit/client-allowance.test.ts | 6 +- .../tron/test/unit/fee-plumbing.test.ts | 110 ------------------ .../mechanisms/tron/test/unit/fee.test.ts | 40 ------- 11 files changed, 14 insertions(+), 286 deletions(-) delete mode 100644 typescript/packages/mechanisms/tron/test/unit/fee-plumbing.test.ts 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..6b794e3f 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,14 +24,9 @@ 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 = {}, ) {} /** @@ -55,21 +49,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 69a031b1..3e249bb2 100644 --- a/typescript/packages/mechanisms/tron/src/exact/server/scheme.ts +++ b/typescript/packages/mechanisms/tron/src/exact/server/scheme.ts @@ -9,7 +9,6 @@ import { import { convertToTokenAmount, numberToDecimalString, parseMoneyString } 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. @@ -131,28 +130,12 @@ export class ExactTronScheme implements SchemeNetworkServer { (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, extra: { ...paymentRequirements.extra, assetTransferMethod: method, ...(method === "permit2" && permit2FacilitatorAddress ? { permit2FacilitatorAddress } : {}), - ...(fee ? { fee } : {}), }, }); } diff --git a/typescript/packages/mechanisms/tron/src/shared/fee.ts b/typescript/packages/mechanisms/tron/src/shared/fee.ts index 969b42d6..4e0c6030 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. */ @@ -111,49 +109,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..eb7631d2 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,11 +24,9 @@ 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 = {}, ) {} /** @@ -52,18 +49,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 db9b8672..e91f30a7 100644 --- a/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts +++ b/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts @@ -9,7 +9,6 @@ import { import { convertToTokenAmount, numberToDecimalString, parseMoneyString } 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. @@ -102,19 +101,6 @@ export class UptoTronScheme implements SchemeNetworkServer { (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, extra: { @@ -123,7 +109,6 @@ export class UptoTronScheme implements SchemeNetworkServer { ...(facilitatorAddress ? { facilitatorAddress, permit2FacilitatorAddress: facilitatorAddress } : {}), - ...(fee ? { fee } : {}), }, }); } 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 c524105c..55715f27 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 f960f42c..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:nile"; -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 cc7a5791..319f4fb2 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,41 +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", () => { From 3f3f9525fc7cf6be4a968263add14ba985c0aee6 Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Fri, 3 Jul 2026 15:15:08 +0800 Subject: [PATCH 2/9] style(tron): fix prettier violations and update README test list - Collapse single-param constructors to one line (exact/upto facilitator) - Remove double blank line left by validateFee deletion in shared/fee.ts - Drop fee-plumbing.test.ts reference from README test list --- typescript/packages/mechanisms/tron/README.md | 2 +- .../packages/mechanisms/tron/src/exact/facilitator/scheme.ts | 4 +--- typescript/packages/mechanisms/tron/src/shared/fee.ts | 1 - .../packages/mechanisms/tron/src/upto/facilitator/scheme.ts | 4 +--- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/typescript/packages/mechanisms/tron/README.md b/typescript/packages/mechanisms/tron/README.md index 0cb3bdd5..a12cdd1f 100644 --- a/typescript/packages/mechanisms/tron/README.md +++ b/typescript/packages/mechanisms/tron/README.md @@ -148,7 +148,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 AgentWallet abstraction. ## Notes & caveats diff --git a/typescript/packages/mechanisms/tron/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/tron/src/exact/facilitator/scheme.ts index 6b794e3f..098aa219 100644 --- a/typescript/packages/mechanisms/tron/src/exact/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/tron/src/exact/facilitator/scheme.ts @@ -25,9 +25,7 @@ export class ExactTronScheme implements SchemeNetworkFacilitator { * * @param signer - The TRON signer for facilitator operations */ - constructor( - private readonly signer: FacilitatorTronSigner, - ) {} + constructor(private readonly signer: FacilitatorTronSigner) {} /** * Gets extra configuration for the facilitator. diff --git a/typescript/packages/mechanisms/tron/src/shared/fee.ts b/typescript/packages/mechanisms/tron/src/shared/fee.ts index 4e0c6030..c440e091 100644 --- a/typescript/packages/mechanisms/tron/src/shared/fee.ts +++ b/typescript/packages/mechanisms/tron/src/shared/fee.ts @@ -109,7 +109,6 @@ export function buildFeeInfo( }; } - /** * Extract a {@link FeeInfo} from a requirement/supported-kind `extra` object. * diff --git a/typescript/packages/mechanisms/tron/src/upto/facilitator/scheme.ts b/typescript/packages/mechanisms/tron/src/upto/facilitator/scheme.ts index eb7631d2..a0fad4bd 100644 --- a/typescript/packages/mechanisms/tron/src/upto/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/tron/src/upto/facilitator/scheme.ts @@ -25,9 +25,7 @@ export class UptoTronScheme implements SchemeNetworkFacilitator { * * @param signer - The TRON signer for facilitator operations */ - constructor( - private readonly signer: FacilitatorTronSigner, - ) {} + constructor(private readonly signer: FacilitatorTronSigner) {} /** * Returns extra metadata for the upto scheme, including the facilitator address From e945422f34f81b20f61573d8402b767a08f472f6 Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Sat, 4 Jul 2026 22:50:25 +0800 Subject: [PATCH 3/9] docs(examples): sync README with current signer signatures and tokens - main README: add BSC testnet USDT, generalize USDC settle bullet to USDC/USDT, rewrite mainnet section (drop EPS, fix stale 'commented-ready' framing), document MCP examples in main line + env Loaded-by column, fix 'three lines' -> 'four lines' - fetch/README + basic/README: fix stale signer signatures after the (client, wallet) -> (wallet, options) refactor: createClientEvmSigner(wallet, { network, rpcUrl }), createClientTronSigner(wallet, { network, apiKey }), createFacilitatorEvmSigner(wallet, { network, rpcUrl }), createFacilitatorTronSigner(wallet, { network, apiKey }) --- examples/typescript/README.md | 29 ++++++++++++------- examples/typescript/clients/fetch/README.md | 11 +++---- .../typescript/facilitator/basic/README.md | 8 ++--- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/examples/typescript/README.md b/examples/typescript/README.md index 7903a63b..403642b7 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:nile` | 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,23 @@ 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:mainnet`) are registered alongside their +testnet counterparts in the same tables — no uncommenting needed. Select mainnet +by pointing the client's `PAY_TARGETS` and the server's payout addresses +(`EVM_ADDRESS` / `TRON_ADDRESS`) at mainnet, 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:mainnet` | USDT | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` | 6 | permit2 | | `tron:mainnet` | 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 +178,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 e3f9b15c..a1fdc58e 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. From 3e09dbeb1438afc2e18765726347456f7d781b70 Mon Sep 17 00:00:00 2001 From: roger-gan Date: Tue, 7 Jul 2026 09:11:04 +0800 Subject: [PATCH 4/9] docs(tron): clarify fee removal and mainnet setup --- examples/typescript/README.md | 10 +++++---- typescript/packages/mechanisms/tron/README.md | 21 +++++++++++++++++-- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/examples/typescript/README.md b/examples/typescript/README.md index 403642b7..9379b8d3 100644 --- a/examples/typescript/README.md +++ b/examples/typescript/README.md @@ -130,10 +130,12 @@ sponsorship). ### Mainnet (real funds) Mainnet networks (`eip155:56`, `tron:mainnet`) are registered alongside their -testnet counterparts in the same tables — no uncommenting needed. Select mainnet -by pointing the client's `PAY_TARGETS` and the server's payout addresses -(`EVM_ADDRESS` / `TRON_ADDRESS`) at mainnet, and set a reliable `EVM_RPC_URL` -for BSC. ⚠️ **REAL FUNDS.** All verified on-chain; sources: the official +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 | diff --git a/typescript/packages/mechanisms/tron/README.md b/typescript/packages/mechanisms/tron/README.md index a12cdd1f..27457a85 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`: @@ -131,10 +144,14 @@ The `permit2` path depends on three on-chain contracts. Addresses live in `src/c | Network | chainId (TIP-712) | Permit2 | x402ExactPermit2Proxy | | --- | --- | --- | --- | | `tron:nile` | 3448148188 | `TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h` | `TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F` | -| `tron:mainnet` | 728126428 | *(placeholder — replace)* | *(placeholder — replace)* | +| `tron:mainnet` | 728126428 | `TTJxU3P8rHycAyFY4kVtGNfmnMH4ezcuM9` | `TN49yaJmZMZoEdDCqjB4uPzQLHvYkGw95m` | | `tron:shasta` | 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 From 443257251dd7cfe985033753fdf9513ff1e36a10 Mon Sep 17 00:00:00 2001 From: roger-gan Date: Tue, 7 Jul 2026 14:30:53 +0800 Subject: [PATCH 5/9] fix(tron): fully remove advisory fee metadata --- compound-engineering.local.md | 14 +++ examples/typescript/README.md | 2 +- ...complete-p2-strip-advisory-fee-metadata.md | 92 +++++++++++++++++++ ...2-complete-p2-fix-stale-tron-mainnet-id.md | 80 ++++++++++++++++ .../003-complete-p2-fix-tron-format-check.md | 80 ++++++++++++++++ .../.changeset/tron-remove-advisory-fee.md | 11 +++ .../tron/src/exact/server/scheme.ts | 10 +- .../mechanisms/tron/src/upto/server/scheme.ts | 5 +- .../test/unit/advisory-fee-removal.test.ts | 57 ++++++++++++ .../mechanisms/tron/test/unit/fee.test.ts | 1 - 10 files changed, 346 insertions(+), 6 deletions(-) create mode 100644 compound-engineering.local.md create mode 100644 todos/001-complete-p2-strip-advisory-fee-metadata.md create mode 100644 todos/002-complete-p2-fix-stale-tron-mainnet-id.md create mode 100644 todos/003-complete-p2-fix-tron-format-check.md create mode 100644 typescript/.changeset/tron-remove-advisory-fee.md create mode 100644 typescript/packages/mechanisms/tron/test/unit/advisory-fee-removal.test.ts diff --git a/compound-engineering.local.md b/compound-engineering.local.md new file mode 100644 index 00000000..64c4718c --- /dev/null +++ b/compound-engineering.local.md @@ -0,0 +1,14 @@ +--- +review_agents: [kieran-typescript-reviewer, code-simplicity-reviewer, security-sentinel, performance-oracle] +plan_review_agents: [kieran-typescript-reviewer, code-simplicity-reviewer] +--- + +# Review Context + +Add project-specific review instructions here. +These notes are passed to all review agents during ce:review and ce:work. + +Examples: +- "We use Turbo Frames heavily — check for frame-busting issues" +- "Our API is public — extra scrutiny on input validation" +- "Performance-critical: we serve 10k req/s on this endpoint" diff --git a/examples/typescript/README.md b/examples/typescript/README.md index 13bed1c9..1b073ad0 100644 --- a/examples/typescript/README.md +++ b/examples/typescript/README.md @@ -129,7 +129,7 @@ sponsorship). ### Mainnet (real funds) -Mainnet networks (`eip155:56`, `tron:mainnet`) are registered alongside their +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 diff --git a/todos/001-complete-p2-strip-advisory-fee-metadata.md b/todos/001-complete-p2-strip-advisory-fee-metadata.md new file mode 100644 index 00000000..99a5823b --- /dev/null +++ b/todos/001-complete-p2-strip-advisory-fee-metadata.md @@ -0,0 +1,92 @@ +--- +status: complete +priority: p2 +issue_id: "001" +tags: [code-review, correctness, tron, payments] +dependencies: [] +--- + +# Strip advisory fee metadata from exact and upto requirements + +## Problem Statement + +The branch claims to remove advisory fees from TRON `exact` and `upto`, but both +server schemes still preserve an upstream `paymentRequirements.extra.fee` value. +Clients now ignore that value, so callers can receive fee metadata which is not +included in allowance calculations or enforced during settlement. + +## Findings + +- `exact/server/scheme.ts:140` spreads all existing `extra` fields into the output. +- `upto/server/scheme.ts:111` does the same. +- The deleted `fee-plumbing.test.ts` explicitly asserted that upstream fees were + preserved; no replacement test asserts the new no-fee invariant. +- This contradicts the README statement that exact and upto do not advertise a fee. + +## Proposed Solutions + +### Option 1: Remove fee during enhancement + +Destructure `fee` out of existing extras before constructing the enhanced +requirements, and add exact/upto regression tests. + +**Pros:** Enforces the documented invariant at the server boundary. + +**Cons:** Intentionally discards caller-provided metadata. + +**Effort:** Small + +**Risk:** Low + +### Option 2: Preserve metadata and narrow the documentation + +Document that only facilitator-generated fee configuration was removed and that +custom upstream fee metadata is passed through but ignored. + +**Pros:** Preserves generic metadata behavior. + +**Cons:** Leaves misleading, non-enforced payment terms in challenges. + +**Effort:** Small + +**Risk:** Medium + +## Recommended Action + +Strip `fee` from a copied `extra` object in both server schemes and cover exact, +exact-without-method, and upto behavior with regression tests. + +## Technical Details + +Affected files: +- `typescript/packages/mechanisms/tron/src/exact/server/scheme.ts` +- `typescript/packages/mechanisms/tron/src/upto/server/scheme.ts` +- TRON exact/upto server tests + +## Acceptance Criteria + +- [x] Exact and upto challenges cannot contain advisory `extra.fee` metadata. +- [x] Regression tests cover caller-provided `extra.fee`. +- [x] GasFree fee metadata remains unchanged. +- [x] TRON unit tests pass. + +## Work Log + +### 2026-07-07 - Initial review + +**By:** Codex + +**Actions:** Traced server enhancement and client allowance paths and identified +the surviving metadata pass-through. + +**Learnings:** Removing fee generation alone does not remove pre-existing fee +metadata because the entire `extra` object is preserved. + +### 2026-07-07 - Completed + +**By:** Codex + +**Actions:** Stripped fee metadata from both server schemes, added three +regression tests, and ran the complete TRON test suite and build. + +**Learnings:** Exact's no-transfer-method early return also needed sanitization. diff --git a/todos/002-complete-p2-fix-stale-tron-mainnet-id.md b/todos/002-complete-p2-fix-stale-tron-mainnet-id.md new file mode 100644 index 00000000..40ef99bb --- /dev/null +++ b/todos/002-complete-p2-fix-stale-tron-mainnet-id.md @@ -0,0 +1,80 @@ +--- +status: complete +priority: p2 +issue_id: "002" +tags: [code-review, documentation, tron] +dependencies: [] +--- + +# Fix stale TRON mainnet network identifier + +## Problem Statement + +The examples README instructs users to use `tron:mainnet`, but the current SDK +requires the hex CAIP-2 identifier `tron:0x2b6653dc` and rejects legacy names. +This is especially risky in a section describing real-funds configuration. + +## Findings + +- `examples/typescript/README.md:132` uses `tron:mainnet`. +- The same file's table and the example source use `tron:0x2b6653dc`. +- The existing CAIP-2 changeset states that legacy string IDs now throw. + +## Proposed Solutions + +### Option 1: Replace the stale identifier + +Change `tron:mainnet` to `tron:0x2b6653dc`. + +**Pros:** Correct and consistent with code and migration guidance. + +**Cons:** None. + +**Effort:** Small + +**Risk:** Low + +### Option 2: Use the exported constant name + +Describe the network as `TRON_MAINNET` (`tron:0x2b6653dc`). + +**Pros:** Improves discoverability of the canonical constant. + +**Cons:** Slightly more verbose in an operational README. + +**Effort:** Small + +**Risk:** Low + +## Recommended Action + +Replace the legacy identifier with the canonical hex CAIP-2 identifier. + +## Technical Details + +Affected file: `examples/typescript/README.md:132`. + +## Acceptance Criteria + +- [x] The mainnet section uses `tron:0x2b6653dc`. +- [x] No legacy TRON network names remain in affected documentation. + +## Work Log + +### 2026-07-07 - Initial review + +**By:** Codex + +**Actions:** Compared documentation against constants, example registrations, +and the CAIP-2 breaking changeset. + +**Learnings:** A post-merge documentation edit reintroduced the removed legacy ID. + +### 2026-07-07 - Completed + +**By:** Codex + +**Actions:** Updated the mainnet documentation and verified affected docs contain +no legacy TRON network names. + +**Learnings:** The canonical identifier now matches constants and example code. diff --git a/todos/003-complete-p2-fix-tron-format-check.md b/todos/003-complete-p2-fix-tron-format-check.md new file mode 100644 index 00000000..21b074af --- /dev/null +++ b/todos/003-complete-p2-fix-tron-format-check.md @@ -0,0 +1,80 @@ +--- +status: complete +priority: p2 +issue_id: "003" +tags: [code-review, quality, typescript] +dependencies: [] +--- + +# Fix branch-introduced TRON formatting failure + +## Problem Statement + +The TRON package fails lint and format checks because the branch leaves an extra +blank line in `fee.test.ts`. This can block CI even though unit tests and build pass. + +## Findings + +- ESLint reports `prettier/prettier` at `test/unit/fee.test.ts:75`. +- `pnpm --filter @bankofai/x402-tron format:check` flags the same file. +- The package also has pre-existing failures in `package.json` and `src/signer.ts`; + those files are not modified by this branch and are outside this finding. + +## Proposed Solutions + +### Option 1: Remove the extra blank line + +Apply Prettier to the changed test or delete the duplicate blank line directly. + +**Pros:** Minimal, isolated fix. + +**Cons:** None. + +**Effort:** Small + +**Risk:** Low + +### Option 2: Format the entire package + +Run the package format command and review all resulting changes. + +**Pros:** Also addresses pre-existing formatting drift. + +**Cons:** Produces unrelated changes in this branch. + +**Effort:** Small + +**Risk:** Medium + +## Recommended Action + +Remove the extra blank line without formatting unrelated package files. + +## Technical Details + +Affected file: `typescript/packages/mechanisms/tron/test/unit/fee.test.ts:75`. + +## Acceptance Criteria + +- [x] The changed test passes Prettier. +- [x] No unrelated formatting changes are introduced. + +## Work Log + +### 2026-07-07 - Initial review + +**By:** Codex + +**Actions:** Ran unit tests, build, ESLint, Prettier, and `git diff --check`. + +**Learnings:** Unit tests and build pass; package-wide lint still has unrelated +pre-existing JSDoc failures. + +### 2026-07-07 - Completed + +**By:** Codex + +**Actions:** Removed the duplicate blank line and ran Prettier and ESLint on all +modified TypeScript files. + +**Learnings:** Targeted checks avoid modifying unrelated pre-existing drift. 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/src/exact/server/scheme.ts b/typescript/packages/mechanisms/tron/src/exact/server/scheme.ts index a4e1b1cb..8e9cabce 100644 --- a/typescript/packages/mechanisms/tron/src/exact/server/scheme.ts +++ b/typescript/packages/mechanisms/tron/src/exact/server/scheme.ts @@ -114,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; @@ -127,7 +131,7 @@ export class ExactTronScheme implements SchemeNetworkServer { : undefined); if (!method) { - return Promise.resolve(paymentRequirements); + return Promise.resolve(requirementsWithoutFee); } const permit2FacilitatorAddress = @@ -135,9 +139,9 @@ export class ExactTronScheme implements SchemeNetworkServer { (supportedKind.extra?.permit2FacilitatorAddress as string | undefined); return Promise.resolve({ - ...paymentRequirements, + ...requirementsWithoutFee, extra: { - ...paymentRequirements.extra, + ...extra, assetTransferMethod: method, ...(method === "permit2" && permit2FacilitatorAddress ? { permit2FacilitatorAddress } : {}), }, diff --git a/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts b/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts index b41aa630..275ba69d 100644 --- a/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts +++ b/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts @@ -99,6 +99,9 @@ export class UptoTronScheme implements SchemeNetworkServer { ): Promise { void extensionKeys; + const extra = { ...paymentRequirements.extra }; + delete extra.fee; + const facilitatorAddress = (paymentRequirements.extra?.permit2FacilitatorAddress as string | undefined) ?? (paymentRequirements.extra?.facilitatorAddress as string | undefined) ?? @@ -108,7 +111,7 @@ export class UptoTronScheme implements SchemeNetworkServer { return Promise.resolve({ ...paymentRequirements, extra: { - ...paymentRequirements.extra, + ...extra, assetTransferMethod: "permit2", ...(facilitatorAddress ? { facilitatorAddress, permit2FacilitatorAddress: facilitatorAddress } 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/fee.test.ts b/typescript/packages/mechanisms/tron/test/unit/fee.test.ts index 86565dc5..fd0c2abb 100644 --- a/typescript/packages/mechanisms/tron/test/unit/fee.test.ts +++ b/typescript/packages/mechanisms/tron/test/unit/fee.test.ts @@ -72,7 +72,6 @@ describe("buildFeeInfo", () => { }); }); - describe("readFeeFromExtra", () => { it("extracts a well-formed fee", () => { expect(readFeeFromExtra({ fee: { feeTo: FEE_TO, feeAmount: "100", caller: "TC" } })).toEqual({ From b41c61fb7616a43b7a51f5f84d0c6bde6b7f2d38 Mon Sep 17 00:00:00 2001 From: roger-gan Date: Tue, 7 Jul 2026 14:32:40 +0800 Subject: [PATCH 6/9] chore: remove review todo artifacts --- ...complete-p2-strip-advisory-fee-metadata.md | 92 ------------------- ...2-complete-p2-fix-stale-tron-mainnet-id.md | 80 ---------------- .../003-complete-p2-fix-tron-format-check.md | 80 ---------------- 3 files changed, 252 deletions(-) delete mode 100644 todos/001-complete-p2-strip-advisory-fee-metadata.md delete mode 100644 todos/002-complete-p2-fix-stale-tron-mainnet-id.md delete mode 100644 todos/003-complete-p2-fix-tron-format-check.md diff --git a/todos/001-complete-p2-strip-advisory-fee-metadata.md b/todos/001-complete-p2-strip-advisory-fee-metadata.md deleted file mode 100644 index 99a5823b..00000000 --- a/todos/001-complete-p2-strip-advisory-fee-metadata.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -status: complete -priority: p2 -issue_id: "001" -tags: [code-review, correctness, tron, payments] -dependencies: [] ---- - -# Strip advisory fee metadata from exact and upto requirements - -## Problem Statement - -The branch claims to remove advisory fees from TRON `exact` and `upto`, but both -server schemes still preserve an upstream `paymentRequirements.extra.fee` value. -Clients now ignore that value, so callers can receive fee metadata which is not -included in allowance calculations or enforced during settlement. - -## Findings - -- `exact/server/scheme.ts:140` spreads all existing `extra` fields into the output. -- `upto/server/scheme.ts:111` does the same. -- The deleted `fee-plumbing.test.ts` explicitly asserted that upstream fees were - preserved; no replacement test asserts the new no-fee invariant. -- This contradicts the README statement that exact and upto do not advertise a fee. - -## Proposed Solutions - -### Option 1: Remove fee during enhancement - -Destructure `fee` out of existing extras before constructing the enhanced -requirements, and add exact/upto regression tests. - -**Pros:** Enforces the documented invariant at the server boundary. - -**Cons:** Intentionally discards caller-provided metadata. - -**Effort:** Small - -**Risk:** Low - -### Option 2: Preserve metadata and narrow the documentation - -Document that only facilitator-generated fee configuration was removed and that -custom upstream fee metadata is passed through but ignored. - -**Pros:** Preserves generic metadata behavior. - -**Cons:** Leaves misleading, non-enforced payment terms in challenges. - -**Effort:** Small - -**Risk:** Medium - -## Recommended Action - -Strip `fee` from a copied `extra` object in both server schemes and cover exact, -exact-without-method, and upto behavior with regression tests. - -## Technical Details - -Affected files: -- `typescript/packages/mechanisms/tron/src/exact/server/scheme.ts` -- `typescript/packages/mechanisms/tron/src/upto/server/scheme.ts` -- TRON exact/upto server tests - -## Acceptance Criteria - -- [x] Exact and upto challenges cannot contain advisory `extra.fee` metadata. -- [x] Regression tests cover caller-provided `extra.fee`. -- [x] GasFree fee metadata remains unchanged. -- [x] TRON unit tests pass. - -## Work Log - -### 2026-07-07 - Initial review - -**By:** Codex - -**Actions:** Traced server enhancement and client allowance paths and identified -the surviving metadata pass-through. - -**Learnings:** Removing fee generation alone does not remove pre-existing fee -metadata because the entire `extra` object is preserved. - -### 2026-07-07 - Completed - -**By:** Codex - -**Actions:** Stripped fee metadata from both server schemes, added three -regression tests, and ran the complete TRON test suite and build. - -**Learnings:** Exact's no-transfer-method early return also needed sanitization. diff --git a/todos/002-complete-p2-fix-stale-tron-mainnet-id.md b/todos/002-complete-p2-fix-stale-tron-mainnet-id.md deleted file mode 100644 index 40ef99bb..00000000 --- a/todos/002-complete-p2-fix-stale-tron-mainnet-id.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -status: complete -priority: p2 -issue_id: "002" -tags: [code-review, documentation, tron] -dependencies: [] ---- - -# Fix stale TRON mainnet network identifier - -## Problem Statement - -The examples README instructs users to use `tron:mainnet`, but the current SDK -requires the hex CAIP-2 identifier `tron:0x2b6653dc` and rejects legacy names. -This is especially risky in a section describing real-funds configuration. - -## Findings - -- `examples/typescript/README.md:132` uses `tron:mainnet`. -- The same file's table and the example source use `tron:0x2b6653dc`. -- The existing CAIP-2 changeset states that legacy string IDs now throw. - -## Proposed Solutions - -### Option 1: Replace the stale identifier - -Change `tron:mainnet` to `tron:0x2b6653dc`. - -**Pros:** Correct and consistent with code and migration guidance. - -**Cons:** None. - -**Effort:** Small - -**Risk:** Low - -### Option 2: Use the exported constant name - -Describe the network as `TRON_MAINNET` (`tron:0x2b6653dc`). - -**Pros:** Improves discoverability of the canonical constant. - -**Cons:** Slightly more verbose in an operational README. - -**Effort:** Small - -**Risk:** Low - -## Recommended Action - -Replace the legacy identifier with the canonical hex CAIP-2 identifier. - -## Technical Details - -Affected file: `examples/typescript/README.md:132`. - -## Acceptance Criteria - -- [x] The mainnet section uses `tron:0x2b6653dc`. -- [x] No legacy TRON network names remain in affected documentation. - -## Work Log - -### 2026-07-07 - Initial review - -**By:** Codex - -**Actions:** Compared documentation against constants, example registrations, -and the CAIP-2 breaking changeset. - -**Learnings:** A post-merge documentation edit reintroduced the removed legacy ID. - -### 2026-07-07 - Completed - -**By:** Codex - -**Actions:** Updated the mainnet documentation and verified affected docs contain -no legacy TRON network names. - -**Learnings:** The canonical identifier now matches constants and example code. diff --git a/todos/003-complete-p2-fix-tron-format-check.md b/todos/003-complete-p2-fix-tron-format-check.md deleted file mode 100644 index 21b074af..00000000 --- a/todos/003-complete-p2-fix-tron-format-check.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -status: complete -priority: p2 -issue_id: "003" -tags: [code-review, quality, typescript] -dependencies: [] ---- - -# Fix branch-introduced TRON formatting failure - -## Problem Statement - -The TRON package fails lint and format checks because the branch leaves an extra -blank line in `fee.test.ts`. This can block CI even though unit tests and build pass. - -## Findings - -- ESLint reports `prettier/prettier` at `test/unit/fee.test.ts:75`. -- `pnpm --filter @bankofai/x402-tron format:check` flags the same file. -- The package also has pre-existing failures in `package.json` and `src/signer.ts`; - those files are not modified by this branch and are outside this finding. - -## Proposed Solutions - -### Option 1: Remove the extra blank line - -Apply Prettier to the changed test or delete the duplicate blank line directly. - -**Pros:** Minimal, isolated fix. - -**Cons:** None. - -**Effort:** Small - -**Risk:** Low - -### Option 2: Format the entire package - -Run the package format command and review all resulting changes. - -**Pros:** Also addresses pre-existing formatting drift. - -**Cons:** Produces unrelated changes in this branch. - -**Effort:** Small - -**Risk:** Medium - -## Recommended Action - -Remove the extra blank line without formatting unrelated package files. - -## Technical Details - -Affected file: `typescript/packages/mechanisms/tron/test/unit/fee.test.ts:75`. - -## Acceptance Criteria - -- [x] The changed test passes Prettier. -- [x] No unrelated formatting changes are introduced. - -## Work Log - -### 2026-07-07 - Initial review - -**By:** Codex - -**Actions:** Ran unit tests, build, ESLint, Prettier, and `git diff --check`. - -**Learnings:** Unit tests and build pass; package-wide lint still has unrelated -pre-existing JSDoc failures. - -### 2026-07-07 - Completed - -**By:** Codex - -**Actions:** Removed the duplicate blank line and ran Prettier and ESLint on all -modified TypeScript files. - -**Learnings:** Targeted checks avoid modifying unrelated pre-existing drift. From e00cb6dae684061faf4d356da7071ef96a3b1f99 Mon Sep 17 00:00:00 2001 From: roger-gan Date: Tue, 7 Jul 2026 14:33:25 +0800 Subject: [PATCH 7/9] chore: remove local review configuration --- compound-engineering.local.md | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 compound-engineering.local.md diff --git a/compound-engineering.local.md b/compound-engineering.local.md deleted file mode 100644 index 64c4718c..00000000 --- a/compound-engineering.local.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -review_agents: [kieran-typescript-reviewer, code-simplicity-reviewer, security-sentinel, performance-oracle] -plan_review_agents: [kieran-typescript-reviewer, code-simplicity-reviewer] ---- - -# Review Context - -Add project-specific review instructions here. -These notes are passed to all review agents during ce:review and ce:work. - -Examples: -- "We use Turbo Frames heavily — check for frame-busting issues" -- "Our API is public — extra scrutiny on input validation" -- "Performance-critical: we serve 10k req/s on this endpoint" From ab923e64c109b5732e1286a3c08fe3845b1daa2c Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Thu, 9 Jul 2026 16:37:28 +0800 Subject: [PATCH 8/9] refactor(tron): mirror exact scheme's requirementsWithoutFee in upto Surface fee-stripping intent explicitly and keep the upto and exact server schemes symmetric. Functionally equivalent; behavior unchanged. --- typescript/packages/mechanisms/tron/src/upto/server/scheme.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts b/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts index 275ba69d..342174fb 100644 --- a/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts +++ b/typescript/packages/mechanisms/tron/src/upto/server/scheme.ts @@ -101,6 +101,7 @@ export class UptoTronScheme implements SchemeNetworkServer { const extra = { ...paymentRequirements.extra }; delete extra.fee; + const requirementsWithoutFee = { ...paymentRequirements, extra }; const facilitatorAddress = (paymentRequirements.extra?.permit2FacilitatorAddress as string | undefined) ?? @@ -109,7 +110,7 @@ export class UptoTronScheme implements SchemeNetworkServer { (supportedKind.extra?.facilitatorAddress as string | undefined); return Promise.resolve({ - ...paymentRequirements, + ...requirementsWithoutFee, extra: { ...extra, assetTransferMethod: "permit2", From 2d8caf63e8ce790ddb2397f0076e6ce04f404b42 Mon Sep 17 00:00:00 2001 From: "roger.gan" Date: Mon, 13 Jul 2026 19:42:34 +0800 Subject: [PATCH 9/9] fix(tron): align balance helper, README, and fee naming with fee-removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - balance.ts: scheme-aware affordability — exact/upto ignore stale extra.fee, only exact_gasfree adds the enforced relayer fee - selection.test.ts: update assertions for new fee semantics + add exact_gasfree affordability tests - README.md: fix signer example (ClientTronWallet/FacilitatorTronWallet, wallet+options API, both async), fix exact-only text, complete exports table with upto/gasfree/batch-settlement subpaths - examples README: remove stale EPS row (not registered as runnable token) - fee.ts: add GasFreeTronFeeConfig alias reflecting current semantics - test helpers/signer-wallet: replace stale AgentWallet references with ClientTronWallet (fixes broken JSDoc link) --- examples/typescript/README.md | 1 - typescript/packages/mechanisms/tron/README.md | 39 ++++++++++++------- .../packages/mechanisms/tron/src/index.ts | 2 +- .../mechanisms/tron/src/shared/balance.ts | 16 +++++--- .../mechanisms/tron/src/shared/fee.ts | 7 ++++ .../mechanisms/tron/test/unit/helpers.ts | 4 +- .../tron/test/unit/selection.test.ts | 38 ++++++++++++++++-- .../tron/test/unit/signer-wallet.test.ts | 4 +- 8 files changed, 81 insertions(+), 30 deletions(-) diff --git a/examples/typescript/README.md b/examples/typescript/README.md index 1b073ad0..1e4cc392 100644 --- a/examples/typescript/README.md +++ b/examples/typescript/README.md @@ -142,7 +142,6 @@ All verified on-chain; sources: the official |---|---|---|---|---| | `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 | diff --git a/typescript/packages/mechanisms/tron/README.md b/typescript/packages/mechanisms/tron/README.md index 0d8783b8..6931bf8d 100644 --- a/typescript/packages/mechanisms/tron/README.md +++ b/typescript/packages/mechanisms/tron/README.md @@ -50,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 @@ -60,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 @@ -70,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/, ""); @@ -79,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( @@ -92,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 @@ -165,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`, `selection.test.ts`, `signer-wallet.test.ts` — token registry, fee policy, 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/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 c440e091..a4e6b6a7 100644 --- a/typescript/packages/mechanisms/tron/src/shared/fee.ts +++ b/typescript/packages/mechanisms/tron/src/shared/fee.ts @@ -48,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. * 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. */