⟴ This is a technical look at making trades with 1 Inch DEX using Web3 and Python. Feel free to start with a less technical overview or skip directly to the code base here. If you’re interested in a similar guide that uses Javascript have a look over here!

Part 1: 1 Inch DEX using Python & Web3 - Making Contract Calls

I’m going to walk through an example of how you can use Web3.Py to get quotes and make trades on 1 Inch Exchange. In the repository, there are examples of trading ETH <–> DAI|MCD.

In this first part of the guide, I’m going to walk through the steps we take to execute a contract call against the 1 Inch Split contract and retrieve a current quote for the price of 1 ETH in DAI on the exchange.

In the next part of the guide, I will take you through the steps to make a DAI–>SNX trade.

Part 1: Key Concepts

Setting up your Ethereum Account

As a starting point, to execute trades using these scripts, you’ll need to have an Ethereum account and the corresponding private key. While it seems likely that anyone reading this will already understand the risks associated with working directly with private keys, I’ll go ahead and throw this out there anyway. If someone gets access to this private key, they can access all coins in that account.

Using a private key is a quick, easy way to test and execute raw transactions using Python and Web3, but if you’re going to use these scripts in production, make sure you have a secure system for managing your private keys. Better yet, fork the repo and add support for other wallets :)

That said, you can use something like MyCrypto to generate an account and get your private key quickly. Make sure to download and verify and the desktop client instead of doing anything in the browser.

1) Set your Ethereum account

First, you will need to set the base account you’ll be using to trade. You can do that by setting an environment variable in the same terminal window you’re running the script from:

export BASE_ACCOUNT='0x7777777777777777777777777777777'

2) Set the private key

巩 - For this part of the guide it’s not required set your private key!

Like the base account, you can set this from the terminal as an environment variable in the directory you’re running the script from:

export PRIVATE_KEY="<your_private_key_goes_here>"

By adding a space before the export command, it will prevent the command from being saved into your bash history :)

Next, we’ll look more into what 1 Inch is, how it works, and how to set your Ethereum provider to interact with the network.

Trading with 1 Inch

1 Inch exchange is a DEX aggregator. When you make a trade on 1 Inch, the contracts will go and look for the best price across a growing number of different DEXs. While you can adjust both the distribution of your order and which exchanges it will use, you can use reasonable defaults instead. Having these features is excellent because while it’s easy to get started making trades, power users can also make tuning adjustments.

1 Inch does offer an API that will allow you to make programmatic trades quickly. You will see some functions for that in the script too. The part that interests me the most, though, and what we’re focused on in this guide, is using Web3.Py to make all interactions directly with the 1 Inch contracts on-chain.

Setting up the trade

1 Inch has elegantly abstracted away a great deal of the complexities and given us an excellent on-chain interface. You can find a copy of the contract on github here and on-chain at 1split.eth.

Most importantly, for what we’re doing, have a look directly at the interface contract functions:

contract IOneSplit is IOneSplitConsts {
    function getExpectedReturn(
        IERC20 fromToken,
        IERC20 toToken,
        uint256 amount,
        uint256 parts,
        uint256 disableFlags
    )
        public
        view
        returns(
            uint256 returnAmount,
            uint256[] memory distribution
        );

    function swap(
        IERC20 fromToken,
        IERC20 toToken,
        uint256 amount,
        uint256 minReturn,
        uint256[] memory distribution,
        uint256 disableFlags
    ) public payable;
}

Based on this, we can see there are two main interaction points we’ll be using. First, looking at the getExpectedReturn method we can see that it is set as public and view. View lets us know that we can make contract calls to the function without actually writing any data to the blockchain. We will start with this because we don’t have to worry about losing Ethereum on failed transactions if we make a mistake. As you might expect, the purpose of this method is to give us information on what we can expect a given trade to return!

Before we can make the contract call, we will make sure our connection to the Ethereum network is active.

3) Establish Ethereum Provider

The most common way to connect to the Ethereum network seems to be using an Infura node. While there is a lot of discussion about how Infura could be a single centralized point of failure, a full digression on that is beyond the scope of this guide.

What I will say is that if you’re looking for a cool way to run an Ethereum node, check out DAppNode. I have been running a DAppNode since 12/2019, and it is awesome. Either way, if you’re using Infura (and that’s what I’ve used for the guide) simply get your Infura project ID and set it in the script here:

eth_provider_url = 'https://mainnet.infura.io/v3/<your_Infura_project_id_here>'

When done it should look something like this:

eth_provider_url = 'https://mainnet.infura.io/v3/asdf345dfg435345345'

Now we will confirm our connection to the Ethereum network is active. In the top of the script there is a flag we will set to make sure we don’t accidentally send transactions:

change this:

production = True

to this:

production = False

Then you can run and test the script and know that you won’t be making any trades. By default, the script will create a contract call to the getExpectedReturn method to check what we can expect for a 1 ETH –> DAI trade.

$ python one_inch_trades.py

2020-04-19 10:31:39,969 - __main__ - INFO - 1 ETH = 176.746129364100033347 DAI on 1 Inch right now!

So we can see that it' working :) Let’s take a look at precisely what we did there.

# get price quote for 1 ETH in DAI right now

ethereum_price = one_inch_get_quote(ethereum, mcd_contract_address, Web3.toWei(1, 'ether'))

logger.info("1 ETH = {0} DAI on 1 Inch right now!".format(Web3.fromWei(ethereum_price[0], 'ether')))

First we call the function: one_inch_get_quote with three parameters. The first parameter represents the _from_token Address, the second is the _to_token Address, and the third is the _amount of the coin we want to trade.

What you’ll notice is that in this case, we’re passing the ethereum variable as our _from_token parameter. Earlier in script we can see that ethereum is set to 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE. What’s that all about?

Within an Ethereum contract that deals with ERC20 coins, it usually makes more sense to reference different tokens by their actual contract address (as opposed to a string or something). Because Ethereum is the mother of all ERC20 tokens and doesn’t have a contract address, 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE is commonly used in contracts to reference Ethereum. As we’ll see in a minute, this parameter maps to the IERC20 fromToken param in the 1 Inch split contract. Easy enough!

It’s also worth note, the guys at EthereumDevIO pointed out that you can also use 0x0 in favor of the longer 0xEeeee.. address :)

The second parameter is the _to_token and will map to the 1 Inch Split contract parameter IERC20 toToken. In this case, the important part is that we’re dealing with the ERC20 contract address for the token, not the tokens' name or something. In the script I’ve manually hardcoded the MCD/DAI contract address as can be seen here:

mcd_contract_address = Web3.toChecksumAddress('0x6b175474e89094c44da98b954eedeac495271d0f')  # DAI Token contract address

The third parameter is _amount or value. I’m pretty sure I shouldn’t be using those terms interchangeably, and I’ll try to get that cleaned up soon, but for now, that’s what you get :)

The _amount parameter is pretty straight forward except that I do want to point out I’ve chosen to use the base unit of Ether in the script as it’s more human-readable to me. At the lower levels and within contracts, the unit of Wei is used. As you can see, we use a built-in Web3 function to abstract away complexity and any floating-point style issues that could arise like this:

Web3.toWei(1, 'ether')

Making a contract call

In the example, we executed a contract call against the Ethereum blockchain (for free!) to get the current price of 1 ETH in DAI across a host of DEX’s using 1 Inch. Let’s take a look at how we made that call and the other parameters we sent with the request.

def one_inch_get_quote(_from_token, _to_token, _amount):
    '''
    Get quote data from one inch split contract using on-chain call
    '''
    # load our contract
    one_inch_join = web3.eth.contract(
        address=one_inch_split_contract, abi=one_inch_split_abi)

    # make call request to contract on the Ethereum blockchain
    contract_response = one_inch_join.functions.getExpectedReturn(
        _from_token, _to_token, _amount, 100, 0).call({'from': base_account})

    # logger.info("contract response: {0}".format(contract_response))
    return contract_response

First, we have to load up the contracts ABI. I’m going to skip a more in-depth explanation of whats going on there for now, but in it’s the simplest form, you can think of the ABI as a way for Web3 to know what functions are available on a given contract and what parameters they expect. As you can see early on in the script, we’ve loaded the different contract ABI files themselves like this:

one_inch_split_abi = json.load(open('abi/one_inch_split.json', 'r'))
mcd_abi = json.load(open('abi/mcd_join.json', 'r'))

From within our one_inch_get_quote method we can then use Web.Py to load up the contracts ABI:

one_inch_join = web3.eth.contract(
    address=one_inch_split_contract, abi=one_inch_split_abi)

Using the address parameter, we also tell Web3 what the contract address on-chain is.

From there we can simply make the the contract call using built in Web3 functions:

contract_response = one_inch_join.functions.getExpectedReturn(
        _from_token, _to_token, _amount, 100, 0).call({'from': base_account})

The final two parameters we can see are 100 and 0 these will map to the following fields in the 1 Inch Split contract:

uint256 parts

and

uint256 disableFlags

For more information on interacting with contracts using Web3, check out the documentation here.

That wraps up part one of the guide. In part two, we’ll dig into how we craft the raw transactions and approve the 1 Inch Split contract to spend our ERC20 tokens!