Skip to content

DApp Development in JavaScript

linj edited this page Nov 23, 2022 · 1 revision

[TOC]

1 DApp Development in JavaScript

  • 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。

1.1 Write Your Own Contract in JavaScript

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.

1.1.1 Define Storage Structure

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")
}

1.1.2 Init Function

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()
}

1.1.3 A Certain Method of Exec

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()
}

1.1.4 A Certain Method of ExecLocal

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)
}

1.1.5 A Certain Method of Query

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)
}

1.2 Deploy Smart Contracts Using Cli Debugging Tools

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.

1.3 Call the JSONRPC Interface to Deploy Smart Contract

1.3.1 Deploy a JSVM Contract CreateTransaction

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
}

1.3.2 Calls the method of the created JSVM contract CreateTransaction

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
}

1.3.3 Query the Created JSVM Contract

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
}
Clone this wiki locally