README ¶
This hands-on tutorial will guide you through the process of developing a Sbit DApp. By following along, you will:
- Run sbit's blockchain services in a docker container, in test mode.
- Generate initial coins for development.
- Deploy a Smart Contract.
- Interact with Smart Contract using RPC.
- Interact with Smart Contract in the browser using
sbitjs
. - Live-edit a DApp using Neutrino.js.
So let's get started. First, clone the repo:
git clone https://github.com/hayeah/neutrino-react-ts-boilerplate.git my-project
This repository provides a simple DApp UI to interact with the Counter.sol contract:
pragma solidity ^0.4.11;
contract Counter {
uint256 count;
event CounterChanged(uint256 n);
function Counter(uint256 startCount) public {
count = startCount;
}
// Increment counter by n
function increment(uint256 n) public {
assert(n >= 0);
count += n;
// emit CounterChanged event
CounterChanged(count);
}
// Return current count
function getCount() public constant returns(uint256) {
return count;
}
}
The DApp looks like this:
You may use this project as a boilerplate to jump start your DApp project.
DApp Development Environment
To simplify setup, we'll run the blockchain RPC service inside a docker container.
docker run -it --rm \
--name myapp \
-v `pwd`:/dapp \
-p 9899:9899 \
-p 9888:9888 \
hayeah/sbitportal
The container will run in the foreground. Hit Ctrl-C
to terminate the container.
In a separate terminal, verify that the container is running:
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
088a9bc5b9c4 hayeah/sbitportal "/bin/sh -c 'mkdir..." 22 minutes ago Up 22 minutes 0.0.0.0:9888->9888/tcp, 0.0.0.0:9899->9899/tcp myapp
There are two exposed service ports:
- (http://localhost:9888) Offers JSON RPC service to access the blockchain.
- (http://localhost:9899) Authorization UI for DApp users to grant permission to sensitive RPC calls.
Opening http://localhost:9899 in the browser, you should see the authorization UI:
Entering Into The Container
We can enter into the container by using the docker exec
command:
docker exec -it myapp bash
Once you are inside, try to make a RPC call using the qcli
command:
qcli getinfo
{
"version": 140700,
"protocolversion": 70016,
"walletversion": 130000,
"balance": 5599997.77299520,
"stake": 19580001.33190480,
"blocks": 1269,
"timeoffset": 0,
"connections": 0,
"proxy": "",
"difficulty": {
"proof-of-work": 4.656542373906925e-10,
"proof-of-stake": 4.656542373906925e-10
},
"testnet": false,
"moneysupply": 25380000,
"keypoololdest": 1510201235,
"keypoolsize": 100,
"paytxfee": 0.00000000,
"relayfee": 0.00400000,
"errors": ""
}
Run qcli help
to list all the available RPC commands:
qcli help
== Blockchain ==
callcontract "address" "data" ( address )
getaccountinfo "address"
getbestblockhash
getblock "blockhash" ( verbose )
getblockchaininfo
getblockcount
getblockhash height
...
sendtocontract "contractaddress" "data" (amount gaslimit gasprice senderaddress broadcast)
setaccount "address" "account"
settxfee amount
signmessage "address" "message"
The qcli
command is actually a convenience wrapper for the sbit-cli
tool:
#!/bin/bash
sbit-cli -rpcuser=$SBIT_RPC_USER -rpcpassword=$SBIT_RPC_PASS -regtest "$@"
Generate Coins
Inside the container we can generate coins for development uses:
qcli generate 600
[
...
"1418c993bcd109508d8474f03d974db272e0edda2177be08589d0df04ed5acaa",
"742fcaca9066119298de55fabde0a0d3b87fcc1a4b98f2e2918e19b6ba9db4da",
"5dd5193e7c1abf1c36832b1cc1ddf9fe060c5de691afbc905d2de144c698001f",
"2cb7c10b1759e0275feeff6ae104aaf23f8a93f27859b27a0d9ce102480fcf78",
"45e38eac7b020f2abf6db2730b272e3eadc17e1e982560c94e69173695964689",
"214042e47c7998f8643f6b29a0c65feeb79e3b1524532de61e183f81fbc34284",
"390695dd85c3b796648da20dca21e47970e45176d0f2e3238bdec123bd36bacc",
"4ebbf505ea2cfb30b59b96ecb615e2f0523282b73290495f4c13ade52771fe6f",
"557c4bd2869d43348e01b5537767ef7693d616e87ebb7d37466e2bf5275a5ada"
]
You should see that the balance is non-zero:
qcli getbalance
2000000.00000000
Deploy The Contract
This DApp project is mapped to the /dapp
directory inside the container. Let's deploy the contract contracts/Counter.sol
to the blockchain.
Inside the container, run:
solar deploy contracts/Counter.sol counter '[1]'
deploy contracts/Counter.sol => counter
🚀 All contracts confirmed
The deploy command takes 3 arguments:
contracts/Counter.sol
is the contract's source file.counter
is the name we give to the contract.'[1]'
is a JSON array of parameters sent to the contract's constructor.
We can check that the contract had been deployed:
solar status
✅ counter
txid: eb3c852f254ecbb6173e2fe653d86214a26142e27af0c28cc1fb0060901f9bb6
address: 9a2349a6a97e27a67c6887633f79384882cfaf02
confirmed: true
Interacting With Deployed Contract
Mostly, you'll be using two RPC calls to interact with a contract's methods:
callcontract
to invoke a method in "query" mode, using data from your local blockchain, but not making changes to it. This is free.senttocontract
to invoke a method in "commit" mode, creating a transaction that changes the blockchain. This costs you gas.
A DApp would be using sbitjs
to make these RPC calls. Before we get to the JavaScript code, let's make these RPC calls manually.
To make a method call, we'll need to encode the method name and the parameters according to the Solidity ABI specification. We can use solar encode
to handle this for us.
solar encode counter getCount
a87d942c
counter
is the name of the contract we've deployed.getCount
is the name of the method.- This method has no parameters.
We'll make the callcontract
RPC call using qcli
. Let's print the help information first:
qcli help callcontract
callcontract "address" "data" ( address )
Argument:
1. "address" (string, required) The account address
2. "data" (string, required) The data hex string
3. address (string, optional) The sender address hex string
4. gasLimit (string, optional) The gas limit for executing the contract
So it takes two mandatory arguments:
address
of theCounter
contract deployed earlier.data
- the ABI encoded method name and parameters.
Let's make the call:
qcli callcontract 9a2349a6a97e27a67c6887633f79384882cfaf02 a87d942c
{
"address": "9a2349a6a97e27a67c6887633f79384882cfaf02",
"executionResult": {
"gasUsed": 21667,
"excepted": "None",
"newAddress": "9a2349a6a97e27a67c6887633f79384882cfaf02",
"output": "0000000000000000000000000000000000000000000000000000000000000001",
"codeDeposit": 0,
"gasRefunded": 0,
"depositSize": 0,
"gasForDeposit": 0
},
"transactionReceipt": {
"stateRoot": "0290be0a95c2d87afe1619cb3bc10abddda63354acb436e2ef340d39e1b3b502",
"gasUsed": 21667,
"bloom": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"log": [
]
}
}
gasUsed
reports how much gas is used to execute this call. But sincecallcontract
executes locally, it doesn't actually cost you that much gas. It's an estimation of how much gas this call might use if it is executed as a transaction by the network.output
this is the ABI-encoded return value ofgetCount
, a hex-encoded number.
Now let's update the counter by creating a transaction. The method to call is increment
:
function increment(uint256 n) public {
assert(n >= 0);
count += n;
// emit CounterChanged event
CounterChanged(count);
}
It changes the count
storage variable, as well as emitting a CounterChanged
event with the latest count
value.
Again, we'll need to encode the method call:
solar encode counter increment '[5]'
7cf5dab00000000000000000000000000000000000000000000000000000000000000005
7cf5dab
is the encoded method name000....005
is an encoded uint256 number
Before making the RPC call, let's print the help info first:
qcli help sendtocontract
sendtocontract "contractaddress" "data" (amount gaslimit gasprice senderaddress broadcast)
... more doc below ...
There are two mandatory arguments, and a few optional ones. In this example, we'll only be using the first two:
qcli sendtocontract \
9a2349a6a97e27a67c6887633f79384882cfaf02 \
7cf5dab00000000000000000000000000000000000000000000000000000000000000005
{
"txid": "cf2fd38a1a8a4414bc11cb2682b9ac9d7ecf2ed09b137f35f424c390afe311dd",
"sender": "qcZGiHTwEh2wqucRkAkmiP9HLgVj8AwrST",
"hash160": "d06502c30f258542891419a62fa01359d70a6c24"
}
A transaction is created. Wait about 30 seconds and it'd be mined, then look it up:
qcli gettransaction \
cf2fd38a1a8a4414bc11cb2682b9ac9d7ecf2ed09b137f35f424c390afe311dd
{
"amount": 0.00000000,
"fee": -0.10107200,
"confirmations": 3,
"blockhash": "ca630653cbf49203c4409181865f76e133b34a942d5c7cda12ac0a744e7017a6",
"blockindex": 2,
"blocktime": 1510281280,
"txid": "cf2fd38a1a8a4414bc11cb2682b9ac9d7ecf2ed09b137f35f424c390afe311dd",
"walletconflicts": [
],
"time": 1510281282,
"timereceived": 1510281282,
"bip125-replaceable": "no",
"details": [
{
"account": "",
"category": "send",
"amount": 0.00000000,
"vout": 1,
"fee": -0.10107200,
"abandoned": false
}
],
"hex": "0200000001b69b1f906000fbc18cc2f07ae24261a21462d853e62f3e17b6cb4e252f853ceb010000006b483045022100e211675b40fc4a8fc69b4878669de436e937b81a77258b0fa7e3bb1b6394ef110220754308ae23157a497048f784125b7498c755f68f7eb6b31205af33393a2c2840012102a0fa0accd9e616fea7d31e71f99b0efb534c875cd48d4ced35909213b19214c2feffffff02c009ee9cd10100001976a914e9a1506597910382abf96c47f24cf6856f1b15a088ac00000000000000004301040390d0030128247cf5dab00000000000000000000000000000000000000000000000000000000000000005149a2349a6a97e27a67c6887633f79384882cfaf02c2dd050000"
}
confirmations
: the number of times this transaction had been confirmed by the blockchain.hex
: this is the raw transaction data. If you squint a bit, you'd see that the ABI encoded method call is in there (i.e.7cf5dab000...0005
).
So gettransaction
gives information about whether a transaction had been mined by the network, but not any information about the method call itself. Specifically, we are not getting the log events emitted by the increment
method.
Some additional information is available via the gettransactionreceipt
RPC call:
qcli gettransactionreceipt \
cf2fd38a1a8a4414bc11cb2682b9ac9d7ecf2ed09b137f35f424c390afe311dd
[
{
"blockHash": "ca630653cbf49203c4409181865f76e133b34a942d5c7cda12ac0a744e7017a6",
"blockNumber": 1502,
"transactionHash": "cf2fd38a1a8a4414bc11cb2682b9ac9d7ecf2ed09b137f35f424c390afe311dd",
"transactionIndex": 2,
"from": "d06502c30f258542891419a62fa01359d70a6c24",
"to": "9a2349a6a97e27a67c6887633f79384882cfaf02",
"cumulativeGasUsed": 27898,
"gasUsed": 27898,
"contractAddress": "9a2349a6a97e27a67c6887633f79384882cfaf02",
"log": [
{
"address": "9a2349a6a97e27a67c6887633f79384882cfaf02",
"topics": [
"f54acf746bc9c7e8bbd50a08d3311c74576035afb12991b2820756ffbf78182c"
],
"data": "0000000000000000000000000000000000000000000000000000000000000006"
}
]
}
]
log
is an array of log items emitted by the contract.log[0].data
this is the ABI encoded value of theCounterChanged(uint256 n)
event. It gives us the new value ofcount
after incrementing by 5.
Now that the transaction had been confirmed, let's verify whether getCount
returns the new counter value. Instead of using qcli callcontract
, let's make a raw HTTP JSON request, just to see what qcli
is doing under the hood.
Within the container, the SBIT_RPC
environment variable is already set to be the URL of the sbitd
JSON HTTP service:
echo $SBIT_RPC
http://sbit:test@localhost:22302
Now make the raw HTTP call with curl:
curl $SBIT_RPC -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "1.0",
"id": 1,
"method": "callcontract",
"params": [
"9a2349a6a97e27a67c6887633f79384882cfaf02",
"a87d942c"
]
}
'
{
"result": {
"address": "9a2349a6a97e27a67c6887633f79384882cfaf02",
"executionResult": {
"gasUsed": 21667,
"excepted": "None",
"newAddress": "9a2349a6a97e27a67c6887633f79384882cfaf02",
"output": "0000000000000000000000000000000000000000000000000000000000000006",
"codeDeposit": 0,
"gasRefunded": 0,
"depositSize": 0,
"gasForDeposit": 0
},
"transactionReceipt": {
"stateRoot": "d56a5fc4fcd6f6283e838e87ce9ddb99a8a4d3003f1fa24d5d7ee0cf072da02a",
"gasUsed": 21667,
"bloom": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"log": []
}
},
"error": null,
"id": 1
}
We can see that output
is the latest coutner value 0x6
.
DApp RPC Security
For building DApp, sbitjs
provides a light-weight wrapper around sbitd
's RPC methods. However, we can't allow DApps to have unfettered access to the RPC directly. For sensitive RPC calls that may involve money, we need give the DApp user an opportunity to review the transaction made.
Using the authorization UI, a DApp user can see what transactions are made, and choose to deny or accept transactions.
The container exposes two service ports for the outside world:
docker ps --format 'table {{.Names}} {{.Image}} {{.Ports}}'
NAMES IMAGE PORTS
myapp hayeah/sbitportal 0.0.0.0:9888->9888/tcp, 0.0.0.0:9899->9899/tcp
9899
is the authorization UI.9888
provides RPC service to DApps, proxying authorized RPC calls to the underlyingsbitd
RPC.
A DApp must not have direct access to the underlying sbitd
RPC service. For this reason, the container does not expose sbitd's RPC port.
Let's verify that we have access to the RPC from outside the container. The callcontract
is read-only, so should work without user authorization:
curl http://localhost:9888 -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "1.0",
"id": 1,
"method": "callcontract",
"params": [
"9a2349a6a97e27a67c6887633f79384882cfaf02",
"a87d942c"
]
}
'
// should succeed, as before
A sendtocontract
should require user authorizatoin:
curl -i http://localhost:9888 -X POST -H "Content-Type: application/json" -d '
{
"jsonrpc": "1.0",
"id": 1,
"method": "sendtocontract",
"params": [
"9a2349a6a97e27a67c6887633f79384882cfaf02",
"a87d942c"
]
}
'
Now it fails with 402 Payment Required
error:
HTTP/1.1 402 Payment Required
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=UTF-8
Vary: Origin
Date: Fri, 10 Nov 2017 03:46:28 GMT
Content-Length: 278
{
"id": "yA3vGXW8b7Ct-rEEEqpK0Tln_G66n8p7jydu-kw3rns",
"state": "pending",
"request": {
"method": "sendtocontract",
"id": 1,
"params": [
"9a2349a6a97e27a67c6887633f79384882cfaf02",
"a87d942c"
],
"auth": "yA3vGXW8b7Ct-rEEEqpK0Tln_G66n8p7jydu-kw3rns"
},
"createdAt": "2017-11-10T03:46:28.616583838Z"
}
And opening the authorization UI at http://localhost:9888, you should see an RPC call pending authorization:
If the user clicks Approve
, the client would then be able to make the same sendtocontract
call again, and succeed.
For details of how RPC authorization works, see SBIT Portal Authorization Design.
sbitjs
handles authorization transparently, so you don't need to think about this in practice.
Developing The DApp UI
A DApp is essentially a UI wrapper for accessing a set of smart contracts' methods.
In solar.development.json there is information about the deployed contracts, which sbitjs can use to call
or send
to a contract.
We can access a contract by initializing a Contract
object:
// SBIT_RPC is the RPC URL
const rpc = new SbitRPC(SBIT_RPC)
// CONTRACTS is the JSON object in solar.development.json
const counter = new Contract(rpc, CONTRACTS.counter)
These two constants are defined in config/development.js.
A Contract
instance provides the call
and send
methods. The first argument is the method name, and the second argument an array of parameter values for the method call:
const r = await counter.call("getCount", [])
// r.outputs is the methods's returned values
const r = await counter.send("increment", [n])
// r.logs is the method's emitted log events
See api.ts for an example of building a simple API wrapper.
The DApp Dev Server
Let's get the DApp running. First, install npm dependencies:
# https://yarnpkg.com/en/
yarn install
Start Neutrino dev server on port 3000:
PORT=3000 yarn start
Open http://localhost:3000, and edit src/index.tsx
for live-reload.
See: YouTube Demo Video
For styling, edit src/index.css
.
Edit the html options to customize the HTML template. See html-webpack-template.
Other Tips
Generating sourcemap may slow down project rebuilding. Webpack provide other sourcemap types that can speed up project building.
In particular, the eval
sourcemap is quite faster.
SOURCEMAP=eval PORT=3000 yarn start
See devtool for a list of sourcemap types.
Documentation ¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Asset ¶
Asset loads and returns the asset for the given name. It returns an error if the asset could not be found or could not be loaded.
func AssetDir ¶
AssetDir returns the file names below a certain directory embedded in the file by go-bindata. For example if you run go-bindata on data/... and data contains the following hierarchy:
data/ foo.txt img/ a.png b.png
then AssetDir("data") would return []string{"foo.txt", "img"} AssetDir("data/img") would return []string{"a.png", "b.png"} AssetDir("foo.txt") and AssetDir("notexist") would return an error AssetDir("") will return []string{"data"}.
func AssetInfo ¶
AssetInfo loads and returns the asset info for the given name. It returns an error if the asset could not be found or could not be loaded.
func MustAsset ¶
MustAsset is like Asset but panics when Asset would return an error. It simplifies safe initialization of global variables.
func RestoreAsset ¶
RestoreAsset restores an asset under the given directory
func RestoreAssets ¶
RestoreAssets restores an asset under the given directory recursively
Types ¶
This section is empty.