Contracts

Learn how to create and deploy contracts on the Trac Network.

Smart Contracts consist of two elements:

  1. The protocol — tells the peer how to pass transaction data and how to behave.

  2. 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