Querying smart contracts from clojurescript

Table of Contents

Introduction

This note describes a basic clojurescript program that interacts with the Ethereum blockchain. It is meant as a demonstration of how to use clojurescript and the cljs-web3-next library. The script connects to a web3 provider (which can be a local Ethereum node or remote service like infura). Then it queries a smart contract and returns some information about the state of the contract. For the example we look at the xKNCa contract and print out what is the current ratio of outstanding xKNCa tokens to KNC tokens staked by the contract at KyberDAO.

Software

We will need to install node.js and the clojure.org package. The other dependencies will be automatically downloaded when building our project. Clojure and node.js are available in the package repositories of common Linux distributions. Otherwise, follow the instructions at the links above. You can verify that node is installed by running node

$ node
> 

Test clojure by running clj:

$ clj                                             
Clojure 1.10.1
user=> 

Now that clj and node are installed, we can move on to putting together our project.

Getting an Infura key

Our program will be interacting with the Ethereum blockchain. One way to do this is by running an Ethereum node on your local machine or another machine you have access to. If this is the case then you can skip this section and modify core.cljs appropriately, replacing the Infura URL with the RPC address of your Ethereum node. For most people, this is impractical but fortunately there is a free service, infura that provides access to the blockchain. To will need to sign up, providing your email in order to get an API key. Specifically, for the code below you'll need what they refer to as the Project ID.

Example code

Create a new directory for the project. Let's call it web3contract. We will create the following files and folders.

$ tree
.
├── deps.edn
├── Makefile
├── src
│   └── hello_world
│       └── core.cljs
└── xknca.abi

2 directories, 5 files

There are a total of 4 files. Set their contents as follows:

The first file deps.edn specifies the dependencies our project will use.

{:deps {org.clojure/clojurescript {:mvn/version "1.10.758"}
	com.cognitect/transit-cljs {:mvn/version "0.8.264"}
	org.clojure/core.async {:mvn/version "1.3.610"}
	org.clojure/clojure {:mvn/version "1.10.1"}
	cljs-web3-next {:mvn/version "0.1.3"}}}

The Makefile simply has the command we will use for compiling the program.

all:
	clj -m cljs.main --target node --output-to main.js -c hello-world.core

The web3.js library needs to know the structure of the contract we will be interacting with in order to properly encode our query into the correct format for the Ethereum virtual machine. This is specified by xknca.abi.

[{"inputs":[{"internalType":"string","name":"_mandate","type":"string"},{"internalType":"address","name":"_kyberStakingAddress","type":"address"},{"internalType":"address","name":"_kyberProxyAddress","type":"address"},{"internalType":"address","name":"_kyberTokenAddress","type":"address"},{"internalType":"address","name":"_kyberDaoAddress","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"}],"name":"AddedToWhitelist","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"bool","name":"redeemedForKnc","type":"bool"},{"indexed":false,"internalType":"uint256","name":"burnAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"Burn","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"EthRewardClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"mintFee","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"burnFee","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"claimFee","type":"uint256"}],"name":"FeeDivisorsSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"ethAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"kncAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"FeeWithdraw","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"ethPayable","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"mintAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"MintWithEth","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"kncPayable","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"mintAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"MintWithKnc","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"}],"name":"PauserAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"}],"name":"PauserRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"}],"name":"RemovedFromWhitelist","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"TokenRewardClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"constant":false,"inputs":[{"internalType":"address","name":"_address","type":"address"}],"name":"addFallbackAllowedAddress","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_kyberfeeHandlerAddress","type":"address"},{"internalType":"address","name":"_tokenAddress","type":"address"}],"name":"addKyberFeeHandler","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"addPauser","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_address","type":"address"}],"name":"addToWhitelist","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"bool","name":"_reset","type":"bool"}],"name":"approveKyberProxyContract","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bool","name":"_reset","type":"bool"}],"name":"approveStakingContract","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"tokensToRedeemTwei","type":"uint256"},{"internalType":"bool","name":"redeemForKnc","type":"bool"},{"internalType":"uint256","name":"minRate","type":"uint256"}],"name":"burn","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"epoch","type":"uint256"},{"internalType":"uint256[]","name":"feeHandlerIndices","type":"uint256[]"},{"internalType":"uint256[]","name":"maxAmountsToSell","type":"uint256[]"},{"internalType":"uint256[]","name":"minRates","type":"uint256[]"}],"name":"claimReward","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"feeDivisors","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"feeStructure","outputs":[{"internalType":"uint256","name":"mintFee","type":"uint256"},{"internalType":"uint256","name":"burnFee","type":"uint256"},{"internalType":"uint256","name":"claimFee","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getAvailableKncBalanceTwei","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"enum xKNC.FeeTypes","name":"_type","type":"uint8"}],"name":"getFeeRate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getFundEthBalanceWei","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getFundKncBalanceTwei","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"isOwner","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"isPauser","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"_address","type":"address"}],"name":"isWhitelisted","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"knc","outputs":[{"internalType":"contract ERC20","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kyberDao","outputs":[{"internalType":"contract IKyberDAO","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"kyberFeeHandlers","outputs":[{"internalType":"contract IKyberFeeHandler","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kyberProxy","outputs":[{"internalType":"contract IKyberNetworkProxy","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kyberStaking","outputs":[{"internalType":"contract IKyberStaking","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"mandate","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"minRate","type":"uint256"}],"name":"mint","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"kncAmountTwei","type":"uint256"}],"name":"mintWithKnc","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_address","type":"address"}],"name":"removefromWhitelist","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"renouncePauser","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"_mintFee","type":"uint256"},{"internalType":"uint256","name":"_burnFee","type":"uint256"},{"internalType":"uint256","name":"_claimFee","type":"uint256"}],"name":"setFeeDivisors","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"unpause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256[]","name":"feeHandlerIndices","type":"uint256[]"},{"internalType":"uint256[]","name":"maxAmountsToSell","type":"uint256[]"},{"internalType":"uint256[]","name":"minRates","type":"uint256[]"}],"name":"unwindRewards","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"campaignID","type":"uint256"},{"internalType":"uint256","name":"option","type":"uint256"}],"name":"vote","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"withdrawFees","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]

Finally, we have our Clojurescript program core.cljs.

(ns hello-world.core
  (:require-macros  [cljs.core.async.macros :refer [go]])
  (:require [cljs-web3-next.core :as web3-core]
            [cljs-web3-next.eth :as web3-eth]
            [cljs-web3-next.utils :as web3-utils]
            [cognitect.transit :as transit]
            [cljs.core.async :refer [<!]]
            [cljs.core.async.interop :refer-macros [<p!]]
            [cljs.core.async :refer [go]]
            [cljs-web3-next.helpers :as web3-helpers]))

(def web3
  (web3-core/http-provider
   "https://mainnet.infura.io/v3/YOUR_KEY_HERE"))

(println "Connected. Getting xKNCa ratio")

(def fs (js/require "fs"))

(def abi (.readFileSync fs "xknca.abi" "utf8"))

(def r (transit/reader :json))

(def tabi (transit/read r abi))

(def contract
  (web3-eth/contract-at web3
                        (clj->js tabi)
                        "0xB088b2C7cE300f3fe679d471C2cE49dFE312Ce75"))

(go
  (let [xknca
        (<p! (web3-eth/contract-call contract :totalSupply [] {}))
        knc
        (<p! (web3-eth/contract-call contract :getFundKncBalanceTwei [] {}))]
    (println (/ knc xknca))))

Note that you'll need to replace the text YOUR_KEY_HERE in the URL passed to http-provider with the project ID you got from Infura.

Compiling the program

We can simple run make

$ make
clj -m cljs.main --target node --output-to main.js -c hello-world.core 
$

You may see a lot of lines of output the first time you execute this command, as clojure automatically downloads missing dependencies. This will produce the output file main.js

Running the code

To run the code, simple run the main.js file with node:

$ node main.js
Connected. Getting xKNCa ratio 
0.10126665565840529   

Note that the number may be different when you run the program as it updates every two weeks or so.

References

Date: 2020-09-12 Sat 00:00

Email: [email protected]

Validate