How to build a DApp on Nebulas (Part 2)

We have released several libraries to make it easier to write smart contracts. This means you don’t need to explicitly create any of these smart contracts from scratch, because we’ve already done the initial work for you.

In the second part of ‘How to build DApp on Nebulas’, we are going to learn about these libraries, what they do, and how to use them.

BigNumber

The BigNumber module uses bignumber.js, a JavaScript library for arbitrary-precision decimal and non-decimal arithmetics. The contract can use BigNumber directly to handle the value of the transaction and other value transfers.

var value = new BigNumber(0);
value.plus(1);
...

Storage

The storage module enables data storage on Nebulas. More precisely, it enables the permanent storage of state variables on Nebulas when a payment is made, i.e. GAS, similar to a traditional key-value storage system. The LocalContractStorage object is the built-in storage object in Nebulas you can use, and accept numbers, strings and JavaScript objects in string format. Only the contract that stores this data can access and manipulate it.

Basics

LocalContractStorage supports three operations, i.e., set, get and del, which allow you to store, read and delete data:

"use strict";
var BankVaultContract = function () {
// nothing
};
BankVaultContract.prototype = {
init: function() {
// nothing
},
set: function(name, value) { // name="robin", value=10000
LocalContractStorage.set("name", name);
// 'put' is equivalent operation to 'set'
LocalContractStorage.put("value", value);
},
get: function() {
var name = LocalContractStorage.get("name");
console.log(name); // prints 'robin'
var value = LocalContractStorage.get("value");
console.log(value); // prints '10000'
},
del: function() {
var result = LocalContractStorage.del("name");
console.log(result); // prints 'robin'
// 'delete' is equivalent operation to 'del'
result = LocalContractStorage.delete("value");
console.log(result); // prints '10000'

//after deletion data could not be read anymore
}
};
module.exports = BankVaultContract;

Advanced

In addition to the above basic usages, LocalContractStorage also supports defining storage properties and storing maps to objects, as well as serialization methods.

  • Storage Properties

A contract property can be bound to a storage property, where both reading and writing of the contract are performed on the LocalContractStorage. There are two methods to define such a binding:

// bind a object property named `fieldName` to `obj` with descriptor.
// default descriptor is JSON.parse()/JSON.stringify(). Whether descriptor is 'null' or 'undefined', the default one will be used.
// return this.
defineProperty(obj, fieldName, [descriptor]);

// bind multiple properties to `obj` in a batch.
// return this.
defineProperties(obj, {
fieldName1: descriptor1,
fieldName2: descriptor2
});

Usually, we map contract properties to store bits in the initialization like this:

"use strict";
var BankVaultContract = function () {
// due to 'null', the default descriptor will be used.
LocalContractStorage.defineProperty(this, "name1", null);

// a custom `descriptor` implementation.
// return BigNumber object during parsing.
LocalContractStorage.defineProperty(this, "value1", {
stringify: function (obj) {
return obj.toString();
},
parse: function (str) {
return new BigNumber(str);
}
});

// batch binding with default serialization implementation.
LocalContractStorage.defineProperties(this, {
name2: null,
value2: null
});
};
module.exports = BankVaultContract;

After that, you can read and write the binding properties like you are directly accessing the storage:

BankVaultContract.prototype = {
init: function(name, value) { // name="robin", value=1
this.name1 = name;
this.value1 = value;
},
testStorage: function(name, value) { // name="ROBIN", value=2
this.name2 = name;
this.value2 = value;

bool r = this.value1.lessThan(new BigNumber(0));
console.log(this.name1 + ":" + r); // robin:false
console.log(this.name2 + ":" + this.value2); // ROBIN:2
}
};
  • Storing Map Data

Nebulas storage implements a map structure with del/delete, get, set/put operators for some scenarios where you need to store key-value data. To achieve that, you can define the contract property as a map. Again, there are two methods to do this:

// single binding, default descriptor implementation is the same with defineProperty.
// return this.
defineMapProperty(obj, mapName, [descriptor]);

// batch binding.
// return this.
defineMapProperties(obj, {
mapName1: descriptor1,
mapName2: descriptor2
});

Lets see a sample of how to use maps:

'use strict';

var BankVaultContract = function () {
LocalContractStorage.defineMapProperty(this, "userMap");

LocalContractStorage.defineMapProperty(this, "userBalanceMap", {
stringify: function (obj) {
return obj.toString();
},
parse: function (str) {
return new BigNumber(str);
}
});

LocalContractStorage.defineMapProperties(this,{
key1Map: null,
key2Map: null
});
};

BankVaultContract.prototype = {
init: function () {
},
testStorage: function () {
this.userMap.set("robin", "1");
this.userBalanceMap.set("robin",new BigNumber(1));
},
testRead: function () {
//Read and store data
var balance = this.userBalanceMap.get("robin");
this.key1Map.set("robin", balance.toString());
this.key2Map.set("robin", balance.toString());
}
};

module.exports = BankVaultContract;

Blockchain

The Blockchain module is used to obtain the transaction and block within the currently executing contract. Also, NAS can be transferred from the contract and the address verification is provided.

Blockchain has two properties:

  1. block current block for contract execution with attributes:

timestamp block timestamp

hash block hash

height block height

2. transaction current transaction for contract execution with attributes:

hash transaction hash

from transaction from address

to transaction to address

value transaction value, a BigNumber object for contract use

nonce transaction nonce

timestamp transaction timestamp

gasPrice transaction gasPrice, a BigNumber object for contract use

gasLimit transaction gasLimit, a BigNumber object for contract use

And Blockchain provides two methods:

  1. transfer(address, value) transfers NAS from contract to address.
  • param address: Nebulas address to receive NAS
  • param value: transferred value, a BigNumber object

return: 0-transfer success, 1-transfer failed.

2. verifyAddress(address) verifies if the parameter address is a valid Nebulas address.

return: 1-address is valid, 0-address is invalid.

Here’s a simple example of this module implemented:

'use strict';

var BankVaultContract = function () {};

BankVaultContract.prototype = {
init: function () {
console.log('init: Blockchain.block.height = ' + Blockchain.block.height);
console.log('init: Blockchain.transaction.from = ' + Blockchain.transaction.from);
},

transfer: function (address, value) {
var result = Blockchain.transfer(address, value);
console.log("transfer result:", result);
},

verifyAddress: function (address) {
var result = Blockchain.verifyAddress(address);
console.log("verifyAddress result:", result);
}
};

module.exports = BankVaultContract;

Event

The Event module is used to record execution events in the contract. The recorded events are stored in the event trie on the chain, which can be fetched by rpc.getEventsByHash with the execution transaction hash. All contract event topics have a chain.contract. prefix with user-defined topics. The usage is:

Event.Trigger(topic, obj);
  • topic: user-defined topic
  • obj: JSON object

Here is the sample:

'use strict';

var BankVaultContract = function () {};

BankVaultContract.prototype = {
init: function () {},
testEvent: function() {
// the stored topic is actually "chain.contract.topic"
Event.Trigger("topic", {
Data: {
value: "Event test."
}
});
}
};

module.exports = BankVaultContract;

Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. console will print all args it received to the Nebulas Logger at a specific level associated with the name of the invoked method.

  • console.log([…args<any>]) — — info level
  • console.debug([…args<any>]) — — debug level
  • console.warn([…args<any>]) — — warn level
  • console.error([…args<any>]) — — error level
  • console.info([…args<any>]) — — alias for console.log()

We are now done talking about the leading feature modules. Next, we’ll take a look at calling contract functions.

How to call a contract?

The recommended approach is to join the Nebulas mainnet or testnet by launching a local node. Here is a quick start guide on how to do that.

Once the node is started, you should first unlock your own account once with unlockAccount() before calling any contract function:

// Request
curl -i -H 'Content-Type: application/json' -X POST http://localhost:8685/v1/admin/account/unlock -d '{"address":"n1czGUvbQQton6KUWga4wKDLLKYDEn39mEk","passphrase":"passphrase","duration":"1000000000"}'

// Result
{
"result":{
"result":true
}
}

Then you can use the sendTransaction() method to invoke the smart contract.

For example, call testEvent()in the previous sample contract:

// Request
curl -i -H 'Accept: application/json' -X POST http://localhost:8685/v1/admin/transaction -H 'Content-Type: application/json' -d '{"from":"n1NZttPdrJCwHgFN3V6YnSDaD5g8UbVppoC","to":"n1qsgj2C5zmYzS9TSkPTnp15bhCCocRPwno", "value":"100","nonce":8,"gasPrice":"1000000","gasLimit":"2000000","contract":{"function":"testEvent","args":"[]"}}'

// Result
{
"result":{"txhash":"b55358c2e12c1d48d4e6beaee7002a59138294fb2896ea8059ff5277553af59f","contract_address":""}
}

For more about the RPC manual, see the user API and admin API.

What comes next?

In the upcoming article, we will share a new feature of smart contract, i.e. the “Accept” function.