-
Notifications
You must be signed in to change notification settings - Fork 256
DApp Development in JavaScript
[TOC]
- Using JavaScript to develop DApp,The first step is to write a contract in a specific format,then deploy the contract to Chain33 using the cli debugging tool or calling the JSONRPC interface,and the desired action will be accomplished by calling the corresponding method。
General js files contain the following rules: Define Init(context) function: this function will be called when the contract is deploying, the purpose is to prepare resources for a contract, mainly store the object kvcreator and the information context. Define a certain method of the object exec:Write some data to stateDB and pass some log information that needs to be recorded in tables. Define a certain value of the ExecLocal object: write log information to a table or write some data to localDB. Certain method of defining the object Query: query the data in the table or localDB.
Based on the framework of Chain33,a method called through the Call of JSVM will execute the corresponding Exec object and the method of ExecLocal object with the same name,such as the payload.funcname of Call is "NewGame",and then execute the NewGame method of Exec object and NewGame method of ExecLocal object.
Below combines game.js to introduce concretely
Game.js defines a game of guessing Numbers. The rules of the game are: the banker hash a number from 0 to 10 (random number + 9) (total compensation amount).Corresponding to the NewGame method. Multiple users can guess this number, corresponding to the Guess method. The lottery, corresponding to the CloseGame method. Here are just a few examples of the NewGame method, with the other two methods referenced in the code linked above.
To store the above information, you need to create three tables, a GameLocalTable,a MatchLocalTable,and a MatchGameTable,MatchGameTable is a temporary table based on the above two tables, which is similar to the relational database generated by join operation . Create a table that contains the config and defaultvalue sections(Only needs to call JoinTable method)config sets table structure,including two types of table property configuration items (starting with #) and column property configuration.Table properties have table name, primary key, and storage db type.Defaultvalue is used to set the default value for the column properties. Here is the code to create the GameLocalTable:
function GameLocalTable(kvc) {
this.config = {
"#tablename" : "game",//Define the name of the table
"#primary" : "gameid", //Define the primary key
"#db" : "localdb", //Define the db type of storage, generally localdb
"gameid" : "%018d", //Table primary key, game ID, can be defined with the transaction general index (txID())
"status" : "%d", //Game state: 0- not start, 1- start, 2- close
"hash" : "%s", //Transaction hash
"addr" : "%s", //Create the trader's address
}
this.defaultvalue = {
"gameid" : 0,
"status" : 0,
"hash" : "",
"addr" : "",
}
return new Table(kvc, this.config, this.defaultvalue)
}
Here is the code to create the MatchLocalTable:
function MatchLocalTable(kvc) {
this.config = {
"#tablename" : "match", //Define the name of the table
"#primary" : "id", //Define the primary key
"#db" : "localdb", //Define the db type of storage, generally localdb
"id" : "%018d", //Table primary key, game ID, can be defined with the transaction general index (txID())
"gameid" : "%018d", //Table foreign key, corresponding to the GameLocalTable table primary key
"hash" : "%s", //Transaction hash
"addr" : "%s", //The address of the number guesser participating in the game
}
this.defaultvalue = {
"id" : 0,
"gameid" : 0,
"hash" : "",
"addr" : "",
}
return new Table(kvc, this.config, this.defaultvalue)
}
Here is the code to create the MatchGameTable:
function MatchGameTable(kvc) {
//The lefttable parameter is a table using foreign keys with a relatively large amount of data, and the index parameter is the field name to query the join table
return new JoinTable(MatchLocalTable(kvc), GameLocalTable(kvc), "addr#status")
}
function Init(context) {
this.kvc = new kvcreator("init")//Create an init type object to store the key and value values
this.context = context //Save context information
return this.kvc.receipt()
}
This method can be called by the chain33-cli debugging tool or the call method of JSONRPC, and the value is the equal to payload.funcname.The main function is to write some information to stateDB through kvc.add(), and pass log information through kvc.addlog().
Exec.prototype.NewGame = function(args) {
//
var game = {__type__ : "game"}
game.gameid = this.txID()
game.height = this.context.height
game.randhash = args.randhash
game.bet = args.bet
game.hash = this.context.txhash
game.obet = game.bet
game.addr = this.context.from
game.status = 1 //open
//The maximum value is 90 million, otherwise when js to int will overflow
if (game.bet < 10 * COINS || game.bet > 10000000 * COINS) {
throwerr("bet low than 10 or hight than 10000000")
}
if (this.kvc.get(game.randhash)) { //If randhash has been used
throwerr("dup rand hash")
}
var err = this.acc.execFrozen(this.name, this.context.from, game.bet) //Freeze declarer's bonus pool
throwerr(err)
this.kvc.add(game.gameid, game) //Store key:gameid and value:game to stateDB
this.kvc.add(game.randhash, "ok")//Store key:randhash与value:"ok" to stateDB
this.kvc.addlog(game) //Pass object game to ExecLocal.prototype.NewGame
return this.kvc.receipt()
}
This method writes the log information passed by the method of the corresponding Exec to table, or calls kvc.add () to directly write the data to stateDB。
function localprocess(args) {
var local = MatchGameTable(this.kvc)
local.addlogs(this.logs) //Operation join table, achieve the purpose of automatically updating the associated table
local.save() //Save data
return this.kvc.receipt()
}
ExecLocal.prototype.NewGame = function(args) {
return localprocess.call(this, args)
}
This method can be called by the chain33-cli debugging tool or the call method of JSONRPC, and the value is the equal to payload.funcname The function of querying data according to the passed parameters can be realized. You can query through tables or directly query localDB.
//Query game information according to the game creator's address
Query.prototype.ListGameByAddr = function(args) {
var local = GameLocalTable(this.kvc)
var q = local.query("addr", args.addr, args.primaryKey, args.count, args.direction)
return querytojson(q)
}
Chain33-cli is our debugger and you can conviniently do some things with the chain33-cli command. Here's how to use the chain33-cli command to deploy the smart contract on the chain33 chain and invoke the methods in the smart contract.
**step1:**Deploy the smart contract on chain chain33 using the chain33-cli command:
./chain33-cli send jsvm create -c "The contract code file path" -n "contract name" -k "The private key or address of the creator of the contract"
root@ubuntu055-3:/home/lcj0# ./chain33-cli send jsvm create -c "/home/hugo/game.js" -n game -k 14KEKbYtKKQm4wMthSK9J4La4nAiidGozt
**step2:**After the deployment of contract code, a transaction hash is generated, which will be used to query the transaction:
./chain33-cli tx query_hash -s "transaction hash"
root@ubuntu055-3:/home/lcj0# ./chain33-cli tx query_hash -s 0xd4cfd1606ea1d9382b3334351e3e741cad8a68b4e7b38a4b02c981203865cb03
- The returned results can be seen as follows: the contract name is used in operations of contract transfer or retrieval, code is the content of js file;
...
"execer": "jsvm",
"payload": {
"create": {
"code": "...js file content...",
"name": "game"
},
"ty": 0
},
...
**step3:**Recharge the contract
Because contract game need the game creator (the banker) to pledge some assets as the prize pool,so the participator need to recharge 100 BTY through the command line to the contract,Note that the - e parameter must be in the form of "user. jsvm. contract name".
./chain33-cli send coins send_exec -a BTY number -e "The contract name" -n "postscript" -k "The private key or address of the creator of the contract"
./chain33-cli send coins send_exec -a 100 -e "user.jsvm.game" -n "12312" -k 14KEKbYtKKQm4wMthSK9J4La4nAiidGozt
Use command to see whether the recharge was successful or not.
./chain33-cli tx query_hash -s 0xa627e07e3ba7c47fd2e16812b34f82d6e07c6c074d2043abdb6b2d57dc9b4d4b
...
"ty": 8,
"tyName": "LogExecDeposit",
"log": {
"execAddr": "15mjHYNvPkSMrp3SQxtczXL6cinMAEdeKs",
"prev": {
"currency": 0,
"balance": "0",
"frozen": "0",
"addr": "14KEKbYtKKQm4wMthSK9J4La4nAiidGozt"
},
"current": {
"currency": 0,
"balance": "10000000000",
"frozen": "0",
"addr": "14KEKbYtKKQm4wMthSK9J4La4nAiidGozt"
}
}
...
current.balance is 100* 1e8 now
**step4:After recharged **, call the NewGame method in the contract to create the game.
./chain33-cli send jsvm call -n "The contract name" -f "The method name" -a "Parameter list in json format" -k "The private key or address of the creator of the contract"
./chain33-cli send jsvm call -n game -f NewGame -a "{\"bet\":1500000000, \"randhash\":\"0x5d2ac15654aa1859af80d1dfa5d7a5775c359fe3a49abce03796f4fbc313b57f\"}" -k 14KEKbYtKKQm4wMthSK9J4La4nAiidGozt
Use the command to query whether the creation was successful.
...
"ty": 9,
"tyName": "LogExecFrozen",
"log": {
"execAddr": "15mjHYNvPkSMrp3SQxtczXL6cinMAEdeKs",
"prev": {
"currency": 0,
"balance": "10000000000",
"frozen": "0",
"addr": "14KEKbYtKKQm4wMthSK9J4La4nAiidGozt"
},
"current": {
"currency": 0,
"balance": "8500000000",
"frozen": "1500000000",
"addr": "14KEKbYtKKQm4wMthSK9J4La4nAiidGozt"
}
},
...
Frozen amount: current.frozen is 15* 1e8, surplus:current.balance is 85* 1e8
...
"ty": 10000,
"tyName": "TyLogJs",
"log": {
"data": "{\"__type__\":\"game\",\"addr\":\"14KEKbYtKKQm4wMthSK9J4La4nAiidGozt\",\"bet\":1500000000,\"gameid\":4100000,\"hash\":\"0x2b3e9d1bd4dfcd6766f6aacc366d8e360e9896de1cc4da0501b30d696a506d2a\",\"height\":41,\"obet\":1500000000,\"randhash\":\"0x5d2ac15654aa1859af80d1dfa5d7a5775c359fe3a49abce03796f4fbc313b57f\",\"status\":1}"
},
...
You can see that the game was created. and gameid is 4100000.then use the same method to call method Guess and CloseGame to guess number and draw a lottery.
step5: Invoke the query interface to list games created by certain address
./chain33-cli jsvm query -a "Parameter list in json format" -f "The method name" -n "The contract name"
./chain33-cli jsvm query -a "{\"addr\":\"14KEKbYtKKQm4wMthSK9J4La4nAiidGozt\", \"count\":20}" -f "ListGameByAddr" -n game
The return contents are as follows:
{
"data": "[{\"left\":{\"__type__\":\"game\",\"addr\":\"14KEKbYtKKQm4wMthSK9J4La4nAiidGozt\",\"bet\":1500000000,\"gameid\":4100000,\"hash\":\"0x2b3e9d1bd4dfcd6766f6aacc366d8e360e9896de1cc4da0501b30d696a506d2a\",\"height\":41,\"obet\":1500000000,\"randhash\":\"0x5d2ac15654aa1859af80d1dfa5d7a5775c359fe3a49abce03796f4fbc313b57f\",\"status\":1}}]"
}
You can use the same method to invole the ListMatchByAddr method to list the game in a certain state created at a certain address. Because of the use of join table queries,you need to find the result through the JoinKey method first, and then use the result as a parameter to call the ListMatchByAddr method to get the final result.
In addition to creating the original transaction here, it is also necessary to sign and send the transaction If digital currency will be used in the contract, it is required to recharge the contract before invoking it.
Request message:
{
"jsonrpc":"2.0",
"method": "Chain33.CreateTransaction",
"params": [
{
"execer": "string",
"actionName": "string",
"payload": {
"code": "string",
"name":"string"
}
}
],
"id":int32,
}
Parameter description:
Parameter | Type | If Necessary | Description |
---|---|---|---|
execer | string | Yes | Executor name, fixed here as JSVM |
actionName | string | Yes | Action name, fixed here as Create |
payload.code | string | Yes | javascript code |
payload.name | string | Yes | contract name |
Response message:
{
"id":int32,
"result":"string",
"error":null
}
Parameter description:
Parameter | Type | Description |
---|---|---|
result | string | hexadecimal encoded transaction content |
Example: Request1 Deploy contract transaction:
{
"method": "Chain33.CreateTransaction",
"params": [
{
"execer": "jsvm",
"actionName": "Create",
"payload": {
"code": "//........................\n//............: ............... 0 - 10 ......... hash(......... + 9) (.....................) NewGame()\n//............................................................ Guess()\n//...... CloseGame()\nfunction Init(context) {\n this.kvc = new kvcreator(\"init\")\n this.context = context\n return this.kvc.receipt()\n}\n\nvar MIN_WAIT_BLOCK = 2\nvar RAND_MAX = 10\n\nfunction ExecInit() {\n this.acc = new account(this.kvc, \"coins\", \"bty\")\n}\n\nExec.prototype.NewGame = function(args) {\n var game = {__type__ : \"game\"}\n game.gameid = this.txID()\n game.height = this.context.height\n game.randhash = args.randhash\n game.bet = args.bet\n game.hash = this.context.txhash\n game.obet = game.bet\n game.addr = this.context.from\n game.status = 1 //open\n //............ 9000...,......js... int .........\n if (game.bet < 10 * COINS || game.bet > 10000000 * COINS) {\n throwerr(\"bet low than 10 or hight than 10000000\")\n }\n if (this.kvc.get(game.randhash)) { //......randhash ..................\n throwerr(\"dup rand hash\")\n }\n var err = this.acc.execFrozen(this.name, this.context.from, game.bet)\n throwerr(err)\n this.kvc.add(game.gameid, game)\n this.kvc.add(game.randhash, \"ok\")\n this.kvc.addlog(game)\n return this.kvc.receipt()\n}\n\nExec.prototype.Guess = function(args) {\n var match = {__type__ : \"match\"}\n match.gameid = args.gameid\n match.bet = args.bet\n match.id = this.txID()\n match.addr = this.context.from\n match.hash = this.context.txhash\n match.num = args.num\n var game = this.kvc.get(match.gameid)\n if (!game) {\n throwerr(\"guess: game id not found\")\n }\n if (game.status != 1) {\n throwerr(\"guess: game status not open\")\n }\n if (this.context.from == game.addr) {\n throwerr(\"guess: game addr and match addr is same\")\n }\n if (match.bet < 1 * COINS || match.bet > game.bet / RAND_MAX) {\n throwerr(\"match bet litte than 1 or big than game.bet/10\")\n }\n var err = this.acc.execFrozen(this.name, this.context.from, match.bet)\n console.log(this.name, this.context.from, err)\n throwerr(err)\n this.kvc.add(match.id, match)\n this.kvc.addlog(match)\n return this.kvc.receipt()\n}\n\nExec.prototype.CloseGame = function(args) {\n var local = MatchLocalTable(this.kvc)\n var game = this.kvc.get(args.gameid)\n if (!game) {\n throwerr(\"game id not found\")\n }\n var querykey = local.get(\"gameid\", args)\n var matches = local.query(\"gameid\", querykey, \"\", 0, 1)\n if (!matches) {\n matches = []\n }\n var n = -1\n for (var i = 0; i < RAND_MAX; i++) {\n if (Sha256(args.randstr + i) == game.randhash) {\n n = i\n }\n }\n if (n == -1) {\n throwerr(\"err rand str\")\n }\n //.........................................................\n if (this.context.height - game.height < MIN_WAIT_BLOCK) {\n throwerr(\"close game must wait \"+MIN_WAIT_BLOCK+\" block\")\n }\n for (var i = 0; i < matches.length; i++) {\n var match = matches[i].left\n if (match.num == n) {\n //................................................................................................ this\n win.call(this, game, match)\n } else {\n fail.call(this, game, match)\n }\n }\n if (game.bet > 0) {\n var err = this.acc.execActive(this.name, game.addr, game.bet)\n throwerr(err)\n game.bet = 0\n }\n game.status = 2\n this.kvc.add(game.gameid, game)\n this.kvc.addlog(game)\n return this.kvc.receipt()\n}\n\nfunction win(game, match) {\n var amount = (RAND_MAX - 1) * match.bet\n if (game.bet - amount < 0) {\n amount = game.bet\n }\n var err \n if (amount > 0) {\n err = this.acc.execTransFrozenToActive(this.name, game.addr, match.addr, amount)\n throwerr(err, \"execTransFrozenToActive\")\n game.bet -= amount\n }\n err = this.acc.execActive(this.name, match.addr, match.bet)\n throwerr(err, \"execActive\")\n}\n\nfunction fail(game, match) {\n var amount = match.bet\n err = this.acc.execTransFrozenToFrozen(this.name, match.addr, game.addr, amount)\n throwerr(err)\n game.bet += amount\n}\n\nExec.prototype.ForceCloseGame = function(args) {\n var local = new MatchLocalTable(this.kvc)\n var game = this.kvc.get(args.id)\n if (!game) {\n throwerr(\"game id not found\")\n }\n var matches = local.getmath(args.id)\n if (!matches) {\n matches = []\n }\n if (this.context.height - game.height < 100) {\n throwerr(\"force close game must wait 100 block\")\n }\n for (var i = 0; i < matches.length; i++) {\n var match = matches[i]\n win.call(this.kvc, game, match)\n }\n if (game.bet > 0) {\n var err = this.acc.execActive(this.name, game.addr, game.bet)\n throwerr(err)\n game.bet = 0\n }\n game.status = 2\n this.kvc.add(game.gameid, game)\n this.kvc.addlog(game)\n return this.kvc.receipt()\n}\n\nExecLocal.prototype.NewGame = function(args) {\n return localprocess.call(this, args)\n}\n\nExecLocal.prototype.Guess = function(args) {\n return localprocess.call(this, args)\n}\n\nExecLocal.prototype.CloseGame = function(args) {\n return localprocess.call(this, args)\n}\n\nExecLocal.prototype.ForceCloseGame = function(args) {\n return localprocess.call(this, args)\n}\n\nfunction localprocess(args) {\n var local = MatchGameTable(this.kvc)\n local.addlogs(this.logs)\n local.save()\n return this.kvc.receipt()\n}\n\nQuery.prototype.ListGameByAddr = function(args) {\n var local = GameLocalTable(this.kvc)\n var q = local.query(\"addr\", args.addr, args.primaryKey, args.count, args.direction)\n return querytojson(q)\n}\n\nQuery.prototype.ListMatchByAddr = function(args) {\n var local = MatchGameTable(this.kvc)\n var q= local.query(\"addr#status\", args[\"addr#status\"], args.primaryKey, args.count, args.direction)\n return querytojson(q)\n}\n\nfunction GameLocalTable(kvc) {\n this.config = {\n \"#tablename\" : \"game\",\n \"#primary\" : \"gameid\",\n \"#db\" : \"localdb\",\n \"gameid\" : \"%018d\",\n \"status\" : \"%d\",\n \"hash\" : \"%s\",\n \"addr\" : \"%s\",\n }\n this.defaultvalue = {\n \"gameid\" : 0,\n \"status\" : 0,\n \"hash\" : \"\",\n \"addr\" : \"\",\n }\n return new Table(kvc, this.config, this.defaultvalue) \n}\n\nfunction MatchLocalTable(kvc) {\n this.config = {\n \"#tablename\" : \"match\",\n \"#primary\" : \"id\",\n \"#db\" : \"localdb\",\n \"id\" : \"%018d\",\n \"gameid\" : \"%018d\",\n \"hash\" : \"%s\",\n \"addr\" : \"%s\",\n }\n this.defaultvalue = {\n \"id\" : 0,\n \"gameid\" : 0,\n \"hash\" : \"\",\n \"addr\" : \"\",\n }\n return new Table(kvc, this.config, this.defaultvalue) \n}\n\nfunction MatchGameTable(kvc) {\n return new JoinTable(MatchLocalTable(kvc), GameLocalTable(kvc), \"addr#status\")\n}\n",
"name": "game"
}
}
],
"id": 0
}
Request message:
{
"jsonrpc":"2.0",
"id":0,
"method": "Chain33.CreateTransaction",
"params": [
{
"execer": "string",
"actionName": "string",
"payload": {
"name": "string",
"funcname": "string",
"args": "string"
}
}
],
}
Parameter description:
Parameter | Type | Description |
---|---|---|
execer | string | contract name,fixed here as user.jsvm.xxxx |
actionName | string | invoke, here fixed as Call |
payload.name | string | contract name defined before |
payload.funcname | string | called method name |
payload.args | string | parameter list |
Response message:
{
"id":int32,
"result":"string",
"error":null
}
Parameter description:
Parameter | Type | Description |
---|---|---|
result | string | hexadecimal encoded transaction string |
Example: Request:
{
"jsonrpc":"2.0",
"id":0,
"method": "Chain33.CreateTransaction",
"params": [
{
"execer": "user.jsvm.game",
"actionName": "Call",
"payload": {
"name": "game",
"funcname": "NewGame",
"args": "{\"bet\":10000000000, \"randhash\":\"0x5d2ac15654aa1859af80d1dfa5d7a5775c359fe3a49abce03796f4fbc313b57f\"}"
}
}
],
}
Response:
{
"id": 0,
"result": "0a0e757365722e6a73766d2e67616d651279180112750a0467616d6512074e657747616d651a647b22626574223a31303030303030303030302c202272616e6468617368223a22307835643261633135363534616131383539616638306431646661356437613537373563333539666533613439616263653033373936663466626333313362353766227d20a08d0630ae97b68eead9ec99083a2231356d6a48594e76506b534d72703353517874637a584c3663696e4d414564654b73",
"error": null
}
Request message:
{
"jsonrpc":"2.0",
"method": "Chain33.Query",
"params": [
{
"execer": "string",
"funcName": "string",
"payload": {
"name": "string",
"funcname": "string",
"args": "{\"addr\":\"14KEKbYtKKQm4wMthSK9J4La4nAiidGozt\", \"count\":20}"
}
}
],
"id": 0
}
Parameter description:
Parameter | Type | If Necessary | Description |
---|---|---|---|
execer | string | Yes | executor name, fixed here as JSVM |
funcName | string | Yes | function name, fixed here as Create |
payload.name | string | Yes | contract name |
payload.funcname | string | Yes | query method name defined in javascript |
payload.args | string | Yes | parameter list of the query method defined in javascript, expressed in json format |
Response message:
{
"id": int32,
"result": {
"data": "string"
},
"error": null
}
Parameter description:
Parameter | Type | Description |
---|---|---|
result.data | string | result |
Example: Request:
{
"method": "Chain33.Query",
"params": [
{
"execer": "jsvm",
"funcName": "Query",
"payload": {
"name": "game",
"funcname": "ListGameByAddr",
"args": "{\"addr\":\"14KEKbYtKKQm4wMthSK9J4La4nAiidGozt\", \"count\":20}"
}
}
],
"id": 0
}
Response:
{
"id": 0,
"result": {
"data": "[{\"left\":{\"__type__\":\"game\",\"addr\":\"14KEKbYtKKQm4wMthSK9J4La4nAiidGozt\",\"bet\":10000000000,\"gameid\":121400000,\"hash\":\"0xd3894f7ab60a535d8de47f8a5e06d7c127a22bfe9ff8f1e00971fae19091126f\",\"height\":1214,\"obet\":10000000000,\"randhash\":\"0x5d2ac15654aa1859af80d1dfa5d7a5775c359fe3a49abce03796f4fbc313b57f\",\"status\":1}}]"
},
"error": null
}
hello world