@agentlayer.tech/wallet 0.1.0
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.
- package/.openclaw/AGENTS.md +98 -0
- package/.openclaw/extensions/agent-wallet/README.md +127 -0
- package/.openclaw/extensions/agent-wallet/index.ts +1520 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +184 -0
- package/.openclaw/extensions/agent-wallet/package.json +11 -0
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +20 -0
- package/CHANGELOG.md +42 -0
- package/LICENSE +104 -0
- package/README.md +332 -0
- package/RELEASING.md +204 -0
- package/agent-wallet/.env.example +62 -0
- package/agent-wallet/AGENTS.md +129 -0
- package/agent-wallet/README.md +527 -0
- package/agent-wallet/agent_wallet/__init__.py +11 -0
- package/agent-wallet/agent_wallet/approval.py +161 -0
- package/agent-wallet/agent_wallet/bootstrap.py +178 -0
- package/agent-wallet/agent_wallet/btc_user_wallets.py +217 -0
- package/agent-wallet/agent_wallet/config.py +382 -0
- package/agent-wallet/agent_wallet/encrypted_storage.py +161 -0
- package/agent-wallet/agent_wallet/evm_user_wallets.py +370 -0
- package/agent-wallet/agent_wallet/exceptions.py +9 -0
- package/agent-wallet/agent_wallet/file_ops.py +34 -0
- package/agent-wallet/agent_wallet/http_client.py +25 -0
- package/agent-wallet/agent_wallet/models.py +66 -0
- package/agent-wallet/agent_wallet/nonce_registry.py +59 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +5128 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +626 -0
- package/agent-wallet/agent_wallet/openclaw_runtime.py +272 -0
- package/agent-wallet/agent_wallet/plugin_bundle.py +42 -0
- package/agent-wallet/agent_wallet/providers/__init__.py +1 -0
- package/agent-wallet/agent_wallet/providers/bags.py +259 -0
- package/agent-wallet/agent_wallet/providers/evm_portfolio.py +470 -0
- package/agent-wallet/agent_wallet/providers/jupiter.py +567 -0
- package/agent-wallet/agent_wallet/providers/kamino.py +215 -0
- package/agent-wallet/agent_wallet/providers/lifi.py +277 -0
- package/agent-wallet/agent_wallet/providers/solana_rpc.py +470 -0
- package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +114 -0
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +205 -0
- package/agent-wallet/agent_wallet/sealed_keys.py +61 -0
- package/agent-wallet/agent_wallet/solana_stake.py +103 -0
- package/agent-wallet/agent_wallet/solana_tx.py +93 -0
- package/agent-wallet/agent_wallet/spending_limits.py +101 -0
- package/agent-wallet/agent_wallet/transaction_policy.py +518 -0
- package/agent-wallet/agent_wallet/user_wallets.py +355 -0
- package/agent-wallet/agent_wallet/validation.py +31 -0
- package/agent-wallet/agent_wallet/wallet_layer/__init__.py +1 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +808 -0
- package/agent-wallet/agent_wallet/wallet_layer/base58.py +44 -0
- package/agent-wallet/agent_wallet/wallet_layer/factory.py +102 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +4252 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +272 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +1628 -0
- package/agent-wallet/examples/bootstrap_wallet.py +21 -0
- package/agent-wallet/examples/openclaw_runtime_onboarding.py +28 -0
- package/agent-wallet/examples/openclaw_user_wallet_example.py +31 -0
- package/agent-wallet/examples/openclaw_wallet_adapter_example.py +33 -0
- package/agent-wallet/openclaw.plugin.json +138 -0
- package/agent-wallet/pyproject.toml +31 -0
- package/agent-wallet/scripts/bootstrap_openclaw_btc.py +278 -0
- package/agent-wallet/scripts/build_release_bundle.py +188 -0
- package/agent-wallet/scripts/finalize_openclaw_local_wallet_config.py +121 -0
- package/agent-wallet/scripts/install_agent_wallet.py +505 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +226 -0
- package/agent-wallet/scripts/install_openclaw_sealed_keys.py +105 -0
- package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +244 -0
- package/agent-wallet/scripts/reveal_btc_seed.sh +130 -0
- package/agent-wallet/scripts/security_utils.py +37 -0
- package/agent-wallet/scripts/setup_btc_wallet.sh +146 -0
- package/agent-wallet/scripts/switch_openclaw_wallet_network.py +106 -0
- package/agent-wallet/skills/wallet-operator/SKILL.md +128 -0
- package/bin/openclaw-agent-wallet.mjs +487 -0
- package/install-from-github.sh +134 -0
- package/package.json +61 -0
- package/setup.sh +40 -0
- package/wdk-btc-wallet/README.md +325 -0
- package/wdk-btc-wallet/bootstrap.sh +22 -0
- package/wdk-btc-wallet/package-lock.json +1839 -0
- package/wdk-btc-wallet/package.json +18 -0
- package/wdk-btc-wallet/run-local.sh +21 -0
- package/wdk-btc-wallet/src/config.js +160 -0
- package/wdk-btc-wallet/src/json.js +35 -0
- package/wdk-btc-wallet/src/local_vault.js +432 -0
- package/wdk-btc-wallet/src/network_state.js +84 -0
- package/wdk-btc-wallet/src/server.js +257 -0
- package/wdk-btc-wallet/src/wdk_btc_wallet.js +332 -0
- package/wdk-evm-wallet/README.md +183 -0
- package/wdk-evm-wallet/bootstrap.sh +8 -0
- package/wdk-evm-wallet/package-lock.json +2340 -0
- package/wdk-evm-wallet/package.json +23 -0
- package/wdk-evm-wallet/run-local.sh +12 -0
- package/wdk-evm-wallet/src/config.js +274 -0
- package/wdk-evm-wallet/src/json.js +35 -0
- package/wdk-evm-wallet/src/local_vault.js +430 -0
- package/wdk-evm-wallet/src/network_state.js +92 -0
- package/wdk-evm-wallet/src/server.js +575 -0
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +4981 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Kamino REST API provider for lending market data and unsigned transaction building."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_wallet.config import settings
|
|
8
|
+
from agent_wallet.exceptions import ProviderError
|
|
9
|
+
from agent_wallet.http_client import get_client
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _normalized_api_base() -> str:
|
|
13
|
+
return settings.kamino_api_base_url.rstrip("/")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalize_named_list_response(
|
|
17
|
+
data: Any,
|
|
18
|
+
*,
|
|
19
|
+
key: str,
|
|
20
|
+
provider_name: str,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
if isinstance(data, list):
|
|
23
|
+
return {key: data}
|
|
24
|
+
if isinstance(data, dict):
|
|
25
|
+
items = data.get(key)
|
|
26
|
+
if isinstance(items, list):
|
|
27
|
+
return data
|
|
28
|
+
fallback = data.get("data")
|
|
29
|
+
if isinstance(fallback, list):
|
|
30
|
+
normalized = dict(data)
|
|
31
|
+
normalized[key] = fallback
|
|
32
|
+
return normalized
|
|
33
|
+
raise ProviderError(provider_name, f"Unexpected {key} response from Kamino.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _normalized_tx_response(data: Any, *, provider_name: str) -> dict[str, Any]:
|
|
37
|
+
if not isinstance(data, dict) or not isinstance(data.get("transaction"), str):
|
|
38
|
+
raise ProviderError(provider_name, "Unexpected transaction build response from Kamino.")
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _env_name(network: str) -> str:
|
|
43
|
+
normalized = str(network).strip().lower()
|
|
44
|
+
if normalized == "devnet":
|
|
45
|
+
return "devnet"
|
|
46
|
+
return "mainnet-beta"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def fetch_lend_markets() -> dict[str, Any]:
|
|
50
|
+
"""Fetch Kamino lending markets for the configured program id."""
|
|
51
|
+
client = get_client()
|
|
52
|
+
response = await client.get(
|
|
53
|
+
f"{_normalized_api_base()}/v2/kamino-market",
|
|
54
|
+
params={"programId": settings.kamino_program_id},
|
|
55
|
+
)
|
|
56
|
+
if response.status_code != 200:
|
|
57
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
58
|
+
return _normalize_named_list_response(
|
|
59
|
+
response.json(),
|
|
60
|
+
key="markets",
|
|
61
|
+
provider_name="kamino",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def fetch_lend_market_reserves(
|
|
66
|
+
*,
|
|
67
|
+
market: str,
|
|
68
|
+
network: str,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
"""Fetch reserve metrics for one Kamino lending market."""
|
|
71
|
+
client = get_client()
|
|
72
|
+
response = await client.get(
|
|
73
|
+
f"{_normalized_api_base()}/kamino-market/{market}/reserves/metrics",
|
|
74
|
+
params={"env": _env_name(network)},
|
|
75
|
+
)
|
|
76
|
+
if response.status_code != 200:
|
|
77
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
78
|
+
return _normalize_named_list_response(
|
|
79
|
+
response.json(),
|
|
80
|
+
key="reserves",
|
|
81
|
+
provider_name="kamino",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def fetch_lend_user_obligations(
|
|
86
|
+
*,
|
|
87
|
+
market: str,
|
|
88
|
+
user: str,
|
|
89
|
+
network: str,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
"""Fetch Kamino obligations for a wallet in a market."""
|
|
92
|
+
client = get_client()
|
|
93
|
+
response = await client.get(
|
|
94
|
+
f"{_normalized_api_base()}/kamino-market/{market}/users/{user}/obligations",
|
|
95
|
+
params={"env": _env_name(network)},
|
|
96
|
+
)
|
|
97
|
+
if response.status_code != 200:
|
|
98
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
99
|
+
return _normalize_named_list_response(
|
|
100
|
+
response.json(),
|
|
101
|
+
key="obligations",
|
|
102
|
+
provider_name="kamino",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def fetch_lend_user_rewards(*, user: str) -> dict[str, Any]:
|
|
107
|
+
"""Fetch Kamino rewards summary for a wallet."""
|
|
108
|
+
client = get_client()
|
|
109
|
+
response = await client.get(
|
|
110
|
+
f"{_normalized_api_base()}/klend/users/{user}/rewards",
|
|
111
|
+
)
|
|
112
|
+
if response.status_code != 200:
|
|
113
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
114
|
+
data = response.json()
|
|
115
|
+
if not isinstance(data, dict):
|
|
116
|
+
raise ProviderError("kamino", "Unexpected rewards response from Kamino.")
|
|
117
|
+
rewards = data.get("rewards")
|
|
118
|
+
if rewards is None:
|
|
119
|
+
data = dict(data)
|
|
120
|
+
data["rewards"] = []
|
|
121
|
+
elif not isinstance(rewards, list):
|
|
122
|
+
raise ProviderError("kamino", "Unexpected rewards response from Kamino.")
|
|
123
|
+
return data
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def build_lend_deposit_transaction(
|
|
127
|
+
*,
|
|
128
|
+
wallet: str,
|
|
129
|
+
market: str,
|
|
130
|
+
reserve: str,
|
|
131
|
+
amount_ui: str,
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
"""Build an unsigned Kamino deposit transaction."""
|
|
134
|
+
client = get_client()
|
|
135
|
+
response = await client.post(
|
|
136
|
+
f"{_normalized_api_base()}/ktx/klend/deposit",
|
|
137
|
+
json={
|
|
138
|
+
"wallet": wallet,
|
|
139
|
+
"market": market,
|
|
140
|
+
"reserve": reserve,
|
|
141
|
+
"amount": amount_ui,
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
if response.status_code != 200:
|
|
145
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
146
|
+
return _normalized_tx_response(response.json(), provider_name="kamino")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def build_lend_withdraw_transaction(
|
|
150
|
+
*,
|
|
151
|
+
wallet: str,
|
|
152
|
+
market: str,
|
|
153
|
+
reserve: str,
|
|
154
|
+
amount_ui: str,
|
|
155
|
+
) -> dict[str, Any]:
|
|
156
|
+
"""Build an unsigned Kamino withdraw transaction."""
|
|
157
|
+
client = get_client()
|
|
158
|
+
response = await client.post(
|
|
159
|
+
f"{_normalized_api_base()}/ktx/klend/withdraw",
|
|
160
|
+
json={
|
|
161
|
+
"wallet": wallet,
|
|
162
|
+
"market": market,
|
|
163
|
+
"reserve": reserve,
|
|
164
|
+
"amount": amount_ui,
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
if response.status_code != 200:
|
|
168
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
169
|
+
return _normalized_tx_response(response.json(), provider_name="kamino")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def build_lend_borrow_transaction(
|
|
173
|
+
*,
|
|
174
|
+
wallet: str,
|
|
175
|
+
market: str,
|
|
176
|
+
reserve: str,
|
|
177
|
+
amount_ui: str,
|
|
178
|
+
) -> dict[str, Any]:
|
|
179
|
+
"""Build an unsigned Kamino borrow transaction."""
|
|
180
|
+
client = get_client()
|
|
181
|
+
response = await client.post(
|
|
182
|
+
f"{_normalized_api_base()}/ktx/klend/borrow",
|
|
183
|
+
json={
|
|
184
|
+
"wallet": wallet,
|
|
185
|
+
"market": market,
|
|
186
|
+
"reserve": reserve,
|
|
187
|
+
"amount": amount_ui,
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
if response.status_code != 200:
|
|
191
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
192
|
+
return _normalized_tx_response(response.json(), provider_name="kamino")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def build_lend_repay_transaction(
|
|
196
|
+
*,
|
|
197
|
+
wallet: str,
|
|
198
|
+
market: str,
|
|
199
|
+
reserve: str,
|
|
200
|
+
amount_ui: str,
|
|
201
|
+
) -> dict[str, Any]:
|
|
202
|
+
"""Build an unsigned Kamino repay transaction."""
|
|
203
|
+
client = get_client()
|
|
204
|
+
response = await client.post(
|
|
205
|
+
f"{_normalized_api_base()}/ktx/klend/repay",
|
|
206
|
+
json={
|
|
207
|
+
"wallet": wallet,
|
|
208
|
+
"market": market,
|
|
209
|
+
"reserve": reserve,
|
|
210
|
+
"amount": amount_ui,
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
if response.status_code != 200:
|
|
214
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
215
|
+
return _normalized_tx_response(response.json(), provider_name="kamino")
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""LI.FI cross-chain quote and status provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_wallet.config import settings
|
|
8
|
+
from agent_wallet.exceptions import ProviderError
|
|
9
|
+
from agent_wallet.http_client import get_client
|
|
10
|
+
|
|
11
|
+
EVM_NATIVE_TOKEN = "0x0000000000000000000000000000000000000000"
|
|
12
|
+
SOLANA_NATIVE_TOKEN = "11111111111111111111111111111111"
|
|
13
|
+
ALWAYS_DENIED_BRIDGES = ("mayan",)
|
|
14
|
+
|
|
15
|
+
_CHAIN_ALIASES = {
|
|
16
|
+
"1": "1",
|
|
17
|
+
"eth": "1",
|
|
18
|
+
"ethereum": "1",
|
|
19
|
+
"mainnet": "1",
|
|
20
|
+
"eth-mainnet": "1",
|
|
21
|
+
"8453": "8453",
|
|
22
|
+
"base": "8453",
|
|
23
|
+
"base-mainnet": "8453",
|
|
24
|
+
"1151111081099710": "1151111081099710",
|
|
25
|
+
"sol": "1151111081099710",
|
|
26
|
+
"solana": "1151111081099710",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_CHAIN_NAMES_BY_ID = {
|
|
30
|
+
"1": "ethereum",
|
|
31
|
+
"8453": "base",
|
|
32
|
+
"1151111081099710": "solana",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
OPENCLAW_SUPPORTED_CHAINS = [
|
|
36
|
+
{"chain": "ethereum", "chain_id": "1", "key": "eth", "name": "Ethereum", "coin": "ETH"},
|
|
37
|
+
{"chain": "base", "chain_id": "8453", "key": "bas", "name": "Base", "coin": "ETH"},
|
|
38
|
+
{"chain": "solana", "chain_id": "1151111081099710", "key": "sol", "name": "Solana", "coin": "SOL"},
|
|
39
|
+
]
|
|
40
|
+
_KNOWN_EVM_TOKEN_ADDRESSES = {
|
|
41
|
+
"1": {
|
|
42
|
+
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
43
|
+
"0xdac17f958d2ee523a2206206994597c13d831ec7": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
|
44
|
+
},
|
|
45
|
+
"8453": {
|
|
46
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def normalize_chain_id(value: str, *, field_name: str) -> str:
|
|
52
|
+
chain = str(value or "").strip().lower()
|
|
53
|
+
normalized = _CHAIN_ALIASES.get(chain, chain)
|
|
54
|
+
if normalized not in _CHAIN_NAMES_BY_ID:
|
|
55
|
+
raise ProviderError("lifi", f"{field_name} is not supported by OpenClaw LI.FI routing: {value}")
|
|
56
|
+
return normalized
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def chain_name_for_id(chain_id: str) -> str:
|
|
60
|
+
return _CHAIN_NAMES_BY_ID.get(str(chain_id), str(chain_id))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def normalize_token_address(token: str, *, chain_id: str) -> str:
|
|
64
|
+
text = str(token or "").strip()
|
|
65
|
+
if not text:
|
|
66
|
+
raise ProviderError("lifi", "token address is required.")
|
|
67
|
+
alias = text.lower()
|
|
68
|
+
if chain_id == "1151111081099710" and alias in {"native", "sol", "solana"}:
|
|
69
|
+
return SOLANA_NATIVE_TOKEN
|
|
70
|
+
if chain_id in {"1", "8453"} and alias in {"native", "eth", "ethereum"}:
|
|
71
|
+
return EVM_NATIVE_TOKEN
|
|
72
|
+
if chain_id in _KNOWN_EVM_TOKEN_ADDRESSES:
|
|
73
|
+
known = _KNOWN_EVM_TOKEN_ADDRESSES[chain_id].get(alias)
|
|
74
|
+
if known:
|
|
75
|
+
return known.lower()
|
|
76
|
+
if chain_id in {"1", "8453"} and alias.startswith("0x") and len(alias) == 42:
|
|
77
|
+
return alias
|
|
78
|
+
return text
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def format_openclaw_supported_chains(chains: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
82
|
+
"""Return OpenClaw's supported LI.FI subset, including Solana if omitted from discovery."""
|
|
83
|
+
items = [
|
|
84
|
+
{
|
|
85
|
+
"chain_id": str(item.get("id") or item.get("chainId") or "").strip(),
|
|
86
|
+
"key": str(item.get("key") or "").strip() or None,
|
|
87
|
+
"name": str(item.get("name") or "").strip() or None,
|
|
88
|
+
"coin": str(item.get("coin") or "").strip() or None,
|
|
89
|
+
"native_token": item.get("nativeToken"),
|
|
90
|
+
"raw": item,
|
|
91
|
+
}
|
|
92
|
+
for item in chains
|
|
93
|
+
]
|
|
94
|
+
supported_ids = {chain["chain_id"] for chain in OPENCLAW_SUPPORTED_CHAINS}
|
|
95
|
+
supported_keys = {chain["key"] for chain in OPENCLAW_SUPPORTED_CHAINS}
|
|
96
|
+
supported_by_id = {
|
|
97
|
+
item["chain_id"]: item
|
|
98
|
+
for item in items
|
|
99
|
+
if item["chain_id"] in supported_ids or str(item.get("key") or "").lower() in supported_keys
|
|
100
|
+
}
|
|
101
|
+
for chain in OPENCLAW_SUPPORTED_CHAINS:
|
|
102
|
+
supported_by_id.setdefault(
|
|
103
|
+
chain["chain_id"],
|
|
104
|
+
{
|
|
105
|
+
"chain_id": chain["chain_id"],
|
|
106
|
+
"key": chain["key"],
|
|
107
|
+
"name": chain["name"],
|
|
108
|
+
"coin": chain["coin"],
|
|
109
|
+
"native_token": None,
|
|
110
|
+
"raw": None,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
return [supported_by_id[chain["chain_id"]] for chain in OPENCLAW_SUPPORTED_CHAINS]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _headers() -> dict[str, str]:
|
|
117
|
+
headers = {
|
|
118
|
+
"Accept": "application/json",
|
|
119
|
+
}
|
|
120
|
+
api_key = settings.lifi_api_key.strip()
|
|
121
|
+
if api_key:
|
|
122
|
+
headers["x-lifi-api-key"] = api_key
|
|
123
|
+
return headers
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _base_url() -> str:
|
|
127
|
+
return settings.lifi_api_base_url.rstrip("/")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _normalize_error(payload: Any) -> str:
|
|
131
|
+
if isinstance(payload, dict):
|
|
132
|
+
message = (
|
|
133
|
+
payload.get("message")
|
|
134
|
+
or payload.get("error")
|
|
135
|
+
or payload.get("detail")
|
|
136
|
+
or payload.get("description")
|
|
137
|
+
)
|
|
138
|
+
if message:
|
|
139
|
+
return str(message)
|
|
140
|
+
errors = payload.get("errors")
|
|
141
|
+
if isinstance(errors, list) and errors:
|
|
142
|
+
return "; ".join(str(item) for item in errors[:3])
|
|
143
|
+
return "Route not found."
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _csv(value: str | list[str] | tuple[str, ...] | None) -> str | None:
|
|
147
|
+
if value is None:
|
|
148
|
+
return None
|
|
149
|
+
if isinstance(value, str):
|
|
150
|
+
text = value.strip()
|
|
151
|
+
return text or None
|
|
152
|
+
if isinstance(value, (list, tuple)):
|
|
153
|
+
items = [str(item).strip() for item in value if str(item).strip()]
|
|
154
|
+
return ",".join(items) if items else None
|
|
155
|
+
return str(value).strip() or None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _merge_bridge_csv(*values: str | list[str] | tuple[str, ...] | None) -> str | None:
|
|
159
|
+
items: list[str] = []
|
|
160
|
+
for value in values:
|
|
161
|
+
text = _csv(value)
|
|
162
|
+
if not text:
|
|
163
|
+
continue
|
|
164
|
+
for item in text.split(","):
|
|
165
|
+
bridge = item.strip()
|
|
166
|
+
if bridge and bridge.lower() not in {existing.lower() for existing in items}:
|
|
167
|
+
items.append(bridge)
|
|
168
|
+
return ",".join(items) if items else None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _clean_params(params: dict[str, Any]) -> dict[str, Any]:
|
|
172
|
+
cleaned: dict[str, Any] = {}
|
|
173
|
+
for key, value in params.items():
|
|
174
|
+
if value is None:
|
|
175
|
+
continue
|
|
176
|
+
if isinstance(value, str) and not value.strip():
|
|
177
|
+
continue
|
|
178
|
+
cleaned[key] = value
|
|
179
|
+
return cleaned
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def fetch_supported_chains() -> list[dict[str, Any]]:
|
|
183
|
+
client = get_client()
|
|
184
|
+
response = await client.get(
|
|
185
|
+
f"{_base_url()}/chains",
|
|
186
|
+
headers=_headers(),
|
|
187
|
+
)
|
|
188
|
+
payload = response.json()
|
|
189
|
+
if response.status_code != 200:
|
|
190
|
+
raise ProviderError("lifi", f"HTTP {response.status_code}: {_normalize_error(payload)}")
|
|
191
|
+
if isinstance(payload, dict) and isinstance(payload.get("chains"), list):
|
|
192
|
+
return [item for item in payload["chains"] if isinstance(item, dict)]
|
|
193
|
+
if isinstance(payload, list):
|
|
194
|
+
return [item for item in payload if isinstance(item, dict)]
|
|
195
|
+
raise ProviderError("lifi", "Unexpected chains response from LI.FI.")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
async def fetch_quote(
|
|
199
|
+
*,
|
|
200
|
+
from_chain: str,
|
|
201
|
+
to_chain: str,
|
|
202
|
+
from_token: str,
|
|
203
|
+
to_token: str,
|
|
204
|
+
amount_in_raw: str,
|
|
205
|
+
from_address: str,
|
|
206
|
+
to_address: str,
|
|
207
|
+
slippage: float | int | None = None,
|
|
208
|
+
integrator: str | None = None,
|
|
209
|
+
allow_bridges: str | list[str] | None = None,
|
|
210
|
+
deny_bridges: str | list[str] | None = None,
|
|
211
|
+
prefer_bridges: str | list[str] | None = None,
|
|
212
|
+
) -> dict[str, Any]:
|
|
213
|
+
from_chain_id = normalize_chain_id(from_chain, field_name="from_chain")
|
|
214
|
+
to_chain_id = normalize_chain_id(to_chain, field_name="to_chain")
|
|
215
|
+
default_deny_bridges = settings.lifi_default_deny_bridges.strip()
|
|
216
|
+
effective_deny_bridges = _merge_bridge_csv(
|
|
217
|
+
default_deny_bridges,
|
|
218
|
+
deny_bridges,
|
|
219
|
+
ALWAYS_DENIED_BRIDGES,
|
|
220
|
+
)
|
|
221
|
+
params = _clean_params(
|
|
222
|
+
{
|
|
223
|
+
"fromChain": from_chain_id,
|
|
224
|
+
"toChain": to_chain_id,
|
|
225
|
+
"fromToken": normalize_token_address(from_token, chain_id=from_chain_id),
|
|
226
|
+
"toToken": normalize_token_address(to_token, chain_id=to_chain_id),
|
|
227
|
+
"fromAmount": str(amount_in_raw).strip(),
|
|
228
|
+
"fromAddress": str(from_address).strip(),
|
|
229
|
+
"toAddress": str(to_address).strip(),
|
|
230
|
+
"slippage": slippage,
|
|
231
|
+
"integrator": (integrator or settings.lifi_integrator).strip(),
|
|
232
|
+
"allowBridges": _csv(allow_bridges),
|
|
233
|
+
"denyBridges": effective_deny_bridges,
|
|
234
|
+
"preferBridges": _csv(prefer_bridges),
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
client = get_client()
|
|
238
|
+
response = await client.get(
|
|
239
|
+
f"{_base_url()}/quote",
|
|
240
|
+
params=params,
|
|
241
|
+
headers=_headers(),
|
|
242
|
+
)
|
|
243
|
+
payload = response.json()
|
|
244
|
+
if response.status_code != 200:
|
|
245
|
+
raise ProviderError("lifi", f"HTTP {response.status_code}: {_normalize_error(payload)}")
|
|
246
|
+
if not isinstance(payload, dict):
|
|
247
|
+
raise ProviderError("lifi", "Unexpected quote response from LI.FI.")
|
|
248
|
+
return payload
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
async def fetch_transfer_status(
|
|
252
|
+
*,
|
|
253
|
+
tx_hash: str,
|
|
254
|
+
bridge: str | None = None,
|
|
255
|
+
from_chain: str | None = None,
|
|
256
|
+
to_chain: str | None = None,
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
params = _clean_params(
|
|
259
|
+
{
|
|
260
|
+
"txHash": str(tx_hash).strip(),
|
|
261
|
+
"bridge": str(bridge).strip() if isinstance(bridge, str) else None,
|
|
262
|
+
"fromChain": normalize_chain_id(from_chain, field_name="from_chain") if from_chain else None,
|
|
263
|
+
"toChain": normalize_chain_id(to_chain, field_name="to_chain") if to_chain else None,
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
client = get_client()
|
|
267
|
+
response = await client.get(
|
|
268
|
+
f"{_base_url()}/status",
|
|
269
|
+
params=params,
|
|
270
|
+
headers=_headers(),
|
|
271
|
+
)
|
|
272
|
+
payload = response.json()
|
|
273
|
+
if response.status_code != 200:
|
|
274
|
+
raise ProviderError("lifi", f"HTTP {response.status_code}: {_normalize_error(payload)}")
|
|
275
|
+
if not isinstance(payload, dict):
|
|
276
|
+
raise ProviderError("lifi", "Unexpected status response from LI.FI.")
|
|
277
|
+
return payload
|