/district-server-smart-contracts

⚠️ This code now resides at d0x monorepo

Primary LanguageClojureEclipse Public License 1.0EPL-1.0

district-server-smart-contracts

CircleCI

Clojurescript-node.js mount module for a district server, that takes care of smart-contracts loading, deployment, function calling and event handling.

Installation

Latest released version of this library:
Clojars Project

Include [district.server.smart-contracts] in your CLJS file, where you use mount/start

API Overview

Warning: district0x modules are still in early stages, therefore API can change in a future.

Real-world example

To see how district server modules play together in real-world app, you can take a look at NameBazaar server folder, where this is deployed in production.

Usage

You can pass following args to smart-contracts module:

  • :contracts-build-path Path to your compiled smart contracts, where you have .bin and .abi files. (default: "<cwd>/resources/public/contracts/build/")
  • :contracts-var Var of smart-contracts map in namespace where you want to store addresses of deployed smart-contracts
  • :print-gas-usage? If true, will print gas usage after each contract deployment or state function call. Useful for development.

Since every time we deploy a smart-contract, it has different address, we need way to store it in a file, so both server and UI can access it, even after restart. For this purpose, we create a namespace containing only smart-contract names and addresses, that will be modified automatically by this module. For example:

(ns my-district.smart-contracts)

(def smart-contracts
  {:my-contract
   {:name "MyContract", ;; By this name .abi and .bin files are loaded
    :address "0x0000000000000000000000000000000000000000"}
   :my-contract-fwd ;; If you're using forwarder smart contract, define :forwards-to with key of a contract forwarded to
   {:name "Forwarder"
    :address "0x0000000000000000000000000000000000000000"
    :forwards-to :my-contract}})

That's all that's needed there. Let's see how can snippet using it look like:

(ns my-district
  (:require [mount.core :as mount]
            [cljs-web3.eth :as web3-eth]
            [my-district.smart-contracts]
            [district.server.smart-contracts :as contracts]))

(-> (mount/with-args
      {:web3 {:port 8545}
       :smart-contracts {:contracts-var #'my-district.smart-contracts/smart-contracts
                         :print-gas-usage? true}})
    (mount/start))

(contracts/contract-address :my-contract)
;; => "0x0000000000000000000000000000000000000000"

(contracts/deploy-smart-contract! :my-contract)
;; (prints) :my-contract 0x575262e80edf7d4b39d95422f86195eb4c21bb52 1,234,435

(contracts/contract-address :my-contract)
;; => "0x575262e80edf7d4b39d95422f86195eb4c21bb52"

(contracts/contract-call :my-contract :my-plus-function [2 3])
;; => 5

;; The module uses just cljs-web3 under the hood, so this is equivalent to the line above
(web3-eth/contract-call (contracts/instance :my-contract) :my-plus-function 2 3)
;; => 5

;; Persist newly deplyed contract addresses into my-district.smart-contracts namespace
(contracts/write-smart-contracts!)
;; (Writes into my-district.smart-contracts, figwheel reloads the file)

Next time you'd start the program, :my-contract contract would be loaded with newly deployed address.

module dependencies

district-server-smart-contracts gets initial args from config provided by district-server-config/config under the key :smart-contracts. These args are then merged together with ones passed to mount/with-args.

district-server-smart-contracts relies on getting web3 instance from district-server-web3/web3. That's why, in example, you need to set up :web3 in mount/with-args as well.

If you wish to use custom modules instead of dependencies above while still using district-server-smart-contracts, you can easily do so by mount's states swapping.

district.server.smart-contracts

Namespace contains following functions for working with smart-contrats:

Returns contract's address

Returns contract's name. E.g "MyContract"

Returns contract's ABI

Returns contract's bin

Returns contract's instance. If provided address, it will create instance related to given address

Convenient wrapper around cljs-web3 contract-call function.
Note : This function needs an unlocked account for signing the transaction!

  • contract can be one of:
    • keyword (e.g :my-contract)
    • keyword of forwarder (e.g my-contract-fwd): If you defined :forwards-to in your smart-contracts definition, you can just use the key of forwarder and it'll know, that it should use ABI of contract in :forwards-to.
    • tuple: keyword + address, for contract at specific address (e.g [:my-contract "0x575262e80edf7d4b39d95422f86195eb4c21bb52"])
    • tuple: keyword + keyword, to use ABI from first contract and address from second contract (e.g [:my-contract :my-other-contract])
  • method : keyword for the method name e.g. :my-method
  • args : a vector of arguments for method (optional)
  • opts: map of options passed as message data (optional), possible keys include:
    • :gas Gas limit, default 4M
    • :from From address, defaults to first address from your accounts
    • :ignore-forward? Will ignore if contract has property :forwards-to and will use ABI of a forwarder

Returns a Promise which resolves to the transaction-hash in the case of state-altering transactions or response in case of retrieve transactions.

Given an event topics and block to start from, returns EventEmitter and calls on-event callback with each event

  • contract can be one of:
    • keyword (e.g :my-contract)
    • tuple: keyword + address, for contract at specific address (e.g [:my-contract "0x575262e80edf7d4b39d95422f86195eb4c21bb52"])
    • tuple: keyword + keyword, to use ABI from first contract and address from second contract (e.g [:my-contract :my-other-contract])
  • event : :camel_case keyword for the event name e.g. :my-event
  • filter-opts : map of indexed return values you want to filter the logs by (see web3 documentation for additional details").
  • opts : specifies additional filter options, can be one of:
    • "latest" to specify that only new observed events should be processed.
    • map {:from-block 0 :to-block 100} specifying earliest and latest block, on which the event handler should fire.
  • on-event : event handler function.
    Returns event filter.

Deploys contract to the blockchain. Returns contract object and also stores new address in internal state.
Note : This function needs an unlocked account for signing the transaction!

  • opts:
    • :arguments Arguments passed to a contract constructor
    • :gas Gas limit, default 4M
    • :from From address, defaults to first address from your accounts
    • :placeholder-replacements a map containing replacements for library placeholders in contract's binary
(def replacements
  ;; Key can be pretty much any string
  ;; Value can be contract-key or address
  {"beefbeefbeefbeefbeefbeefbeefbeefbeefbeef" :my-other-contract
   "__Set________Set________Set________Set__" "0x575262e80edf7d4b39d95422f86195eb4c21bb52"})

Returns a Promise which resolves to the contracts address.

Writes smart-contracts that are currently in module's state into file that was passed to :contracts-var.

Function blocks until block with the specified number is mined. Takes a nodejs-style callback as a second parameter.

Will return first contract event with name event-name that occured during execution of transaction with hash tx-hash. This is useful, when you look for data in specific event after doing some transaction. For example in tests, or mock data generating. Advantage is that this function is synchronous, compared to setting up event filter with web3.

(let [opts {:gas 200000 :from some-address}
      tx-hash (contracts/contract-call :my-contract :fn-that-fires-event)]
  (contracts/contract-event-in-tx tx-hash :my-contract :TheEvent))
  ;; {:block-number 12 :args {:a 1 :b 2} :event "TheEvent" ... }

The same as contract-event-in-tx but instead of first event, returns collection of all events with name event-name.

Reruns all past events and calls callback for each one. This is similiar to what you do with normal web3 event filter, but with this one you can slow down rate at which callbacks are fired. Helps in case you have large number of events with slow callbacks, to prevent unresponsive app. Opts you can pass:

  • :delay - To put delay in between callbacks in ms
  • :transform-fn - Function to transform collection of events
  • :on-chunk - Will be called after calling callback for each chunk
  • :on-finish - Will be called on the very end
(-> (contracts/subscribe-events :my-contract :on-some-event {} {:from-block 0})
  (replay-past-events on-some-event {:delay 10})) ;; in ms

Given a collection of filters get all past events from the filters, sorts them by :block-number :transaction-index :log-index and calls callback for each one in order. Event passed into callback contains :contract and :event keys, to easily identify the event. If callback function returns a JS/Promise it will block until executed. NOTE there is no built-in error handling, so the callback needs to handle promise rejections on it's own.

Opts you can pass:

  • :transform-fn - Function to transform collection of sorted events
  • :on-finish - Will be called after calling callback for all events
  • :from-block - Only download and replay past events starting from this block
  • :to-block - Only download and replay past to this block
  • :block-step - Blocks numbered :from-block until :to-block will be requested in equal chunks of size block-step to avoid sending too big of a request to the node.
  • :skip-log-indexes - A set of tuples like [tx log-index]. Logs in :from-block block with this [tx log-index] will be skipped
(contracts/replay-past-events-in-order
   [(contracts/subscribe-events :my-contract :on-special-event {} {:from-block 0 :to-block "latest"})
    (contracts/subscribe-events :my-contract :on-counter-incremented {} {:from-block 0 :to-block "latest"})]
   (fn [err evt]
    (println "Contract: " (:name (:contract evt)) ", event: " (:event evt) ", args: " (:args evt)))
   {:on-finish (fn []
                 (println "Finished calling callbacks"))})

Development, test & release

  1. Build: npx shadow-cljs compile test-node
  • also need to deploy contracts: npx truffle migrate --network ganache --reset
  1. Tests: node out/node-tests.js

To release (happens automatically on CI at merge to master)

  1. Build: clj -T:build jar
  2. Release: clj -T:build deploy