diff --git a/README.md b/README.md index 0f04224b..891929b3 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Below is a list of existing and anticipated agents that AutoTx can use. If you'd | Agent | Description | Status | |-|-|-| | [Send Tokens](./autotx/agents/SendTokensAgent.py) | Send tokens (ERC20 & ETH) to a receiving address. | :rocket: | -| [Swap Tokens](./autotx/agents/SwapTokensAgent.py) | Swap from one token to another. Currently integrated with Uniswap. | :rocket: | +| [Swap Tokens](./autotx/agents/SwapTokensAgent.py) | Swap from one token to another. Currently integrated with Lifi. | :rocket: | | [Token Research](./autotx/agents/ResearchTokensAgent.py) | Research tokens, liquidity, prices, graphs, etc. | :rocket: | | Earn Yield | Stake assets to earn yield. | :memo: [draft](https://github.com/polywrap/AutoTx/issues/98) | | Bridge Tokens | Bridge tokens from one chain to another. | :memo: [draft](https://github.com/polywrap/AutoTx/issues/46) | diff --git a/autotx/agents/SwapTokensAgent.py b/autotx/agents/SwapTokensAgent.py index 3b6c0f63..5435e5aa 100644 --- a/autotx/agents/SwapTokensAgent.py +++ b/autotx/agents/SwapTokensAgent.py @@ -7,8 +7,6 @@ from autotx.utils.ethereum.eth_address import ETHAddress from autotx.utils.ethereum.lifi.swap import build_swap_transaction from autotx.utils.ethereum.networks import NetworkInfo -# from autotx.utils.ethereum.uniswap.swap import SUPPORTED_UNISWAP_V3_NETWORKS, build_swap_transaction -# from gnosis.eth import EthereumNetworkNotSupported as ChainIdNotSupported name = "swap-tokens" @@ -77,11 +75,6 @@ def get_tokens_address(token_in: str, token_out: str, network_info: NetworkInfo) token_in = token_in.lower() token_out = token_out.lower() - # if not network_info.chain_id in SUPPORTED_UNISWAP_V3_NETWORKS: - # raise ChainIdNotSupported( - # f"Network {network_info.chain_id.name.lower()} not supported for swap" - # ) - if token_in not in network_info.tokens: raise Exception(f"Token {token_in} is not supported in network {network_info.chain_id.name.lower()}") diff --git a/autotx/tests/agents/token/test_swap.py b/autotx/tests/agents/token/test_swap.py index 0d44b4f3..171486fb 100644 --- a/autotx/tests/agents/token/test_swap.py +++ b/autotx/tests/agents/token/test_swap.py @@ -29,7 +29,7 @@ def test_auto_tx_swap_native(configuration, auto_tx): auto_tx.run(prompt, non_interactive=True) new_balance = manager.balance_of(usdc_address) - + print(new_balance) assert balance + 100 <= new_balance def test_auto_tx_swap_multiple_1(configuration, auto_tx): diff --git a/autotx/tests/integration/test_lifi_swap.py b/autotx/tests/integration/test_lifi_swap.py deleted file mode 100644 index 43c07955..00000000 --- a/autotx/tests/integration/test_lifi_swap.py +++ /dev/null @@ -1,86 +0,0 @@ -import json -from autotx.utils.ethereum.eth_address import ETHAddress -from autotx.utils.ethereum.lifi.swap import build_swap_transaction -from autotx.utils.ethereum.networks import NetworkInfo - - -def test_swap_through_safe(configuration): - (_, _, client, manager) = configuration - network_info = NetworkInfo(client.w3.eth.chain_id) - - eth_address = ETHAddress(network_info.tokens["eth"]) - usdc_address = ETHAddress(network_info.tokens["usdc"]) - wbtc_address = ETHAddress(network_info.tokens["wbtc"]) - shib_address = ETHAddress(network_info.tokens["shib"]) - - balance = manager.balance_of(usdc_address) - print(balance) - - transactions = build_swap_transaction( - client, - 1, - eth_address, - usdc_address, - manager.address, - True, - network_info.chain_id, - ) - print(transactions[0].summary) - hash = manager.send_tx(transactions[0].tx) - manager.wait(hash) - balance = manager.balance_of(usdc_address) - print(balance) - - - transactions = build_swap_transaction( - client, - 0.01, - usdc_address, - wbtc_address, - manager.address, - False, - network_info.chain_id, - ) - print(transactions[0].summary) - hash = manager.send_tx(transactions[0].tx) - manager.wait(hash) - print(transactions[1].summary) - hash = manager.send_tx(transactions[1].tx) - manager.wait(hash) - balance = manager.balance_of(wbtc_address) - print(balance) - - # transactions = build_swap_transaction( - # client, - # 2000, - # usdc_address, - # wbtc_address, - # manager.address, - # True, - # network_info.chain_id, - # ) - - # hash = manager.send_tx(transactions[0].tx) - # manager.wait(hash) - # hash = manager.send_tx(transactions[1].tx) - # manager.wait(hash) - # balance = manager.balance_of(wbtc_address) - # print(balance) - - - # transactions = build_swap_transaction( - # client, - # 0.01, - # wbtc_address, - # shib_address, - # manager.address, - # True, - # network_info.chain_id, - # ) - # hash = manager.send_tx(transactions[0].tx) - # manager.wait(hash) - # hash = manager.send_tx(transactions[1].tx) - # manager.wait(hash) - # balance = manager.balance_of(shib_address) - # print(balance) - diff --git a/autotx/tests/integration/test_swap.py b/autotx/tests/integration/test_swap.py index db32a6ab..712542d4 100644 --- a/autotx/tests/integration/test_swap.py +++ b/autotx/tests/integration/test_swap.py @@ -1,121 +1,70 @@ -from autotx.utils.ethereum import ( - get_erc20_balance, - get_native_balance, -) -from autotx.utils.ethereum.networks import NetworkInfo from autotx.utils.ethereum.eth_address import ETHAddress -from autotx.utils.ethereum.uniswap.swap import build_swap_transaction +from autotx.utils.ethereum.lifi.swap import build_swap_transaction +from autotx.utils.ethereum.networks import NetworkInfo -def test_swap(configuration): - (user, _, client, _) = configuration +def test_swap_through_safe(configuration): + (_, _, client, manager) = configuration network_info = NetworkInfo(client.w3.eth.chain_id) - weth_address = ETHAddress(network_info.tokens["weth"]) - wbtc_address = ETHAddress(network_info.tokens["wbtc"]) - - user_addr = ETHAddress(user.address) - - balance = get_erc20_balance(client.w3, wbtc_address, user_addr) - assert balance == 0 - - txs = build_swap_transaction( - client, 0.05, weth_address.hex, wbtc_address.hex, user_addr.hex, False - ) - - for i, tx in enumerate(txs): - transaction = user.sign_transaction( - { - **tx.tx, - "nonce": client.w3.eth.get_transaction_count(user_addr.hex), - "gas": 200000, - } - ) - - hash = client.w3.eth.send_raw_transaction(transaction.rawTransaction) - - receipt = client.w3.eth.wait_for_transaction_receipt(hash) - if receipt["status"] == 0: - print(f"Transaction #{i} failed ") - break - - new_balance = get_erc20_balance(client.w3, wbtc_address, user_addr) - assert new_balance == 0.05 - -def test_swap_recieve_native(configuration): - (user, _, client, _) = configuration - - network_info = NetworkInfo(client.w3.eth.chain_id) eth_address = ETHAddress(network_info.tokens["eth"]) usdc_address = ETHAddress(network_info.tokens["usdc"]) - - user_addr = ETHAddress(user.address) - - balance = get_native_balance(client.w3, user_addr) - assert int(balance) == 9989 - - tx = build_swap_transaction( - client, 5, eth_address.hex, usdc_address.hex, user_addr.hex, True - ) - - transaction = user.sign_transaction( - { - **tx[0].tx, - "nonce": client.w3.eth.get_transaction_count(user_addr.hex), - "gas": 200000, - } + wbtc_address = ETHAddress(network_info.tokens["wbtc"]) + shib_address = ETHAddress(network_info.tokens["shib"]) + + usdc_balance = manager.balance_of(usdc_address) + assert usdc_balance == 0 + + sell_eth_for_usdc_transaction = build_swap_transaction( + client, + 1, + eth_address, + usdc_address, + manager.address, + True, + network_info.chain_id, ) - - hash = client.w3.eth.send_raw_transaction(transaction.rawTransaction) - - receipt = client.w3.eth.wait_for_transaction_receipt(hash) - - if receipt["status"] == 0: - print(f"Transaction to swap ETH -> USDC failed ") - - balance = get_native_balance(client.w3, user_addr) - assert int(balance) == 9984 - - txs = build_swap_transaction( - client, 4, usdc_address.hex, eth_address.hex, user_addr.hex, False + hash = manager.send_tx(sell_eth_for_usdc_transaction[0].tx) + manager.wait(hash) + usdc_balance = manager.balance_of(usdc_address) + assert usdc_balance > 2900 + + wbtc_balance = manager.balance_of(wbtc_address) + assert wbtc_balance == 0 + + buy_wbtc_with_usdc_transaction = build_swap_transaction( + client, + 0.01, + usdc_address, + wbtc_address, + manager.address, + False, + network_info.chain_id, ) - for i, tx in enumerate(txs): - transaction = user.sign_transaction( - { - **tx.tx, - "nonce": client.w3.eth.get_transaction_count(user_addr.hex), - "gas": 200000, - } - ) - - hash = client.w3.eth.send_raw_transaction(transaction.rawTransaction) - - receipt = client.w3.eth.wait_for_transaction_receipt(hash) - - if receipt["status"] == 0: - print(f"Transaction #{i} to swap USDC -> ETH failed ") - break - - balance = get_native_balance(client.w3, user_addr) - assert int(balance) == 9988 - -def test_swap_through_safe(configuration): - (_, _, client, manager) = configuration - - network_info = NetworkInfo(client.w3.eth.chain_id) - weth_address = ETHAddress(network_info.tokens["weth"]) - usdc_address = ETHAddress(network_info.tokens["usdc"]) - - balance = manager.balance_of(usdc_address) - assert balance == 0 - - txs = build_swap_transaction( - client, 6000, weth_address.hex, usdc_address.hex, manager.address.hex, False + hash = manager.send_tx(buy_wbtc_with_usdc_transaction[0].tx) + manager.wait(hash) + hash = manager.send_tx(buy_wbtc_with_usdc_transaction[1].tx) + manager.wait(hash) + wbtc_balance = manager.balance_of(wbtc_address) + assert wbtc_balance >= 0.01 + + shib_balance = manager.balance_of(shib_address) + assert shib_balance == 0 + + sell_wbtc_for_shib = build_swap_transaction( + client, + 0.005, + wbtc_address, + shib_address, + manager.address, + True, + network_info.chain_id, ) - - hash = manager.send_tx_batch(txs, require_approval=False) + hash = manager.send_tx(sell_wbtc_for_shib[0].tx) manager.wait(hash) - - new_balance = manager.balance_of(usdc_address) - assert new_balance == 6000 + hash = manager.send_tx(sell_wbtc_for_shib[1].tx) + manager.wait(hash) + shib_balance = manager.balance_of(shib_address) + shib_balance = manager.balance_of(shib_address) + assert shib_balance > 0 diff --git a/autotx/utils/ethereum/helpers/get_native_token_symbol.py b/autotx/utils/ethereum/helpers/get_native_token_symbol.py index 5ce5a703..07a316a0 100644 --- a/autotx/utils/ethereum/helpers/get_native_token_symbol.py +++ b/autotx/utils/ethereum/helpers/get_native_token_symbol.py @@ -19,4 +19,4 @@ def get_native_token_symbol(network: ChainId) -> str: if not native_token_symbol: raise Exception(f"Native token not found for network {network.name}") - return native_token_symbol + return native_token_symbol.upper() diff --git a/autotx/utils/ethereum/lifi/__init__.py b/autotx/utils/ethereum/lifi/__init__.py index 764e458c..bc8a0d2f 100644 --- a/autotx/utils/ethereum/lifi/__init__.py +++ b/autotx/utils/ethereum/lifi/__init__.py @@ -18,6 +18,7 @@ def get_quote( amount: int, _from: ETHAddress, chain: ChainId, + slippage: float ) -> dict[str, Any]: params = { "fromToken": from_token.hex, @@ -26,6 +27,7 @@ def get_quote( "fromAddress": _from.hex, "fromChain": chain.value, "toChain": chain.value, + "slippage": slippage } response = requests.get(cls.BASE_URL + "/quote", params=params) # type: ignore if response.status_code == 200: diff --git a/autotx/utils/ethereum/lifi/swap.py b/autotx/utils/ethereum/lifi/swap.py index 91eefff3..9e248ba1 100644 --- a/autotx/utils/ethereum/lifi/swap.py +++ b/autotx/utils/ethereum/lifi/swap.py @@ -9,6 +9,7 @@ from gnosis.eth import EthereumClient from web3.types import TxParams, Wei +SLIPPAGE = 0.01 # 1% def build_swap_transaction( ethereum_client: EthereumClient, @@ -31,11 +32,11 @@ def build_swap_transaction( token_out_price_in_usd = Lifi.get_token_price(token_out_address, chain) amount_token_to_buy = (token_out_price_in_usd * amount) / token_in_price_in_usd amount_in_integer = int(amount_token_to_buy * (10**decimals)) - # add slippage (default is 0.5%) to ensure we get the expected amount - amount_in_integer = int(amount_in_integer * 0.005 + amount_in_integer) + # add slippage plus 0.05% to ensure we get the expected amount + amount_in_integer = int(amount_in_integer * (SLIPPAGE + 0.005) + amount_in_integer) quote = Lifi.get_quote( - token_in_address, token_out_address, amount_in_integer, _from, chain + token_in_address, token_out_address, amount_in_integer, _from, chain, SLIPPAGE ) transactions: list[PreparedTx] = [] diff --git a/autotx/utils/ethereum/uniswap/__init__.py b/autotx/utils/ethereum/uniswap/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/autotx/utils/ethereum/uniswap/swap.py b/autotx/utils/ethereum/uniswap/swap.py deleted file mode 100644 index e8b8a270..00000000 --- a/autotx/utils/ethereum/uniswap/swap.py +++ /dev/null @@ -1,226 +0,0 @@ -from decimal import Decimal -from typing import Literal, Union -from gnosis.eth import EthereumClient -from gnosis.eth.oracles.uniswap_v3 import UniswapV3Oracle -import requests -from web3.types import Wei - -from autotx.utils.PreparedTx import PreparedTx -from autotx.utils.ethereum.constants import GAS_PRICE_MULTIPLIER, NATIVE_TOKEN_ADDRESS - -from autotx.utils.ethereum.erc20_abi import ERC20_ABI -from autotx.utils.ethereum.networks import ChainId, NetworkInfo -from autotx.utils.ethereum.weth_abi import WETH_ABI - -SLIPPAGE = 0.05 -SQRT_PRICE_LIMIT = 0 - -SUPPORTED_UNISWAP_V3_NETWORKS = { - ChainId.MAINNET: "https://api.thegraph.com/subgraphs/name/revert-finance/uniswap-v3-mainnet", - ChainId.OPTIMISM: "https://api.thegraph.com/subgraphs/name/revert-finance/uniswap-v3-optimism", - ChainId.ARBITRUM_ONE: "https://api.thegraph.com/subgraphs/name/revert-finance/uniswap-v3-arbitrum", - ChainId.BASE_MAINNET: "https://api.thegraph.com/subgraphs/name/revert-finance/uniswap-v3-base", - ChainId.POLYGON: "https://api.thegraph.com/subgraphs/name/revert-finance/uniswap-v3-polygon", - ChainId.SEPOLIA: "https://api.thegraph.com/subgraphs/name/revert-finance/uniswap-v3-mainnet", -} - -def get_swap_information( - amount: Decimal, - token_in_decimals: int, - token_out_decimals: int, - price: Decimal, - exact_input: bool, -) -> tuple[int, int, Union[Literal["exactInputSingle"] | Literal["exactOutputSingle"]]]: - if exact_input: - amount_compared_with_token = amount * price - minimum_amount_out = int(amount_compared_with_token * 10**token_out_decimals) - amount_out = int(minimum_amount_out - int(Decimal(minimum_amount_out) * Decimal(str(SLIPPAGE)))) - return (amount_out, int(amount * 10**token_in_decimals), "exactInputSingle") - else: - amount_compared_with_token = amount / price - max_amount_in = int(amount_compared_with_token * 10**token_in_decimals) - amount_in = int(max_amount_in + int(Decimal(max_amount_in) * Decimal(str(SLIPPAGE)))) - return ( - int(amount * 10**token_out_decimals), - amount_in, - "exactOutputSingle", - ) - -def get_best_fee_tier(token_in_address: str, token_out_address: str, subgraph_url: str) -> int: - token_in_lower = token_in_address.lower() - token_out_lower = token_out_address.lower() - reversed = token_in_lower > token_out_lower - - (token0, token1) = ( - (token_out_lower, token_in_lower) - if reversed - else (token_in_lower, token_out_lower) - ) - - data = { - "query": """ - query GetPools($token0: String, $token1: String) { - pools( - where: { - token0: $token0 - token1: $token1 - } - ) { - id - feeTier - sqrtPrice - liquidity - token0 { - id - symbol - } - token1 { - id - symbol - } - } - } - """, - "variables": {"token0": token0, "token1": token1}, - } - response = requests.post(url=subgraph_url, json=data) - - if response.status_code == 200: - if not "data" in response.json(): - raise Exception(f"Request failed with response: {response.json()}") - pools = response.json()["data"]["pools"] - - max_liquidity_pool = max(pools, key=lambda x: int(x["liquidity"])) - return int(max_liquidity_pool["feeTier"]) - else: - raise Exception(f"Request failed with status code: {response.status_code}") - -def build_swap_transaction( - etherem_client: EthereumClient, - amount: Decimal, - token_in_address: str, - token_out_address: str, - _from: str, - exact_input: bool, -) -> list[PreparedTx]: - amount_dec = Decimal(str(amount)) # Convert to Decimal to avoid floating point errors - - uniswap = UniswapV3Oracle(etherem_client) - web3 = etherem_client.w3 - - token_in_is_native = token_in_address == NATIVE_TOKEN_ADDRESS - token_out_is_native = token_out_address == NATIVE_TOKEN_ADDRESS - - token_in = web3.eth.contract( - address=uniswap.weth_address if token_in_is_native else web3.to_checksum_address(token_in_address), - abi=WETH_ABI if token_in_address == uniswap.weth_address or token_in_is_native else ERC20_ABI, - ) - - token_out = web3.eth.contract( - address=uniswap.weth_address if token_out_is_native else web3.to_checksum_address(token_out_address), - abi=WETH_ABI if token_out_address == uniswap.weth_address or token_out_is_native else ERC20_ABI, - ) - - transactions: list[PreparedTx] = [] - if token_in_is_native and token_out.address == uniswap.weth_address: - transactions.append( - PreparedTx( - f"Swap ETH to WETH", - token_out.functions.deposit().build_transaction( - { - "from": _from, - "gasPrice": Wei(int(web3.eth.gas_price * GAS_PRICE_MULTIPLIER)), - "gas": None, # type: ignore - "value": Wei(int(amount_dec * 10**18)), - } - ), - ) - ) - return transactions - - if token_out_is_native and token_in_address == uniswap.weth_address: - transactions.append( - PreparedTx( - f"Swap WETH to ETH", - token_out.functions.withdraw(int(amount_dec * 10**18)).build_transaction( - { - "from": _from, - "gasPrice": Wei(int(web3.eth.gas_price * GAS_PRICE_MULTIPLIER)), - "gas": None, # type: ignore - } - ), - ) - ) - return transactions - - price = uniswap.get_price(token_in.address, token_out.address) - price_dec = Decimal(str(price)) - - token_in_decimals = token_in.functions.decimals().call() - token_out_decimals = token_out.functions.decimals().call() - (amount_out, amount_in, method) = get_swap_information( - amount_dec, token_in_decimals, token_out_decimals, price_dec, exact_input - ) - - token_in_symbol = token_in.functions.symbol().call() - token_out_symbol = token_out.functions.symbol().call() - - if not token_in_is_native: - allowance = token_in.functions.allowance(_from, uniswap.router_address).call() - if allowance < amount_in: - tx = token_in.functions.approve( - uniswap.router_address, amount_in - ).build_transaction( - { - "from": _from, - "gasPrice": Wei(int(web3.eth.gas_price * GAS_PRICE_MULTIPLIER)), - } - ) - transactions.append( - PreparedTx( - f"Approve {Decimal(amount_in) / 10 ** token_in_decimals} {token_in_symbol} to Uniswap", - tx, - ) - ) - subgraph_url = SUPPORTED_UNISWAP_V3_NETWORKS[NetworkInfo(web3.eth.chain_id).chain_id] - - fee = get_best_fee_tier(token_in.address, token_out.address, subgraph_url) - swap_transaction = uniswap.router.functions[method]( # type: ignore - ( - token_in.address, - token_out.address, - fee, - _from, - amount_out if method == "exactOutputSingle" else amount_in, - amount_in if method == "exactOutputSingle" else amount_out, - SQRT_PRICE_LIMIT, - ) - ).build_transaction( - { - "value": amount_in if token_in_is_native else 0, - "gas": None, - "gasPrice": Wei(int(web3.eth.gas_price * GAS_PRICE_MULTIPLIER)), - } - ) - - token_in_symbol = "ETH" if token_in_is_native else token_in_symbol - token_out_symbol = "ETH" if token_out_is_native else token_out_symbol - - transactions.append( - PreparedTx( - f"Swap {Decimal(amount_in) / 10 ** token_in_decimals} {token_in_symbol} for {Decimal(amount_out) / 10 ** token_out_decimals} {token_out_symbol}", - swap_transaction, - ) - ) - - if token_out_is_native: - tx = token_out.functions.withdraw(amount_out).build_transaction( - { - "from": _from, - "gasPrice": Wei(int(web3.eth.gas_price * GAS_PRICE_MULTIPLIER)), - "gas": None, # type: ignore - } - ) - transactions.append(PreparedTx(f"Convert WETH to ETH", tx)) - - return transactions