wayfinder-paths 0.1.7__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.

Files changed (149) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +399 -0
  2. wayfinder_paths/__init__.py +22 -0
  3. wayfinder_paths/abis/generic/erc20.json +383 -0
  4. wayfinder_paths/adapters/__init__.py +0 -0
  5. wayfinder_paths/adapters/balance_adapter/README.md +94 -0
  6. wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
  7. wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
  8. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  9. wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
  10. wayfinder_paths/adapters/brap_adapter/README.md +249 -0
  11. wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
  12. wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
  13. wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
  15. wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
  16. wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
  18. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
  19. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
  20. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
  21. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
  23. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
  24. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
  28. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
  29. wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
  30. wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
  31. wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
  32. wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
  33. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
  34. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
  35. wayfinder_paths/adapters/pool_adapter/README.md +206 -0
  36. wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
  37. wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
  38. wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
  39. wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
  40. wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
  41. wayfinder_paths/adapters/token_adapter/README.md +101 -0
  42. wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
  43. wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
  44. wayfinder_paths/adapters/token_adapter/examples.json +26 -0
  45. wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
  46. wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
  47. wayfinder_paths/config.example.json +22 -0
  48. wayfinder_paths/conftest.py +31 -0
  49. wayfinder_paths/core/__init__.py +18 -0
  50. wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
  51. wayfinder_paths/core/adapters/__init__.py +5 -0
  52. wayfinder_paths/core/adapters/base.py +5 -0
  53. wayfinder_paths/core/adapters/models.py +46 -0
  54. wayfinder_paths/core/analytics/__init__.py +11 -0
  55. wayfinder_paths/core/analytics/bootstrap.py +57 -0
  56. wayfinder_paths/core/analytics/stats.py +48 -0
  57. wayfinder_paths/core/analytics/test_analytics.py +170 -0
  58. wayfinder_paths/core/clients/AuthClient.py +83 -0
  59. wayfinder_paths/core/clients/BRAPClient.py +109 -0
  60. wayfinder_paths/core/clients/ClientManager.py +210 -0
  61. wayfinder_paths/core/clients/HyperlendClient.py +192 -0
  62. wayfinder_paths/core/clients/LedgerClient.py +443 -0
  63. wayfinder_paths/core/clients/PoolClient.py +128 -0
  64. wayfinder_paths/core/clients/SimulationClient.py +192 -0
  65. wayfinder_paths/core/clients/TokenClient.py +89 -0
  66. wayfinder_paths/core/clients/TransactionClient.py +63 -0
  67. wayfinder_paths/core/clients/WalletClient.py +94 -0
  68. wayfinder_paths/core/clients/WayfinderClient.py +269 -0
  69. wayfinder_paths/core/clients/__init__.py +48 -0
  70. wayfinder_paths/core/clients/protocols.py +392 -0
  71. wayfinder_paths/core/clients/sdk_example.py +110 -0
  72. wayfinder_paths/core/config.py +458 -0
  73. wayfinder_paths/core/constants/__init__.py +26 -0
  74. wayfinder_paths/core/constants/base.py +42 -0
  75. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  76. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  77. wayfinder_paths/core/engine/StrategyJob.py +188 -0
  78. wayfinder_paths/core/engine/__init__.py +5 -0
  79. wayfinder_paths/core/engine/manifest.py +97 -0
  80. wayfinder_paths/core/services/__init__.py +0 -0
  81. wayfinder_paths/core/services/base.py +179 -0
  82. wayfinder_paths/core/services/local_evm_txn.py +430 -0
  83. wayfinder_paths/core/services/local_token_txn.py +231 -0
  84. wayfinder_paths/core/services/web3_service.py +45 -0
  85. wayfinder_paths/core/settings.py +61 -0
  86. wayfinder_paths/core/strategies/Strategy.py +280 -0
  87. wayfinder_paths/core/strategies/__init__.py +5 -0
  88. wayfinder_paths/core/strategies/base.py +7 -0
  89. wayfinder_paths/core/strategies/descriptors.py +81 -0
  90. wayfinder_paths/core/utils/__init__.py +1 -0
  91. wayfinder_paths/core/utils/evm_helpers.py +206 -0
  92. wayfinder_paths/core/utils/wallets.py +77 -0
  93. wayfinder_paths/core/wallets/README.md +91 -0
  94. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  95. wayfinder_paths/core/wallets/__init__.py +7 -0
  96. wayfinder_paths/policies/enso.py +17 -0
  97. wayfinder_paths/policies/erc20.py +34 -0
  98. wayfinder_paths/policies/evm.py +21 -0
  99. wayfinder_paths/policies/hyper_evm.py +19 -0
  100. wayfinder_paths/policies/hyperlend.py +12 -0
  101. wayfinder_paths/policies/hyperliquid.py +30 -0
  102. wayfinder_paths/policies/moonwell.py +54 -0
  103. wayfinder_paths/policies/prjx.py +30 -0
  104. wayfinder_paths/policies/util.py +27 -0
  105. wayfinder_paths/run_strategy.py +411 -0
  106. wayfinder_paths/scripts/__init__.py +0 -0
  107. wayfinder_paths/scripts/create_strategy.py +181 -0
  108. wayfinder_paths/scripts/make_wallets.py +169 -0
  109. wayfinder_paths/scripts/run_strategy.py +124 -0
  110. wayfinder_paths/scripts/validate_manifests.py +213 -0
  111. wayfinder_paths/strategies/__init__.py +0 -0
  112. wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
  113. wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
  114. wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
  115. wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
  116. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
  117. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
  118. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
  119. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
  120. wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
  121. wayfinder_paths/strategies/config.py +85 -0
  122. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
  123. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
  124. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  125. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
  126. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
  127. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
  128. wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
  129. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  130. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
  131. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
  132. wayfinder_paths/templates/adapter/README.md +105 -0
  133. wayfinder_paths/templates/adapter/adapter.py +26 -0
  134. wayfinder_paths/templates/adapter/examples.json +8 -0
  135. wayfinder_paths/templates/adapter/manifest.yaml +6 -0
  136. wayfinder_paths/templates/adapter/test_adapter.py +49 -0
  137. wayfinder_paths/templates/strategy/README.md +153 -0
  138. wayfinder_paths/templates/strategy/examples.json +11 -0
  139. wayfinder_paths/templates/strategy/manifest.yaml +8 -0
  140. wayfinder_paths/templates/strategy/strategy.py +57 -0
  141. wayfinder_paths/templates/strategy/test_strategy.py +197 -0
  142. wayfinder_paths/tests/__init__.py +0 -0
  143. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  144. wayfinder_paths/tests/test_test_coverage.py +212 -0
  145. wayfinder_paths/tests/test_utils.py +64 -0
  146. wayfinder_paths-0.1.7.dist-info/LICENSE +21 -0
  147. wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
  148. wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
  149. wayfinder_paths-0.1.7.dist-info/WHEEL +4 -0
@@ -0,0 +1,81 @@
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class Volatility(Enum):
8
+ LOW = "low"
9
+ MEDIUM = "medium"
10
+ HIGH = "high"
11
+
12
+
13
+ class Frequency(Enum):
14
+ LOW = "low"
15
+ MEDIUM = "medium"
16
+ HIGH = "high"
17
+
18
+
19
+ class Directionality(Enum):
20
+ LONG = "Long"
21
+ SHORT = "Short"
22
+ MARKET_NEUTRAL = "market_neutral"
23
+ DELTA_NEUTRAL = "delta_neutral"
24
+
25
+
26
+ class Complexity(Enum):
27
+ LOW = "low"
28
+ MEDIUM = "medium"
29
+ HIGH = "high"
30
+
31
+
32
+ class TokenExposure(Enum):
33
+ STABLECOINS = "Stablecoins"
34
+ MAJORS = "Majors"
35
+ ALTS = "Alts"
36
+
37
+
38
+ # Default token rewards for all strategies
39
+ DEFAULT_TOKEN_REWARDS = [
40
+ {
41
+ "symbol": "B3",
42
+ "name": "B3",
43
+ "address": "0xB3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3",
44
+ "image_url": "https://storage.googleapis.com/prod-wayfinder-app-assets/asset-icons/B3.png",
45
+ "chain_id": 8453,
46
+ },
47
+ ]
48
+
49
+
50
+ class StratDescriptor(BaseModel):
51
+ description: str
52
+
53
+ summary: str
54
+
55
+ gas_token_symbol: str
56
+ gas_token_id: str
57
+ deposit_token_id: str
58
+ minimum_net_deposit: float
59
+ gas_maximum: float
60
+ gas_threshold: float
61
+
62
+ available_rewards: dict[str, Any] = {
63
+ "token_rewards": DEFAULT_TOKEN_REWARDS,
64
+ "point_rewards": None,
65
+ }
66
+
67
+ # risk indicators
68
+ volatility: Volatility
69
+ volatility_description_short: str
70
+ directionality: Directionality
71
+ directionality_description: str
72
+ complexity: Complexity
73
+ complexity_description: str
74
+ token_exposure: TokenExposure
75
+ token_exposure_description: str
76
+ frequency: Frequency
77
+ frequency_description: str
78
+
79
+ return_drivers: list[str]
80
+
81
+ config: dict[str, Any]
@@ -0,0 +1 @@
1
+ """Utility helpers for local functionality not tied to external APIs."""
@@ -0,0 +1,206 @@
1
+ """
2
+ EVM helper utilities for common blockchain operations.
3
+
4
+ This module provides reusable functions for EVM-related operations that are shared
5
+ across multiple adapters, extracted from evm_transaction_adapter.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from loguru import logger
14
+ from web3 import AsyncHTTPProvider, AsyncWeb3
15
+
16
+ from wayfinder_paths.core.constants.base import CHAIN_CODE_TO_ID
17
+
18
+
19
+ def chain_code_to_chain_id(chain_code: str | None) -> int | None:
20
+ """
21
+ Convert chain code to chain ID.
22
+
23
+ Args:
24
+ chain_code: Chain code string (e.g., "ethereum", "base")
25
+
26
+ Returns:
27
+ Chain ID as integer, or None if not found
28
+ """
29
+ if not chain_code:
30
+ return None
31
+ return CHAIN_CODE_TO_ID.get(chain_code.lower())
32
+
33
+
34
+ def resolve_chain_id(token_info: dict[str, Any], logger_instance=None) -> int | None:
35
+ """
36
+ Extract chain ID from token_info dictionary.
37
+
38
+ Args:
39
+ token_info: Dictionary containing token information with 'chain' key
40
+ logger_instance: Optional logger instance for debug messages
41
+
42
+ Returns:
43
+ Chain ID as integer, or None if not found
44
+ """
45
+ log = logger_instance or logger
46
+ chain_meta = token_info.get("chain") or {}
47
+ chain_id = chain_meta.get("chain_id")
48
+ try:
49
+ if chain_id is not None:
50
+ return int(chain_id)
51
+ except (ValueError, TypeError):
52
+ log.debug("Invalid chain_id in token_info.chain_id: %s", chain_id)
53
+ return chain_code_to_chain_id(chain_meta.get("code"))
54
+
55
+
56
+ def resolve_rpc_url(
57
+ chain_id: int | None,
58
+ config: dict[str, Any],
59
+ explicit_rpc_url: str | None = None,
60
+ ) -> str:
61
+ """
62
+ Resolve RPC URL from config or environment variables.
63
+
64
+ Args:
65
+ chain_id: Chain ID to look up RPC URL for
66
+ config: Configuration dictionary
67
+ explicit_rpc_url: Explicitly provided RPC URL (takes precedence)
68
+
69
+ Returns:
70
+ RPC URL string
71
+
72
+ Raises:
73
+ ValueError: If RPC URL cannot be resolved
74
+ """
75
+ if explicit_rpc_url:
76
+ return explicit_rpc_url
77
+ strategy_cfg = config.get("strategy") or {}
78
+ mapping = strategy_cfg.get("rpc_urls") if isinstance(strategy_cfg, dict) else None
79
+ if chain_id is not None and isinstance(mapping, dict):
80
+ by_int = mapping.get(chain_id)
81
+ if by_int:
82
+ return str(by_int)
83
+ by_str = mapping.get(str(chain_id))
84
+ if by_str:
85
+ return str(by_str)
86
+ env_rpc = os.getenv("RPC_URL")
87
+ if env_rpc:
88
+ return env_rpc
89
+ raise ValueError("RPC URL not provided. Set strategy.rpc_urls or env RPC_URL.")
90
+
91
+
92
+ async def get_next_nonce(
93
+ from_address: str, rpc_url: str, use_latest: bool = False
94
+ ) -> int:
95
+ """
96
+ Get the next nonce for the given address.
97
+
98
+ Args:
99
+ from_address: Address to get nonce for
100
+ rpc_url: RPC URL to connect to
101
+ use_latest: If True, use 'latest' block instead of 'pending'
102
+
103
+ Returns:
104
+ Next nonce as integer
105
+ """
106
+ w3 = AsyncWeb3(AsyncHTTPProvider(rpc_url))
107
+ try:
108
+ if use_latest:
109
+ return await w3.eth.get_transaction_count(from_address, "latest")
110
+ return await w3.eth.get_transaction_count(from_address)
111
+ finally:
112
+ try:
113
+ await w3.provider.session.close()
114
+ except Exception:
115
+ pass
116
+
117
+
118
+ def resolve_private_key_for_from_address(
119
+ from_address: str, config: dict[str, Any]
120
+ ) -> str | None:
121
+ """
122
+ Resolve private key for the given address from config or environment.
123
+
124
+ Args:
125
+ from_address: Address to resolve private key for
126
+ config: Configuration dictionary containing wallet information
127
+
128
+ Returns:
129
+ Private key string, or None if not found
130
+ """
131
+ from_addr_norm = (from_address or "").lower()
132
+ main_wallet = config.get("main_wallet")
133
+ strategy_wallet = config.get("strategy_wallet")
134
+
135
+ main_pk = None
136
+ strategy_pk = None
137
+ try:
138
+ if isinstance(main_wallet, dict):
139
+ main_pk = main_wallet.get("private_key") or main_wallet.get(
140
+ "private_key_hex"
141
+ )
142
+ if isinstance(strategy_wallet, dict):
143
+ strategy_pk = strategy_wallet.get("private_key") or strategy_wallet.get(
144
+ "private_key_hex"
145
+ )
146
+ except (AttributeError, TypeError) as e:
147
+ logger.debug("Error resolving private keys from wallet config: %s", e)
148
+
149
+ main_addr = None
150
+ strategy_addr = None
151
+ try:
152
+ main_addr = (main_wallet or {}).get("address") or (
153
+ (main_wallet or {}).get("evm") or {}
154
+ ).get("address")
155
+ strategy_addr = (strategy_wallet or {}).get("address") or (
156
+ (strategy_wallet or {}).get("evm") or {}
157
+ ).get("address")
158
+ except (AttributeError, TypeError) as e:
159
+ logger.debug("Error resolving addresses from wallet config: %s", e)
160
+
161
+ if main_addr and from_addr_norm == (main_addr or "").lower():
162
+ return main_pk or os.getenv("PRIVATE_KEY")
163
+ if strategy_addr and from_addr_norm == (strategy_addr or "").lower():
164
+ return (
165
+ strategy_pk or os.getenv("PRIVATE_KEY_STRATEGY") or os.getenv("PRIVATE_KEY")
166
+ )
167
+
168
+ # Fallback to environment variables
169
+ return os.getenv("PRIVATE_KEY_STRATEGY") or os.getenv("PRIVATE_KEY")
170
+
171
+
172
+ async def _get_abi(chain_id: int, address: str) -> str | None:
173
+ os.makedirs(f"abis/{chain_id}/", exist_ok=True)
174
+
175
+ abi_file = f"abis/{chain_id}/{address}.json"
176
+ if not os.path.exists(abi_file):
177
+ raise ValueError(
178
+ f"There is no downloaded ABI for {address} on chain {chain_id} -- please download it to ({abi_file}) (make sure to get the implementation if this address is a proxy)"
179
+ )
180
+
181
+ with open(abi_file) as f:
182
+ abi = f.read()
183
+
184
+ return abi
185
+
186
+
187
+ # We filter ABIs for Privy Policy since most of the abi is useless, and we don't wanna upload big ABIs for both size and readability reasons.
188
+ async def get_abi_filtered(
189
+ chain_id: int, address: str, function_names: list[str]
190
+ ) -> list | None:
191
+ full_abi = await _get_abi(chain_id, address)
192
+ if full_abi is None:
193
+ raise Exception("Could not pull ABI, get_abi returned None")
194
+ abi_json = json.loads(full_abi)
195
+ filtered_abi = [
196
+ item
197
+ for item in abi_json
198
+ if item.get("type") == "function" and item.get("name") in function_names
199
+ ]
200
+ return filtered_abi
201
+
202
+
203
+ with open(Path(__file__).parent.parent.parent.joinpath("abis/generic/erc20.json")) as f:
204
+ erc20_abi_raw = f.read()
205
+
206
+ ERC20_ABI = json.loads(erc20_abi_raw)
@@ -0,0 +1,77 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from eth_account import Account
6
+
7
+
8
+ def make_random_wallet() -> dict[str, str]:
9
+ """Generate a new random wallet.
10
+
11
+ Returns a mapping with keys: "address" and "private_key_hex" (0x-prefixed).
12
+ """
13
+ acct = Account.create() # uses os.urandom
14
+ return {
15
+ "address": acct.address,
16
+ "private_key_hex": acct.key.hex(),
17
+ }
18
+
19
+
20
+ def _load_existing_wallets(file_path: Path) -> list[dict[str, Any]]:
21
+ if not file_path.exists():
22
+ return []
23
+ try:
24
+ parsed = json.loads(file_path.read_text())
25
+ if isinstance(parsed, list):
26
+ return parsed
27
+ if isinstance(parsed, dict):
28
+ wallets = parsed.get("wallets")
29
+ if isinstance(wallets, list):
30
+ return wallets
31
+ return []
32
+ except Exception:
33
+ # If the file is malformed, start fresh rather than raising.
34
+ return []
35
+
36
+
37
+ def _save_wallets(file_path: Path, wallets: list[dict[str, Any]]) -> None:
38
+ # Ensure stable ordering by address for readability
39
+ sorted_wallets = sorted(wallets, key=lambda w: w.get("address", ""))
40
+ file_path.write_text(json.dumps(sorted_wallets, indent=2))
41
+
42
+
43
+ def write_wallet_to_json(
44
+ wallet: dict[str, str], out_dir: str | Path = ".", filename: str = "wallets.json"
45
+ ) -> Path:
46
+ """Create or update a wallets.json with the provided wallet.
47
+
48
+ - Ensures the output directory exists.
49
+ - Merges with existing entries keyed by address (updates if present, appends otherwise).
50
+ - Writes a pretty-printed JSON list of wallet objects.
51
+ """
52
+ out_dir_path = Path(out_dir)
53
+ out_dir_path.mkdir(parents=True, exist_ok=True)
54
+ file_path = out_dir_path / filename
55
+
56
+ existing = _load_existing_wallets(file_path)
57
+ index_by_address: dict[str, int] = {}
58
+ for i, w in enumerate(existing):
59
+ addr = w.get("address")
60
+ if isinstance(addr, str):
61
+ index_by_address[addr.lower()] = i
62
+
63
+ addr_key = wallet["address"].lower()
64
+ if addr_key in index_by_address:
65
+ existing[index_by_address[addr_key]] = wallet
66
+ else:
67
+ existing.append(wallet)
68
+
69
+ _save_wallets(file_path, existing)
70
+ return file_path
71
+
72
+
73
+ def load_wallets(
74
+ out_dir: str | Path = ".", filename: str = "wallets.json"
75
+ ) -> list[dict[str, Any]]:
76
+ """Public helper to read wallets.json as a list of wallet dicts."""
77
+ return _load_existing_wallets(Path(out_dir) / filename)
@@ -0,0 +1,91 @@
1
+ # Wallet Abstraction Layer
2
+
3
+ Wayfinder strategies interact with blockchains through a single abstraction: the `EvmTxn` interface defined in `wayfinder_paths/core/services/base.py`. The default implementation (`LocalEvmTxn`) signs transactions with private keys pulled from config/environment variables, while `WalletManager` resolves which provider to use at runtime.
4
+
5
+ ## Pieces
6
+
7
+ 1. **`EvmTxn` (interface)** – describes balance lookups, ERC-20 approvals, raw transaction broadcasting, and AsyncWeb3 access.
8
+ 2. **`LocalEvmTxn`** – the built-in provider that signs transactions locally via `eth_account`. It handles RPC resolution, nonce management, gas estimation, and status checks.
9
+ 3. **`WalletManager`** – light factory that reads `wallet_type` from strategy config and returns the appropriate `EvmTxn`. Today it always returns `LocalEvmTxn` unless you inject your own provider.
10
+ 4. **`DefaultWeb3Service`** – convenience wrapper that bundles an `EvmTxn` (wallet provider) with a `LocalTokenTxnService` (transaction builders used by adapters).
11
+
12
+ ## Using the defaults
13
+
14
+ ```python
15
+ from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
16
+ from wayfinder_paths.core.wallets.WalletManager import WalletManager
17
+
18
+ config = {...} # contains main_wallet / strategy_wallet entries
19
+ wallet_provider = WalletManager.get_provider(config)
20
+ web3_service = DefaultWeb3Service(config, wallet_provider=wallet_provider)
21
+
22
+ # Strategies typically pass web3_service.evm_transactions into adapters that require wallet access.
23
+ balance_adapter = BalanceAdapter(config, web3_service=web3_service)
24
+ ```
25
+
26
+ If you want to provide a custom wallet provider (e.g., Privy, Turnkey, Fireblocks), implement the `EvmTxn` interface and hand it to `DefaultWeb3Service`/adapters directly—`WalletManager` is purely a helper.
27
+
28
+ ## Implementing a custom provider
29
+
30
+ Subclass `EvmTxn` and implement every abstract method:
31
+
32
+ ```python
33
+ from typing import Any
34
+ from web3 import AsyncWeb3
35
+
36
+ from wayfinder_paths.core.services.base import EvmTxn
37
+
38
+
39
+ class PrivyWallet(EvmTxn):
40
+ def __init__(self, privy_client):
41
+ self._client = privy_client
42
+
43
+ async def get_balance(self, address: str, token_address: str | None, chain_id: int) -> tuple[bool, Any]:
44
+ ...
45
+
46
+ async def read_erc20_allowance(self, chain_id: int, token_address: str, owner_address: str, spender_address: str) -> tuple[bool, Any]:
47
+ ...
48
+
49
+ async def approve_token(...):
50
+ ...
51
+
52
+ async def broadcast_transaction(...):
53
+ ...
54
+
55
+ async def transaction_succeeded(self, tx_hash: str, chain_id: int, timeout: int = 120) -> bool:
56
+ ...
57
+
58
+ def get_web3(self, chain_id: int) -> AsyncWeb3:
59
+ ...
60
+ ```
61
+
62
+ Methods should mirror `LocalEvmTxn`’s behavior: return `(True, payload)` on success, `(False, "reason")` on failure, and expose AsyncWeb3 instances tied to the requested chain.
63
+
64
+ Once implemented:
65
+
66
+ ```python
67
+ custom_wallet = PrivyWallet(privy_client)
68
+ web3_service = DefaultWeb3Service(config, wallet_provider=custom_wallet)
69
+ ```
70
+
71
+ ## Configuration hints
72
+
73
+ `WalletManager.get_provider` looks for `wallet_type` on the top-level config, `main_wallet`, and `strategy_wallet`. Example:
74
+
75
+ ```json
76
+ {
77
+ "strategy": {
78
+ "wallet_type": "local",
79
+ "main_wallet": {"address": "0x...", "wallet_type": "local"},
80
+ "strategy_wallet": {"address": "0x..."}
81
+ }
82
+ }
83
+ ```
84
+
85
+ Currently only `"local"` is supported through configuration—the custom path is injection.
86
+
87
+ ## Why this layer?
88
+
89
+ - Strategies never touch raw `AsyncWeb3`; they call adapters, which call clients, which call a wallet provider.
90
+ - Alternate signing backends can be plugged in without modifying strategy code.
91
+ - Tests can patch `WalletManager.get_provider` or inject stub `EvmTxn` implementations to avoid real RPC calls.
@@ -0,0 +1,56 @@
1
+ """
2
+ Wallet Manager
3
+
4
+ Factory class for resolving and instantiating wallet providers based on configuration.
5
+ Provides convenience methods for config-based wallet provider resolution.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from loguru import logger
11
+
12
+ from wayfinder_paths.core.services.base import EvmTxn
13
+ from wayfinder_paths.core.services.local_evm_txn import LocalEvmTxn
14
+
15
+
16
+ class WalletManager:
17
+ """
18
+ Factory class for wallet providers.
19
+
20
+ Resolves appropriate wallet provider based on config, defaulting to LocalWalletProvider.
21
+ This is a convenience helper - adapters support direct injection, making this optional.
22
+ """
23
+
24
+ @staticmethod
25
+ def get_provider(config: dict[str, Any] | None = None) -> EvmTxn:
26
+ """
27
+ Get wallet provider based on configuration.
28
+
29
+ Args:
30
+ config: Configuration dictionary. May contain wallet_type in wallet configs.
31
+
32
+ Returns:
33
+ WalletProvider instance. Defaults to LocalWalletProvider if no type specified.
34
+ """
35
+ config = config or {}
36
+ wallet_type = config.get("wallet_type")
37
+
38
+ if not wallet_type:
39
+ main_wallet = config.get("main_wallet")
40
+ if isinstance(main_wallet, dict):
41
+ wallet_type = main_wallet.get("wallet_type")
42
+
43
+ if not wallet_type:
44
+ strategy_wallet = config.get("strategy_wallet")
45
+ if isinstance(strategy_wallet, dict):
46
+ wallet_type = strategy_wallet.get("wallet_type")
47
+
48
+ if not wallet_type or wallet_type == "local":
49
+ logger.debug("Using LocalWalletProvider (default)")
50
+ return LocalEvmTxn(config)
51
+
52
+ logger.warning(
53
+ f"Unknown wallet_type '{wallet_type}', defaulting to LocalWalletProvider. "
54
+ "To use custom wallet providers, inject them directly into adapters."
55
+ )
56
+ return LocalEvmTxn(config)
@@ -0,0 +1,7 @@
1
+ """Wallet abstraction layer for supporting multiple wallet types."""
2
+
3
+ from wayfinder_paths.core.services.base import EvmTxn
4
+ from wayfinder_paths.core.services.local_evm_txn import LocalEvmTxn
5
+ from wayfinder_paths.core.wallets.WalletManager import WalletManager
6
+
7
+ __all__ = ["EvmTxn", "LocalEvmTxn", "WalletManager"]
@@ -0,0 +1,17 @@
1
+ from wayfinder_paths.policies.util import allow_functions
2
+
3
+ ENSO_ROUTER = "0xf75584ef6673ad213a685a1b58cc0330b8ea22cf"
4
+
5
+
6
+ async def enso_swap():
7
+ return await allow_functions(
8
+ policy_name="Allow Enso Swap",
9
+ abi_chain_id=8453,
10
+ address=ENSO_ROUTER,
11
+ function_names=[
12
+ "routeMulti",
13
+ "routeSingle",
14
+ "safeRouteMulti",
15
+ "safeRouteSingle",
16
+ ],
17
+ )
@@ -0,0 +1,34 @@
1
+ from wayfinder_paths.core.utils.evm_helpers import ERC20_ABI
2
+
3
+
4
+ def any_erc20_function(token_address: str) -> dict:
5
+ return {
6
+ "name": "Allow Any ERC20 Transfer To Address",
7
+ "method": "eth_signTransaction",
8
+ "action": "ALLOW",
9
+ "conditions": [
10
+ {
11
+ "field_source": "ethereum_transaction",
12
+ "field": "to",
13
+ "operator": "eq",
14
+ "value": token_address,
15
+ }
16
+ ],
17
+ }
18
+
19
+
20
+ def erc20_spender_for_any_token(spender_address: str) -> dict:
21
+ return {
22
+ "name": "Allow Any ERC20 Approve To Spender",
23
+ "method": "eth_signTransaction",
24
+ "action": "ALLOW",
25
+ "conditions": [
26
+ {
27
+ "field_source": "ethereum_calldata",
28
+ "field": "approve.spender",
29
+ "abi": ERC20_ABI,
30
+ "operator": "eq",
31
+ "value": spender_address,
32
+ },
33
+ ],
34
+ }
@@ -0,0 +1,21 @@
1
+ def native_transfer(destination_address: str, value: int) -> dict:
2
+ # TODO THIS FUNCTION IS NOT DONE CAUSE POLICIES DONT KNOW THE WALLET ADDRESS YET.
3
+ return {
4
+ "name": "Allow Native Transfer To Address",
5
+ "method": "eth_signTransaction",
6
+ "action": "ALLOW",
7
+ "conditions": [
8
+ {
9
+ "field_source": "ethereum_transaction",
10
+ "field": "to",
11
+ "operator": "eq",
12
+ "value": destination_address,
13
+ },
14
+ {
15
+ "field_source": "ethereum_transaction",
16
+ "field": "value",
17
+ "operator": "eq",
18
+ "value": hex(value),
19
+ },
20
+ ],
21
+ }
@@ -0,0 +1,19 @@
1
+ from wayfinder_paths.policies.evm import native_transfer
2
+ from wayfinder_paths.policies.util import allow_functions
3
+
4
+ WHYPE_TOKEN = "0x5555555555555555555555555555555555555555"
5
+ HYPERCORE_SENTINEL_ADDRESS = "0x2222222222222222222222222222222222222222"
6
+ HYPERCORE_SENTINEL_VALUE = 100_000_000_000
7
+
8
+
9
+ def hypecore_sentinel_deposit():
10
+ return native_transfer(HYPERCORE_SENTINEL_ADDRESS, HYPERCORE_SENTINEL_VALUE)
11
+
12
+
13
+ async def whype_deposit_and_withdraw():
14
+ return await allow_functions(
15
+ policy_name="Allow WHYPE Deposit and Withdraw",
16
+ abi_chain_id=999,
17
+ address=WHYPE_TOKEN,
18
+ function_names=["deposit", "withdraw"],
19
+ )
@@ -0,0 +1,12 @@
1
+ from wayfinder_paths.policies.util import allow_functions
2
+
3
+ HYPERLEND_POOL = "0x00A89d7a5A02160f20150EbEA7a2b5E4879A1A8b"
4
+
5
+
6
+ async def hyperlend_supply_and_withdraw():
7
+ return await allow_functions(
8
+ policy_name="Allow Hyperlend Supply and Withdraw",
9
+ abi_chain_id=999,
10
+ address=HYPERLEND_POOL,
11
+ function_names=["supply", "withdraw"],
12
+ )
@@ -0,0 +1,30 @@
1
+ def any_hyperliquid_l1_payload():
2
+ return {
3
+ "name": "Allow Hypecore L1 Payload",
4
+ "method": "eth_signTypedData_v4",
5
+ "action": "ALLOW",
6
+ "conditions": [
7
+ {
8
+ "field": "chainId",
9
+ "field_source": "ethereum_typed_data_domain",
10
+ "operator": "eq",
11
+ "value": "1337",
12
+ }
13
+ ],
14
+ }
15
+
16
+
17
+ def any_hyperliquid_user_payload():
18
+ return {
19
+ "name": "Allow User Signed Payload",
20
+ "method": "eth_signTypedData_v4",
21
+ "action": "ALLOW",
22
+ "conditions": [
23
+ {
24
+ "field": "chainId",
25
+ "field_source": "ethereum_typed_data_domain",
26
+ "operator": "eq",
27
+ "value": "421614",
28
+ }
29
+ ],
30
+ }