From 19204c709bcd2cf01c59fca4f798e1521eb90398 Mon Sep 17 00:00:00 2001 From: Hades Date: Mon, 9 Mar 2026 22:13:12 +0800 Subject: [PATCH 01/10] feat: add AssetRegistry for multi-asset symbol-based token lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add AssetRegistry class (TS + Python) that maps (network, symbol) to token metadata, enabling B-form shorthand where developers specify assets: ["USDT", "USDC"] instead of manual addresses/decimals. - New AssetRegistry with built-in tokens for EVM (eip155:1/56/97) and TRON (mainnet/shasta/nile) networks - convertMoney utility for Money → smallest-unit conversion - Global singleton (globalAssetRegistry / global_asset_registry) - ResourceConfig and PaymentOption gain optional assets field - x402ResourceServer auto-expands B-form to D-form PaymentRequirements - 25 TS + 25 Python unit tests for registry and conversion --- python/x402/src/bankofai/x402/__init__.py | 6 + python/x402/src/bankofai/x402/http/types.py | 1 + python/x402/src/bankofai/x402/registry.py | 301 ++++++++++++++++++ .../x402/src/bankofai/x402/schemas/config.py | 1 + python/x402/src/bankofai/x402/server_base.py | 47 +++ .../tests/unit/core/test_asset_registry.py | 168 ++++++++++ .../core/src/http/x402HTTPResourceServer.ts | 1 + typescript/packages/core/src/index.ts | 3 + .../core/src/registry/assetRegistry.ts | 245 ++++++++++++++ .../packages/core/src/registry/index.ts | 11 + .../core/src/server/x402ResourceServer.ts | 53 ++- .../test/unit/registry/assetRegistry.test.ts | 201 ++++++++++++ 12 files changed, 1037 insertions(+), 1 deletion(-) create mode 100644 python/x402/src/bankofai/x402/registry.py create mode 100644 python/x402/tests/unit/core/test_asset_registry.py create mode 100644 typescript/packages/core/src/registry/assetRegistry.ts create mode 100644 typescript/packages/core/src/registry/index.ts create mode 100644 typescript/packages/core/test/unit/registry/assetRegistry.test.ts diff --git a/python/x402/src/bankofai/x402/__init__.py b/python/x402/src/bankofai/x402/__init__.py index d08acfdb..6ff1357e 100644 --- a/python/x402/src/bankofai/x402/__init__.py +++ b/python/x402/src/bankofai/x402/__init__.py @@ -63,6 +63,7 @@ x402ClientSync, ) from .facilitator import x402Facilitator, x402FacilitatorSync +from .registry import AssetInfo, AssetRegistry, convert_money, global_asset_registry # Interfaces (for implementing custom schemes) from .interfaces import ( @@ -140,6 +141,11 @@ __version__ = "0.1.0" __all__ = [ + # Asset Registry + "AssetInfo", + "AssetRegistry", + "convert_money", + "global_asset_registry", # Version "__version__", # Core components - Async (default) diff --git a/python/x402/src/bankofai/x402/http/types.py b/python/x402/src/bankofai/x402/http/types.py index 49d43cd9..29aa4a7e 100644 --- a/python/x402/src/bankofai/x402/http/types.py +++ b/python/x402/src/bankofai/x402/http/types.py @@ -167,6 +167,7 @@ class PaymentOption: network: Network max_timeout_seconds: int | None = None extra: dict[str, Any] | None = None + assets: list[str] | None = None @dataclass diff --git a/python/x402/src/bankofai/x402/registry.py b/python/x402/src/bankofai/x402/registry.py new file mode 100644 index 00000000..1d6852fb --- /dev/null +++ b/python/x402/src/bankofai/x402/registry.py @@ -0,0 +1,301 @@ +"""Asset registry for the x402 Python SDK. + +Provides a centralized registry of known token assets across networks, +allowing symbol-based lookup instead of manual address specification. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from .schemas.base import Network + + +@dataclass +class AssetInfo: + """Information about a token asset on a specific network. + + Attributes: + address: Token contract address. + decimals: Number of decimal places. + name: EIP-712 domain name (optional). + version: EIP-712 domain version (optional). + asset_transfer_method: Transfer method (e.g., "permit2"). + supports_eip2612: Whether token supports EIP-2612. + extra: Additional metadata. + """ + + address: str + decimals: int + name: str | None = None + version: str | None = None + asset_transfer_method: str | None = None + supports_eip2612: bool | None = None + extra: dict[str, Any] = field(default_factory=dict) + + +class AssetRegistry: + """Registry of known token assets across networks. + + Provides symbol-based lookup for token metadata, with built-in + knowledge of common tokens on supported networks. + """ + + def __init__(self) -> None: + # network -> symbol -> AssetInfo + self._assets: dict[str, dict[str, AssetInfo]] = {} + # network -> default symbol + self._defaults: dict[str, str] = {} + self._register_builtins() + + def register(self, network: Network, symbol: str, info: AssetInfo) -> None: + """Register a single asset for a network.""" + if network not in self._assets: + self._assets[network] = {} + self._assets[network][symbol] = info + + def register_all( + self, network: Network, assets: dict[str, AssetInfo] + ) -> None: + """Batch-register multiple assets for a network.""" + for symbol, info in assets.items(): + self.register(network, symbol, info) + + def set_default(self, network: Network, symbol: str) -> None: + """Set the default asset symbol for a network.""" + if not self.has(network, symbol): + raise ValueError( + f'Cannot set default: asset "{symbol}" is not registered ' + f'on network "{network}"' + ) + self._defaults[network] = symbol + + def resolve(self, network: Network, symbol: str) -> AssetInfo: + """Resolve a symbol to its AssetInfo on a network. + + Raises: + KeyError: If the symbol is not registered on the network. + """ + network_assets = self._assets.get(network) + if network_assets is None or symbol not in network_assets: + available = ( + ", ".join(network_assets.keys()) if network_assets else "none" + ) + raise KeyError( + f'Asset "{symbol}" is not registered on network "{network}". ' + f"Available: {available}" + ) + return network_assets[symbol] + + def get_default(self, network: Network) -> tuple[str, AssetInfo]: + """Get the default asset for a network. + + Returns: + Tuple of (symbol, AssetInfo). + + Raises: + KeyError: If no default is configured for the network. + """ + symbol = self._defaults.get(network) + if symbol is None: + raise KeyError( + f'No default asset configured for network "{network}"' + ) + return symbol, self.resolve(network, symbol) + + def get_symbols(self, network: Network) -> list[str]: + """List all registered symbols for a network.""" + network_assets = self._assets.get(network) + return list(network_assets.keys()) if network_assets else [] + + def has(self, network: Network, symbol: str) -> bool: + """Check if an asset is registered on a network.""" + return symbol in self._assets.get(network, {}) + + def _register_builtins(self) -> None: + """Register built-in known assets. + + Data sourced from EVM mechanism's getDefaultAsset() and + x402-deprecated token registry. + """ + # ── EVM Networks ────────────────────────────────────────── + + # eip155:1 — Ethereum Mainnet + self.register_all( + "eip155:1", + { + "USDC": AssetInfo( + address="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + decimals=6, + name="USD Coin", + version="2", + ), + "USDT": AssetInfo( + address="0xdAC17F958D2ee523a2206206994597C13D831ec7", + decimals=6, + name="Tether USD", + version="1", + asset_transfer_method="permit2", + ), + }, + ) + self._defaults["eip155:1"] = "USDC" + + # eip155:56 — BSC Mainnet (BEP-20, no EIP-3009/EIP-2612) + self.register_all( + "eip155:56", + { + "USDC": AssetInfo( + address="0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + decimals=18, + name="USD Coin", + version="1", + asset_transfer_method="permit2", + ), + "USDT": AssetInfo( + address="0x55d398326f99059fF775485246999027B3197955", + decimals=18, + name="Tether USD", + version="1", + asset_transfer_method="permit2", + ), + "EPS": AssetInfo( + address="0xA7f552078dcC247C2684336020c03648500C6d9F", + decimals=18, + name="Ellipsis", + version="1", + asset_transfer_method="permit2", + ), + }, + ) + self._defaults["eip155:56"] = "USDC" + + # eip155:97 — BSC Testnet (BEP-20, no EIP-3009/EIP-2612) + self.register_all( + "eip155:97", + { + "USDT": AssetInfo( + address="0x337610d27c682E347C9cD60BD4b3b107C9d34dDd", + decimals=18, + name="Tether USD", + version="1", + asset_transfer_method="permit2", + ), + "USDC": AssetInfo( + address="0x64544969ed7EBf5f083679233325356EbE738930", + decimals=18, + name="USD Coin", + version="1", + asset_transfer_method="permit2", + ), + "DHLU": AssetInfo( + address="0x375cADdd2cB68cE82e3D9B075D551067a7b4B816", + decimals=6, + name="DA HULU", + version="1", + asset_transfer_method="permit2", + ), + }, + ) + self._defaults["eip155:97"] = "USDT" + + # ── TRON Networks ───────────────────────────────────────── + + # tron:mainnet + self.register_all( + "tron:mainnet", + { + "USDT": AssetInfo( + address="TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + decimals=6, + name="Tether USD", + version="1", + asset_transfer_method="permit2", + ), + "USDD": AssetInfo( + address="TXDk8mbtRbXeYuMNS83CfKPaYYT8XWv9Hz", + decimals=18, + name="Decentralized USD", + version="1", + supports_eip2612=True, + ), + }, + ) + self._defaults["tron:mainnet"] = "USDT" + + # tron:shasta + self.register( + "tron:shasta", + "USDT", + AssetInfo( + address="TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs", + decimals=6, + name="Tether USD", + version="1", + asset_transfer_method="permit2", + ), + ) + self._defaults["tron:shasta"] = "USDT" + + # tron:nile + self.register_all( + "tron:nile", + { + "USDT": AssetInfo( + address="TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", + decimals=6, + name="Tether USD", + version="1", + asset_transfer_method="permit2", + ), + "USDD": AssetInfo( + address="TGjgvdTWWrybVLaVeFqSyVqJQWjxqRYbaK", + decimals=18, + name="Decentralized USD", + version="1", + supports_eip2612=True, + ), + }, + ) + self._defaults["tron:nile"] = "USDT" + + +def convert_money(price: str | int | float, decimals: int) -> str: + """Convert a Money value to token smallest-unit string. + + Args: + price: User-friendly price (e.g., "$1.50", 1.5, "0.10"). + decimals: Token decimal places. + + Returns: + Amount in smallest unit as string. + + Raises: + ValueError: If price format is invalid. + """ + if isinstance(price, str): + cleaned = price.lstrip("$").strip() + try: + numeric = float(cleaned) + except ValueError: + raise ValueError(f"Invalid money format: {price}") from None + else: + numeric = float(price) + + # Use string math to avoid floating point issues + str_amount = str(numeric) + if "." in str_amount: + int_part, dec_part = str_amount.split(".") + else: + int_part, dec_part = str_amount, "" + + padded_dec = (dec_part + "0" * decimals)[:decimals] + result = (int_part + padded_dec).lstrip("0") or "0" + return result + + +#: Global shared AssetRegistry instance with built-in token data. +#: Used by default in x402ResourceServer. Developers can register +#: custom tokens on this instance or create their own if isolation is needed. +global_asset_registry = AssetRegistry() diff --git a/python/x402/src/bankofai/x402/schemas/config.py b/python/x402/src/bankofai/x402/schemas/config.py index 22d22a67..cd9c686d 100644 --- a/python/x402/src/bankofai/x402/schemas/config.py +++ b/python/x402/src/bankofai/x402/schemas/config.py @@ -22,6 +22,7 @@ class ResourceConfig(BaseX402Model): price: Price network: Network max_timeout_seconds: int | None = None + assets: list[str] | None = None class FacilitatorConfig(TypedDict, total=False): diff --git a/python/x402/src/bankofai/x402/server_base.py b/python/x402/src/bankofai/x402/server_base.py index 75e06e4f..b8343fee 100644 --- a/python/x402/src/bankofai/x402/server_base.py +++ b/python/x402/src/bankofai/x402/server_base.py @@ -11,6 +11,7 @@ from typing_extensions import Self from .interfaces import SchemeNetworkServer +from .registry import AssetInfo, AssetRegistry, convert_money, global_asset_registry from .schemas import ( AbortResult, Network, @@ -143,6 +144,7 @@ class x402ResourceServerBase: def __init__( self, facilitator_clients: (_AnyFacilitatorClient | list[_AnyFacilitatorClient] | None) = None, + asset_registry: AssetRegistry | None = None, ) -> None: """Initialize base server.""" # Normalize to list @@ -153,6 +155,8 @@ def __init__( else: self._facilitator_clients = [facilitator_clients] + self.asset_registry: AssetRegistry = asset_registry or global_asset_registry + # Scheme servers self._schemes: dict[Network, dict[str, SchemeNetworkServer]] = {} @@ -314,6 +318,49 @@ def build_payment_requirements( if supported_kind is None: raise SchemeNotFoundError(config.scheme, config.network) + # New path: assets field + Money price → resolve via AssetRegistry + if ( + hasattr(config, "assets") + and config.assets + and isinstance(config.price, (str, int, float)) + ): + results: list[PaymentRequirements] = [] + for symbol in config.assets: + asset_info = self.asset_registry.resolve(config.network, symbol) + amount = convert_money(config.price, asset_info.decimals) + + # Build extra with EIP-712 domain fields conditionally + include_eip712 = ( + not asset_info.asset_transfer_method + or asset_info.supports_eip2612 + ) + extra: dict[str, Any] = {} + if include_eip712 and asset_info.name: + extra["name"] = asset_info.name + if include_eip712 and asset_info.version: + extra["version"] = asset_info.version + if asset_info.asset_transfer_method: + extra["assetTransferMethod"] = asset_info.asset_transfer_method + extra.update(asset_info.extra) + + base_req = PaymentRequirements( + scheme=config.scheme, + network=config.network, + asset=asset_info.address, + amount=amount, + pay_to=config.pay_to, + max_timeout_seconds=config.max_timeout_seconds or 300, + extra=extra, + ) + + enhanced = server.enhance_payment_requirements( + base_req, + supported_kind, + extensions or [], + ) + results.append(enhanced) + return results + # Parse price asset_amount = server.parse_price(config.price, config.network) diff --git a/python/x402/tests/unit/core/test_asset_registry.py b/python/x402/tests/unit/core/test_asset_registry.py new file mode 100644 index 00000000..39a1c6dc --- /dev/null +++ b/python/x402/tests/unit/core/test_asset_registry.py @@ -0,0 +1,168 @@ +"""Tests for AssetRegistry and convert_money.""" + +import pytest + +from bankofai.x402.registry import AssetInfo, AssetRegistry, convert_money + + +class TestAssetRegistry: + """Tests for AssetRegistry class.""" + + def test_builtin_eth_mainnet_usdc_and_usdt(self) -> None: + registry = AssetRegistry() + usdc = registry.resolve("eip155:1", "USDC") + assert usdc.address == "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + assert usdc.decimals == 6 + assert usdc.asset_transfer_method is None + + usdt = registry.resolve("eip155:1", "USDT") + assert usdt.asset_transfer_method == "permit2" + + def test_register_custom_asset(self) -> None: + registry = AssetRegistry() + info = AssetInfo(address="0xCustom", decimals=18) + registry.register("eip155:1", "WETH", info) + assert registry.resolve("eip155:1", "WETH") == info + + def test_register_on_new_network(self) -> None: + registry = AssetRegistry() + registry.register( + "eip155:999", "FOO", AssetInfo(address="0xFoo", decimals=8) + ) + assert registry.has("eip155:999", "FOO") + + def test_register_all(self) -> None: + registry = AssetRegistry() + registry.register_all( + "eip155:999", + { + "AAA": AssetInfo(address="0xAAA", decimals=6), + "BBB": AssetInfo(address="0xBBB", decimals=18), + }, + ) + assert registry.has("eip155:999", "AAA") + assert registry.has("eip155:999", "BBB") + + def test_set_default_and_get_default(self) -> None: + registry = AssetRegistry() + symbol, info = registry.get_default("eip155:1") + assert symbol == "USDC" + assert info.address == "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + + def test_set_default_unregistered_raises(self) -> None: + registry = AssetRegistry() + with pytest.raises(ValueError, match="not registered"): + registry.set_default("eip155:1", "NONEXISTENT") + + def test_get_default_no_default_raises(self) -> None: + registry = AssetRegistry() + registry.register( + "eip155:999", "FOO", AssetInfo(address="0xFoo", decimals=6) + ) + with pytest.raises(KeyError, match="No default"): + registry.get_default("eip155:999") + + def test_resolve_unregistered_symbol_raises(self) -> None: + registry = AssetRegistry() + with pytest.raises(KeyError, match="not registered"): + registry.resolve("eip155:1", "NONEXISTENT") + + def test_resolve_unregistered_network_raises(self) -> None: + registry = AssetRegistry() + with pytest.raises(KeyError, match="not registered"): + registry.resolve("eip155:999999", "USDC") + + def test_get_symbols(self) -> None: + registry = AssetRegistry() + symbols = registry.get_symbols("eip155:1") + assert "USDC" in symbols + assert "USDT" in symbols + + def test_get_symbols_unknown_network(self) -> None: + registry = AssetRegistry() + assert registry.get_symbols("eip155:999999") == [] + + def test_has_true(self) -> None: + registry = AssetRegistry() + assert registry.has("eip155:1", "USDC") is True + + def test_has_false(self) -> None: + registry = AssetRegistry() + assert registry.has("eip155:1", "WETH") is False + + def test_builtin_bsc_mainnet_tokens(self) -> None: + registry = AssetRegistry() + usdc = registry.resolve("eip155:56", "USDC") + assert usdc.address == "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" + assert usdc.decimals == 18 + assert usdc.asset_transfer_method == "permit2" + assert usdc.supports_eip2612 is None + + usdt = registry.resolve("eip155:56", "USDT") + assert usdt.address == "0x55d398326f99059fF775485246999027B3197955" + assert usdt.asset_transfer_method == "permit2" + + assert registry.has("eip155:56", "EPS") + + def test_builtin_bsc_testnet_tokens(self) -> None: + registry = AssetRegistry() + assert registry.has("eip155:97", "USDT") + assert registry.has("eip155:97", "USDC") + assert registry.has("eip155:97", "DHLU") + dhlu = registry.resolve("eip155:97", "DHLU") + assert dhlu.decimals == 6 + assert dhlu.asset_transfer_method == "permit2" + + def test_builtin_tron_mainnet_tokens(self) -> None: + registry = AssetRegistry() + usdt = registry.resolve("tron:mainnet", "USDT") + assert usdt.address == "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" + assert usdt.decimals == 6 + assert usdt.asset_transfer_method == "permit2" + + usdd = registry.resolve("tron:mainnet", "USDD") + assert usdd.decimals == 18 + assert usdd.supports_eip2612 is True + assert usdd.asset_transfer_method is None + + def test_builtin_tron_testnet_tokens(self) -> None: + registry = AssetRegistry() + assert registry.has("tron:shasta", "USDT") + assert registry.has("tron:nile", "USDT") + assert registry.has("tron:nile", "USDD") + + def test_builtin_bsc_tron_defaults(self) -> None: + registry = AssetRegistry() + assert registry.get_default("eip155:56")[0] == "USDC" + assert registry.get_default("eip155:97")[0] == "USDT" + assert registry.get_default("tron:mainnet")[0] == "USDT" + assert registry.get_default("tron:shasta")[0] == "USDT" + assert registry.get_default("tron:nile")[0] == "USDT" + + +class TestConvertMoney: + """Tests for convert_money function.""" + + def test_dollar_string_6_decimals(self) -> None: + assert convert_money("$1.50", 6) == "1500000" + + def test_number_6_decimals(self) -> None: + assert convert_money(1.5, 6) == "1500000" + + def test_18_decimals(self) -> None: + assert convert_money("$1.50", 18) == "1500000000000000000" + + def test_integer_price(self) -> None: + assert convert_money("$1", 6) == "1000000" + assert convert_money(1, 6) == "1000000" + + def test_small_amount(self) -> None: + assert convert_money("$0.001", 6) == "1000" + + def test_zero(self) -> None: + assert convert_money(0, 6) == "0" + assert convert_money("$0", 6) == "0" + + def test_invalid_format_raises(self) -> None: + with pytest.raises(ValueError, match="Invalid money format"): + convert_money("abc", 6) diff --git a/typescript/packages/core/src/http/x402HTTPResourceServer.ts b/typescript/packages/core/src/http/x402HTTPResourceServer.ts index c4cfb851..38f317ae 100644 --- a/typescript/packages/core/src/http/x402HTTPResourceServer.ts +++ b/typescript/packages/core/src/http/x402HTTPResourceServer.ts @@ -122,6 +122,7 @@ export interface PaymentOption { network: Network; maxTimeoutSeconds?: number; extra?: Record; + assets?: string[]; } /** diff --git a/typescript/packages/core/src/index.ts b/typescript/packages/core/src/index.ts index 3942c73e..f4db6ce8 100644 --- a/typescript/packages/core/src/index.ts +++ b/typescript/packages/core/src/index.ts @@ -1 +1,4 @@ export const x402Version = 2; + +export { AssetRegistry, convertMoney, globalAssetRegistry } from "./registry/index.js"; +export type { AssetInfo } from "./registry/index.js"; diff --git a/typescript/packages/core/src/registry/assetRegistry.ts b/typescript/packages/core/src/registry/assetRegistry.ts new file mode 100644 index 00000000..8c6e4ddd --- /dev/null +++ b/typescript/packages/core/src/registry/assetRegistry.ts @@ -0,0 +1,245 @@ +import { Network } from "../types/index.js"; + +/** + * Information about a token asset on a specific network. + */ +export interface AssetInfo { + address: string; + decimals: number; + name?: string; + version?: string; + assetTransferMethod?: string; + supportsEip2612?: boolean; + [key: string]: unknown; +} + +/** + * Registry of known token assets across networks. + * Provides symbol-based lookup for token metadata. + */ +export class AssetRegistry { + private assets: Map> = new Map(); + private defaults: Map = new Map(); + + constructor() { + this.registerBuiltins(); + } + + /** + * Register a single asset for a network. + */ + register(network: Network, symbol: string, info: AssetInfo): void { + if (!this.assets.has(network)) { + this.assets.set(network, new Map()); + } + this.assets.get(network)!.set(symbol, info); + } + + /** + * Batch-register multiple assets for a network. + */ + registerAll(network: Network, assets: Record): void { + for (const [symbol, info] of Object.entries(assets)) { + this.register(network, symbol, info); + } + } + + /** + * Set the default asset symbol for a network. + */ + setDefault(network: Network, symbol: string): void { + if (!this.has(network, symbol)) { + throw new Error( + `Cannot set default: asset "${symbol}" is not registered on network "${network}"`, + ); + } + this.defaults.set(network, symbol); + } + + /** + * Resolve a symbol to its AssetInfo on a network. + * Throws if not found. + */ + resolve(network: Network, symbol: string): AssetInfo { + const networkAssets = this.assets.get(network); + if (!networkAssets || !networkAssets.has(symbol)) { + throw new Error( + `Asset "${symbol}" is not registered on network "${network}". ` + + `Available: ${networkAssets ? Array.from(networkAssets.keys()).join(", ") : "none"}`, + ); + } + return networkAssets.get(symbol)!; + } + + /** + * Get the default asset for a network. + */ + getDefault(network: Network): { symbol: string; info: AssetInfo } { + const symbol = this.defaults.get(network); + if (!symbol) { + throw new Error(`No default asset configured for network "${network}"`); + } + return { symbol, info: this.resolve(network, symbol) }; + } + + /** + * List all registered symbols for a network. + */ + getSymbols(network: Network): string[] { + const networkAssets = this.assets.get(network); + return networkAssets ? Array.from(networkAssets.keys()) : []; + } + + /** + * Check if an asset is registered on a network. + */ + has(network: Network, symbol: string): boolean { + return this.assets.get(network)?.has(symbol) ?? false; + } + + /** + * Register built-in known assets. + * Data sourced from EVM mechanism's getDefaultAsset() and x402-deprecated token registry. + */ + private registerBuiltins(): void { + // ── EVM Networks ────────────────────────────────────────────── + + // eip155:1 — Ethereum Mainnet + this.registerAll("eip155:1" as Network, { + USDC: { + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + decimals: 6, + name: "USD Coin", + version: "2", + }, + USDT: { + address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + decimals: 6, + name: "Tether USD", + version: "1", + assetTransferMethod: "permit2", + }, + }); + this.defaults.set("eip155:1", "USDC"); + + // eip155:56 — BSC Mainnet (BEP-20, no EIP-3009/EIP-2612) + this.registerAll("eip155:56" as Network, { + USDC: { + address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + decimals: 18, + name: "USD Coin", + version: "1", + assetTransferMethod: "permit2", + }, + USDT: { + address: "0x55d398326f99059fF775485246999027B3197955", + decimals: 18, + name: "Tether USD", + version: "1", + assetTransferMethod: "permit2", + }, + EPS: { + address: "0xA7f552078dcC247C2684336020c03648500C6d9F", + decimals: 18, + name: "Ellipsis", + version: "1", + assetTransferMethod: "permit2", + }, + }); + this.defaults.set("eip155:56", "USDC"); + + // eip155:97 — BSC Testnet (BEP-20, no EIP-3009/EIP-2612) + this.registerAll("eip155:97" as Network, { + USDT: { + address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd", + decimals: 18, + name: "Tether USD", + version: "1", + assetTransferMethod: "permit2", + }, + USDC: { + address: "0x64544969ed7EBf5f083679233325356EbE738930", + decimals: 18, + name: "USD Coin", + version: "1", + assetTransferMethod: "permit2", + }, + DHLU: { + address: "0x375cADdd2cB68cE82e3D9B075D551067a7b4B816", + decimals: 6, + name: "DA HULU", + version: "1", + assetTransferMethod: "permit2", + }, + }); + this.defaults.set("eip155:97", "USDT"); + + // ── TRON Networks ───────────────────────────────────────────── + + // tron:mainnet + this.registerAll("tron:mainnet" as Network, { + USDT: { + address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + decimals: 6, + name: "Tether USD", + version: "1", + assetTransferMethod: "permit2", + }, + USDD: { + address: "TXDk8mbtRbXeYuMNS83CfKPaYYT8XWv9Hz", + decimals: 18, + name: "Decentralized USD", + version: "1", + supportsEip2612: true, + }, + }); + this.defaults.set("tron:mainnet", "USDT"); + + // tron:shasta + this.register("tron:shasta" as Network, "USDT", { + address: "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs", + decimals: 6, + name: "Tether USD", + version: "1", + assetTransferMethod: "permit2", + }); + this.defaults.set("tron:shasta", "USDT"); + + // tron:nile + this.registerAll("tron:nile" as Network, { + USDT: { + address: "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", + decimals: 6, + name: "Tether USD", + version: "1", + assetTransferMethod: "permit2", + }, + USDD: { + address: "TGjgvdTWWrybVLaVeFqSyVqJQWjxqRYbaK", + decimals: 18, + name: "Decentralized USD", + version: "1", + supportsEip2612: true, + }, + }); + this.defaults.set("tron:nile", "USDT"); + } +} + +/** + * Convert a Money value (e.g., "$1.50", 1.5) to token smallest-unit string + * using the given decimals. + */ +export function convertMoney(price: string | number, decimals: number): string { + const numericAmount = + typeof price === "string" ? parseFloat(price.replace(/^\$/, "").trim()) : price; + + if (isNaN(numericAmount)) { + throw new Error(`Invalid money format: ${price}`); + } + + // Use string math to avoid floating point issues + const [intPart, decPart = ""] = String(numericAmount).split("."); + const paddedDec = decPart.padEnd(decimals, "0").slice(0, decimals); + return (intPart + paddedDec).replace(/^0+/, "") || "0"; +} diff --git a/typescript/packages/core/src/registry/index.ts b/typescript/packages/core/src/registry/index.ts new file mode 100644 index 00000000..6cf95c9f --- /dev/null +++ b/typescript/packages/core/src/registry/index.ts @@ -0,0 +1,11 @@ +export { AssetRegistry, convertMoney } from "./assetRegistry.js"; +export type { AssetInfo } from "./assetRegistry.js"; + +import { AssetRegistry } from "./assetRegistry.js"; + +/** + * Global shared AssetRegistry instance with built-in token data. + * Used by default in x402ResourceServer. Developers can register + * custom tokens on this instance or create their own if isolation is needed. + */ +export const globalAssetRegistry = new AssetRegistry(); diff --git a/typescript/packages/core/src/server/x402ResourceServer.ts b/typescript/packages/core/src/server/x402ResourceServer.ts index e1459fd6..c32e9c81 100644 --- a/typescript/packages/core/src/server/x402ResourceServer.ts +++ b/typescript/packages/core/src/server/x402ResourceServer.ts @@ -16,6 +16,8 @@ import { Price, Network, ResourceServerExtension, VerifyError } from "../types"; import { deepEqual, findByNetworkAndScheme } from "../utils"; import { FacilitatorClient, HTTPFacilitatorClient } from "../http/httpFacilitatorClient"; import { x402Version } from ".."; +import { AssetRegistry, convertMoney, globalAssetRegistry } from "../registry/index.js"; +import type { AssetInfo } from "../registry/index.js"; /** * Configuration for a protected resource @@ -28,6 +30,7 @@ export interface ResourceConfig { network: Network; maxTimeoutSeconds?: number; extra?: Record; // Scheme-specific additional data + assets?: string[]; // Asset symbols to resolve via AssetRegistry (B-form) } /** @@ -105,6 +108,7 @@ export class x402ResourceServer { private facilitatorClientsMap: Map>> = new Map(); private registeredExtensions: Map = new Map(); + public readonly assetRegistry: AssetRegistry; private beforeVerifyHooks: BeforeVerifyHook[] = []; private afterVerifyHooks: AfterVerifyHook[] = []; @@ -117,8 +121,13 @@ export class x402ResourceServer { * Creates a new x402ResourceServer instance. * * @param facilitatorClients - Optional facilitator client(s) for payment processing + * @param assetRegistry - Optional asset registry for symbol-based asset resolution */ - constructor(facilitatorClients?: FacilitatorClient | FacilitatorClient[]) { + constructor( + facilitatorClients?: FacilitatorClient | FacilitatorClient[], + assetRegistry?: AssetRegistry, + ) { + this.assetRegistry = assetRegistry ?? globalAssetRegistry; // Normalize facilitator clients to array if (!facilitatorClients) { // No clients provided, create a default HTTP client @@ -442,6 +451,48 @@ export class x402ResourceServer { SchemeNetworkServer.scheme, ); + // New path: assets field + Money price → resolve via AssetRegistry + if ( + resourceConfig.assets?.length && + (typeof resourceConfig.price === "string" || typeof resourceConfig.price === "number") + ) { + const results: PaymentRequirements[] = []; + for (const symbol of resourceConfig.assets) { + const assetInfo = this.assetRegistry.resolve(resourceConfig.network, symbol); + const amount = convertMoney(resourceConfig.price, assetInfo.decimals); + const { address, decimals, name, version, assetTransferMethod, supportsEip2612, ...rest } = + assetInfo; + + // Build extra: include EIP-712 domain fields conditionally (same logic as EVM scheme) + const includeEip712Domain = !assetTransferMethod || supportsEip2612; + const extra: Record = { + ...(includeEip712Domain && name && { name }), + ...(includeEip712Domain && version && { version }), + ...(assetTransferMethod && { assetTransferMethod }), + ...rest, + ...resourceConfig.extra, + }; + + const baseReq: PaymentRequirements = { + scheme: SchemeNetworkServer.scheme, + network: resourceConfig.network, + amount, + asset: address, + payTo: resourceConfig.payTo, + maxTimeoutSeconds: resourceConfig.maxTimeoutSeconds || 300, + extra, + }; + + const enhanced = await SchemeNetworkServer.enhancePaymentRequirements( + baseReq, + { ...supportedKind, x402Version }, + facilitatorExtensions, + ); + results.push(enhanced); + } + return results; + } + // Parse the price using the scheme's price parser const parsedPrice = await SchemeNetworkServer.parsePrice( resourceConfig.price, diff --git a/typescript/packages/core/test/unit/registry/assetRegistry.test.ts b/typescript/packages/core/test/unit/registry/assetRegistry.test.ts new file mode 100644 index 00000000..779d4563 --- /dev/null +++ b/typescript/packages/core/test/unit/registry/assetRegistry.test.ts @@ -0,0 +1,201 @@ +import { describe, it, expect } from "vitest"; +import { AssetRegistry, convertMoney } from "../../../src/registry/index.js"; +import type { AssetInfo } from "../../../src/registry/index.js"; +import type { Network } from "../../../src/types/index.js"; + +describe("AssetRegistry", () => { + describe("built-in assets", () => { + it("should have USDC and USDT on Ethereum mainnet", () => { + const registry = new AssetRegistry(); + const usdc = registry.resolve("eip155:1" as Network, "USDC"); + expect(usdc.address).toBe("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + expect(usdc.decimals).toBe(6); + expect(usdc.assetTransferMethod).toBeUndefined(); + + const usdt = registry.resolve("eip155:1" as Network, "USDT"); + expect(usdt.assetTransferMethod).toBe("permit2"); + }); + + it("should have BSC Mainnet tokens with permit2", () => { + const registry = new AssetRegistry(); + const usdc = registry.resolve("eip155:56" as Network, "USDC"); + expect(usdc.address).toBe("0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"); + expect(usdc.decimals).toBe(18); + expect(usdc.assetTransferMethod).toBe("permit2"); + expect(usdc.supportsEip2612).toBeUndefined(); + + const usdt = registry.resolve("eip155:56" as Network, "USDT"); + expect(usdt.address).toBe("0x55d398326f99059fF775485246999027B3197955"); + expect(usdt.assetTransferMethod).toBe("permit2"); + + expect(registry.has("eip155:56" as Network, "EPS")).toBe(true); + }); + + it("should have BSC Testnet tokens with permit2", () => { + const registry = new AssetRegistry(); + expect(registry.has("eip155:97" as Network, "USDT")).toBe(true); + expect(registry.has("eip155:97" as Network, "USDC")).toBe(true); + expect(registry.has("eip155:97" as Network, "DHLU")).toBe(true); + const dhlu = registry.resolve("eip155:97" as Network, "DHLU"); + expect(dhlu.decimals).toBe(6); + expect(dhlu.assetTransferMethod).toBe("permit2"); + }); + + it("should have TRON mainnet tokens with correct transfer methods", () => { + const registry = new AssetRegistry(); + const usdt = registry.resolve("tron:mainnet" as Network, "USDT"); + expect(usdt.address).toBe("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"); + expect(usdt.decimals).toBe(6); + expect(usdt.assetTransferMethod).toBe("permit2"); + + const usdd = registry.resolve("tron:mainnet" as Network, "USDD"); + expect(usdd.decimals).toBe(18); + expect(usdd.supportsEip2612).toBe(true); + expect(usdd.assetTransferMethod).toBeUndefined(); + }); + + it("should have TRON testnet tokens", () => { + const registry = new AssetRegistry(); + expect(registry.has("tron:shasta" as Network, "USDT")).toBe(true); + expect(registry.has("tron:nile" as Network, "USDT")).toBe(true); + expect(registry.has("tron:nile" as Network, "USDD")).toBe(true); + }); + + it("should have correct defaults for BSC and TRON", () => { + const registry = new AssetRegistry(); + expect(registry.getDefault("eip155:56" as Network).symbol).toBe("USDC"); + expect(registry.getDefault("eip155:97" as Network).symbol).toBe("USDT"); + expect(registry.getDefault("tron:mainnet" as Network).symbol).toBe("USDT"); + expect(registry.getDefault("tron:shasta" as Network).symbol).toBe("USDT"); + expect(registry.getDefault("tron:nile" as Network).symbol).toBe("USDT"); + }); + }); + + describe("register", () => { + it("should register and resolve a custom asset", () => { + const registry = new AssetRegistry(); + const network = "eip155:1" as Network; + const info: AssetInfo = { + address: "0xCustomToken", + decimals: 18, + }; + registry.register(network, "WETH", info); + expect(registry.resolve(network, "WETH")).toEqual(info); + }); + + it("should register on a new network", () => { + const registry = new AssetRegistry(); + const network = "eip155:999" as Network; + registry.register(network, "FOO", { address: "0xFoo", decimals: 8 }); + expect(registry.has(network, "FOO")).toBe(true); + }); + }); + + describe("registerAll", () => { + it("should batch-register multiple assets", () => { + const registry = new AssetRegistry(); + const network = "eip155:999" as Network; + registry.registerAll(network, { + AAA: { address: "0xAAA", decimals: 6 }, + BBB: { address: "0xBBB", decimals: 18 }, + }); + expect(registry.has(network, "AAA")).toBe(true); + expect(registry.has(network, "BBB")).toBe(true); + }); + }); + + describe("setDefault / getDefault", () => { + it("should set and get default asset", () => { + const registry = new AssetRegistry(); + const result = registry.getDefault("eip155:1" as Network); + expect(result.symbol).toBe("USDC"); + expect(result.info.address).toBe("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + }); + + it("should throw if setting default for unregistered asset", () => { + const registry = new AssetRegistry(); + expect(() => registry.setDefault("eip155:1" as Network, "NONEXISTENT")).toThrow( + /not registered/, + ); + }); + + it("should throw for network with no default", () => { + const registry = new AssetRegistry(); + const network = "eip155:999" as Network; + registry.register(network, "FOO", { address: "0xFoo", decimals: 6 }); + expect(() => registry.getDefault(network)).toThrow(/No default/); + }); + }); + + describe("resolve", () => { + it("should throw for unregistered symbol", () => { + const registry = new AssetRegistry(); + expect(() => registry.resolve("eip155:1" as Network, "NONEXISTENT")).toThrow( + /not registered/, + ); + }); + + it("should throw for unregistered network", () => { + const registry = new AssetRegistry(); + expect(() => registry.resolve("eip155:999999" as Network, "USDC")).toThrow(/not registered/); + }); + }); + + describe("getSymbols", () => { + it("should list symbols for a network", () => { + const registry = new AssetRegistry(); + const symbols = registry.getSymbols("eip155:1" as Network); + expect(symbols).toContain("USDC"); + expect(symbols).toContain("USDT"); + }); + + it("should return empty for unknown network", () => { + const registry = new AssetRegistry(); + expect(registry.getSymbols("eip155:999999" as Network)).toEqual([]); + }); + }); + + describe("has", () => { + it("should return true for registered asset", () => { + const registry = new AssetRegistry(); + expect(registry.has("eip155:1" as Network, "USDC")).toBe(true); + }); + + it("should return false for unregistered asset", () => { + const registry = new AssetRegistry(); + expect(registry.has("eip155:1" as Network, "WETH")).toBe(false); + }); + }); +}); + +describe("convertMoney", () => { + it("should convert dollar string to 6-decimal token amount", () => { + expect(convertMoney("$1.50", 6)).toBe("1500000"); + }); + + it("should convert number to 6-decimal token amount", () => { + expect(convertMoney(1.5, 6)).toBe("1500000"); + }); + + it("should convert to 18-decimal token amount", () => { + expect(convertMoney("$1.50", 18)).toBe("1500000000000000000"); + }); + + it("should handle integer prices", () => { + expect(convertMoney("$1", 6)).toBe("1000000"); + expect(convertMoney(1, 6)).toBe("1000000"); + }); + + it("should handle small amounts", () => { + expect(convertMoney("$0.001", 6)).toBe("1000"); + }); + + it("should handle zero", () => { + expect(convertMoney(0, 6)).toBe("0"); + expect(convertMoney("$0", 6)).toBe("0"); + }); + + it("should throw for invalid format", () => { + expect(() => convertMoney("abc", 6)).toThrow(/Invalid money format/); + }); +}); From df244e01105ada60a4495d5dcbcbd4dabb635e59 Mon Sep 17 00:00:00 2001 From: Hades Date: Tue, 10 Mar 2026 11:28:44 +0800 Subject: [PATCH 02/10] refactor: clean up Python AssetRegistry formatting and imports - Fix import ordering in __init__.py - Remove unused AssetInfo import in server_base.py - Collapse unnecessary multi-line expressions per ruff format --- python/x402/src/bankofai/x402/__init__.py | 2 +- python/x402/src/bankofai/x402/registry.py | 18 +++++------------- python/x402/src/bankofai/x402/server_base.py | 7 ++----- .../tests/unit/core/test_asset_registry.py | 8 ++------ 4 files changed, 10 insertions(+), 25 deletions(-) diff --git a/python/x402/src/bankofai/x402/__init__.py b/python/x402/src/bankofai/x402/__init__.py index 6ff1357e..4729edd4 100644 --- a/python/x402/src/bankofai/x402/__init__.py +++ b/python/x402/src/bankofai/x402/__init__.py @@ -63,7 +63,6 @@ x402ClientSync, ) from .facilitator import x402Facilitator, x402FacilitatorSync -from .registry import AssetInfo, AssetRegistry, convert_money, global_asset_registry # Interfaces (for implementing custom schemes) from .interfaces import ( @@ -73,6 +72,7 @@ SchemeNetworkFacilitatorV1, SchemeNetworkServer, ) +from .registry import AssetInfo, AssetRegistry, convert_money, global_asset_registry # Types (re-export commonly used types) from .schemas import ( diff --git a/python/x402/src/bankofai/x402/registry.py b/python/x402/src/bankofai/x402/registry.py index 1d6852fb..5f907a51 100644 --- a/python/x402/src/bankofai/x402/registry.py +++ b/python/x402/src/bankofai/x402/registry.py @@ -55,9 +55,7 @@ def register(self, network: Network, symbol: str, info: AssetInfo) -> None: self._assets[network] = {} self._assets[network][symbol] = info - def register_all( - self, network: Network, assets: dict[str, AssetInfo] - ) -> None: + def register_all(self, network: Network, assets: dict[str, AssetInfo]) -> None: """Batch-register multiple assets for a network.""" for symbol, info in assets.items(): self.register(network, symbol, info) @@ -66,8 +64,7 @@ def set_default(self, network: Network, symbol: str) -> None: """Set the default asset symbol for a network.""" if not self.has(network, symbol): raise ValueError( - f'Cannot set default: asset "{symbol}" is not registered ' - f'on network "{network}"' + f'Cannot set default: asset "{symbol}" is not registered on network "{network}"' ) self._defaults[network] = symbol @@ -79,12 +76,9 @@ def resolve(self, network: Network, symbol: str) -> AssetInfo: """ network_assets = self._assets.get(network) if network_assets is None or symbol not in network_assets: - available = ( - ", ".join(network_assets.keys()) if network_assets else "none" - ) + available = ", ".join(network_assets.keys()) if network_assets else "none" raise KeyError( - f'Asset "{symbol}" is not registered on network "{network}". ' - f"Available: {available}" + f'Asset "{symbol}" is not registered on network "{network}". Available: {available}' ) return network_assets[symbol] @@ -99,9 +93,7 @@ def get_default(self, network: Network) -> tuple[str, AssetInfo]: """ symbol = self._defaults.get(network) if symbol is None: - raise KeyError( - f'No default asset configured for network "{network}"' - ) + raise KeyError(f'No default asset configured for network "{network}"') return symbol, self.resolve(network, symbol) def get_symbols(self, network: Network) -> list[str]: diff --git a/python/x402/src/bankofai/x402/server_base.py b/python/x402/src/bankofai/x402/server_base.py index b8343fee..ac565524 100644 --- a/python/x402/src/bankofai/x402/server_base.py +++ b/python/x402/src/bankofai/x402/server_base.py @@ -11,7 +11,7 @@ from typing_extensions import Self from .interfaces import SchemeNetworkServer -from .registry import AssetInfo, AssetRegistry, convert_money, global_asset_registry +from .registry import AssetRegistry, convert_money, global_asset_registry from .schemas import ( AbortResult, Network, @@ -330,10 +330,7 @@ def build_payment_requirements( amount = convert_money(config.price, asset_info.decimals) # Build extra with EIP-712 domain fields conditionally - include_eip712 = ( - not asset_info.asset_transfer_method - or asset_info.supports_eip2612 - ) + include_eip712 = not asset_info.asset_transfer_method or asset_info.supports_eip2612 extra: dict[str, Any] = {} if include_eip712 and asset_info.name: extra["name"] = asset_info.name diff --git a/python/x402/tests/unit/core/test_asset_registry.py b/python/x402/tests/unit/core/test_asset_registry.py index 39a1c6dc..635ff168 100644 --- a/python/x402/tests/unit/core/test_asset_registry.py +++ b/python/x402/tests/unit/core/test_asset_registry.py @@ -26,9 +26,7 @@ def test_register_custom_asset(self) -> None: def test_register_on_new_network(self) -> None: registry = AssetRegistry() - registry.register( - "eip155:999", "FOO", AssetInfo(address="0xFoo", decimals=8) - ) + registry.register("eip155:999", "FOO", AssetInfo(address="0xFoo", decimals=8)) assert registry.has("eip155:999", "FOO") def test_register_all(self) -> None: @@ -56,9 +54,7 @@ def test_set_default_unregistered_raises(self) -> None: def test_get_default_no_default_raises(self) -> None: registry = AssetRegistry() - registry.register( - "eip155:999", "FOO", AssetInfo(address="0xFoo", decimals=6) - ) + registry.register("eip155:999", "FOO", AssetInfo(address="0xFoo", decimals=6)) with pytest.raises(KeyError, match="No default"): registry.get_default("eip155:999") From 1af6467f6b2f03139fafd6696b320e363947d4c7 Mon Sep 17 00:00:00 2001 From: Hades Date: Tue, 10 Mar 2026 11:48:08 +0800 Subject: [PATCH 03/10] chore: remove EPS and DHLU tokens from AssetRegistry Internal test tokens no longer needed in the built-in registry. --- python/x402/src/bankofai/x402/registry.py | 14 -------------- python/x402/tests/unit/core/test_asset_registry.py | 6 ------ .../packages/core/src/registry/assetRegistry.ts | 14 -------------- .../core/test/unit/registry/assetRegistry.test.ts | 6 ------ 4 files changed, 40 deletions(-) diff --git a/python/x402/src/bankofai/x402/registry.py b/python/x402/src/bankofai/x402/registry.py index 5f907a51..d1b1ffee 100644 --- a/python/x402/src/bankofai/x402/registry.py +++ b/python/x402/src/bankofai/x402/registry.py @@ -152,13 +152,6 @@ def _register_builtins(self) -> None: version="1", asset_transfer_method="permit2", ), - "EPS": AssetInfo( - address="0xA7f552078dcC247C2684336020c03648500C6d9F", - decimals=18, - name="Ellipsis", - version="1", - asset_transfer_method="permit2", - ), }, ) self._defaults["eip155:56"] = "USDC" @@ -181,13 +174,6 @@ def _register_builtins(self) -> None: version="1", asset_transfer_method="permit2", ), - "DHLU": AssetInfo( - address="0x375cADdd2cB68cE82e3D9B075D551067a7b4B816", - decimals=6, - name="DA HULU", - version="1", - asset_transfer_method="permit2", - ), }, ) self._defaults["eip155:97"] = "USDT" diff --git a/python/x402/tests/unit/core/test_asset_registry.py b/python/x402/tests/unit/core/test_asset_registry.py index 635ff168..b5b96041 100644 --- a/python/x402/tests/unit/core/test_asset_registry.py +++ b/python/x402/tests/unit/core/test_asset_registry.py @@ -98,16 +98,10 @@ def test_builtin_bsc_mainnet_tokens(self) -> None: assert usdt.address == "0x55d398326f99059fF775485246999027B3197955" assert usdt.asset_transfer_method == "permit2" - assert registry.has("eip155:56", "EPS") - def test_builtin_bsc_testnet_tokens(self) -> None: registry = AssetRegistry() assert registry.has("eip155:97", "USDT") assert registry.has("eip155:97", "USDC") - assert registry.has("eip155:97", "DHLU") - dhlu = registry.resolve("eip155:97", "DHLU") - assert dhlu.decimals == 6 - assert dhlu.asset_transfer_method == "permit2" def test_builtin_tron_mainnet_tokens(self) -> None: registry = AssetRegistry() diff --git a/typescript/packages/core/src/registry/assetRegistry.ts b/typescript/packages/core/src/registry/assetRegistry.ts index 8c6e4ddd..5acb196f 100644 --- a/typescript/packages/core/src/registry/assetRegistry.ts +++ b/typescript/packages/core/src/registry/assetRegistry.ts @@ -138,13 +138,6 @@ export class AssetRegistry { version: "1", assetTransferMethod: "permit2", }, - EPS: { - address: "0xA7f552078dcC247C2684336020c03648500C6d9F", - decimals: 18, - name: "Ellipsis", - version: "1", - assetTransferMethod: "permit2", - }, }); this.defaults.set("eip155:56", "USDC"); @@ -164,13 +157,6 @@ export class AssetRegistry { version: "1", assetTransferMethod: "permit2", }, - DHLU: { - address: "0x375cADdd2cB68cE82e3D9B075D551067a7b4B816", - decimals: 6, - name: "DA HULU", - version: "1", - assetTransferMethod: "permit2", - }, }); this.defaults.set("eip155:97", "USDT"); diff --git a/typescript/packages/core/test/unit/registry/assetRegistry.test.ts b/typescript/packages/core/test/unit/registry/assetRegistry.test.ts index 779d4563..44bba4a7 100644 --- a/typescript/packages/core/test/unit/registry/assetRegistry.test.ts +++ b/typescript/packages/core/test/unit/registry/assetRegistry.test.ts @@ -27,18 +27,12 @@ describe("AssetRegistry", () => { const usdt = registry.resolve("eip155:56" as Network, "USDT"); expect(usdt.address).toBe("0x55d398326f99059fF775485246999027B3197955"); expect(usdt.assetTransferMethod).toBe("permit2"); - - expect(registry.has("eip155:56" as Network, "EPS")).toBe(true); }); it("should have BSC Testnet tokens with permit2", () => { const registry = new AssetRegistry(); expect(registry.has("eip155:97" as Network, "USDT")).toBe(true); expect(registry.has("eip155:97" as Network, "USDC")).toBe(true); - expect(registry.has("eip155:97" as Network, "DHLU")).toBe(true); - const dhlu = registry.resolve("eip155:97" as Network, "DHLU"); - expect(dhlu.decimals).toBe(6); - expect(dhlu.assetTransferMethod).toBe("permit2"); }); it("should have TRON mainnet tokens with correct transfer methods", () => { From 2a1a6de020a20fa81d77ce72c84348802aabf82f Mon Sep 17 00:00:00 2001 From: Hades Date: Tue, 10 Mar 2026 12:29:50 +0800 Subject: [PATCH 04/10] ci: update GitHub workflows and fix lint errors --- .github/workflows/check_format.yml | 22 ++++++ .github/workflows/check_lint.yml | 22 ++++++ .github/workflows/check_package_lock.yml | 29 +++++++ .github/workflows/check_python.yml | 6 +- .github/workflows/check_typescript.yml | 77 ------------------- .github/workflows/publish_npm_x402_aptos.yml | 54 +++++++++++++ .github/workflows/publish_npm_x402_axios.yml | 54 +++++++++++++ .github/workflows/publish_npm_x402_core.yml | 54 +++++++++++++ .github/workflows/publish_npm_x402_evm.yml | 54 +++++++++++++ .../workflows/publish_npm_x402_express.yml | 54 +++++++++++++ .../workflows/publish_npm_x402_extensions.yml | 54 +++++++++++++ .github/workflows/publish_npm_x402_fetch.yml | 54 +++++++++++++ .github/workflows/publish_npm_x402_hono.yml | 54 +++++++++++++ .github/workflows/publish_npm_x402_mcp.yml | 54 +++++++++++++ .github/workflows/publish_npm_x402_next.yml | 54 +++++++++++++ .../workflows/publish_npm_x402_paywall.yml | 54 +++++++++++++ .../workflows/publish_npm_x402_stellar.yml | 54 +++++++++++++ .github/workflows/publish_npm_x402_svm.yml | 54 +++++++++++++ ...publish_pypi.yml => publish_pypi_x402.yml} | 15 ++-- .github/workflows/unit_tests.yml | 28 +++++++ python/x402/src/bankofai/x402/mcp/client.py | 3 +- python/x402/src/bankofai/x402/mcp/server.py | 1 - .../x402/tests/integrations/test_mcp_evm.py | 9 ++- .../payment_identifier/test_utils.py | 5 +- .../unit/http/middleware/test_fastapi.py | 8 +- .../tests/unit/http/middleware/test_flask.py | 16 +++- .../core/src/registry/assetRegistry.ts | 41 +++++++++- .../packages/core/src/registry/index.ts | 4 +- .../core/src/server/x402ResourceServer.ts | 5 +- .../aptos/src/exact/client/scheme.ts | 6 +- 30 files changed, 891 insertions(+), 108 deletions(-) create mode 100644 .github/workflows/check_format.yml create mode 100644 .github/workflows/check_lint.yml create mode 100644 .github/workflows/check_package_lock.yml delete mode 100644 .github/workflows/check_typescript.yml create mode 100644 .github/workflows/publish_npm_x402_aptos.yml create mode 100644 .github/workflows/publish_npm_x402_axios.yml create mode 100644 .github/workflows/publish_npm_x402_core.yml create mode 100644 .github/workflows/publish_npm_x402_evm.yml create mode 100644 .github/workflows/publish_npm_x402_express.yml create mode 100644 .github/workflows/publish_npm_x402_extensions.yml create mode 100644 .github/workflows/publish_npm_x402_fetch.yml create mode 100644 .github/workflows/publish_npm_x402_hono.yml create mode 100644 .github/workflows/publish_npm_x402_mcp.yml create mode 100644 .github/workflows/publish_npm_x402_next.yml create mode 100644 .github/workflows/publish_npm_x402_paywall.yml create mode 100644 .github/workflows/publish_npm_x402_stellar.yml create mode 100644 .github/workflows/publish_npm_x402_svm.yml rename .github/workflows/{publish_pypi.yml => publish_pypi_x402.yml} (75%) create mode 100644 .github/workflows/unit_tests.yml diff --git a/.github/workflows/check_format.yml b/.github/workflows/check_format.yml new file mode 100644 index 00000000..936d6f12 --- /dev/null +++ b/.github/workflows/check_format.yml @@ -0,0 +1,22 @@ +name: Format +on: [pull_request] + +jobs: + check-format-typescript: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda + with: + run_install: false + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + cache-dependency-path: ./typescript + - name: Ensure formatting + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm format:check diff --git a/.github/workflows/check_lint.yml b/.github/workflows/check_lint.yml new file mode 100644 index 00000000..f6315454 --- /dev/null +++ b/.github/workflows/check_lint.yml @@ -0,0 +1,22 @@ +name: Lint +on: [pull_request] + +jobs: + check-lint-typescript: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda + with: + run_install: false + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + cache-dependency-path: ./typescript + - name: Ensure Linting + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm lint:check diff --git a/.github/workflows/check_package_lock.yml b/.github/workflows/check_package_lock.yml new file mode 100644 index 00000000..89eead02 --- /dev/null +++ b/.github/workflows/check_package_lock.yml @@ -0,0 +1,29 @@ +name: Package Lock +on: [pull_request] + +jobs: + check-package-lock-typescript: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda + with: + run_install: false + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Check if pnpm-lock.yaml changed + working-directory: ./typescript + run: | + pnpm install + if [ -n "$(git diff pnpm-lock.yaml)" ]; then + echo "Error: pnpm-lock.yaml was modified after running pnpm install. Please commit the updated pnpm-lock.yaml file." + git diff pnpm-lock.yaml + exit 1 + fi diff --git a/.github/workflows/check_python.yml b/.github/workflows/check_python.yml index 42b45857..79c1beb0 100644 --- a/.github/workflows/check_python.yml +++ b/.github/workflows/check_python.yml @@ -23,7 +23,7 @@ jobs: run: uv sync --all-extras --dev - name: Run Tests - run: uv run python -m pytest + run: uv run pytest lint-python: runs-on: ubuntu-latest @@ -39,8 +39,8 @@ jobs: enable-cache: true cache-dependency-glob: "python/x402/uv.lock" - - name: Lint with ruff + - name: Lint run: uvx ruff check - - name: Format check with ruff + - name: Check formatting run: uvx ruff format --check diff --git a/.github/workflows/check_typescript.yml b/.github/workflows/check_typescript.yml deleted file mode 100644 index a643ebeb..00000000 --- a/.github/workflows/check_typescript.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Check TypeScript -on: [pull_request] - -jobs: - lint-typescript: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda - with: - version: 9 - run_install: false - - - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "pnpm" - cache-dependency-path: ./typescript - - - name: Install dependencies - working-directory: ./typescript - run: pnpm install --frozen-lockfile - - - name: Lint - working-directory: ./typescript - run: pnpm lint - - build-typescript: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda - with: - version: 9 - run_install: false - - - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "pnpm" - cache-dependency-path: ./typescript - - - name: Install and build - working-directory: ./typescript - run: | - pnpm install --frozen-lockfile - pnpm build - - test-typescript: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: ["18", "20", "22"] - steps: - - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda - with: - version: 9 - run_install: false - - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: "pnpm" - cache-dependency-path: ./typescript - - - name: Install and Test - working-directory: ./typescript - run: | - pnpm install --frozen-lockfile - pnpm test diff --git a/.github/workflows/publish_npm_x402_aptos.yml b/.github/workflows/publish_npm_x402_aptos.yml new file mode 100644 index 00000000..34af66ad --- /dev/null +++ b/.github/workflows/publish_npm_x402_aptos.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-aptos package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-aptos: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-aptos run build + + - name: Publish @bankofai/x402-aptos package + working-directory: ./typescript/packages/mechanisms/aptos + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_axios.yml b/.github/workflows/publish_npm_x402_axios.yml new file mode 100644 index 00000000..676dfd95 --- /dev/null +++ b/.github/workflows/publish_npm_x402_axios.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-axios package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-axios: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-axios run build + + - name: Publish @bankofai/x402-axios package + working-directory: ./typescript/packages/http/axios + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_core.yml b/.github/workflows/publish_npm_x402_core.yml new file mode 100644 index 00000000..11e97f25 --- /dev/null +++ b/.github/workflows/publish_npm_x402_core.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-core package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-core: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core run build + + - name: Publish @bankofai/x402-core package + working-directory: ./typescript/packages/core + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_evm.yml b/.github/workflows/publish_npm_x402_evm.yml new file mode 100644 index 00000000..7746a846 --- /dev/null +++ b/.github/workflows/publish_npm_x402_evm.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-evm package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-evm: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-extensions --filter=@bankofai/x402-evm run build + + - name: Publish @bankofai/x402-evm package + working-directory: ./typescript/packages/mechanisms/evm + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_express.yml b/.github/workflows/publish_npm_x402_express.yml new file mode 100644 index 00000000..e04b2360 --- /dev/null +++ b/.github/workflows/publish_npm_x402_express.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-express package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-express: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-extensions --filter=@bankofai/x402-express run build + + - name: Publish @bankofai/x402-express package + working-directory: ./typescript/packages/http/express + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_extensions.yml b/.github/workflows/publish_npm_x402_extensions.yml new file mode 100644 index 00000000..af19c830 --- /dev/null +++ b/.github/workflows/publish_npm_x402_extensions.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-extensions package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-extensions: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-extensions run build + + - name: Publish @bankofai/x402-extensions package + working-directory: ./typescript/packages/extensions + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_fetch.yml b/.github/workflows/publish_npm_x402_fetch.yml new file mode 100644 index 00000000..4fe919ff --- /dev/null +++ b/.github/workflows/publish_npm_x402_fetch.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-fetch package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-fetch: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-fetch run build + + - name: Publish @bankofai/x402-fetch package + working-directory: ./typescript/packages/http/fetch + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_hono.yml b/.github/workflows/publish_npm_x402_hono.yml new file mode 100644 index 00000000..a27688be --- /dev/null +++ b/.github/workflows/publish_npm_x402_hono.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-hono package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-hono: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-extensions --filter=@bankofai/x402-hono run build + + - name: Publish @bankofai/x402-hono package + working-directory: ./typescript/packages/http/hono + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_mcp.yml b/.github/workflows/publish_npm_x402_mcp.yml new file mode 100644 index 00000000..2520a238 --- /dev/null +++ b/.github/workflows/publish_npm_x402_mcp.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-mcp package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-mcp: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-mcp run build + + - name: Publish @bankofai/x402-mcp package + working-directory: ./typescript/packages/mcp + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_next.yml b/.github/workflows/publish_npm_x402_next.yml new file mode 100644 index 00000000..e06bf6e3 --- /dev/null +++ b/.github/workflows/publish_npm_x402_next.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-next package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-next: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-extensions --filter=@bankofai/x402-next run build + + - name: Publish @bankofai/x402-next package + working-directory: ./typescript/packages/http/next + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_paywall.yml b/.github/workflows/publish_npm_x402_paywall.yml new file mode 100644 index 00000000..a92fb78d --- /dev/null +++ b/.github/workflows/publish_npm_x402_paywall.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-paywall package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-paywall: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-extensions --filter=@bankofai/x402-evm --filter=@bankofai/x402-svm --filter=@bankofai/x402-paywall run build + + - name: Publish @bankofai/x402-paywall package + working-directory: ./typescript/packages/http/paywall + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_stellar.yml b/.github/workflows/publish_npm_x402_stellar.yml new file mode 100644 index 00000000..931ec59a --- /dev/null +++ b/.github/workflows/publish_npm_x402_stellar.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-stellar package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-stellar: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-stellar run build + + - name: Publish @bankofai/x402-stellar package + working-directory: ./typescript/packages/mechanisms/stellar + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_x402_svm.yml b/.github/workflows/publish_npm_x402_svm.yml new file mode 100644 index 00000000..ed1299d0 --- /dev/null +++ b/.github/workflows/publish_npm_x402_svm.yml @@ -0,0 +1,54 @@ +name: Publish @bankofai/x402-svm package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-svm: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@bankofai/x402-core --filter=@bankofai/x402-svm run build + + - name: Publish @bankofai/x402-svm package + working-directory: ./typescript/packages/mechanisms/svm + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi_x402.yml similarity index 75% rename from .github/workflows/publish_pypi.yml rename to .github/workflows/publish_pypi_x402.yml index 354df981..da78c50b 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi_x402.yml @@ -1,24 +1,22 @@ -name: Publish to PyPI +name: Publish x402 to PyPI on: workflow_dispatch: jobs: - publish-pypi: + publish-pypi-x402: runs-on: ubuntu-latest defaults: run: working-directory: ./python/x402 environment: name: pypi - url: https://pypi.org/p/x402-tron + url: https://pypi.org/p/bankofai-x402 permissions: contents: read - id-token: write steps: - uses: actions/checkout@v4 - - run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" @@ -32,7 +30,7 @@ jobs: uses: astral-sh/setup-uv@v5 with: enable-cache: true - cache-dependency-glob: "python/x402/uv.lock" + cache-dependency-glob: "uv.lock" - name: Install dependencies run: uv sync @@ -43,7 +41,8 @@ jobs: uv build echo "version=$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml)" >> $GITHUB_OUTPUT - - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + - name: Publish package + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc with: packages-dir: python/x402/dist/ + password: ${{ secrets.PYPI_X402_TOKEN }} diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 00000000..35568e4b --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,28 @@ +name: Run Unit Tests +on: [pull_request] + +jobs: + test-x402-typescript: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: ["20", "22"] + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda + with: + run_install: false + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Install and Test + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm run test diff --git a/python/x402/src/bankofai/x402/mcp/client.py b/python/x402/src/bankofai/x402/mcp/client.py index c5c54a1b..3782508d 100644 --- a/python/x402/src/bankofai/x402/mcp/client.py +++ b/python/x402/src/bankofai/x402/mcp/client.py @@ -309,9 +309,8 @@ async def create_x402_mcp_client( result = await mcp.call_tool("get_weather", {"city": "SF"}) ``` """ - from mcp.client.sse import sse_client - from mcp import ClientSession + from mcp.client.sse import sse_client sse_url = server_url.rstrip("/") if not sse_url.endswith("/sse"): diff --git a/python/x402/src/bankofai/x402/mcp/server.py b/python/x402/src/bankofai/x402/mcp/server.py index f5961eaa..18e930bc 100644 --- a/python/x402/src/bankofai/x402/mcp/server.py +++ b/python/x402/src/bankofai/x402/mcp/server.py @@ -85,7 +85,6 @@ def create_payment_wrapper( """ # Lazy import mcp types so the module can be imported without mcp installed from mcp.server.fastmcp import Context - from mcp.types import CallToolResult, TextContent if not accepts: diff --git a/python/x402/tests/integrations/test_mcp_evm.py b/python/x402/tests/integrations/test_mcp_evm.py index 88a7ae36..072eb5ea 100644 --- a/python/x402/tests/integrations/test_mcp_evm.py +++ b/python/x402/tests/integrations/test_mcp_evm.py @@ -22,11 +22,11 @@ import pytest mcp = pytest.importorskip("mcp", reason="mcp package not available") +from mcp import ClientSession # noqa: E402 from mcp.client.streamable_http import streamable_http_client # noqa: E402 from mcp.server.fastmcp import FastMCP # noqa: E402 - -from mcp import ClientSession # noqa: E402 from mcp.types import TextContent # noqa: E402 + from bankofai.x402 import x402ClientSync, x402FacilitatorSync, x402ResourceServerSync # noqa: E402 from bankofai.x402.mcp import create_payment_wrapper, x402MCPClientSync # noqa: E402 from bankofai.x402.mechanisms.evm.exact import ( # noqa: E402 @@ -35,7 +35,10 @@ ExactEvmSchemeConfig, ExactEvmServerScheme, ) -from bankofai.x402.mechanisms.evm.signers import EthAccountSigner, FacilitatorWeb3Signer # noqa: E402 +from bankofai.x402.mechanisms.evm.signers import ( # noqa: E402 + EthAccountSigner, + FacilitatorWeb3Signer, +) from bankofai.x402.schemas import ResourceConfig, ResourceInfo # noqa: E402 # Environment variables diff --git a/python/x402/tests/unit/extensions/payment_identifier/test_utils.py b/python/x402/tests/unit/extensions/payment_identifier/test_utils.py index 08254da4..0ad3722e 100644 --- a/python/x402/tests/unit/extensions/payment_identifier/test_utils.py +++ b/python/x402/tests/unit/extensions/payment_identifier/test_utils.py @@ -1,6 +1,9 @@ """Tests for Payment-Identifier utility functions.""" -from bankofai.x402.extensions.payment_identifier.utils import generate_payment_id, is_valid_payment_id +from bankofai.x402.extensions.payment_identifier.utils import ( + generate_payment_id, + is_valid_payment_id, +) class TestGeneratePaymentId: diff --git a/python/x402/tests/unit/http/middleware/test_fastapi.py b/python/x402/tests/unit/http/middleware/test_fastapi.py index fee5ae44..a299a375 100644 --- a/python/x402/tests/unit/http/middleware/test_fastapi.py +++ b/python/x402/tests/unit/http/middleware/test_fastapi.py @@ -353,7 +353,9 @@ def protected_route(): payment_payload = make_v2_payload() payment_requirements = make_payment_requirements() - with patch("bankofai.x402.http.middleware.fastapi.x402HTTPResourceServer") as mock_http_server: + with patch( + "bankofai.x402.http.middleware.fastapi.x402HTTPResourceServer" + ) as mock_http_server: mock_http_server_instance = MagicMock() mock_http_server_instance.requires_payment.return_value = True mock_http_server_instance.process_http_request = AsyncMock( @@ -409,7 +411,9 @@ def protected_route(): payment_payload = make_v2_payload() payment_requirements = make_payment_requirements() - with patch("bankofai.x402.http.middleware.fastapi.x402HTTPResourceServer") as mock_http_server: + with patch( + "bankofai.x402.http.middleware.fastapi.x402HTTPResourceServer" + ) as mock_http_server: mock_http_server_instance = MagicMock() mock_http_server_instance.requires_payment.return_value = True mock_http_server_instance.process_http_request = AsyncMock( diff --git a/python/x402/tests/unit/http/middleware/test_flask.py b/python/x402/tests/unit/http/middleware/test_flask.py index f0cbf6dc..1fbbaaba 100644 --- a/python/x402/tests/unit/http/middleware/test_flask.py +++ b/python/x402/tests/unit/http/middleware/test_flask.py @@ -420,7 +420,9 @@ def public_route(): } # Create middleware with mocked http server - with patch("bankofai.x402.http.middleware.flask.x402HTTPResourceServerSync") as mock_http_server: + with patch( + "bankofai.x402.http.middleware.flask.x402HTTPResourceServerSync" + ) as mock_http_server: mock_http_server_instance = MagicMock() mock_http_server_instance.requires_payment.return_value = False mock_http_server.return_value = mock_http_server_instance @@ -453,7 +455,9 @@ def protected_route(): } # Create middleware with mocked http server - with patch("bankofai.x402.http.middleware.flask.x402HTTPResourceServerSync") as mock_http_server: + with patch( + "bankofai.x402.http.middleware.flask.x402HTTPResourceServerSync" + ) as mock_http_server: mock_http_server_instance = MagicMock() mock_http_server_instance.requires_payment.return_value = True mock_http_server_instance.process_http_request.return_value = HTTPProcessResult( @@ -496,7 +500,9 @@ def protected_route(): payment_payload = make_v2_payload() payment_requirements = make_payment_requirements() - with patch("bankofai.x402.http.middleware.flask.x402HTTPResourceServerSync") as mock_http_server: + with patch( + "bankofai.x402.http.middleware.flask.x402HTTPResourceServerSync" + ) as mock_http_server: mock_http_server_instance = MagicMock() mock_http_server_instance.requires_payment.return_value = True mock_http_server_instance.process_http_request.return_value = HTTPProcessResult( @@ -535,7 +541,9 @@ def protected_route(): payment_payload = make_v2_payload() payment_requirements = make_payment_requirements() - with patch("bankofai.x402.http.middleware.flask.x402HTTPResourceServerSync") as mock_http_server: + with patch( + "bankofai.x402.http.middleware.flask.x402HTTPResourceServerSync" + ) as mock_http_server: mock_http_server_instance = MagicMock() mock_http_server_instance.requires_payment.return_value = True mock_http_server_instance.process_http_request.return_value = HTTPProcessResult( diff --git a/typescript/packages/core/src/registry/assetRegistry.ts b/typescript/packages/core/src/registry/assetRegistry.ts index 5acb196f..a69774ee 100644 --- a/typescript/packages/core/src/registry/assetRegistry.ts +++ b/typescript/packages/core/src/registry/assetRegistry.ts @@ -4,13 +4,20 @@ import { Network } from "../types/index.js"; * Information about a token asset on a specific network. */ export interface AssetInfo { + /** Additional metadata */ + [key: string]: unknown; + /** Token contract address */ address: string; + /** Number of decimal places */ decimals: number; + /** Human-readable token name (used in EIP-712 domain) */ name?: string; + /** Token version string (used in EIP-712 domain) */ version?: string; + /** Transfer method identifier (e.g. "permit2") */ assetTransferMethod?: string; + /** Whether the token supports EIP-2612 permit */ supportsEip2612?: boolean; - [key: string]: unknown; } /** @@ -21,12 +28,17 @@ export class AssetRegistry { private assets: Map> = new Map(); private defaults: Map = new Map(); + /** Creates a new AssetRegistry pre-populated with built-in token data. */ constructor() { this.registerBuiltins(); } /** * Register a single asset for a network. + * + * @param network - The network identifier (e.g. "eip155:1") + * @param symbol - The token symbol (e.g. "USDT") + * @param info - Asset metadata */ register(network: Network, symbol: string, info: AssetInfo): void { if (!this.assets.has(network)) { @@ -37,6 +49,9 @@ export class AssetRegistry { /** * Batch-register multiple assets for a network. + * + * @param network - The network identifier + * @param assets - Map of symbol to AssetInfo */ registerAll(network: Network, assets: Record): void { for (const [symbol, info] of Object.entries(assets)) { @@ -46,6 +61,9 @@ export class AssetRegistry { /** * Set the default asset symbol for a network. + * + * @param network - The network identifier + * @param symbol - The symbol to set as default */ setDefault(network: Network, symbol: string): void { if (!this.has(network, symbol)) { @@ -58,7 +76,11 @@ export class AssetRegistry { /** * Resolve a symbol to its AssetInfo on a network. - * Throws if not found. + * + * @param network - The network identifier + * @param symbol - The token symbol + * @returns The resolved AssetInfo + * @throws If the asset is not registered */ resolve(network: Network, symbol: string): AssetInfo { const networkAssets = this.assets.get(network); @@ -73,6 +95,10 @@ export class AssetRegistry { /** * Get the default asset for a network. + * + * @param network - The network identifier + * @returns The default symbol and its AssetInfo + * @throws If no default is configured */ getDefault(network: Network): { symbol: string; info: AssetInfo } { const symbol = this.defaults.get(network); @@ -84,6 +110,9 @@ export class AssetRegistry { /** * List all registered symbols for a network. + * + * @param network - The network identifier + * @returns Array of registered symbols */ getSymbols(network: Network): string[] { const networkAssets = this.assets.get(network); @@ -92,6 +121,10 @@ export class AssetRegistry { /** * Check if an asset is registered on a network. + * + * @param network - The network identifier + * @param symbol - The token symbol + * @returns True if the asset is registered */ has(network: Network, symbol: string): boolean { return this.assets.get(network)?.has(symbol) ?? false; @@ -215,6 +248,10 @@ export class AssetRegistry { /** * Convert a Money value (e.g., "$1.50", 1.5) to token smallest-unit string * using the given decimals. + * + * @param price - The price as a string or number + * @param decimals - The number of decimal places for the token + * @returns The amount in smallest units as a string */ export function convertMoney(price: string | number, decimals: number): string { const numericAmount = diff --git a/typescript/packages/core/src/registry/index.ts b/typescript/packages/core/src/registry/index.ts index 6cf95c9f..2d5e2da7 100644 --- a/typescript/packages/core/src/registry/index.ts +++ b/typescript/packages/core/src/registry/index.ts @@ -1,8 +1,8 @@ +import { AssetRegistry } from "./assetRegistry.js"; + export { AssetRegistry, convertMoney } from "./assetRegistry.js"; export type { AssetInfo } from "./assetRegistry.js"; -import { AssetRegistry } from "./assetRegistry.js"; - /** * Global shared AssetRegistry instance with built-in token data. * Used by default in x402ResourceServer. Developers can register diff --git a/typescript/packages/core/src/server/x402ResourceServer.ts b/typescript/packages/core/src/server/x402ResourceServer.ts index c32e9c81..e78487ee 100644 --- a/typescript/packages/core/src/server/x402ResourceServer.ts +++ b/typescript/packages/core/src/server/x402ResourceServer.ts @@ -17,7 +17,6 @@ import { deepEqual, findByNetworkAndScheme } from "../utils"; import { FacilitatorClient, HTTPFacilitatorClient } from "../http/httpFacilitatorClient"; import { x402Version } from ".."; import { AssetRegistry, convertMoney, globalAssetRegistry } from "../registry/index.js"; -import type { AssetInfo } from "../registry/index.js"; /** * Configuration for a protected resource @@ -101,6 +100,8 @@ export type OnSettleFailureHook = ( * Transport-agnostic implementation of the x402 payment protocol */ export class x402ResourceServer { + public readonly assetRegistry: AssetRegistry; + private facilitatorClients: FacilitatorClient[]; private registeredServerSchemes: Map> = new Map(); private supportedResponsesMap: Map>> = @@ -108,7 +109,6 @@ export class x402ResourceServer { private facilitatorClientsMap: Map>> = new Map(); private registeredExtensions: Map = new Map(); - public readonly assetRegistry: AssetRegistry; private beforeVerifyHooks: BeforeVerifyHook[] = []; private afterVerifyHooks: AfterVerifyHook[] = []; @@ -460,6 +460,7 @@ export class x402ResourceServer { for (const symbol of resourceConfig.assets) { const assetInfo = this.assetRegistry.resolve(resourceConfig.network, symbol); const amount = convertMoney(resourceConfig.price, assetInfo.decimals); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { address, decimals, name, version, assetTransferMethod, supportsEip2612, ...rest } = assetInfo; diff --git a/typescript/packages/mechanisms/aptos/src/exact/client/scheme.ts b/typescript/packages/mechanisms/aptos/src/exact/client/scheme.ts index 4b6ef468..06de6035 100644 --- a/typescript/packages/mechanisms/aptos/src/exact/client/scheme.ts +++ b/typescript/packages/mechanisms/aptos/src/exact/client/scheme.ts @@ -1,5 +1,9 @@ import { AccountAddress, Aptos, AptosConfig, SimpleTransaction } from "@aptos-labs/ts-sdk"; -import type { PaymentPayload, PaymentRequirements, SchemeNetworkClient } from "@bankofai/x402-core/types"; +import type { + PaymentPayload, + PaymentRequirements, + SchemeNetworkClient, +} from "@bankofai/x402-core/types"; import { APTOS_ADDRESS_REGEX, getAptosNetwork, getAptosRpcUrl } from "../../constants"; import type { ClientAptosSigner, ClientAptosConfig } from "../../signer"; import type { ExactAptosPayload } from "../../types"; From b22c5d5ead5813007c454a95a56e22aa6d39ed4a Mon Sep 17 00:00:00 2001 From: Hades Date: Tue, 10 Mar 2026 12:36:48 +0800 Subject: [PATCH 05/10] chore: remove unnecessary root package.json --- package.json | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 package.json diff --git a/package.json b/package.json deleted file mode 100644 index 98ee83f9..00000000 --- a/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "bofai-x402", - "version": "0.1.0", - "private": true, - "workspaces": [ - "typescript/packages/*", - "typescript/packages/http/*", - "typescript/packages/mechanisms/*", - "typescript/packages/legacy/*" - ], - "scripts": { - "prepare": "npm run -ws --if-present build" - }, - "devDependencies": { - "typescript": "^5.3.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "dependencies": { - "tronweb": "^6.2.0", - "viem": "^2.45.2" - } -} From 88ae6ca5488fafb6de3822cf11d724eaef64b775 Mon Sep 17 00:00:00 2001 From: Hades Date: Tue, 10 Mar 2026 12:39:27 +0800 Subject: [PATCH 06/10] Revert "chore: remove unnecessary root package.json" This reverts commit b22c5d5ead5813007c454a95a56e22aa6d39ed4a. --- package.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 00000000..98ee83f9 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "bofai-x402", + "version": "0.1.0", + "private": true, + "workspaces": [ + "typescript/packages/*", + "typescript/packages/http/*", + "typescript/packages/mechanisms/*", + "typescript/packages/legacy/*" + ], + "scripts": { + "prepare": "npm run -ws --if-present build" + }, + "devDependencies": { + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "tronweb": "^6.2.0", + "viem": "^2.45.2" + } +} From 617b6fc55dfc582ddbba7f4517a3a3db53250244 Mon Sep 17 00:00:00 2001 From: Hades Date: Tue, 10 Mar 2026 12:40:27 +0800 Subject: [PATCH 07/10] fix: add packageManager field for CI pnpm version detection --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 98ee83f9..57fc4a1d 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "bofai-x402", "version": "0.1.0", "private": true, + "packageManager": "pnpm@10.7.0", "workspaces": [ "typescript/packages/*", "typescript/packages/http/*", From 175ba874ef8cffdcc4918b88a1e81cdcff4deaa6 Mon Sep 17 00:00:00 2001 From: Hades Date: Tue, 10 Mar 2026 12:46:12 +0800 Subject: [PATCH 08/10] chore: clean up root package.json and remove unnecessary pyproject.toml --- package.json | 23 +---------------------- pyproject.toml | 7 ------- 2 files changed, 1 insertion(+), 29 deletions(-) delete mode 100644 pyproject.toml diff --git a/package.json b/package.json index 57fc4a1d..bd3ef4e2 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,4 @@ { - "name": "bofai-x402", - "version": "0.1.0", "private": true, - "packageManager": "pnpm@10.7.0", - "workspaces": [ - "typescript/packages/*", - "typescript/packages/http/*", - "typescript/packages/mechanisms/*", - "typescript/packages/legacy/*" - ], - "scripts": { - "prepare": "npm run -ws --if-present build" - }, - "devDependencies": { - "typescript": "^5.3.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "dependencies": { - "tronweb": "^6.2.0", - "viem": "^2.45.2" - } + "packageManager": "pnpm@10.7.0" } diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index e04da222..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,7 +0,0 @@ -[tool.uv] -dev-dependencies = [ - "pytest>=7.0.0", - "pytest-asyncio>=0.21.0", - "mypy>=1.0.0", - "ruff>=0.1.0", -] From 3a572b52fa60b29d446af060a0898a56ca250c8c Mon Sep 17 00:00:00 2001 From: Hades Date: Tue, 10 Mar 2026 12:49:46 +0800 Subject: [PATCH 09/10] fix: auto-format TypeScript files to pass CI prettier checks --- .../extensions/src/bazaar/facilitator.ts | 6 +++++- typescript/packages/http/axios/src/index.test.ts | 6 +++++- .../packages/http/express/src/index.test.ts | 6 +++++- typescript/packages/http/fetch/src/index.test.ts | 6 +++++- typescript/packages/http/hono/src/index.test.ts | 6 +++++- typescript/packages/http/next/src/index.test.ts | 6 +++++- .../packages/legacy/x402-axios/src/index.test.ts | 16 +++++++++++++--- .../packages/legacy/x402-fetch/src/index.test.ts | 4 +++- typescript/packages/mcp/src/index.ts | 6 +++++- .../stellar/src/exact/client/scheme.ts | 6 +++++- .../mechanisms/svm/src/exact/client/scheme.ts | 6 +++++- 11 files changed, 61 insertions(+), 13 deletions(-) diff --git a/typescript/packages/extensions/src/bazaar/facilitator.ts b/typescript/packages/extensions/src/bazaar/facilitator.ts index 44d78ef5..34c48717 100644 --- a/typescript/packages/extensions/src/bazaar/facilitator.ts +++ b/typescript/packages/extensions/src/bazaar/facilitator.ts @@ -8,7 +8,11 @@ */ import Ajv from "ajv/dist/2020.js"; -import type { PaymentPayload, PaymentRequirements, PaymentRequirementsV1 } from "@bankofai/x402-core/types"; +import type { + PaymentPayload, + PaymentRequirements, + PaymentRequirementsV1, +} from "@bankofai/x402-core/types"; import type { DiscoveryExtension, DiscoveryInfo } from "./types"; import type { McpDiscoveryInfo } from "./mcp/types"; import type { DiscoveredHTTPResource } from "./http/types"; diff --git a/typescript/packages/http/axios/src/index.test.ts b/typescript/packages/http/axios/src/index.test.ts index ce42d3ed..c2f5f9ac 100644 --- a/typescript/packages/http/axios/src/index.test.ts +++ b/typescript/packages/http/axios/src/index.test.ts @@ -8,7 +8,11 @@ import { import { beforeEach, describe, expect, it, vi } from "vitest"; import { wrapAxiosWithPayment, wrapAxiosWithPaymentFromConfig } from "./index"; import type { x402Client, x402ClientConfig } from "@bankofai/x402-core/client"; -import type { PaymentPayload, PaymentRequired, PaymentRequirements } from "@bankofai/x402-core/types"; +import type { + PaymentPayload, + PaymentRequired, + PaymentRequirements, +} from "@bankofai/x402-core/types"; // Mock the @bankofai/x402-core/client module vi.mock("@bankofai/x402-core/client", () => { diff --git a/typescript/packages/http/express/src/index.test.ts b/typescript/packages/http/express/src/index.test.ts index 61443bf9..6917294a 100644 --- a/typescript/packages/http/express/src/index.test.ts +++ b/typescript/packages/http/express/src/index.test.ts @@ -10,7 +10,11 @@ import { x402ResourceServer, x402HTTPResourceServer as HTTPResourceServer, } from "@bankofai/x402-core/server"; -import type { PaymentPayload, PaymentRequirements, SchemeNetworkServer } from "@bankofai/x402-core/types"; +import type { + PaymentPayload, + PaymentRequirements, + SchemeNetworkServer, +} from "@bankofai/x402-core/types"; import { paymentMiddleware, paymentMiddlewareFromConfig, type SchemeRegistration } from "./index"; // --- Test Fixtures --- diff --git a/typescript/packages/http/fetch/src/index.test.ts b/typescript/packages/http/fetch/src/index.test.ts index 0b09bba6..538cb9b5 100644 --- a/typescript/packages/http/fetch/src/index.test.ts +++ b/typescript/packages/http/fetch/src/index.test.ts @@ -1,7 +1,11 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { wrapFetchWithPayment, wrapFetchWithPaymentFromConfig } from "./index"; import type { x402Client, x402HTTPClient, x402ClientConfig } from "@bankofai/x402-core/client"; -import type { PaymentPayload, PaymentRequired, PaymentRequirements } from "@bankofai/x402-core/types"; +import type { + PaymentPayload, + PaymentRequired, + PaymentRequirements, +} from "@bankofai/x402-core/types"; // Mock the @bankofai/x402-core/client module vi.mock("@bankofai/x402-core/client", () => { diff --git a/typescript/packages/http/hono/src/index.test.ts b/typescript/packages/http/hono/src/index.test.ts index 89c1b12a..426475d8 100644 --- a/typescript/packages/http/hono/src/index.test.ts +++ b/typescript/packages/http/hono/src/index.test.ts @@ -10,7 +10,11 @@ import { x402ResourceServer, x402HTTPResourceServer as HTTPResourceServer, } from "@bankofai/x402-core/server"; -import type { PaymentPayload, PaymentRequirements, SchemeNetworkServer } from "@bankofai/x402-core/types"; +import type { + PaymentPayload, + PaymentRequirements, + SchemeNetworkServer, +} from "@bankofai/x402-core/types"; import { paymentMiddleware, paymentMiddlewareFromConfig, type SchemeRegistration } from "./index"; // --- Test Fixtures --- diff --git a/typescript/packages/http/next/src/index.test.ts b/typescript/packages/http/next/src/index.test.ts index a1d4d99e..ba47eb9e 100644 --- a/typescript/packages/http/next/src/index.test.ts +++ b/typescript/packages/http/next/src/index.test.ts @@ -7,7 +7,11 @@ import type { FacilitatorClient, } from "@bankofai/x402-core/server"; import { x402ResourceServer, x402HTTPResourceServer } from "@bankofai/x402-core/server"; -import type { PaymentPayload, PaymentRequirements, SchemeNetworkServer } from "@bankofai/x402-core/types"; +import type { + PaymentPayload, + PaymentRequirements, + SchemeNetworkServer, +} from "@bankofai/x402-core/types"; import { paymentProxy, paymentProxyFromConfig, withX402, type SchemeRegistration } from "./index"; import { createHttpServer } from "./utils"; diff --git a/typescript/packages/legacy/x402-axios/src/index.test.ts b/typescript/packages/legacy/x402-axios/src/index.test.ts index 099b8276..53ef56dd 100644 --- a/typescript/packages/legacy/x402-axios/src/index.test.ts +++ b/typescript/packages/legacy/x402-axios/src/index.test.ts @@ -6,7 +6,13 @@ import { InternalAxiosRequestConfig, } from "axios"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { evm, PaymentRequirements, ChainIdToNetwork, Signer, MultiNetworkSigner } from "@bankofai/x402-legacy/types"; +import { + evm, + PaymentRequirements, + ChainIdToNetwork, + Signer, + MultiNetworkSigner, +} from "@bankofai/x402-legacy/types"; import { withPaymentInterceptor } from "./index"; // Mock the createPaymentHeader function @@ -111,7 +117,9 @@ describe("withPaymentInterceptor()", () => { const paymentHeader = "payment-header-value"; const successResponse = { data: "success" } as AxiosResponse; - const { createPaymentHeader, selectPaymentRequirements } = await import("@bankofai/x402-legacy/client"); + const { createPaymentHeader, selectPaymentRequirements } = await import( + "@bankofai/x402-legacy/client" + ); (createPaymentHeader as ReturnType).mockResolvedValue(paymentHeader); (selectPaymentRequirements as ReturnType).mockImplementation( (requirements, _) => requirements[0], @@ -179,7 +187,9 @@ describe("withPaymentInterceptor()", () => { const paymentHeader = "payment-header-value"; const successResponse = { data: "success" } as AxiosResponse; - const { createPaymentHeader, selectPaymentRequirements } = await import("@bankofai/x402-legacy/client"); + const { createPaymentHeader, selectPaymentRequirements } = await import( + "@bankofai/x402-legacy/client" + ); (createPaymentHeader as ReturnType).mockResolvedValue(paymentHeader); (selectPaymentRequirements as ReturnType).mockImplementation( (requirements, _) => requirements[0], diff --git a/typescript/packages/legacy/x402-fetch/src/index.test.ts b/typescript/packages/legacy/x402-fetch/src/index.test.ts index 01d0898a..3e23797e 100644 --- a/typescript/packages/legacy/x402-fetch/src/index.test.ts +++ b/typescript/packages/legacy/x402-fetch/src/index.test.ts @@ -68,7 +68,9 @@ describe("fetchWithPayment()", () => { const paymentHeader = "payment-header-value"; const successResponse = createResponse(200, { data: "success" }); - const { createPaymentHeader, selectPaymentRequirements } = await import("@bankofai/x402-legacy/client"); + const { createPaymentHeader, selectPaymentRequirements } = await import( + "@bankofai/x402-legacy/client" + ); (createPaymentHeader as ReturnType).mockResolvedValue(paymentHeader); (selectPaymentRequirements as ReturnType).mockImplementation( (requirements, _) => requirements[0], diff --git a/typescript/packages/mcp/src/index.ts b/typescript/packages/mcp/src/index.ts index 5057bd8a..20eb2683 100644 --- a/typescript/packages/mcp/src/index.ts +++ b/typescript/packages/mcp/src/index.ts @@ -81,7 +81,11 @@ export { // reducing the number of separate package imports required. export { x402Client } from "@bankofai/x402-core/client"; -export type { x402ClientConfig, SelectPaymentRequirements, PaymentPolicy } from "@bankofai/x402-core/client"; +export type { + x402ClientConfig, + SelectPaymentRequirements, + PaymentPolicy, +} from "@bankofai/x402-core/client"; export { x402ResourceServer } from "@bankofai/x402-core/server"; diff --git a/typescript/packages/mechanisms/stellar/src/exact/client/scheme.ts b/typescript/packages/mechanisms/stellar/src/exact/client/scheme.ts index a9af0bc2..1a4af567 100644 --- a/typescript/packages/mechanisms/stellar/src/exact/client/scheme.ts +++ b/typescript/packages/mechanisms/stellar/src/exact/client/scheme.ts @@ -12,7 +12,11 @@ import { validateStellarDestinationAddress, } from "../../utils"; import type { ClientStellarSigner } from "../../signer"; -import type { PaymentPayload, PaymentRequirements, SchemeNetworkClient } from "@bankofai/x402-core/types"; +import type { + PaymentPayload, + PaymentRequirements, + SchemeNetworkClient, +} from "@bankofai/x402-core/types"; /** Base fee in stroops (0.001 XLM) used when building the final tx fee after auth signing. */ const DEFAULT_BASE_FEE_STROOPS = 10_000; diff --git a/typescript/packages/mechanisms/svm/src/exact/client/scheme.ts b/typescript/packages/mechanisms/svm/src/exact/client/scheme.ts index 040e42aa..72459d92 100644 --- a/typescript/packages/mechanisms/svm/src/exact/client/scheme.ts +++ b/typescript/packages/mechanisms/svm/src/exact/client/scheme.ts @@ -20,7 +20,11 @@ import { setTransactionMessageLifetimeUsingBlockhash, type Address, } from "@solana/kit"; -import type { PaymentPayload, PaymentRequirements, SchemeNetworkClient } from "@bankofai/x402-core/types"; +import type { + PaymentPayload, + PaymentRequirements, + SchemeNetworkClient, +} from "@bankofai/x402-core/types"; import { DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS, From 187aab11d70b5bccc69e0a3c7e162a9c5e2db52d Mon Sep 17 00:00:00 2001 From: Hades Date: Tue, 10 Mar 2026 13:02:15 +0800 Subject: [PATCH 10/10] refactor: keep AssetInfo fields in natural order with eslint-disable --- typescript/packages/core/src/registry/assetRegistry.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/typescript/packages/core/src/registry/assetRegistry.ts b/typescript/packages/core/src/registry/assetRegistry.ts index a69774ee..14fae9ae 100644 --- a/typescript/packages/core/src/registry/assetRegistry.ts +++ b/typescript/packages/core/src/registry/assetRegistry.ts @@ -4,8 +4,6 @@ import { Network } from "../types/index.js"; * Information about a token asset on a specific network. */ export interface AssetInfo { - /** Additional metadata */ - [key: string]: unknown; /** Token contract address */ address: string; /** Number of decimal places */ @@ -18,6 +16,9 @@ export interface AssetInfo { assetTransferMethod?: string; /** Whether the token supports EIP-2612 permit */ supportsEip2612?: boolean; + /** Additional metadata */ + // eslint-disable-next-line @typescript-eslint/member-ordering + [key: string]: unknown; } /**