Using Azure Functions to Call an Ethereum Smart Contract

in #blockchain7 years ago (edited)

A guide to ingest Blockchain data using Azure's serverless infrastructure

I recently started mining Ether with a friend as a hobby.  Occasionally our mining rig would go down for miscellaneous reasons (power outage, network failure, Windows Defender targeting our mining client).  While the miner is down, we're losing potential profit.  To solve this, I built a miner monitor that will alert me in the case that the hashrate of the rig drops below a certain threshold.  This is a common problem for all miners, so I opened it up to the public.  To help fund the costs of the monitor, I incorporated a fuel token which gets burnt every time you get alerted.  You can replenish your fuel tokens using a smart contract with Ether.

Create your own miner monitor here

MinerMonitor

I had a particular problem where I wanted to use Azure Functions to create a cron job to query data from Ethereum and store it in a database.  Unfortunately, Azure Functions does not support Ethereum event triggers (yet!) so I had to setup an Azure Function job using a timer.  If you're not familiar with Azure Functions, check out my previous post to learn more.

My goal was to update a user's balance if they've made a deposit to a smart contract.  Let's walk through how it's done.  I'll assume that you've already setup your smart contract and have a basic understanding of Azure Functions.  

1. Create an Account on Infura

The first thing we'll want to do is go Infura and create an account:
infura.PNG

https://infura.io/signup

Since we don't want to run a light client in Azure, we'll rely upon an endpoint hosted by Infura to reach the Ethereum network.

2. Deploy Smart Contract

Collect the Smart Contract deployment address and ABI.  Remix is a great tool for deploying your smart contract and collecting this information.

3. Create Azure Function

Create a new Javascript Azure Function with a Storage Output.  Add Storage
This is where we will be storing the results from our blockchain function calls.  By creating this output object, we will get the storage connection string passed in as an environment variable.  The variable will be named the same as your storage account.  Try adding the following snippet to ensure this is setup correctly:

module.exports = function (context, myTimer) {
    context.log(process.env.YOUR_STORAGE_NAME);
    context.done();
}

4. Add NPM Modules

We'll need to manually install the npm modules to use them within the Azure Functions.

{
  "name": "tableupdatedelete",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "azure-storage": "^2.8.0",
    "web3": "0.20.5"
  }
}
  • Run npm install in the D:\home\site\wwwroot directory
    Debug Console
  • Verify that the install worked by adding require statements at the top of your script and try running it
var azure = require('azure-storage');
var Web3 = require("web3")

5. Add Your JavaScript

The full JavaScript can be found below.  I'll break down the various sections.

Setup Your Smart Contact

Test this code snippet by writing out the results to the context.log.  This code will create your contract JavaScript object and call a function in our smart contract titled balanceOf.

    web3 = new Web3(new Web3.providers.HttpProvider("https://kovan.infura.io/YOUR_INFURA_KEY"))
    // ABI from Remix
    var abi = [{ "constant": true, "inputs": [{ "name": "weiAmount", "type": "uint256" }], "name": "weiToToken", "outputs": [{ "name": "tokenAmount", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [{ "name": "tokenAmount", "type": "uint256" }], "name": "tokenToWei", "outputs": [{ "name": "weiAmount", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [{ "name": "_owner", "type": "string" }], "name": "balanceOf", "outputs": [{ "name": "balance", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "TotalFunds", "outputs": [{ "name": "balance", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "priceInWei", "type": "uint256" }], "name": "ChangePrice", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "anonymous": false, "inputs": [{ "indexed": false, "name": "_managementKey", "type": "string" }], "name": "Deposit", "type": "event" }, { "constant": false, "inputs": [], "name": "Withdrawal", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "name": "managementKey", "type": "string" }], "name": "addFuel", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function" }, { "inputs": [], "payable": false, "stateMutability": "nonpayable", "type": "constructor" }];
    // Replace with your Kovan address
    contractAddress = "0x3b9...5f4";
    var ClientReceipt = web3.eth.contract(abi);
    contract = ClientReceipt.at(contractAddress);
    // Call "balanceOf" function in smart contract
   // pass in string param: managementKey
   // interpret int256 result using
    contract.balanceOf(managementKey, function (scError, result) {
        context.log(result.toNumber());
    } 

Query Your Table

This snippet will query your table for all entities in a given partition and output some property.  In this example, I'm querying the users partition, and writing out the managementKey property.

let connectionString = process.env.YOUR_STORAGE_NAME;
let tableService = azure.createTableService(connectionString);
// I'm querying all rows from the 'users' partition
var query = new azure.TableQuery()
.where('PartitionKey eq ?', 'users');
tableService.queryEntities('YOUR_TABLE_NAME', query, null, function(queryError, queryResult, response) {
    if(!queryError) {
        for (var i = 0; i < queryResult.entries.length; i++) {
            var user = queryResult.entries[i];
            // Log row property "managementKey"
            context.log(user.managementKey._);
        }
    }
});

Update Your Table

Often you'll want to modify some record in the table based on the result from the smart contract function.  Here, I'm updating the user's balance and replacing the entity in the table.  Since the user object is the same object that we read from the table, it will contain appropriate RowKey, PartitionKey, and ETag.

user.accountBalance._ = balance;
user.disabled._ = false;
// Async call to update record
tableService.replaceEntity('YOUR_TABLE_NAME',  user, (error, result, response) => {
    if (!error) {
        resolve("Successfully updated user balance");
    }                    
    else {
        reject("Update record error: " + error);
    }
});

Putting it All Together

var azure = require('azure-storage');
var Web3 = require("web3")

module.exports = function (context, myTimer) {
    var timeStamp = new Date().toISOString();
    
    if(myTimer.isPastDue)
    {
        context.log('JavaScript is running late!');
    }
    context.log('JavaScript timer trigger function ran!', timeStamp);   
    
    // Setup smart contract
    web3 = new Web3(new Web3.providers.HttpProvider("https://kovan.infura.io/YOUR_INFURA_KEY"))
    var abi = [{ "constant": true, "inputs": [{ "name": "weiAmount", "type": "uint256" }], "name": "weiToToken", "outputs": [{ "name": "tokenAmount", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [{ "name": "tokenAmount", "type": "uint256" }], "name": "tokenToWei", "outputs": [{ "name": "weiAmount", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [{ "name": "_owner", "type": "string" }], "name": "balanceOf", "outputs": [{ "name": "balance", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "TotalFunds", "outputs": [{ "name": "balance", "type": "uint256" }], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [{ "name": "priceInWei", "type": "uint256" }], "name": "ChangePrice", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "anonymous": false, "inputs": [{ "indexed": false, "name": "_managementKey", "type": "string" }], "name": "Deposit", "type": "event" }, { "constant": false, "inputs": [], "name": "Withdrawal", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [{ "name": "managementKey", "type": "string" }], "name": "addFuel", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function" }, { "inputs": [], "payable": false, "stateMutability": "nonpayable", "type": "constructor" }];
    // Your Kovan address
    contractAddress = "0x3b9...05f4";
    var ClientReceipt = web3.eth.contract(abi);
    contract = ClientReceipt.at(contractAddress);

    let connectionString = process.env.YOUR_STORAGE_NAME;
    let tableService = azure.createTableService(connectionString);
    var query = new azure.TableQuery()
    .where('PartitionKey eq ?', 'users');
    tableService.queryEntities('YOUR_TABLE_NAME', query, null, function(queryError, queryResult, response) {
        if(!queryError) {
            var blockchainRequests = [];
            for (var i = 0; i < queryResult.entries.length; i++) {
                var user = queryResult.entries[i];
                blockchainRequests.push(new Promise(function(resolve, reject) { 
                    resolve(checkBlockchainForUpdate(context, tableService, user, contract))
                }));
            }
            // Resolve only once all requests have completed
            Promise.all(blockchainRequests).then(function(values) {
              context.log("blockchainRequests: " + values);
              context.done();
            });
        }
        else {
            context.log("Query error: " + queryError);
            context.done();
        }
    });
};

function checkBlockchainForUpdate(context, tableService, user, contract){
    var managementKey = user.managementKey._;
    // Async request to Ethereum contract
    return new Promise(function(resolve, reject) {
        contract.balanceOf(managementKey, function (scError, result) 
        { 
            if (scError){
                reject("BalanceOf error: " + scError);
            }
            else {
                // return the user balance
                resolve(result.toNumber());
            }
        });
    }).then(function(balance) {
        if (user.accountBalance._ != balance){
            return new Promise(function(resolve, reject) {
                context.log("Updating user's balance\n user:"+managementKey+", old balance:"+user.accountBalance._+", new balance:"+balance);
                user.accountBalance._ = balance;
                user.disabled._ = false;
                // Async call to update record
                tableService.replaceEntity('YOUR_TABLE_NAME', user, (error, result, response) => {
                    if (!error) {
                        resolve("Successfully updated user balance");
                    }                    
                    else {
                        reject("Update record error: " + error);
                    }
                });
            })
        }
        return Promise.resolve(managementKey+": Balance up to date");
    })
    .then(function(message) {
        return message;
    })
    .catch((err) => {
        return 'checkBlockchainForUpdate failed: ' + err;
    })
}

Conclusion

Although this application is not decentralized, the application is more valuable thanks to the ability to sync state with the blockchain.  100% of my users will have Ether (as miners), thus Ethereum provides a convenient and anonymous way to make small payments.  Pulling blockchain data into an Azure Table opens up some really cool integration options:

Sort:  

Great work!!