This demo shows the entire process of building a MVP Dapp on Appchain, which run in neuron wallet.
Notice: This tutorial is for the developers who is able to build webapps and has basic knowledge of Blockchain and Smart Contract.
All interactions with Smart Contract are:
-
Store Text in Smart Contract: an
sendTransaction
action; -
Get TextList from Smart Contract: an
call
action; -
Get Text from Smart Contract: an
call
action;
The final project looks like
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── Routes.jsx
│ ├── components
│ ├── config.js.example
│ ├── containers
│ ├── contracts
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── nervos.js
│ ├── public
│ ├── registerServiceWorker.js
│ └── simpleStore.js
└── yarn.lock
This Demo use create-react-app
to start the project, so you need the create-react-app
scaffold firstly
yarn global add create-react-app
After that the project can be initiated by
create-react-app first_forever && cd first_forever
Now the project looks like
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
└── src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
├── public
└── registerServiceWorker.js
This step is very familiar to webapp developers, Route, Containers and Components will be added to the Dapp
└── src
├── Routes.jsx
├── components
└── containers
The Route indicates that the demo has 4 pages:
All above are just traditional webapp development, and next we are going to dapp development.
A DApp need to talk Neuron wallet some information of blockchain by manifest.json file, which contains chain name, chain id, node httpprovider etc.
As follows, we provider an example of manifest.json. In general, we suggest to put manifest.json in root directory of the project.
If you have more than one chains, you should set more pairs of chain id and node httpprovider in chain set.
{
"name": "Nervos First Forever", // chain name
"blockViewer": "https://etherscan.io/", // bowser of blockchain
"chainSet": { // chainId and node httpprovider
"1": "http://121.196.200.225:1337" // key is chainId, value is node httpprovider
},
"icon": "http://7xq40y.com1.z0.glb.clouddn.com/23.pic.jpg", // chain icon
"entry": "index.html", // DAPP entry
"provider": "https://etherscan.io/" // DAPP provider
}
You should also set path of manifest.json in html file using link tag.
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
This is the core of Dapps running on Appchain, all interactions between Appchain is executed by the nervos.js
Details of nervos
can be accessed at @nervos/chain
Add nervos.js as other packages, simply yarn add @nervos/chain
, then instantiate nervos
in src/nervos.js
Fisrt you should set a provider (HttpProvider), the method of setHost will tell neuron wallet which chain you want to connect.
const { default: Nervos } = require('@nervos/web3')
const config = require('./config')
if (typeof window.nervos !== 'undefined') {
window.nervos = Nervos(window.nervos.currentProvider)
window.nervos.currentProvider.setHost('localhost:1337') // set CITA node IP address and port
} else {
console.log('No nervos? You should consider trying Neuron!')
window.nervos = Nervos(config.chain)
}
var nervos = window.nervos
module.exports = nervos
This Dapp works with an extremely simple smart contract -- SimpleStore.
pragma solidity 0.4.24;
contract SimpleStore {
mapping (address => mapping (uint256 => string)) private records;
mapping (address => uint256[]) private categories;
event Recorded(address _sender, string indexed _text, uint256 indexed _time);
function _addToList(address from, uint256 time) private {
categories[from].push(time);
}
function getList()
public
view
returns (uint256[])
{
return categories[msg.sender];
}
function add(string text, uint256 time) public {
records[msg.sender][time]=text;
_addToList(msg.sender, time);
emit Recorded(msg.sender, text, time);
}
function get(uint256 time) public view returns(string) {
return records[msg.sender][time];
}
}
Smart Contract can be debugged on Remix, an online solidity debugger
By clicking on Detail
in the right-side panel, compiled details will show as follow
In details, bytecode and abi will be used in this demo.
bytecode is used to deploy the contract, and abi is used to instantiate a contract instance for interacting.
Create directory in src
├── contracts
│ ├── SimpleStore.sol
│ ├── compiled.js
│ ├── contracts.test.js
│ ├── deploy.js
│ └── transaction.js
-
Store SimpleStore Source Code in SimpleStore.sol
-
Store bytecode and abi in compiled.js
-
Store transaction template in transaction.js
This dapp is running in neuron wallet who will provide from address and private key.
const nervos = require('../nervos')
const transaction = {
nonce: 999999,
quota: 1000000,
chainId: 1,
version: 0,
validUntilBlock: 999999,
value: '0x0',
}
- Store deploy script in deploy.js
You should deploy contract on develop branch, which contains private key
const nervos = require('../nervos')
const { abi, bytecode } = require('./compiled.js')
const transaction = require('./transaction')
let _contractAddress = ''
nervos.appchain
.getBlockNumber()
.then(current => {
transaction.validUntilBlock = +current + 88 // update transaction.validUntilBlock
return nervos.appchain.deploy(bytecode, transaction) // deploy contract
})
.then(res => {
const { contractAddress, errorMessage } = res
if (errorMessage) throw new Error(errorMessage)
console.log(`contractAddress is: ${contractAddress}`)
_contractAddress = contractAddress
return nervos.appchain.storeAbi(contractAddress, abi, transaction) // store abi on the chain
})
.then(res => {
if (res.errorMessage) throw new Error(res.errorMessage)
return nervos.appchain.getAbi(_contractAddress).then(console.log) // get abi from the chain
})
.catch(err => console.error(err))
-
Store test script in contracts.js
const nervos = require('../nervos') const { abi } = require('./compiled') const { contractAddress } = require('../config') const transaction = require('./transaction') const simpleStoreContract = new nervos.appchain.Contract(abi, contractAddress) // instantiate contract nervos.appchain.getBalance(nervos.eth.accounts.wallet[0].address).then(console.log) // check balance of account console.log(`Interact with contract at ${contractAddress}`) const time = new Date().getTime() const text = 'hello world at ' + time test( `Add record of (${text}, ${time})`, async () => { const current = await nervos.appchain.getBlockNumber() transaction.validUntilBlock = +current + 88 // update transaction.validUntilBlock const txResult = await simpleStoreContract.methods.add(text, time).send(transaction) // sendTransaction to the contract const receipt = await nervos.listeners.listenToTransactionReceipt(txResult.hash) // listen to the receipt expect(receipt.errorMessage).toBeNull() }, 10000, ) test( `Get record of (${text}, ${time})`, async () => { const list = await simpleStoreContract.methods.getList().call({ from: transaction.from, }) // check list const msg = await simpleStoreContract.methods.get(time).call({ from: transaction.from, }) // check message expect(+list[list.length - 1]).toBe(time) expect(msg).toBe(text) }, 3000, )
After all of that, npm run deploy
to deploy the contract, and set the contractAddress in /config.js
, and then use npm test
to test the contract.
For now the config.js should be like:
const config = {
chain: '{host address of appchain you are using}',
privateKey: '{your private key}',
contractAddress: '{deployed contract address}',
}
module.exports = config
Instantiate Contract in simpleStore.js under src
const nervos = require('./nervos')
const { abi } = require('./contracts/compiled.js')
const { contractAddress } = require('./config')
const transaction = require('./contracts/transaction')
const simpleStoreContract = new nervos.appchain.Contract(abi, contractAddress)
module.exports = {
transaction,
simpleStoreContract,
}
In src/containers/Add/index.jsx
, bind the following method to submit button
handleSubmit = e => {
const { time, text } = this.state
nervos.appchain.getBlockNumber().then(current => {
const tx = {
...transaction,
from: window.neuron.getAccount(),
validUntilBlock: +current + 88,
}
this.setState({
submitText: submitTexts.submitting,
})
var that = this
simpleStoreContract.methods.add(text, +time).send(tx, function(err, res) {
if (res) {
nervos.listeners.listenToTransactionReceipt(res).then(receipt => {
if (!receipt.errorMessage) {
that.setState({ submitText: submitTexts.submitted })
} else {
throw new Error(receipt.errorMessage)
}
})
} else {
throw new Error('No Transaction Hash Received' + err)
}
})
})
}
In src/containers/List/index.jsx
, load memos on mount
componentDidMount() {
const from = window.neuron.getAccount()
simpleStoreContract.methods
.getList()
.call({
from,
})
.then(times => {
times.reverse()
this.setState({ times })
return Promise.all(times.map(time => simpleStoreContract.methods.get(time).call({ from })))
})
.then(texts => {
this.setState({ texts })
})
.catch(console.error)
}
In src/containers/Show/index.jsx
, load memo on mount
componentDidMount() {
const { time } = this.props.match.params
if (time) {
simpleStoreContract.methods
.get(time)
.call({
from: window.neuron.getAccount(),
})
.then(text => {
this.setState({ time, text })
})
.catch(error => this.setState({ errorText: JSON.stringify(error) }))
} else {
this.setState({ errorText: 'No Time Specified' })
}
}
As all of these done, start the local server by npm start
to launch the dapp.