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:
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:
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 topicobj
: 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.