diff --git a/.env.example b/.env.example index 5d3d5aed..dff6f0c1 100644 --- a/.env.example +++ b/.env.example @@ -10,8 +10,8 @@ LOG_LEVEL=info TRON_RPC_URL= # Public base URL used in x402 resource metadata. Set this behind gateways that reject localhost/private URLs. PUBLIC_RESOURCE_BASE_URL= -# x402 settlement endpoint. -X402_FACILITATOR_URL=https://facilitator-v2.bankofai.io +# x402 production settlement endpoint. +X402_FACILITATOR_URL=https://facilitator.bankofai.io # Facilitator auth for higher rate limits. Sent as X-API-KEY. X402_FACILITATOR_API_KEY= # Backward-compatible alias. Use X402_FACILITATOR_API_KEY for new deployments. diff --git a/Dockerfile b/Dockerfile index f614ef3e..d9a34390 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,10 @@ RUN npm run build FROM node:22-slim -ENV NODE_ENV=production +ENV NODE_ENV=production \ + BANKOFAI_ENV=prod \ + X402_FACILITATOR_URL=https://facilitator.bankofai.io \ + TRON_RPC_URL= WORKDIR /app diff --git a/config/networks.json b/config/networks.json index 598aa277..8fd965b3 100644 --- a/config/networks.json +++ b/config/networks.json @@ -1,7 +1,7 @@ { "mainnet": { "name": "TRON Mainnet", - "paymentNetwork": "tron:mainnet", + "paymentNetwork": "tron:0x2b6653dc", "rpcUrl": "https://api.trongrid.io", "explorer": "https://tronscan.org", "chainId": "728126428", diff --git a/package-lock.json b/package-lock.json index 2f0f7734..4297954c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "name": "x402-recharge-server", "version": "2.0.0", "dependencies": { - "@bankofai/x402-core": "1.0.0", - "@bankofai/x402-evm": "1.0.0", - "@bankofai/x402-tron": "1.0.0", + "@bankofai/x402-core": "1.0.1", + "@bankofai/x402-evm": "1.0.1", + "@bankofai/x402-tron": "1.0.1", "@modelcontextprotocol/sdk": "1.29.0", "dotenv": "^16.4.7", "express": "^5.1.0", @@ -68,32 +68,32 @@ } }, "node_modules/@bankofai/x402-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@bankofai/x402-core/-/x402-core-1.0.0.tgz", - "integrity": "sha512-uVvvXCGfk/HqusxcKmjeTgBlJBMTR7QPZH2IDZlqoliv5O36YASaHSgSo/JOKDvVzPoXlY+5VmxEG6EdWA+wGA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bankofai/x402-core/-/x402-core-1.0.1.tgz", + "integrity": "sha512-2D4W1dTIlaHFrK+C6tazk12iERAcofHMHwG9qFuUhC4Dubhma4YJfpDsbbXQm5QXOsHDMOCTItpdp7nvZoLCeQ==", "license": "Apache-2.0", "dependencies": { "zod": "^3.24.2" } }, "node_modules/@bankofai/x402-evm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@bankofai/x402-evm/-/x402-evm-1.0.0.tgz", - "integrity": "sha512-j0T88Y5ngItIS9FTWXtbvoITePISzhuurU640dDL9075uQxBiLzYt20rkPebM7gi9D00HTbyJXbHnCq0zajCBQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bankofai/x402-evm/-/x402-evm-1.0.1.tgz", + "integrity": "sha512-4JSElA0vdxdDhkqHfXMT0Q3QH7II7CrHfct8ZV714N42PrxCWdGglHSsSeC2ZFBdracozBZyFxPWZKDSy6CTxg==", "license": "Apache-2.0", "dependencies": { - "@bankofai/x402-core": "~1.0.0", + "@bankofai/x402-core": "~1.0.1", "viem": "^2.48.11", "zod": "^3.24.2" } }, "node_modules/@bankofai/x402-tron": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@bankofai/x402-tron/-/x402-tron-1.0.0.tgz", - "integrity": "sha512-ycq+I1jMFLoQdMP8AzgXEUkSjzQFPrMkRAehcfiFACzrtAciq8Ev3uB0N8+2TXIC55l9fcTxrm+EB/C75T9xVg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bankofai/x402-tron/-/x402-tron-1.0.1.tgz", + "integrity": "sha512-dsjoOSOJ97dk/o8vpOPJ/FIYAJt2N06SonN9H51WwKdqGoiK5w5fz0n5P36drny/d+Zvtxsy7EMI/g09pRxJUA==", "license": "Apache-2.0", "dependencies": { - "@bankofai/x402-core": "~1.0.0", + "@bankofai/x402-core": "~1.0.1", "tronweb": "^6.1.0" } }, @@ -2956,27 +2956,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/viem/node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2999,9 +2978,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 47a2d123..b5a39884 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ "node": ">=22.0.0" }, "dependencies": { - "@bankofai/x402-core": "1.0.0", - "@bankofai/x402-evm": "1.0.0", - "@bankofai/x402-tron": "1.0.0", + "@bankofai/x402-core": "1.0.1", + "@bankofai/x402-evm": "1.0.1", + "@bankofai/x402-tron": "1.0.1", "@modelcontextprotocol/sdk": "1.29.0", "dotenv": "^16.4.7", "express": "^5.1.0", @@ -31,5 +31,8 @@ "tsx": "^4.21.0", "typescript": "^5.7.3", "viem": "^2.54.1" + }, + "overrides": { + "ws": "8.21.0" } } diff --git a/src/config.ts b/src/config.ts index 24e5086e..4c7ee62e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -50,14 +50,25 @@ function envNumber(name: string, fallback: number): number { return parsed; } +function defaultFacilitatorUrl(bankofaiEnv: string): string { + return bankofaiEnv.toLowerCase().trim() === "dev" + ? "https://tn-facilitator.bankofai.io" + : "https://facilitator.bankofai.io"; +} + +const bankofaiEnv = envString("BANKOFAI_ENV", "prod"); +const x402FacilitatorUrl = bankofaiEnv.toLowerCase().trim() === "dev" + ? defaultFacilitatorUrl(bankofaiEnv) + : envString("X402_FACILITATOR_URL", defaultFacilitatorUrl(bankofaiEnv)); + export const settings = { - bankofaiEnv: envString("BANKOFAI_ENV", "prod"), + bankofaiEnv, tronRpcUrl: envString("TRON_RPC_URL", ""), publicResourceBaseUrl: envString("PUBLIC_RESOURCE_BASE_URL", ""), host: envString("HOST", "0.0.0.0"), port: envNumber("PORT", 8000), logLevel: envString("LOG_LEVEL", "info"), - x402FacilitatorUrl: envString("X402_FACILITATOR_URL", "https://facilitator-v2.bankofai.io"), + x402FacilitatorUrl, facilitatorApiKey: envString("X402_FACILITATOR_API_KEY", envString("FACILITATOR_API_KEY", "")), facilitatorTimeoutSeconds: envNumber("FACILITATOR_TIMEOUT_SECONDS", 10), facilitatorVerifyRetries: envNumber("FACILITATOR_VERIFY_RETRIES", 1), diff --git a/src/payments.ts b/src/payments.ts index ed09f3ec..5d8ab4aa 100644 --- a/src/payments.ts +++ b/src/payments.ts @@ -2,7 +2,7 @@ import crypto from "node:crypto"; import type { Network, PaymentPayload, PaymentRequired, PaymentRequirements, SettleResponse } from "@bankofai/x402-core/types"; import { HTTPFacilitatorClient } from "@bankofai/x402-core/http"; import { decodePaymentSignatureHeader } from "@bankofai/x402-core/http"; -import { getToken } from "@bankofai/x402-tron"; +import { getToken, TRON_MAINNET } from "@bankofai/x402-tron"; import { getDefaultAsset } from "@bankofai/x402-evm"; import { NetworkConfig, networkConfig, networkConfigs, settings } from "./config.js"; import { PaymentFlowError } from "./errors.js"; @@ -11,7 +11,8 @@ import { logger } from "./logger.js"; const ALLOWED_TRC20_TOKENS = new Set(["USDT", "USDD"]); const BSC_ALLOWED_TOKENS = new Set(["USDT"]); -const PAYMENT_SCHEME = "exact"; +const EXACT_SCHEME = "exact"; +const GASFREE_SCHEME = "exact_gasfree"; const MIN_RECHARGE_AMOUNT = "1"; const MAX_RECHARGE_AMOUNT = "20000"; @@ -168,12 +169,34 @@ function decimalToSmallestUnit(amount: string, decimals: number): bigint { return BigInt(`${parsed.intPart}${paddedFrac}` || "0"); } -function paymentExtra(cfg: NetworkConfig, tokenSymbol: string): Record { +function sdkTronNetwork(paymentNetwork: string): Network { + if (paymentNetwork === "tron:mainnet") { + return TRON_MAINNET as Network; + } + return paymentNetwork as Network; +} + +function facilitatorNetworkCandidates(paymentNetwork: string): Network[] { + const candidates = [paymentNetwork as Network]; + const sdkNetwork = sdkTronNetwork(paymentNetwork); + if (!candidates.includes(sdkNetwork)) { + candidates.push(sdkNetwork); + } + return candidates; +} + +function paymentExtra(cfg: NetworkConfig, tokenSymbol: string, scheme: string): Record { if (cfg.paymentNetwork.startsWith("tron:")) { - const token = getToken(cfg.paymentNetwork as Network, tokenSymbol); + const token = getToken(sdkTronNetwork(cfg.paymentNetwork), tokenSymbol); if (!token) { return {}; } + if (scheme === GASFREE_SCHEME) { + return { + name: token.name, + ...(token.version !== undefined ? { version: token.version } : {}) + }; + } const includeTip712Domain = !token.assetTransferMethod || Boolean(token.supportsEip2612); return { ...(includeTip712Domain && token.version !== undefined ? { name: token.name, version: token.version } : {}), @@ -264,15 +287,20 @@ export async function buildRechargeChallenge(amount: string, token: string, reso continue; } - accepts.push({ - scheme: PAYMENT_SCHEME, - network: cfg.paymentNetwork as Network, - amount: amountSmallest.toString(), - asset: tokenCfg.address, - payTo: cfg.bankofaiDepositAddress, - maxTimeoutSeconds: 3600, - extra: paymentExtra(cfg, tokenSymbol) - }); + const schemes = cfg.paymentNetwork.startsWith("tron:") + ? [EXACT_SCHEME, GASFREE_SCHEME] + : [EXACT_SCHEME]; + for (const scheme of schemes) { + accepts.push({ + scheme, + network: cfg.paymentNetwork as Network, + amount: amountSmallest.toString(), + asset: tokenCfg.address, + payTo: cfg.bankofaiDepositAddress, + maxTimeoutSeconds: 3600, + extra: paymentExtra(cfg, tokenSymbol, scheme) + }); + } } if (accepts.length === 0) { @@ -289,8 +317,10 @@ export async function buildRechargeChallenge(amount: string, token: string, reso } const supportedKinds = supported.kinds ?? []; - const filteredAccepts = accepts.map((accept) => { - const supportedKind = supportedKinds.find((kind) => kind.scheme === accept.scheme && kind.network === accept.network); + const filteredAccepts = accepts.flatMap((accept) => { + const supportedKind = supportedKinds.find((kind) => + kind.scheme === accept.scheme && kind.network !== undefined && facilitatorNetworkCandidates(String(accept.network)).includes(kind.network as Network) + ); if (!supportedKind) { logger.warn("advertising route not present in facilitator supported response", { tokenSymbol, @@ -301,6 +331,7 @@ export async function buildRechargeChallenge(amount: string, token: string, reso } return { ...accept, + network: (supportedKind.network ?? accept.network) as Network, extra: { ...(supportedKind.extra ?? {}), ...(accept.extra ?? {}) @@ -357,6 +388,10 @@ function selectedRequirementFromPayload(payload: PaymentPayload, challenge: Paym function paymentWalletAddress(payload: PaymentPayload): string { const payment = payload.payload as Record; + const gasfree = payment.gasfree as Record | undefined; + if (typeof gasfree?.user === "string") { + return gasfree.user; + } const permit = payment.paymentPermit as Record | undefined; if (typeof permit?.buyer === "string") { return permit.buyer; @@ -454,7 +489,10 @@ async function settleDecodedPayload( export function findNetworkConfigByPaymentNetwork(paymentNetwork: string): NetworkConfig { for (const cfg of supportedPaymentNetworkConfigs()) { - if (String(cfg.paymentNetwork) === String(paymentNetwork)) { + if ( + String(cfg.paymentNetwork) === String(paymentNetwork) || + String(sdkTronNetwork(cfg.paymentNetwork)) === String(paymentNetwork) + ) { return cfg; } } @@ -470,7 +508,7 @@ export function txExplorerUrl(txHash: string, paymentNetwork: string): string { } export function bankofaiChainId(paymentNetwork: string): string { - if (paymentNetwork === "tron:mainnet") { + if (paymentNetwork === TRON_MAINNET || paymentNetwork === "tron:mainnet") { return "eip155:728126428"; } if (paymentNetwork === "eip155:56") { diff --git a/test/payments.test.ts b/test/payments.test.ts index b6f4ff00..c44982c0 100644 --- a/test/payments.test.ts +++ b/test/payments.test.ts @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { networkConfigs } from "../src/config.js"; import { bankofaiChainId, normalizeToken, supportedTokens } from "../src/payments.js"; test("supported tokens are restricted to recharge payment scope", () => { @@ -10,6 +11,8 @@ test("supported tokens are restricted to recharge payment scope", () => { }); test("BANK OF AI chain ids are mainnet only", () => { + assert.equal(networkConfigs.mainnet.paymentNetwork, "tron:0x2b6653dc"); + assert.equal(bankofaiChainId("tron:0x2b6653dc"), "eip155:728126428"); assert.equal(bankofaiChainId("tron:mainnet"), "eip155:728126428"); assert.equal(bankofaiChainId("eip155:56"), "eip155:56"); assert.throws(() => bankofaiChainId("tron:nile"), /Unsupported chain mapping/);