Using Azure Functions to Call an Ethereum Smart Contract
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
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:
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.
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.
- Navigate to the backend of your Azure Function app:
https://YOUR_AZURE_FUNCTION_NAME.scm.azurewebsites.net/DebugConsole - Select Debug Console -> CMD
- Create a file named package.json in
D:\home\site\wwwroot
directory - Add the following contents to install
azure-storage
andweb3
:
{
"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 theD:\home\site\wwwroot
directory
- 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:
- Anonymous payments (see above example)
- Machine learning and analytics on Blockchain data
- IOT integration
- Event ingestion using Event Hub or Service Bus
Great work!!