rss logo

Comment effectuer un Swap de cryptos sur la chaîne Gnosis avec la bibliothèque Python Web3.py

Le logo de la Gnosis Chain

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

Créer un compte 1inch

Le logo de 1inch network

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.

Portail de connexion 1inch.dev
  • Cela permettra d'obtenir une clé API indispensable pour effectuer des requêtes vers l'API 1inch :
Portail 1inch.dev avec la clé API

Installer la bibliothèque Web3.py

Le logo de Python

Remarque : Pour cet exemple, j'ai utilisé une machine fraichement installée avec Debian.

  • Créer un espace virtuel Python :
john@desktop:~$ python3 -m venv .web3
  • Activer l'espace virtuel Python :
john@desktop:~$ source .web3/bin/activate
  • Installer la bibliothèque Web3.py :
(.web3) john@desktop:~$ python3 -m pip install web3
  • Mettre à jour la bibliothèque Web3.py :
(.web3) john@desktop:~$ python3 -m pip install --upgrade web3
  • Exécuter 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. >>>

Créer un Wallet

  • Se connecter à l'espace virtuel Python :
john@desktop:~$ source .web3/bin/activate (.web3) john@desktop:~$ python3
  • Importer la bibliothèque Web3 :
>>> from web3 import Web3 >>> w3 = Web3()
  • Créer un portefeuille :
>>> acc = w3.eth.account.create()
  • Afficher la clé privée associée :
>>> w3.to_hex(acc._private_key)
  • Afficher l'adresse du portefeuille :
>>> acc.address
  • Nous avons maintenant :
    • Notre adresse de portefeuille que nous pouvons partager
    • Notre clé privée que nous devons garder... privée...

Connecter MetaMask au Wallet

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.

  • À partir d'un MetaMask préconfiguré, cliquer sur le Compte actuel, puis sélectionner Ajouter un compte ou un portefeuille matériel, puis Importer un compte :
Étapes MetaMask pour importer un compte à partir d'une clé privée
  • À partir d'ici, simplement coller la clé privée et cliquer sur Importer :
Étape d'importation de clé privée depuis MetaMask
  • Basculer vers le Réseau Gnosis :
Étapes MetaMask pour basculer vers le réseau Gnosis
  • À partir d'ici, on peut importer nos jetons avec leurs adresses respectives :
Lien d'importation de jetons sur le portefeuille Web MetaMask

Utiliser l'API 1inch

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.

  • Choses à savoir :
    • Le numéro de la chaîne Gnosis est : 100
    • L'adresse Gnosis du jeton USDT est : 0x4ECaBa5870353805a9F068101A40E0f32ed605C6
    • L'adresse Gnosis du jeton USDC est : 0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83
    • On peut trouver plus d'adresses de jetons, par exemple, ici : https://www.coingecko.com/ Adresse du jeton USDT sur coingecko.com
    • Il faudra penser à convertir les adresses Ethereum avec l'instruction Web3.to_checksum_address lors de l'utilisation de la bibliothèque Web3.py car les adresses Ethereum sont sensibles à la casse. Exemple : Web3.to_checksum_address("0x4ecaba5870353805a9f068101a40e0f32ed605c6")

Se connecter au portefeuille précédemment créé

La première étape avant d'interagir avec l'API 1inch est de se connecter à notre portefeuille précédemment créé.

  • Se connecter à l'espace virtuel Python :
john@desktop:~$ source .web3/bin/activate (.web3) john@desktop:~$ python3
  • Et importer la bibliothèque Web3 puis la clé privée :
>>> from web3 import Web3 >>> private_key = "0xPRIVATE_KEY"
  • Afficher l'adresse du wallet :
>>> 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

Obtenir le solde de son Wallet

1inch API

Nous pouvons utiliser l'API 1inch pour récupérer le solde de notre portefeuille. Voyons comment faire cela.

  • Importer le module requests qui permet d'envoyer des requêtes HTTP en utilisant Python :
>>> import requests
  • Définir les 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"
  • Afficher l'url formatée :
>>> url https://api.1inch.dev/balance/v1.2/100/balances/0xWALLET_ADDRESS
  • Demander à l'API 1inch le solde du wallet:
>>> response = requests.get(url, headers={'Authorization': f'Bearer {api_key}'})
  • Vérification de la réponse à la demande :
>>> response.status_code #200 veut dire OK 200
  • Afficher le solde de chaque token :
>>> for token, balance in response.json().items(): ... print(f"{token}: {balance}") ... […] 0xc38e84bcc2d2693dd77d89f2b86a83e7fe98afa5: 0 0xaf204776c7245bf4147c2612bf6e5972ee483701: 0 0xce11e14225575945b8e6dc0d4f2dd4c570f79d9f: 0
  • Afficher uniquement les soldes non nuls :
>>> 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

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 :(
  • Obtenir le solde des jeteons natifs (Note: xDai pour Gnosis):
>>> w3.eth.get_balance(wallet_address) 550838582408935942

Obtenir la valeur d'une cryptomonnaie

L'API 1inch est capable de nous donner la valeur d'un token, voyons comment ça fonctionne.

  • Importer le module requests qui permet d'envoyer des requêtes HTTP en utilisant Python :
>>> import requests
  • Définir les variables :
>>> 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"
  • Obtenir la valeur de l'USDT:
>>> response = requests.post(url, headers={'Authorization': f'Bearer {api_key}'}, json={"tokens": f'{usdt_token}'}) >>> response.json() {'0x4ecaba5870353805a9f068101a40e0f32ed605c6': '1001591626048481422'}
  • Grace à cette information on peut en déduire le montant de nos xDAI en USDT:
>>> 550838582408935942/1001591626048481422 0.5499632465799719

Swapper des USDT contre des USDC

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.

Prérequis

  • Importer les modules requests et la bibliothèque Web3 :
>>> import requests >>> from web3 import Web3
  • Se connecter à un nœud Gnosis et vérifier la connexion :

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
  • Définir les variables:
>>> 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" }

Les Paramètres de Swap

  • Définir les paramètres du swap :
>>> swapParams = { ... "src": "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", #USDT ... "dst": "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", #USDC ... "amount": "10000", #0.01$ ... "from": wallet_address, ... "slippage": 1, ... "disableEstimate": False, ... "allowPartialFill": False ... }

Allocation de jetons

Prétraitement

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'}
  • Reformater la transaction :

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

Ajouter la valeur estimé du gas

>>> 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

Signer et envoyer 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'}

Contrôler et vérifier

Gnosisscan
La page web gnosisscan.io avec un hachage de transaction dans la barre de recherche
  • Et rechercher la transaction d'approbation :
The gnosisscan.io web page with an approved transaction
Revoke.cash
La page web revoke.cash avec un jeton approuvé
1inch API
  • On peut aussi ici utiliser l'API 1inch:
>>> 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'}

Réalisation du Swap

Prétraitement

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'}}
  • Reformater la 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"]}

Ajouter l'estimation de gas

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

Signature et envoie de la transaction

>>> 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'}

Gnosisscan

  • On pourra ici aussi vérifier la bonne prise en compte de la transaction depuis le site https://gnosisscan.io/ :
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