⚠️Avertissement : Les cryptomonnaies sont encore une technologie émergente, utilisez uniquement de l'argent que vous pouvez vous permettre de perdre. L'automatisation des transactions sur les blockchains peut être extrêmement dangereuse et source d'erreurs pouvant entraîner la perte totale des sommes investies. Assurez-vous de comprendre chaque action avant de suivre les étapes décrites ci-dessous.⚠️
Je cherchais un moyen de réaliser des échanges sur la chaîne Gnosis (qui est une sidechain Ethereum) via des scripts. Après quelques recherches, j'ai entendu parler de la bibliothèque Web3.py en Python, qui permet d'interagir avec un nœud Ethereum. J'ai ensuite découvert que l'agrégateur d'échanges https://app.1inch.io/ propose une API qui prend en charge Web3.py, et ils fournissent également une documentation ici : https://portal.1inch.dev ! Facile, n'est-il pas ? Eh bien… Malheureusement, la documentation est horrible et elle ne fonctionne tout simplement pas. (Si c'était le cas, cet article ne serait pas utile, n'est-ce pas ?). Je vais donc décrire ici comment réaliser des échanges sur la chaîne Gnosis en utilisant l'API 1inch.
Afin de pouvoir utiliser leur API, on devra se connecter depuis un compte GitHub ou Google sur le portail 1inch ici https://portal.1inch.dev/login.
Remarque : Pour cet exemple, j'ai utilisé une machine fraichement installée avec Debian.
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
Je recommande particulièrement (surtout au début) d'utiliser un Web Wallet en parallèle pour vérifier que tout ce que nous faisons en script est correct. Cela peut se faire avec MetaMask, par exemple, ou tout autre portefeuille. S'assurer simplement de télécharger un portefeuille de confiance depuis une source de confiance.
Pour rappel, nous aurons besoin de la clé d'API 1inch, voir ici pour savoir comment l'obtenir. Et voyons voir quelques exemples afin de comprendre comment interagir avec l'API 1inch et de nous familiariser avec elle.
La première étape avant d'interagir avec l'API 1inch est de se connecter à notre portefeuille précédemment créé.
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
Nous pouvons utiliser l'API 1inch pour récupérer le solde de notre portefeuille. Voyons comment faire cela.
>>> 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 veut dire OK
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
L'API 1inch ne liste pas tous les tokens, mais nous pouvons demander le solde d'un token précis directement avec la bibliothèque Web3.py.
>>> 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 #Je suis pauvre :(
>>> w3.eth.get_balance(wallet_address)
550838582408935942
L'API 1inch est capable de nous donner la valeur d'un token, voyons comment ça fonctionne.
>>> import requests
>>> chainId = 100 #C'est le numéro de la chaine Gnosis, utiliser 1 pour 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
Maintenant que nous sommes à l'aise avec l'interaction de notre portefeuille et de l'API 1inch, nous pouvons passer à l'étape suivante, qui est l'objectif initial de ce tutoriel : échanger un token contre un autre. Pour simplifier les choses, nous allons échanger une petite quantité d'USDT contre des USDC.
>>> import requests
>>> from web3 import Web3
Mise à jour du 2024.06 : J'ai tout récemment eu l'erreur : "too many arguments, want at most 1" lors de l'utilisation de la fonction estimate_gas, résolu en remplaçant le provider (voir https://chainlist.org/chain/100 pour d'autres alternatives)
>>> w3 = Web3(Web3.HTTPProvider("https://gnosis.drpc.org")) #préciser un nœud gnosis sur lequel se connecter, voir https://docs.gnosischain.com/tools/rpc/ pour une liste
>>> w3.is_connected() #Vérifier si l'on est correctement connecté au nœud, doit retourner la valeur True
>>> chainId = 100 #C'est le numéro de la chaine Gnosis, utiliser 1 pour ethereum, 137 pour 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
... }
Nous devons permettre au routeur 1inch d'accéder aux jetons dans notre portefeuille que nous voulons échanger. Pour ce faire, nous enverrons une requête HTTP avec les valeurs d'autorisation de jetons à l'API 1inch et elle nous renverra une valeur data à signer.
>>> 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'}
Nous devons reformater les valeurs envoyées car elles sont inutilisables dans leur état actuel.
>>> 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') #supprimer la valeur gasPrice
>>> w3.eth.estimate_gas(transaction) #afficher la valeur du gas
51759
>>> up = {'gas': w3.eth.estimate_gas(transaction)}
>>> transaction.update(up) #ajouter la valeur du gas estimé dans la transaction
>>> signed_transaction = w3.eth.account.sign_transaction(transaction, private_key) #signer la transaction avec la clé privée
>>> signed_transaction["raw_transaction"] #Afficher la transaction
HexBytes('0x02b2a3acd005afb5d0c879683d6c49d321c78b7afb706618b12bfacf9c77521344679d7e0bfdf715908417d8ebe2eac74239f232f5ba4f3e2a32c2c80a493a03ed4b5cb68511a69116c10b3f48b166d219b469889652c61f2d45a2be4282b18b044c6976753796a50c17892902f44f86ed8e0d56fc706d7dbf2706c02f03517a4213e846dfa6028c5c03ee1d19b3838280326398f435331c457452a218eb8efa8aa064e3737750353a5604388c1c674ef0c8aa0c')
>>> payload = { "rawTransaction": signed_transaction["raw_transaction"].hex()} #On définit les informations a envoyer à l'API
>>> url = f'https://api.1inch.dev/tx-gateway/v1.1/{chainId}/broadcast' #On paramétre l'url
>>> response = requests.post(url, json=payload, headers=headers) #On envoie les informations à l'API
>>> response.json() #Afficher le Hash de la 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'}
Tout d'abord, nous configurons l'URL avec les paramètres d'échange puis on demande à l'API 1inch les données d'échange que l'on devra renvoyer signées.
>>> 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() #print 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) #signer la transaction avec la clé privée
>>> payload = { "rawTransaction": signed_transaction["raw_transaction"].hex()} #On définit les informations a envoyer à l'API
>>> response = requests.post(f'https://api.1inch.dev/tx-gateway/v1.1/{chainId}/broadcast', json=payload, headers=headers) #On envoie les informations à l'API
>>> response.json() #Afficher le Hash de la transaction
{'transactionHash': '0xbabdab5472fd9f8458cb37bea01fa9005a64e481f3009e9dce8bcaa9b98aa829'}
Contact :