EOLink 0.1.4: Scripts

If you like this content consider tipping in ETH, LINK or an ERC20 stablecoin at this address: 0xf9f6849230cBe10200B43dB103511b778898e71C

Note, in this series I am using linux (Ubuntu 20.04) and Python 3.8 – some instructions will be specific to this OS/env

In all previous posts I have manually deployed, funded and transacted with smart contracts using the brownie console. However, for a more transparent and repeatable workflow, the same process can be scripted in Python files.

The project code can be found on my Github.

Deployment and funding contract

For the flood insurance project I decided to deploy and fund the contract in one single script. For brownie to run a script, it must a) be a Python file, and b) have a main() function.

A main() function can be thought of as the “control” function in a script – it does not need to be explicitly called, it will always execute when the script is run. Everything inside main() happens, anything outside of main() does not. We can define other functions outside of main(), but they will not execute unless they are called inside it.

The structure of the file will therefore be:

#!/usr/bin/python3
from brownie import FloodInsurance, accounts, config, network

def main():

    nLINK = 10 # how many LINk to send to contract
    account1 = load_account('main') # load account

    deploy_contract(account1)
    fund_contract(nLINK, account1)

    return


def load_account(accountName):

    account = accounts.load(accountName)

    return account



def deploy_contract(account1):

    print("HELLO!")


    return


def fund_contract(nLink, account1):

    print("GOODBYE!")

    return

For the deployment function I will need to define a wallet address to be the contract owner, and this wallet will need to have been prefunded with ETH for transaction gas and LINK for oracle gas. I have previously added my private keys to brownie accounts, so they can be loaded into the script using accounts.load(). This account will then be needed by both the deployment and funding functions. I will also need to define how many LINK to send to the contract in the fund_contract() function. These can be defined in main(). Before building the actual deployment and funding functions, this script can just be run to check the structure is correct. I expect the dummy deploy_contract() and fund_contract() functions to be called – I will know this has happened when I see “HELLO!” and then “GOODBYE!” in the console. To get this far, the account will have had to be correctly loaded.

The script has to be run from inside brownie (i.e. it can’t simply be run using $python deploy_contract.py in the terminal). To run the script:

$ brownie run ./scripts/deploy_contract.py --network development

>>> Enter the password to unlock this account:
>>> HELLO!
>>> GOODBYE!

The script was run using the network tag “development” which is a local Ganache chain. There is no need to deploy this to a testnet at this stage. The script loads in a password protected account so a password request will be displayed in the console. As expected the two strings were printed, indicating the functions were called. Now the function bodies can be fleshed out.

deploy_contract()

In brownie console, deploying a contract requires calling the deploy method on a contract saved in the /contracts folder. This is also true for the scripted version. First, we need to import the contract into the script. This is available from brownie. To grab the information about the oracle required for the contract constructor the configuration file must also be imported from brownie, along with the network and account information. These are all included in the import statement in the skeleton code above.

This means we can callt the deploy method on the contract object inside the script. The values for the four input variables (oracle address, jobID, fee, link address) are all grabbed from the config file. After adding a print statement to report a successful deployment, the deployment function looks like this:

def deploy_contract(account1):

    print("Deploying contract to {} network".format(network.show_active()))

    floodInsurance.deploy(
    # deploy with args required by contract's constructor func:
    # 1) oracle address
    # 2) jobID
    # 3) LINK fee to pay oracle
    # 4) LINK token address 
    # 5) customer address
    # 6) warningThreshold
    # values for args 1-5 are in brownie-config.yaml
    config["networks"][network.show_active()]["oracle"], 
    config["networks"][network.show_active()]["jobId"],
    config["networks"][network.show_active()]["fee"],
    config["networks"][network.show_active()]["link_token"],
    account2,
    3,
    {'from':account1}
    )
    
    # report
    print("Contract deployed to address: {}".format(floodInsurance[len(floodInsurance) - 1].address))

    return

This function will deploy the contract to the active network. Next, to fund the contract the address to which the contract is deployed is needed. Multiple deployments of a contract are managed in brownie by stacking them in a list with the most recent coming last, so the most recently deployed contract can be retrieved by grabbing the final index of the imported contract object:

contract = floodInsurance[len(floodInsurance) - 1]

This contract instance can now be used as the destination for a LINK transfer. To make the transfer the .transfer() method in the LINK token interface is used. Since the LINK token interface is saved in the /interfaces directory, it is visible to brownie. The transfer syntax is:

interface.LinkTokenInterface(LINK _address).transfer(destination_address, amount, sender)

The contract is the destination account. We have already defined a variable for the LINK amount in the main() function. In this example I sent 10 LINK, so nLink=10. However, to account for how Solidity deals with decimals, this value should be multiplied by 1e18 (equivalent to providing the ETH balance in wei). After adding a print statement to report the successful transfer, the fund_contract() looks like this:

def fund_contract(nLink, account1):

    # grab most recently deployed contract
    contract = floodInsurance[len(floodInsurance) - 1]

    # make transfer of LINK
    interface.LinkTokenInterface(
        config["networks"][network.show_active()]["link_token"]
    ).transfer(contract, nLink*1e18, {"from": account1})
    
    print("Funded {} with {} LINK".format(contract.address, nLINK))


    return

This script can now be run from brownie by opening a terminal, navigating to the project folder, activating the Python development environment where brownie is installed, and running:

$ brownie run ./scripts/deploy_contract.py

request_and_settle()

The deployed contract can request data from a Chainlink oracle, then transacts LINK tokens to one of two accounts depending upon the value the oracle returns. First, I will write a small function to check that the contract holds sufficent LINK tokens to pay the oracle, because if it does not the transaction will fail.

def hasEnoughLINK(contract):

    link_address = config["networks"][network.show_active()]["link_token"]
    link_fee = config["networks"][network.show_active()]["fee"]
    
    balance = interface.LinkTokenInterface(link_address).balanceOf(contract)

    assert balance > link_fee, "Contract cannot pay oracle gas, please send LINK"

    return

This function grabs the LINK token address and the oracle fee from the brownie-config.yaml file and uses them to check how many LINK tokens are held in the contract. If the amount is less than the oracle fee then it throws an exception and requests funds to be sent. If this function is being run immediately after running the deploy_contract() script above, then the contract should be holding 10 LINK. If this is being run separately and there is no LINK held in the contract, it can be sent easily from brownie console:

$ interface.LinkTokenInterface( '0xa36085F69e2889c224210F603D836748e7dC0088').transfer(contract,10*1e18,{'from':account1})123

Assuming the contract is sufficiently funded with LINK, the contract can be instructed to get the flood warning level data from the Chainlink oracle using the requestWarningLevel() function, and act on the data via the settleClaim() function.

def requestData(contract, insurerAccount):
    
    transaction = contract.requestWarningLevel({'from':insurerAccount})

    return 


def transferFunds(contract, insurerAccount):

    contract.settleClaim({'from':insurerAccount})

    return

Finally, it would be nice to see the balances of each of the three accounts involved in this contract so we can see where the payment was made to and verify that the transactions acted as expected (formal testing coming later).

def checkBalances(contract, insurerAccount, customerAccount):

    link_address = config["networks"][network.show_active()]["link_token"]
    insurerBalance = interface.LinkTokenInterface(link_address).balanceOf(insurerAccount)
    customerBalance = interface.LinkTokenInterface(link_address).balanceOf(customerAccount)
    contractBalance = interface.LinkTokenInterface(link_address).balanceOf(contract)

    print("**BALANCES**")
    print("Contract: {}".format(contractBalance/1e18))
    print("Customer: {}".format(customerBalance/1e18))
    print("Insurer: {}".format(insurerBalance/1e18))

    return

Running this script from the terminal should then return the following:

The initial balance in the insurer’s wallet was 78.4 LINK. 10 was sent to the contract. The oracle then delivered the flood warning level which was above the threshold for a payout, and the funds held in the contract were payed out to the customer, less 0.1 LINK for the oracle gas.

Summary

In this post I developed Python scripts for deploying and interacting with the smart contract on the Kova testnet. Scripting these actions is better practice than using the console because it is more repeatable and the full workflow can be archived along with the project code.

One thought on “EOLink 0.1.4: Scripts

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s