rss logo

How to Swap on the Gnosis Chain with the Web3.py Python library

Gnosis Chain Logo

⚠️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.

Create a 1inch account

1inch network Logo

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.

1inch.dev sing in portal
  • It will allow to get a API Key essential to make requests on the 1inch API:
1inch.dev portal with the API key

Install the Web3.py library

Python Logo

Note: For this example I used a fresh Debian installed machine.

  • Create a Python virtual space:
john@desktop:~$ python3 -m venv .web3
  • Load the Python virtual space:
john@desktop:~$ source .web3/bin/activate
  • Install the Web3.py library:
(.web3) john@desktop:~$ python3 -m pip install web3
  • Run Python:
(.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. >>>

Create a Wallet

  • Connect to the Python virtual space:
john@desktop:~$ source .web3/bin/activate (.web3) john@desktop:~$ python3
  • Import the Web3 library:
>>> from web3 import Web3 >>> w3 = Web3()
  • Create the wallet:
>>> acc = w3.eth.account.create()
  • Show the private key:
>>> w3.to_hex(acc._private_key)
  • Show the wallet address:
>>> acc.address
  • Now we have:
    • Our wallet address that we can share
    • Our private key that we need to keep… private…

Connect MetaMask to the Wallet

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.

  • From a preconfigured MetaMask click to current Account, then select Add account or hardware wallet, then Import account:
MetaMask steps for importing an account from a private key
  • From here just paste your private key and click Import:
Import private key step from MetaMask
  • Switch to the Gnosis Network:
MetaMask steps to switch to the Gnosis Network
  • From here, you can import your tokens with their respective addresses:
Import tokens link on the MetaMask Web Wallet

Using the 1inch API

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.

  • Things to know:
    • The Gnosis Chain number is: 100
    • The USDT token Gnosis address is: 0x4ECaBa5870353805a9F068101A40E0f32ed605C6
    • The USDC token Gnosis address is: 0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83
    • You can find more token addresses, for example, here: https://www.coingecko.com/ USDT token address on coingecko.com
    • Convert Ethereum addresses with Web3.to_checksum_address instruction when using the Web3.py library because Ethereum addresses are case sensitives. Ex: Web3.to_checksum_address("0x4ecaba5870353805a9f068101a40e0f32ed605c6")

Connect to the previously created wallet

The first step before interacting with the 1inch API is to connect to our previously created wallet.

  • Connect to the Python virtual space:
john@desktop:~$ source .web3/bin/activate (.web3) john@desktop:~$ python3
  • Import the Web3 library then the private key:
>>> from web3 import Web3 >>> private_key = "0xPRIVATE_KEY"
  • Show the wallet address:
>>> 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

Get balance

1inch API

We can use the 1inch API to check our wallet balance. Let's see how to do that.

  • Import requests module which allow to send HTTP requests using Python:
>>> import requests
  • Set variables:
>>> 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"
  • Show formated url:
>>> url https://api.1inch.dev/balance/v1.2/100/balances/0xWALLET_ADDRESS
  • Ask balance information to the 1inch API:
>>> response = requests.get(url, headers={'Authorization': f'Bearer {api_key}'})
  • Check request response:
>>> response.status_code #200 means good 200
  • Show the balance of every token:
>>> for token, balance in response.json().items(): ... print(f"{token}: {balance}") ... […] 0xc38e84bcc2d2693dd77d89f2b86a83e7fe98afa5: 0 0xaf204776c7245bf4147c2612bf6e5972ee483701: 0 0xce11e14225575945b8e6dc0d4f2dd4c570f79d9f: 0
  • Show non null balance only:
>>> 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

Alternative

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://rpc.ankr.com/gnosis")) >>> 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 :(
  • To obtain the balance of native tokens (Note: xDai for Gnosis):
>>> w3.eth.get_balance(wallet_address) 550838582408935942

Get token value

1inch API can give us the token value, let's see how to do that.

  • Import requests module which allow to send HTTP requests using Python:
>>> import requests
  • Set variables:
>>> 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"
  • Get USDT value:
>>> response = requests.post(url, headers={'Authorization': f'Bearer {api_key}'}, json={"tokens": f'{usdt_token}'}) >>> response.json() {'0x4ecaba5870353805a9f068101a40e0f32ed605c6': '1001591626048481422'}
  • From here we can convert our xDAI value to USDT:
>>> 550838582408935942/1001591626048481422 0.5499632465799719

Swap USDT to USDC

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.

Prerequisites

  • Import requests modules and Web3 library:
>>> import requests >>> from web3 import Web3
  • Connect to a Gnosis node and check connection:
>>> w3 = Web3(Web3.HTTPProvider("https://rpc.ankr.com/gnosis")) #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
  • Set variables:
>>> chainId = 100 #It's the Gnosis chain number, use 1 for ethereum >>> 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" }

Swap Parameters

  • Define swap parameters:
>>> swapParams = { ... "src": "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", #USDT ... "dst": "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", #USDC ... "amount": "10000", #0.01$ ... "from": wallet_address, ... "slippage": 1, ... "disableEstimate": False, ... "allowPartialFill": False ... }

Token Allowance

Pre-treatment

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'}
  • Reformat the transaction:

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

Add estimate gas

>>> w3.eth.estimate_gas(transaction) #show gas value 51759 >>> up = {'gas': w3.eth.estimate_gas(transaction)} >>> transaction.update(up) #add gas value inside transaction

Sign and send transaction

>>> signed_transaction = w3.eth.account.sign_transaction(transaction, private_key) #sign the transaction with the private key >>> signed_transaction["rawTransaction"] #show rawTransaction HexBytes('0x02b2a3acd005afb5d0c879683d6c49d321c78b7afb706618b12bfacf9c77521344679d7e0bfdf715908417d8ebe2eac74239f232f5ba4f3e2a32c2c80a493a03ed4b5cb68511a69116c10b3f48b166d219b469889652c61f2d45a2be4282b18b044c6976753796a50c17892902f44f86ed8e0d56fc706d7dbf2706c02f03517a4213e846dfa6028c5c03ee1d19b3838280326398f435331c457452a218eb8efa8aa064e3737750353a5604388c1c674ef0c8aa0c') >>> payload = { "rawTransaction": signed_transaction["rawTransaction"].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'}

Check and verify

Gnosisscan
The gnosisscan.io web page with a transaction hash in the search bar
  • And look for an Approve transaction:
The gnosisscan.io web page with an approved transaction
Revoke.cash
The revoke.cash web page with an approved token
1inch API
  • We can also use the 1inch API:
>>> 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'}

Do the Swap

Pre-treatment

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'}}
  • Reformat the transaction:
>>> 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"]}

Add estimate gas

>>> up = {'gas': w3.eth.estimate_gas(transaction), 'value': 0} >>> transaction.update(up)

Sign and send transaction

>>> signed_transaction = w3.eth.account.sign_transaction(transaction, private_key) #sign the transaction with the private key >>> payload = { "rawTransaction": signed_transaction["rawTransaction"].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'}
Gnosisscan
The gnosisscan.io web page with an Unoswap transaction
Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Contact :

contact mail address