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

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
tx = requests.post(LAM + '/transactions', headers=headers, json=tx_data)

print(tx.json())

which returns the following response:

Copy
Copied
{'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:

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
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:

Copy
Copied
{'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:

Copy
Copied
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:

Copy
Copied
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)
Copyright © Ledger Enterprise Platform 2023. All right reserved.