Clojurescript-node.js mount module for a district server, that takes care of smart-contracts loading, deployment, function calling and event handling.
Latest released version of this library:
Include [district.server.smart-contracts]
in your CLJS file, where you use mount/start
Warning: district0x modules are still in early stages, therefore API can change in a future.
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.
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.
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.
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]
)
- keyword (e.g
method
: keyword for the method name e.g.:my-method
args
: a vector of arguments formethod
(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]
)
- keyword (e.g
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"))})
- Build:
npx shadow-cljs compile test-node
- also need to deploy contracts:
npx truffle migrate --network ganache --reset
- Tests:
node out/node-tests.js
To release (happens automatically on CI at merge to master)
- Build:
clj -T:build jar
- Release:
clj -T:build deploy