@agentlayer.tech/wallet 0.1.93 → 0.1.94
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/extensions/agent-wallet/dist/index.js +41 -0
- package/.openclaw/extensions/agent-wallet/index.ts +41 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +4 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +18 -0
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/autonomous_permissions.py +1 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +136 -0
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +2 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +18 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +97 -0
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/server.py +1 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/.env.example +3 -0
- package/wdk-evm-wallet/README.md +6 -0
- package/wdk-evm-wallet/package.json +1 -1
- package/wdk-evm-wallet/src/config.js +9 -0
- package/wdk-evm-wallet/src/server.js +12 -0
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +345 -0
|
@@ -29,6 +29,7 @@ const AUTONOMOUS_DEFI_TOOLS = new Set([
|
|
|
29
29
|
"manage_evm_lido_withdrawal",
|
|
30
30
|
"manage_evm_morpho_market_position",
|
|
31
31
|
"manage_evm_morpho_vault_position",
|
|
32
|
+
"manage_evm_uniswap_liquidity",
|
|
32
33
|
]);
|
|
33
34
|
const approvalPreviewCache = new Map();
|
|
34
35
|
const WALLET_TOOL_ONLY_GUIDANCE =
|
|
@@ -1977,6 +1978,23 @@ const evmToolDefinitions = [
|
|
|
1977
1978
|
additionalProperties: false,
|
|
1978
1979
|
},
|
|
1979
1980
|
},
|
|
1981
|
+
{
|
|
1982
|
+
name: "search_uniswap_pairs",
|
|
1983
|
+
description: "Search read-only onchain Uniswap-tradeable pairs by name, ticker, or ERC-20 address. Verify any returned token address independently before quoting or trading.",
|
|
1984
|
+
parameters: {
|
|
1985
|
+
type: "object",
|
|
1986
|
+
properties: {
|
|
1987
|
+
query: { type: "string" },
|
|
1988
|
+
token_address: { type: "string" },
|
|
1989
|
+
chain: { type: "string" },
|
|
1990
|
+
dex_id: { type: "string" },
|
|
1991
|
+
all_chains: { type: "boolean" },
|
|
1992
|
+
limit: { type: "integer" },
|
|
1993
|
+
network: { type: "string", enum: EVM_CORE_NETWORKS },
|
|
1994
|
+
},
|
|
1995
|
+
additionalProperties: false,
|
|
1996
|
+
},
|
|
1997
|
+
},
|
|
1980
1998
|
{
|
|
1981
1999
|
name: "swap_evm_uniswap_tokens",
|
|
1982
2000
|
description: "Preview, prepare, or execute a supported Uniswap path on ethereum, base, or robinhood: CLASSIC, UniswapX orders, or canonical ETH↔WETH wrap/unwrap. ERC-20 paths may use Permit2 EIP-712 or an UniswapX order signature. Preview or prepare first. After the user explicitly confirms the shown summary in chat, call execute; the OpenClaw plugin handles the internal execution authorization automatically.",
|
|
@@ -1997,6 +2015,29 @@ const evmToolDefinitions = [
|
|
|
1997
2015
|
additionalProperties: false,
|
|
1998
2016
|
},
|
|
1999
2017
|
},
|
|
2018
|
+
{
|
|
2019
|
+
name: "manage_evm_uniswap_liquidity",
|
|
2020
|
+
description: "Preview, prepare, or execute a Uniswap V3/V4 liquidity action on ethereum, base, or robinhood. Supported actions are create, increase, decrease, and claim_fees. The official Liquidity API builds the transaction again immediately before signing, so confirmation is scoped to the LP intent rather than fragile ticks or calldata.",
|
|
2021
|
+
optional: true,
|
|
2022
|
+
parameters: {
|
|
2023
|
+
type: "object",
|
|
2024
|
+
properties: {
|
|
2025
|
+
action: { type: "string", enum: ["create", "increase", "decrease", "claim_fees"] },
|
|
2026
|
+
protocol: { type: "string", enum: ["V3", "V4"] },
|
|
2027
|
+
request: {
|
|
2028
|
+
type: "object",
|
|
2029
|
+
description: "Official Uniswap Liquidity API fields. walletAddress, chainId, protocol, simulateTransaction, and permit/signature fields are controlled by the wallet.",
|
|
2030
|
+
additionalProperties: true,
|
|
2031
|
+
},
|
|
2032
|
+
mode: { type: "string", enum: ["preview", "prepare", "execute"] },
|
|
2033
|
+
purpose: { type: "string" },
|
|
2034
|
+
user_intent: { type: "boolean" },
|
|
2035
|
+
network: { type: "string", enum: EVM_CORE_NETWORKS },
|
|
2036
|
+
},
|
|
2037
|
+
required: ["action", "protocol", "request", "mode", "purpose"],
|
|
2038
|
+
additionalProperties: false,
|
|
2039
|
+
},
|
|
2040
|
+
},
|
|
2000
2041
|
{
|
|
2001
2042
|
name: "swap_evm_lifi_cross_chain_tokens",
|
|
2002
2043
|
description: "Preview, prepare, or execute an EVM-origin cross-chain swap through LI.FI. This currently supports ethereum/base as the source network and ethereum/base/solana as the destination chain. Preview or prepare first. After the user explicitly confirms the shown summary in chat, call execute; the OpenClaw plugin handles the internal execution authorization automatically.",
|
|
@@ -29,6 +29,7 @@ const AUTONOMOUS_DEFI_TOOLS = new Set([
|
|
|
29
29
|
"manage_evm_lido_withdrawal",
|
|
30
30
|
"manage_evm_morpho_market_position",
|
|
31
31
|
"manage_evm_morpho_vault_position",
|
|
32
|
+
"manage_evm_uniswap_liquidity",
|
|
32
33
|
]);
|
|
33
34
|
const approvalPreviewCache = new Map();
|
|
34
35
|
const WALLET_TOOL_ONLY_GUIDANCE =
|
|
@@ -1977,6 +1978,23 @@ const evmToolDefinitions = [
|
|
|
1977
1978
|
additionalProperties: false,
|
|
1978
1979
|
},
|
|
1979
1980
|
},
|
|
1981
|
+
{
|
|
1982
|
+
name: "search_uniswap_pairs",
|
|
1983
|
+
description: "Search read-only onchain Uniswap-tradeable pairs by name, ticker, or ERC-20 address. Verify any returned token address independently before quoting or trading.",
|
|
1984
|
+
parameters: {
|
|
1985
|
+
type: "object",
|
|
1986
|
+
properties: {
|
|
1987
|
+
query: { type: "string" },
|
|
1988
|
+
token_address: { type: "string" },
|
|
1989
|
+
chain: { type: "string" },
|
|
1990
|
+
dex_id: { type: "string" },
|
|
1991
|
+
all_chains: { type: "boolean" },
|
|
1992
|
+
limit: { type: "integer" },
|
|
1993
|
+
network: { type: "string", enum: EVM_CORE_NETWORKS },
|
|
1994
|
+
},
|
|
1995
|
+
additionalProperties: false,
|
|
1996
|
+
},
|
|
1997
|
+
},
|
|
1980
1998
|
{
|
|
1981
1999
|
name: "swap_evm_uniswap_tokens",
|
|
1982
2000
|
description: "Preview, prepare, or execute a supported Uniswap path on ethereum, base, or robinhood: CLASSIC, UniswapX orders, or canonical ETH↔WETH wrap/unwrap. ERC-20 paths may use Permit2 EIP-712 or an UniswapX order signature. Preview or prepare first. After the user explicitly confirms the shown summary in chat, call execute; the OpenClaw plugin handles the internal execution authorization automatically.",
|
|
@@ -1997,6 +2015,29 @@ const evmToolDefinitions = [
|
|
|
1997
2015
|
additionalProperties: false,
|
|
1998
2016
|
},
|
|
1999
2017
|
},
|
|
2018
|
+
{
|
|
2019
|
+
name: "manage_evm_uniswap_liquidity",
|
|
2020
|
+
description: "Preview, prepare, or execute a Uniswap V3/V4 liquidity action on ethereum, base, or robinhood. Supported actions are create, increase, decrease, and claim_fees. The official Liquidity API builds the transaction again immediately before signing, so confirmation is scoped to the LP intent rather than fragile ticks or calldata.",
|
|
2021
|
+
optional: true,
|
|
2022
|
+
parameters: {
|
|
2023
|
+
type: "object",
|
|
2024
|
+
properties: {
|
|
2025
|
+
action: { type: "string", enum: ["create", "increase", "decrease", "claim_fees"] },
|
|
2026
|
+
protocol: { type: "string", enum: ["V3", "V4"] },
|
|
2027
|
+
request: {
|
|
2028
|
+
type: "object",
|
|
2029
|
+
description: "Official Uniswap Liquidity API fields. walletAddress, chainId, protocol, simulateTransaction, and permit/signature fields are controlled by the wallet.",
|
|
2030
|
+
additionalProperties: true,
|
|
2031
|
+
},
|
|
2032
|
+
mode: { type: "string", enum: ["preview", "prepare", "execute"] },
|
|
2033
|
+
purpose: { type: "string" },
|
|
2034
|
+
user_intent: { type: "boolean" },
|
|
2035
|
+
network: { type: "string", enum: EVM_CORE_NETWORKS },
|
|
2036
|
+
},
|
|
2037
|
+
required: ["action", "protocol", "request", "mode", "purpose"],
|
|
2038
|
+
additionalProperties: false,
|
|
2039
|
+
},
|
|
2040
|
+
},
|
|
2000
2041
|
{
|
|
2001
2042
|
name: "swap_evm_lifi_cross_chain_tokens",
|
|
2002
2043
|
description: "Preview, prepare, or execute an EVM-origin cross-chain swap through LI.FI. This currently supports ethereum/base as the source network and ethereum/base/solana as the destination chain. Preview or prepare first. After the user explicitly confirms the shown summary in chat, call execute; the OpenClaw plugin handles the internal execution authorization automatically.",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agent-wallet",
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Official OpenClaw plugin bridge for the agent-wallet backends, including Solana, local BTC, and local EVM.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.94",
|
|
6
6
|
"contracts": {
|
|
7
7
|
"tools": [
|
|
8
8
|
"agentlayer_autonomous_approve",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"get_evm_token_balance",
|
|
31
31
|
"get_evm_token_metadata",
|
|
32
32
|
"get_evm_transaction_receipt",
|
|
33
|
+
"get_uniswap_swap_quote",
|
|
33
34
|
"get_flash_trade_markets",
|
|
34
35
|
"get_flash_trade_positions",
|
|
35
36
|
"get_kamino_earn_positions",
|
|
@@ -61,11 +62,13 @@
|
|
|
61
62
|
"manage_evm_morpho_vault_position",
|
|
62
63
|
"manage_evm_lido_position",
|
|
63
64
|
"manage_evm_lido_withdrawal",
|
|
65
|
+
"manage_evm_uniswap_liquidity",
|
|
64
66
|
"search_uniswap_pairs",
|
|
65
67
|
"set_evm_network",
|
|
66
68
|
"set_wallet_backend",
|
|
67
69
|
"sign_wallet_message",
|
|
68
70
|
"swap_evm_lifi_cross_chain_tokens",
|
|
71
|
+
"swap_evm_uniswap_tokens",
|
|
69
72
|
"swap_evm_tokens",
|
|
70
73
|
"swap_solana_lifi_cross_chain_tokens",
|
|
71
74
|
"swap_solana_tokens",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.1.94 - 2026-08-13
|
|
6
|
+
|
|
7
|
+
- **Added Uniswap V3/V4 liquidity provisioning for Ethereum, Base, and
|
|
8
|
+
Robinhood Chain.** The EVM wallet now previews, prepares, and executes
|
|
9
|
+
create, increase, decrease, and fee-claim actions through Uniswap's
|
|
10
|
+
Liquidity API. Final LP calldata is refreshed and simulated immediately
|
|
11
|
+
before signing; executable transactions are accepted only for pinned
|
|
12
|
+
PositionManager deployments and matching wallet/network fields.
|
|
13
|
+
- **Added bounded LP approval handling and intent-safe confirmations.** Token
|
|
14
|
+
approvals are restricted to LP-request assets and approved spenders, while
|
|
15
|
+
confirmations bind the action, network, protocol, pool/position, and assets
|
|
16
|
+
rather than fast-expiring ticks or calldata. Single-sided concentrated
|
|
17
|
+
positions with a zero amount on one ERC-20 side no longer fail approval
|
|
18
|
+
discovery.
|
|
19
|
+
- **Exposed LP actions through the provider gateway, OpenClaw, and Codex.**
|
|
20
|
+
The provider gateway forwards the existing shared Uniswap API key to the
|
|
21
|
+
Liquidity API; OpenClaw's pair-search contract was restored and registered.
|
|
22
|
+
|
|
5
23
|
## v0.1.93 - 2026-08-10
|
|
6
24
|
|
|
7
25
|
- **Added `/cards` (Claude Code) and `cards` (Codex) for Laso Finance card
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.94
|
|
@@ -751,6 +751,28 @@ class OpenClawWalletAdapter:
|
|
|
751
751
|
"quote_fingerprint": provided_fingerprint,
|
|
752
752
|
}
|
|
753
753
|
|
|
754
|
+
if asset_type == "evm-uniswap-liquidity":
|
|
755
|
+
request = payload.get("request") if isinstance(payload.get("request"), dict) else {}
|
|
756
|
+
existing_pool = request.get("existingPool") if isinstance(request.get("existingPool"), dict) else {}
|
|
757
|
+
return {
|
|
758
|
+
"operation": action_label,
|
|
759
|
+
"network": str(payload.get("network") or getattr(self.backend, "network", "unknown")),
|
|
760
|
+
"from_address": payload.get("from_address"),
|
|
761
|
+
"protocol": payload.get("protocol"),
|
|
762
|
+
"liquidity_action": payload.get("liquidity_action"),
|
|
763
|
+
"liquidity_protocol": payload.get("liquidity_protocol"),
|
|
764
|
+
# Intent binds to the action and caller-provided LP scope, not a
|
|
765
|
+
# per-block transaction hash, adjusted ticks, or calculated amount.
|
|
766
|
+
"token0_address": request.get("token0Address") or existing_pool.get("token0Address"),
|
|
767
|
+
"token1_address": request.get("token1Address") or existing_pool.get("token1Address"),
|
|
768
|
+
"pool_reference": existing_pool.get("poolReference"),
|
|
769
|
+
"position_token_id": request.get("nftTokenId") or request.get("tokenId") or payload.get("position_token_id"),
|
|
770
|
+
"independent_token": request.get("independentToken"),
|
|
771
|
+
"liquidity_percentage_to_decrease": request.get("liquidityPercentageToDecrease"),
|
|
772
|
+
"slippage_tolerance": request.get("slippageTolerance"),
|
|
773
|
+
"position_manager": payload.get("position_manager"),
|
|
774
|
+
}
|
|
775
|
+
|
|
754
776
|
if asset_type == "evm-morpho-vault":
|
|
755
777
|
return {
|
|
756
778
|
"operation": action_label,
|
|
@@ -1722,6 +1744,35 @@ class OpenClawWalletAdapter:
|
|
|
1722
1744
|
risk_level="low",
|
|
1723
1745
|
),
|
|
1724
1746
|
)
|
|
1747
|
+
tools.insert(
|
|
1748
|
+
13,
|
|
1749
|
+
AgentToolSpec(
|
|
1750
|
+
name="manage_evm_uniswap_liquidity",
|
|
1751
|
+
description=(
|
|
1752
|
+
"Preview, prepare, or execute a Uniswap V3/V4 liquidity action on ethereum, base, or robinhood. "
|
|
1753
|
+
"Supported actions are create, increase, decrease, and claim_fees. The request is passed to the "
|
|
1754
|
+
"official Uniswap Liquidity API; execute refreshes the transaction immediately before signing."
|
|
1755
|
+
),
|
|
1756
|
+
input_schema={
|
|
1757
|
+
"type": "object",
|
|
1758
|
+
"properties": {
|
|
1759
|
+
"action": {"type": "string", "enum": ["create", "increase", "decrease", "claim_fees"]},
|
|
1760
|
+
"protocol": {"type": "string", "enum": ["V3", "V4"]},
|
|
1761
|
+
"request": {"type": "object", "description": "Uniswap LP API fields excluding walletAddress, chainId, protocol, and simulateTransaction; those are set from the active wallet.", "additionalProperties": True},
|
|
1762
|
+
"mode": {"type": "string", "enum": ["preview", "prepare", "execute"]},
|
|
1763
|
+
"purpose": {"type": "string"},
|
|
1764
|
+
"user_intent": {"type": "boolean"},
|
|
1765
|
+
"approval_token": {"type": "string"},
|
|
1766
|
+
"network": {"type": "string", "enum": ["ethereum", "base", "robinhood"]},
|
|
1767
|
+
},
|
|
1768
|
+
"required": ["action", "protocol", "request", "mode", "purpose"],
|
|
1769
|
+
"additionalProperties": False,
|
|
1770
|
+
},
|
|
1771
|
+
read_only=False,
|
|
1772
|
+
requires_explicit_user_intent=True,
|
|
1773
|
+
risk_level="high",
|
|
1774
|
+
),
|
|
1775
|
+
)
|
|
1725
1776
|
tools.insert(
|
|
1726
1777
|
14,
|
|
1727
1778
|
AgentToolSpec(
|
|
@@ -5757,6 +5808,91 @@ class OpenClawWalletAdapter:
|
|
|
5757
5808
|
),
|
|
5758
5809
|
)
|
|
5759
5810
|
|
|
5811
|
+
if tool_name == "manage_evm_uniswap_liquidity":
|
|
5812
|
+
action = args.get("action")
|
|
5813
|
+
protocol = args.get("protocol")
|
|
5814
|
+
request = args.get("request")
|
|
5815
|
+
mode = args.get("mode")
|
|
5816
|
+
purpose = args.get("purpose")
|
|
5817
|
+
user_intent = args.get("user_intent", False)
|
|
5818
|
+
approval_token = args.get("approval_token")
|
|
5819
|
+
if action not in {"create", "increase", "decrease", "claim_fees"}:
|
|
5820
|
+
raise WalletBackendError("action must be create, increase, decrease, or claim_fees.")
|
|
5821
|
+
if protocol not in {"V3", "V4"}:
|
|
5822
|
+
raise WalletBackendError("protocol must be V3 or V4.")
|
|
5823
|
+
if not isinstance(request, dict):
|
|
5824
|
+
raise WalletBackendError("request must be an object.")
|
|
5825
|
+
if mode not in {"preview", "prepare", "execute"}:
|
|
5826
|
+
raise WalletBackendError("mode must be 'preview', 'prepare' or 'execute'.")
|
|
5827
|
+
if not isinstance(purpose, str) or not purpose.strip():
|
|
5828
|
+
raise WalletBackendError("purpose is required.")
|
|
5829
|
+
# The caller must not override values that are derived from the
|
|
5830
|
+
# active wallet/network in the WDK layer.
|
|
5831
|
+
forbidden = {"walletAddress", "chainId", "protocol", "simulateTransaction", "signature", "batchPermitData", "v4BatchPermitData", "v3NftPermitData"}
|
|
5832
|
+
overlap = forbidden.intersection(request)
|
|
5833
|
+
if overlap:
|
|
5834
|
+
raise WalletBackendError(f"request must not set wallet-controlled fields: {', '.join(sorted(overlap))}.")
|
|
5835
|
+
preview_kwargs = {"action": action, "protocol": protocol, "request": dict(request)}
|
|
5836
|
+
if mode == "preview":
|
|
5837
|
+
preview = await active_backend.preview_uniswap_liquidity(**preview_kwargs)
|
|
5838
|
+
return AgentToolResult(tool=tool_name, ok=True, data=self._annotate_sensitive_payload(preview, action_label="Uniswap liquidity", mode="preview"))
|
|
5839
|
+
if mode == "prepare":
|
|
5840
|
+
self._require_prepare_intent(user_intent)
|
|
5841
|
+
preview = await active_backend.preview_uniswap_liquidity(**preview_kwargs)
|
|
5842
|
+
return AgentToolResult(tool=tool_name, ok=True, data=self._annotate_sensitive_payload(self._build_prepare_plan(preview_payload=preview, action_label="Uniswap liquidity"), action_label="Uniswap liquidity", mode="prepare"))
|
|
5843
|
+
if isinstance(approval_token, str) and approval_token.strip():
|
|
5844
|
+
approval_payload = inspect_approval_token(
|
|
5845
|
+
approval_token,
|
|
5846
|
+
tool_name=tool_name,
|
|
5847
|
+
network=str(getattr(active_backend, "network", "unknown")),
|
|
5848
|
+
require_mainnet_confirmation=self._is_mainnet_for_backend(active_backend),
|
|
5849
|
+
)
|
|
5850
|
+
approval_summary = approval_payload.get("binding", {}).get("summary")
|
|
5851
|
+
if not isinstance(approval_summary, dict):
|
|
5852
|
+
raise WalletBackendError("approval_token does not match the requested LP operation. Generate a new approval after prepare.")
|
|
5853
|
+
approval_summary_copy = dict(approval_summary)
|
|
5854
|
+
else:
|
|
5855
|
+
fresh_preview = await active_backend.preview_uniswap_liquidity(**preview_kwargs)
|
|
5856
|
+
approval_summary_copy = self._build_confirmation_summary(action_label="Uniswap liquidity", payload=fresh_preview)
|
|
5857
|
+
expected = {
|
|
5858
|
+
"operation": "Uniswap liquidity",
|
|
5859
|
+
"network": str(getattr(active_backend, "network", "unknown")),
|
|
5860
|
+
"liquidity_action": action,
|
|
5861
|
+
"liquidity_protocol": protocol,
|
|
5862
|
+
}
|
|
5863
|
+
for key, value in expected.items():
|
|
5864
|
+
if approval_summary_copy.get(key) != value:
|
|
5865
|
+
raise WalletBackendError("approval_token does not match the requested LP operation. Generate a new approval after prepare.")
|
|
5866
|
+
# An LP intent deliberately does not bind API-calculated ticks,
|
|
5867
|
+
# price, calldata, or quote amounts: those are regenerated just
|
|
5868
|
+
# before broadcast. It must still be scoped to the selected
|
|
5869
|
+
# assets/pool/position so an approval cannot be reused for a
|
|
5870
|
+
# different liquidity position.
|
|
5871
|
+
requested_scope = self._build_confirmation_summary(
|
|
5872
|
+
action_label="Uniswap liquidity",
|
|
5873
|
+
payload={
|
|
5874
|
+
"asset_type": "evm-uniswap-liquidity",
|
|
5875
|
+
"network": str(getattr(active_backend, "network", "unknown")),
|
|
5876
|
+
"protocol": "uniswap",
|
|
5877
|
+
"liquidity_action": action,
|
|
5878
|
+
"liquidity_protocol": protocol,
|
|
5879
|
+
"request": preview_kwargs["request"],
|
|
5880
|
+
},
|
|
5881
|
+
)
|
|
5882
|
+
for key in ("token0_address", "token1_address", "pool_reference", "position_token_id", "independent_token"):
|
|
5883
|
+
requested_value = requested_scope.get(key)
|
|
5884
|
+
if requested_value is not None and approval_summary_copy.get(key) != requested_value:
|
|
5885
|
+
raise WalletBackendError("approval_token does not match the requested LP assets or position. Generate a new approval after prepare.")
|
|
5886
|
+
self._require_execute_approval(
|
|
5887
|
+
approval_token=approval_token,
|
|
5888
|
+
tool_name=tool_name,
|
|
5889
|
+
summary=approval_summary_copy,
|
|
5890
|
+
action_label="Uniswap liquidity",
|
|
5891
|
+
backend=active_backend,
|
|
5892
|
+
)
|
|
5893
|
+
result = await active_backend.send_uniswap_liquidity(**preview_kwargs)
|
|
5894
|
+
return AgentToolResult(tool=tool_name, ok=True, data=self._annotate_sensitive_payload(result, action_label="Uniswap liquidity", mode="execute"))
|
|
5895
|
+
|
|
5760
5896
|
if tool_name == "issue_wallet_approval":
|
|
5761
5897
|
from agent_wallet.approval import issue_approval_token
|
|
5762
5898
|
|
|
@@ -40,6 +40,8 @@ LONG_RUNNING_POST_PATHS = {
|
|
|
40
40
|
"/v1/evm/swap/send",
|
|
41
41
|
"/v1/evm/uniswap/swap/quote",
|
|
42
42
|
"/v1/evm/uniswap/swap/send",
|
|
43
|
+
"/v1/evm/uniswap/liquidity/quote",
|
|
44
|
+
"/v1/evm/uniswap/liquidity/send",
|
|
43
45
|
"/v1/evm/lifi/quote",
|
|
44
46
|
"/v1/evm/lifi/send",
|
|
45
47
|
"/v1/evm/transfer/send",
|
|
@@ -349,6 +349,24 @@ class AgentWalletBackend(ABC):
|
|
|
349
349
|
) -> dict[str, Any]:
|
|
350
350
|
raise WalletBackendError(f"{self.name} does not support Uniswap swaps.")
|
|
351
351
|
|
|
352
|
+
async def preview_uniswap_liquidity(
|
|
353
|
+
self,
|
|
354
|
+
*,
|
|
355
|
+
action: str,
|
|
356
|
+
protocol: str,
|
|
357
|
+
request: dict[str, Any],
|
|
358
|
+
) -> dict[str, Any]:
|
|
359
|
+
raise WalletBackendError(f"{self.name} does not support Uniswap liquidity previews.")
|
|
360
|
+
|
|
361
|
+
async def send_uniswap_liquidity(
|
|
362
|
+
self,
|
|
363
|
+
*,
|
|
364
|
+
action: str,
|
|
365
|
+
protocol: str,
|
|
366
|
+
request: dict[str, Any],
|
|
367
|
+
) -> dict[str, Any]:
|
|
368
|
+
raise WalletBackendError(f"{self.name} does not support Uniswap liquidity operations.")
|
|
369
|
+
|
|
352
370
|
async def preview_evm_native_transfer(
|
|
353
371
|
self,
|
|
354
372
|
*,
|
|
@@ -2028,6 +2028,103 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
2028
2028
|
"source": str(data.get("source") or "wdk-evm-wallet"),
|
|
2029
2029
|
}
|
|
2030
2030
|
|
|
2031
|
+
def _normalize_uniswap_liquidity_payload(
|
|
2032
|
+
self,
|
|
2033
|
+
data: dict[str, Any],
|
|
2034
|
+
*,
|
|
2035
|
+
action: str,
|
|
2036
|
+
protocol: str,
|
|
2037
|
+
) -> dict[str, Any]:
|
|
2038
|
+
transaction = dict(data.get("transaction") or {})
|
|
2039
|
+
result = dict(data.get("result") or {})
|
|
2040
|
+
request = dict(data.get("request") or {})
|
|
2041
|
+
return {
|
|
2042
|
+
"chain": self.chain,
|
|
2043
|
+
"network": self.network,
|
|
2044
|
+
"asset_type": "evm-uniswap-liquidity",
|
|
2045
|
+
"protocol": str(data.get("protocol") or "uniswap"),
|
|
2046
|
+
"liquidity_action": str(data.get("liquidityAction") or action),
|
|
2047
|
+
"liquidity_protocol": str(data.get("liquidityProtocol") or protocol).upper(),
|
|
2048
|
+
"from_address": str(data.get("address") or "").strip() or None,
|
|
2049
|
+
"request": request,
|
|
2050
|
+
"request_id": str(data.get("requestId") or "").strip() or None,
|
|
2051
|
+
"token0": data.get("token0"),
|
|
2052
|
+
"token1": data.get("token1"),
|
|
2053
|
+
"tick_lower": data.get("tickLower"),
|
|
2054
|
+
"tick_upper": data.get("tickUpper"),
|
|
2055
|
+
"adjusted_min_price": data.get("adjustedMinPrice"),
|
|
2056
|
+
"adjusted_max_price": data.get("adjustedMaxPrice"),
|
|
2057
|
+
"position_token_id": str(data.get("positionTokenId") or "").strip() or None,
|
|
2058
|
+
"position_manager": str(data.get("positionManager") or "").strip() or None,
|
|
2059
|
+
"gas_fee": str(data.get("gasFee")) if data.get("gasFee") is not None else None,
|
|
2060
|
+
"approvals": list(data.get("approvals") or []),
|
|
2061
|
+
"approval_results": list(data.get("approvalResults") or []),
|
|
2062
|
+
"simulation": _normalize_swap_simulation(data.get("simulation")),
|
|
2063
|
+
"transaction": {
|
|
2064
|
+
"to": str(transaction.get("to") or "").strip() or None,
|
|
2065
|
+
"value": str(transaction.get("value") or "0"),
|
|
2066
|
+
"data_hash": str(transaction.get("dataHash") or "").strip() or None,
|
|
2067
|
+
},
|
|
2068
|
+
"hash": result.get("hash"),
|
|
2069
|
+
"result": result,
|
|
2070
|
+
"chain_id": int(data.get("chainId") or 0),
|
|
2071
|
+
"broadcasted": bool(result.get("hash")),
|
|
2072
|
+
"confirmed": bool(data.get("confirmed")),
|
|
2073
|
+
"source": str(data.get("source") or "wdk-evm-wallet"),
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
async def preview_uniswap_liquidity(
|
|
2077
|
+
self,
|
|
2078
|
+
*,
|
|
2079
|
+
action: str,
|
|
2080
|
+
protocol: str,
|
|
2081
|
+
request: dict[str, Any],
|
|
2082
|
+
) -> dict[str, Any]:
|
|
2083
|
+
if not isinstance(request, dict):
|
|
2084
|
+
raise WalletBackendError("Uniswap liquidity request must be an object.")
|
|
2085
|
+
data = await self.client.post(
|
|
2086
|
+
"/v1/evm/uniswap/liquidity/quote",
|
|
2087
|
+
{
|
|
2088
|
+
"walletId": self.wallet_id,
|
|
2089
|
+
"address": await self.get_address(),
|
|
2090
|
+
"accountIndex": self.account_index,
|
|
2091
|
+
"network": self.network,
|
|
2092
|
+
"action": action,
|
|
2093
|
+
"protocol": protocol,
|
|
2094
|
+
"request": request,
|
|
2095
|
+
},
|
|
2096
|
+
)
|
|
2097
|
+
normalized = self._normalize_uniswap_liquidity_payload(data, action=action, protocol=protocol)
|
|
2098
|
+
normalized["from_address"] = await self.get_address()
|
|
2099
|
+
normalized["execution_supported"] = not self.sign_only
|
|
2100
|
+
return normalized
|
|
2101
|
+
|
|
2102
|
+
async def send_uniswap_liquidity(
|
|
2103
|
+
self,
|
|
2104
|
+
*,
|
|
2105
|
+
action: str,
|
|
2106
|
+
protocol: str,
|
|
2107
|
+
request: dict[str, Any],
|
|
2108
|
+
) -> dict[str, Any]:
|
|
2109
|
+
if self.sign_only:
|
|
2110
|
+
raise WalletBackendError("wdk_evm_local is configured as sign_only.")
|
|
2111
|
+
if not isinstance(request, dict):
|
|
2112
|
+
raise WalletBackendError("Uniswap liquidity request must be an object.")
|
|
2113
|
+
data = await self.client.post(
|
|
2114
|
+
"/v1/evm/uniswap/liquidity/send",
|
|
2115
|
+
{
|
|
2116
|
+
"walletId": self.wallet_id,
|
|
2117
|
+
"accountIndex": self.account_index,
|
|
2118
|
+
"network": self.network,
|
|
2119
|
+
"action": action,
|
|
2120
|
+
"protocol": protocol,
|
|
2121
|
+
"request": request,
|
|
2122
|
+
},
|
|
2123
|
+
)
|
|
2124
|
+
normalized = self._normalize_uniswap_liquidity_payload(data, action=action, protocol=protocol)
|
|
2125
|
+
normalized["from_address"] = await self.get_address()
|
|
2126
|
+
return normalized
|
|
2127
|
+
|
|
2031
2128
|
async def preview_evm_lifi_cross_chain_swap(
|
|
2032
2129
|
self,
|
|
2033
2130
|
*,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agent-wallet",
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.94",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-wallet",
|
|
3
3
|
"displayName": "Agent Wallet",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.94",
|
|
5
5
|
"description": "Claude Code bridge for the existing AgentLayer wallet runtime. Connects to Solana, Bitcoin, and EVM wallets without creating a new one.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AgentLayer"
|
package/package.json
CHANGED
|
@@ -26,6 +26,9 @@ MORPHO_API_BASE_URL=https://api.morpho.org/graphql
|
|
|
26
26
|
# directly (https://trade-api.gateway.uniswap.org/v1) for legacy/offline direct mode.
|
|
27
27
|
UNISWAP_API_KEY=
|
|
28
28
|
# UNISWAP_TRADING_API_BASE_URL= # defaults to PROVIDER_GATEWAY_URL/v1/evm/uniswap
|
|
29
|
+
# LP actions use the Liquidity API. Gateway mode is the default; set this only
|
|
30
|
+
# for direct API access (for example https://liquidity.api.uniswap.org).
|
|
31
|
+
# UNISWAP_LIQUIDITY_API_BASE_URL= # defaults to PROVIDER_GATEWAY_URL/v1/evm/uniswap/lp
|
|
29
32
|
UNISWAP_ROUTER_VERSION=2.0
|
|
30
33
|
# Optional per-network override; each value must exist in the wallet's reviewed
|
|
31
34
|
# execution profile and match the provider-gateway's per-chain configuration.
|
package/wdk-evm-wallet/README.md
CHANGED
|
@@ -187,6 +187,7 @@ Environment variables:
|
|
|
187
187
|
- `MORPHO_API_BASE_URL`
|
|
188
188
|
- `UNISWAP_API_KEY`
|
|
189
189
|
- `UNISWAP_TRADING_API_BASE_URL`
|
|
190
|
+
- `UNISWAP_LIQUIDITY_API_BASE_URL`
|
|
190
191
|
- `UNISWAP_ROUTER_VERSION`
|
|
191
192
|
- `UNISWAP_ROUTER_VERSION_BY_NETWORK`
|
|
192
193
|
- `UNISWAP_DEFAULT_SLIPPAGE_BPS`
|
|
@@ -214,6 +215,11 @@ Swap providers:
|
|
|
214
215
|
- `UNISWAP_API_KEY` is required for the Uniswap routes; it identifies the
|
|
215
216
|
integrator (this service), not an end user — swaps are scoped per request by the
|
|
216
217
|
active wallet address, so a single key never mixes users
|
|
218
|
+
- LP actions (`create`, `increase`, `decrease`, `claim_fees`) are exposed at
|
|
219
|
+
`/v1/evm/uniswap/liquidity/*` and use Uniswap's Liquidity API. The runtime
|
|
220
|
+
accepts transactions only for pinned V3/V4 PositionManager deployments on
|
|
221
|
+
Ethereum, Base, and Robinhood, refreshes the final LP transaction just before
|
|
222
|
+
signing, and executes only API-returned bounded approvals.
|
|
217
223
|
|
|
218
224
|
Gateway mode:
|
|
219
225
|
|
|
@@ -359,6 +359,13 @@ export function loadConfig(env = process.env) {
|
|
|
359
359
|
: "https://trade-api.gateway.uniswap.org/v1");
|
|
360
360
|
const uniswapViaGateway =
|
|
361
361
|
Boolean(gatewayBaseTrimmed) && uniswapTradingApiBaseUrl.startsWith(gatewayBaseTrimmed);
|
|
362
|
+
const uniswapLiquidityApiBaseUrl =
|
|
363
|
+
String(env.UNISWAP_LIQUIDITY_API_BASE_URL ?? "").trim() ||
|
|
364
|
+
(gatewayBaseTrimmed
|
|
365
|
+
? `${gatewayBaseTrimmed}/v1/evm/uniswap/lp`
|
|
366
|
+
: "https://liquidity.api.uniswap.org");
|
|
367
|
+
const uniswapLiquidityViaGateway =
|
|
368
|
+
Boolean(gatewayBaseTrimmed) && uniswapLiquidityApiBaseUrl.startsWith(gatewayBaseTrimmed);
|
|
362
369
|
|
|
363
370
|
return {
|
|
364
371
|
host,
|
|
@@ -392,6 +399,8 @@ export function loadConfig(env = process.env) {
|
|
|
392
399
|
lidoReferralAddress: String(env.LIDO_REFERRAL_ADDRESS ?? "").trim(),
|
|
393
400
|
uniswapTradingApiBaseUrl,
|
|
394
401
|
uniswapViaGateway,
|
|
402
|
+
uniswapLiquidityApiBaseUrl,
|
|
403
|
+
uniswapLiquidityViaGateway,
|
|
395
404
|
providerGatewayToken,
|
|
396
405
|
uniswapApiKey: String(env.UNISWAP_API_KEY ?? "").trim(),
|
|
397
406
|
uniswapRouterVersion: String(env.UNISWAP_ROUTER_VERSION ?? "").trim() || "2.0",
|
|
@@ -640,6 +640,18 @@ async function handleRequest(request, response) {
|
|
|
640
640
|
return sendJson(response, 200, { ok: true, data });
|
|
641
641
|
}
|
|
642
642
|
|
|
643
|
+
if (method === "POST" && url.pathname === "/v1/evm/uniswap/liquidity/quote") {
|
|
644
|
+
const body = await withResolvedNetwork(await withResolvedSeedOrAddress(await readJsonBody(request)));
|
|
645
|
+
const data = await service.quoteUniswapLiquidity(body);
|
|
646
|
+
return sendJson(response, 200, { ok: true, data });
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (method === "POST" && url.pathname === "/v1/evm/uniswap/liquidity/send") {
|
|
650
|
+
const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
|
|
651
|
+
const data = await service.sendUniswapLiquidity(body);
|
|
652
|
+
return sendJson(response, 200, { ok: true, data });
|
|
653
|
+
}
|
|
654
|
+
|
|
643
655
|
if (method === "POST" && url.pathname === "/v1/evm/transfer/quote") {
|
|
644
656
|
const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
|
|
645
657
|
const data = await service.quoteNativeTransfer(body);
|
|
@@ -23,6 +23,28 @@ const DEFAULT_LIFI_SLIPPAGE = 0.005;
|
|
|
23
23
|
const ALWAYS_DENIED_LIFI_BRIDGES = ["mayan"];
|
|
24
24
|
const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
25
25
|
const UNISWAP_SUPPORTED_CHAIN_IDS = { ethereum: 1, base: 8453, robinhood: 4663 };
|
|
26
|
+
const UNISWAP_LIQUIDITY_ACTIONS = new Set(["create", "increase", "decrease", "claim_fees"]);
|
|
27
|
+
const UNISWAP_LIQUIDITY_PROTOCOLS = new Set(["V3", "V4"]);
|
|
28
|
+
// LP API transactions are accepted only for these official per-chain position
|
|
29
|
+
// managers. Keeping the list here makes the local signer a narrow protocol
|
|
30
|
+
// integration rather than a generic calldata relay.
|
|
31
|
+
const UNISWAP_LIQUIDITY_POSITION_MANAGERS = {
|
|
32
|
+
ethereum: {
|
|
33
|
+
V3: "0xc36442b4a4522e871399cd717abdd847ab11fe88",
|
|
34
|
+
V4: "0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e",
|
|
35
|
+
},
|
|
36
|
+
base: {
|
|
37
|
+
V3: "0x03a520b32c04bf3beef7beb72e919cf822ed34f1",
|
|
38
|
+
V4: "0x7c5f5a4bbd8fd63184577525326123b519429bdc",
|
|
39
|
+
},
|
|
40
|
+
robinhood: {
|
|
41
|
+
V3: "0x73991a25c818bf1f1128deaab1492d45638de0d3",
|
|
42
|
+
V4: "0x58daec3116aae6d93017baaea7749052e8a04fa7",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const ERC20_APPROVE_INTERFACE = new Interface([
|
|
46
|
+
"function approve(address spender,uint256 amount) returns (bool)",
|
|
47
|
+
]);
|
|
26
48
|
// Every executable path is declared here rather than inferred from a Trading
|
|
27
49
|
// API response. A new chain/router version therefore requires an explicit,
|
|
28
50
|
// reviewed allow-list entry before it can receive a signed transaction.
|
|
@@ -925,6 +947,31 @@ function assertUniswapSupportedNetwork(network) {
|
|
|
925
947
|
return chainId;
|
|
926
948
|
}
|
|
927
949
|
|
|
950
|
+
function normalizeUniswapLiquidityAction(value) {
|
|
951
|
+
const action = String(value || "").trim().toLowerCase();
|
|
952
|
+
if (!UNISWAP_LIQUIDITY_ACTIONS.has(action)) {
|
|
953
|
+
throw new Error("Uniswap liquidity action must be create, increase, decrease, or claim_fees.");
|
|
954
|
+
}
|
|
955
|
+
return action;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function normalizeUniswapLiquidityProtocol(value) {
|
|
959
|
+
const protocol = String(value || "").trim().toUpperCase();
|
|
960
|
+
if (!UNISWAP_LIQUIDITY_PROTOCOLS.has(protocol)) {
|
|
961
|
+
throw new Error("Uniswap liquidity protocol must be V3 or V4.");
|
|
962
|
+
}
|
|
963
|
+
return protocol;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function getUniswapLiquidityPositionManager(network, protocol) {
|
|
967
|
+
assertUniswapSupportedNetwork(network);
|
|
968
|
+
const manager = UNISWAP_LIQUIDITY_POSITION_MANAGERS[network]?.[protocol];
|
|
969
|
+
if (!manager) {
|
|
970
|
+
throw new Error(`Uniswap ${protocol} liquidity is not configured for ${network}.`);
|
|
971
|
+
}
|
|
972
|
+
return manager;
|
|
973
|
+
}
|
|
974
|
+
|
|
928
975
|
function getUniswapNetworkExecutionProfile(network) {
|
|
929
976
|
const profile = UNISWAP_EXECUTION_PROFILES[network];
|
|
930
977
|
if (!profile) {
|
|
@@ -5743,6 +5790,304 @@ export class WdkEvmWalletService {
|
|
|
5743
5790
|
return payload;
|
|
5744
5791
|
}
|
|
5745
5792
|
|
|
5793
|
+
async #uniswapLiquidityApiRequest(action, body) {
|
|
5794
|
+
const normalizedAction = String(action || "").trim().toLowerCase();
|
|
5795
|
+
const headers = { "Content-Type": "application/json", Accept: "application/json" };
|
|
5796
|
+
if (this.config.uniswapLiquidityViaGateway) {
|
|
5797
|
+
const token = String(this.config.providerGatewayToken || "").trim();
|
|
5798
|
+
if (token) {
|
|
5799
|
+
headers.Authorization = `Bearer ${token}`;
|
|
5800
|
+
}
|
|
5801
|
+
} else {
|
|
5802
|
+
if (!this.config.uniswapApiKey) {
|
|
5803
|
+
throw createTaggedError(
|
|
5804
|
+
"UNISWAP_API_KEY is not configured. Set it, or route Uniswap through the provider gateway, to use liquidity operations.",
|
|
5805
|
+
"uniswap_api_key_missing",
|
|
5806
|
+
{ provider: "uniswap" }
|
|
5807
|
+
);
|
|
5808
|
+
}
|
|
5809
|
+
headers["x-api-key"] = this.config.uniswapApiKey;
|
|
5810
|
+
}
|
|
5811
|
+
const base = String(this.config.uniswapLiquidityApiBaseUrl).replace(/\/+$/, "");
|
|
5812
|
+
const suffix = this.config.uniswapLiquidityViaGateway ? `/${normalizedAction}` : `/lp/${normalizedAction}`;
|
|
5813
|
+
let response;
|
|
5814
|
+
try {
|
|
5815
|
+
response = await fetch(`${base}${suffix}`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
5816
|
+
} catch (error) {
|
|
5817
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5818
|
+
throw createTaggedError(`Uniswap liquidity API unavailable: ${message}`, "network_unavailable", {
|
|
5819
|
+
provider: "uniswap",
|
|
5820
|
+
action: normalizedAction,
|
|
5821
|
+
});
|
|
5822
|
+
}
|
|
5823
|
+
let payload;
|
|
5824
|
+
try {
|
|
5825
|
+
payload = await response.json();
|
|
5826
|
+
} catch {
|
|
5827
|
+
payload = null;
|
|
5828
|
+
}
|
|
5829
|
+
if (!response.ok || !payload || typeof payload !== "object") {
|
|
5830
|
+
throw createTaggedError(
|
|
5831
|
+
String(payload?.message || payload?.error || `Uniswap liquidity ${normalizedAction} failed with HTTP ${response.status}.`),
|
|
5832
|
+
"network_unavailable",
|
|
5833
|
+
{ provider: "uniswap", action: normalizedAction, httpStatus: response.status }
|
|
5834
|
+
);
|
|
5835
|
+
}
|
|
5836
|
+
return payload;
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5839
|
+
#buildUniswapLiquidityRequest({ action, protocol, address, request }) {
|
|
5840
|
+
const body = { ...(request && typeof request === "object" ? request : {}) };
|
|
5841
|
+
// Caller cannot select identity, chain or simulator behavior. These are set
|
|
5842
|
+
// locally to make every preview/send reproducible through the active wallet.
|
|
5843
|
+
body.walletAddress = address;
|
|
5844
|
+
body.protocol = protocol;
|
|
5845
|
+
body.simulateTransaction = true;
|
|
5846
|
+
delete body.signature;
|
|
5847
|
+
delete body.batchPermitData;
|
|
5848
|
+
delete body.v4BatchPermitData;
|
|
5849
|
+
delete body.v3NftPermitData;
|
|
5850
|
+
if (action === "claim_fees") {
|
|
5851
|
+
delete body.independentToken;
|
|
5852
|
+
}
|
|
5853
|
+
return body;
|
|
5854
|
+
}
|
|
5855
|
+
|
|
5856
|
+
#extractUniswapLiquidityTransaction(payload, action) {
|
|
5857
|
+
const field = action === "create" ? "create" : action === "increase" ? "increase" : action === "decrease" ? "decrease" : "claim";
|
|
5858
|
+
const tx = payload?.[field] || payload?.transaction;
|
|
5859
|
+
if (!tx || typeof tx !== "object") {
|
|
5860
|
+
throw createTaggedError("Uniswap liquidity API returned no executable transaction.", "network_unavailable", {
|
|
5861
|
+
provider: "uniswap",
|
|
5862
|
+
action,
|
|
5863
|
+
});
|
|
5864
|
+
}
|
|
5865
|
+
return tx;
|
|
5866
|
+
}
|
|
5867
|
+
|
|
5868
|
+
async #getUniswapLiquidityApprovals({ runtimeConfig, protocol, action, address, payload }) {
|
|
5869
|
+
if (action !== "create" && action !== "increase") {
|
|
5870
|
+
return [];
|
|
5871
|
+
}
|
|
5872
|
+
const lpTokens = [payload?.token0, payload?.token1]
|
|
5873
|
+
.filter((token) => token && typeof token === "object")
|
|
5874
|
+
// The LP API represents native ETH with the zero-address sentinel. It
|
|
5875
|
+
// cannot have an ERC-20 allowance, so exclude it from check_approval.
|
|
5876
|
+
.filter((token) => !isZeroAddress(String(token.tokenAddress || "")))
|
|
5877
|
+
.map((token) => {
|
|
5878
|
+
const amount = BigInt(assertNonNegativeBigIntString(token.amount, "lp token amount"));
|
|
5879
|
+
return {
|
|
5880
|
+
tokenAddress: normalizeAddress(String(token.tokenAddress || ""), "lp token address"),
|
|
5881
|
+
amount,
|
|
5882
|
+
};
|
|
5883
|
+
})
|
|
5884
|
+
// Concentrated positions can legitimately be single-sided while the
|
|
5885
|
+
// current price lies outside their selected range. A zero contribution
|
|
5886
|
+
// has no ERC-20 allowance requirement and must not block the LP action.
|
|
5887
|
+
.filter((token) => token.amount > 0n)
|
|
5888
|
+
.map((token) => ({ ...token, amount: token.amount.toString() }));
|
|
5889
|
+
if (!lpTokens.length) {
|
|
5890
|
+
return [];
|
|
5891
|
+
}
|
|
5892
|
+
const expectedTokens = new Set(
|
|
5893
|
+
lpTokens.map((token) => String(token.tokenAddress).toLowerCase())
|
|
5894
|
+
);
|
|
5895
|
+
const approvalPayload = await this.#uniswapLiquidityApiRequest("check_approval", {
|
|
5896
|
+
walletAddress: address,
|
|
5897
|
+
chainId: runtimeConfig.chainId,
|
|
5898
|
+
protocol,
|
|
5899
|
+
lpTokens,
|
|
5900
|
+
action: action.toUpperCase(),
|
|
5901
|
+
// The initial version uses ordinary, bounded approval transactions. This
|
|
5902
|
+
// keeps the send path observable and avoids requiring an extra signature
|
|
5903
|
+
// protocol before LP workflows have seen live use.
|
|
5904
|
+
generatePermitAsTransaction: true,
|
|
5905
|
+
});
|
|
5906
|
+
const transactions = Array.isArray(approvalPayload.transactions) ? approvalPayload.transactions : [];
|
|
5907
|
+
return transactions.map((item, index) => {
|
|
5908
|
+
const raw = item?.transaction;
|
|
5909
|
+
if (!raw || typeof raw !== "object") {
|
|
5910
|
+
throw createTaggedError("Uniswap liquidity approval response is malformed.", "uniswap_liquidity_invalid_approval", { index });
|
|
5911
|
+
}
|
|
5912
|
+
const to = normalizeAddress(String(raw.to || ""), "liquidity approval.to");
|
|
5913
|
+
if (!expectedTokens.has(to.toLowerCase())) {
|
|
5914
|
+
throw createTaggedError("Uniswap liquidity approval is for a token outside this LP request.", "uniswap_liquidity_unexpected_token", { to });
|
|
5915
|
+
}
|
|
5916
|
+
const data = assertNonEmptyString(String(raw.data || ""), "liquidity approval.data");
|
|
5917
|
+
if (parseHexOrDecimalBigInt(raw.value || "0", "liquidity approval.value") !== 0n) {
|
|
5918
|
+
throw createTaggedError("Uniswap liquidity approval must not transfer native value.", "uniswap_liquidity_invalid_approval", { to });
|
|
5919
|
+
}
|
|
5920
|
+
if (raw.chainId !== undefined && Number(raw.chainId) !== runtimeConfig.chainId) {
|
|
5921
|
+
throw createTaggedError("Uniswap liquidity approval has the wrong chain id.", "uniswap_liquidity_chain_mismatch");
|
|
5922
|
+
}
|
|
5923
|
+
if (raw.from && normalizeAddress(String(raw.from), "liquidity approval.from").toLowerCase() !== address.toLowerCase()) {
|
|
5924
|
+
throw createTaggedError("Uniswap liquidity approval sender does not match the active wallet.", "uniswap_liquidity_sender_mismatch");
|
|
5925
|
+
}
|
|
5926
|
+
let decoded;
|
|
5927
|
+
try {
|
|
5928
|
+
decoded = ERC20_APPROVE_INTERFACE.parseTransaction({ data });
|
|
5929
|
+
} catch {
|
|
5930
|
+
decoded = null;
|
|
5931
|
+
}
|
|
5932
|
+
if (!decoded || decoded.name !== "approve") {
|
|
5933
|
+
throw createTaggedError("Uniswap liquidity approval is not a bounded ERC-20 approve call.", "uniswap_liquidity_invalid_approval", { to });
|
|
5934
|
+
}
|
|
5935
|
+
const spender = normalizeAddress(String(decoded.args[0]), "liquidity approval spender").toLowerCase();
|
|
5936
|
+
const amount = BigInt(decoded.args[1]);
|
|
5937
|
+
const positionManager = getUniswapLiquidityPositionManager(runtimeConfig.network, protocol);
|
|
5938
|
+
if (spender !== positionManager && spender !== PERMIT2_ADDRESS.toLowerCase()) {
|
|
5939
|
+
throw createTaggedError("Uniswap liquidity approval has an unexpected spender.", "uniswap_liquidity_unexpected_spender", { spender, positionManager });
|
|
5940
|
+
}
|
|
5941
|
+
if (amount <= 0n || amount >= (2n ** 255n)) {
|
|
5942
|
+
throw createTaggedError("Uniswap liquidity approval must use a bounded amount.", "uniswap_liquidity_unbounded_approval", { spender });
|
|
5943
|
+
}
|
|
5944
|
+
return {
|
|
5945
|
+
tx: { to, data, value: 0n },
|
|
5946
|
+
token: to,
|
|
5947
|
+
spender,
|
|
5948
|
+
amount,
|
|
5949
|
+
action: String(item?.action || action).toUpperCase(),
|
|
5950
|
+
};
|
|
5951
|
+
});
|
|
5952
|
+
}
|
|
5953
|
+
|
|
5954
|
+
#validateUniswapLiquidityTransaction({ runtimeConfig, protocol, address, transaction }) {
|
|
5955
|
+
const to = normalizeAddress(String(transaction.to || ""), "liquidity transaction.to");
|
|
5956
|
+
const expectedManager = getUniswapLiquidityPositionManager(runtimeConfig.network, protocol);
|
|
5957
|
+
if (to.toLowerCase() !== expectedManager) {
|
|
5958
|
+
throw createTaggedError("Uniswap liquidity API returned an unexpected PositionManager.", "uniswap_unexpected_position_manager", {
|
|
5959
|
+
expected: expectedManager,
|
|
5960
|
+
actual: to.toLowerCase(),
|
|
5961
|
+
network: runtimeConfig.network,
|
|
5962
|
+
protocol,
|
|
5963
|
+
});
|
|
5964
|
+
}
|
|
5965
|
+
if (transaction.chainId !== undefined && Number(transaction.chainId) !== runtimeConfig.chainId) {
|
|
5966
|
+
throw createTaggedError("Uniswap liquidity transaction has the wrong chain id.", "uniswap_liquidity_chain_mismatch", {
|
|
5967
|
+
expected: runtimeConfig.chainId,
|
|
5968
|
+
actual: transaction.chainId,
|
|
5969
|
+
});
|
|
5970
|
+
}
|
|
5971
|
+
if (transaction.from && normalizeAddress(String(transaction.from), "liquidity transaction.from").toLowerCase() !== address.toLowerCase()) {
|
|
5972
|
+
throw createTaggedError("Uniswap liquidity transaction sender does not match the active wallet.", "uniswap_liquidity_sender_mismatch");
|
|
5973
|
+
}
|
|
5974
|
+
const data = assertNonEmptyString(String(transaction.data || ""), "liquidity transaction.data");
|
|
5975
|
+
if (!/^0x[0-9a-fA-F]+$/.test(data) || data.length < 10) {
|
|
5976
|
+
throw createTaggedError("Uniswap liquidity transaction calldata is invalid.", "uniswap_liquidity_invalid_calldata");
|
|
5977
|
+
}
|
|
5978
|
+
return { to, data, value: parseHexOrDecimalBigInt(transaction.value || "0", "liquidity transaction.value") };
|
|
5979
|
+
}
|
|
5980
|
+
|
|
5981
|
+
#formatUniswapLiquidityResponse({ runtimeConfig, accountIndex, address, action, protocol, request, payload, transaction, approvals = [], simulation = null }) {
|
|
5982
|
+
const tx = transaction ? {
|
|
5983
|
+
to: transaction.to,
|
|
5984
|
+
value: transaction.value.toString(),
|
|
5985
|
+
dataHash: sha256Hex(transaction.data),
|
|
5986
|
+
} : null;
|
|
5987
|
+
return {
|
|
5988
|
+
network: runtimeConfig.network,
|
|
5989
|
+
chainId: runtimeConfig.chainId,
|
|
5990
|
+
accountIndex,
|
|
5991
|
+
address,
|
|
5992
|
+
protocol: "uniswap",
|
|
5993
|
+
liquidityAction: action,
|
|
5994
|
+
liquidityProtocol: protocol,
|
|
5995
|
+
request,
|
|
5996
|
+
requestId: String(payload?.requestId || "").trim() || null,
|
|
5997
|
+
token0: payload?.token0 || null,
|
|
5998
|
+
token1: payload?.token1 || null,
|
|
5999
|
+
tickLower: payload?.tickLower ?? null,
|
|
6000
|
+
tickUpper: payload?.tickUpper ?? null,
|
|
6001
|
+
adjustedMinPrice: payload?.adjustedMinPrice ?? null,
|
|
6002
|
+
adjustedMaxPrice: payload?.adjustedMaxPrice ?? null,
|
|
6003
|
+
gasFee: payload?.gasFee ?? null,
|
|
6004
|
+
positionTokenId: String(request?.nftTokenId || request?.tokenId || "").trim() || null,
|
|
6005
|
+
positionManager: getUniswapLiquidityPositionManager(runtimeConfig.network, protocol),
|
|
6006
|
+
approvals: approvals.map((approval) => ({
|
|
6007
|
+
token: approval.token,
|
|
6008
|
+
spender: approval.spender,
|
|
6009
|
+
amount: approval.amount.toString(),
|
|
6010
|
+
action: approval.action,
|
|
6011
|
+
})),
|
|
6012
|
+
transaction: tx,
|
|
6013
|
+
simulation,
|
|
6014
|
+
source: "uniswap-liquidity-api",
|
|
6015
|
+
};
|
|
6016
|
+
}
|
|
6017
|
+
|
|
6018
|
+
async quoteUniswapLiquidity({ seedPhrase, address, action, protocol, request, accountIndex = 0, network }) {
|
|
6019
|
+
return this.#withReadableAccount({ seedPhrase, address, accountIndex, network }, async (account, runtimeConfig) => {
|
|
6020
|
+
assertUniswapSupportedNetwork(runtimeConfig.network);
|
|
6021
|
+
const normalizedAction = normalizeUniswapLiquidityAction(action);
|
|
6022
|
+
const normalizedProtocol = normalizeUniswapLiquidityProtocol(protocol);
|
|
6023
|
+
const walletAddress = await account.getAddress();
|
|
6024
|
+
const body = this.#buildUniswapLiquidityRequest({ action: normalizedAction, protocol: normalizedProtocol, address: walletAddress, request });
|
|
6025
|
+
body.chainId = runtimeConfig.chainId;
|
|
6026
|
+
const payload = await this.#uniswapLiquidityApiRequest(normalizedAction, body);
|
|
6027
|
+
const transaction = this.#validateUniswapLiquidityTransaction({
|
|
6028
|
+
runtimeConfig,
|
|
6029
|
+
protocol: normalizedProtocol,
|
|
6030
|
+
address: walletAddress,
|
|
6031
|
+
transaction: this.#extractUniswapLiquidityTransaction(payload, normalizedAction),
|
|
6032
|
+
});
|
|
6033
|
+
const approvals = await this.#getUniswapLiquidityApprovals({ runtimeConfig, protocol: normalizedProtocol, action: normalizedAction, address: walletAddress, payload });
|
|
6034
|
+
const simulation = await this.#simulatePreparedTransaction({ runtimeConfig, from: walletAddress, tx: transaction, operationLabel: "Uniswap liquidity" });
|
|
6035
|
+
return this.#formatUniswapLiquidityResponse({ runtimeConfig, accountIndex, address: walletAddress, action: normalizedAction, protocol: normalizedProtocol, request: body, payload, transaction, approvals, simulation });
|
|
6036
|
+
});
|
|
6037
|
+
}
|
|
6038
|
+
|
|
6039
|
+
async sendUniswapLiquidity({ seedPhrase, action, protocol, request, accountIndex = 0, network }) {
|
|
6040
|
+
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
6041
|
+
assertUniswapSupportedNetwork(runtimeConfig.network);
|
|
6042
|
+
const normalizedAction = normalizeUniswapLiquidityAction(action);
|
|
6043
|
+
const normalizedProtocol = normalizeUniswapLiquidityProtocol(protocol);
|
|
6044
|
+
const address = await account.getAddress();
|
|
6045
|
+
// Rebuild immediately before signing: price/ticks and calldata can change
|
|
6046
|
+
// while an intent approval is being reviewed.
|
|
6047
|
+
const body = this.#buildUniswapLiquidityRequest({ action: normalizedAction, protocol: normalizedProtocol, address, request });
|
|
6048
|
+
body.chainId = runtimeConfig.chainId;
|
|
6049
|
+
let payload = await this.#uniswapLiquidityApiRequest(normalizedAction, body);
|
|
6050
|
+
let transaction = this.#validateUniswapLiquidityTransaction({
|
|
6051
|
+
runtimeConfig,
|
|
6052
|
+
protocol: normalizedProtocol,
|
|
6053
|
+
address,
|
|
6054
|
+
transaction: this.#extractUniswapLiquidityTransaction(payload, normalizedAction),
|
|
6055
|
+
});
|
|
6056
|
+
const approvals = await this.#getUniswapLiquidityApprovals({ runtimeConfig, protocol: normalizedProtocol, action: normalizedAction, address, payload });
|
|
6057
|
+
const approvalResults = [];
|
|
6058
|
+
for (const approval of approvals) {
|
|
6059
|
+
const approvalSimulation = await this.#simulatePreparedTransaction({ runtimeConfig, from: address, tx: approval.tx, operationLabel: "Uniswap liquidity approval" });
|
|
6060
|
+
this.#assertSimulationSucceeded(approvalSimulation);
|
|
6061
|
+
const approvalResult = await this.#sendBufferedDefiTransaction({ account, runtimeConfig, from: address, tx: approval.tx, operationLabel: "Uniswap liquidity approval" });
|
|
6062
|
+
await this.#waitForTransactionReceipt(runtimeConfig, approvalResult.hash, { operationLabel: "Uniswap liquidity approval", failureCode: "uniswap_liquidity_approval_reverted", timeoutCode: "uniswap_liquidity_approval_timeout" });
|
|
6063
|
+
approvalResults.push({ token: approval.token, spender: approval.spender, amount: approval.amount.toString(), hash: approvalResult.hash });
|
|
6064
|
+
}
|
|
6065
|
+
if (approvals.length) {
|
|
6066
|
+
payload = await this.#uniswapLiquidityApiRequest(normalizedAction, body);
|
|
6067
|
+
transaction = this.#validateUniswapLiquidityTransaction({
|
|
6068
|
+
runtimeConfig,
|
|
6069
|
+
protocol: normalizedProtocol,
|
|
6070
|
+
address,
|
|
6071
|
+
transaction: this.#extractUniswapLiquidityTransaction(payload, normalizedAction),
|
|
6072
|
+
});
|
|
6073
|
+
}
|
|
6074
|
+
const simulation = await this.#simulatePreparedTransaction({ runtimeConfig, from: address, tx: transaction, operationLabel: "Uniswap liquidity" });
|
|
6075
|
+
this.#assertSimulationSucceeded(simulation);
|
|
6076
|
+
const result = await this.#sendBufferedDefiTransaction({ account, runtimeConfig, from: address, tx: transaction, operationLabel: "Uniswap liquidity" });
|
|
6077
|
+
await this.#waitForTransactionReceipt(runtimeConfig, result.hash, {
|
|
6078
|
+
operationLabel: "Uniswap liquidity",
|
|
6079
|
+
failureCode: "uniswap_liquidity_reverted",
|
|
6080
|
+
timeoutCode: "uniswap_liquidity_confirmation_timeout",
|
|
6081
|
+
});
|
|
6082
|
+
return {
|
|
6083
|
+
...this.#formatUniswapLiquidityResponse({ runtimeConfig, accountIndex, address, action: normalizedAction, protocol: normalizedProtocol, request: body, payload, transaction, approvals, simulation }),
|
|
6084
|
+
result,
|
|
6085
|
+
approvalResults,
|
|
6086
|
+
confirmed: true,
|
|
6087
|
+
};
|
|
6088
|
+
});
|
|
6089
|
+
}
|
|
6090
|
+
|
|
5746
6091
|
async #fetchUniswapQuote({ runtimeConfig, routerProfile, address, swapRequest }) {
|
|
5747
6092
|
const chainId = UNISWAP_SUPPORTED_CHAIN_IDS[runtimeConfig.network];
|
|
5748
6093
|
const quoteRequest = {
|