⚠️Disclaimer: Crypto-currencies are still a young technology, so only use money you can afford to lose. Automating transactions on blockchains can be extremely dangerous and a source of errors that can result in the total loss of invested sums. Make sure you understand every action before you take any of the steps described below.⚠️
I was looking for a way to make swaps on the Gnosis Chain (which is an Ethereum sidechain) via scripts. After some research, I heard about the Web3.py Python library that lets you interact with an Ethereum node. I then discovered that the exchange aggregator https://app.1inch.io/ provides an API which support Web3.py, and they also provide documentation here: https://portal.1inch.dev/! Easy money! Well… Unfortunately it's pretty awful and it's broken. (If it wasn't, this article wouldn't be useful, would it?) So, I'm going to describe here how to swap on the Gnosis Chain using the 1inch API.
To be able to use their API you need to sign in via a GitHub or a Google from the 1inch portal here: https://portal.1inch.dev/login.
Note: For this example I used a fresh Debian installed machine.
john@desktop:~$ python3 -m venv .web3
john@desktop:~$ source .web3/bin/activate
(.web3) john@desktop:~$ python3 -m pip install web3
(.web3) john@desktop:~$ python3 -m pip install --upgrade web3
(.web3) john@desktop:~$ python3
Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
john@desktop:~$ source .web3/bin/activate
(.web3) john@desktop:~$ python3
>>> from web3 import Web3
>>> w3 = Web3()
>>> acc = w3.eth.account.create()
>>> w3.to_hex(acc._private_key)
>>> acc.address
I particularly recommend (especially at the beginning) using a Web Wallet in parallel to check that everything we do is correct. This can be done using MetaMask, for example, or any other. Just make sure you download a trusted wallet from a trusted source.
As a reminder, we'll need the 1inch API key, see here to find out how to get it. And let's look at a few examples to see how to interact with 1inch API to get familiar with it.
The first step before interacting with the 1inch API is to connect to our previously created wallet.
john@desktop:~$ source .web3/bin/activate
(.web3) john@desktop:~$ python3
>>> from web3 import Web3
>>> private_key = "0xPRIVATE_KEY"
>>> from eth_account import Account
>>> from eth_account.signers.local import LocalAccount
>>> account: LocalAccount = Account.from_key(private_key)
>>> print(f"The wallet address is: {account.address}")
The wallet address is: 0xWALLET_ADDRESS
We can use the 1inch API to check our wallet balance. Let's see how to do that.
>>> import requests
>>> chainId = 100 #It's the Gnosis chain number, use 1 for ethereum
>>> wallet_address = account.address
>>> url = f'https://api.1inch.dev/balance/v1.2/{chainId}/balances/{wallet_address}'
>>> api_key = "1INCH_API_KEY"
>>> url
https://api.1inch.dev/balance/v1.2/100/balances/0xWALLET_ADDRESS
>>> response = requests.get(url, headers={'Authorization': f'Bearer {api_key}'})
>>> response.status_code #200 means good
200
>>> for token, balance in response.json().items():
... print(f"{token}: {balance}")
...
[…]
0xc38e84bcc2d2693dd77d89f2b86a83e7fe98afa5: 0
0xaf204776c7245bf4147c2612bf6e5972ee483701: 0
0xce11e14225575945b8e6dc0d4f2dd4c570f79d9f: 0
>>> for token, balance in response.json().items():
... if balance != "0":
... print(f"{token}: {balance}")
...
0xddafbb505ad214d7b80b1f830fccc89b60fb7a83: 1100000 #USDC token
0x4ecaba5870353805a9f068101a40e0f32ed605c6: 210038 #USDT token
0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee: 550838582408935942 #xDai token
The 1inch API doesn't list all tokens, but we can request the balance of a specific token directly with the Web3.py library.
>>> import json
>>> from web3 import Web3
>>> wallet_address = "0xWALLET_ADDRESS"
>>> w3 = Web3(Web3.HTTPProvider("https://gnosis.drpc.org"))
>>> usdc_token = Web3.to_checksum_address("0x4ecaba5870353805a9f068101a40e0f32ed605c6")
>>> token_t_abi = json.loads('[{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]')
>>> token = w3.eth.contract(address=usdc_token, abi=token_t_abi)
>>> token.functions.balanceOf(wallet_address).call()
50286 #I'm poor :(
>>> w3.eth.get_balance(wallet_address)
550838582408935942
1inch API can give us the token value, let's see how to do that.
>>> import requests
>>> chainId = 100 #It's the Gnosis chain number, use 1 for ethereum
>>> url = f'https://api.1inch.dev/price/v1.1/{chainId}'
>>> api_key = "1INCH_API_KEY"
>>> usdt_token = "0x4ECaBa5870353805a9F068101A40E0f32ed605C6"
>>> response = requests.post(url, headers={'Authorization': f'Bearer {api_key}'}, json={"tokens": f'{usdt_token}'})
>>> response.json()
{'0x4ecaba5870353805a9f068101a40e0f32ed605c6': '1001591626048481422'}
>>> 550838582408935942/1001591626048481422
0.5499632465799719
Now that we're comfortable with the interaction between our wallet and the 1inch API, we can move on to the next step, which is the objective of this tutorial: swapping one token to another. To make the things simple, we're going to swap a small quantity of USDT tokens for USDC.
>>> import requests
>>> from web3 import Web3
2024.06 Update: I recently had an error: "too many arguments, want at most 1" when using the estimate_gas function, resolved by replacing the provider (see https://chainlist.org/chain/100 for others alternatives)
>>> w3 = Web3(Web3.HTTPProvider("https://gnosis.drpc.org")) #specify a gnosis node to connect to, see https://docs.gnosischain.com/tools/rpc/ for more
>>> w3.is_connected() #Check if well connected to the node, should return True
>>> chainId = 100 #It's the Gnosis chain number, use 1 for ethereum, 137 for Polygon etc…
>>> api_key = "1INCH_API_KEY"
>>> wallet_address = "0xWALLET_ADDRESS"
>>> private_key = "0xPRIVATE_KEY"
>>> usdt_token = "0x4ECaBa5870353805a9F068101A40E0f32ed605C6"
>>> usdc_token = "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83"
>>> headers = { "Authorization": f'Bearer {api_key}', "accept": "application/json" }
>>> swapParams = {
... "src": "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", #USDT
... "dst": "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", #USDC
... "amount": "10000", #0.01$
... "from": wallet_address,
... "slippage": 1,
... "disableEstimate": False,
... "allowPartialFill": False
... }
We need to allow the 1inch router to access to the tokens within our wallet we want to swap. To do so we will send a http request with token allowance values to the 1inch API and it will send us back a data value to sign.
>>> url = f'https://api.1inch.dev/swap/v5.2/{chainId}/approve/transaction?tokenAddress={swapParams["src"]}&amount={swapParams["amount"]}'
>>> url
'https://api.1inch.dev/swap/v5.2/100/approve/transaction?tokenAddress=0x4ECaBa5870353805a9F068101A40E0f32ed605C6&amount=10000'
>>> response = requests.get(url, headers=headers)
>>> response.json()
{'data': '0x095ea7b30000000000000000000000001111111263eec25477b68fb85ef929f73d9955120000000000000000000000000000000000000000000000000000000000001620', 'gasPrice': '50041321313', 'to': '0x4ecaba5870353805a9f068101a40e0f32ed605c6', 'value': '0'}
We need to reformat values sent because it's unusable in its current state.
>>> transaction = response.json()
>>> up = {'to': Web3.to_checksum_address(swapParams["src"]), 'from': wallet_address, 'chainId': chainId, 'value': int(transaction["value"]), 'maxFeePerGas': int(transaction["gasPrice"]), 'maxPriorityFeePerGas': int(transaction["gasPrice"]), 'nonce': w3.eth.get_transaction_count(wallet_address)}
>>> transaction.update(up)
>>> transaction.pop('gasPrice') #remove gasPrice value
>>> w3.eth.estimate_gas(transaction) #show gas value
51759
>>> up = {'gas': w3.eth.estimate_gas(transaction)}
>>> transaction.update(up) #add gas value inside transaction
>>> signed_transaction = w3.eth.account.sign_transaction(transaction, private_key) #sign the transaction with the private key
>>> signed_transaction["raw_transaction"] #show rawTransaction
HexBytes('0x02b2a3acd005afb5d0c879683d6c49d321c78b7afb706618b12bfacf9c77521344679d7e0bfdf715908417d8ebe2eac74239f232f5ba4f3e2a32c2c80a493a03ed4b5cb68511a69116c10b3f48b166d219b469889652c61f2d45a2be4282b18b044c6976753796a50c17892902f44f86ed8e0d56fc706d7dbf2706c02f03517a4213e846dfa6028c5c03ee1d19b3838280326398f435331c457452a218eb8efa8aa064e3737750353a5604388c1c674ef0c8aa0c')
>>> payload = { "rawTransaction": signed_transaction["raw_transaction"].hex()} #build the payload to send
>>> url = f'https://api.1inch.dev/tx-gateway/v1.1/{chainId}/broadcast' #Format the url
>>> response = requests.post(url, json=payload, headers=headers) #Send payload to API
>>> response.json() #show Hash transaction
{'transactionHash': '0x9f88569114b0fee6ff2d1499047cb7790fac5b1c23a063eb1a48594f64bdbbc6'}
>>> url = f'https://api.1inch.dev/swap/v5.2/{chainId}/approve/allowance?tokenAddress={swapParams["src"]}&walletAddress={wallet_address}'
>>> response = requests.get(url, headers={'Authorization': f'Bearer {api_key}'})
>>> response.json()
>>> {'allowance': '10000'}
First, we are setting up the url with the swap parameters and request the swap data from the 1inch API.
>>> url = f'https://api.1inch.dev/swap/v5.2/{chainId}/swap?src={swapParams["src"]}&dst={swapParams["dst"]}&amount={swapParams["amount"]}&from={wallet_address}&slippage={swapParams["slippage"]}&disableEstimate={swapParams["disableEstimate"]}&allowPartialFill={swapParams["allowPartialFill"]}'
>>> response = requests.get(url, headers=headers)
>>> response.json() #show response
{'toAmount': '10000', 'tx': {'from': '0xWALLET_ADDRESS', 'to': '0x1111111254eeb25477b68fb85ed929f73a960582', 'data': '0x0502b1c5000000000000000000000000ddafbb505ad214d7b80b1f830fccc89b60fb7a83000000000000000000000000000000000000000000000000000000000000162000000000000000000000000000000000000000000000000000000000000026ac0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000180000000000000003c6c004074c2eff722017ad7c142476f545a051084da2c428b1bcac8', 'value': '0', 'gas': 147876, 'gasPrice': '12282946363'}}
>>> raw = response.json()
>>> transaction = {'to': Web3.to_checksum_address(raw.get('tx', None)["to"]), 'from': Web3.to_checksum_address(raw.get('tx', None)["from"]), 'chainId': chainId, 'maxFeePerGas': int(raw.get('tx', None)["gasPrice"]), 'maxPriorityFeePerGas': int(raw.get('tx', None)["gasPrice"]), 'nonce': w3.eth.get_transaction_count(wallet_address), 'data': raw.get('tx', None)["data"]}
>>> up = {'gas': w3.eth.estimate_gas(transaction), 'value': 0}
>>> transaction.update(up)
>>> signed_transaction = w3.eth.account.sign_transaction(transaction, private_key) #sign the transaction with the private key
>>> payload = { "rawTransaction": signed_transaction["raw_transaction"].hex()} #build the payload to send
>>> response = requests.post(f'https://api.1inch.dev/tx-gateway/v1.1/{chainId}/broadcast', json=payload, headers=headers) #send the transaction
>>> response.json() #show Hash transaction
{'transactionHash': '0xbabdab5472fd9f8458cb37bea01fa9005a64e481f3009e9dce8bcaa9b98aa829'}
Contact :