wayfinder-paths 0.1.29__py3-none-any.whl → 0.1.30__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wayfinder-paths might be problematic. Click here for more details.

@@ -10,9 +10,15 @@ from eth_utils import function_signature_to_4byte_selector, to_checksum_address
10
10
  from loguru import logger
11
11
 
12
12
  from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
13
- from wayfinder_paths.core.constants.contracts import BOROS_MARKET_HUB, BOROS_ROUTER
13
+ from wayfinder_paths.core.constants.chains import CHAIN_ID_HYPEREVM
14
+ from wayfinder_paths.core.constants.contracts import (
15
+ BOROS_MARKET_HUB,
16
+ BOROS_ROUTER,
17
+ HYPE_OFT_ADDRESS,
18
+ )
19
+ from wayfinder_paths.core.constants.hype_oft_abi import HYPE_OFT_ABI
14
20
  from wayfinder_paths.core.utils.tokens import build_approve_transaction
15
- from wayfinder_paths.core.utils.transaction import send_transaction
21
+ from wayfinder_paths.core.utils.transaction import encode_call, send_transaction
16
22
  from wayfinder_paths.core.utils.web3 import web3_from_chain_id
17
23
 
18
24
  from .client import BorosClient
@@ -114,6 +120,11 @@ class BorosAdapter(BaseAdapter):
114
120
  }
115
121
  ]
116
122
 
123
+ @staticmethod
124
+ def _pad_address_bytes32(address: str) -> bytes:
125
+ checksum = to_checksum_address(address)
126
+ return bytes.fromhex(checksum[2:]).rjust(32, b"\x00")
127
+
117
128
  async def get_cash_fee_data(self, *, token_id: int) -> tuple[bool, dict[str, Any]]:
118
129
  """Read MarketHub.getCashFeeData(tokenId) from chain.
119
130
 
@@ -168,7 +179,6 @@ class BorosAdapter(BaseAdapter):
168
179
  *,
169
180
  token_id: int,
170
181
  market_id: int | None = None,
171
- token_decimals: int = 18,
172
182
  ) -> tuple[bool, dict[str, Any]]:
173
183
  """Sweep isolated cash -> cross cash for a given token (optionally per-market).
174
184
 
@@ -176,22 +186,17 @@ class BorosAdapter(BaseAdapter):
176
186
  this helper moves that isolated cash back to cross margin using cash_transfer.
177
187
 
178
188
  Notes:
189
+ - Uses `get_account_balances()` (collaterals endpoint) to find isolated cash.
179
190
  - cash_transfer uses 1e18 internal cash units, not token native decimals.
180
191
  - This does NOT touch isolated positions for other markets unless market_id is None.
181
192
  """
182
193
  if self.simulation:
183
194
  return True, {"status": "simulated"}
184
195
 
185
- ok_state, state = await self.get_full_user_state(
186
- token_id=int(token_id),
187
- token_decimals=int(token_decimals),
188
- include_open_orders=False,
189
- include_withdrawal_status=False,
190
- )
191
- if not ok_state or not isinstance(state, dict):
192
- return False, {"error": f"Failed to read Boros state: {state}"}
196
+ ok_bal, balances = await self.get_account_balances(token_id=int(token_id))
197
+ if not ok_bal or not isinstance(balances, dict):
198
+ return False, {"error": f"Failed to read Boros balances: {balances}"}
193
199
 
194
- balances = state.get("balances") or {}
195
200
  isolated_positions = balances.get("isolated_positions") or []
196
201
  if not isinstance(isolated_positions, list):
197
202
  isolated_positions = []
@@ -1304,6 +1309,302 @@ class BorosAdapter(BaseAdapter):
1304
1309
  logger.error(f"Failed to cash transfer: {e}")
1305
1310
  return False, {"error": str(e)}
1306
1311
 
1312
+ async def bridge_hype_oft_hyperevm_to_arbitrum(
1313
+ self,
1314
+ *,
1315
+ amount_wei: int,
1316
+ max_value_wei: int | None = None,
1317
+ to_address: str | None = None,
1318
+ from_address: str | None = None,
1319
+ dst_eid: int = 30110,
1320
+ min_amount_wei: int = 0,
1321
+ ) -> tuple[bool, dict[str, Any]]:
1322
+ """Bridge native HYPE from HyperEVM -> Arbitrum via LayerZero OFT.
1323
+
1324
+ Notes:
1325
+ - Uses HyperEVM chain id (999) for the transaction.
1326
+ - `amount_wei` is in 1e18 (native HYPE).
1327
+ - The OFT bridge requires `msg.value = amount + nativeFee`.
1328
+ - Amount must be rounded down to `decimalConversionRate()`.
1329
+ - If `max_value_wei` is provided, clamps amount so that (amount + fee) <= max_value_wei.
1330
+ """
1331
+ if amount_wei <= 0:
1332
+ return True, {"status": "no_op", "amount_wei": 0}
1333
+
1334
+ if self.simulation:
1335
+ return True, {
1336
+ "status": "simulated",
1337
+ "amount_wei": int(amount_wei),
1338
+ "dst_eid": int(dst_eid),
1339
+ "to": to_address or self.user_address,
1340
+ }
1341
+
1342
+ if not self.sign_callback:
1343
+ return False, {"error": "sign_callback not configured"}
1344
+
1345
+ sender = from_address or self.user_address
1346
+ recipient = to_address or self.user_address
1347
+ if not sender or not recipient:
1348
+ return False, {"error": "from_address/to_address not configured"}
1349
+
1350
+ try:
1351
+ async with web3_from_chain_id(CHAIN_ID_HYPEREVM) as w3:
1352
+ contract = w3.eth.contract(
1353
+ address=w3.to_checksum_address(HYPE_OFT_ADDRESS),
1354
+ abi=HYPE_OFT_ABI,
1355
+ )
1356
+
1357
+ conversion_rate = int(
1358
+ await contract.functions.decimalConversionRate().call()
1359
+ )
1360
+ if conversion_rate > 0:
1361
+ amount_wei = (int(amount_wei) // conversion_rate) * conversion_rate
1362
+ else:
1363
+ amount_wei = int(amount_wei)
1364
+
1365
+ if amount_wei <= 0:
1366
+ return True, {"status": "no_op", "amount_wei": 0}
1367
+
1368
+ to_bytes32 = self._pad_address_bytes32(recipient)
1369
+
1370
+ def _send_params(amount_ld: int) -> tuple[Any, ...]:
1371
+ return (
1372
+ int(dst_eid),
1373
+ to_bytes32,
1374
+ int(amount_ld),
1375
+ int(min_amount_wei),
1376
+ b"",
1377
+ b"",
1378
+ b"",
1379
+ )
1380
+
1381
+ send_params = _send_params(int(amount_wei))
1382
+ fee = await contract.functions.quoteSend(send_params, False).call()
1383
+ native_fee_wei = int(fee[0])
1384
+ lz_token_fee_wei = int(fee[1])
1385
+
1386
+ if max_value_wei is not None:
1387
+ max_send_amount_wei = max(0, int(max_value_wei) - native_fee_wei)
1388
+ if conversion_rate > 0:
1389
+ max_send_amount_wei = (
1390
+ max_send_amount_wei // conversion_rate
1391
+ ) * conversion_rate
1392
+ if amount_wei > max_send_amount_wei:
1393
+ amount_wei = int(max_send_amount_wei)
1394
+ if amount_wei <= 0:
1395
+ return False, {
1396
+ "error": "Insufficient balance to cover OFT fee",
1397
+ "native_fee_wei": native_fee_wei,
1398
+ "max_value_wei": int(max_value_wei),
1399
+ }
1400
+ send_params = _send_params(int(amount_wei))
1401
+ fee = await contract.functions.quoteSend(
1402
+ send_params, False
1403
+ ).call()
1404
+ native_fee_wei = int(fee[0])
1405
+ lz_token_fee_wei = int(fee[1])
1406
+
1407
+ total_value_wei = int(amount_wei) + int(native_fee_wei)
1408
+ if max_value_wei is not None and total_value_wei > int(max_value_wei):
1409
+ return False, {
1410
+ "error": "Insufficient balance after fee quote",
1411
+ "amount_wei": int(amount_wei),
1412
+ "native_fee_wei": int(native_fee_wei),
1413
+ "total_value_wei": int(total_value_wei),
1414
+ "max_value_wei": int(max_value_wei),
1415
+ }
1416
+
1417
+ tx = await encode_call(
1418
+ target=HYPE_OFT_ADDRESS,
1419
+ abi=HYPE_OFT_ABI,
1420
+ fn_name="send",
1421
+ args=[
1422
+ send_params,
1423
+ (int(native_fee_wei), int(lz_token_fee_wei)),
1424
+ to_checksum_address(sender),
1425
+ ],
1426
+ from_address=sender,
1427
+ chain_id=CHAIN_ID_HYPEREVM,
1428
+ value=int(total_value_wei),
1429
+ )
1430
+ tx_hash = await send_transaction(
1431
+ tx, self.sign_callback, wait_for_receipt=True
1432
+ )
1433
+ return True, {
1434
+ "status": "ok",
1435
+ "tx_hash": tx_hash,
1436
+ "amount_wei": int(amount_wei),
1437
+ "native_fee_wei": int(native_fee_wei),
1438
+ "lz_token_fee_wei": int(lz_token_fee_wei),
1439
+ "total_value_wei": int(total_value_wei),
1440
+ "dst_eid": int(dst_eid),
1441
+ "to": recipient,
1442
+ "from": sender,
1443
+ "layerzeroscan": f"https://layerzeroscan.com/tx/{tx_hash}",
1444
+ }
1445
+ except Exception as exc: # noqa: BLE001
1446
+ return False, {"error": str(exc)}
1447
+
1448
+ async def close_positions_except(
1449
+ self,
1450
+ *,
1451
+ keep_market_id: int,
1452
+ token_id: int = 3,
1453
+ market_ids: list[int] | None = None,
1454
+ best_effort: bool = True,
1455
+ ) -> tuple[bool, dict[str, Any]]:
1456
+ """Close all Boros positions except `keep_market_id` (best-effort by default)."""
1457
+ if self.simulation:
1458
+ return True, {
1459
+ "status": "simulated",
1460
+ "keep_market_id": int(keep_market_id),
1461
+ "markets": market_ids or [],
1462
+ }
1463
+
1464
+ if market_ids is None:
1465
+ ok_pos, positions = await self.get_active_positions()
1466
+ if not ok_pos:
1467
+ return False, {"error": f"Failed to get positions: {positions}"}
1468
+ market_ids = sorted(
1469
+ {
1470
+ int(p.get("marketId"))
1471
+ for p in positions
1472
+ if p.get("marketId") is not None
1473
+ }
1474
+ )
1475
+
1476
+ results: list[dict[str, Any]] = []
1477
+ failures: list[dict[str, Any]] = []
1478
+ for mid in market_ids:
1479
+ if int(mid) == int(keep_market_id):
1480
+ continue
1481
+ ok_close, res_close = await self.close_positions_market(
1482
+ int(mid), token_id=int(token_id)
1483
+ )
1484
+ entry = {"market_id": int(mid), "ok": bool(ok_close), "res": res_close}
1485
+ results.append(entry)
1486
+ if not ok_close:
1487
+ failures.append(entry)
1488
+ if not best_effort:
1489
+ return False, {
1490
+ "error": f"Failed to close position for market {mid}",
1491
+ "results": results,
1492
+ }
1493
+
1494
+ return True, {"status": "ok", "results": results, "failures": failures}
1495
+
1496
+ async def ensure_position_size_yu(
1497
+ self,
1498
+ *,
1499
+ market_id: int,
1500
+ token_id: int,
1501
+ target_size_yu: float,
1502
+ tif: str = "IOC",
1503
+ min_resize_excess_usd: float | None = None,
1504
+ yu_to_usd: float | None = None,
1505
+ ) -> tuple[bool, dict[str, Any]]:
1506
+ """Ensure the Boros position size (YU) for a market.
1507
+
1508
+ - Uses `place_rate_order(... side="short")` to increase the position.
1509
+ - Uses `close_positions_market(..., size_yu_wei=...)` to decrease it.
1510
+ - If `min_resize_excess_usd` is provided, uses `yu_to_usd` to skip small resizes.
1511
+ """
1512
+ if self.simulation:
1513
+ return True, {
1514
+ "status": "simulated",
1515
+ "market_id": int(market_id),
1516
+ "token_id": int(token_id),
1517
+ "target_size_yu": float(target_size_yu),
1518
+ }
1519
+
1520
+ ok_pos, positions = await self.get_active_positions(market_id=int(market_id))
1521
+ if not ok_pos:
1522
+ return False, {"error": f"Failed to get positions: {positions}"}
1523
+
1524
+ if positions:
1525
+ current_size_yu = abs(float(positions[0].get("size", 0) or 0.0))
1526
+ else:
1527
+ current_size_yu = 0.0
1528
+
1529
+ diff_yu = float(target_size_yu) - float(current_size_yu)
1530
+ diff_abs_yu = abs(diff_yu)
1531
+
1532
+ if min_resize_excess_usd is not None:
1533
+ if yu_to_usd is None or yu_to_usd <= 0:
1534
+ return False, {
1535
+ "error": "yu_to_usd required when min_resize_excess_usd is set",
1536
+ "yu_to_usd": yu_to_usd,
1537
+ }
1538
+ diff_usd_equiv = diff_abs_yu * float(yu_to_usd)
1539
+ if diff_usd_equiv < float(min_resize_excess_usd):
1540
+ return True, {
1541
+ "status": "ok",
1542
+ "action": "no_op",
1543
+ "market_id": int(market_id),
1544
+ "token_id": int(token_id),
1545
+ "current_size_yu": float(current_size_yu),
1546
+ "target_size_yu": float(target_size_yu),
1547
+ "diff_yu": float(diff_yu),
1548
+ }
1549
+
1550
+ if diff_abs_yu < 1e-9:
1551
+ return True, {
1552
+ "status": "ok",
1553
+ "action": "no_op",
1554
+ "market_id": int(market_id),
1555
+ "token_id": int(token_id),
1556
+ "current_size_yu": float(current_size_yu),
1557
+ "target_size_yu": float(target_size_yu),
1558
+ "diff_yu": float(diff_yu),
1559
+ }
1560
+
1561
+ size_yu_wei = int(diff_abs_yu * 1e18)
1562
+
1563
+ if diff_yu > 0:
1564
+ ok_open, res_open = await self.place_rate_order(
1565
+ market_id=int(market_id),
1566
+ token_id=int(token_id),
1567
+ size_yu_wei=int(size_yu_wei),
1568
+ side="short",
1569
+ tif=tif,
1570
+ )
1571
+ if not ok_open:
1572
+ return False, {
1573
+ "error": f"Failed to open/increase position: {res_open}",
1574
+ "market_id": int(market_id),
1575
+ }
1576
+ return True, {
1577
+ "status": "ok",
1578
+ "action": "increase_short",
1579
+ "market_id": int(market_id),
1580
+ "token_id": int(token_id),
1581
+ "current_size_yu": float(current_size_yu),
1582
+ "target_size_yu": float(target_size_yu),
1583
+ "diff_yu": float(diff_yu),
1584
+ "tx": res_open,
1585
+ }
1586
+
1587
+ ok_close, res_close = await self.close_positions_market(
1588
+ market_id=int(market_id),
1589
+ token_id=int(token_id),
1590
+ size_yu_wei=int(size_yu_wei),
1591
+ )
1592
+ if not ok_close:
1593
+ return False, {
1594
+ "error": f"Failed to close/decrease position: {res_close}",
1595
+ "market_id": int(market_id),
1596
+ }
1597
+ return True, {
1598
+ "status": "ok",
1599
+ "action": "decrease",
1600
+ "market_id": int(market_id),
1601
+ "token_id": int(token_id),
1602
+ "current_size_yu": float(current_size_yu),
1603
+ "target_size_yu": float(target_size_yu),
1604
+ "diff_yu": float(diff_yu),
1605
+ "tx": res_close,
1606
+ }
1607
+
1307
1608
  async def place_rate_order(
1308
1609
  self,
1309
1610
  *,
@@ -1,6 +1,7 @@
1
1
  """Tests for BorosAdapter."""
2
2
 
3
- from unittest.mock import AsyncMock, patch
3
+ from types import SimpleNamespace
4
+ from unittest.mock import AsyncMock, MagicMock, patch
4
5
 
5
6
  import pytest
6
7
  from eth_abi import encode as abi_encode
@@ -384,16 +385,14 @@ class TestBorosAdapter:
384
385
  async def test_sweep_isolated_to_cross_filters_by_market(self, adapter):
385
386
  """Test isolated -> cross sweep only affects the specified market."""
386
387
  adapter.simulation = False
387
- adapter.get_full_user_state = AsyncMock(
388
+ adapter.get_account_balances = AsyncMock(
388
389
  return_value=(
389
390
  True,
390
391
  {
391
- "balances": {
392
- "isolated_positions": [
393
- {"market_id": 18, "balance_wei": 111},
394
- {"market_id": 19, "balance_wei": 222},
395
- ]
396
- }
392
+ "isolated_positions": [
393
+ {"market_id": 18, "balance_wei": 111},
394
+ {"market_id": 19, "balance_wei": 222},
395
+ ]
397
396
  },
398
397
  )
399
398
  )
@@ -414,15 +413,13 @@ class TestBorosAdapter:
414
413
  async def test_sweep_isolated_to_cross_errors_on_failed_transfer(self, adapter):
415
414
  """Test sweep fails fast when an isolated->cross transfer fails."""
416
415
  adapter.simulation = False
417
- adapter.get_full_user_state = AsyncMock(
416
+ adapter.get_account_balances = AsyncMock(
418
417
  return_value=(
419
418
  True,
420
419
  {
421
- "balances": {
422
- "isolated_positions": [
423
- {"market_id": 18, "balance_wei": 111},
424
- ]
425
- }
420
+ "isolated_positions": [
421
+ {"market_id": 18, "balance_wei": 111},
422
+ ]
426
423
  },
427
424
  )
428
425
  )
@@ -487,6 +484,120 @@ class TestBorosAdapter:
487
484
 
488
485
  mock_sweep.assert_awaited_once_with(token_id=3, market_id=18)
489
486
 
487
+ @pytest.mark.asyncio
488
+ async def test_close_positions_except_skips_keep_market(self, adapter):
489
+ adapter.simulation = False
490
+ adapter.close_positions_market = AsyncMock(
491
+ return_value=(True, {"status": "ok"})
492
+ )
493
+
494
+ ok, res = await adapter.close_positions_except(
495
+ keep_market_id=19, token_id=3, market_ids=[18, 19, 20]
496
+ )
497
+ assert ok is True
498
+ assert res["status"] == "ok"
499
+
500
+ calls = adapter.close_positions_market.await_args_list
501
+ assert len(calls) == 2
502
+ assert calls[0].args[0] == 18
503
+ assert calls[1].args[0] == 20
504
+
505
+ @pytest.mark.asyncio
506
+ async def test_ensure_position_size_yu_increases_short(self, adapter):
507
+ adapter.simulation = False
508
+ adapter.get_active_positions = AsyncMock(return_value=(True, []))
509
+ adapter.place_rate_order = AsyncMock(return_value=(True, {"tx_hash": "0xopen"}))
510
+
511
+ ok, res = await adapter.ensure_position_size_yu(
512
+ market_id=18, token_id=3, target_size_yu=1.5
513
+ )
514
+ assert ok is True
515
+ assert res["action"] == "increase_short"
516
+ adapter.place_rate_order.assert_awaited_once()
517
+
518
+ _, kwargs = adapter.place_rate_order.await_args
519
+ assert kwargs["market_id"] == 18
520
+ assert kwargs["token_id"] == 3
521
+ assert kwargs["side"] == "short"
522
+ assert kwargs["tif"] == "IOC"
523
+ assert kwargs["size_yu_wei"] == int(1.5 * 1e18)
524
+
525
+ @pytest.mark.asyncio
526
+ async def test_ensure_position_size_yu_decreases(self, adapter):
527
+ adapter.simulation = False
528
+ adapter.get_active_positions = AsyncMock(
529
+ return_value=(True, [{"size": 2.0, "sizeWei": int(2e18)}])
530
+ )
531
+ adapter.close_positions_market = AsyncMock(
532
+ return_value=(True, {"tx_hash": "0xclose"})
533
+ )
534
+
535
+ ok, res = await adapter.ensure_position_size_yu(
536
+ market_id=18, token_id=3, target_size_yu=1.0
537
+ )
538
+ assert ok is True
539
+ assert res["action"] == "decrease"
540
+ adapter.close_positions_market.assert_awaited_once_with(
541
+ market_id=18, token_id=3, size_yu_wei=int(1.0 * 1e18)
542
+ )
543
+
544
+ @pytest.mark.asyncio
545
+ async def test_bridge_hype_oft_rounds_amount_and_builds_tx(self, adapter):
546
+ adapter.simulation = False
547
+ adapter.sign_callback = object()
548
+
549
+ mock_contract = SimpleNamespace()
550
+ mock_dec_fn = SimpleNamespace(call=AsyncMock(return_value=10))
551
+ mock_quote_fn = SimpleNamespace(call=AsyncMock(return_value=(5, 0)))
552
+ mock_functions = SimpleNamespace(
553
+ decimalConversionRate=MagicMock(return_value=mock_dec_fn),
554
+ quoteSend=MagicMock(return_value=mock_quote_fn),
555
+ )
556
+ mock_contract.functions = mock_functions
557
+
558
+ mock_web3 = SimpleNamespace(
559
+ eth=SimpleNamespace(contract=MagicMock(return_value=mock_contract)),
560
+ to_checksum_address=lambda x: x,
561
+ )
562
+ mock_cm = AsyncMock()
563
+ mock_cm.__aenter__.return_value = mock_web3
564
+ mock_cm.__aexit__.return_value = False
565
+
566
+ with (
567
+ patch(
568
+ "wayfinder_paths.adapters.boros_adapter.adapter.web3_from_chain_id",
569
+ return_value=mock_cm,
570
+ ),
571
+ patch(
572
+ "wayfinder_paths.adapters.boros_adapter.adapter.encode_call",
573
+ new=AsyncMock(return_value={"chainId": 999}),
574
+ ) as mock_encode,
575
+ patch(
576
+ "wayfinder_paths.adapters.boros_adapter.adapter.send_transaction",
577
+ new=AsyncMock(return_value="0xtx"),
578
+ ),
579
+ ):
580
+ ok, res = await adapter.bridge_hype_oft_hyperevm_to_arbitrum(
581
+ amount_wei=123,
582
+ max_value_wei=1000,
583
+ to_address=adapter.user_address,
584
+ from_address=adapter.user_address,
585
+ )
586
+
587
+ assert ok is True
588
+ assert res["status"] == "ok"
589
+ assert res["tx_hash"] == "0xtx"
590
+ assert res["amount_wei"] == 120 # rounded down by conversion rate 10
591
+ assert res["native_fee_wei"] == 5
592
+ assert res["total_value_wei"] == 125
593
+
594
+ _, kwargs = mock_encode.await_args
595
+ assert kwargs["fn_name"] == "send"
596
+ assert kwargs["value"] == 125
597
+ send_params = kwargs["args"][0]
598
+ assert send_params[0] == 30110
599
+ assert send_params[2] == 120
600
+
490
601
  @pytest.mark.asyncio
491
602
  async def test_withdraw_collateral_simulation(self, adapter):
492
603
  """Test withdraw in simulation mode."""
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import time
5
+ from collections.abc import Awaitable, Callable
5
6
  from decimal import ROUND_DOWN, Decimal, getcontext
6
7
  from typing import Any
7
8
 
@@ -22,7 +23,6 @@ from wayfinder_paths.core.constants.hyperliquid import (
22
23
  from wayfinder_paths.core.constants.hyperliquid import (
23
24
  HYPERLIQUID_BRIDGE_ADDRESS as _HYPERLIQUID_BRIDGE_ADDRESS,
24
25
  )
25
- from wayfinder_paths.core.types import HyperliquidSignCallback
26
26
 
27
27
  # Re-export Bridge2 constants for backwards compatibility.
28
28
  HYPERLIQUID_BRIDGE_ADDRESS = _HYPERLIQUID_BRIDGE_ADDRESS
@@ -68,7 +68,7 @@ class HyperliquidAdapter(BaseAdapter):
68
68
  config: dict[str, Any] | None = None,
69
69
  *,
70
70
  simulation: bool = False,
71
- sign_callback: HyperliquidSignCallback | None = None,
71
+ sign_callback: Callable[[dict], Awaitable[str]] | None = None,
72
72
  ) -> None:
73
73
  super().__init__("hyperliquid_adapter", config)
74
74
 
@@ -1,3 +1,4 @@
1
+ from collections.abc import Awaitable, Callable
1
2
  from decimal import Decimal
2
3
  from typing import Any, Literal
3
4
 
@@ -24,7 +25,6 @@ from loguru import logger
24
25
  from web3 import Web3
25
26
 
26
27
  from wayfinder_paths.adapters.hyperliquid_adapter.util import Util
27
- from wayfinder_paths.core.types import HyperliquidSignCallback
28
28
 
29
29
  ARBITRUM_CHAIN_ID = "0xa4b1"
30
30
  MAINNET = "Mainnet"
@@ -39,7 +39,7 @@ class Exchange:
39
39
  self,
40
40
  info: Info,
41
41
  util: Util,
42
- sign_callback: HyperliquidSignCallback,
42
+ sign_callback: Callable[[dict], Awaitable[str]],
43
43
  signing_type: Literal["eip712", "local"],
44
44
  ):
45
45
  self.info = info
@@ -1,9 +1,8 @@
1
+ from collections.abc import Awaitable, Callable
1
2
  from typing import Any
2
3
 
3
4
  from eth_account import Account
4
5
 
5
- from wayfinder_paths.core.types import HyperliquidSignCallback
6
-
7
6
 
8
7
  def _resolve_private_key(config: dict[str, Any]) -> str | None:
9
8
  """Extract private key from config."""
@@ -26,23 +25,7 @@ def _resolve_private_key(config: dict[str, Any]) -> str | None:
26
25
  return None
27
26
 
28
27
 
29
- def create_local_signer(config: dict[str, Any]) -> HyperliquidSignCallback:
30
- """
31
- Create a Hyperliquid signing callback using private key from config.
32
-
33
- For local signing, the payload is a keccak hash (0x...) of the encoded EIP-712 typed data.
34
- The callback signs with the private key and returns a signature dict.
35
-
36
- Args:
37
- config: Configuration dict containing private key in strategy_wallet or main_wallet
38
-
39
- Returns:
40
- HyperliquidSignCallback that signs payloads with the local private key
41
-
42
- Raises:
43
- ValueError: If no private key found in config
44
- """
45
- # Extract private key from config
28
+ def create_local_signer(config: dict[str, Any]) -> Callable[[dict], Awaitable[str]]:
46
29
  private_key = _resolve_private_key(config)
47
30
  if not private_key:
48
31
  raise ValueError(
@@ -57,20 +40,6 @@ def create_local_signer(config: dict[str, Any]) -> HyperliquidSignCallback:
57
40
  async def sign(
58
41
  action: dict[str, Any], payload: str, address: str
59
42
  ) -> dict[str, str] | None:
60
- """
61
- Sign a Hyperliquid action with local private key.
62
-
63
- For local signing, payload is keccak hash (0x...) of encoded typed data.
64
- Sign with account and return signature dict.
65
-
66
- Args:
67
- action: The action being signed (not used for local signing)
68
- payload: Keccak hash (0x...) of encoded EIP-712 typed data
69
- address: The address signing (validation check)
70
-
71
- Returns:
72
- Signature dict {"r": "0x...", "s": "0x...", "v": 28} or None if validation fails
73
- """
74
43
  # Verify address matches account
75
44
  if address.lower() != account.address.lower():
76
45
  return None