Contracts
Learn how to create and deploy contracts on the Trac Network.
Smart Contracts consist of two elements:
The protocol — tells the peer how to pass transaction data and how to behave.
The contract — the actual contract logic, "listening" to the terms setup in the protocol.
Anatomy of an example Protocol/Contract pair
Please read the code comments in the files below. Examples are taken from our Example Contract Github repo.
Protocol
import {Protocol} from "trac-peer";
class SampleProtocol extends Protocol{
/**
* Extending from Protocol inherits its capabilities and allows you to define your own protocol.
* The protocol supports the corresponding contract. Both files come in pairs.
*
* Instances of this class do NOT run in contract context. The constructor is only called once on Protocol
* instantiation.
*
* this.peer: an instance of the entire Peer class, the actual node that runs the contract and everything else.
* this.base: the database engine, provides await this.base.view.get('key') to get unsigned data (not finalized data).
* this.options: the option stack passed from Peer instance.
*
* @param peer
* @param base
* @param options
*/
constructor(peer, base, options = {}) {
// calling super and passing all parameters is required.
super(peer, base, options);
}
/**
* The Protocol superclass ProtocolApi instance already provides numerous api functions.
* You can extend the built-in api based on your protocol requirements.
*
* @returns {Promise<void>}
*/
async extendApi(){
this.api.getSampleData = function(){
return 'Some sample data';
}
}
/**
* In order for a transaction to successfully trigger,
* you need to create a mapping for the incoming tx command,
* pointing at the contract function to execute.
*
* You can perform basic sanitization here, but do not use it to protect contract execution.
* Instead, use the built-in schema support for in-contract sanitization instead
* (Contract.addSchema() in contract constructor).
*
* @param command
* @returns {{type: string, value: *}|null}
*/
mapTxCommand(command){
// prepare the payload
let obj = { type : '', value : null };
/*
Triggering contract function in terminal will look like this:
/tx --command 'something'
You can also simulate a tx prior broadcast
/tx --command 'something' --sim 1
To programmatically execute a transaction from "outside",
the api function "this.api.tx()" needs to be exposed by adding
"api_tx_exposed : true" to the Peer instance options.
Once exposed, it can be used directly through peer.protocol_instance.api.tx()
Please study the superclass of this Protocol and Protocol.api to learn more.
*/
if(command === 'something'){
// type points at the "storeSomething" function in the contract.
obj.type = 'storeSomething';
// value can be null as there is no other payload, but the property must exist.
obj.value = null;
// return the payload to be used in your contract
return obj;
} else {
/*
now we assume our protocol allows to submit a json string with information
what to do (the op) then we pass the parsed object to the value.
the accepted json string can be executed as tx like this:
/tx --command '{ "op" : "do_something", "some_key" : "some_data" }'
Of course we can simulate this, as well:
/tx --command '{ "op" : "do_something", "some_key" : "some_data" }' --sim 1
*/
const json = this.safeJsonParse(command);
if(json.op !== undefined && json.op === 'do_something'){
obj.type = 'submitSomething';
obj.value = json;
return obj;
}
}
// return null if no case matches.
// if you do not return null, your protocol might behave unexpected.
return null;
}
/**
* Prints additional options for your protocol underneath the system ones in terminal.
*
* @returns {Promise<void>}
*/
async printOptions(){
console.log(' ');
console.log('- Sample Commands:');
console.log("- /print | use this flag to print some text to the terminal: '--text \"I am printing\"");
// further protocol specific options go here
}
/**
* Extend the terminal system commands and execute your custom ones for your protocol.
* This is not transaction execution itself (though can be used for it based on your requirements).
* For transactions, use the built-in /tx command in combination with command mapping (see above)
*
* @param input
* @returns {Promise<void>}
*/
async customCommand(input) {
await super.tokenizeInput(input);
if (this.input.startsWith("/print")) {
const splitted = this.parseArgs(input);
console.log(splitted.text);
}
}
}
export default SampleProtocol;
Contract
import {Contract} from 'trac-peer'
class SampleContract extends Contract {
/**
* Extending from Contract inherits its capabilities and allows you to define your own contract.
* The contract supports the corresponding protocol. Both files come in pairs.
*
* Instances of this class run in contract context. The constructor is only called once on Peer
* instantiation.
*
* Please avoid using the following in your contract functions:
*
* No try-catch
* No throws
* No random values
* No http / api calls
* No super complex, costly calculations
* No massive storage of data.
* Never, ever modify "this.op" or "this.value", only read from it and use safeClone to modify.
* ... basically nothing that can lead to inconsistencies akin to Blockchain smart contracts.
*
* Running a contract on Trac gives you a lot of freedom, but it comes with additional responsibility.
* Make sure to benchmark your contract performance before release.
*
* If you need to inject data from "outside", you can utilize the Feature class and create your own
* oracles. Instances of Feature can be injected into the main Peer instance and enrich your contract.
*
* In the current version (Release 1), there is no inter-contract communication yet.
* This means it's not suitable yet for token standards.
* However, it's perfectly equipped for interoperability or standalone tasks.
*
* this.protocol: the peer's instance of the protocol managing contract concerns outside of its execution.
* this.options: the option stack passed from Peer instance
*
* @param protocol
* @param options
*/
constructor(protocol, options = {}) {
// calling super and passing all parameters is required.
super(protocol, options);
// simple function registration.
// since this function does not expect value payload, no need to sanitize.
// note that the function must match the type as set in Protocol.mapTxCommand()
this.addFunction('storeSomething');
// now we register the function with a schema to prevent malicious inputs.
// the contract uses the schema generator "fastest-validator" and can be found on npmjs.org.
//
// Since this is the "value" as of Protocol.mapTxCommand(), we must take it full into account.
// $$strict : true tells the validator for the object structure to be precise after "value".
//
// note that the function must match the type as set in Protocol.mapTxCommand()
this.addSchema('submitSomething', {
value : {
$$strict : true,
$$type: "object",
op : { type : "string", min : 1, max: 128 },
some_key : { type : "string", min : 1, max: 128 }
}
});
// in preparation to add an external Feature (aka oracle), we add a loose schema to make sure
// the Feature key is given properly. it's not required, but showcases that even these can be
// sanitized.
this.addSchema('feature_entry', {
key : { type : "string", min : 1, max: 256 },
value : { type : "any" }
});
// now we are registering the timer feature itself (see /features/time/ in package).
// note the naming convention for the feature name <feature-name>_feature.
// the feature name is given in app setup, when passing the feature classes.
const _this = this;
// this feature registers incoming data from the Feature and if the right key is given,
// stores it into the smart contract storage.
// the stored data can then be further used in regular contract functions.
this.addFeature('timer_feature', async function(){
if(false === _this.validateSchema('feature_entry', _this.op)) return;
if(_this.op.key === 'currentTime') {
if(null === await _this.get('currentTime')) console.log('timer started at', _this.op.value);
await _this.put(_this.op.key, _this.op.value);
}
});
// last but not least, you may intercept messages from the built-in
// chat system, and perform actions similar to features to enrich your
// contract. check the _this.op value after you enabled the chat system
// and posted a few messages.
this.messageHandler(async function(){
console.log('message triggered contract', _this.op);
});
}
/**
* A simple contract function without values (=no parameters).
*
* Contract functions must be registered through either "this.addFunction" or "this.addSchema"
* or it won't execute upon transactions. "this.addFunction" does not sanitize values, so it should be handled with
* care or be used when no payload is to be expected.
*
* Schema is recommended to sanitize incoming data from the transaction payload.
* The type of payload data depends on your protocol.
*
* This particular function does not expect any payload, so it's fine to be just registered using "this.addFunction".
*
* However, as you can see below, what it does is checking if an entry for key "something" exists already.
* With the very first tx executing it, it will return "null" (default value of this.get if no value found).
* From the 2nd tx onwards, it will print the previously stored value "there is something".
*
* It is recommended to check for null existence before using put to avoid duplicate content.
*
* As a rule of thumb, all "this.put()" should go at the end of function execution to avoid code security issues.
*
* Putting data is atomic, should a Peer with a contract interrupt, the put won't be executed.
*/
async storeSomething(){
const something = await this.get('something');
console.log('is there already something?', something);
if(null === something) {
await this.put('something', 'there is something');
}
}
/**
* Now we are using the schema-validated function defined in the constructor.
*
* The function also showcases some of the handy features like safe functions
* to prevent throws and safe bigint/decimal conversion.
*/
async submitSomething(){
// the value of some_key shouldn't be empty, let's check that
if(this.value.some_key === ''){
return new Error('Cannot be empty');
// alternatively false for generic errors:
// return false;
}
// of course the same works with assert (always use this.assert)
this.assert(this.value.some_key !== '', new Error('Cannot be empty'));
// btw, please use safeBigInt provided by the contract protocol's superclass
// to calculate big integers:
const bigint = this.protocol.safeBigInt("1000000000000000000");
// making sure it didn't fail
this.assert(bigint !== null);
// you can also convert a bigint string into its decimal representation (as string)
const decimal = this.protocol.fromBigIntString(bigint.toString(), 18);
// and back into a bigint string
const bigint_string = this.protocol.toBigIntString(decimal, 18);
// let's clone the value
const cloned = this.protocol.safeClone(this.value);
// we want to pass the time from the timer feature.
// since mmodifications of this.value is not allowed, add this to the clone instead for storing:
cloned['timestamp'] = await this.get('currentTime');
// making sure it didn't fail (be aware of false-positives if null is passed to safeClone)
this.assert(cloned !== null);
// and now let's stringify the cloned value
const stringified = this.protocol.safeJsonStringify(cloned);
// and, you guessed it, best is to assert against null once more
this.assert(stringified !== null);
// and guess we are parsing it back
const parsed = this.protocol.safeJsonParse(stringified);
// parsing the json is a bit different: instead of null, we check against undefined:
this.assert(parsed !== undefined);
// finally we are storing what address submitted the tx and what the value was
await this.put('submitted_by/'+this.address, parsed.some_key);
// printing into the terminal works, too of course:
console.log('submitted by', this.address, parsed);
}
}
export default SampleContract;
Last updated