Skip to content

Commit 2524eac

Browse files
authored
Merge pull request #55 from omnuron/fix/x402-direct-route-balance-check
Fix x402 URL route balance selection
2 parents 0b5bc36 + ab73d57 commit 2524eac

4 files changed

Lines changed: 305 additions & 51 deletions

File tree

src/omniclaw/client.py

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,19 @@ def _nanopayment_network(self) -> str:
284284
)
285285
return network
286286

287+
@staticmethod
288+
def _route_value(route: Any) -> str:
289+
return str(route.value if hasattr(route, "value") else route or "").strip().lower()
290+
291+
@classmethod
292+
def _route_uses_gateway_balance(cls, detected_route: Any, preferred_url_route: Any) -> bool:
293+
preferred_route = cls._route_value(preferred_url_route)
294+
if preferred_route in {"nanopayment", "gateway", "circle_gateway"}:
295+
return True
296+
if preferred_route == PaymentMethod.X402.value:
297+
return False
298+
return cls._route_value(detected_route) == PaymentMethod.NANOPAYMENT.value
299+
287300
def _init_nanopayments(self) -> None:
288301
"""Initialize nanopayments components (direct private key only)."""
289302
if not self._config.nanopayments_enabled:
@@ -975,9 +988,9 @@ async def pay(
975988
guards_passed: list[str] = []
976989

977990
# Detect payment route early to know which balance to check
991+
source_network: Network | None = None
978992
try:
979993
# Try to get source network from Circle wallet first, then fall back to config default.
980-
source_network = None
981994
try:
982995
wallet_info = self._wallet_service.get_wallet(wallet_id)
983996
source_network = Network.from_string(wallet_info.blockchain)
@@ -1086,16 +1099,17 @@ async def pay(
10861099
if consume_intent_id:
10871100
await self._reservation.release(consume_intent_id)
10881101

1089-
# Get appropriate balance based on payment route
1090-
# For X402/nanopayment routes → check Gateway balance
1091-
# For Transfer/crosschain routes → check Circle wallet balance
1092-
circle_balance = self._wallet_service.get_usdc_balance_amount(wallet_id)
1102+
# Get appropriate balance based on payment route.
1103+
# Gateway nanopayments check Gateway balance. Direct x402 exact is
1104+
# delegated to the x402 adapter. Transfer/crosschain routes use Circle balance.
10931105
reserved_total = await self._reservation.get_reserved_total(wallet_id)
1106+
preferred_url_route = kwargs.get("preferred_url_route")
1107+
required_amount = amount_decimal
10941108

1095-
# Check if this is a Gateway-based route
1096-
route_uses_gateway = detected_route in (PaymentMethod.X402, PaymentMethod.NANOPAYMENT)
1097-
1098-
if route_uses_gateway and self._nano_adapter:
1109+
if (
1110+
self._route_uses_gateway_balance(detected_route, preferred_url_route)
1111+
and self._nano_adapter
1112+
):
10991113
try:
11001114
gateway_balance = await self.get_gateway_balance(wallet_id)
11011115
available = Decimal(str(gateway_balance.available_decimal))
@@ -1106,20 +1120,24 @@ async def pay(
11061120
self._logger.warning(f"Gateway balance check failed: {e}")
11071121
available = Decimal("0")
11081122
balance_source = "Gateway available: (check failed)"
1123+
elif self._route_value(detected_route) == PaymentMethod.X402.value:
1124+
available = amount_decimal
1125+
balance_source = "Direct x402: deferred to x402 adapter"
11091126
else:
1127+
circle_balance = self._wallet_service.get_usdc_balance_amount(wallet_id)
11101128
available = circle_balance - reserved_total
11111129
balance_source = f"Circle: {available}"
11121130
if lock_lost_event.is_set():
11131131
raise PaymentError("Wallet lock lease was lost before execution could start.")
1114-
if amount_decimal > available:
1115-
error_msg = f"Insufficient available balance ({balance_source}, Reserved: {reserved_total}, Required: {amount_decimal})"
1132+
if required_amount > available:
1133+
error_msg = f"Insufficient available balance ({balance_source}, Reserved: {reserved_total}, Required: {required_amount})"
11161134
if guards_chain and reservation_tokens:
11171135
await guards_chain.release(reservation_tokens)
11181136
await self._ledger.update_status(
11191137
ledger_entry.id, LedgerEntryStatus.FAILED, metadata_updates={"error": error_msg}
11201138
)
11211139
raise InsufficientBalanceError(
1122-
error_msg, current_balance=available, required_amount=amount_decimal
1140+
error_msg, current_balance=available, required_amount=required_amount
11231141
)
11241142

11251143
# Resilience Shell
@@ -1359,15 +1377,18 @@ async def simulate(
13591377
amount_decimal = Decimal(str(amount))
13601378

13611379
# Detect the actual route early so early-return reasons include it
1380+
source_network: Network | None = None
13621381
try:
1382+
source_network = Network.from_string(
1383+
self._wallet_service.get_wallet(wallet_id).blockchain
1384+
)
13631385
detected_route = (
13641386
self._router.detect_method(
13651387
recipient,
1366-
source_network=Network.from_string(
1367-
self._wallet_service.get_wallet(wallet_id).blockchain
1368-
),
1388+
source_network=source_network,
13691389
destination_chain=kwargs.get("destination_chain"),
13701390
amount=amount_decimal,
1391+
preferred_url_route=kwargs.get("preferred_url_route"),
13711392
)
13721393
or PaymentMethod.TRANSFER
13731394
)
@@ -1384,15 +1405,17 @@ async def simulate(
13841405
),
13851406
)
13861407

1387-
# Get appropriate balance based on payment route
1388-
# For X402/nanopayment routes → check Gateway balance
1389-
# For Transfer/crosschain routes → check Circle wallet balance
1390-
circle_balance = self._wallet_service.get_usdc_balance_amount(wallet_id)
1408+
# Get appropriate balance based on payment route.
1409+
# Gateway nanopayments check Gateway balance. Direct x402 exact is delegated
1410+
# to the x402 adapter. Transfer/crosschain routes use Circle balance.
13911411
reserved_total = await self._reservation.get_reserved_total(wallet_id)
1412+
preferred_url_route = kwargs.get("preferred_url_route")
1413+
required_amount = amount_decimal
13921414

1393-
route_uses_gateway = detected_route in (PaymentMethod.X402, PaymentMethod.NANOPAYMENT)
1394-
1395-
if route_uses_gateway and self._nano_adapter:
1415+
if (
1416+
self._route_uses_gateway_balance(detected_route, preferred_url_route)
1417+
and self._nano_adapter
1418+
):
13961419
try:
13971420
# Direct private key mode - use ON-CHAIN query
13981421
from omniclaw.protocols.nanopayments.wallet import GatewayWalletManager
@@ -1412,15 +1435,19 @@ async def simulate(
14121435
self._logger.warning(f"Gateway balance check failed: {e}")
14131436
available = 0
14141437
balance_source = "Gateway: (check failed)"
1438+
elif self._route_value(detected_route) == PaymentMethod.X402.value:
1439+
available = amount_decimal
1440+
balance_source = "Direct x402: deferred to x402 adapter"
14151441
else:
1442+
circle_balance = self._wallet_service.get_usdc_balance_amount(wallet_id)
14161443
available = circle_balance - reserved_total
14171444
balance_source = f"Circle: {available}"
14181445

1419-
if amount_decimal > available:
1446+
if required_amount > available:
14201447
return SimulationResult(
14211448
would_succeed=False,
14221449
route=detected_route,
1423-
reason=f"Insufficient available balance ({balance_source}, Reserved: {reserved_total}, Required: {amount_decimal})",
1450+
reason=f"Insufficient available balance ({balance_source}, Reserved: {reserved_total}, Required: {required_amount})",
14241451
)
14251452

14261453
# Check guards first

src/omniclaw/payment/router.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,15 @@ def get_adapters(self) -> list[ProtocolAdapter]:
4747
return list(self._adapters)
4848

4949
@staticmethod
50-
def _matches_preferred_route(adapter: ProtocolAdapter, preferred_route: Any) -> bool:
50+
def _route_value(route: Any) -> str:
51+
return str(route.value if hasattr(route, "value") else route or "").strip()
52+
53+
@classmethod
54+
def _matches_preferred_route(cls, adapter: ProtocolAdapter, preferred_route: Any) -> bool:
5155
if not preferred_route:
5256
return True
5357
adapter_method = getattr(adapter, "method", None)
54-
adapter_value = adapter_method.value if hasattr(adapter_method, "value") else adapter_method
55-
return str(adapter_value) == str(preferred_route)
58+
return cls._route_value(adapter_method) == cls._route_value(preferred_route)
5659

5760
def detect_method(
5861
self,

0 commit comments

Comments
 (0)