Swap tokens
In this tutorial we present how to swap tokens on Uniswap using the Ledger Enterprise API.
Requirements
To run these operations, you need to have access to your workspace's Ledger Authentication Module (LAM) and have the necessary credentials, i.e. an API account and API key (if applicable).
LAM = '<your_LAM_address>'
headers = {'X-Ledger-API-User': '<your_api_username>', 'X-Ledger-API-Key': '<your_api_key>'}
The account you will use swap tokens must have Smart Contract Interaction enabled and be configured so that the API user is authorized to create transaction requests from this particular account.
Swap contract and function
To swap tokens, we will interact with the Uniswap router contract on Polygon:
uniswap_contract_address = '0xE592427A0AEce92De3Edee1F18E0157C05861564'
This contract corresponds to the V3 of the Uniswap protocol. We will use the exactInputSingle
function which can be used to swap a fixed amount of one token for a maximum possible amount of another token. For more information about the specs, see the official documentation.
Define swap parameters
First, we need to define the parameters of the swap call we want to make. The exactInputSingle
function expects as input a ExactInputSingleParams
structure defining the parameters of the swap. A brief overview of the expected parameters is given below:
-
tokenIn
The contract address of the inbound token. -
tokenOut
The contract address of the outbound token. -
fee
The fee tier of the pool to use. -
recipient
the destination address of the outbound token -
deadline
: the unix time after which a swap will fail. -
amountOutMinimum
: we are setting to zero, but this is a significant risk in production. For a real deployment, this value should be calculated using our SDK or an onchain price oracle - this helps protect against getting an unusually bad price for a trade due to a front running sandwich or another type of price manipulation -
sqrtPriceLimitX96
: We set this to zero - which makes this parameter inactive. In production, this value can be used to set the limit for the price the swap will push the pool to, which can help protect against price impact or for setting up logic in a variety of price-relevant mechanisms.
In this tutorial, we want to swap some native MATIC
tokens against USDC
tokens. The addresses of the two token contracts are defined below:
WMATIC = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270'
USDC = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'
Remark
Do note that even though we swap native MATIC
tokens, the Uniswap contract expects the wrapped MATIC (wMATIC) address as address of the input token.
We will execute the swap from the Liquidity-Polygon
account on the Ledger Enterprise platform and want to swap 0.01
MATIC from the corresponding address:
account_name = 'Liquidity-Polygon'
account_address = '0x95dBFF72d215e4a0834Aed61A189882876116751'
amount_matic_to_swap = 0.01
We set the swap deadline to 10 minutes, meaning we will have 10 minutes to sign and broadcast the transaction before the swap request expires:
import time
deadline = int(time.time() + 60 * 10)
Finally, we gather all the swap parameters into a single dictionary, which follows the expected input structure:
swap_parameters = {
'tokenIn': WMATIC,
'tokenOut': USDC,
'fee': 3000,
'recipient': account_address,
'deadline': deadline,
'amountIn': int(amount_matic_to_swap * 1e18),
'amountOutMinimum': 0,
'sqrtPriceLimitX96': 0
}
To execute the swap, we now have two options:
- Option 1: provide the function name and arguments to sign the corresponding swap transaction.
- Option 2: compute the raw transaction from the Uniswap contract and sign the raw transaction data.
Option 1 - Sign function call with arguments
We can execute the swap transaction by simply providing the swap function name and corresponding arguments to the POST /transactions
endpoint. Below we define the parameters of the transaction we want to sign:
function_name = 'exactInputSingle'
function_arguments = {'params': swap_parameters}
tx_data = {
'account_name': account_name,
'amount': int(amount_matic_to_swap * 1e18),
'coin_fields': {
'smart_contract': {
'function_arguments': function_arguments,
'function_name': function_name,
}
},
'recipient': uniswap_contract_address,
'speed': 'NORMAL'
}
Before sending the transaction request, we can estimate the transaction fees and add the estimated fees limit to our transaction parameters:
import requests
fees_request = requests.post(LAM + '/transactions/fees', headers=headers, json=tx_data)
tx_data['max_fees'] = fees_request.json()['max_fees']
Finally, we create the transaction request using the Ledger Enterprise API:
tx = requests.post(LAM + '/transactions', headers=headers, json=tx_data)
print(tx.json())
which returns the following response:
{'account_id': 39,
'account_index': 1,
'amount': '10000000000000000',
'block': None,
'broadcast_on': None,
'coin_fields': {'gas_limit': '237376',
'gas_price': '343501468070',
'smart_contract': {'contract_data': '414bf3890000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000000000000000000000000000000000bb800000000000000000000000095dbff72d215e4a0834aed61a189882876116751000000000000000000000000000000000000000000000000000000006476dc3b000000000000000000000000000000000000000000000000002386f26fc1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
'contract_name': None,
'dapp': None,
'function_arguments': {'params': ['0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
'0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
3000,
'0x95dBFF72d215e4a0834Aed61A189882876116751',
1685511227,
10000000000000000,
0,
0]},
'function_name': 'exactInputSingle',
'smart_contract_interaction_type': 'UNKNOWN'},
'type': 'EthereumAndEvm'},
'confirmations': 0,
'created_by': 18,
'created_on': '2023-05-31T05:24:22.569083+00:00',
'currency': 'polygon',
'fees': None,
'id': 1111,
'interaction_type': 'UNKNOWN',
'last_request': 929,
'max_fees': '81208835364793600',
'metadata': None,
'min_confirmations': 30,
'notes': [{'content': '', 'title': ''}],
'recipient': '0xE592427A0AEce92De3Edee1F18E0157C05861564',
'senders': None,
'speed': 'NORMAL',
'status': 'PENDING_APPROVAL',
'tx_hash': None,
'type': 'SEND',
'uid': None}
Once all approvals have been received, one can fetch a confirmation of the transaction using the corresponding transaction id
, which will provide the transaction hash that we can use to check the transaction on a blockchain explorer:
tx_id = tx.json()['id']
tx_confirmation = requests.get(LAM + f'/transactions/{tx_id}', headers=headers)
tx_hash = tx_confirmation.json()['tx_hash']
print(f'Transaction on explorer: https://polygonscan.com/tx/{tx_hash}')
which returns: Transaction on explorer:
https://polygonscan.com/tx/0xe9c1e10752a00e5b97449c1c7f973d37078010b9d893b0d4b8f6e05a62fbba0d
Remark
Do note that in this particular case, as we swap native MATIC tokens, the corresponding amount of MATIC must provided as the transaction amount
and be paid as part of the transaction.
When swapping two ERC20 tokens, the amount
parameter must be set to 0
, and only gas fees must be paid for the swap.
Remark
Do also note that as we swap native MATIC tokens which we provide as the transaction payable amount, no token approval is required for the swap here.
When swapping two ERC20 tokens, the user should first provide approval for the input token spending. See the official documentation for more info on this.
Option 2 - Sign raw transaction data
Another way to execute the swap is to compute the raw swapping transaction using the Uniswap smart contract and sign the raw transaction using the Ledger Enterprise API.
For that we need to interact with the Uniswap Router contract to compute the swap transaction for our set of parameters. Below we connect to the router contract using the Web3
library:
from web3 import Web3
w3 = Web3(Web3.HTTPProvider('https://polygon-rpc.com'))
contract_addr = Web3.to_checksum_address(uniswap_contract_address)
abi = [{"inputs":[{"internalType":"address","name":"_factory","type":"address"}, ... ...] # Copy full ABI here - Can get ABI from contract page on polygonscan.
swap_contract = w3.eth.contract(contract_addr, abi=abi)
Then, we compute the raw transaction corresponding to our swap parameters:
swap_parameters['deadline'] = int(time.time() + 60 * 10) # don't forget to update the deadline paramater if expired.
raw_tx = swap_contract.functions.exactInputSingle(swap_parameters).build_transaction({
'value': int(amount_matic_to_swap * 1e18),
'from': account_address,
'nonce': w3.eth.get_transaction_count(account_address),
'gasPrice': Web3.to_wei('300', 'gwei') # adjust gas price if too low
})
print(raw_tx)
which returns the following raw transaction:
{'gas': 139633,
'chainId': 137,
'value': 10000000000000000,
'from': '0x95dBFF72d215e4a0834Aed61A189882876116751',
'nonce': 24,
'gasPrice': 300000000000,
'to': '0xE592427A0AEce92De3Edee1F18E0157C05861564',
'data': '0x414bf3890000000000000000000000000d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000000000000000000000000000000000bb800000000000000000000000095dbff72d215e4a0834aed61a189882876116751000000000000000000000000000000000000000000000000000000006476e3cc000000000000000000000000000000000000000000000000002386f26fc1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'}
We define our transaction parameters and set contract_data
as the raw transaction data computed beforehand:
tx_data = {
'account_name': account_name,
'amount': int(amount_matic_to_swap * 1e18),
'coin_fields': {
'smart_contract': {
'contract_data': raw_tx['data'],
}
},
'recipient': uniswap_contract_address,
'speed': 'NORMAL'
}
Finally we can create the transaction requests exactly like for Option 1:
fees_request = requests.post(LAM + '/transactions/fees', headers=headers, json=tx_data)
tx_data['max_fees'] = fees_request.json()['max_fees']
tx = requests.post(LAM + '/transactions', headers=headers, json=tx_data)