Only this pageAll pages
Powered by GitBook
1 of 34

Obyte developer resources

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Tutorials for newcomers

Step by step tutorials to get started with example bots.

Type of repositories on Obyte Github page

Obyte Github page is located there https://github.com/byteball

Obyte repository dependency graph

ocore - main core library

  • syncs full database if full node.

  • relays storage units if full node.

obyte-gui-wallet - wallet apps with GUI

  • inherits ocore library.

  • light node or full node.

  • wallet functionality.

  • graphical user interface for Windows, MacOS, Linux, Android and iOS.

headless-obyte - wallet for server without GUI

  • inherits ocore library.

  • light node or full node.

  • wallet functionality.

  • can be controlled via chat.

obyte-witness - nodes that determine the main chain

  • inherits headless-obyte and ocore library.

  • only full node.

  • recommended to be behind TOR.

obyte-relay - peer that can be connected directly

  • inherits ocore library.

  • only full node.

  • accepts incoming connections.

  • provides new units, stable units, balances & suitable parents to light nodes.

obyte-hub - hub that relays chat messages

  • inherits obyte-relay and ocore library.

  • relays encrypted chat messages between devices.

  • notifies wallet apps about new app version.

bot-example - template for new projects

  • inherits headless-obyte and ocore library.

  • customized can be added.

  • customized actions on and can be added.

Tutorials to setup a node

Examples of using these libraries

Configuration

These settings are applicable to most Obyte nodes projects that have 'headless-obyte' or 'ocore' library as dependency.

The default settings are in the core library's conf.js file, they can be overridden in your project root's conf.js and then in conf.json in the app data folder. The app data folder is:

  • macOS: ~/Library/Application Support/<appname>

  • Linux: ~/.config/<appname>

  • Windows: %LOCALAPPDATA%\<appname>

<appname> is name in your package.json, e.g. headless-obyte.

Database location

At some point you'll surely want to have a peek into the database, or even make a modification in it. Default SQLite database file and RocksDB folder are located near the config file, you can find the correct paths for each platform above.

Testnet network

Most bot repositories have example .env.testnet file in their root folder. In order to connect use testnet network, just run cp .env.testnet .env. Backup and delete the database if you already ran it on MAINNET. Wallet app for website.

If you don't have .env.testnet example file then just create a .env file that contains this row:

Settings

This is the list of some of the settings that the library understands (your app can add more settings that only your app understands):

conf.port

The port to listen on. If you don't want to accept incoming connections at all, set port to null, which is the default. If you do want to listen, you will usually have a proxy, such as nginx, accept websocket connections on standard port 443 and forward them to your Obyte daemon that listens on port 6611 on the local interface.

conf.storage

Storage backend -- mysql or sqlite, the default is sqlite. If sqlite, the database files are stored in the app data folder. If mysql, you need to also initialize the database with and set connection params:

MySQL conf for faster syncing

To lower disk load and increase sync speed, you can optionally disable flushing to disk every transaction, instead doing it once a second. This can be done by setting innodb_flush_log_at_trx_commit=0 in your MySQL server config file (my.ini)

conf.bLight

Work as light node (true) or full node (false). The default is full client. Light node only holds data that is relevant to your wallet, full node sync all the data in public database. Benefit of light node is that you can start using it immediately, but the downside is that you might not get updates about new transactions and confirmations as fast as full node does.

conf.bServeAsHub

Whether to serve as hub on the Obyte network (store and forward e2e-encrypted messages for devices that connect to your hub). The default is false.

conf.myUrl

If your node accepts incoming connections, this is its URL. The node will share this URL with all its outgoing peers so that they can reconnect in any direction in the future. By default the node doesn't share its URL even if it accepts connections.

conf.bWantNewPeers

Whether your node wants to learn about new peers from its current peers (true, the default) or not (false). Set it to false to run your node in stealth mode so that only trusted peers can see its IP address (e.g. if you have online wallets on your server and don't want potential attackers to learn its IP).

conf.socksHost, conf.socksPort, and conf.socksLocalDNS

Settings for connecting through optional SOCKS5 proxy. Use them to connect through TOR and hide your IP address from peers even when making outgoing connections. This is useful and highly recommended when you are running an online wallet on your server and want to make it harder for potential attackers to learn the IP address of the target to attack. Set socksLocalDNS to false to route DNS queries through TOR as well.

Email notifications

Most bots out there expect user's machine to have UNIX sendmail and by default sendmail function in mail module will try to use that, but it is possible to configure your node to use SMTP relay. This way you could use Gmail or Sendmail SMTP server or even something like Mailtrap.io (excellent for testing purposes if you don't want the actual email recipient to receive your test messages). This is how the configuration would look:

smtpTransport can take one of three values:

  • local: send email using locally installed sendmail. Normally, sendmail is not installed by default and when installed, it needs to be properly configured to actually send emails. If you choose this option, no other conf settings are required for email. This is the default option.

  • direct: send email by connecting directly to the recipient's SMTP server. This option is not recommended.

Accepting incoming connections

Obyte network works over secure WebSocket protocol wss://. To accept incoming connections, you'll need a valid TLS certificate (you can get a free one from ) and a domain name (you can get a free domain from ). Then you accept connections on standard port 443 and proxy them to your locally running Obyte daemon.

This is an example configuration for nginx to accept websocket connections at wss://obyte.one/bb and forward them to locally running daemon that listens on port 6611:

If your server doesn't support IPv6, comment or delete the two lines containing [::] or nginx won't start

By default Node limits itself to 1.76GB the RAM it uses. If you accept incoming connections, you will likely reach this limit and get this error after some time:

To prevent this, increase the RAM limit by adding --max_old_space_size=<size> to the launch command where size is the amount in MB you want to allocate.

For example --max-old-space-size=4096, if your server has at least 4GB available.

Checking if the node is running

Sending the notification to admin when node stops

Many bots have a file named included in the repository, which looks like this

This can be executed with crontab with the command mentioned in and when the email is properly configured on the server and the node crashes, this script will notify you that the node went down.

If you are executing your node with node start.js --max-old-space-size=4096 command then you should also change the checkDaemonAndNotify parameter.

Restarting the node automatically when it stops

If you are running a wallet-less node (Hub, Relay or Explorer) or password-less headless wallet (conf.bNoPassphrase=true) then you can restart the node automatically when it stops. In order to do that, you could use checkDaemonAndRestart function, instead of checkDaemonAndNotify.

The first parameter in checkDaemonAndRestart is the process that gets searched from ps x command response, the second parameter is a command that will get executed when node has stopped.

Above example is for wallet-less node, which will log the stdout and stderr (>> for appending) to file (). If the command uses parameters like --max-old-space-size=4096 then these should be added to both parameters, but output directing (1>log and 2>>err) should not be added to first parameter.

If you are using NVM to manage your Node.js versions then you might need to add PATH and LD_LIBRARY_PATH to your crontab configuration as well, otherwise crontab might not be able to restart the node.

Autonomous Agents

An Autonomous Agent (AA) is a special address (account) on the ledger that acts according to a program associated with it. Its behavior is similar to that of a vending machine that receives coins and data entered on a keypad and in response, releases a cup of coffee, plays a song, or does whatever it was programmed to do.

Getting started developer guide

Introduction to Autonomous Agents (announcement post on Medium)

Oscript — the language of autonomous agents

Autonomous Agents are written in Oscript — a new programming language developed specifically for this purpose. The language is very simple and any developer who has experience in curly-brace languages such as JavaScript, PHP, Java, C/C++, etc should not have any difficulty learning it.

Some of the features of the language:

  • convenient access to variables that describe the state of the ledger and the triggering (requesting, activating) transaction received. That’s what makes the language domain-specific. In particular, the following variables are available:

  • amounts received in the triggering transaction;

  • data received in the triggering transaction;

AA code can be deployed with and . There is also a and .

Textcoins

Textcoins are a handy way of transfering money and asset to other people, in a text form.

Textcoin is a payment sent though email or other text based media, such as chat apps. Textcoins can also be printed on paper, scratch cards, or PIN envelopes and passed on like paper wallets.

Sending textcoins with bot

Sending textcoins from server-side (headless) wallets is easy: you use the same functions you normally use to send to Obyte addresses but instead of the recipient's Obyte address you write:

Prosaic contracts

Prosaic contracts are the feature of Obyte platform that helps people to make an agreement on, basically, anything, as text of contracts is a simple text. The benefit of using Obyte prosaic contracts is that after both sides of the contract sign it, a unit, which in fact is a public proof of contract agreement, is posted into DAG, which contains a hash of contract text, while being signed by both peers of the contract. So anyone can check the fact of agreement on the exact same text by those two contract peers. The contract text itself is not revealed but stored in peers wallets.

To offer a prosaic contract from your bot, you have to follow these steps:

  • user pairs with your bot.

  • you ask the user to send his payment address (it will be included in the contract) in the chat.

Obyte for merchants

Obyte has a payment gateway, Woocommerce plugin and cashback program for merchants who would like to accept Bytes for goods or services.

relay: send email through a relay server, like most email apps do. You need to also configure the server's host smtpRelay, its port smtpPort if it differs from the default port 25, and smtpUser and smtpPassword for authentication to the server.
TESTNET can be downloaded from Obyte.org
initial .sql file
letsencrypt.org
Freenom
check_daemon.js
crontab.txt
headless wallet has logging built-in
textcoin:userEmailAddress for sending to email, or
  • textcoin:someUniqueIdentifierOtherThanEmail for sending via any other text based media

  • Examples: textcoin:[email protected] or textcoin:ASDFGHJKL.

    Sample code:

    Sending textcoin emails

    The opts object has an optional field email_subject which is the subject of the email sent to the user. To send emails, you need to set the following fields in your configuration files:

    • from_email: the address your emails are sent from. Must be on your domain, otherwise the emails are likely to be non-delivered or labeled as spam

    • smtpTransport: type of SMTP transport used to deliver emails:

      • local: (this is the default) use /usr/sbin/sendmail

      • direct: connect directly to the recipient's MX server, this minimizes the number of intermediaries who are able to see and potentially steal the textcoin, but this method is not reliable as there are no retries

      • relay: send through a relay server. In this case, you need to also set its host name in smtpRelay. If it requires authentication, you need to also set smtpUser and smtpPassword.

    Callback response

    The callback of issueChangeAddressAndSendMultiPayment and all similar functions has an optional 3rd parameter assocMnemonics which is an associative array of all generated textcoins (BIP39 mnemonics) indexed by the address from the input parameters.

    All the keys of the array have a textcoin: prefix, like in input parameters.

    If you are sending though any media other than email, you use assocMnemonics to find mnemonics generated for each output address (in the example above, you would find it by the key textcoin:ASDFGHJKL) and then deliver them yourself.

    For example, if you are writing a Telegram bot that rewards users for certain actions, you create a clickable link by prefixing the mnemonic with https://obyte.org/#textcoin?:

    and send it in chat.

    More resources

    If you are going to print a paper wallet from an ATM, you just print the mnemonic and optionally encode textcoin words in the QR code. You could create your own QR code generator on website too with jQuery.

    More about textcoins in Obyte blog https://medium.com/obyte/sending-cryptocurrency-to-email-5c9bce22b8a9

    Example code in a Telegram bot that sends users textcoins for passing a quiz: https://github.com/byteball/telegram-quiz/blob/master/src/wallet.js

    Example code that creates a list of textcoins for subsequent mass sending (e.g. via MailChimp): https://github.com/byteball/headless-obyte/blob/master/tools/create_textcoins_list.js

    you send the user the private profile of the other side of the contract.

  • you create a new prosaic contract using the user's address and your address as parties of the contract (also defining contract's title, text and how long the offer is valid).

  • you send specially formatted chat message with the contract to the user and wait for the response on it ('accept' or 'decline').

  • the user views the contract in his wallet and agrees or declines it.

  • on receiving 'accepted' response, you generate new shared address with special definition ('and' between two addresses).

  • then you top up this shared address to compensate fees.

  • you initiate posting a 'data' message from this shared address, containing contract text hash. User will then be asked to sign this transaction from the same shared address (as he is a part of its definition).

  • after getting the user's signature for the 'data' unit, you post it into DAG. Mission accomplished.

  • We have a special wrapper API for all of these steps in ocore/prosaic_contract_api.js, which in turn is calling ocore/prosaic_contract.js methods. For simplicity, here is an example of using *_api.js, you can check it's code to get a better understanding of what's going on.

    callbacks object consists of four optional callback functions, which called in following sequence:

    • onOfferCreated is called right after the contract was created and sent to peer. Always called.

    • onResponseReceived is called when peer respond to the contract, supplying the response in accepted bool argument.

    • onSigned indicates that the contract was successfully signed and unit with contract's hash was posted into DAG.

    • onError can only happen when handling the response, if your bot did receive any response, either onError or onSigned will be called.

    The core module for prosaic contracts is ocore/prosaic_contract.js. You can directly use it's exported methods, so check the content of it first.

    testnet=1
    conf.json
    {
        "storage": "mysql",
        "database": {
            "host"     : "localhost",
            "user"     : "DATABASE_USER",
            "password" : "DATABASE_PASSWORD",
            "name"     : "DATABASE_NAME"
        }
    }
    conf.json
    {
        "smtpTransport": "relay",
        "smtpRelay": "smtp.mailtrap.io",
        "smtpUser": "MAILTRAP_INBOX_USER",
        "smtpPassword": "MAILTRAP_INBOX_PASSWORD",
        "smtpSsl": false,
        "smtpPort": 2525,
        "admin_email": "[email protected]",
        "from_email": "[email protected]"
    }
    server {
        listen 80 default_server;
        listen [::]:80 default_server;
        listen 443 ssl;
        listen [::]:443 ssl;
        ssl_certificate "/etc/letsencrypt/live/obyte.one/fullchain.pem";
        ssl_certificate_key "/etc/letsencrypt/live/obyte.one/privkey.pem";
    
        if ($host != "obyte.one") {
            rewrite ^(.*)$ https://obyte.one$1 permanent;
        }
        if ($https != "on") {
            rewrite ^(.*)$ https://obyte.one$1 permanent;
        }
    
        location = /bb {
            proxy_pass http://localhost:6611;
            proxy_http_version 1.1;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }
    
        root /var/www/html;
        server_name _;
    }
    FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
    1: node::Abort() [node]
    ...
    ...
    12: 0x3c0f805c7567
    Out of memory
    const check_daemon = require('ocore/check_daemon.js');
    check_daemon.checkDaemonAndNotify('node start.js');
    const check_daemon = require('ocore/check_daemon.js');
    check_daemon.checkDaemonAndRestart('node start.js', 'node start.js 1>log 2>>err');
    var headlessWallet = require('headless-obyte');
    
    let opts = {
        asset: null, 
        amount: 1000, // bytes
        to_address: "textcoin:[email protected]",
        email_subject: "Payment in textcoin"
    };
    
    headlessWallet.issueChangeAddressAndSendMultiPayment(opts, (err, unit, assocMnemonics) => {
        ....
    });
    {
        "textcoin:[email protected]": "barely-album-remove-version-endorse-vocal-weasel-kitten-when-fit-elbow-crop"
    }
    https://obyte.org/#textcoin?barely-album-remove-version-endorse-vocal-weasel-kitten-when-fit-elbow-crop
    start.js
    "use strict";
    const headlessWallet = require('headless-obyte');
    const eventBus = require('ocore/event_bus.js');
    const prosaic_contract = require('headless-obyte/prosaic_contract_api.js');
    
    function onReady() {
        prosaic_contract.listenForPendingContracts(headlessWallet.signWithLocalPrivateKey);
    
        let contract_text = "I pay Tom $20, if he sends me a pair of his Air Jordans.";
        let contract_title = "Air Jordans Purchase from Tom";
        let contract_is_valid_until = 24; //hours
    
        headlessWallet.readFirstAddress(my_address => {
            let peer_address = "62SU7724ME5MWB7VEP64VURYZHDRSDWN";
            let peer_device_address = "0PUQK5HOEKS4NONGL2ITJI3HMJ76P3LKI";
            prosaic_contract.offer(contract_title,
                                    contract_text,
                                    my_address,
                                    peer_address,
                                    peer_device_address,
                                    contract_is_valid_until,
                                    [],
                                    headlessWallet.signWithLocalPrivateKey, {
                onOfferCreated: function(objContract) {
                    console.log("on offer created", objContract);
                },
                onResponseReceived: function(accepted) {
                    console.log("on response received", accepted);
                },
                onSigned: function(objContract) {
                    console.log("on signed", objContract);
                },
                onError: function(error) {
                    console.log("on error", error);
                }
            });
        });
    };
    eventBus.once('headless_wallet_ready', onReady);

    can be controlled via JSON-RPC service.

    post new units (moves itself funds or posts oracle data) serially.
  • notifies wallet apps about new witnesses.

  • shows list of chat bots that are available to pair with.
  • shows metadata about assets.

  • can be used as a base of any new project that needs Obyte wallet functionality.

    chat messages handling
    new transactions
    stable transactions
    Setting up headless wallet (Debian/Ubuntu Linux)
    Setting up headless wallet with Docker image
    Setting up headless wallet on Raspberry Pi
    Ping-poing payments
    Logging into website
    Weather oracle
    Bet on weather bot
    who sent the triggering transaction;
  • state variables of the current and other AAs;

  • data feeds;

  • attestations;

  • balances of this and other AAs;

  • information about assets;

  • arithmetic operations;

  • logical operations (AND, OR, etc);

  • comparisons;

  • concatenations;

  • some math functions;

  • some cryptography functions for calculating hashes and validating signatures created off-chain;

  • branching with if/else;

  • no loops (just iteration methods for objects and arrays);

  • no recursive functions;

  • scalar and object data types;

  • generation of deterministic pseudo-random number from seed.

  • Oscript language reference for Autonomous Agents
    Oscript
    (mainnet)
    editor
    Oscript
    (testnet)
    editor
    extension for VS Code
    Autonomous Agent testkit

    Payments and transactions

    One of the core features that almost every existing Obyte chatbot has is sending and receiving payments. This enables bot developers immediately add payment to the service they could be providing with

    Requesting payments

    When you include a valid Obyte address anywhere in the text of your response to the user, the address will be automatically highlighted in the user's chat window, and after clicking it the user will be able to pay arbitrary amount of arbitrary asset to this address.

    When you want to request a specific amount of a specific asset, format your payment request this way:

    Example:

    Amount is in the smallest units, such as bytes. If you omit

    Contracts

    Obyte provides users with the ability to create Smart Contracts as well as simple prosaic contracts.

    There are two ways of making a contract between two Obyte users: either to agree on some conditions and formalize them in our Smart Contract Language, which later can be evaluated by machine code and decide who spends the money from the contract (), or just publish a hash of simple prosaic text of a contract into DAG ().

    If you are looking to build more complex dApps that are as capable as Ethereum smart-contracts, then you need to use .

    Smart Contracts
    Prosaic Contracts
    Autonomous Agents

    Websocket API

    WebSocket API is a technology that makes it possible to open a communication session between the nodes. Normally you would use the Core library (ocore) or Web library (obyte.js) to make these calls.

    JSON-RPC

    JSON-RPC is a remote procedure call protocol encoded in JSON. It allows multiple calls to be sent to the server which may be answered out of order.

    &asset=...
    part, base asset (bytes) is assumed. If you want to request payment in another asset, indicate its identifier such as
    oj8yEksX9Ubq7lLc+p6F2uyHUuynugeVq4+ikT67X6E=
    for blackbytes (don't forget to url-encode it).

    You will likely want to generate a unique payment address per user, per transaction. This code sample might help:

    Payments from single address wallet

    It is possible to request that the payment would be done from any single address wallet or specific single address wallet. This works only in chat messages.

    Requesting multiple payments

    To request the user to pay multiple payments at once, create the below Javascript object objPaymentRequest which can contain multiple addresses and multiple assets, encode the object in base64, and send it over to the user:

    The user's wallet will parse this message, display the multiple payments in a user-readable form, and offer the user to pay the requested amounts. Your payment-waiting code will be called when the payment is seen on the DAG.

    Waiting for payments

    If you include a headless wallet

    you can get notified when any of your addresses receives a payment

    arrUnits is an array of units (more accurately, unit hashes) that contained any transaction involving your addresses. The event new_my_transactions is triggered for outgoing transactions too, you should check if the new transaction credits one of the addresses you are expecting payments to.

    Waiting for finality of payments

    To get notified when any of your transactions become stable (confirmed), subscribe to my_transactions_became_stable event:

    arrUnits is again the array of units that just became stable and they contained at least one transaction involving your addresses.

    The above events work in both full and light nodes. If your node is full, you can alternatively subscribe to event mci_became_stable which is emitted each time a new main chain index (MCI) becomes stable:

    Sending payments from chat bot

    To send payments, you need to include a headless wallet

    and use this function after the headless wallet becomes ready:

    asset is the asset you are paying in (null for bytes), amount is payment amount in the smallest units. If the payment was successful, you get its unit in the callback and can save it or watch further events on this unit.

    There are many other functions for sending payments, for example sending multiple payments in multiple assets at the same time, see exports of https://github.com/byteball/headless-obyte/blob/master/start.js.

    Smart contracts

    Smart contracts on Obyte are expressions that evaluate to true or false, the money stored on the contract can be spent only when it evaluates to true.

    The smart contract language is declarative, meaning that it expresses what conditions must be met to allow movement of money, rather than how the decisions are made. This makes it easy to see that the implementation of the contract matches its intent, and hard to make mistakes (which cannot be undone in distributed ledgers).

    However, the language is not as powerful as Ethereum’s Solidity, it is not Turing-complete, it doesn’t allow to code any program, rather it is a domain specific language for money on the distributed ledger. They are more like Discreet Log Contracts for Bitcoin. If you are looking to build more complex dApps that are as capable as Ethereum smart-contracts, then you need to use Autonomous Agents.

    Money on Obyte is stored on addresses. Address is just a hash (plus checksum) of an address definition, and the address definition is an expression in the Obyte smart contract language that evaluates to either true or false.

    Offering smart contracts

    When you want to create a new smart contract with a user, your sequence is usually as follows:

    • you ask the user to send his payment address (it will be included in the contract)

    • you define a new contract using the user's address and your address as parties of the contract

    • you pay your share to the contract

    • at the same time, you send a specially formatted payment request (different from the payment request above) to the user to request his share. You start waiting for the user's payment

    We'll discuss creating and offering contracts below. These two steps are similar to what happens in the GUI wallet when a user designs a new contract.

    Creating contract definition

    Create a JSON object that defines the contract:

    Create another object that describes the positions of your and user addresses in the above definition:

    The keys of this object are r (from "root") followed by indexes into arrays in the definition's orand and conditions. Since the conditions can be nested, there can be many indexes, they are separated by dot.

    Then you create the smart contract address:

    If the address was successfully created, it was also already automatically sent to the user, so the user's wallet will know it.

    Example of vesting contracts: , , . Examples with oracles: , . Examples with tokens: , .

    More definition examples can be seen on .

    Sending a request for contract payment

    To request the user to pay his share to the contract, create the below Javascript object objPaymentRequest which contains both the payment request and the definition of the contract, encode the object in base64, and send it over to the user:

    The user's wallet will parse this message, display the definition of the contract in a user-readable form, and offer the user to pay the requested amount. Your payment-waiting code will be called when the payment is seen on the DAG.

    Re-sending lost smart-contracts

    Since smart-contracts are private until they have been spent, users might loose them if they probably didn't backup their wallet or restored from seed words. Some attestation bots lock user's reward for months or years into smart-contracts and it would be more user-friendly if users could still restore the smart-contract even after they have lost it. Since all parties (2 or more) have the definition of the smart-contract in their wallet, one could ask to resend it from the other. With chat bots, this could be done like this:

    Ping-pong paymets

    Hello. I’m starting a series of tutorials about creating projects on Obyte. Today we will consider the creation of ping-pong payment bot. First we will need Linux or MacOS, node.js, and . So, let's clone it and run

    We run cp .env.testnet .env for changing it configuration to testnet network. When you starting bot at first, he will ask you passphrase(don’t forget it, since you need to enter each time you start). When starting bot in console you will see “my pairing code”. It looks like this: Copy this pairing code.

    Now add our bot, open the Obyte wallet and go to Chat (below) > Add a new device > Accept invitation from the other device then insert your pairing code (instead of an asterisk any word, for example, test) and click "PAIR”

    After that you will have a chat with your bot, write to him and he will answer you with your message. All right, now let's teach the bot to give us our money back.

    Any text before [payment description, will be ignored](obyte:ACCOUNT_ADDRESS?amount=123000&asset=base) any text after
    var headlessWallet = require('headless-obyte');
    
    eventBus.once('headless_wallet_ready', () => {
    
        eventBus.on('text', function(from_address, text) {
            const device = require('ocore/device.js');
            
            if (text === 'resend') {
                headlessWallet.issueNextMainAddress((deposit_address) => {
                    // send it over to the user
                    device.sendMessageToDevice(from_address, 'text',
                        '[...](obyte:'+ deposit_address +'?amount=123000&asset=base)');
                    });
                });
            }
        });
    });
    [...](obyte:ACCOUNT_ADDRESS?amount=1&single_address=1)
    [...](obyte:ACCOUNT_ADDRESS?amount=1&single_address=1&from_address=UEPO3OD2TUUJUOECVMKVSRHRDJ4ST2FS)
    var payments = [
        {address: address1, amount: amount1, asset: asset1},
        {address: address1, amount: amount2, asset: asset2},
        {address: address2, amount: amount3, asset: asset1}
    ];
    var objPaymentRequest = {payments};
    var paymentJson = JSON.stringify(objPaymentRequest);
    var paymentJsonBase64 = Buffer.from(paymentJson).toString('base64');
    var paymentRequestCode = 'payment:'+paymentJsonBase64;
    var paymentRequestText = '[...]('+paymentRequestCode+')';
    
    device.sendMessageToDevice(from_address, 'text', paymentRequestText);
    var headlessWallet = require('headless-obyte');
    eventBus.on('new_my_transactions', function(arrUnits){
    	// fetch more data about my addresses
    	db.query("SELECT outputs.address, amount, asset FROM outputs \
    			JOIN my_addresses USING (address) \
    			WHERE unit IN(?);", [arrUnits], (rows) => {
    		if (rows.length === 0) return;
    		rows.forEach((row) => {
    		    // react to each unconfirmed payment
    		});
    	});
    });
    eventBus.on('my_transactions_became_stable', function(arrUnits){
    	// fetch more data about my addresses
    	db.query("SELECT outputs.address, amount, asset FROM outputs \
    			JOIN my_addresses USING (address) \
    			WHERE unit IN(?);", [arrUnits], (rows) => {
    		if (rows.length === 0) return;
    		rows.forEach((row) => {
    		    // react to each confirmed payment
    		});
    	});
    });
    eventBus.on('mci_became_stable', function(mci){
        // check if there are any units you are interested in 
        // that had this MCI and react to their becoming stable
    });
    var headlessWallet = require('headless-obyte');
    eventBus.once('headless_wallet_ready', () => {
        headlessWallet.issueChangeAddressAndSendPayment(asset, amount, account_address, from_address, (err, unit) => {
            if (err){
                // something went wrong, maybe put this payment on a retry queue
                return;
            }
            // handle successful payment
        });
    });
  • the user views the contract definition in his wallet and agrees to pay

  • you receive the payment notification and wait for it to get confirmed

  • after the payment is confirmed, the contract is fully funded

  • Real Name Attestation
    Steem Attestation
    Bitcointalk attestation
    Flight Delays Insurance
    GUI wallet
    Condiitonal token sale
    Exchange bot
    smart contracts definitions page
    var arrDefinition = ['or', [
        ['and', [
            ['address', user_address],
            // conditions when user can unlock the contract
        ]],
        ['and', [
            ['address', bot_address],
            // conditions when I can unlock the contract
        ]]
    ]];
    var device = require('ocore/device.js');
    var assocSignersByPath = {
        'r.0.0': {
            address: user_address,
            member_signing_path: 'r', // unused, should be always 'r'
            device_address: from_address
        },
        'r.1.0': {
            address: bot_address,
            member_signing_path: 'r', // unused, should be always 'r'
            device_address: device.getMyDeviceAddress()
        }
    };
    var walletDefinedByAddresses = require('ocore/wallet_defined_by_addresses.js');
    walletDefinedByAddresses.createNewSharedAddress(arrDefinition, assocSignersByPath, {
        ifError: function(err){
            // handle error
        },
        ifOk: function(shared_address){
            // new shared address created
        }
    });
    var payments = [
        {address: shared_address, amount: peer_amount, asset: peerAsset}
    ];
    var definitions = {};
    definitions[shared_address] = {
        definition: arrDefinition,
        signers: assocSignersByPath
    };
    var objPaymentRequest = {payments, definitions};
    var paymentJson = JSON.stringify(objPaymentRequest);
    var paymentJsonBase64 = Buffer.from(paymentJson).toString('base64');
    var paymentRequestCode = 'payment:'+paymentJsonBase64;
    var paymentRequestText = '[...]('+paymentRequestCode+')';
    
    device.sendMessageToDevice(from_address, 'text', paymentRequestText);
    const eventBus = require('ocore/event_bus.js');
    
    eventBus.on('text', function(from_address, text) {
    	const device = require('ocore/device.js');
    
    	if (text === 'resend') {
    		const walletDefinedByAddresses = require('ocore/wallet_defined_by_addresses');
    		walletDefinedByAddresses.sendToPeerAllSharedAddressesHavingUnspentOutputs(from_address, 'base', {
    			ifFundedSharedAddress: function(numberOfContracts) {
    				device.sendMessageToDevice(from_address, "text",
    					`Found and resent ${numberOfContracts} smart contracts that have Bytes on them to your wallet.`
    				);
    			},
    			ifNoFundedSharedAddress: function() {
    				device.sendMessageToDevice(from_address, "text",
    					`No smart contracts with Bytes on it were found.`
    				);
    			}
    		});
    	}
    });
    Opening the start.js file we see 5 events:

    headless_wallet_ready - Triggered when the bot is running and ready to work

    paired - Triggered when someone adds our bot

    text - Triggered when we receive a message

    new_my_transactions - Triggered when we receive a transaction(but it is not stable)

    my_transactions_became_stable - Triggered when the transaction has become stable

    First, let's create variables where the associations for addresses will be stored. We will learn how to work with the database in the following lessons.

    Now, lets teach the bot to send a request to send us your address:

    Here we see the function sendMessageToDevice, we will use it whenever we want to send a message to the user.

    The function takes 3 parameters

    1. device address

    2. subject - for now we will use only ‘text’

    3. our text message

    In order to send bot our address we need to go to our chat and click "Insert my address”:

    Great, now we will teach our bot to check and save the user's address, and also create an address for accepting payments and request 5000 bytes from the user.

    This time we used issueNextMainAddress, this function creates a new wallet address. We need it to distinguish between payments.

    Now the most important thing! First, we need to inform the user that we have received the payment and waiting until it becomes stable.

    After that, we send the user his payment minus the payment fee:

    Here, we use sendAllBytesFromAddress, as the name implies, we just send all the funds(minus the payment fee) from one address to another.

    That's it. Now let's test it. To do this you need to add faucet bot: AxBxXDnPOzE/[email protected]/bb-test#0000

    And send him your address, wait until it becomes stable(this can be seen on the main screen), that's all. Send payments to the bot and get them back.

    All code you can find on github. In the next lessons we will continue, but for now I recommend to read the official wiki. Did you like the lesson? Any questions? What topics have caused you difficulties and it is worth talking about them? Write in the comments and I will help you. Also, join my chat in telegram - @obytedev.

    Obyte wallet for testnet
    bot-example
    image3.png

    Quick Start

    Obyte is cryptocurrency platform, which is written with NodeJS. This site is for developers and should help developers to get started using Obyte platform in their projects.

    To begin developing any app which is based on headless-obyte, first thing to do is to set up your machine, install Node.js and tools. Complete guide of preparing your blank Debian-based machine for Obyte development. You can skip this step if you are familiar with Node.js development and already know how to do it in your way.

    Create a new node.js package for your chatbot: npm init. You will definitely need modules from ocore and if you are going to send payments, you will also need headless-obyte. Your package.json should list these dependencies:

    Now run npm install to fetch dependencies.

    Full configuration options are described in Configuration section. As for now, in your configuration file (conf.js in project folder or conf.json in user folder) switch your app to light node and add hub URL:

    If you want to connect to testnet network instead of spamming mainnet for development then you can do that by adding .env file, which should contain just one line:

    Sending / receiving txs:

    Go ahead and run the code: node index.js. You'll see initialization lines printed by ocore in console, which contain your wallet first address printed and an error saying that There are no funded addresses. This is our failed attempt to issueChangeAddressAndSendPayment(), as you don't have any bytes in your wallet yet.

    We wrapped all of our code in 'headless_wallet_ready' event handler, at this point the private keys of your wallet are decrypted and are ready for use.

    Find out in console output your first address.

    You can deposit some bytes on it to be able to make outbound transactions. Right after ocore is done with initialization (onReady function), we send one byte to some address. First argument is an asset, its the asset you are paying in (null for bytes), amount is payment amount in the smallest units. If the payment was successful, you get its unit in the callback and can save it or watch further events on this unit.

    There are many other functions for sending payments, for example sending multiple payments in multiple assets at the same time, see exports of .

    The new_my_transactions and my_transactions_became_stable event handlers are to be invoked after any new transaction received to any of your wallet addresses and when this transactions become stable, respectively. arrUnits is an array of units (more accurately, unit hashes) that contained any transaction involving your addresses. The event new_my_transactions is triggered for outgoing transactions too, you should check if the new transaction credits one of the addresses you are expecting payments to.

    The above events work in both full and light nodes. If your node is full, you can alternatively subscribe to event mci_became_stable which is emitted each time a new main chain index (MCI) becomes stable:

    Simple chat bot

    Every Obyte wallet can be paired with any other Obyte wallet, because every wallet has it's unique pairing code. Headless wallet will print it's pairing code to console right after the launch. Depending on what kind of app you want to build, you will or will not need this pairing code. Simplest operations on Obyte platform require no chat bot (like sending/receiving txs, for example).

    For more handy interaction with your userbase, you mostly surely need to write a chat bot, to set up direct interaction between your clients Obyte apps and your wallet (this process is called 'pairing'). Chat bot is Obyte wallet which can receive chat messages and react to them. Clients can pair and chat with your chat bot inside their Obyte wallet app, your task is to handle incoming messages and act accordingly. Through chat, users can provide to your chat bot their addresses, KYC profiles, or any other information with couple of clicks.

    You can use bot-example repository as a starting point for your chat bot. You can also use the module from previous step, but it will require you to add some more config lines, so its better to git clone [email protected]:byteball/bot-example.git, remove .git folder rm -fr bot-example/.git and tweak some default configs there (remember to give a name to your bot, switch to light node, set up a pairing secret, etc.)

    When you start your node, it will print its full pairing code:

    The pairing code consists of your node's public key (device public key), hub address, and pairing secret (after #).

    Publish this pairing code anywhere as a link with byteball: scheme, users will be able to open a chat with your bot by clicking your link (the link opens in their Obyte app and starts a chat):

    You also need this pairing code to add your bot to the .

    When a user pairs his device with your bot (e.g. by clicking the link to your bot), you receive a pairing notification and can welcome the user:

    The behavior of your event handler can depend on pairing_secret, the second argument of the event handler. For example, you can have a one-time (non-permanent) pairing secret equal to session ID on your website; after the user clicks the pairing link and opens chat, you can link his chat session with his web session, see .

    Receiving chat messages

    To receive chat messages, subscribe to 'text' events on event bus:

    from_address is user's device address (not to be confused with payment addresses), user_message is their message.

    Sending chat messages

    Messages to user's device can be sent with sendMessageToDevice function in device module:

    So, in case we want to echo back what user wrote, we could combine those two above examples into this:

    Great success, we now have a bot, which is like a parrot (repeating everything you wrote to it).

    Predefined chat commands

    To give access to predefined commands, format your responses this way:

    The user will see the text in square brackets "Command name", it will be highlighted as a link, and when the user clicks it, his app will send command code text to your bot.

    Command suggestion

    Sometimes you might want to suggest the command without actually sending it immediately, so user could have the possibility to edit the command before sending, this could be done like this:

    Here is how the result looks like:

    Next step

    That's it. You've completed your first chat bot on Obyte platform. You can check other chat bot examples provided by our team:

    • : start from here

    • : sending payments

    • : sending and receiving payments

    • : receiving payments without storage of private keys

    Now you are ready to make something useful, go ahead to our Tutorials section and carefully read all of them to get better understanding of Obyte code API. .

    Libraries and Scripts

    • - official core networking and consensus library for node.js

    • - official wallet and messaging library for node.js

    • - official messaging library for node.js and browsers

    Address signing/verification

    If you need to verify that a user owns a particular address, you can do it by offering to sign a message.

    You can also verify ownership of address by requesting a payment from their address and this method is more appropriate if you need to receive a payment anyway.

    To request the user to sign a message, send this message in chat:

    where challenge is the message to be signed. This request will be displayed in the user's wallet as a link that the user can click and confirm signing. Once the user signs it, your chat bot receives a specially formatted message:

    You can parse and validate it, here is an example how to verify user's account address:

    The above code also checks that the user signed the correct message and with the correct address.

    objSignedMessage received from the peer has the following fields:

    git clone https://github.com/byteball/bot-example
    cd bot-example 
    npm install
    cp .env.testnet .env
    node start.js
    let assocDeviceAddressToPeerAddress = {};
    let assocDeviceAddressToMyAddress = {};
    let assocMyAddressToDeviceAddress = {};
    eventBus.on('paired', (from_address, pairing_secret) => {
      const device = require('ocore/device.js');
      device.sendMessageToDevice(from_address, 'text', "Please send me your address");
    });
    
    eventBus.on('text', (from_address, text) => {
      text = text.trim();
    
      const device = require('ocore/device.js');
      device.sendMessageToDevice(from_address, 'text', "Please send me your address");
    });
    eventBus.on('text', (from_address, text) => {
      const device = require('ocore/device.js');
      text = text.trim();
      if (validationUtils.isValidAddress(text)) {
         assocDeviceAddressToPeerAddress[from_address] = text;
         device.sendMessageToDevice(from_address, 'text', 'Saved your wallet address');
         headlessWallet.issueNextMainAddress((address) => {
            assocMyAddressToDeviceAddress[address] = from_address;
            assocDeviceAddressToMyAddress[from_address] = address;
            device.sendMessageToDevice(from_address, 'text', '[balance](byteball:' + address + '?amount=5000)');
         })
      } else if (assocDeviceAddressToMyAddress[from_address]) {
         device.sendMessageToDevice(from_address, 'text', '[balance](byteball:' + assocDeviceAddressToMyAddress[from_address] + '?amount=5000)');
      } else {
         device.sendMessageToDevice(from_address, 'text', "Please send me your address");
      }
    });
    eventBus.on('new_my_transactions', (arrUnits) => {
      const device = require('ocore/device.js');
      db.query("SELECT address, amount, asset FROM outputs WHERE unit IN (?)", [arrUnits], rows => {
         rows.forEach(row => {
            let deviceAddress = assocMyAddressToDeviceAddress[row.address];
            if (row.asset === null && deviceAddress) {
               device.sendMessageToDevice(deviceAddress, 'text', 'I received your payment: ' + row.amount + ' bytes');
               return true;
            }
         })
      });
    });
    eventBus.on('my_transactions_became_stable', (arrUnits) => {
      const device = require('ocore/device.js');
      db.query("SELECT address, amount, asset FROM outputs WHERE unit IN (?)", [arrUnits], rows => {
         rows.forEach(row => {
            let deviceAddress = assocMyAddressToDeviceAddress[row.address];
            if (row.asset === null && deviceAddress) {
               headlessWallet.sendAllBytesFromAddress(row.address, assocDeviceAddressToPeerAddress[deviceAddress], deviceAddress, (err, unit) => {
                  if(err) device.sendMessageToDevice(deviceAddress, 'text', 'Oops, there\'s been a mistake. : ' + err);
    
                  device.sendMessageToDevice(deviceAddress, 'text', 'I sent back your payment! Unit: ' + unit);
                  return true;
               })
            }
         })
      });
    });
    package.json
    "dependencies": {
        "headless-obyte": "git+https://github.com/byteball/headless-obyte.git",
        "ocore": "git+https://github.com/byteball/ocore.git"
    }

    obyte.js - Lightweight wallet library for working with Obyte from browser and node.js

  • kbyte.js - Lightweight networking-only JS library (predecessor of obyte.js)

  • owallet.js - Lightweight wallet library JS library (based on obyte.js)

  • VS Code plugin for Oscript

  • aa-testkit - Instant Obyte devnet network set up and testing

  • parser-aa-vars - Script for parsing variables from AA

  • aabot - Node.js library for interacting with Autonomous Agents and tracking their state

  • aagent.js - Helper library for AA data and events

  • AA-channels-lib - a general purpose library for payment channels

  • Pay-Per-Call API Library - pay-as-you-go payment library for API use

  • byteduino - A very light C/C++ implementation of Obyte for Arduino ESP8266 and ESP32

  • ocore-wallet-service - A Multisig HD Obyte Light Wallet API Service

  • ocore-wallet - A Simple Command Line Interface Wallet for Obyte using OWS/OWC

  • Obyte Assets Ledger - API for headless wallet, which can be used as internal exchange ledger with its own tokens.

  • byteball-rpc-client - JSON-RPC client on Python

  • ocore
    headless-obyte
    obyte-browser-chat

    signed_message: (string) the signed message

  • authors: array of objects, each object has the following structure (similar to the structure of authors in a unit):

    • address: (string) the signing address

    • definition: (array) definition of the address

    • authentifiers: (object) signatures for different signing paths

  • To validate the message, call validation.validateSignedMessage as in the example above.

    Note that the challenge that you offer to sign must be both clear to the user and sufficiently unique. The latter is required to prevent reuse of a previously saved signed message.

    [...](sign-message-request:challenge)
    [...](signed-message:base64_encoded_text)
    // verify posted addresss
    const validationUtils = require('ocore/validation_utils');
    const device = require('ocore/device.js');
    var sessionData = [];
    
    eventBus.on('text', (from_address, text) => {
    	let arrSignedMessageMatches = text.match(/\(signed-message:(.+?)\)/);
    	
    	if (validationUtils.isValidAddress(text)) {
    		sessionData[from_address] = text;
    		let challenge = 'My address is '+text;
    		return device.sendMessageToDevice(from_address, 'text',
    				'[...](sign-message-request:'+ challenge  +')');
    	}
    	else if (arrSignedMessageMatches){
    		let signedMessageBase64 = arrSignedMessageMatches[1];
    		let validation = require('ocore/validation.js');
    		let signedMessageJson = Buffer.from(signedMessageBase64, 'base64').toString('utf8');
    		try{
    			var objSignedMessage = JSON.parse(signedMessageJson);
    		}
    		catch(e){
    			return null;
    		}
    		validation.validateSignedMessage(objSignedMessage, err => {
    			let user_address = sessionData[from_address];
    			let challenge = 'My address is '+user_address;
    			if (err)
    				return device.sendMessageToDevice(from_address, 'text', err);
    			if (objSignedMessage.signed_message !== challenge)
    				return device.sendMessageToDevice(from_address, 'text',
    					"You signed a wrong message: "+objSignedMessage.signed_message+", expected: "+challenge);
    			if (objSignedMessage.authors[0].address !== user_address)
    				return device.sendMessageToDevice(from_address, 'text',
    					"You signed the message with a wrong address: "+objSignedMessage.authors[0].address+", expected: "+user_address);
    			
    			// all is good, address proven, continue processing
    		});
    	}
    });
  • Flight delay insurance: offering contracts

  • Internal asset exchange: offering contracts

  • GUI wallet: not a bot, but you'll find code for offering contracts

  • https://github.com/byteball/headless-obyte/blob/master/start.js
    Bot Store
    Authenticating users on websites
    Bot example
    Faucet
    ICO bot
    Merchant
    Tutorials for newcomers

    Setting up headless wallet

    Instructions how to set up headless wallet on a blank Debian machine.

    Setting up a Headless wallet is required for those wanting to build their own chat bots. That is, if they want to host the bot on their own server, at least. This guide will take you through the installation in a step by step process but won’t venture into how to build the chat bot itself. There are several possibilities to rent a Virtual Private Server (VPS) and apart from a fairly fast SSD storage there are not a lot of requirements.

    The first thing you should consider, is whether you want to run your bot on a full node or a light node. When setting up the server, the choice to run on a full node will mean you need to make sure to have enough disk space available. The installation is the same whether you run a full node or a light node, but running a full node will allow your bot to have access to all units ever posted to the DAG and thereby be able to perform more complex tasks. If light wallet is enough for you and have no interest in renting a VPS then you can run a light Obyte headless wallet also on Raspberry Pi 3+.

    Most VPS providers allow you to choose an operating system, and since this guide will be based on Debian 9, you might want to find a provider that offers this as one of their options. Since I will be running a full node, I also must make sure to have enough disk space available and that it is on SSD, since HDD will be too slow.

    • 1 server (with Debian in this case)

    • At least 140-150 GB of fast SSD disk space (HDD is too slow for initial syncing)

    • 1 GB RAM (hubs/relays that accept incoming connections might need up to 4GB)

    Setting up the server.

    Having created the server, I need to connect to the server. I use a super lightweight SSH terminal called “putty” from my Windows laptop.(available on )

    First time you log on, you will see a notice that you have not connected to this host before. Click “Yes” to save the key.

    You are now logged on to your server:

    Running the headless wallet as root is not recommended, so the first thing I do, is create the user we will run the hub as. I chose the username “obyte”.

    adduser obyte

    I will be installing all prerequisites as root for this guide. Only the actual headless wallet stuff will be installed from the user we just created. For now, stay logged in as root.

    The fundamentals

    First, we need some basic tools. To make sure we get the newest versions, first run an update of the apt tool: apt update

    Then install following software: apt-get install -y git curl software-properties-common

    To make sure all binaries build properly, we need the build-essentials as well: apt-get install -y build-essential

    Now log on with the user we initially created (obyte in this example) su -l obyte

    The headless wallet is based on nodejs, and one of the easiest way to control node versions is by the script called “nvm”. Just fire these three commands, and you’re all set (make sure not to miss any of the backticks in the above): nvm_version=`curl --silent -L https://api.github.com/repos/nvm-sh/nvm/releases/latest | /usr/bin/awk '/tag_name/ { print $2 }' | /bin/sed 's/[",]//g'` curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/$nvm_version/install.sh | bash

    (thanks to for the above two commands)

    Then exit user, log in again and install version 14 of nodejs: exit su -l obyte nvm install 14

    Build tools for Windows can be installed with following command (you need to run PowerShell or CMD.exe as Administrator to install this): npm install --global --production windows-build-tools

    You will also need for cases where the pre-built binaries (secp256k1, sqlite, rocksdb) are not available for your system configuration. That you can install with this command: npm install -g node-gyp

    Installing the Headless Wallet

    So far so good - we’re now almost ready to start actually installing the headless wallet. But first. We need to find the repository that we want to clone. Go to and click the “Clone or download” button:

    Copy the URL from there and clone the repository

    git clone https://github.com/byteball/headless-obyte.git

    It creates a directory called headless-obyte.

    cd headless-obyte

    Then install it

    npm install

    Configuring and starting your headless wallet

    In your .config folder, you should create a file called conf.json and edit it. It will contain values that is used by the headless wallet. They override the values that you can find in the file called conf.js located in your headless-obyte folder.

    mkdir -p ~/.config/headless-obyte nano ~/.config/headless-obyte/conf.json

    I will briefly explain each of the entries, you should put in the file (if you’re not familiar with the structure of a json object, you could check out sites such as jsoneditoronline.org)

    There is also a detailed explanation of the values on at the section called “Customize”.

    deviceName config

    This is pretty much self explanatory. The name of your device. This is the name that others will see when they pair with your bot. It’s not to be confused with the bot’s name, which you will have to have the Obyte hub-operator create for you.

    permanent_pairing_secret config

    This is the part that goes after the # in your pairing invitation that you will use from your control device - usually a GUI wallet.

    control_addresses config

    This is one of the two most important settings. This is an array of device addresses (not wallet addresses) that are allowed to control your headless wallet and request payments from it. You will most likely want to control the headless wallet from a GUI wallet on a phone or desktop. You find the device address by clicking the burger-menu and clicking “Settings” and then scrolling down to the section saying “Device Address” on the wallet(s) you want to be able to connect to the headless wallet from.

    payout_address config

    The other important setting is the payout_address which is the wallet address that the bot is allowed to send funds to. When a device with the correct device address requests the bot to send a payment of X bytes, the bot automatically pays it to the wallet you specify here. If you are installing the bot to allow users to send funds to it or to be part of smart contracts that it will potentially withdraw from, you might want to periodically send funds away from the headless wallet and into another wallet you control.

    bLight config

    This setting determines whether your headless wallet will run as a full node or a light node. If your bot needs access to all transactions on the DAG, you will need to run a full node. Therefore, in most cases, this should be set to false.

    The final file would look something like this:

    Wallet is ready to be started

    That’s it, your headless wallet is now ready to be started. If you set bLight = false; your wallet will start synchronizing the entire DAG. This can take a long time depending on the speed of your server and particularly the SSD disk.

    To make sure the process doesn’t run out of memory (which can cause crashes) you should manually assign additional memory to the nodejs process:

    node --max-old-space-size=4096 start.js

    Run on background using "bg" and "fg"

    Send to background

    ctrl+z bg

    Stop the headless wallet

    fg ctrl+c

    Run on background using “screen”

    It might just be me, but I always found that a better way to leave processes running even after I log off the server, is to use the program screen. It’s super easy to use. So I suggest, that even though the bot say you can send the process to the background by using ctrl+z and bg, i have a harder time separating the various background processes. So I suggest you simply do this:

    sudo apt-get install screen

    This allows you to start, detach and re-attach screens as you please.

    Start a new screen

    screen - run it when you have detached from screen and want to start a totally new screen.

    Send screen to background

    ctrl+a and d - this detaches from screens and sends it to background (be careful not to miss pressing a before d.

    Resuming a screen from background

    screen -r - this reattaches to most recently started screen. If you have started multiple then use screen -list to get a list of all started screens and then reattached with screen -r PPID

    Stop the headless wallet

    ctrl+d - while attached to screen, this terminates the screen and stops the headless wallet

    Run on background non-interactively

    If you are unable to enter a passphrase every time the wallet starts and/or are willing to accept the security risks, set bNoPassphrase to true and daemonize the app when starting:

    Starting the daemon

    Stopping the daemon

    Checking if node runs

    ps -ef | { head -1; grep node; } or ps -fp $(pgrep -d, -x node)

    kill PID - sends SIGTERM command to stop the process by PID number.

    If you want to automatically stop all node processes then use pkill node

    Useful extras

    Since Obyte runs on an SQLite database by default, which is , you might want to be able to explore the data stored yourself, thereby making it easier to create the logic for the bot if you need to access data in the databases. If you wish to then that can be changed with a configuration file. apt-get install sqlite3

    Monitoring logs

    Checking the log to see if things are running (by default, log file is in ):

    tail -f ~/headless-obyte/log.txt

    Disabling logs

    By default, headless wallets are set to write logs into a file, but you make them to write to console instead by setting logToSTDOUT configuration to true.

    If don't wish to get the logs to console and you also would like to disable writing the log file then this can be done by adding a variable like this to the configuration file:

    Documentation

    Full documentation for headless Obyte node can be found there

    conf.js
    exports.bLight = true;
    exports.hub = process.env.testnet ? 'obyte.org/bb-test' : 'obyte.org/bb';
    testnet=1
    index.js
    var headlessWallet = require('headless-obyte');
    var eventBus = require('ocore/event_bus.js');
    
    function onReady() {
        let amount = 1; //in bytes
        let user_address = "62SU7724ME5MWB7VEP64VURYZHDRSDWN"; // account address
        let user_device_address = null; // device address
        headlessWallet.issueChangeAddressAndSendPayment(
            null /*asset, null for bytes*/,
            amount,
            user_address,
            user_device_address,
        (err, unit) => {
            if (err){
                return console.error(err);
            }
            // handle successful payment
        });
    
        // received new payment
        eventBus.on('new_my_transactions', (arrUnits) => {
            // handle new unconfirmed units here
            // at this stage, payment is not yet confirmed
        });
    
        // payment is confirmed
        eventBus.on('my_transactions_became_stable', (arrUnits) => {
            // handle payments becoming confirmed
        });
    };
    eventBus.once('headless_wallet_ready', onReady);
    ====== my first address: 2PJHQKMP7NH6MUYUQPZ4S46C2Q7CQI32
    index.js
    eventBus.on('mci_became_stable', function(mci){
        // check if there are any units you are interested in 
        // that had this MCI and react to their becoming stable
    });
    ====== my pairing code: [email protected]/bb#0000
    <a href="byteball:[email protected]/bb#0000">Chat with my test bot</a>
    index.js
    eventBus.on('paired', function(from_address, pairing_secret){
        var device = require('ocore/device.js');
        device.sendMessageToDevice(from_address, 'text', 'Hi! I am bot.');
    });
    index.js
    eventBus.on('text', function(from_address, user_message){
        // your code here
    });
    index.js
    var device = require('ocore/device.js');
    var message_to_user = 'Message from bot to user';
    device.sendMessageToDevice(from_address, 'text', message_to_user);
    index.js
    var eventBus = require('ocore/event_bus.js');
    var device = require('ocore/device.js');
    
    eventBus.on('text', function(from_address, user_message){
        device.sendMessageToDevice(from_address, 'text', user_message);
    });
    click this link: [Command name](command:command code)
    Example command: we suggest to [buy (number) apples](suggest-command:buy 5 apples)
    https://www.putty.org
    http://blog.mobnia.com/installing-node-with-nvm-on-a-debian-server/
    node-gyp
    https://github.com/byteball/headless-obyte
    https://github.com/byteball/headless-obyte
    located in user data folder
    setup Obyte to use MySQL database instead
    same folder as configuration
    https://byteball.github.io/headless-obyte/

    Sending data to DAG

    You can store arbitrary data (any sequence of bytes) in Obyte public DAG. Some special types of data also supported, like 'text', 'profile', 'poll', etc.

    Data with any structure

    To send data to DAG database, code below is a minimal that you can use. Since the opts.messages is array, it means that all the examples on this page can be changed to multi-message transactions.

    AAs can be triggered with data by manually entering the key/value pairs to GUI wallet, prompting users to submit pre-filled Send screen, making the AA to trigger another AA or with the script from headless wallet.

    Key-value data feed

    Previous example shows how to send any data to DAG, but if you would want to store data that is searchable and can be used in or then you would need to post it as key-value pairs and with app type data_feed. If we also send it always from the same first address in our wallet then we have basically become an oracle.

    Profile about yourself

    Anybody on Obyte network can post some data about themselves, which will be linked to their address. This app type is called profile. Payload for that message can contain any keys. For example, web service lets you post a profile with keys like this:

    Attestation profile

    Attestation profile is similar to previous one with one difference that attestation type message are posted by somebody and they describe somebody else. These profiles are used to do KYC by .

    Private/Public attestations

    bot creates an attestation profile, which keeps all the data private on user device while makes it possible for the others to recognize unique users by user_id (it's a salted hash of first_name, last_name, dob and country) and also makes it possible to validate with profile_hash if user provides them a private profile.

    In case user would want to make their profile public like with and , true could be sent as a second parameter of hideProfile() function. To avoid people doxxing themselves, the possibility to make public attestation with Real Name Attestation has not been included. This choice is only on Email Attestation and Steem Attestation.

    In order for the user to share private profile with others, your bot should also .

    Public only attestations

    If there is no plan to provide private attestations and all the attestation will always be public then there is no need for user_id. Then the attestation profile can be as simple as this, but the attestator needs to make sure that the same username attestation is not done to multiple users. One example of that kind is .

    For public attestation, there is not private profile to send to user's wallet because the bots who need information about the user could simply just .

    Poll question and choices

    Anybody can post a poll and let others vote on it, this can be done like this data and it will also appear on web service from where it will direct users to .

    Plain text data

    And last and not the least, it is possible to post plain text to DAG too.

    More examples

    Shorter code examples using composer library are available in .

    If you wish to send data to DAG database periodically then full code examples for , or can be found on Github.

    Contracts with arbitration

    Contracts with arbitration are peer-to-peer free-form textual contracts coupled with simple smart contracts that safely lock the money for the duration of the contract and allow disputes to be resolved by mutually agreed arbiters. In other words, a contract with arbitration is a prosaic contract plus a smart contract that guarantees that the parties faithfully follow the contract.

    If you are developing a bot that makes contract offers, here is an outline of the contract's lifecycle:

    • Your bot (offerer) receives a user's address in chat from a user.

    • The bot then sends a contract with arbitration offer to the user. In the offer, it sets the user's address as the other party of the contract, amount of the contract, and an arbiter's address of your choosing. You can also choose the role for your bot in this contract (buyer or seller).

    • The user reviews the offer in their wallet and accepts the contract.

    • Your bot receives the user's acceptance, generates a new shared address with smart contract definition, and initiates signing and posting of a special data unit with the hash of the agreed contract text. The unit is to be signed by the shared address (shared between the bot and the user) and each party's signature means that they agree with the contract's text. Your bot signs first, then waits for the user to sign the transaction.

    • After the unit has been posted, the buyer side of the contract should pay to this contract's address the full amount that was specified in the contract.

    • As soon as the funds are locked on the contract, the seller of the goods/services starts fulfilling their obligations.

    • When all the work is done and the buyer is satisfied with the result, they can release the funds from the contract sending them to the seller's address. Also at any time the seller can refund the full amount back to the buyer. The smart contract ensures that in the absence of any arbiter decision each party can send the contract's funds only to the other party: buyer to the seller to release the funds after the work is done, or seller to the buyer to refund. Either outcome completes the contract.

    • In case of a disagreement, the user can open a dispute and invoke the arbiter by clicking the corresponding button in their wallet, or your bot can call an API method to open a dispute. The plaintiff has to pay for the arbiter's service before the arbiter starts looking into the case. After the contract status becomes in_dispute, both parties have to wait for the arbiter to post a resolution unit. After the arbiter has studied the contract and the evidence provided by the parties, they make a decision and post the resolution unit. Then, the winning side can claim the funds from the contract.

    Note that in order to open a dispute from the bot, your bot should implement the corresponding API (see below). After you open a dispute, the arbiter will set a fee for their service and the arbstore will send a payment request to your bot. Your bot should be able to handle such requests and decide about paying them, either autonomously, or forward them to a human operator.

    All the API methods available for your bot are located in file.

    Example

    There is an example script for a part of the API functionality in inside headless-wallet repository, you can start by modifying it to your needs:

    Basically, all you need to do is calling the exported methods of arbiter_contract.js module and reacting to arbiter_contract_* events on the event bus.

    API reference:

    1. createAndSend(contractObj, callback(contract){})

      This method creates a contract from the provided object and sends it to the peer. contractObj should contain all the fields needed to assemble a new contract offer. The function is then sends the offer to the contractObj.peer_device_address via chat. The contract received in the callback function has hash field, it is the contract's hash which acts as the primary key when referencing contracts.

      contractObj required fields:

    Event reference

    Almost every event is supplied with additional arguments that you can use in your event handler functions. For examples, see the code snippet above.

    1. arbiter_contract_offer : (contract_hash) - this event is fired when core received new contract with arbitration offer from any correspondent.

    2. arbiter_contract_response_received : (contract) - fired when user responds to your contract offer. To get their response, check the status field of the contract, it should be either "accepted" or "declined".

    Cashback API

    This API is for merchants and payment processors who participate in the Obyte cashback program.

    This is API documentation to integrate cashback to your point of sale device, but you could also build your own cashback app, where this API has already been implemented. The first thing you need sign up to Cashback Program in order to get partner name and partner key.

    After you accepted payment from the customer, you use this API to request cashback for your customer. You send a POST request to:

    https://byte.money/new_purchase

    (for testnet use https://byte.money/test/new_purchase)

    and pass the following parameters:

    • partner: (string) your partner name you were assigned when you signed up for the program.

    • partner_key: (string) partner key used to authenticate your requests.

    • customer: (string) any id of the customer who made a purchase such as customer name or id in your database. This field must be the same when the same customer makes another purchase.

    • order_id: (string) unique id of the order associated with the purchase. You should never request cashback for the same order id again.

    • description: (string) description of the purchase.

    • merchant: (string) this field is used by payment processors only to identify the individual merchant. When another purchase is made from the same merchant, this fiels should be the same.

    • address: (string) Wallet address or email address of the customer. The cashback will be sent to this address. If it is email address, a will be sent. If the textcoin is not claimed within 1 week, it will be taken back.

    • currency: (string) currency of the purchase. Supported values: USD, EUR, RUR, GBYTE, BTC. If the purchase was paid in any other currency, you should convert its amount to any of the supported currencies except GBYTE.

    • currency_amount: (number) amount of the purchase in currency.

    • partner_cashback_percentage: (number) the percentage of the amount you want to pay to the customer out of your own funds in addition to the regular cashback. Obyte will add the same percentage out of the distribution fund (merchant match). Default it 0. You have to deposit the funds in advance in order to fund this option.

    • purchase_unit: (string) unit (transaction id) of the customer's purchase if it was paid in GBYTE.

    Response format

    The response is in JSON format. It always contains a result field, which is either 'ok' or 'error'.

    If the cashback was successfully sent to the customer, the response has "result": "ok" and also indicates the cashback amount and the unit it was sent in:

    In case of an error, the response has "result": "error" and error description in error field:

    conf.json
    {
      "deviceName": "Obyte Denmark",
      "permanent_pairing_secret": "0000",
      "control_addresses": [
        "DEVICE_ADDRESS_1",
        "DEVICE_ADDRESS_2",
        "DEVICE_ADDRESS_3"
      ],
      "payout_address": "YOUR_WALLET_ADDRESS",
      "bLight": false
    }
    node start.js 1>log 2>errlog &
    conf.json
    {
        "logToSTDOUT": false,
        "LOG_FILENAME": "/dev/null"
    }
    const headlessWallet = require('headless-obyte');
    const eventBus       = require('ocore/event_bus.js');
    
    function postData() {
        let json_data = {
            age: 78.90,
            props: {
                sets: [
                    '0bbb',
                    'zzz',
                    1/3
                ]
            }
        };
        let objMessage = {
            app: 'data',
            payload_location: 'inline',
            payload: json_data
        };
        let opts = {
            messages: [objMessage]
        };
    
        ​headlessWallet.issueChangeAddressAndSendMultiPayment(opts, (err, unit) => {
            if (err){
                /*
                something went wrong,
                maybe put this transaction on a retry queue
                */
                return;
            }
            // handle successful payment
        });
    }
    
    eventBus.on('headless_wallet_ready', postData);
    smart-contracts
    autonomous agents
    obyte.io
    asking user to provide data about themselves from trusted attestor
    Real Name Attestation
    Email Attestation
    Steem Attestation
    send the private profile to user's wallet
    Username Attestation bot
    retrieve the attestation profile from the DAG
    obyte.io
    Poll chat bot
    "/tools" folder of headless-wallet
    price oracle
    bitcoin oracle
    sports oracle

    title - contract title, string

  • text - contract text, string

  • arbiter_address - the address of the picked arbiter, string

  • amount - amount in asset to be paid to the seller, int

  • asset - asset in which the payment should be done, can be null or "base" for Bytes, or any other asset ID, string

  • peer_address - address of the other side of the contract, string

  • my_address - your address, string

  • me_is_payer - flag specifying if you are the buyer or seller, bool

  • peer_device_address - the device address of the other side of the contract, string

  • ttl - Time-To-Live, in hours, till which the contract can be accepted, then it expires, float

  • cosigners - array of your wallet cosigners that would be used when posting contract signature unit, leave empty if not multi-sig or when you want to use all of the cosigners, array of device addresses

  • my_contact_info - free-form text with your contact info so the peer and arbiter can contact you, it is important to note that if you are sending contract offers from a bot, then arbiter can only pair with your bot and not you, therefore you should put your own wallet's pairing code here or other contacts by which you (as a human) can be reached,string

  • contract object supplied to your callback will mostly be identical to the contractObj that you provided to this function, but with some additional fields populated, such as hash, creation_date, my_pairing_code.\

  • createSharedAddressAndPostUnit(contract_hash, walletInstance, callback(error, contract){})

    This method is used to react to the peer accepting your offer. It creates a new shared address with smart contract definition and posts the unit into the DAG. walletInstance is a reference to wallet that has sendMultiPayment() method with an appropriate signer inside. For bots, it is usually the main headlessWallet module. If the error argument for the callback is null, it means that the unit was posted successfully, the contract object is returned with the updated fields.\

  • pay(contract_hash, walletInstance, arrSigningDeviceAddresses, callback(error, contract, unit){})

    This method pays to the contract if you are the buyer side. If you use a multi-signature wallet, then fill the arrSigningDeviceAddresses array with signing device addresses, otherwise provide an empty array. The unit argument in the callback function is the unit ID in case the payment succeeded.\

  • complete(contract_hash, walletInstance, arrSigningDeviceAddresses, callback(error, contract, unit){})

    This method sends the funds locked on the contract to the peer. Depending on your role, this action performs either a successful completion (you are a buyer), or a refund (you are the seller).\

  • respond(contract_hash, status, signedMessageBase64, signer, callback(error, contract){})

    If you allow users to make offers, use this function after receiving a contract offer, usually as a reaction to the event arbiter_contract_offer. To get a better idea how to construct signedMessageBase64 and signer arguments for this function, you can take a look at an example of how GUI wallet is invoking it: https://github.com/byteball/obyte-gui-wallet/blob/95d3354de9f9156fd06ccc63c0a4cac8c94f2510/src/js/services/correspondentService.js#L669\

  • openDispute(contract_hash, callback(error, response, contract){})

    This function raises a dispute by making an HTTPS request to the contract arbiter's current ArbStore web address. Callback is provided with an error, if any, http response and the updated contract object.\

  • appeal(contract_hash, callback(error, response, contract){})

    This method is similar to the previous one, but is used when you are not satisfied with the arbiter's decision. Also makes an HTTPS request to the ArbStore, but this time calls to ArbStore moderator, who can penalize the arbiter, in case they find the arbiter's decision / actions inappropriate.\

  • getByHash(contract_hash, callback(contract){}) / getBySharedAddress(address, callback(contract){}) / getAllBy*()

    These are all similar functions to retrieve one or many contracts by specific criteria. To get a detailed overview of all the methods you can look at exported methods of arbiter_contract.js module.

  • arbiter_contract_update : (contract, field, value, unit, winner, isPrivate) - this is the most common event that you will react to. It is raised in many situations, like:

    • changed status of a contract: field = "status" (contract was revoked, or put in_dispute, in_appeal, was paid, completed, etc.). This is the full list of the contract statuses: 'pending', 'revoked', 'accepted', 'signed', 'declined', 'paid', 'in_dispute', 'dispute_resolved', 'in_appeal', 'appeal_approved', 'appeal_declined', 'cancelled', 'completed'

    • unit with contract hash was posted: field = "unit"

    ocore/arbiter_contract.js
    tools/arbiter_contract_example.js
    textcoin
    const headlessWallet = require('headless-obyte');
    const eventBus = require('ocore/event_bus.js');
    
    function postDataFeed(first_address) {
        let json_data = {
            time: new Date().toString(), 
            timestamp: Date.now()
        };
        let objMessage = {
            app: 'data_feed',
            payload_location: 'inline',
            payload: json_data
        };
        let opts = {
            paying_addresses: [first_address],
            messages: [objMessage]
        };
    
        ​headlessWallet.issueChangeAddressAndSendMultiPayment(opts, (err, unit) => {
            if (err){
                /*
                something went wrong,
                maybe put this transaction on a retry queue
                */
                return;
            }
            // handle successful payment
        });
    }
    
    eventBus.once('headless_wallet_ready', () => {
        headlessWallet.readFirstAddress(postDataFeed);
    });
    let json_data = {
        name:     'a', 
        about:    'b',
        location: 'c',
        website:  'd',
        email:    'e'        
    };
    let objMessage = {
        app: 'profile',
        payload_location: 'inline',
        payload: json_data
    };
    const conf = require('ocore/conf.js');
    const objectHash = require('ocore/object_hash.js');
    
    let bPublic = false; // ask user if they want public or private profile
    
    function hideProfile(profile, bPublic = false){
        const composer = require('ocore/composer.js');
        let hidden_profile = {};
        let src_profile = {};
        for (let field in profile){
            let value = profile[field];
            let blinding = composer.generateBlinding();
            let hidden_value = objectHash.getBase64Hash([value, blinding], true);
            hidden_profile[field] = hidden_value;
            src_profile[field] = [value, blinding];
        }
        let shortProfile = {
            first_name: profile.first_name,
            last_name:  profile.last_name,
            dob:        profile.dob,
            country:    profile.country,
        };
        let public_profile;
        if (bPublic) {
            public_profile = profile;
            public_profile.user_id = objectHash.getBase64Hash([shortProfile, conf.salt]);
            return [public_profile, null];
        }
        else {
            public_profile = {
                profile_hash: objectHash.getBase64Hash(hidden_profile, true),
                user_id: objectHash.getBase64Hash([shortProfile, conf.salt])
            };
            return [public_profile, src_profile];
        }
    }
    
    let profile = {
        first_name: "", // required
        last_name:  "", // required
        dob:        "", // required
        country:    "", // required
        us_state:   "", // optional
        id_type:    ""  // optional
    };
    for (let field in profile){
        if (typeof profile[field] === 'undefined' || profile[field] === null || profile[field] === '') {
            // remove negative values, except number 0
            delete profile[field];
        }
        else {
            // convert everything else to string
            profile[field] = String(profile[field]);
        }
    }
    let [public_data, src_profile] = hideProfile(profile, bPublic);
    let json_data = {
        address: 'ABCDEF...',
        profile: public_data
    };
    let objMessage = {
        app: 'attestation',
        payload_location: 'inline',
        payload: json_data
    };
    let json_data = {
        address: 'ABCDEF...',
        profile: {
            username: ""
        }    
    };
    let objMessage = {
        app: 'attestation',
        payload_location: 'inline',
        payload: json_data
    };
    let json_data = {
        question: 'abc?', 
        choices: [
            'a',
            'b',
            'c',
        ]        
    };
    let objMessage = {
        app: 'poll',
        payload_location: 'inline',
        payload: json_data
    };
    let text_data = '';
    let objMessage = {
        app: 'text',
        payload_location: 'inline',
        payload: text_data
    };
    "use strict";
    const headlessWallet = require('../start.js');
    const eventBus = require('ocore/event_bus.js');
    const arbiter_contract = require('ocore/arbiter_contract.js');
    const device = require('ocore/device.js');
    const validationUtils = require('ocore/validation_utils');
    
    function onReady() {
    	let contract_text = "Bill pays Tom $20, if Tom sends Bill a pair of his Air Jordans.";
    	let contract_title = "Air Jordans Purchase from Tom";
    	let amount = 10000; // in bytes, min 10000
    	let asset = null;
    	let ttl = 24; //hours
    	let arbiter_address = "VYDZPZABPIYNNHCPQKTMIKAEBHWEW3SQ";
    	let my_contacts = `Email me at: [email protected]`;
    	let me_is_payer = true;
    
    	let my_address;
    	headlessWallet.readFirstAddress(address => {
    		my_address = address;
    	});
    
    	eventBus.on('paired', from_address => {
    		device.sendMessageToDevice(from_address, 'text', `My address: ${my_address}, now send me your's.`);
    	});
    
    	/* ================ OFFER ================ */
    	eventBus.on('text', (from_address, text) => {
    		text = text.trim();
    		if (!validationUtils.isValidAddress(text))
    			return device.sendMessageToDevice(from_address, 'text', `does not look like an address`);
    		let contract = {
    			title: contract_title,
    			text: contract_text,
    			arbiter_address: arbiter_address,
    			amount: amount,
    			asset: asset,
    			peer_address: text,
    			my_address: my_address,
    			me_is_payer: me_is_payer,
    			peer_device_address: from_address,
    			ttl: ttl,
    			cosigners: [],
    			my_contact_info: my_contacts
    		};
    
    		arbiter_contract.createAndSend(contract, contract => {
    			console.log('contract offer sent', contract);
    		});
    	});
    
    	/* ================ OFFER ACCEPTED ================ */
    	eventBus.on("arbiter_contract_response_received", contract => {
    		if (contract.status != 'accepted') {
    			console.warn('contract declined');
    			return;
    		}
    		arbiter_contract.createSharedAddressAndPostUnit(contract.hash, headlessWallet, (err, contract) => {
    			if (err)
    				throw err;
    			console.log('Unit with contract hash was posted into DAG\nhttps://explorer.obyte.org/#' + contract.unit);
    
    	/* ================ PAY TO THE CONTRACT ================ */
    			if (contract.me_is_payer) {
    				arbiter_contract.pay(contract.hash, headlessWallet, [],	(err, contract, unit) => {
    					if (err)
    						throw err;
    					console.log('Unit with contract payment was posted into DAG\nhttps://explorer.obyte.org/#' + unit);
    
    					setTimeout(() => {completeContract(contract)}, 3 * 1000); // complete the contract in 3 seconds
    				});
    			}
    		});
    	});
    
    	/* ================ CONTRACT FULFILLED - UNLOCK FUNDS ================ */
    	function completeContract(contract) {
    		arbiter_contract.complete(contract.hash, headlessWallet, [], (err, contract, unit) => {
    			if (err)
    				throw err;
    			console.log(`Contract completed. Funds locked on contract with hash ${contract.hash} were sent to peer, unit: https://explorer.obyte.org/#${unit}`);
    		});
    	}
    
    	/* ================ CONTRACT EVENT HANDLERS ================ */
    	eventBus.on("arbiter_contract_update", (contract, field, value, unit) => {
    		if (field === "status" && value === "paid") {
    			// do something usefull here
    			console.log(`Contract was paid, unit: https://explorer.obyte.org/#${unit}`);
    		}
    	});
    };
    eventBus.once('headless_wallet_ready', onReady);
    {
        "result": "ok",
        "cashback_amount": 101318,
        "unit": "kYbc5KGKB/KnjynYePEF4cpumKmOz22xVKSVWbmtZ0M="
    }
    {
        "result": "error",
        "error": "authentication failed"
    }

    Bet on weather bot

    Hello, everybody. Today we will learn how to create our first contract. We will do this on the example of a bot that accepts a bet on the weather.

    We will need bot-example. Let’s install it.

    git clone https://github.com/byteball/bot-example
    cd bot-example
    npm install
    cp .env.testnet .env

    First, let’s create variables.

    const request = require('request');
    const correspondents = require('./correspondents');
    
    let steps = {}; // store user steps
    let assocDeviceAddressToCurrentCityAndTemp = {}; 
    let assocDeviceAddressToAddress = {};
    let bets = [];
    let KEY_API = '50ace22603c84daba1780432180111'; // apixu.com
    let my_address;

    Save our address for use in the contract

    Let’s add a text handler and analyze what happens:

    Now let’s talk about contract creation. We need a separate function

    Let’s analyze what happens arrSeenConditionPeer - watching if there was a payment in the contract with the amount same of the user. arrSeenConditionMyInput - сhecking, did we withdraw bytes? If yes, then allow the user to pick up their bytes.

    arrDefinition - this is what our contract looks like, let’s look at it in details. When we need some address to subscribe we use [‘address’, address] When we need to check the publication data_feed we use [‘in data feed’, [[conf.oracle_address], name, operator, value]]

    assocSignersByPath - in this object, we prescribe the path in the array of the contract before you need to sign and who should sign it. In our example, we sign all ‘address’. The path starts with r-this is the first element in the array, in our case ‘or’ construction. Other examples with signatures will be discussed in the following lessons.

    walletDefinedByAddresses.createNewSharedAddress - we create a contract address headlessWallet.issueChangeAddressAndSendPayment - we add the address of the contract and make a payment to it

    This is how we create a message that contains a contract and a payment request.

    Create correspondents.js

    We need to tell the oracle to check and publish the weather at the specified time, for this we use this feature.

    We check whether there is an oracle in our correspondents and if it is not - we add.

    That’s all. I also have a homework for you. Complete the oracle and our bot so that oracle informs us that he published and the publication has become stable. After oracle message, notify the user that he lost or won. And if he lost then take the money out of the contract. All code is available on . Did you like the lesson? Any questions? What topics have caused you difficulties and it is worth talking about them? Write in the comments and I will help you. Also join my telegram chat - .

    Issuing assets on Obyte

    On Obyte, users can issue new assets and define rules that govern their transferability.

    To start a user-defined asset on Obyte, you need to complete two steps

    • Define your asset. Asset definition is a special type of message (distinct from payments) that you send to the Obyte DAG

    • Issue the asset by sending the 1st transaction that spends it

    Asset ID

    After an asset is defined, the hash of the unit where it was defined becomes the unique ID of the asset. The asset is referenced by this ID in future payments and definitions (smart contracts). There are no user-assignable names of assets in the protocol, but there are independent registries that link asset IDs to user-friendly names for a fee. One such registry is , another decentralized registry is

    If you are not a developer, but wish to issue an asset on Obyte then you can also use to do that without writing any code. If Obyte Asset Registry is too limited for your needs or you need to issue an asset on TESTNET then continue reading.

    Defining a new asset

    See a sample of asset-defining transaction at .

    To define a new asset you need a headless wallet. In your dependencies:

    Call this function to create a new asset definition and broadcast it:

    Here my_address is your address you use to post the asset-defining message. The address must be funded to pay at least the transaction fees (about 1000 bytes).

    asset is a javascript object that describes the properties of your asset. Here is an example:

    cap is the total number of coins that can be issued (money supply). If omitted, the number is unlimited.

    is_private: indicates whether the asset is private (such as blackbytes) or publicly traceable (similar to bytes).

    is_transferrable: indicates whether the asset can be freely transferred among arbitrary parties or all transfers should involve the definer address as either sender or recipient. The latter can be useful e.g. for loyalty points that cannot be resold.

    auto_destroy: indicates whether the asset is destroyed when it is sent to the definer address.

    fixed_denominations: indicates whether the asset exists as coins (banknotes) of a limited set of denominations, similar to blackbytes. If it is true, the definition must also include property denominations, which is an array of all denominations and the number of coins of that denomination:

    issued_by_definer_only: indicates whether the asset can be issued only by the definer address. If false, anyone can issue the asset, in this case cap must be unlimited and it would make sense that issuing is limited by issue_condition.

    cosigned_by_definer: indicates whether each operation with the asset must be cosigned by the definer address. Useful for regulated assets where the issuer (bank) wants to perform various compliance checks (such as the funds are not arrested by a court order) prior to approving a transaction. This could also be used to allow fee-less asset transfers for users (paid by definer), but wallets don't support that yet.

    spender_attested: indicates whether the spender of the asset must be attested by one of approved attestors. Also useful for regulated assets e.g. to limit the access to the asset only to KYC'ed users. If true, the definition must also include the list of approved attestor addresses:

    The definition can also include two optional properties issue_condition and transfer_condition which specify the restrictions when the asset can be issued and transferred. They evaluate to a boolean and are coded in the same as address definitions.

    The above conditions stipulate that the newly defined asset can be issued if the issuer also sends equal amount of bytes to the address MO7ZZIU5VXHRZGGHVSZWLWL64IEND5K2, and it can be transferred if equal amount of bytes is transferred to the same address at the same time.

    Issuance of a new asset

    The new asset can be issued after its definition is confirmed. To issue, you need just to spend the asset from an address that is allowed to issue and it will be issued to address you transferred it.

    You cannot use wallet.sendMultiPayment(), headlessWallet.issueChangeAddressAndSendPayment()and similar functions because they can do only transfers and they don't know about addresses that can issue new coins. Instead, you have to use lower level functions:

    • composeAndSaveDivisibleAssetPaymentJoint from ocore/divisible_asset.js

    • composeAndSaveIndivisibleAssetPaymentJoint from ocore/indivisible_asset.js

    See (assets without fixed denominations) and (assets with fixed denominations) for examples.

    See an example of how an asset can be both defined and issued in a single script

    Whitepaper

    For more details see chapter 24 of the .

    Smart contract language reference

    Authentication

    These clauses authenticate the author(s) of the unit.

    sig

    Payments gateway

    Obyte can accept Byte payments for you and notify you when the Bytes have arrived to your account. There is also an easy-to-setup Wordpress/Woocommerce plugin.

    Requirements

    • A Obyte light wallet to provide us with your merchant Obyte address (you do not need to run the heavy full wallet, we are running it for you). Download the wallet from .

    headlessWallet.readSingleWallet(walletId => {
      db.query("SELECT address FROM my_addresses WHERE wallet=?", [walletId], function (rows) {
         if (rows.length === 0)
            throw Error("no addresses");
         my_address = rows[0].address;
      });
    });
    eventBus.on('text', (from_address, text) => {
         text = text.trim().toLowerCase();
         if (!steps[from_address]) steps[from_address] = 'start';
         let step = steps[from_address];
         const device = require('ocore/device.js');
         if (validationUtils.isValidAddress(text.toUpperCase())) { // If address sent to us - save it.
            assocDeviceAddressToAddress[from_address] = text.toUpperCase();
            return device.sendMessageToDevice(from_address, 'text', 'I saved your address. Send me the name of the city');
         } else if (!assocDeviceAddressToAddress[from_address]) { // If we do not know the address - asking it to send for us.
            return device.sendMessageToDevice(from_address, 'text', 'Please send me your byteball address(... > Insert my address)');
         } else if (step === 'city' && (text === 'more' || text === 'less')) { // Create a contract and ask to pay for it
            let time = Date.now() + 20 * 60 * 1000;
            let name = assocDeviceAddressToCurrentCityAndTemp[from_address].city + '_' + time;
            let operator = text === 'more' ? '>=' : '<=';
            let value = assocDeviceAddressToCurrentCityAndTemp[from_address].temp;
            createContract(my_address, assocDeviceAddressToAddress[from_address], 2001, 2000, from_address, name, operator, value, time,
               (err, paymentRequestText, shared_address, timeout) => {
                  if (err) throw err;
                  findOracleAndSendMessage(assocDeviceAddressToCurrentCityAndTemp[from_address].city + ':' + time, () => {
                     bets.push({
                        myAmount: 2001,
                        peerAmount: 2000,
                        myAddress: my_address,
                        peerAddress: assocDeviceAddressToAddress[from_address],
                        sharedAddress: shared_address,
                        peerDeviceAddress: from_address,
                        name,
                        operator,
                        value,
                        timeout
                     });
                     device.sendMessageToDevice(from_address, 'text', paymentRequestText);
                     setTimeout(() => { // If the payment was not made for 10 minutes-we take the money
                        headlessWallet.sendAllBytesFromAddress(shared_address, my_address, from_address, (err, unit) => {
                           if (!err) console.error('unit:: ', unit);
                        });
                     }, 15 * 60 * 1000);
                  });
               });
         } else if (step === 'start') { // Check the weather and specify what rate the user wants to make
            switch (text) {
               case 'berlin':
               case 'moscow':
               case 'helsinki':
               case 'washington':
                  request('https://api.apixu.com/v1/current.json?key=' + KEY_API + '&q=' + text, function (error, response, body) {
                     if (error) {
                        console.error(error);
                        device.sendMessageToDevice(from_address, 'text', 'An error occurred. Try again later.');
                     } else {
                        let result = JSON.parse(body);
                        let temp = result.current.temp_c;
                        device.sendMessageToDevice(from_address, 'text', 'Want to bet that in an hour the weather in ' + text + ' will be\n[more ' + temp + 'c](command:more) or [less ' + temp + 'c](command:less)?');
                        steps[from_address] = 'city';
                        assocDeviceAddressToCurrentCityAndTemp[from_address] = {city: text, temp};
                     }
                  });
                  break;
               default:
                  device.sendMessageToDevice(from_address, 'text', "City not support");
                  break;
            }
         } else {
            device.sendMessageToDevice(from_address, 'text', "unknown command");
         }
      });
    });
    function createContract(myAddress, peerAddress, myAmount, peerAmount, peerDeviceAddress, name, operator, value, time, cb) {
      const device = require('ocore/device');
      let timeout = Date.now() + 10 * 60 * 1000;
      let inverseOperator = operator === '>=' ? '<' : '>';
      let arrSeenConditionPeer = ['seen', {
         what: 'output',
         address: 'this address',
         asset: 'base',
         amount: peerAmount
      }];
      let arrSeenConditionMyInput = ['seen', {
         what: 'input',
         address: 'this address',
         asset: 'base'
      }];
      let arrDefinition = ['or', [
         ['or', [
            ['and', [
               ['address', peerAddress],
               arrSeenConditionPeer,
               arrSeenConditionMyInput
            ]],
            ['or', [
               ['and', [
                  ['address', peerAddress],
                  ['in data feed', [[conf.oracle_address], name, operator, value]],
               ]],
               ['and', [
                  ['address', myAddress],
                  ['in data feed', [[conf.oracle_address], name, inverseOperator, value]]
               ]]
            ]],
         ]],
         ['and', [
            ['address', myAddress],
            ['not', arrSeenConditionPeer],
            ['in data feed', [[conf.TIMESTAMPER_ADDRESS], 'timestamp', '>', timeout]]
         ]]
      ]];
      let assocSignersByPath = {
         'r.0.0.0': {
            address: peerAddress,
            member_signing_path: 'r',
            device_address: peerDeviceAddress
         }, 'r.0.1.0.0': {
            address: peerAddress,
            member_signing_path: 'r',
            device_address: peerDeviceAddress
         },
         'r.1.0': {
            address: myAddress,
            member_signing_path: 'r',
            device_address: device.getMyDeviceAddress()
         },
         'r.0.1.1.0': {
            address: myAddress,
            member_signing_path: 'r',
            device_address: device.getMyDeviceAddress()
         }
      };
     
      let walletDefinedByAddresses = require('ocore/wallet_defined_by_addresses.js');
      walletDefinedByAddresses.createNewSharedAddress(arrDefinition, assocSignersByPath, {
         ifError: (err) => {
            cb(err);
         },
         ifOk: (shared_address) => {
            headlessWallet.issueChangeAddressAndSendPayment('base', myAmount, shared_address, peerDeviceAddress, (err, unit) => {
               if (err) return cb(err);
               let arrPayments = [{
                  address: shared_address,
                  amount: peerAmount,
                  asset: 'base'
               }];
               let assocDefinitions = {};
               assocDefinitions[shared_address] = {
                  definition: arrDefinition,
                  signers: assocSignersByPath
               };
               let objPaymentRequest = {payments: arrPayments, definitions: assocDefinitions};
               let paymentJson = JSON.stringify(objPaymentRequest);
               let paymentJsonBase64 = Buffer.from(paymentJson).toString('base64');
               let paymentRequestCode = 'payment:' + paymentJsonBase64;
               let paymentRequestText = '[...](' + paymentRequestCode + ')';
               cb(null, paymentRequestText, shared_address, timeout);
            });
         }
      });
    }
    let arrPayments = [{
                  address: shared_address,
                  amount: peerAmount,
                  asset: 'base'
               }];
               let assocDefinitions = {};
               assocDefinitions[shared_address] = {
                  definition: arrDefinition,
                  signers: assocSignersByPath
               };
               let objPaymentRequest = {payments: arrPayments, definitions: assocDefinitions};
               let paymentJson = JSON.stringify(objPaymentRequest);
               let paymentJsonBase64 = Buffer(paymentJson).toString('base64');
               let paymentRequestCode = 'payment:' + paymentJsonBase64;
               let paymentRequestText = '[your share of payment to the contract](' + paymentRequestCode + ')';
    github
    @obytedev
    Obyte Asset Registry
    tokens.ooo
    Obyte Asset Registry
    https://github.com/byteball/headless-obyte/blob/master/tools/create_asset.js
    smart contract language
    create_divisible_asset_payment.js
    create_indivisible_asset_payment.js
    https://github.com/byteball/ico-bot/blob/master/scripts/issue_tokens.js
    whitepaper
    Here is an example of the simplest address definition that defines an address controlled by a single private key:

    The pubkey above is base64-encoded public key. The sig expression evaluates to true if the signature provided with the transaction is valid and produced by the private key that corresponds to the above public key. The address (checksummed hash in base32) corresponding to this definition is A2WWHN7755YZVMXCBLMFWRSLKSZJN3FU.

    hash

    This clause evaluates to true if the author supplies a preimage of the hash specified in the address definition.

    Logical operators

    These clauses allow to combine other conditions with logical operators.

    and, or

    All expressions in this language evaluate to a boolean value, and multiple boolean subexpressions can be combined using boolean operators and and or. For example, this is a definition that requires two signatures:

    To spend funds from the address equal to the hash of the above definition, one would need to provide two signatures.

    As you noticed, we use JSON to construct the language expressions. This is an unusual choice but allows to use existing well-debugged, well-supported, and well-optimized JSON parsers rather than invent our own.

    “Or” condition can be used to require signatures by any one of the listed public keys:

    The above is useful when you want to control the same address from any of the 3 devices: your laptop, your phone, and your tablet.

    The conditions can be nested:

    r of set

    A definition can require a minimum number of conditions to be true out of a larger set, for example, a 2-of-3 signature:

    (“r” stands for “required”) which features both the security of two mandatory signatures and the reliability, so that in case one of the keys is lost, the address is still usable and can be used to change its definition and replace the lost 3rd key with a new one, or to move the funds to another address.

    weighted and

    Also, different conditions can be given different weights, of which a minimum is required:

    not

    Subsequent conditions can be negated with not clause:

    Since it is legal to select very old parents (that didn’t see the newer data feed posts), one usually combines negative conditions such as the above with the requirement that the timestamp is after a certain date.

    sig, hash, address, cosigned by, and in merkle cannot be negated.

    References

    These clauses redirect the evaluation of the subdefinition to something else.

    address

    A definition can contain reference to another address using address clause:

    which delegates signing to another address and is useful for building shared control addresses (addresses controlled by several users in contracts). This syntax gives the users the flexibility to change definitions of their own component addresses whenever they like, without bothering the other user.

    definition template

    A definition can reference a definition template:

    The parameters specify values of variables to be replaced in the template. The template needs to be saved before (and as usual, be stable before use) with a special message type app=“definition_template”, the template itself is in message payload, and the template looks like normal definition but may include references to variables in the syntax @param1, @param2. Definition templates enable code reuse. They may in turn reference other templates.

    Extrospection

    These clauses inspect data outside of the current unit.

    in data feed

    in data feed clause can be used to make queries about data previously stored on Obyte:

    This condition evaluates to true if there is at least one previous message stored in Obyte database that has “data feed name” equal to “expected value”. Instead of =, you can also use >, <, >=, <=, or != The data feed must be posted to Obyte decentralized database by one of the oracles whose addresses are “ADDRESS1”, “ADDRESS2”, … Since oracles post to the common database, we call them on-chain oracles.

    On-chain oracles are a very powerful thing indeed. For example, this address definition represents a binary option:

    Initially, the two parties fund the address defined by this definition by sending their respective stakes to the address. Then if the EUR/USD exchange rate published by the exchange address ever exceeds 1.2500, the first party can sweep the funds. If this doesn’t happen before timeout period, the second party can sweep all the funds stored on this address.

    Another example would be a customer who buys goods from a merchant but he doesn’t quite trust that merchant and wants his money back in case the goods are not delivered. The customer pays to a shared address defined by:

    The definition depends on the FedEx oracle that posts tracking numbers of all successfully delivered shipments. If the shipment is delivered, the merchant will be able to unlock the money using the first condition. If it is not delivered before the specified timeout period, the customer can take his money back. This example is somewhat crazy because it requires FedEx to post each and every shipment. See in merkle clause below for a more practical way to achieve the same result.

    in merkle

    in merkle is a more economical way to query the presence of a particular data entry in a large data set. Instead of posting every data entry in a data_feed message, only the merkle root of the large data set is posted as a data_feed, and the signer has to provide the data entry and its merkle path:

    seen address

    This clause evaluates to true if the specified address was seen as author in at least one past unit included in the last stable unit.

    seen

    This clause evaluates to true if there was an input or output in the past (before last stable unit) that satisfies the specified condition. The syntax for the search condition is the same as for has clause below.

    seen definition change

    This clause evaluates to true if there was a definition change of the specified address and the c-hash (checksummed hash) of the new definition is equal to the specified value.

    age

    This clause evaluates to true if the age of all inputs spent from this address satisfies the specified condition. The age is the difference between last ball mci and the input’s mci.

    attested

    This clause evaluates to true if the specified address is attested by one of the listed attestors. The address can also be “this address”.

    Introspection

    These clauses inspect data within the current unit

    cosigned by

    A subdefinition may require that the transaction be cosigned by another address:

    has, has one

    A definition can also include queries about the transaction itself, which can be used for example to code limit orders on a trustless exchange. Assume that a user wants to buy 1,200 units of some asset for which he is willing to pay no more than 1,000 bytes (the native currency of Obyte). Also, he is not willing to stay online all the time while he is waiting for a seller. He would rather just post an order at an exchange and let it execute when a matching seller comes along. He can create a limit order by sending 1,000 bytes to an address defined by this definition, which makes use of has clause:

    The first or-alternative lets the user take back his bytes whenever he likes, thus cancelling the order. The second alternative delegates the exchange the right to spend the funds, provided that another output on the same transaction pays at least 1,200 units of the other asset to the user’s address. The exchange would publicly list the order, a seller would find it, compose a transaction that exchanges assets, and sign it together with the exchange. Note that the exchange does not receive arbitrary control over the user’s funds, it can spend them only if it simultaneously pays the alternative asset to the user, while the user retains full control over his funds and can withdraw them from the contract when he likes.

    The has clause evaluates to true if the transaction has at least one input or output that satisfies all the set conditions:

    • what: input or output, required

    • asset: ID of asset (44-character string) or base for bytes

    • type: for inputs only, transfer or issue. If specified, it searches only transfers or only issues

    • amount_at_least, amount_at_most, amount: amount must be at least, at most, or exactly equal to the specified value

    • address: address where the output is sent to or the input is spent from, can be literal address or “this address” or “other address”

    Similar clause has one has exactly the same syntax but evaluates to true if there is exactly one input or output that satisfies the search conditions.

    has equal, has one equal

    The following requirement can also be included in a subdefinition:

    It evaluates to true if there is at least one pair of inputs or outputs that satisfy the search criteria and the fields specified in equal_fields are equal. The first element of the pair is searched by the first set of filters, the second by the second, and the syntax for the search conditions is the same as in has clause.

    A similar condition has one equal requires that there is exactly one such pair.

    sum

    This clause evaluates to true if the sum of inputs or outputs that satisfy the filter is equal, at least, or at most the target value.

    The syntax for the filter is the same as for has clause.

    has definition change

    This clause evaluates to true if the unit has a definition change of the specified address and the c-hash (checksummed hash) of the new definition is equal to the specified value.

    mci

    This clause evaluates to true if last_ball_mci of the current unit is greater than (other possible comparisons: >=, <, <=, =) than the specified value. It can be useful to make the address spendable only after some point in the future and not rely on any timestamp oracles.

    timestamp

    This clause evaluates to true If the time is longer than the specified time. You can also use "=", "<", ">=", "<=", "!=". This example returns true 2 hours after the deployment.

    const db = require('ocore/db');
    
    exports.addCorrespondent = (code, name, cb) => {
      let device = require('ocore/device');
     
      function handleCode(code) {
         let matches = code.match(/^([\w\/+]+)@([\w.:\/-]+)#([\w\/+-]+)$/);
         if (!matches)
            return cb("Invalid pairing code");
         let pubkey = matches[1];
         let hub = matches[2];
         let pairing_secret = matches[3];
         if (pubkey.length !== 44)
            return cb("Invalid pubkey length");
        
         acceptInvitation(hub, pubkey, pairing_secret, cb);
      }
     
      function acceptInvitation(hub_host, device_pubkey, pairing_secret, cb) {
         if (device_pubkey === device.getMyDevicePubKey())
            return cb("cannot pair with myself");
         if (!device.isValidPubKey(device_pubkey))
            return cb("invalid peer public key");
         // the correspondent will be initially called 'New', we'll rename it as soon as we receive the reverse pairing secret back
         device.addUnconfirmedCorrespondent(device_pubkey, hub_host, name, (device_address) => {
            device.startWaitingForPairing((reversePairingInfo) => {
               device.sendPairingMessage(hub_host, device_pubkey, pairing_secret, reversePairingInfo.pairing_secret, {
                  ifOk: () =>{
                     cb(null, device_address);
                  },
                  ifError: cb
               });
            });
         });
      }
     
      handleCode(code);
    };
    
    exports.findCorrespondentByPairingCode = (code, cb) => {
      let matches = code.match(/^([\w\/+]+)@([\w.:\/-]+)#([\w\/+-]+)$/);
      if (!matches)
         return cb("Invalid pairing code");
      let pubkey = matches[1];
      let hub = matches[2];
     
      db.query("SELECT * FROM correspondent_devices WHERE pubkey = ? AND hub = ?", [pubkey, hub], (rows) => {
         return cb(rows.length ? rows[0] : null);
      });
    };
    function findOracleAndSendMessage(value, cb) {
      const device = require('ocore/device');
      correspondents.findCorrespondentByPairingCode(conf.oracle_pairing_code, (correspondent) => {
         if (!correspondent) {
            correspondents.addCorrespondent(conf.oracle_pairing_code, 'flight oracle', (err, device_address) => {
               if (err)
                  throw new Error(err);
               device.sendMessageToDevice(device_address, 'text', value);
               cb();
            });
         } else {
            device.sendMessageToDevice(correspondent.device_address, 'text', value);
            cb();
         }
      });
    }
    "dependencies": {
    	"headless-obyte": "git+https://github.com/byteball/headless-obyte.git",
    	"ocore": "git+https://github.com/byteball/ocore.git"
    	// .....
    }
    var headlessWallet = require('headless-obyte');
    var network = require('ocore/network.js');
    var composer = require('ocore/composer.js');
    
    var my_address = ''; // set definer address.
    var asset = {}; // defined asset here.
    composer.composeAssetDefinitionJoint(my_address, asset, headlessWallet.signer,
        {
            ifError: console.error,
            ifNotEnoughFunds: console.error,
            ifOk: function(objJoint, assocPrivatePayloads, composer_unlock) {
                network.broadcastJoint(objJoint);
                console.error('==== Asset ID:'+ objJoint.unit.unit);
            }
        }
    );
    var asset = {
    	cap: 1000000,
    	is_private: false,
    	is_transferrable: true,
    	auto_destroy: false,
    	fixed_denominations: false,
    	issued_by_definer_only: true,
    	cosigned_by_definer: false,
    	spender_attested: false
    }
    var asset = {
    	// ....
    	fixed_denominations: true,
    	denominations: [
    		{denomination: 1, count_coins: 1e10},
    		{denomination: 2, count_coins: 2e10},
    		{denomination: 5, count_coins: 1e10},
    		{denomination: 10, count_coins: 1e10},
    		{denomination: 20, count_coins: 2e10},
    		{denomination: 50, count_coins: 1e10},
    		{denomination: 100, count_coins: 1e10},
    		{denomination: 200, count_coins: 2e10},
    		{denomination: 500, count_coins: 1e10},
    		{denomination: 1000, count_coins: 1e10},
    		{denomination: 2000, count_coins: 2e10},
    		{denomination: 5000, count_coins: 1e10},
    		{denomination: 10000, count_coins: 1e10},
    		{denomination: 20000, count_coins: 2e10},
    		{denomination: 50000, count_coins: 1e10},
    		{denomination: 100000, count_coins: 1e10}
    	]
    };
    var asset = {
    	// ....
    	spender_attested: true,
    	attestors: ["ADDRESS1", "ADDRESS2"]
    };
    var asset = {
    	// ....
    	issue_condition: ["and", [
    		["has one", {what: "output", asset: "this asset"}],
    		["has", {what: "output", asset: "base", address: "MO7ZZIU5VXHRZGGHVSZWLWL64IEND5K2"}],
    		["has one equal", {
    			equal_fields: ["amount"], 
    			search_criteria: [{what: "output", asset: "base"}, {what: "output", asset: "this asset"}]
    		}]
    	]],
    	transfer_condition: ["and", [
    		["has one", {what: "output", asset: "this asset"}],
    		["has one equal", {
    			equal_fields: ["address", "amount"], 
    			search_criteria: [{what: "output", asset: "base"}, {what: "output", asset: "this asset"}]
    		}]
    	]]
    };
    ["sig", {"pubkey": "Ald9tkgiUZQQ1djpZgv2ez7xf1ZvYAsTLhudhvn0931w"}]
    ["hash", {"hash": "value of sha256 hash in base64"}]
    ["and", [
        ["sig", {pubkey: "one pubkey in base64"}],
        ["sig", {pubkey: "another pubkey in base64"}]
    ]]
    ["or", [
        ["sig", {pubkey: "laptop pubkey"}],
        ["sig", {pubkey: "smartphone pubkey"}],
        ["sig", {pubkey: "tablet pubkey"}]
    ]]
    ["and", [
        ["or", [
            ["sig", {pubkey: "laptop pubkey"}],
            ["sig", {pubkey: "tablet pubkey"}]
        ]],
        ["sig", {pubkey: "smartphone pubkey"}]
    ]]
    ["r of set", {
        required: 2,
        set: [
            ["sig", {pubkey: "laptop pubkey"}],
            ["sig", {pubkey: "smartphone pubkey"}],
            ["sig", {pubkey: "tablet pubkey"}]
        ]
    }]
    ["weighted and", {
        required: 50,
        set: [
            {weight: 40, value: ["sig", {pubkey: "CEO pubkey"}] },
            {weight: 20, value: ["sig", {pubkey: "COO pubkey"}] },
            {weight: 20, value: ["sig", {pubkey: "CFO pubkey"}] },
            {weight: 20, value: ["sig", {pubkey: "CTO pubkey"}] }
        ]
    }]
    ["not", ["in data feed", [["NOAA ADDRESS"], "wind_speed", ">", "200"]]]
    ["and", [
        ["address", "ADDRESS 1 IN BASE32"],
        ["address", "ADDRESS 2 IN BASE32"]
    ]]
    ["definition template", [
        "hash of unit where the template was defined",
        {param1: "value1", param2: "value2"}
    ]]
    ["in data feed", [
        ["ADDRESS1", "ADDRESS2", …], 
        "data feed name", 
        "=", 
        "expected value"
    ]]
    ["or", [
        ["and", [
            ["address", "ADDRESS 1"],
            ["in data feed", [["EXCHANGE ADDRESS"], "EURUSD", ">", "1.2500"]]
        ]],
        ["and", [
            ["address", "ADDRESS 2"],
            ["timestamp", [">", Math.round(Date.now()/1000 + timeout_seconds)]]
        ]]
    ]]
    ["or", [
        ["and", [
            ["address", "MERCHANT ADDRESS"],
            ["in data feed", [["FEDEX ADDRESS"], "tracking", "=", "123456"]]
        ]],
        ["and", [
            ["address", "BUYER ADDRESS"],
            ["timestamp", [">", Math.round(Date.now()/1000 + timeout_seconds)]]
        ]]
    ]]
    ["in merkle", [
        ["ADDRESS1", "ADDRESS2", ...],
        "data feed name",
        "expected value"
    ]]
    ["seen address", "ANOTHER ADDRESS IN BASE32"]
    ["seen", {
        what: "output",
        address: "ADDRESS",
        asset: "asset or base",
        amount: 12345
    }]
    ["seen definition change", ["ADDRESS", "NEW DEFINITION CHASH"] ]
    ["age", [">", 1234]]
    ["attested", ["ADDRESS", ["ATTESTOR1", "ATTESTOR2", ...]]]
    ["cosigned by", "ANOTHER ADDRESS IN BASE32"]
    ["or", [
        ["address", "USER ADDRESS"],
        ["and", [
            ["address", "EXCHANGE ADDRESS"],
            ["has", {
                what: "output", 
                asset: "ID of alternative asset", 
                amount_at_least: 1200, 
                address: "USER ADDRESS"
            }]
        ]]
    ]]
    ["has equal", {
        equal_fields: ["address", "amount"],
        search_criteria: [
            {what: "output", asset: "asset1", address: "ADDRESS IN BASE32"},
            {what: "input", asset: "asset2", type: "issue", address: "ANOTHER ADDRESS IN BASE32"}
        ]
    }]
    ["sum", {
        filter: {
            what: "input"|"output", asset: "asset or base", type: "transfer"|"issue",
            address: "ADDRESS IN BASE32"
        },
        at_least: 120,
        at_most: 130,
        equals: 123
    }]
    ["has definition change", ["ADDRESS", "NEW DEFINITION CHASH"] ]
    ["mci", ">", 123456]
    ['timestamp', ['>', Math.round(Date.now() / 1000 + 2 * 3600)]]

    If you want to use our Woocommerce plugin skip reading now and go to: https://wordpress.org/plugins/woobytes-gateway/

  • Upon going live a merchant_id and a secret_key. Contact us on https://obyte.org/discord #merchants channel to get your merchant_id and you secret_key.

  • Integration process

    Step 1: Either in test mode or in live mode, customize the Obyte quick payment button script with your merchant parameters and insert it in your checkout web page. Obyte quick payment button init script has to be inserted in the header section of your checkout page and the button itself can be displayed anywhere on the page in a Div container. The test mode triggers the whole notification process except there is no real payment. Once you are done with your tests, switch to live mode by changing the "mode" parameter from "test" to "live" in the button init script. See example.html: https://github.com/tarmo888/bbfm/tree/master/bbfm_www/example

    Step 2: We notify you (upon your choice), either by mail and/or by a GET or a POST to your server, when a payment for your sale has arrived, we simultaneously send you the payment to your merchant address, minus our fee. At this stage you receive a first notification with an unconfirmed "result" field. When the payment is confirmed by the network you receive a second notification with a ok "result" field. Such notification of a confirmed payment typically takes between 5 to 15 minutes from the customer click to pay (live mode) or 1 minute (in test mode). You then have to handle the notification on your end so that you actualy process your customer order. You can process the delivery as soon as you received the ok notification (processing on unconfirmed notification is at your own risk!). In any case always check on your end that received_amount does match amount_asked_in_B before processing the order or contact your customer for missing Bytes (the event of an incomplete payment is not an error case on our end but it must be one on your end).

    Parameters

    Step 1: Obyte payment button field list

    • order_UID (mandatory): Your merchant order ID (should match /^[a-zA-Z0-9-]{4,20}$/).

    • merchant_return_url (optional): The full URL we will POST or GET to notify you a payment arrived.

    • mode_notif (optional, default is get): Shall we GET or POST the notification to your server? (should match get | post).

    • currency (optional, default B): Currency from which convert the amount into Bytes (should be B (for Bytes), BTC, ETH, USD, EUR, PLN, KRW, GBP, CAD, JPY, RUB, TRY, NZD, AUD, CHF, UAH, HKD, SGD, NGN, PHP, MXN, BRL, THB, CLP, CNY, CZK, DKK, HUF, IDR, ILS, INR, MYR, NOK, PKR, SEK, TWD, ZAR, VND, BOB, COP, PEN, ARS, ISK). Conversion is done automaticaly in real time at rates from .

    • merchant_email (optional): Fill in your email address if you want to be notified by email when payment arrives OR to get an alert email if we were not able to GET or POST to notify you on your merchant_return_url. Take care to whitelist emails from "[email protected]" (you can usually do that by adding this email to your contacts).

    • amount (mandatory): The amount charged to your customer in currency (should match /^[0-9]+.?[0-9]*$/). Minimum amount is 1500 Bytes (or its equivalent in currency) Maximum amount is 1000 GBytes.

    • byteball_merchant_address (mandatory): Your unique Obyte merchant address to receive your payment.

    • merchant_id (mandatory): In test mode merchant_id is test_merchant. Contact us on #merchants channel to get a merchant id when you first go live.

    • mode (mandatory): live | test. Are you testing the implementation or are you running the Obyte payment button from a production server? (should match live or test and will be returned as is on merchant_return_url).

    • attach (optional): free text field (255 chars). Will be returned as is for your own convenience (must match /^[a-zA-Z0-9_.@,=?& -]{0,255}$/).

    • qrcode (optional): you can here indicate your own qrcode url mask to use the qrcode api of your choice, using {text} as url placeholder. By default, the Google QRCode API is used, that is to say the value is: '' But you can change it and choose for instance the Chinese liantu API by setting the value: ''

    Step 2: Returned field list on merchant_return_url

    • mode: live | test

      WARNING! WARNING! WARNING! Always check this field as in test mode the notification of payment received is always sent for integration and test purpose without any effective payment. Do not use the test mode on a production merchant server.

    • result: nok | incoming | unconfirmed | ok

      'nok': check error_msg for text error.

      'incoming': the payment has been sent to us by your customer but it is still waiting for network confirmation.

      'unconfirmed': the payment has been sent to you but it is still waiting for network confirmation.

      'ok': final step. The payment unit we sent to you has been confirmed by the network. Process the order on your end.

    • error_msg (occurence: 1 on result=nok or 0): human readable text field

    • order_UID: Your merchant order ID

    • currency: B (for Bytes, see rest above).

    • amount_asked_in_currency (occurence: 0 on currency=B or 1): decimal

      WARNING! WARNING! WARNING! Always check amount_asked_in_currency does match the amount of the order in your own database and in your expected currency.

    • currency_B_rate (occurence: 0 on currency=B or 1): decimal

    • amount_asked_in_B: integer (in Bytes)

    • fee: our fee (in Bytes)

    • received_amount: the amount we received (in Bytes).

      WARNING! WARNING! WARNING! check on your end that received_amount does match amount_asked_in_B before processing the order. Contact your customer if some Bytes are missing from what you expected (the event of an incomplete payment is not an error case on our end but it must be one on your end).

    • receive_unit: the DAG unit that shows the payment from your customer to our Obyte address.

    • amount_sent: the amount sent to your byteball_merchant_address (received_amount minus fee)

    • unit: the DAG unit that shows the payment from our Obyte address to your byteball_merchant_address

    • sha256_digest: the digest of the notification is computed as sha256(secret_key+order_UID+merchant_return_url+merchant_email) where '+' means concatenate.

      WARNING! WARNING! WARNING! You SHOULD re-calculate on your end the sha256 digest on each notification to check if it does match sha256_digest. An attacker could POST or GET to your merchant server a false notification but he will never be able to compute a correct sha256_digest. In test mode secret_key is 134c8nfWK4XrrfkgN1UJ9ytqae76d7uDbs.

    • attach: free text field (255 chars) returned "as is" from step 1 for your own convenience.

    Note: if you filled in the optional merchant_email on step 1 you will receive the same information in an email plus possible alert email when - for any reason - if we were not able to notify you on merchant_return_url (for example a server time out on your end). We advise you to provide the merchant_email in order to receive such alert emails. We strongly advise you to whitelist [email protected]

    https://obyte.org
    CoinPaprika
    https://obyte.org/discord
    https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl={text}&chld=H
    http://qr.liantu.com/api.php?text={text}&w=100

    Exposing RPC interface

    If you are developing a service on Obyte and your programming language is node.js, your best option is to just require() the ocore modules that you need.

    For exchanges and other custody wallets, there is a specialized JSON RPC service. However it is limited and exposes only those functions that the exchanges need.

    If you are developing a service on Obyte and your programming language is node.js, your best option is to just require() the ocore modules that you need (most likely you need headless-obyte and various modules inside ocore). This way, you'll also be running a Obyte node in-process.

    If you are programming in another language, or you'd like to run your Obyte node in a separate process, you can still access many of the functions of headless-obyte and ocoreby creating a thin RPC wrapper around the required functions and then calling them via JSON-RPC.

    Get started

    To get started, we can add RPCify to existing project or directly to headless-obyte with this command:

    See the documentation about for more details.

    Exposing functions and events

    To expose the required functions via JSON-RPC, create a project that has headless-obyte, ocore (and any other ocore modules) and as dependencies:

    Calling with HTTP requests

    From another Node.js app, calling the function would look something like this:

    Listening via WebSockets

    From another Node.js app, sending function calls and listening to events would look something like this:

    JustSaying

    This is a message type, which doesn't require response back.

    Example:

    Following is a list of justsaying type JSON messages that are sent over the network:

    Send version information

    Running RPC service

    If you run an exchange, you will likely want to interact with your Obyte node via RPC interface.

    By default, RPC service is not enabled for security reasons. To enable it, you should start your headless node differently: instead of node start.js, cd to tools folder and start RPC-enabled node:

    This headless node works as usual, plus it listens to port 6332 of loop-back interface (configured in or conf.json) for JSON-RPC commands. Here are some of the commands that are usually needed by custody wallet solutions, like exchanges (these are similar to older Bitcoin node RPC methods): getinfo, getnewaddress, validateaddress, getbalance, listtransactions, sendtoaddress

    RPCify
    RPCify
    Send free joints

    Send a private transaction

    Share your node WebSocket URL to accept incoming connections

    Ask to verify your WebSocket URL

    Verify your WebSocket URL with echo message

    Log in to Hub

    Get new messages

    Remove handled message

    Send pairing message

    Send message to device

    Ask more messages

    Light wallet transaction update

    Light wallet sequence became bad

    Add light wallet to monitor address

    Send bug report

    Push project number (only accepted from hub)

    New version is available (only accepted from hub)

    Exchange rates (only accepted from hub)

    Ask to update (only accepted from hub)

    Watch system variables

    This will subscribe you to updates of the currently active system variables and votes for them.

    Immediately after this request, you'll get the current state of all system variables and their past values with the MCIs when they were voted in:

    Similar data will be pushed to you whenever any system variable changes as a result of vote count.

    Also, user votes will be pushed to you as soon as they are sent by users (first, with is_stable=0, then with is_stable=1 as soon as the voting transaction becomes final):

    Custom JustSaying

    You can add your own communication protocol on top of the Obyte one. See event there.

    .

    There are more methods available, which documentation can be read from headless wallet documentation page, which is generated from JSDoc code comments.

    These are all simple HTTP request, but you can build your own custom JSON-RPC API that can use WebSockets too and even make it to listen events.

    getinfo

    The command returns information about the current state of the DAG.

    The command has no parameters and returns an object with 4 fields:

    • connections: number of incoming and outgoing connections

    • last_mci: the highest known main chain index (MCI)

    • last_stable_mci: last stable MCI (stability point)

    • count_unhandled: number of unhandled units in the queue. Large number indicates that sync is still in progress, 0 or small number means that the node is synced (it can occasionally go above 0 when new units are received out of order).

    getnewaddress

    This command generates a new address in your wallet. You will likely want to use it to create a new deposit address and bind it to a user account.

    Example usage:

    The command has no parameters and the response is a newly generated wallet address (32-character string).

    validateaddress

    This command validates a wallet address and returns true or false.

    You will likely want to use it before saving a withdrawal address for a customer. There is verifyaddress alias for this method too.

    getbalance

    Returns the balance of the specified address or the entire wallet.

    Example usage for querying wallet balance:

    Querying balance of an individual address:

    To query the balance of the entire wallet, parameters must be empty. To query the balance of an individual address, pass it as the only element of the params array.

    The response is an object, keyed by asset ID ("base" for Bytes). For each asset, there is another nested object with keys stable and pending for stable and pending balances respectively. Balances are in the smallest units (bytes for the native currency), they are always integers.

    If the queried address is invalid, you receive error "invalid address". If the address does not belong to your wallet, you receive error "address not found".

    listtransactions

    Use it to get the list of transactions on the wallet or on a specific address.

    Example request for transactions on the entire wallet (all addresses):

    On individual address:

    To query the transactions on an individual address, pass it as the only element of the params array. In this case, only transactions in bytes are returned. If the passed address is invalid, you receive error "invalid address".

    Using params as an object gives more flexibility of parameter ordering, but if address is set in a object or first element of array, all other parameters are ignored.

    To get the list of transactions in a particular asset, set an asset parameter in params. If asset is null or omitted, transactions in bytes will be returned.

    To query the list of transactions since a particular main chain index (MCI), specify since_mciproperty in the params object, e.g. "params": {"since_mci":254000} or "params": {"since_mci":254000; "asset": "f2TMkqij/E3qx3ALfVBA8q5ve5xAwimUm92UrEribIE="}. The full list of matching transactions will be returned, however large it is.

    To query an individual transaction, specify its unit in the params object: "params": {"unit":"vlt1vzMtLCIpb8K+IrvqdpNLA9DkkNAGABJ420NvOBs="}.

    The response is an array of transactions in reverse chronological order.

    Each transaction is described by an object with the following fields:

    • action: string, one of invalid, received, sent, moved.

    • amount: integer, amount of the transaction in the smallest units

    • my_address: string, the address that belongs to your wallet and received funds (for receivedand moved actions only)

    • addressTo: string, the address where the funds were moved (for moved and sent actions only)

    • arrPayerAddresses: array of payer addresses (for received only)

    • confirmations: integer 0 (pending) or 1 (final), shows confirmation status of the transaction

    • unit: string, unit of the transaction (also known as transaction id)

    • fee: integer, fee in bytes

    • time: integer, seconds since the Epoch

    • level: integer, level of the unit in the DAG

    • mci: integer, MCI of the unit. It can change while the unit is still pending and becomes immutable after the unit gets final

    Waiting for deposits

    To operate an exchange, you'll want to wait for new deposits using the following scenario:

    • call getinfo and remember last_stable_mci, you'll use it in the following (not this one) iteration;

    • call listtransactions with since_mci set to last_stable_mci remembered from the previous (not this one!) iteration;

    • look for transactions with actions received and moved (you need moved in case one user withdraws to a deposit address of another user) and identify the user by my_address;

    • wait;

    • repeat the cycle.

    In each cycle, save last_stable_mci in persistent storage and start from it after the wallet is restarted.

    If you support several Obyte-issued assets, you'll need to call listtransactions with that asset parameter and store last_stable_mci from getinfo method for each asset individually.

    sendtoaddress

    Use this command to withdraw bytes or another asset. Example usage:

    There are 3 parameters to this command:

    • destination address (32-character string);

    • amount in bytes or smallest indivisible units (integer);

    • (optional) asset (44-character string or null). If missing or null, the asset is assumed to be bytes.

    On success, the response is the unit of the spending transaction (string). If the command failed, an error message is returned. It is possible that the command returns error due to lack of confirmed funds, in this case you should retry the command in a few minutes. If the request timed out without returning an error, do not retry automatically, the funds might be already sent!

    If the destination address is invalid, the command returns error "invalid address". To avoid this, it is recommended to validate user-entered withdrawal address using validateaddress above or C++ function or ocore library module in NodeJs:

    Additional methods

    More detailed documentation of all RPC service methods can be found there https://byteball.github.io/headless-obyte/rpc_service.html

    conf.js
    npm install https://github.com/byteball/rpcify.git
    var rpcify = require('rpcify');
    var eventBus = require('ocore/event_bus.js');
    
    // this is a module whose methods you want to expose via RPC
    var headlessWallet = require('headless-obyte'); // when headless-obyte is dependency of your project
    //var headlessWallet = require('../start.js'); // when this script is in headless-obyte tools folder
    var balances = require('ocore/balances.js'); // another such module
    
    // most of these functions become available only after the passphrase is entered
    eventBus.once('headless_wallet_ready', function(){
    	// start listening on RPC port
    	rpcify.listen(6333, '127.0.0.1');
    
    	// expose some functions via RPC
    	rpcify.expose(headlessWallet.issueChangeAddressAndSendPayment);
    	rpcify.expose(balances.readBalance, true);
    	rpcify.expose(balances.readAllUnspentOutputs);
    	rpcify.expose([
    		headlessWallet.readFirstAddress,
    		headlessWallet.readSingleWallet,
    		headlessWallet.issueOrSelectAddressByIndex
    	], true);
    
    	// expose some events 
    	rpcify.exposeEvent(eventBus, "my_transactions_became_stable");
    	rpcify.exposeEvent(eventBus, "new_my_transactions");
    });
    var rpc    = require('json-rpc2');
    var client = rpc.Client.$create(6333, '127.0.0.1');
    
    client.call('issueChangeAddressAndSendPayment', [asset, amount, to_address, device_address], function(err, unit) {
        ...
    });
    var WebSocket = require('ws');
    var ws        = new WebSocket("ws://127.0.0.1:6333");
    
    ws.on('open', function onWsOpen() {
    	console.log("ws open");
    	ws.send(JSON.stringify({
    		"jsonrpc":"2.0",
    		"id":1,
    		"method":"readBalance",
    		"params":["LUTKZPUKQJDQMUUZAH4ULED6FSCA2FLI"]
    	})); // send a command
    });
    
    ws.on('error', function onWsError(e){
    	console.log("websocket error"+e);
    })
    
    ws.on('message', function onWsMessage(message){ // JSON responses
    	console.error(message);
    	// {"jsonrpc":"2.0","result":{"base":{"stable":0,"pending":0}},"error":null,"id":1} // response to readBalance
      // {"event":"my_transactions_became_stable","data":[["1pLZa3aVicNLE6vcClG2IvBe+tO0V7kDsxuzQCGlGuQ="]]} // event my_transactions_became_stable
    });
    const network = require('ocore/network.js');
    const eventBus = require('ocore/event_bus.js');
    const ws = network.getInboundDeviceWebSocket(device_address);
    
    // function parameters: websocket, subject, body
    network.sendJustsaying(ws, 'custom', 'some data');
    
    eventBus.on('custom_justsaying', function(ws, content) {
        console.log(content);
    };
    {
        type: 'justsaying',
        content: {
            subject: 'version', 
            body: {
                protocol_version: protocol_version, 
                alt: alt, 
                library: name, 
                library_version: version, 
                program: program, 
                program_version: program_version
            }
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'free_joints_end', 
            body: null
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'private_payment', 
            body: privateElement
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'my_url', 
            body: my_url
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'want_echo', 
            body: random_echo_string
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'your_echo', 
            body: echo_string
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'hub/login', 
            body: {
                challenge: challenge,
                pubkey: pubkey,
                signature: signature
            }
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'hub/refresh', 
            body: null
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'hub/delete', 
            body: message_hash
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'hub/challenge', 
            body: challenge
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'hub/message', 
            body: {
                message_hash: message_hash,
                message: message
            }
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'hub/message_box_status', 
            body: 'has_more'
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'light/have_updates', 
            body: null
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'light/sequence_became_bad', 
            body: arrUniqueUnits
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'light/new_address_to_watch', 
            body: address
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'bugreport', 
            body: {
                message: message,
                exception: exception
            }
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'hub/push_project_number', 
            body: {
                projectNumber: projectNumber
            }
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'new_version', 
            body: {
                version: version
            }
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'exchange_rates', 
            body: exchangeRates
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'upgrade_required', 
            body: null
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'watch_system_vars', 
            body: null
        }
    }
    {
        type: 'justsaying',
        content: {
            subject: 'system_vars', 
            body: {
                op_list: [
                    {
                        vote_count_mci: 123,
                        is_emergency: 0,
                        value: [
                            "2FF7PSL7FYXVU5UIQHCVDTTPUOOG75GX",
                            ....
                        ]
                    },
                    .....
                ],
                threshold_size: [
                    {
                        vote_count_mci: 456,
                        is_emergency: 0,
                        value: 10000
                    },
                    .....
                ],
                .....    
            }
        }
    }
    {
        type: "justsaying",
        content: {
            subject: "system_var_vote", 
            body: {
                subject: "op_list",
                value: [
                    "2FF7PSL7FYXVU5UIQHCVDTTPUOOG75GX",
                    ....
                ],
                author_addresses: ["EJC4A7WQGHEZEKW6RLO7F26SAR4LAQBU"],
                unit: "LpxhHEfxbOyj0sPlp6UC6XrRABvRJiH4qKEsOcMd1Bc=",
                is_stable: 1
            }
        }
    }
    {
        type: 'justsaying',
        content: {
            tag: tag,
            subject: 'custom',
            body: body
        }
    }
    cd tools
    node rpc_service.js
    $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getinfo", "params":{} }' http://127.0.0.1:6332 | json_pp
    
    {
      "jsonrpc": "2.0",
      "result": {
        "connections": 5,
        "last_mci": 253151,
        "last_stable_mci": 253120,
        "count_unhandled": 0
      },
      "id": 1
    }
    $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getnewaddress", "params":{} }' http://127.0.0.1:6332 | json_pp
    
    {
      "jsonrpc": "2.0",
      "result": "QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3",
      "id": 1
    }
    $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"validateaddress", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3"] }' http://127.0.0.1:6332 | json_pp
    
    {
      "jsonrpc": "2.0",
      "result": true,
      "id": 1
    }
    $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getbalance", "params":{} }' http://127.0.0.1:6332 | json_pp
    
    {
      "jsonrpc": "2.0",
      "result": {
        "base": {
          "stable": 8000,
          "pending": 0
        }
      },
      "id": 1
    }
    $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getbalance", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3"] }' http://127.0.0.1:6332 | json_pp
    $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"listtransactions", "params":{"since_mci": 1234} }' http://127.0.0.1:6332 | json_pp
    
    {
      "jsonrpc": "2.0",
      "result": [
        {
          "action": "received",
          "amount": 3000,
          "my_address": "YA3RYZ6FEUG3YEIDIJICGVPD6PPCTIZK",
          "arrPayerAddresses": [
            "EENED5HS2Y7IJ5HACSH4GHSCFRBLA6CN"
          ],
          "confirmations": 0,
          "unit": "sALugOU8fjVyUvtfKPP0pxlE74GlPqOJxMbwxA1B+eE=",
          "fee": 588,
          "time": "1490452729",
          "level": 253518
        },
        {
          "action": "received",
          "amount": 5000,
          "my_address": "QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3",
          "arrPayerAddresses": [
            "UOOHQW4ZKPTII4ZEE4ENAM5PC6LWAQHQ"
          ],
          "confirmations": 1,
          "unit": "vlt1vzMtLCIpb8K+IrvqdpNLA9DkkNAGABJ420NvOBs=",
          "fee": 541,
          "time": "1490452322",
          "level": 253483
        }
      ],
      "id": 1
    }
    $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"listtransactions", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3"] }' http://127.0.0.1:6332 | json_pp
    
    {
      "jsonrpc": "2.0",
      "result": [
        {
          "action": "received",
          "amount": 5000,
          "my_address": "QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3",
          "arrPayerAddresses": [
            "UOOHQW4ZKPTII4ZEE4ENAM5PC6LWAQHQ"
          ],
          "confirmations": 0,
          "unit": "vlt1vzMtLCIpb8K+IrvqdpNLA9DkkNAGABJ420NvOBs=",
          "fee": 541,
          "time": "1490452322",
          "level": 253483
        }
      ],
      "id": 1
    }
    $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"sendtoaddress", "params":["BVVJ2K7ENPZZ3VYZFWQWK7ISPCATFIW3", 1000] }' http://127.0.0.1:6332 | json_pp
    
    {
      "jsonrpc": "2.0",
      "result": "vuudtbL5ASwr0LJZ9tuV4S0j/lIsotJCKifphvGATmU=",
      "id": 1
    }
    var validationUtils = require("ocore/validation_utils.js");
    if (!validationUtils.isValidAddress(address)){
      // notify user that the entered wallet address is invalid
    }

    URI protocol

    This URI protocol enables developers to open the wallet app in desired screen with pre-filled inputs.

    If you want to open Obyte app for users then there is a obyte: protocol for that. If you are not aware what it is then it's like mailto: , ftp: , tel: or . When user visits a link with that protocol then it can open the wallet app in specific screen with pre-filled inputs, helping users to insert long account addresses or amounts without the need to copy-paste them.

    Opening Send screen

    Following hyperlinks will open the wallet app on Send screen. These hyperlinks (without the HTML) also work when posted as chat messages.

    Requesting to send funds

    We can request payments with chat bot messages and we can use the same commands as links on the website. This is how it will look as a hyperlink:

    ACCOUNT_ADDRESS should be replaced with the wallet address where the funds should be sent, amount parameter should have bigInt type number in bytes (not KByte, not MByte, not GByte) and asset parameter is optional, by default it is base for bytes, but it can be issued asset unit ID too.

    One website that heavily relies on this feature is Currency converter for Obyte (source code).

    It is also possible to request that the payment to be done from specific address in user's wallet. Since wallet version 3.3.0, this also work from chat messages.

    If you don't know user's account address, but still want them to user single-address account then you can use single_address=1 parameter like this:

    Requesting to send data to AA

    In order to open a Send screen with pre-filled payment and data, the data object needs to be converted into URI encoded Base64 (using JSON.stringify, btoa andencodeURIComponent) string. Many cases, when the Autonomous Agent stores data based on triggering address, we would also want the users to use single-address account, this can by adding the single_address=1 parameter. Autonomous agents can also accept nested data.

    This will open Send screen, which will be pre-filled to post 10 000 bytes and data object.

    Requesting to post data

    This will open Send screen, which will be pre-filled to post unstructured key/value pairs as data (single-address account required). Keys need to be unique.

    Requesting to post temporary data

    Like the above, this will open Send screen, which will be pre-filled to post unstructured key/value pairs as data (single-address account required) meant for temporary storage. Keys need to be unique.

    The data posted this way will be available on the Obyte DAG only for 1 day, after which it will be purged but the hashes of the data will stay on the DAG forever. Unlike permanent data, temporary data costs less in fees.

    Requesting to post data feed

    This will open Send screen, which will be pre-filled to post indexable key/value pairs as data feed (single-address account required). Keys need to be unique.

    Requesting to post a profile

    This will open Send screen, which will be pre-filled to post key/value pairs as profile info (single-address account required). Keys need to be unique.

    Requesting to post attestation

    This will open Send screen, which will be pre-filled to post key/value pairs as someone else attestation profile (single-address account required). Keys need to be unique.

    Requesting to post a poll

    This will open Send screen, which will be pre-filled to post poll (single-address account required). Option keys can be named anything, but values need to be unique. Question parameter is also required.

    Requesting to post a AA definition

    This will open Send screen, which will be pre-filled to post Autonomous Agent definition (single-address account required). The definition parameter needs to be URL encoded.

    Because most browsers support only URLs up to 2048 characters length, it is also possible to post a AA definition by making it fetch the definition from some other other address like this:

    Requesting to post short text content

    This will open Send screen, which will be pre-filled to post short text content (single-address account required). The content parameter needs to be URL encoded and can be maximum of 140 chars before encoding.

    Requesting to post a system vote

    This will open Send screen, which will be pre-filled to post a vote for a system variable.

    The subject parameter is one of op_list, threshold_size, base_tps_fee, tps_interval, tps_fee_multiplier, it is the name of the variable being voted for. The value parameter is the value of the variable that the voter supports. For op_list, it must be a list of 12 valid addresses of the proposed order providers (OPs) separated by \n (url-encoded, of course). All other variables have positive numeric (including fractional) values.

    The votes are used to govern the network and change the values of the said system-wide variables (described in OIP 3, OIP 5, and OIP 6). The weight of each vote depends on the balance of the voting address(es) in Bytes.

    Requesting to count votes for a system variable

    This will open Send screen, which will be pre-filled to post a vote-count request for a system variable.

    The subject parameter is one of op_list, threshold_size, base_tps_fee, tps_interval, tps_fee_multiplier, it is the name of the variable whose vote count is requested.

    The vote-count requests are used to govern the network and commit changes to the values of the said system-wide variables (described in OIP 3, OIP 5, and OIP 6). All votes for the requested variable will be tallied, weighted by the voter's current balance in Bytes, and the value that received most votes will become active. For op_list, total votes for each proposed OP will be calculated, and the top 12 will become the new active OPs.

    The user pays a 1 GB fee for this request.

    Receiving textcoins via link

    Textcoins are funds that can be sent via text messages. These text messages contain 12 randomly picked words and by entering them into the wallet app in exact same order, user will get the funds that were added to them. If you have ever received any textcoins then you also know that you don't actually have to re-type or copy-paste those words, textcoin receivers are usually directed to Obyte textcoin claiming page, which has green "Receive funds" button, which will launch the wallet app with exactly those textcoin words. Obyte textcoin claiming page tries to check first if user has wallet installed, but basically, underneath that button is a hyperlink similar to this:

    So, additionally to sending textcoins with a bot, you can also make clickable textcoins on your website (link either to textcoin claiming page or directly to obyte: protocol). Just make sure each user on your webiste has access to textcoins specifically generated for them. Otherwise, if all users see the same textcoins then it can become a rush to who claims the textcoins first.

    Interacting with chat bot

    Bot Store on Obyte is like Apple App Store or Google Play Store, but instead of apps, Bot Store has chat bots that are written for Obyte platform. Each Obyte Hub can have each own bots that are shown for users of that app. Currently, most users are using the official wallet app and official Hub, but it makes sense that if somebody forks the wallet, they would use their own Hub for it too. This means that in the future, in order to get your chat bot exposed to maximum amount of users, it needs to get listed on all Hubs.

    Pairing with another device

    Luckily, adding chat bots to your wallet app is much easier on Obyte than it is to side-load apps on iOS or Android. All you need to do is add the pairing code that bot outputs to your website - everyone who has Byteball app already installed and clicks on it, will get automatically paired with your chat bot. Hyperlink to pairing code would be something like this:

    One example, where all the official Hub bots are displayed on a website can be seen on Bots section obyte.io.

    Pairing codes in hyperlinks are not limited to only chat bots, you could create a pairing link to any device, all you need to do is find you pairing invitation code (Chat > Add new device > Invite other device) and add the code after obyte: protocol inside the hyperlink.

    Sending commands to chat bots

    Once you have paired with a chat bot already, you might wonder whether it is possible to get users from your website to some specific state in chat bot (for example, by sending a predefined command) with a click on any hyperlink. There are 2 options how to do that, first option would be to fill the pairing_secrets database table with all the possible commands, but if there could be indefinite number of valid commands then easiest would be to accept any pairing secret and use them as custom commands.

    Websites that use this feature are BB Odds and Polls section on obyte.io. How it can be done can be seen from Poll bot (source code). For example, opening a poll app with results of specific poll can be done with hyperlink like this:

    Steem Attestation bot uses this method also as an alternative way how to refer other people, source code for that is more advanced that the poll bot code, but it can be found on Github.

    Using protocol URI in a QR code

    obyte: protocol is not only for websites, it could be used in physical world too with QR codes. This means that if users have the Obyte app installed and they scan any QR code that contains any of the above codes (just value of the href, without quotes and without the HTML around them) then the installed Obyte app will open the same way. The QR code is not just meant to be printed on paper, it can work on websites too, giving the users the ability to use your services cross-device (browse the website on a laptop, scan the QR code with a phone and complete the payment on the phone).

    There are many online QR code generators to create those QR codes manually, but QR codes can be created in real-time too. Following is the example how to create a QR code with obyte: protocol link using jQuery.

    A website that creates QR codes this way is Currency converter for Obyte (source code) and Steem Attestation bot (source code) generates referral links this way. Alternatively, there is also code example how to generate QR code like that on server side.

    Using these with testnet wallets

    If you are testing these functions with TESTNET headless wallet and TESTNET GUI wallet then instead of obyte: protocol, use obyte-tn:. This can be easily controlled in your bot's code like this:

    URI
    intents on Android
    <a href="obyte:ACCOUNT_ADDRESS?amount=123000&amp;asset=base">
        open Send screen with bytes
    </a>
    <a href="obyte:ACCOUNT_ADDRESS?amount=123000&amp;from_address=USER_ACCOUNT_ADDRESS">
        open Send screen with bytes
    </a>
    <a href="obyte:ACCOUNT_ADDRESS?amount=123000&amp;single_address=1">
        open Send screen with bytes
    </a>
    var base64 = global.btoa || require('btoa');
    var data = {
        number: 1,
        foo: "bar",
        foobar: {
            foo: "bar",
        },
        "?<?>?": ["a",1,2,3],
    };
    var json_string = JSON.stringify(data);
    var base64_string = base64(json_string);
    var uri_encoded_base64data = encodeURIComponent(base64_string);
    <a href="obyte:AA_ADDRESS?amount=10000&amp;single_address=1&amp;base64data=eyJudW1iZXIiOjEsImZvbyI6ImJhciIsImZvb2JhciI6eyJmb28iOiJiYXIifSwiPzw%2FPj8iOlsiYSIsMSwyLDNdfQ%3D%3D">
        open Send screen with bytes and data
    </a>
    <a href="obyte:data?app=data&amp;anykey1=anyvalue&amp;anykey2=anyvalue">
        open Send screen with data
    </a>
    <a href="obyte:data?app=temp_data&amp;anykey1=anyvalue&amp;anykey2=anyvalue">
        open Send screen with temp data
    </a>
    <a href="obyte:data?app=data_feed&amp;anykey1=anyvalue&amp;anykey2=anyvalue">
        open Send screen with data feed
    </a>
    <a href="obyte:data?app=profile&amp;anykey1=anyvalue&amp;anykey2=anyvalue">
        open Send screen with profile
    </a>
    <a href="obyte:data?app=attestation&amp;address=ACCOUNT_ADDRESS&amp;anykey1=anyvalue&amp;anykey2=anyvalue">
        open Send screen with attestation
    </a>
    <a href="obyte:data?app=poll&amp;question=How%20are%20you&amp;option1=fine&amp;option2=tres%20bien">
        open Send screen with poll
    </a>
    <a href="obyte:data?app=definition&amp;definition=%7B%0A%09messages%3A%20%5B%0A%09%09%7B%0A%09%09%09app%3A%20'payment'%2C%0A%09%09%09payload%3A%20%7B%0A%09%09%09%09asset%3A%20'base'%2C%0A%09%09%09%09outputs%3A%20%5B%0A%09%09%09%09%09%7B%0A%09%09%09%09%09%09address%3A%20%22%7Btrigger.address%7D%22%0A%09%09%09%09%09%7D%0A%09%09%09%09%5D%0A%09%09%09%7D%0A%09%09%7D%0A%09%5D%0A%7D">
        open Send screen with AA definition
    </a>
    <a href="obyte:data?app=definition&amp;definition=https%3A%2F%2Fexample.com%2Fdefinition.json">
        open Send screen with AA definition
    </a>
    <a href="obyte:data?app=text&amp;content=Lorem%20ipsum%20dolor%20sit%20amet%2C%20consectetur%20adipiscing%20elit.%0AQuisque%20venenatis%20elementum%20dolor%20in%20malesuada.%20Cras%20ultrices%20ultrices%20fermentum.%20Nullam%20ac%20commodo%20ante.%20Pellentesque%20quis%20nibh%20laoreet%2C%20sagittis%20augue%20in%2C%20dapibus%20nunc.%20Etiam%20vel%20eleifend%20urna%2C%20in%20tincidunt%20turpis%20nullam.">
        open Send screen with text content
    </a>
    <a href="obyte:data?app=system_vote&subject=base_tps_fee&value=11.5">
        open Send screen with the system vote
    </a>
    <a href="obyte:data?app=system_vote_count&subject=base_tps_fee">
        open Send screen with the system vote count request
    </a>
    <a href="obyte:textcoin?RANDOMLY-GENERATED-TWELVE-WORDS">
        Recieve funds
    </a>
    <a href="obyte:[email protected]/bb#0000">
        Add Sports Betting bot
    </a>
    conf.js
    exports.permanent_pairing_secret = '*';
    <a href="obyte:AhMVGrYMCoeOHUaR9v/[email protected]/bb#stats-HAXKXC1EBn8GtiAiW0xtYLAJiyV4V1jTt1rc2TDJ7p4=">
        see stats
    </a>
    <div class="container">
    	<div id="qr_code"></div>
    </div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
    	integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
    	crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.qrcode/1.0/jquery.qrcode.min.js"
    	integrity="sha256-9MzwK2kJKBmsJFdccXoIDDtsbWFh8bjYK/C7UjB1Ay0="
    	crossorigin="anonymous"></script>
    <script type="text/javascript">
    $(document).ready(function() {
    	$('#qr_code').html('').qrcode({
    		width: 512,
    		height: 512,
    		text: 'obyte:ACCOUNT_ADDRESS?amount=123000&asset=base'
    	});
    });
    </script>
    const pairingProtocol = process.env.testnet ? 'obyte-tn:' : 'obyte:';

    Events list

    Event bus provides a way to subscribe to events from any of the services running.

    Wallet is ready (only headless wallet)

    Headless wallet is needed for bots that hold private keys and can send transactions. This event is emitted when passphrase has been entered by bot admin and single wallet is loaded. Before that event, bot outputs device address, device pubkey and pairing key.

    Start database update

    This event is emitted when bot executes series database queries to upgrade the database to new version.

    End of database update

    This event is emitted when bot finishes database upgrade.

    Connection to the hub via ws is established

    Connection to the hub is established and an error is possible

    The url parameter is protocol + domain + path (for example: wss://obyte.org/bb).

    Connected to ws

    Not a fatal error

    Unit validation status

    New joint

    Main chain index has become stable

    New unstable transactions

    Stable transactions

    Bad transactions

    First history received

    Changes with addresses leading to possible new transactions

    New unstable unit

    New private payment

    Stable unit

    Syncing is idle

    Catching up done

    Catching up started

    Catchup next hash tree

    New direct private chains

    Unhandled private payments left

    Version of the connected user

    New wallet version available

    This event is emitted when Hub sends message about new wallet version.

    Received the project number for notifications on the phone

    Client logged in

    Message from the hub

    Message for light client

    Exchange rates are updated

    This event is emitted when wallet updated rates that they got from Hub.

    Message received by hub

    Notifications are enabled

    Notifications are disabled

    Create new wallet

    Wallet created and signed

    Wallet was rejected by the other side

    Wallet has been confirmed by the other party

    Created new address

    Created new address

    Unit saved

    Attempt to pair us with another correspondent

    This event is emitted when there is a pairing attempt, this enables bot to decide with the code if the pairing code is valid or not. If you would like to accept any pairing code then .

    Pairing was successful

    This event is emitted on successful pairing (also when user removed and re-added the bot).

    Pairing was successful using pairing secret

    The paired device has been removed

    History update started

    History update finished

    A new message string type message has been received

    This event is emitted when text message is received by bot.

    A new message object type message has been received

    This event is emitted when object message is received by bot.

    Change in permission to store message history

    Created a new shared address

    Signing request

    Signed event

    Refuse to sign

    Confirmed on another device

    All private payments are processed

    All private payments are processed in a unit

    Event for custom Request

    You can add your own communication protocol on top of the Obyte one. See Request example .

    Event for custom JustSaying

    You can add your own communication protocol on top of the Obyte one. See JustSaying example .

    Database is read and ready to work (only Cordova)

    Error sending message via ws

    const eventBus = require('ocore/event_bus.js');
    eventBus.on('headless_wallet_ready', () => {
    
    });
    there is easier solution
    there
    there
    eventBus.on('started_db_upgrade', () => {
    
    });
    eventBus.on('finished_db_upgrade', () => {
    
    });
    eventBus.on('connected', () => {
    
    });
    eventBus.on('open-' + url, (error) => {
    
    });
    eventBus.on('connected_to_source', (new_ws) => {
    
    });
    eventBus.on('nonfatal_error', (msg, Error) => {
    
    });
    eventBus.on("validated-" + unit, (bValid) => {
    
    });
    eventBus.on('new_joint', (objJoint) => {
    
    });
    eventBus.on('mci_became_stable', (mci) => {
    
    });
    eventBus.on('new_my_transactions', (arrNewUnits) => {
    
    });
    eventBus.on('my_transactions_became_stable', (arrUnits) => {
    
    });
    eventBus.on('sequence_became_bad', (arrUniqueUnits) => {
    
    });
    eventBus.on('first_history_received', () => {
    
    });
    eventBus.on('maybe_new_transactions', () => {
    
    });
    eventBus.on("new_my_unit-" + unit, (objJoint) => {
    
    });
    eventBus.on("received_payment", (payer_device_address, assocAmountsByAsset, asset, message_counter, bToSharedAddress) => {
    
    });
    eventBus.on('my_stable-' + unit , () => {
    
    });
    eventBus.on('sync_idle' , () => {
    
    });
    eventBus.on('catching_up_done' , () => {
    
    });
    eventBus.on('catching_up_started' , () => {
    
    });
    eventBus.on('catchup_next_hash_tree' , () => {
    
    });
    eventBus.on('new_direct_private_chains' , (arrChains) => {
    
    });
    eventBus.on('unhandled_private_payments_left' , (rowsLength) => {
    
    });
    eventBus.on('peer_version', (ws, body) => {
    
    });
    eventBus.on('new_version', (ws, body) => {
    
    });
    eventBus.on('receivedPushProjectNumber', (ws, body) => {
    
    });
    eventBus.on('client_logged_in', (ws) => {
    
    });
    eventBus.on("message_from_hub", (ws, subject, body) => {
    
    });
    eventBus.on("message_for_light", (ws, subject, body) => {
    
    });
    const network = require('ocore/network.js');
    eventBus.on("rates_updated", () => {
        console.log(JSON.stringify(network.exchangeRates, null, 2));
    });
    eventBus.on('peer_sent_new_message', (ws, objDeviceMessage) => {
    
    });
    eventBus.on("enableNotification", (device_address, params) => {
    
    });
    eventBus.on("disableNotification", (device_address, params) => {
    
    });
    eventBus.on("create_new_wallet", (walletId, wallet_definition_template, arrDeviceAddresses, wallet_name, other_cosigners, is_single_address) => {
    
    });
    eventBus.on('wallet_completed', (walletId) => {
    
    });
    eventBus.on('wallet_declined', (wallet, rejector_device_address) => {
    
    });
    eventBus.on('wallet_approved', (wallet, device_address) => {
    
    });
    eventBus.on('new_wallet_address', (address) => {
    
    });
    eventBus.on("new_address-" + address, () => {
    
    });
    eventBus.on('saved_unit-' + unit, (objJoint) => {
    
    });
    eventBus.on("pairing_attempt", (from_address, pairing_secret) => {
        if (pairing_secret == "SOME_SECRET") {
            eventBus.emit("paired", from_address, pairing_secret);
        }
    });
    eventBus.on("paired", (from_address, pairing_secret) => {
        let device = require('ocore/device.js');
        // say hi to anyone who pair with bot
        device.sendMessageToDevice(from_address, 'text', 'Hi!');
    });
    eventBus.on('paired_by_secret-' + body, (from_address) => {
    
    });
    eventBus.on("removed_paired_device", (from_address) => {
    
    });
    eventBus.on('refresh_light_started', () => {
    
    });
    eventBus.on('refresh_light_done', () => {
    
    });
    eventBus.on("text", (from_address, body, message_counter) => {
        let device = require('ocore/device.js');
        // echo back the same message
        device.sendMessageToDevice(from_address, 'text', 'ECHO: '+ body);
    });
    eventBus.on("object", (from_address, body, message_counter) => {
        let device = require('ocore/device.js');
        // echo back the same object back
        device.sendMessageToDevice(from_address, 'object', body);
    });
    eventBus.on("chat_recording_pref", (from_address, body, message_counter) => {
    
    });
    eventBus.on("create_new_shared_address", (address_definition_template, assocMemberDeviceAddressesBySigningPaths) => {
    
    });
    eventBus.on("signing_request", (objAddress, address, objUnit, assocPrivatePayloads, from_address, signing_path) => {
    
    });
    eventBus.on("signature-" + device_address + "-" + address + "-" + signing_path + "-" + buf_to_sign.toString("base64"), (sig) => {
    
    });
    eventBus.on('refused_to_sign', (device_address) => {
    
    });
    eventBus.on('confirm_on_other_devices', () => {
    
    });
    eventBus.on('all_private_payments_handled', (from_address) => {
    
    });
    eventBus.on('all_private_payments_handled-' + first_chain_unit, () => {
    
    });
    const network = require('ocore/network.js');
    
    eventBus.on('custom_request', function(ws, params, tag) {
        var response = 'put response here';
        return network.sendResponse(ws, tag, response);
    }
    eventBus.on('custom_justsaying', function(ws, content) {
    
    };
    eventEmitter.once('ready', () => {
       console.log("db is now ready");
    });
    ws.on('error', (error) => {
    
    });

    Logging into website

    Hey guys! Today we will learn how to log in using a Obyte wallet, and also make paid access to the article. To do this, we need project that i have prepared for you Tutorial-2-source. Install it.

    git clone https://github.com/xJeneKx/Tutorial-2-source
    cd Tutorial-2-source
    npm install

    If you run project you will see a blog with articles. After opening any article you will see a request to log in. Let’s create an authorization. First we need a pairing code, you could see it on startup

    Copy it and change our code a bit

    pairingCode = 'Aly99HdWoCFaxJiIHs1ldREAN/[email protected]/bb#' + code;

    Run and see the link. Сlick on it and bot will be added. Now let’s look at the console.

    Add the link

    b = '<br>Please login using this pairing code: <a href="obyte:' + pairingCode + '">' + pairingCode + '</a>';

    And QR

    Save our device address, we will need it later.

    Now we need to tell the browser that we are logged in To implement this, we add function in start.js

    And also one more routing

    In article.html we will add a request to check

    What’s going on here? The user scans the QR code, adds our bot. Bot associates code with his device_address. In the browser, we knock and ask: did we logged in? and adding cookies.

    Now we need to create an address and associate it with the user and the article. Add a variable

    Add the address and display it

    And finally the function getAssocAddress

    This time we will not wait for the stabilization of the unit and immediately open access. Add a variable

    Add a check that the user has paid for

    Add a routing

    And payment processing

    That’s all, make a working second article - will be your homework:) You should get this:

    All the code you can find on . Did you like the lesson? Any questions? What topics have caused you difficulties and it is worth talking about them? Write in the comments and I will help you. Also join my telegram chat - .

    eventBus.on('paired', (from_address, pairing_secret) => {
      console.error('paired', from_address, pairing_secret);
    });
    let dataURL = await QRCode.toDataURL("obyte:" + pairingCode);
    b = '<br>Please login using this pairing code: <a href="obyte:' + pairingCode + '">' + pairingCode + '</a><br><img src="' + dataURL + '">';
    eventBus.on('paired', (from_address, pairing_secret) => {
      let device = require('ocore/device');
      assocCodeToDeviceAddress[pairing_secret] = from_address;
      device.sendMessageToDevice(from_address, 'text', 'ok');
    });
    async function amILogged(ctx) {
      let url = ctx.request.url;
      let match = url.match(/code=([0-9]+)/);
      if (match && assocCodeToDeviceAddress[match[1]]) {
         ctx.cookies.set('ac', match[1]);
         ctx.body = 'true';
      } else {
         ctx.body = 'false';
      }
    }
    https://www.youtube.com/watch?v=vvB6A1wBecQ
    github
    @obytedev
    app.use(mount('/amilogged', amILogged));
    var step = '<%= step %>';
    var code = '<%= code %>';
    if (step === 'login') {
      setInterval(() => {
         fetch('/amilogged?code=' + code)
            .then((response) => {
               return response.text();
            }).then(result => {
            if (result === 'true') {
               location.reload(true);
            }
         })
            .catch(alert);
      }, 5000);
    }
    let assocCodeAndNumberToAddress = {};
    step = 'paid';
    let address = await getAssocAddress(assocCode, 1);
    let dataURL = await QRCode.toDataURL("obyte:" + address + '?amount=100');
    b = '<br>Please pay for the article. <br>Address: ' + address + '<br>Amount: 100<br><img src="' + dataURL + '"><br>' +
      '<a href="obyte:' + address + '?amount=100">Pay</a>';
    function getAssocAddress(assocCode, number) {
      return new Promise(resolve => {
         let name = assocCode + '_' + number;
         if (assocCodeAndNumberToAddress[name]) {
            return resolve(assocCodeAndNumberToAddress[name]);
         } else {
            headlessWallet.issueNextMainAddress(address => {
               assocCodeAndNumberToAddress[name] = address;
               return resolve(address);
            });
         }
      });
    }
    let assocCodeToPaid = {};
    async function amIPaid(ctx) {
      let code = ctx.cookies.get('ac');
      ctx.body = (code && itPaid(code)) ? 'true' : 'false';
    }
    
    function itPaid(code) {
      return !!assocCodeToPaid[code];
    }
    app.use(mount('/amipaid', amIPaid));
    eventBus.on('new_my_transactions', (arrUnits) => {
      const device = require('ocore/device.js');
      db.query("SELECT address, amount, asset FROM outputs WHERE unit IN (?)", [arrUnits], rows => {
         rows.forEach(row => {
            if (row.amount === 100 && row.asset === null) {
               for (let key in assocCodeAndNumberToAddress) {
                  if (assocCodeAndNumberToAddress[key] === row.address) {
                     let assocCode = key.split('_')[0];
                     assocCodeToPaid[assocCode] = true;
                     device.sendMessageToDevice(assocCodeToDeviceAddress[assocCode], 'text', 'I received your payment');
                     return;
                  }
               }
            }
         })
      });
    });

    Weather oracle

    Hi, everybody. Today we will create our own oracle. To do this, we need a bot-example. Let’s install it.

    git clone https://github.com/byteball/bot-example
    cd bot-example
    npm install
    cp .env.testnet .env

    Our oracle will accept a message like City: timestamp, where timestamp is the time to publish. Let’s write a message handler

    headlessWallet.readSingleAddress(address => {
      my_address = address;
      console.error('my_address', address)
    });
    
    eventBus.on('text', (from_address, text) => {
      text = text.trim();
      const device = require('ocore/device.js');
      const args = text.toLowerCase().split(':');
      if (args.length === 2){
         switch (args[0]) {
            case 'berlin':
            case 'moscow':
            case 'helsinki':
            case 'washington':
               if(parseInt(args[1]) <= Date.now()){
                  console.error('dateNow', Date.now());
                  device.sendMessageToDevice(from_address, 'text', "Incorrect time");
               }else {
                  arrQueue.push({city: args[0], time: args[1], device_address: from_address});
                  device.sendMessageToDevice(from_address, 'text', "ok");
               }
               break;
            default:
               device.sendMessageToDevice(from_address, 'text', "City not support");
               break;
         }
      }else {
         device.sendMessageToDevice(from_address, 'text', "Incorrect command");
      }
    });

    Add a variable to store the queue

    Now add to the queue

    Now we need to check the queue once a minute

    Now we come to the main point. We need to publish data_feed

    What is happening?

    1) We create an object in data_feed and keep the temperature in it.

    2) We send the payment to ourselves as we only need to publish data_feed

    3) Create a message with data and signature

    4) Publish

    That’s all. Your first oracle is ready. For the experiment, you can add more cities or make paid access. All code is available on .Did you like the lesson? Any questions? What topics have caused you difficulties and it is worth talking about them? Write in the comments and I will help you. Also join my telegram chat - .

    let arrQueue = [];
    if(parseInt(args[1]) <= Date.now()){
      device.sendMessageToDevice(from_address, 'text', "Incorrect time");
    }else {
      arrQueue.push({city: args[0], time: args[1], device_address: from_address});
      device.sendMessageToDevice(from_address, 'text', "ok");
    }
    function checkQueue(){
      arrQueue.forEach((el, index) => {
         if(el.time <= Date.now()){
            let result = JSON.parse(body);
            postDataFeed(el.city, el.time, result.current.temp_c);
         }
      });
    }
    setTimeout(checkQueue, 60000);
    function postDataFeed(city, time, temp){
      const network = require('ocore/network.js');
      const composer = require('ocore/composer.js');
    
      let data_feed = {};
      data_feed[city + '_' + time] = temp;
      let params = {
         paying_addresses: [my_address],
         outputs: [{address: my_address, amount: 0}],
         signer: headlessWallet.signer,
         callbacks: composer.getSavingCallbacks({
            ifNotEnoughFunds: console.error,
            ifError: console.error,
            ifOk: function(objJoint){
               network.broadcastJoint(objJoint);
            }
         })
      };
      let objMessage = {
         app: "data_feed",
         payload_location: "inline",
         payload: data_feed
      };
      params.messages = [objMessage];
      composer.composeJoint(params);
    }
    let data_feed = {};
    data_feed[city + '_' + time] = temp;
    paying_addresses: [my_address],
    outputs: [{address: my_address, amount: 0}]
    let objMessage = {
         app: "data_feed",
         payload_location: "inline",
         payload: data_feed
      };
    composer.composeJoint(params);
    github
    @obytedev

    Attestation profiles / KYC

    This guide explains how to request, validate, and use the attested data in your chat bot.

    Users can have some of their data verified by a trusted third party (attestor). The attestor posts an attestation record to the Obyte DAG, this record serves as a proof for relying parties that a user was attested. The attested data itself can be either posted publicly by the attestor, or only a hash of this data is posted while the plain-text data is saved in the wallet of the attested user. In the latter case, the user can disclose the attested data to selected peers, for example to your bot.

    This can be used to KYC your users prior to providing a service, see https://medium.com/byteball/bringing-identity-to-crypto-b35964feee8e. You may need it e.g. to comply with regulations or protect against fraud.

    ICO bot https://github.com/byteball/ico-bot already uses the attested private profiles in order to allow only verified users to invest and to collect their personal data. You can use its code as reference.

    Requesting private profile

    You should point the user to your privacy policy before requesting sensitive personal data.

    To request a private profile, you need to send profile-request message to the user. The message includes the list of fields you require the user to disclose. The message will be displayed in the user's chat window as a clickable link, which allows them to select one of the profiles stored in his wallet and send it over to the peer (your chat bot).

    Real Name Attestation

    This is a profile request message that asks the fields that all profiles have:

    Here is the full list of available fields:

    • first_name: first name

    • last_name: last name

    • dob: date of birth in YYYY-MM-DD format_

    Email Attestation

    This is a profile request message that asks the email field that all profiles have:

    Steem Attestation

    This is a profile request message that asks the fields that all profiles have:

    Receiving private profile

    When a user sends you his private profile (on profile request or by choosing "Insert private profile" from the menu), you receive it in a chat message that includes:

    where privateProfileJsonBase64 is a base64-encoded JSON of the private profile object. You can easily find the profile in an incoming message using regular expression:

    Then, decode the profile using private_profile.js module in ocore:

    objPrivateProfile object has 3 fields:

    Next, you need to validate this object and extract information about the attested and attestor addresses:

    This function verifies that the provided profile matches the hash stored on the DAG by the attestor. It also returns the user's address address (hence you don't need to ask the user about his address, the profile is enough) and the attestor address attestor (make sure it is on the list of attestors you trust for each type of attestations).

    The src_profile field of objPrivateProfile contains an associative array of attested fields. But not all fields have to be disclosed by the user.

    • If a field is disclosed, the value of objPrivateProfile[field] is an array of two elements: the plain-text value of the field and blinding. Blinding is a random string generated by the attestor when creating the profile, it serves to protect the private data from brute force attacks (the profile data is too predictable and can be easily checked against known hashes).

    • If the field is not disclosed, the value of objPrivateProfile[field] is a hash of plain-text value and blinding.

    To extract just the disclosed data and remove all the bindings, use

    You should double check if the user sent all the required fields because users can send private profiles on profile request (required fields in locked state) or by choosing "Insert private profile" from the menu (none of the fields in locked state).

    To save the received private profile of previously mentioned Real Name Attestation profile request, use code like this:

    It will save the profile in the tables private_profiles and private_profile_fields, which you can later query to read the data.

    Sending private profile

    If your bot is setting up between 2 users then they need to get each others private profiles. All you need to do is first request the profiles from both users, then receive and save the profiles from both users and then send the first user and second user's profile and the second user a first user's profile.

    Retrieving public attestation

    profiles are all private, but and bots let user to choose whether they like to publish the data publicly or privately and bot publishes attestations only publicly. So, if you are asking attested email address from the user, you should let them do it with public attestation too.

    For full nodes that is easy, you ask user to , after which you get a wallet address that they own. Then you just query your full node database for that data:

    On the light node, it is also possible, but more complicated. You need to ask the Hub for all the attestation for user address and then you would need to request proofs for those attestation units:

    Creating attestation profiles

    Above examples here show how to use different existing profiles from other attestation bots, but if you would like to create your own attestation bot, it is recommended to click on the links of these bots on this article and take a look how they have been done from the source code. There is also article page.

    country: the country that issued the ID (2-letter ISO code)

  • us_state: US state (only if country=US)

  • personal_code: government issued personal code (not all countries)

  • id_number: government issued document ID (not all documents)

  • id_type: ID type

  • id_subtype: ID sub-type (not all attestations)

  • id_expiry: ID expires at (not all attestations)

  • id_issued_at: ID issued at (not all attestations)

  • Real Name Attestation
    Email Attestation
    Steem Attestation
    prosaic contracts
    Real Name Attestation
    Email Attestation
    Steem Attestation
    Username Attestation
    sign some message
    "How to create private/public attestation profile" on Sending data to DAG
    [...](profile-request:first_name,last_name,dob,country,id_type)
    [...](profile-request:email)
    [...](profile-request:steem_username,reputation)
    [...](profile:privateProfileJsonBase64)
    let arrProfileMatches = text.match(/\(profile:(.+?)\)/);
    const privateProfile = require('ocore/private_profile.js');
    let privateProfileJsonBase64 = arrProfileMatches[1];
    let objPrivateProfile = privateProfile.getPrivateProfileFromJsonBase64(privateProfileJsonBase64);
    objPrivateProfile {
        unit: "...", // attestation unit
        payload_hash: "...", // pointer to attestation payload in this unit
        src_profile: object // attested fields
    }
    privateProfile.parseAndValidatePrivateProfile(objPrivateProfile, (err, address, attestor) => {
        if (err) {
            device.sendMessageToDevice(from_address, 'text', err);
        }
        // validated
        // ...
    });
    let assocPrivateData = privateProfile.parseSrcProfile(objPrivateProfile.src_profile);
    let required_fields = ['first_name', 'last_name', 'dob', 'country', 'id_type'];
    if (required_fields.every(key => key in assocPrivateData)) {
        privateProfile.savePrivateProfile(objPrivateProfile, address, attestor);
    }
    else {
        device.sendMessageToDevice(from_address, 'text',
                'Please share all the required fields.');
    }
    db.query("SELECT unit, payload_hash, src_profile \
    		FROM private_profiles \
    		WHERE address = ? AND attestor_address = ? \
    		LIMIT 1;", [wallet_address_1, attestor], (rows) => {
    	if (rows.length === 0) {
    		return device.sendMessageToDevice(device_address_1, 'text',
    				'Please provide your private email \
    				[...](profile-request:first_name,last_name)');
    	}
    	let objPrivateProfile = {
    		unit: rows[0].unit,
    		payload_hash: rows[0].payload_hash,
    		src_profile: JSON.parse(rows[0].src_profile)
    	};
    	let privateProfileJsonBase64 = Buffer.from(JSON.stringify(objPrivateProfile)).toString('base64');
    	device.sendMessageToDevice(device_address_2, 'text',
    			'Save the private profile of the other side of the contract. \
    			[...](profile:'+ privateProfileJsonBase64 +')');
    });
    const device = require('ocore/device.js');
    let challenge = 'This is my address.';
    device.sendMessageToDevice(from_address, 'text', 'Please provide your private email [...](profile-request:email) \
            or insert the wallet address, which has publicly attested email address [...](sign-message-request:'+ challenge +'). \
            \nIf you haven\'t verified your email address yet then please do it with Email Attestation bot from Bot Store.');
    const email_attestor = 'H5EZTQE7ABFH27AUDTQFMZIALANK6RBG';
    /*
    convert base64 signed message into JSON object
    like it is described in Wallet address verification documentation.
    */
    
    validation.validateSignedMessage(objSignedMessage, err => {
        if (err)
            return device.sendMessageToDevice(from_address, 'text', err);
        if (objSignedMessage.signed_message !== challenge)
            return device.sendMessageToDevice(from_address, 'text',
                    "You signed a wrong message: "+objSignedMessage.signed_message+", expected: "+challenge);
    
        db.query("SELECT unit, field, value FROM attested_fields \
                WHERE attestor_address = ? AND address = ?;",
                [email_attestor, objSignedMessage.authors[0].address], (rows) => {
            if (rows.length === 0) return;
            rows.forEach((row) => {
                if (row.field == 'email') {
                    device.sendMessageToDevice(from_address, 'text',
                            'Found: '+ row.value);
                }
            });
        });
    });
    const email_attestor = 'H5EZTQE7ABFH27AUDTQFMZIALANK6RBG';
    /*
    convert base64 signed message into JSON object
    like it is described in Wallet address verification documentation.
    */
    
    validation.validateSignedMessage(objSignedMessage, err => {
        if (err)
            return device.sendMessageToDevice(from_address, 'text', err);
        if (objSignedMessage.signed_message !== challenge)
            return device.sendMessageToDevice(from_address, 'text',
                    "You signed a wrong message: "+objSignedMessage.signed_message+", expected: "+challenge);
    
        const network = require('ocore/network.js');
        network.requestFromLightVendor('light/get_attestations', {
            address: objSignedMessage.authors[0].address
        },
        (ws, request, response) => {
            if (response.error){
                console.error('light/get_attestations failed: '+response.error);
                return;
            }
            let arrAttestations = response;
            if (!Array.isArray(arrAttestations)){
                console.error('light/get_attestations response is not an array: '+response);
                return;
            }
            if (arrAttestations.length === 0){
                return device.sendMessageToDevice(from_address, 'text',
                        "Not an attested address.");
            }
            console.log('attestations', arrAttestations);
            let arrUnits = arrAttestations.map(attestation => attestation.unit);
            network.requestProofsOfJointsIfNewOrUnstable(arrUnits, err => {
                if (err){
                    console.log('requestProofsOfJointsIfNewOrUnstable failed: '+err);
                    return;
                }
                arrAttestations.forEach( (attestation) => {
                    if (attestation.attestor_address === email_attestor) {
                        device.sendMessageToDevice(from_address, 'text',
                                'Found: '+ attestation.profile.email);
                    }
                });
            });
        });
    });

    Getting started guide

    Autonomous agents allow to quickly create decentralized finance applications on a distributed ledger.

    They operate based on code that is deployed on the ledger once and can never be changed. The code is open and is executed by all nodes on the network.

    Anybody can activate an AA just by sending a transaction to its address. Here is an example of a simple AA:

    messages is a template of the response transaction that will be generated by the AA. It follows the structure of a regular Obyte transaction but its sections between curly braces {} contain code that makes the resulting transaction dependent on the triggering (activating) transaction. This is similar to how PHP code can be inserted into HTML to make the resulting page dependent on the request. The language autonomous agents are written in is called Oscript.

    The above example of an AA just sends the received money less 1000 bytes back to the sender. trigger.address is the address of the sender who activated the AA. trigger.output allows to find the amounts in different currencies sent to the AA, trigger.output[[asset=base]] is the amount in the base asset -- bytes.

    Another example that sells tokens for bytes at 1.5 tokens per byte:

    This code needs to be deployed on the ledger before it can be used. Visit , copy/paste the above code, and click "Deploy". You'll then get the address of your new autonomous agent. Anybody can now send money to this address to trigger execution of this AA's code.

    Passing data to AA

    AA's response can also depend on data it receives in the triggering transaction.

    In Obyte, any transaction can contain one or more messages of different types. These types are called apps. The most common app is payment. At least one payment is mandatory in every transaction, it is necessary to at least pay fees. Another app is data. This type of message delivers an arbitrary json object that can contain any data:

    The object in payload is the data this message delivers.

    When an AA receives a data message (along with a payment message, of course) in the triggering transaction, it can access the data through trigger.data and its action can depend on the data received:

    The above AA tries to send trigger.data.withdrawal_amount bytes back to the sender. withdrawal_amount is a field in the data message of the triggering transaction:

    AAs can be triggered with data by manually entering the key/value pairs to GUI wallet, , making the or with the .

    Handling errors

    In the above example, if the withdrawal_amount is greater than the balance that the AA has (including the amount received from the triggering transaction), the AA's response will fail. When an AA fails to handle a trigger transaction for any reason, it rolls back all changes and attempts to send back to sender all the received funds, in all assets, less the bounce fees. By default, the bounce fees are 10000 bytes for "base" asset and 0 for all other assets. This is also the minimum. The default can be overridden by adding a bounce_fees field in the autonomous agent definition:

    The above AA will charge 20000 bytes and 100 units of the asset "n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=" if it has to bounce back a triggering transaction. Sending less than 20000 bytes to the AA will result in the AA silently eating the coins (they are not enough even for the bounce fees). When non-zero amount of asset "n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=" is sent to the AA, it should also be at least 100, otherwise the AA eats the coins.

    Sending data messages

    As said above, payment is the most common message but other messages can be sent as well by any address, AA included.

    Sending data feeds

    AA can act as an oracle by sending a data feed:

    This example shows that object keys can also be parameterized through Oscript, like object values.

    The above AA doesn't have a payment message, it will be added automatically just to pay for the fees.

    Sending data

    An AA can also send any structured data:

    Unlike data_feed messages, data messages:

    • can have any structure with any nesting depth (data feeds are flat key-value pairs);

    • are not indexed for search;

    • can be used to pass data to other AAs.

    The above AA forwards the received money (less 1000 bytes) to another address, which might be an AA. It also passes data, which includes the initially received data parameters. The value of trigger.data will be expanded into an object and added as value of initial_data field.

    If the secondary recipient (JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY) happens to be an AA, its execution will start only after the execution of the current AA is finished. There is no reentrancy issue.

    Sending texts

    This AA will create a simple text message and store it on the DAG. || is the operator for string concatenation.

    Defining new assets

    The above AA creates a definition of a new asset based on parameters passed in data.

    Some syntax elements that we see here for the first time:

    • ! is a logical NOT operator;

    • otherwise is an operator that returns the first operand if it is truthy (anything except false, 0, and empty string) or the second operand otherwise;

    • when an object or array value evaluates to an empty string, the corresponding object/array element is removed. This means that if cap

    Querying state

    The AA's behavior can also depend on various state variables that describe the ledger as a whole.

    Balance

    Balance of other AAs (but not regular addresses) can also be queried:

    The above AA forwards any received payment (less 1000 bytes) to another AA JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY if its balance is less than 1,000,000 bytes, or returns to sender otherwise.

    Data feeds

    The above AA pays to 7XZSBB32I5XPIAVWUYCABKO67TZLHKZW if Charlotte Hornets won and to FDZVVIOJZZVFAP6FLW6GMPDYYHI6W4JG otherwise TKT4UESIKTTRALRRLWS4SENSTJX6ODCW is the address of a sports oracle that posts the results of sports events.

    Before the data feed is posted, any attempt to evaluate it will fail the AA and bounce the triggering transaction.

    Note that the amount field in the output is omitted, which means that the entire AA balance will be paid out.

    Attestations

    The above AA pays to the triggering user 50000 bytes if his address is attested by Steem attestation bot JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725 and his Steem reputation is at least 50. Otherwise, it "pays" 0 bytes. Trying to pay 0 amount results in the output being removed. Since it is the only output in this case and it is removed, the resulting response transaction would produce no effect, therefore it is not created at all.

    Variables

    State variables

    An autonomous agent can have its own state saved between invocations. Use var to access and assign state variables:

    The above AA saves the address that sent the previous triggering transaction, waits for the next invocation, and sends the received money less 1000 bytes to the previous user.

    Assigning state variables is only allowed in the special message with app set to state. This message must have a single other field called state which is a set of Oscript statements. These statements are allowed to assign state variables. The state message is not included in the final response transaction, it can only affect state by modifying the state variables.

    When an uninitialized var is accessed, it evaluates to false.

    The above example has an if field in the first message. When var['previous_address'] is not initialized yet (first call), the if oscript evaluates to false and the entire first message is excluded from the transaction.

    State variables of other AAs can also be read, but not assigned:

    Local constants

    Use local constants to save intermediate results during script execution.

    Local constant names are prefixed with $. Unlike state variables, they are not saved between invocations.

    To make it easier to argue about a local constant's value, there is one restriction that is not common in other languages: single-assignment rule. A local constant can be assigned a value only once and that value remains constant until the end of the script. An attempt to re-assign a value will fail the script and make the AA bounce.

    Conditional sections

    As we've seen in the State variables example before, some parts of the response transaction generated by the AA can be excluded by adding an if field. If the Oscript in the if evaluates to true or any truthy value (anything except false, 0, and empty string), the enclosing object is included in the final response, otherwise, it is excluded.

    The if field itself is of course always removed from the final response.

    Initialization code

    Along with if, any object can have an init field that includes initialization code:

    The init code contains only statements, it does not return any value unlike most other Oscripts. The other exception is state message described in State variables above, it is also statements-only. All other Oscripts can contain 0 or more statements but must end with an expression (without a ;) whose value becomes the value of the Oscript.

    Init code is evaluated immediately after if (if it is present and evaluated to true of course) and before everything else. It is often used to set local constants that are used later.

    Local constants in if and init

    Local constants that are set in if and init are also available in all other Oscripts of the current and nested objects. In the above example, $amount1 and $amount2 were set in init of the message object, and they are used in payload/outputs/output-0/amount of this message.

    Local constants set in if are also available in the corresponding init.

    Statements-only vs return-value Oscripts

    As said above, there are two types of Oscripts: those that contain only statements and those that return a value.

    There are only two statements-only Oscripts: init and state script. Here is an example of an init script that we've seen before:

    All other Oscripts return a value. They either contain a single expression:

    or a set of statements and an expression at the end:

    The value of the last expression is the value of the Oscript.

    Cases

    When you need to route the code execution along several mutually exclusive paths depending on input and environment parameters, use cases:

    The regular messages array is replaced with an object with a single element called cases. cases is an array that lists several mutually exclusive alternatives. Every alternative except the last must have an if field. The first alternative whose if evaluates to true defines the messages that will take the place of the higher-level messages field.

    The conditions in the ifs may not be mutually exclusive but the alternative that is listed earlier in the list takes precedence. Only one alternative is always taken.

    In the above example, if trigger.data has no define field but has a truthy issue field, the 2nd alternative is selected, and the original object folds into:

    Cases can be nested. Everything said above about if, init, and local constants, applies to cases as well.

    if else

    The above examples showed how execution can be branched along different parts of json template.

    Oscript code itself can be branched as well by using if/else:

    return

    If you want to interrupt a script's execution and return from it, with or without a value, use return.

    Here is an example of return in a return-value Oscript, it must return a value:

    A return in a statements-only Oscript, such as init, doesn't return any value, it just interrupts the script:

    bounce/require

    If you find an error condition and want to prematurely stop the AA execution and bounce, use bounce:

    or require:

    In this example, timestamp is roughly the current timestamp (number of seconds since Jan 1, 1970 00:00:00 UTC).

    All applied changes will be rolled back and any received coins will be bounced back to the sender less the bounce fees.

    _________________

    This was a quick introduction to programming of autonomous agents. Read the full to learn more, and enjoy your journey through decentralized finance and beyond!

    ["autonomous agent", {
        messages: [
            {
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{trigger.address}",
                            amount: "{trigger.output[[asset=base]] - 1000}"
                        }
                    ]
                }
            }
        ]
    }]
    parameter is not passed then
    cap
    in
    payload
    evaluates to an empty string, therefore
    cap
    is removed from asset definition and an asset with infinite supply will be defined;
  • fields whose value is an empty array/object are also removed. Therefore, if none of the attestor1, attestor2, attestor3 fields was set, the attestors array becomes empty and will be excluded from the final definition.

  • oscript.org
    prompting users to submit pre-filled Send screen
    AA to trigger another AA
    script from headless wallet
    language reference
    ["autonomous agent", {
        messages: [
            {
                app: "payment",
                payload: {
                    // this is the token being sold
                    asset: "n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=",
                    outputs: [
                        {
                            address: "{trigger.address}",
                            amount: "{ round(trigger.output[[asset=base]] * 1.5) }"
                        }
                    ]
                }
            }
        ]
    }]
    {
        app: "data",
        payload: {
            withdrawal_amount: 25000,
            nested_array: [
                99,
                {another_field: 44}
            ],
            nested_object: {yet_another_field: "value2"}
        },
         ...
    }
    ["autonomous agent", {
        messages: [
            {
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{trigger.address}",
                            amount: "{trigger.data.withdrawal_amount}"
                        }
                    ]
                }
            }
        ]
    }]
    {
        withdrawal_amount: 3000
    }
    ["autonomous agent", {
        bounce_fees: {
            base: 20000,
            "n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=": 100
        },
        messages: [
            {
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{trigger.address}",
                            amount: "{trigger.data.withdrawal_amount}"
                        }
                    ]
                }
            }
        ]
    }]
    ["autonomous agent", {
        messages: [
            {
                app: "data_feed",
                payload: {
                    "{trigger.data.feed_name}": "{trigger.data.feed_value}"
                }
            }
        ]
    }]
    ["autonomous agent", {
        messages: [
            {
                app: "data",
                payload: {
                    forwarded: 1,
                    initial_data: "{trigger.data}"
                }
            },
            {
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY",
                            amount: "{trigger.output[[asset=base]] - 1000}"
                        }
                    ]
                }
            }
        ]
    }]
    ["autonomous agent", {
        messages: [
            {
                app: "text",
                payload: "{'Hello, ' || trigger.data.name || '!'}"
            }
        ]
    }]
    ["autonomous agent", {
        messages: [
            {
                app: "asset",
                payload: {
                    cap: "{trigger.data.cap otherwise ''}",
                    is_private: false,
                    is_transferrable: true,
                    auto_destroy: "{!!trigger.data.auto_destroy}",
                    fixed_denominations: false,
                    issued_by_definer_only: "{!!trigger.data.issued_by_definer_only}",
                    cosigned_by_definer: false,
                    spender_attested: false,
                    attestors: [
                        "{trigger.data.attestor1 otherwise ''}",
                        "{trigger.data.attestor2 otherwise ''}",
                        "{trigger.data.attestor3 otherwise ''}",
                    ]
                }
            }
        ]
    }]
    ["autonomous agent", {
    	messages: [
    		{
    			app: "payment",
    			payload: {
    				asset: "base",
    				outputs: [
    					{
    						address: "{trigger.address}",
    						amount: "{ round(balance[base]/2) }"
    					}
    				]
    			}
    		}
    	]
    }]
    ["autonomous agent", {
        messages: [
            {
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{ (balance['JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY'][base] < 1e6) ? 'JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY' : trigger.address }",
                            amount: "{ trigger.output[[asset=base]] - 1000 }"
                        }
                    ]
                }
            }
        ]
    }]
    ["autonomous agent", {
        messages: [
            {
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{(data_feed[[oracles='TKT4UESIKTTRALRRLWS4SENSTJX6ODCW', feed_name='BROOKLYNNETS_CHARLOTTEHORNETS_2019-07-21']] == 'CHARLOTTEHORNETS') ? '7XZSBB32I5XPIAVWUYCABKO67TZLHKZW' : 'FDZVVIOJZZVFAP6FLW6GMPDYYHI6W4JG'}"
                        }
                    ]
                }
            }
        ]
    }]
    ["autonomous agent", {
        messages: [
            {
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{trigger.address}",
                            amount: "{ (attestation[[attestors='JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725', address=trigger.address]].reputation >= 50) ? 50000 : 0 }"
                        }
                    ]
                }
            }
        ]
    }]
    ["autonomous agent", {
        messages: [
            {
                if: "{ var['previous_address'] }",
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{ var['previous_address'] }",
                            amount: "{ trigger.output[[asset=base]] - 1000 }"
                        }
                    ]
                }
            },
            {
                app: "state",
                state: `{
                    var['previous_address'] = trigger.address;
                }`
            }
        ]
    }]
    var['JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY']['var_name']
    ["autonomous agent", {
        messages: [
            {
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{ trigger.address }",
                            amount: `{
                                $amount1 = trigger.output[[asset=base]] - 5000;
                                $amount2 = round(balance[base] * 0.9);
                                max($amount1, $amount2)
                            }`
                        }
                    ]
                }
            }
        ]
    }]
    ["autonomous agent", {
        messages: [
            {
                if: "{ trigger.data.pay }",
                init: `{
                    $amount1 = trigger.output[[asset=base]] - 5000;
                    $amount2 = round(balance[base] * 0.9);
                }`,
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{ trigger.address }",
                            amount: "{max($amount1, $amount2)}"
                        }
                    ]
                }
            }
        ]
    }]
    {
        init: `{
            $amount1 = trigger.output[[asset=base]] - 5000;
            $amount2 = round(balance[base] * 0.9);
        }`,
         ...
    }
    {
        address: "{ trigger.address }",
         ...
    }
    {
        amount: `{
            $amount1 = trigger.output[[asset=base]] - 5000;
            $amount2 = round(balance[base] * 0.9);
            max($amount1, $amount2)
        }`,
         ...
    }
    ["autonomous agent", {
        messages: {
            cases: [
                {
                    if: "{trigger.data.define}",
                    messages: [
                        // first version of messages
                        ....
                    ]
                },
                {
                    if: "{trigger.data.issue}",
                    init: "{$amount = trigger.output[[asset=base]];}",
                    messages: [
                        // second version of messages
                        ....
                    ]
                },
                {
                    messages: [
                        // default version of messages
                        ....
                    ]
                }
            ]
        }
    }]
    ["autonomous agent", {
        messages: [
            // second version of messages
            ....
        ]
    }]
    if (var['started']){
        $amount = balance[base];
        $current_winner = var['current_winner'];
    }
    else{
        $amount = trigger.output[[asset=base]];
        $current_winner = trigger.address;
    }
    $amount = trigger.output[[asset=base]];
    if ($amount > 1e6)
        return $amount;
    // here comes some other code that will not be executed if $amount > 1e6
    $amount = trigger.output[[asset=base]];
    if ($amount > 1e6)
        return;
    // here comes some other code that will not be executed if $amount > 1e6
    $maturity = var['maturity_timestamp'];
    if (timestamp < $maturity) {
        bounce("too early");
    }
    $maturity = var['maturity_timestamp'];
    require(timestamp >= $maturity, "too early");

    Request

    This is a message type, which gets a reply with a response type message.

    Example:

    Following is a list of request type JSON messages that are sent over the network:

    Get node list

    This requests returns response with nodes that accept incoming connections.

    Get a list of witnesses

    This requests returns response with list of witnesses.

    Get transaction data

    This requests returns response with unit data. Example shows a unit, which created a new asset.

    Send transaction data

    This requests returns response whether composed unit was accepted or not. Unit object can be composed with ocore/composer.js.

    Send heartbeat that you don't sleep

    Subscribe to transaction data

    Synchronous transaction data

    Get the hash tree

    Get the main chain index

    Send message to client that is connected to hub

    Get temporary public key

    Update temporary public key

    Enable notifications

    Disable notifications

    Get list of chat bots

    This requests returns response with chat bots of connected hub.

    Get asset metadata

    This requests returns response with asset metadata (unit and registry). Example gets WCG Point asset metadata.

    Get transaction history

    This requests returns response with transaction history of specified addresses.

    Get chain link proofs

    Get the parent unit and the witness unit

    Get specific attestation unit

    This requests returns response with attestation units for specified field and value.

    Get all attestation data about address

    This requests returns response with all the attestation data about specific address.

    Pick divisible inputs for amount

    This requests returns response with spendable inputs for specified asset, amount and addresses.

    Get address definition

    This requests returns response with address definition (smart-contracts have address smart-contract definition only after somebody has spent from it).

    Get balances of addresses

    This request returns a response with balances of one or more addresses.

    Get profile unit IDs of addresses

    It is possible for users to post a profile information about themselves, this request returns a response with all the profile data units.

    Get the current state of votes for system vars

    Get information about the current state of user votes for system variables such as op_list, threshold_size, base_tps_fee, tps_interval, and tps_fee_multiplier. Since later votes by the same user concerning the same variable overwrite their previous votes, only the latest votes are shown. The response also includes the balances of the voting addresses. They determine the weight of the votes.

    Custom Request

    You can add your own communication protocol on top of the Obyte one. See event .

    const network = require('ocore/network.js');
    const ws = network.getInboundDeviceWebSocket(device_address);
    
    // function parameters: websocket, command, params, bReroutable, callback
    var tag = network.sendRequest(ws, 'get_witnesses', null, false, getParentsAndLastBallAndWitnessListUnit);
    
    function getParentsAndLastBallAndWitnessListUnit(ws, req, witnesses) {
      var params = {
        witnesses: witnesses
      };
      network.sendRequest(ws, 'light/get_parents_and_last_ball_and_witness_list_unit', params, false,
        function(ws, req, response) {
          console.log(response);
        }
      );
    }
    
    //you can have your own timeout logic and delete a pending request like this
    setTimeout(function() {
      var deleted = network.deletePendingRequest(ws, tag); // true if request was deleted
    }, 30*1000);
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'get_peers'
        }
    }
    there
    {
        type: 'response',
        content: {
            tag: tag,
            response: [
              "wss://relay.bytes.cash/bb",
              "wss://hub.obytechina.org/bb",
              "wss://hub.obyte.connectory.io/bb",
              "wss://odex.ooo/bb",
              ...
            ]
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'get_witnesses'
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: [
              "4GDZSXHEFVFMHCUCSHZVXBVF5T2LJHMU",
              "BVVJ2K7ENPZZ3VYZFWQWK7ISPCATFIW3",
              "DJMMI5JYA5BWQYSXDPRZJVLW3UGL3GJS",
              ...
            ]
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'get_joint',
            params: '0xXOuaP5e3z38TF5ooNtDhmwNkh1i21rBWDvrrxKt0U='
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: {
              "joint": {
                "unit": {
                  "unit": "0xXOuaP5e3z38TF5ooNtDhmwNkh1i21rBWDvrrxKt0U=",
                  "version": "1.0",
                  "alt": "1",
                  "witness_list_unit": "oj8yEksX9Ubq7lLc+p6F2uyHUuynugeVq4+ikT67X6E=",
                  "last_ball_unit": "AV4qvQ5JuI7J3EO5pFD6UmMu+j0crPbH17aJfigozrc=",
                  "last_ball": "Ogq4+uR6LaDspK89MmyydxRchgdN/BYwXvWXwNCMubQ=",
                  "headers_commission": 344,
                  "payload_commission": 607,
                  "main_chain_index": 2336026,
                  "timestamp": 1524844417,
                  "parent_units": [
                    "IFiHjYbzuAUC9TwcTrmpvezjTKBl6nIXjgjoKuVyCz0="
                  ],
                  "authors": [
                    {
                      "address": "AM6GTUKENBYA54FYDAKX2VLENFZIMXWG",
                      "authentifiers": {
                        "r": "MBWlL31OkibUXhoxmwIcWB/fAx1uWdNTE8PYTBNeN3w+tev4N1anOXjGtXiGW4whW3PfJeTO9fA5WxwyqK/m2w=="
                      }
                    }
                  ],
                  "messages": [
                    {
                      "app": "data",
                      "payload_hash": "MiuMtTSgP0brPQijsRc0igb+dxcrkLjXWRE253AE/S8=",
                      "payload_location": "inline",
                      "payload": {
                        "asset": "IYzTSjJg4I3hvUaRXrihRm9+mSEShenPK8l8uKUOD3o=",
                        "decimals": 0,
                        "name": "WCG Point by Byteball",
                        "shortName": "WCG Point",
                        "issuer": "Byteball",
                        "ticker": "WCG",
                        "description": "WCG Point is a honorific token, a recognition of contributing to World Community Grid projects. The token is not transferable, therefore, it cannot be sold and the balance reflects a lifetime contribution to WCG. Some services might choose to offer a privilege to users with large balance of this token."
                      }
                    },
                    {
                      "app": "payment",
                      "payload_hash": "FaVxPdeRKTgcO/LCDtu/YUZego8xyrnpUMF3V/sujr8=",
                      "payload_location": "inline",
                      "payload": {
                        "inputs": [
                          {
                            "unit": "1p85M8VSRDaAkNoCAdtMrv6cVGpPNNeVJpLzOkzWLk8=",
                            "message_index": 1,
                            "output_index": 0
                          }
                        ],
                        "outputs": [
                          {
                            "address": "AM6GTUKENBYA54FYDAKX2VLENFZIMXWG",
                            "amount": 17890
                          }
                        ]
                      }
                    }
                  ]
                },
                "ball": "Ef36OjK5m6lQMII4S9MN3iGtQcsvTzzQFLKQZ44UBCg="
              }
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'post_joint',
            params: {Object}
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: 'accepted'
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'heartbeat'
        }
    }
    {
        type: "response",
        content: {
            tag: tag,
            response: null
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: 'sleep'
        }
    }
    {
        type: "request",
        content: {
            tag: tag,
            command: "subscribe",
            params: {
                subscription_id: crypto.randomBytes(30).toString('base64'),
                last_mci: 0,
                library_version: "0.1"
            }
        }
    }
    {
        type: "response",
        content: {
            tag: tag,
            response: "subscribed"
        }
    }
    {
        type: "response",
        content: {
            tag: tag,
            response: {
                error: "I'm light, cannot subscribe you to updates",
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'catchup',
            params: {
                witnesses: witnesses,
                last_stable_mci: last_stable_mci,
                last_known_mci: last_known_mci
            }
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: {
                status: 'current'
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'get_hash_tree',
            params: {
                from_ball: from_ball,
                to_ball: to_bal      
            }
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: {
              "balls": [
                {
                  "unit": "c2teVop7xa1BmH1LPvytvOKU8HHTrprGWQw+uHlWPHo=",
                  "ball": "ht1QP48paWg5hpjx+Nbd/DSRFT8WlbQKk9+Uum0/tso=",
                  "parent_balls": [
                    "aEU1WiY9FQ9ihv9cKkX/EHWDxYYVaWYs2AL1yxyYZAQ="
                  ]
                },
                {
                  "unit": "tPbC6QLeweGuiGYRPsIhfd0TQWXWASYBJJUPGa9AOfw=",
                  "ball": "ba5wFB+gewdRX6nSpxt6+Nt8PaRen4pLIYg3jC6EvIw=",
                  "parent_balls": [
                    "ht1QP48paWg5hpjx+Nbd/DSRFT8WlbQKk9+Uum0/tso="
                  ]
                },
                {
                  "unit": "J7quEBEZ5ksq+0vK4cku7NtWkZ4Kxb8aHCu66RkN2eU=",
                  "ball": "4B+R+gW91BPmTqxF3G10oWmF4tg5LiHdpMQcL1ezqCo=",
                  "parent_balls": [
                    "ba5wFB+gewdRX6nSpxt6+Nt8PaRen4pLIYg3jC6EvIw="
                  ]
                },
                ...
              ]
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'get_last_mci'
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: 3795741
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'hub/deliver',
            params: {
                to: device_address,
                pubkey: pubkey,
                signature: signature,
                encrypted_package: encrypted_message
            }
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: 'accepted'
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'hub/get_temp_pubkey',
            params: permanent_pubkey
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: 'TEMP_PUBKEY_PACKAGE'
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'hub/temp_pubkey',
            params: {
                temp_pubkey: temp_pubkey,
                pubkey: permanent_pubkey,
                signature: signature
            }
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: 'updated'
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'hub/enable_notification',
            params: registrationId
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: 'ok'
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'hub/disable_notification',
            params: registrationId
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: 'ok'
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'hub/get_bots'
        }
    }
    {
    	type: 'response',
    	content: {
    		tag: tag,
    		response: [
    			{
    				"id": 29,
    				"name": "Buy Bytes with Visa or Mastercard",
    				"pairing_code": "A1i/[email protected]/bb#0000",
    				"description": "This bot helps to buy Bytes with Visa or Mastercard.  The payments are processed by Indacoin.  Part of the fees paid is offset by the reward you receive from the undistributed funds."
    			},
    			{
    				"id": 31,
    				"name": "World Community Grid linking bot",
    				"pairing_code": "A/JWTKvgJQ/[email protected]/bb#0000",
    				"description": "Donate your device’s spare computing power to help scientists solve the world’s biggest problems in health and sustainability, and earn some Bytes in the meantime.  This bot allows you to link your Byteball address and WCG account in order to receive daily rewards for your contribution to WCG computations.\n\nWCG is an IBM sponsored project, more info at https://www.worldcommunitygrid.org"
    			},
    			{
    			"id": 36,
    				"name": "Username registration bot",
    				"pairing_code": "[email protected]/bb#0000",
    				"description": "Buy a username and receive money to your @username instead of a less user-friendly cryptocurrency address.\n\nProceeds from the sale of usernames go to Byteball community fund and help fund the development and promotion of the platform."
    			},
    			...
    		]
    	}
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'hub/get_asset_metadata',
            params: 'IYzTSjJg4I3hvUaRXrihRm9+mSEShenPK8l8uKUOD3o='
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: {
                "metadata_unit": "0xXOuaP5e3z38TF5ooNtDhmwNkh1i21rBWDvrrxKt0U=",
                "registry_address": "AM6GTUKENBYA54FYDAKX2VLENFZIMXWG",
                "suffix": null
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'light/get_history',
            params: {
                witnesses: witnesses,
                requested_joints: joints,
                addresses: addresses
            }
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: {Object}
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'light/get_link_proofs',
            params: units
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: [Array]
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'light/get_parents_and_last_ball_and_witness_list_unit',
            params: {
                witnesses: witnesses
            }
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: {
              "parent_units": [
                "QEdpDamxpez8dlOE4nF8TWwHs+efokBXlxQHK27/y4g="
              ],
              "last_stable_mc_ball": "xF0K/NKO6CGMU6XeBGXzurEcCajomfZMOAU4XmAy+6o=",
              "last_stable_mc_ball_unit": "R/RosYwuXJ/mXw5TTfWLVLHdtnnzaKn5EgJkDfzagcs=",
              "last_stable_mc_ball_mci": 3795817,
              "witness_list_unit": "J8QFgTLI+3EkuAxX+eL6a0q114PJ4h4EOAiHAzxUp24="
            }
        }
    }
    {
        type: "response",
        content: {
            tag: tag,
            response: {
                error: "I'm light myself, can't serve you",
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'light/get_attestation',
            params: {
                attestor_address: 'FZP4ZJBMS57LYL76S3J75OJYXGTFAIBL',
                field: 'name',
                value: 'tarmo888'
            }
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: 'jeiBABcZI5fjyIPHkpb2PipLHzjUgafoPd0b6bdsGUI='
        }
    }
    {
        type: "response",
        content: {
            tag: tag,
            response: {
                error: "I'm light myself, can't serve you",
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'light/get_attestations',
            params: {
                address: 'MNWLVYTQL5OF25DRRCR5DFNYXLSFY43K'
            }
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: [
              {
                "unit": "jeiBABcZI5fjyIPHkpb2PipLHzjUgafoPd0b6bdsGUI=",
                "attestor_address": "FZP4ZJBMS57LYL76S3J75OJYXGTFAIBL",
                "profile": {
                  "name": "tarmo888"
                }
              }
            ]
        }
    }
    {
        type: "response",
        content: {
            tag: tag,
            response: {
                error: "I'm light myself, can't serve you",
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'light/pick_divisible_coins_for_amount',
            params: {
                asset: '',
                addresses: addresses,
                last_ball_mci: last_ball_mci,
                amount: amount,
                spend_unconfirmed: 'own'
            }
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: {Object}
        }
    }
    {
        type: "response",
        content: {
            tag: tag,
            response: {
                error: "I'm light myself, can't serve you",
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'light/get_definition',
            params: 'JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725'
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: [
              "sig",
              {
                "pubkey": "Aiy5z+jM1ySTZl1Qz1YZJouF7tU6BU++SYc/xe0Rj5OZ"
              }
            ]
        }
    }
    {
        type: "response",
        content: {
            tag: tag,
            response: {
                error: "I'm light myself, can't serve you",
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'light/get_balances',
            params: [
                'JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725',
                'UENJPVZ7HVHM6QGVGT6MWOJGGRTUTJXQ'
            ]
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: {
              "JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725": {
                "base": {
                  "stable": 1454617324,
                  "pending": 2417784
                }
              },
              "UENJPVZ7HVHM6QGVGT6MWOJGGRTUTJXQ": {
                "base": {
                  "stable": 0,
                  "pending": 6
                }
              }
            }
        }
    }
    {
        type: "response",
        content: {
            tag: tag,
            response: {
                error: "I'm light myself, can't serve you",
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'light/get_profile_units',
            params: [
                'MNWLVYTQL5OF25DRRCR5DFNYXLSFY43K'
            ]
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: [
              "gUrJfmdDeYhTKAHM5ywydHXpvcensbkv8TPQLuPG3b0="
            ]
        }
    }
    {
        type: "response",
        content: {
            tag: tag,
            response: {
                error: "I'm light myself, can't serve you",
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'get_system_var_votes'
        }
    }
    {
        type: "response",
        content: {
            tag: tag,
            response: {
                votes: {
                    op_list: [
                        {
                            address: "EJC4A7WQGHEZEKW6RLO7F26SAR4LAQBU",
                            unit: "LpxhHEfxbOyj0sPlp6UC6XrRABvRJiH4qKEsOcMd1Bc=",
                            timestamp: 1727794413,
                            value: [
                                "2FF7PSL7FYXVU5UIQHCVDTTPUOOG75GX",
                                ....
                            ],
                            is_stable: 1
                        },
                        ....
                    ],
                    threshold_size: [
                        {
                            address: "EJC4A7WQGHEZEKW6RLO7F26SAR4LAQBU",
                            unit: "LpxhHEfxbOyj0sPlp6UC6XrRABvRJiH4qKEsOcMd1Bc=",
                            timestamp: 1727794413,
                            value: 10000,
                            is_stable: 1
                        },
                        ....
                    ],
                    ....
                },
                balances: {
                    "EJC4A7WQGHEZEKW6RLO7F26SAR4LAQBU": 188576687136,
                    ....
                }
            }
        }
    }
    {
        type: 'request',
        content: {
            tag: tag,
            command: 'custom',
            params: params
        }
    }
    {
        type: 'response',
        content: {
            tag: tag,
            response: 0 || 'some response' || {Object} || [Array]
        }
    }

    Oscript language reference

    Autonomous agents

    Autonomous agents (AA) are special addresses (accounts) on the ledger that do not belong to anybody. An AA can send transactions only in response to a triggering transaction sent to it and strictly according to a program associated with the AA. This program is open and known in advance, it specifies the AA's reaction to triggering transactions. The reaction depends on:

    • coins sent by the triggering transaction: how much of which asset were sent;

    • data sent by the triggering transaction;

    • environment that exists in the ledger when the trigger is received: balances, data posted by oracles, attestations, state variables.

    The AA's reaction can be one or a combination of:

    • sending some other coins back to the sender or to a third party (which can be another AA);

    • changing the environment of the ledger by posting data feeds, attestations, updating state variables, etc.

    The behavior of autonomous agents is similar to vending machines: they accept coins and data entered on a keypad (the triggering action), and in response they release a cup of coffee or play a song, or do whatever they are programmed to do. What's common between them, their behavior is predictable, known in advance.

    There are no private/public keys associated with AAs. Their transactions do not have signatures. Their transactions are created by all (full) nodes just by following the common protocol rules and executing the code associated with the AA. All nodes always come to the same result and produce exactly the same transaction that should be sent on behalf of the AA. As one comes to expect from a DAG, all nodes arrive at the same view of the ledger state just by following the rules, without any votings, proof-of competitions, or leaders.

    The AA-generated transactions are not broadcast to peers as peers are expected to generate the same transactions on their own.

    The language used to specify the AA behavior is a domain specific language that we call Oscript. It was designed specifically for AAs to make it easy to write an autonomous agent behavior, make it easy to argue about what is going on in the AA code, and make it hard to make difficult-to-track errors.

    To control the resource consumption, the language does not support loops. Resource usage is controlled by setting a cap on the complexity of the program -- too resource heavy programs are simply not allowed. However, the resource caps provide enough room for the vast majority of practical applications.

    AAs can "call" other AAs by sending coins to them. All the scripts in the calling AA are finished and all changes committed before passing control on to the next AA, this avoids completely the reentrancy problem common in other smart contract platforms.

    Autonomous agent definition

    Addresses of autonomous agents follow the same general rules as all other Obyte addresses: their definitions are two-element arrays and the address is a checksummed hash of the array encoded in base32.

    AA address is defined as:

    The second element of the above array is an object that defines a template for future units created by the AA. The template's structure follows the structure of a regular unit in general, with some elements dynamic and dependent upon the input and state parameters. The dynamic elements are designated with special markup and include code in a domain specific language called Oscript:

    The concept is similar to how such languages as PHP, ASP, and JSP work. For example, a PHP script is a HTML file interspersed with fragments of PHP code enclosed between <?php and ?> tags. These fragments make the HTML page dynamic while keeping its HTML structure. While this mix can quickly become messy when PHP is used to generate HTML code, the ease of creating dynamic web pages by just inserting small pieces of code into HTML was one of the main selling points of these programming languages in the early days of the web. And it enabled quick prototyping, iteration, experimentation, and eventually led to the creation of some of the biggest pieces of the modern Internet (wordpress and facebook, to name a few).

    Transactions, or storage units as we call them in Obyte, are similarly the basic building units of distributed ledgers. They are usually represented as JSON objects, which are the counterparts of HTML pages on the web. Now, to make it easy to create dynamic, parameterized JSON objects representing transactions, we allow to inject some code into JSON and invite the developers to apply their creativity and do the rest.

    Document format
    Scripting language

    This is an example of autonomous agent definition:

    Here messages is a template for the AA's response transaction. It has only one message -- a payment message that will send the received amount less 1000 bytes back to sender. Omitting the amount entirely would send everything back (minus the fees). There are code fragments in strings enclosed in curly braces "{}", they are evaluated and the result is inserted in place of the code.

    Before sending the resulting response transaction, the nodes on the network will also enhance it with change outputs (if necessary) and add other necessary fields in order to attach the transaction to the DAG: parents, last stable unit, authors, timestamp, fees, etc.

    Below is a specification of the unit template format.

    JSON format

    bounce_fees

    This is an optional field of the unit template that specifies the fees charged from sender if the AA execution fails. In this case, all the received money in all assets is automatically bounced back to sender, less the bounce fees. The fees are keyed by asset ID (base for bytes).

    The minimum and default bounce fee for bytes is 10000 bytes. The minimum and default bounce fee for all other assets is 0. Non-base bounce fees apply only to those assets that were actually received by the autonomous agent.

    Sending to an autonomous agent anything less than the bounce fees will result in no response and the AA silently eating the coins. However this rule applies only to money sent from regular addresses. Bounce fees are not checked when the money is received from another AA.

    bounce_fees field is removed from the final unit.

    doc_url

    Each deployed autonomous agent can have a URL that points to a JSON formatted documentation file, which content will be shown to users in wallet app. URL can contain {{aa_address}} placeholder, which will be replaced with actual AA address before fetching the JSON file. The structure of the JSON file should look like this:

    Keys in field_descriptions object should match all the keys in trigger.data that users can use with this AA, wallet app will add value of that fields as an explanation what it does. The value of version should be as shown above ("1.0") - it should NOT used as version of AA.

    messages

    This is the main field of autonomous agent definition. It specifies templates for the messages to be generated, and the templates are parameterized with oscript code.

    The messages can be of any type (called app) that Obyte supports. The most common app is payment, it is used to send payments in any asset back to sender or to a third party. Other apps are:

    • asset: used to define a new asset. Different parameters that can be used are documented on ;

    • data: used to send data, this includes sending data parameters to other (secondary) AAs;

    • data_feed: used to send data feeds. By doing this, the AA becomes an oracle;

    Structure of those messages types is documented on and .

    There is also another, special, app called state, which is not possible in regular units but is used only in AAs to produce state changes. More about it in a separate chapter.

    base_aa

    It is possible to create parameterized Autonomous Agents, which are based on previously deployed templates. Their structure for that kind of Autonomous Agent would look like this:

    The base AA template would need to reference these parameters as params.name1 and params.name2.

    Oscript replacements

    Any string, number, or boolean in the template can be calculated by a script. The script is evaluated and the result is inserted in place of the script, the type is preserved. For example, if 20000 bytes were sent from address 2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7 to an AA, then this code

    would be replaced with

    Object keys can also be parameterized with Oscript:

    See the Oscript language reference below.

    cases - template objects

    JSON has scalars (strings, numbers, booleans) and objects (objects and arrays). Scalars can be parameterized through Oscript. Objects, on the other hand, are parameterized differently. They can have several alternative versions in the AA template, and only one version is selected based on input parameters and state. The versions and their selection criteria are specified using an object in cases array:

    The regular value of an object/array is replaced with an object whose single element is an array cases. Each element of the cases array is an object with up to 3 elements:

    • if: an Oscript expression. If the result of its evaluation is truthy then this case is selected. All other cases are not evaluated. if is required for all cases except the last, the last one may or may not have an if. If all previous cases evaluated to a falsy value and the last one is without an if, the last one is selected;

    In the above example, if the 2nd case were selected, the original object would fold into:

    Cases can be nested.

    Cases can be used for any non-scalar value inside messages, not just messages themselves.

    if - conditional objects

    Similar to the cases above, any object can have an additional if field. It is evaluated first, and if it is falsy, the entire object is removed from the enclosing object or array. Its internal Oscripts are not evaluated in this case.

    In the above example, the payment message will be generated only if trigger.data.withdrawal_amount is a number greater than 0.

    The if field itself is removed from the object.

    init - statements object

    Similar to the cases above, any object can have an additional init field. It is evaluated immediately after if when if is present and truthy. If there is no if, init is unconditionally evaluated first.

    init must be a statements-only Oscript, it does not return a value.

    Example:

    The init field itself is removed from the object.

    Conditional object fields and array elements

    If the value of any object field or array value evaluates to an empty string, this field or element is removed.

    For example, object

    will become

    Array

    will become

    Sending all coins

    If the amount field in an output within a payment message is omitted or evaluates to an empty string (which results in its removal per the above rules), this output receives all the remaining coins.

    In the above example, 1000 bytes are sent to 2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7, the rest of the AA's balance in bytes is sent to the address that triggered the AA.

    Empty objects and arrays

    If an object or array becomes empty as a result of various removals, it is also removed from the enclosing object or array.

    State message

    A state message is a special message in the messages array that performs state changes. It is the only oscript where state variables are assigned. Unlike regular messages that always have payload, state message has a field named state instead that contains a state changing script:

    The state message must always be the last message in the messages array. It is not included in the final response unit and its script (state script) is evaluated after the response unit is already prepared. It is the only oscript where response_unit variable is available. State script contains only statements, it is not allowed to return any value.

    Constants/Variables

    Local constants

    Local constant names are always prefixed with $. If the constant name itself needs to be calculated, the expression is enclosed in curly braces.

    Local constants exist only during AA evaluation. Each constant can be assigned a value only once (single-assignment rule).

    Each local constant is visible only in its own oscript after it was assigned. If it was assigned in if block, it is also available in all adjacent and enclosed oscripts. If it was assigned in init block, it is also available in all adjacent oscripts except if and in all oscripts enclosed in the same object.

    If an unassigned local constant is referenced, it is taken as false.

    If-else blocks in curly braces do not create separate scopes for local constants:

    Local constants can hold values of any type: string, number, boolean, or object (including array).

    Objects and arrays

    When a local constant holds an object, individual fields of the object can be accessed through dot or [] selector:

    If the specified object field does not exist, the value is taken as false.

    Objects and arrays can be initialized using familiar syntax as in other languages:

    Although all local constants are single-assignment, objects and arrays can be mutated by modifying, adding, or deleting their fields:

    The left-hand selectors can be arbitrarily long .a.b[2].c.3.d[].e. If some elements do not exist, an empty object or array is created automatically. [] adds to the end of an array. Array indexes cannot be skipped, i.e. if an array has length 5, you cannot assign to element 7. Once you are done mutating an object, can call freeze() to prevent further accidental mutations.

    Local functions

    Functions are local constants and the same rules apply to them.

    The return value is the value of the last expression or a return statement.

    A function sees all other local constants and functions declared before it. Because of this, recursion is impossible.

    Constants declared before the function cannot be shadowed by its arguments or other local constants declared within the function.

    Complexity of a function is the sum of complexities of all operations within it. Every time a function is called, its complexity is added to the total complexity of the AA. If a function is declared but never called, its complexity doesn't affect the complexity of the AA.

    Remote functions (getters)

    Getters are read-only functions available to other AAs and non-AA code.

    They are meant to extract information about an AA state that is not directly available through state vars. E.g. in order to fetch this information one needs to perform some calculation over state vars or access several state vars and have good knowledge about the way information is stored in the AA.

    Examples:

    • oswap could expose functions that would calculate the price of a future exchange. Otherwise, clients would have to do non-trivial math themselves.

    • the token registry could expose a single getter $getTokenDescription($asset) for reading a token description, and a similar one for decimals. Otherwise, one has to read first $desc_hash = var['current_desc_' || $asset], then var['description_' || $desc_hash].

    Getters are declared in a top-level getters section which is evaluated before everything else.

    The code in getters section can contain only function declarations and constants. Request-specific information such as trigger.data, trigger.outputs, etc is not available in getters.

    In the AA which declares them, getters can be accessed like normal functions.

    Other AAs can call them by specifying the remote AA address before the function name using this syntax:

    or

    If $remote_aa is a variable that cannot be evaluated at deploy time, you need to indicate the max complexity of the remote call either explicitly, after a #:

    or as a variable that can be evaluated at deploy time:

    or by indicating a base AA that the $remote_aa is a parameterized AA of:

    The complexity of a remote call is the complexity of its function, plus one.

    All functions declared in the getters section are publicly available. If some of them are not meant for public use, one can indicate this by a naming convention, e.g. by starting their names with an underscore $_callPrivateFunction() but information hiding cannot be really enforced since all getters operate on public data anyway.

    Getters can also be conveniently called from non-AAs. In node.js code:

    For remote clients, there is a light/execute_getter command in WebSocket API, hopefully it will be shortly available through obyte.js.

    State variables

    State variables are persisted across invocations of autonomous agents.

    Accessing state variables:

    Assigning state variables:

    var['var_name'] reads the value of state variable var_name stored under current AA.

    var['AA_ADDRESS']['var_name'] reads the value of state variable var_name stored under AA AA_ADDRESS. AA_ADDRESS is a valid address or this_address to refer to the current AA.

    If there is no such variable, false is returned.

    State variables can be accessed in any oscripts, but can be assigned only in script. Only state vars of the current AA can be assigned, state vars of other AAs are read-only. State vars can be reassigned multiple times but only the final value will be saved to the database and only if the AA finishes successfully. All changes are committed atomically. If the AA fails, all changes to state vars are rolled back.

    State vars can temporarily hold strings, numbers, objects, and booleans but when persisting, true values are converted to 1 and false values result in removal of the state variable from storage. AAs are required to hold a minimum balance in bytes equal to the size of their storage (length of var names + length of var values). This will also incentivize them to free up unused storage. The size of AA’s storage occupied before the current invocation is in variable storage_size.

    Internally, objects are stored as JSON strings and their length is limited. Don't try to store a structure in a state var if this structure can grow indefinitely.

    In addition to regular assignment =, state variables can also be modified in place using the following operators:

    • +=: increment by;

    • -=: decrement by;

    • *=: multiply by;

    For concatenation, the existing value of the var is converted to string.

    For +=, -=, *=, /=, %=, the existing boolean value is converted to 1 or 0, strings result in error.

    If the variable didn't exist prior to one of these assignments, it is taken as false and converted to number or string accordingly.

    Each read or write operation on a state variable adds +1 to complexity. Assignment with modification also costs 1 in complexity.

    Examples:

    Response variables

    Adds a key to the response object. Response variables do not affect state, they are meant to only inform the caller, and other interested parties, about the actions performed by the AA.

    Response vars can only be assigned, never read. Response vars can be assigned and reassigned multiple times in any oscript. They can hold values of types: string, number, boolean. Attempting to assign an object would result in true being assigned.

    Example: assigning these response variables

    will result in the following response object:

    Responses

    The AAs are activated and responses are generated when the triggering unit gets stabilized. If the triggering unit triggers several AAs or there are several triggers that are included in the same MC unit and therefore get stabilized at the same time, the triggers are handled in deterministic order to ensure reproducibility on all nodes.

    The first response unit has one or two parents:

    • the MC unit that just got stabilized and which includes the triggering unit;

    • the previous AA-generated unit (meaning, generated by any AA, not just the current one) if it is not already included in the first parent.

    Any subsequent responses (generated by secondary AAs and in response to other triggers at the same MCI) are chained after the first response and then one after another.

    After every response, 4 events are emitted:

    • aa_response

    • aa_response_to_unit- + trigger_unit

    • aa_response_to_address- + trigger_address

    Applications, which are based on a full node can subscribe to these events to receive information about responses they are interested in, e.g.:

    Applications, which are based on light node will also need to add the address to their watched list in order to subscribe to these events:

    All 4 event handlers receive objAAResponse object as a single argument:

    The object has the following fields:

    • mci: the MCI of the triggering unit. The response unit is attached to the NC unit with the same MCI;

    • trigger_address: the address that sent coins to the AA and thus triggered it;

    • aa_address: AA address

    Secondary AAs

    The generated response unit may contain outputs to other AAs. In this case, the receiving AAs are triggered too. They receive the outputs from the previous AA and the data (if any) from its generated data message.

    Secondary AAs behave like the primary ones except that they may receive less than the minimum bounce fees.

    If there are several secondary AAs triggered by a previous AA, they are handled in a deterministic order to make sure that the results are reproducible on all nodes. If a secondary AA triggers a ternary one in turn, it is handled before going on to the next secondary AA.

    If any of the secondary AAs fails, the entire chain fails, all the changes produced by its AAs are rolled back, and the primary AA bounces.

    The total number of secondary AAs stemming from a single primary AA cannot exceed 10, otherwise the primary AA fails and bounces.

    Failures

    If an AA fails for any reason (bad formula, attempt to send an invalid response, attempt to send more money than it has, etc), and attempt is made to bounce all the received coins back to sender, less bounce fees. All state changes are rolled back.

    If creation of the bouncing transaction fails too, no transaction is created in response, i.e. the AA eats the coins.

    The response object, which is not saved to the ledger, contains an error message explaining the reasons for failure.

    Semicolon

    Every variable assignment must be terminated by a semicolon ;. return and bounce statements are also terminated with a semicolon.

    Two types of Oscripts

    There are two types of Oscripts that can be used in AAs:

    • statements-only scripts that consist only of statements (such as assignments) and don't return any value. init script and state message are the only two allowed statements-only scripts;

    • scripts that return a value. They can have 0 or more statements but the last evaluated expression must not be a statement and its result is the result of the script.

    Example of a statements-only script:

    Example of a script that returns a value:

    The result of evaluation of the last expression $amount*2 is returned.

    Non-scalar return values

    Usually, the return value of oscript is a scalar: string, number, or boolean. It is just inserted in place of the oscript.

    If the return value is an object, it is similarly expanded and inserted in place of the original oscript. This can be used to send prepared objects through trigger.data.

    For example, if trigger.data is

    and an AA has this message

    The resulting message will be

    Flow control

    return

    Interrupts the script's execution and returns the value of expr. This syntax can be used only in oscripts that are supposed to return a value.

    Interrupts the script's execution without returning any value. This syntax can be used only in statements-only oscripts: init and state.

    if else

    Evaluates the first block of statements if the condition is truthy, the second block otherwise. The else part is optional.

    If the block includes only one statement, enclosing it in {} is optional:

    Complexity

    Like other smart contract definitions, AAs have a capped complexity which cannot exceed 100. Some operations involve complex computation or access to the database, such operations are counted and add to the total complexity count. Other operations such as +, -, etc, are relatively inexpensive and do not add to the complexity count. The language reference indicates which operations count towards complexity.

    Total complexity is the sum of complexities of all oscripts. It is calculated and checked only during deployment and includes the complexity of all branches even though some of them might not be activated at run time.

    If the complexity exceeds 100, validation fails and the AA cannot be deployed.

    Operators

    Arithmetic operators +, -, *, /, %, ^

    Operands are numbers or converted to numbers if possible.

    Additional rules for power operator ^:

    • e^x is calculated with exact (not rounded) value of e,

    • an exponent greater or equal to MAX_SAFE_INTEGER will cause an error,

    • for non-integer exponents, the result is calculated as x^y = e^(y * ln(x)) with rounding of the intermediary result, which causes precision loss but guaranties reproducible results;

    Concatenation operator ||

    If the same key is found in both objects, the value from the right-hand one prevails.

    Trying to concat an array with object results in error.

    If either operand is a scalar (strings, numbers, booleans), both are converted to strings and concatenated as strings. Objects/arrays become "true".

    Binary logical operators AND, OR

    Lowercase names and, or are also allowed.

    Non-boolean operands are converted to booleans.

    The result is a boolean.

    If the first operand evaluates to true, second operand of OR is not evaluated.

    If the first operand evaluates to false, second operand of AND is not evaluated.

    Unary logical operator NOT

    Lowercase name not is also allowed. The operator can be also written as !.

    Non-boolean operand is converted to boolean.

    The result is a boolean.

    Operator OTHERWISE

    Lowercase name otherwise is also allowed.

    If expr1 is truthy, its result is returned and expr2 is not evaluated. Otherwise, expr2 is evaluated and its result returned.

    Comparison operators ==, !=, >, >=, <, <=

    If both operands are booleans or both operands are numbers, the result is straightforward.

    If both operands are strings, they are compared in lexicographical order.

    If both operands are objects, only == and != are allowed, other operators will cause an error.

    If operands are of different types:

    • if any of them is an object or boolean, it causes an error,

    • if any of them is a string, only == and != are allowed and non-string operand will be converted to string before comparison, other operators will cause an error,

    • all other combinations of types cause an error.

    Ternary operator ? :

    If condition is truthy, expr1 is evaluated and returned, otherwise expr2 is evaluated and returned.

    Operator precedence

    Operators have the following precedence in the order of decreasing "stickiness":

    • ^

    • !

    • *, /, %

    Global constants

    pi

    Pi constant rounded to 15 digits precision: 3.14159265358979.

    e

    Euler's number rounded to 15 digits precision: 2.71828182845905.

    Built-in functions

    Type of variable

    Returns "string", "number", "boolean" or "object".

    Square root and natural logarithm

    These functions add +1 to complexity count.

    Negative numbers cause an error. Non-number inputs are converted to numbers or result in error.

    Absolute value

    Returns absolute value of a number. Non-number inputs are converted to numbers or result in error.

    Rounding (round, ceil, floor)

    Rounds the input number to the specified number of decimal places (0 if omitted). round uses ROUND_HALF_EVEN rules. Non-number inputs are converted to numbers or result in error. Negative or non-integer decimal_places results in error. decimal_places greater than 15 results in error.

    Minimum and maximum

    Returns minimum or maximum among the set of numbers. Non-number inputs are converted to numbers or result in error.

    Square root of the sum of squares

    Returns the square root of the sum of squares of all arguments. Boolean parameters are converted to 1 and 0, objects are taken as 1, all other types result in error. The function returns a non-infinity result even if some intermediary results (squares) would overflow.

    This function adds +1 to complexity count.

    Get part of a string

    Returns part of the string. If length is not set then returns rest of the string from start index. If start_index is negative then substring uses it as a character index from the end of the string. If start_index is negative and absolute of start_index is larger than the length of the string then substring uses 0 as the start_index.

    Find starting index of searched string

    Returns integer index (starting from 0) of searched string position in string. If searched string is not found then -1 is returned. Use contains if you don't need to know the index of the searched string.

    String search within string

    Returns boolean true if the string starts, ends or contains searched string.

    Uppercase/Lowercase string

    Returns the string with changed case.

    Replace string

    Replaces all occurrences of search_str in str with replacement and returns the new string.

    Validate string

    Returns true if str consists only of characters in allowed_chars. allowed_chars is a group of characters recognized by regular expressions, examples: a-z0-9, \w. has_only adds +1 to complexity.

    Seconds until specified date

    Attempts to parse string of date or date + time and returns timestamp. If you need to get seconds from UNIX Epoch of a current unit then use .

    Convert seconds to date + time, date or time

    Returns string format of date + time (default), date or time from . Timezone is UTC.

    Parse a JSON string into object

    Attempts to parse the input JSON string. If the result of parsing is an object, the object is returned. If the result is a scalar (boolean, string, number), the scalar is returned.

    This function adds +1 to complexity count.

    If parsing fails, false is returned.

    Non-string input is converted to string.

    Serialize an object into JSON string

    Stringifies the input parameter into JSON. The parameter can also be a number, boolean, or string. If it is a number outside the IEEE754 range, the formula fails. Objects in the returned JSON are sorted by keys.

    Iteration methods (map, reduce, foreach, filter)

    The function for map, foreach, and filter accepts 1 or 2 arguments. If it accepts 1 argument, the value of each element is passed to it. If it accepts 2 arguments, key and value for objects or index and element for arrays are passed.

    The second argument is the maximum number of elements that an array or object can have. If it is larger, the script fails. This number must be a constant so that it can be known at deploy time, and the complexity of the entire operation is the complexity of the callback function times maximum number of elements. If the function has 0 complexity, the total complexity of map/reduce/foreach/filter is assumed to be 1 independently of the max number of elements. Max number of elements cannot exceed 100.

    A function is executed over each element of an array or object. The callback function can be anonymous like in the example above, or referenced by name:

    Callback for map and filter can also be a :

    reduce has one additional argument for initial value:

    The callback function for reduce accepts 2 or 3 arguments: accumulator and value or accumulator, key, and value (accumulator, index, and element for arrays).

    All these 4 functions are similar to their Javascript counterparts but unlike Javascript they can also operate on objects, not just arrays.

    split, join

    The functions are similar to their counterparts in other languages. join can be applied to objects too, in this case the elements are sorted by key and their values are joined.

    Reverse array

    The function reverses an array and returns a new one. Deep copies of all elements are created.

    Passing a non-array to this function results in error.

    Keys of an object

    Returns the keys of an object. The keys are sorted. Passing anything but an object results in error.

    Length of any variable

    Returns the length of string. number, object or array. When passed an object or array, it returns the number of elements in object or array. Scalar types are converted to strings and the length of the string is returned.

    Length of an array

    Returns number of elements if the object is an array. Have to use to determine if object is an array. Use instead.

    Number from seed string

    Generates a number from a seed string. The same seed always produces the same number. The numbers generated from different seed strings are uniformly distributed in the specified interval.

    The first form returns a fractional number from 0 to 1.

    The second form returns an integer number from 0 to max inclusive.

    The third form returns an integer number from min to max inclusive.

    This function is useful for generating pseudo-random numbers from a seed string. It adds +1 to complexity count.

    SHA-256 hash

    Returns SHA-256 hash of input string/object in Base64 encoding (default), Base32 or Hex encoding. Non-string inputs are converted to strings. This function adds +1 to complexity count.

    Checksummed 160-bit hash

    Returns a 160-bit checksummed hash of an object, it is most useful for calculating an address when you know its definition, e.g. if you programmatically define a new AA and want to know its address in order to immediately trigger it.

    Check trigger data existence

    Returns boolean true if the trigger data parameter with name param exists.

    is_integer

    Returns boolean true if the number is without fractionals.

    is_array

    Returns boolean true if the object is an array.

    is_assoc

    Returns boolean true if the object is an associative array (dictionary).

    is_valid_address

    Returns boolean true if the string is valid Obyte wallet address.

    is_aa

    Returns boolean true if the string is Autonomous Agent address.

    is_valid_amount

    Returns boolean true if number is positive, integer, and below MAX_CAP (maximum cap that any token can have on Obyte platform).

    is_valid_signed_package

    Returns true if signedPackage object is a valid signed package signed by address address, returns false otherwise (the formula doesn't fail even if signedPackage doesn't have the correct format). address must be a valid address, otherwise the expression fails with an error. This function adds +1 to complexity count.

    signedPackage object is usually passed through the trigger and has the following structure:

    Here:

    • signed_message is the message being signed, it can be an object, an array, or scalar;

    • authors is an array of authors who signed the message (usually one), it has the same structure as unit authors and includes the signing address, authentifiers (usually signatures) and optionally definitions;

    • last_ball_unit: optional unit of last ball that indicates the position on the DAG at which the message was signed. If definition is not included in

    Usually, signedPackage is created by calling signMessage function from signed_message module:

    The function creates a correctly structured signedPackage object which can be added to trigger.data.

    is_valid_sig

    Returns true if signature is a correct ECDSA signature of message by the private key corresponding to public_key, returns false otherwise.

    • message is a string corresponding to the message being signed, the function will hash the message with SHA-256 before verifying the signature. In case message is not a string, the formula will fail.

    • public_key is a string containing the public key in a PEM format. For example:

      -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- can be omitted, spaces or carriage returns will be ignored. If public_key

    Supported algorithms:

    ECDSA

    brainpoolP160r1, brainpoolP160t1, brainpoolP192r1, brainpoolP192t1, brainpoolP224r1, brainpoolP224t1, brainpoolP256r1, brainpoolP256t1, prime192v1, prime192v2, prime192v3, prime239v1, prime239v2, prime239v3, prime256v1, secp112r1, secp112r2, secp128r1, secp128r2, secp160k1, secp160r1, secp160r2, secp192k1, secp224k1, secp224r1, secp256k1, secp384r1, sect113r1, sect113r2, sect131r1, sect131r2, wap-wsg-idm-ecid-wtls1, wap-wsg-idm-ecid-wtls4, wap-wsg-idm-ecid-wtls6, wap-wsg-idm-ecid-wtls7, wap-wsg-idm-ecid-wtls8, wap-wsg-idm-ecid-wtls9

    RSA

    PKCS #1 - 512 to 4096 bits

    vrf_verify

    Returns true if proof is valid, return false otherwise.

    • seed is a string from which is derived a proof unique for this RSA key. The formula will fail in case seed is not a string or is empty.

    • proof is an hexadecimal string value from 128 to 1024 characters (depending of RSA key size). Can be used with number_from_seed to obtain a verifiable random number.

    Supported algorithm: RSAPKCS #1 - 512 to 4096 bits

    is_valid_merkle_proof

    Check if a given element is included in a merkle tree. The proof has the following structure {root: string, siblings: array, index: number} and would normally come from trigger.data. One should check is_valid_merkle_proof and look up the merkle hash root from a data feed.

    bounce

    Aborts the script's execution with error message passed as the function's argument. The received money will be bounced to sender (less bounce fees).

    require

    Aborts the script's execution with error message error_message if the condition evaluates to a falsy value. The received money will be bounced to the sender (less bounce fees).

    This is equivalent to

    log

    Use this function for debugging your AA. Its arguments are evaluated and are available in a logs array within the AA response object. You can access this object and inspect the logs while running tests through . It is available even if the script bounced. The logs are not saved anywhere in the DAG but the expressions are still evaluated and might add to your script's complexity. Remove all log() calls after debugging.

    Ocore functions

    These functions are to be used in conjunction with is_valid_sig and vrf_verify.

    signMessageWithEcPemPrivKey

    Returns a signature that can be verified with is_valid_sig

    message is a string to be signed.

    encodingis either 'base64 or 'hex'

    pem_key is a string containing ECDSA private key in PEM format

    signMessageWithRsaPemPrivKey�

    Returns a signature that can be verified with is_valid_sig.

    message is a string to be signed.

    encodingis either 'base64 or 'hex'

    pem_key is a string containing RSA private key in PEM format

    vrfGenerate

    _**_Returns a proof that can verified with vrf_verify.

    seed is a string from which is derived the unique proof

    pem_key _**_is a string containing RSA private key in PEM format�

    Key generation with openssl

    Keys for the functions above can be generated with openssl.

    RSA (mandatory for vrfGenerate and vrf_verify):

    private key

    openssl genrsa -out priv_key.pem 2048

    Replace 2048 by any key length from 512 to 4096.

    public key

    openssl rsa -in priv_key.pem -outform PEM -pubout -out pub_key.pem

    ECDSA:

    private key

    openssl ecparam -name prime256v1 -genkey -noout -out priv_key.pem �Replace prime256v1 by any curve identifier listed above

    public key

    openssl ec -in priv_key.pem -pubout -out pub_key.pem

    Conversions

    To-string conversion

    Some functions and operators that expect strings as inputs need to convert non-string inputs to strings:

    • numbers are converted to strings using their decimal representation. For numbers whose exponent is greater than or equal to 21 or less than or equal to -7, exponential representation is used.

    • booleans are converted to strings true and false.

    • objects become strings true.

    To-number conversion

    Some functions and operators that expect numbers as inputs need to convert non-number inputs to numbers:

    • booleans true and false are converted to numbers 1 and 0 respectively.

    • objects become 1.

    • strings become numbers, +

    To-boolean conversion

    0 and empty string become false, all other values become true. Any value that would convert to true is called truthy, otherwise falsy.

    Comments

    Line comments

    as well as block comments are supported:

    References to external variables

    trigger.address

    The address of the sender who sent money to this AA. If the sending unit was signed by several addresses, the first one is used.

    trigger.initial_address

    The address of the sender who sent money to the initial AA of a chain of AAs. Same as trigger.address if there was no chain. When an AA sends money to another AA, trigger.initial_address remains unchanged.

    trigger.unit

    The unit that sent money to this AA.

    trigger.initial_unit

    The trigger unit that started a chain of AA calls. Same as trigger.unit if there was no chain. When an AA sends money to another AA, trigger.initial_unit remains unchanged.

    trigger.output

    Output sent to the AA address in the specified asset.

    assetID can be base for bytes or any expression that evaluates to asset ID.

    field can be amount or asset or omitted. If omitted, amount is assumed. If the trigger unit had several outputs in the same asset to this AA address, their amounts are summed.

    The search criteria can only be = (asset=$assetID) or != (asset!=$assetID).

    Examples:

    If there is no output that satisfies the search criteria, the returned .amount is 0 and the returned .asset is a string none. Your code should check for this string if necessary.

    If there is more than one output that satisfies the search criteria (which is possible only for !=), the returned .asset is a string ambiguous. Your code should check for this string if necessary. Trying to access .amount of an ambiguous asset fails the script.

    trigger.outputs

    An object that stores the outputs sent to the AA address in various assets.

    assetID can be 'base' for bytes or any expression that evaluates to asset ID.

    Examples:

    trigger.outputs is an associative array, you can iterate it with , , , , etc.

    trigger.data

    Data sent with the trigger unit in its data message. trigger.data returns the entire data object, trigger.data.field1.field2 or trigger.data.field1[expr2] tries to access a deeper nested field:

    • if it is an object, object is returned;

    • if it is a scalar (string, number, or boolean), scalar is returned;

    • if it doesn't exist, false is returned.

    For example, if the trigger unit had this data message

    trigger.data would be equal to

    trigger.data.field1 would be equal to

    trigger.data.field1.field2 would be equal to string value2,

    trigger.data.field1['a' || 'bc'] would be equal to number 88,

    trigger.data.field1.nonexistent would be equal to boolean false,

    trigger.data.nonexistent.anotherfield would be equal to boolean false.

    mci

    MCI of the trigger unit, which is the same as MCI of MC unit the response unit (if any) will be attached to.

    timestamp

    Timestamp of the MC unit that recently became stable, this is the unit whose stabilization triggered the execution of this AA. This is the same unit the response unit (if any) will be attached to. It's number of seconds since Epoch - Jan 01 1970. (UTC).

    mc_unit

    Hash of the MC unit that includes (or is equal to) the trigger unit.

    storage_size

    The size of AA’s storage occupied before the current invocation

    number_of_responses

    Built-in variable says how many responses were already generated in response to a primary trigger and might help to avoid exceeding the limit of 10 responses per primary trigger.

    previous_aa_responses

    Built-in variable that holds an array of responses generated by previous AAs in the chain (empty array for the first AA in the chain). Each element of the array is an object with the following fields:

    • unit_obj: response unit object (if any);

    • trigger_unit: trigger unit for this response;

    • trigger_address: trigger address for this response;

    this_address

    The address of this AA.

    response_unit

    The hash of the unit that will be generated by the AA in response to the trigger. This variable is available only in state script. Any references to this variable in any other scripts will fire an error.

    definition

    It allows to inspect the definition of any address using definition['ADDRESS'] syntax.

    Examples:

    asset

    Extracts information about an asset. This adds +1 to complexity. expr is base for bytes or an expression that evaluates to an asset ID.

    field is one of the following, or field_expr should evaluate to one of the following:

    • exists: boolean, returns false if asset ID is invalid;

    • cap: number, total supply of the asset. For uncapped assets, 0 is returned;

    • is_private: boolean, is the asset private?

    Examples:

    If the asset ID is valid, but does not exist then false is returned for any field.

    data_feed

    Finds data feed value by search criteria. This adds +1 to complexity.

    There are multiple search criteria listed between the double brackets, their order is insignificant.

    • oracles: string, list of oracle addresses delimited by : (usually only one oracle). this_address refers to the current AA;

    • feed_name: string, the name of the data feed;

    • feed_value

    Data feeds are searched before the MCI of the triggering unit (inclusively). If there are several AAs stemming from the same MCI, previous AA responses are also searched.

    Examples:

    in_data_feed

    Determines if a data feed can be found by search criteria. Returns true or false. This adds +1 to complexity.

    There are multiple search criteria listed between the double brackets, their order is insignificant.

    • oracles: string, list of oracle addresses delimited by : (usually only one oracle). this_address refers to the current AA;

    • feed_name: string, the name of the data feed;

    • feed_value

    Data feeds are searched before the MCI of the triggering unit (inclusively). If there are several AAs stemming from the same MCI, previous AA responses are also searched.

    Examples:

    attestation

    Finds an attestation by search criteria. This adds +1 to complexity.

    There are multiple search criteria listed between the double brackets, their order is insignificant.

    • attestors: string, list of attestor addresses delimited by : (usually only one attestor). this_address refers to the current AA;

    • address: string, the address that was attested;

    • ifseveral

    field string or field_expr expression are optional and they indicate the attested field whose value should be returned. Without field or field_expr, true is returned if an attestation is found.

    If no matching attestation is found, ifnone value is returned (independently of field). If there is no ifnone, false is returned.

    If a matching attestation exists but the requested field does not, the result is as if the attestation did not exist.

    Attestations are searched before the MCI of the triggering unit (inclusively). If there are several AAs stemming from the same MCI, previous AA responses are also searched.

    Examples:

    balance

    Returns the balance of an AA in the specified asset. If aa_address is omitted, the current AA is assumed. asset can be base for bytes, asset id for any other asset, or any expression that evaluates to an asset id or base string.

    This adds +1 to complexity count.

    The returned balance includes the outputs received from the current trigger.

    Examples:

    input, output

    Tries to find an input or output in the current unit by search criteria.

    These language constructs are available only in non-AA formulas in smart contracts (["formula", ...] clause).

    There are multiple search criteria listed between the double brackets, their order is insignificant. All search criteria are optional but at least one must be present.

    • asset: string, asset of input or output, can be base for bytes. Comparison operators can be only = or !=;

    • address: string, the address receives an output or spends an input, can be this_address. Comparison operators can be only = or !=

    field is one of amount, address, and asset. It indicates which information about the input or output we are interested in.

    If no input/output is found by search criteria or there is more than one matching entry, the formula fails.

    Examples:

    unit

    Searching and filtering

    Assuming $x is a variable that contains an object, arrays within this object can be searched and filtered like this:

    The above filters the ‘messages’ array by 3 filtering criteria. Like elsewhere in Oscript, double brackets indicate searching by listed criteria (similar to WHERE in SQL).

    When the object is a 1-element array but the next key is a string, unwrap the array. This helps to avoid writing [0]. For example, $x.messages.payload.asset and $x.messages[0].payload.asset are equivalent if ‘messages’ is a 1-element array.

    Examples:

    Limits

    • strings cannot be longer than 4096 characters;

    • state var value strings cannot be longer than 1024 characters;

    • state var names cannot be longer than 128 characters;

    • numbers must be in IEEE754 double range;

    Any attempt to exceed these limits will result in script's failure.

    Deployment

    AA code can be deployed with and , but AA code can also be deployed by sending a unit that includes a message with app=definition. This can be done with headless wallet or with AA itself, which has the definition of new AA in its payload.

    There is no error if the same definition is posted twice.

    The user who deployed the AA doesn't have any privileges over the AA aside from those directly coded in the AA source code.

    Once deployed, the AA definition can never be changed.

    Any outputs sent to the AA address before its definition was revealed ("before" means before the MCI of the first unit where the definition was revealed) are treated like regular outputs, they just increase the address'es balance but do not trigger any action, even delayed action after the definition was revealed. This might be a convenient way to fill the AA with some coins before actually launching it.

    It is possible for AA to deploy another AA, which is especially useful when creating for previously deployed AA templates:

    In case you wish to enter expressions that doesn't get evaluated with the deployment of new AA, but as actual expressions for a new AA, instead of "{expression}" write an expression that outputs another expression "{'{expression}'}".

    profile: used to send one's own profile. Maybe an AA wants to say something to the world about itself;

  • text: used to save arbitrary text to the DAG;

  • definition: used to post a definition of a new AA;

  • asset_attestors: used to change the attestor list of an asset previously defined by this AA;

  • attestation: used to post information about some other address. By doing this, the AA becomes an attestor;

  • definition_template: used to post a template for smart contract definition;

  • poll: used to create a poll;

  • vote: used to vote in a poll. Every AA has voting rights after all.

  • init: an optional statements-only Oscript that is evaluated immediately after if if this case is selected;

  • a mandatory element that is named the same as the original field (messages in the above example). If this case is selected, the original (3 levels higher) field is replaced with the value of this element.

  • /=: divide by;

  • %=: remainder of division by;

  • ||=: concatenate string/object/array.

  • aa_response_from_aa- + aa_address

    bounced: true if the trigger was bounced, false otherwise;

  • response_unit: hash of the response unit or null if there is no response;

  • objResponseUnit: response unit object or null if there is no response;

  • response: response from the script. The object can have up to two fields: error for error message and responseVars for response variables set by scripts.

  • this operator adds +1 to complexity count.

    +, -, ||

  • ==, !=, >, >=, <, <=

  • AND

  • OR

  • ?:

  • OTHERWISE

  • author
    , it must be known at this point in the ledger history. If there is no
    last_ball_unit
    in
    signedPackage
    , including address definition as part of each
    author
    is required;
  • version: always 2.0.

  • is not a string, is not for a supported curve, or doesn't have the required length then the formula will fail.
  • signature is a string containing the signature in Base64 or hexadecimal format. In case signature is not a string or is not Base64 nor hexadecimal format, the formula will fail.

  • pubkey is a string containing the RSA public key in a PEM spki format. For example:

    -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- can be omitted, spaces or carriage returns will be ignored. If public_key is not a string, is not for a supported curve, or doesn't have the required length then the formula will fail.

    can be used to force the conversion, e.g.
    +'3'
    becomes
    3
    ,
    +false
    becomes
    0
    .
    exists
    function can be used to check if param
    .

    aa_address: address of the AA that handled the trigger and generated the response.

    is_transferrable: boolean, is the asset transferrable?

  • auto_destroy: boolean, does the asset gets autodestroyed when sent to definer address?

  • fixed_denominations: boolean, is the asset issued in fixed denominations? Currently AAs can't send fixed denomination assets, but if issued_by_definer_only is false then somebody else can issue them.

  • issued_by_definer_only: boolean, is the asset issued by definer only?

  • cosigned_by_definer: boolean, should each transfer be cosigned by definer?

  • spender_attested: boolean, should each holder be attested?

  • is_issued: boolean, is any amount of the asset already issued?

  • definer_address: string, returns wallet address of the definer.

  • : string or number, optional, search only for this specific value of the data feed;
  • min_mci: number, optional, search only since the specified MCI;

  • ifseveral: string, optional, last or abort, what to do if several values found that match all the search criteria, return the last one or abort the script with error, default is last

  • ifnone: string or number or boolean, optional, the value to return if nothing is found. By default, this results in an error and aborts the script;

  • what: string, optional, value or unit, what to return, the data feed value or the unit where it was posted, default is value;

  • type: string, optional, auto or string, what type to return, default is auto. For auto, data feed values that look like valid IEEE754 numbers are returned as numbers, otherwise they are returned as strings. If string, the returned value is always a string. This setting affects only the values extracted from the database; if ifnone is used, the original type of ifnone value is always preserved.

  • : string or number, search only for values of the data feed that are
    =
    ,
    !=
    ,
    >
    ,
    >=
    ,
    <
    , or
    <=
    than the specified value;
  • min_mci: number, optional, search only since the specified MCI.

  • : string, optional,
    last
    or
    abort
    , what to do if several matching attestations are found, return the last one or abort the script with error, default is
    last
  • ifnone: string or number or boolean, optional, the value to return if nothing is found. By default, this results in an error and aborts the script;

  • type: string, optional, auto or string, what type to return, default is auto. For auto, attested field values that look like valid IEEE754 numbers are returned as numbers, otherwise they are returned as strings. If string, the returned value is always a string. This setting affects only the values extracted from the database; if ifnone is used, the original type of ifnone value is always preserved.

  • (other addresses);
  • amount: number, the condition for the amount of an input or output. Allowed comparison operators are: =, !=, >, >=, <, <=.

  • all intermediary calculations are rounded to 15 significant digits;

  • total complexity of all scripts cannot exceed 100;

  • total number of operations of all scripts cannot exceed 2000.

  • Web

    HTML

    PHP

    Obyte

    JSON

    Oscript

    Issuing assets page
    Sending data messages section
    Sending data to DAG page
    state message
    timestamp
    timestamp
    remote getter
    is_array
    length
    AA Testkit
    map, foreach, etc
    delete elements
    pass to functions
    post as data message
    Oscript
    (mainnet)
    editor
    Oscript
    (testnet)
    editor
    parameterized AAs
    exists
    ["autonomous agent", {
        // here goes the AA code
    }]
    {
        address: "{trigger.address}",
        amount: "{trigger.output[[asset=base]] - 1000}"
    }
    ["autonomous agent", {
        bounce_fees: { base: 10000 },
        doc_url: "https://example.com/doc_urls/{{aa_address}}.json",
        messages: [
            {
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{trigger.address}",
                            amount: "{trigger.output[[asset=base]] - 1000}"
                        }
                    ]
                }
            }
        ]
    }]
    ["autonomous agent", {
        bounce_fees: {
            base: 10000,
            "n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=": 100
        },
        ...
    }]
    {
    	"version": "1.0",
    	"description": "Description shown to users",
    	"homepage_url": "https://example.com",
    	"source_url": "https://github.com/byteball/ocore",
    	"field_descriptions": {
    		"some_field_name": "Description how to use this parameter",
    		"some_other_field_name": "Description how to use this other parameter",
    		...
    	}
    }
    ["autonomous agent", {
        base_aa: "ADDRESS_OF_BASE_AA",
        params: {
            name1: "value1",
            name2: "value2",
            ...
        }
    }]
    {
        address: "{trigger.address}",
        amount: "{trigger.output[[asset=base]] - 1000}"
    }
    {
        address: "2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7",
        amount: 19000
    }
    {
        "{trigger.data.key}": "value"
    }
    {
        messages: {
            cases: [
                {
                    if: "{trigger.data.define}",
                    messages: [
                        // first version of messages
                        ....
                    ]
                },
                {
                    if: "{trigger.data.issue}",
                    init: "{$amount = trigger.output[[asset=base]];}",
                    messages: [
                        // second version of messages
                        ....
                    ]
                },
                {
                    messages: [
                        // default version of messages
                        ....
                    ]
                }
            ]
        }
    }
    {
        messages: [
            // second version of messages
            ....
        ]
    }
    {
        messages: [
            {
                app: "data",
                payload: {
                    timestamp: "{timestamp}",
                    subscriber: "{trigger.address}"
                }
            },
            {
                if: "{trigger.data.withdrawal_amount > 0}",
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{trigger.address}",
                            amount: "{trigger.data.withdrawal_amount}"
                        }
                    ]
                }
            }
       ]
    }
    {
        messages: [
            {
                init: "{ $addr = trigger.address; }",
                app: "data",
                payload: {
                    timestamp: "{timestamp}",
                    subscriber: "{$addr}"
                }
            },
            {
                if: "{trigger.data.withdrawal_amount > 1000}",
                init: "{ $amount = trigger.data.withdrawal_amount - 1000; }",
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{trigger.address}",
                            amount: "{$amount}"
                        }
                    ]
                }
            }
       ]
    }
    {
        field1: "{ (1 == 2) ? 'value1' : '' }",
        field2: "value2"
    }
    {
        field2: "value2"
    }
    [ `{ (1 == 2) ? "value1" : "" }`, "value2" ]
    [ "value2" ]
    {
        messages: [
            {
                if: "{trigger.data.send_all}",
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7",
                            amount: 1000
                        },
                        {
                            address: "{trigger.address}"
                        }
                    ]
                }
            }
       ]
    }
    {
        messages: [
            {
                app: "payment",
                payload: {
                    asset: "base",
                    outputs: [
                        {
                            address: "{trigger.address}",
                            amount: "{trigger.output[[asset=base]] - 1000}"
                        }
                    ]
                }
            },
            {
                app: "state",
                state: `{
                    var['responded'] = 1;
                    var['total_balance_sent_back'] += trigger.output[[asset=base]] - 1000;
                    var[trigger.address || '_response_unit'] = response_unit;
                }`
            }
        ]
    }
    $name1 = 1;
    $name2 = 'value';
    ${'name' || 3} = $name1 + 10;
    [
        {
            if: `{
                $amount = trigger.output[[asset=base]];
                // the result of the last expression is the result of if
                $amount == 10000
            }`,
            init: `{
                // here we can reference $amount set in if
                $half_amount = round($amount / 2); 
            }`,
            messages: [
                {
                    app: "payment",
                    payload: {
                        asset: "base",
                        outputs: [
                            {
                                address: "{ trigger.address }",
                                amount: `{
                                    // we are in an enclosed oscript
                                    // and can reference $half_amount set in init
                                    $half_amount
                                }`
                            }
                        ]
                    }
                },
                {
                    app: "state",
                    state: `{
                        // we are in an enclosed oscript
                        // and can reference $amount set in if
                        var['received'] = $amount;
                        // we are in an enclosed oscript
                        // and can reference $half_amount set in init
                        var['sent_back'] = $half_amount;
                    }`
                }
            ]
        },
        {
            if: "{trigger.data.payout}",
            init: `{
                // here we cannot reference $amount nor $half_amount from the above
                // we can even assign other values to them without breaking the single-assignment rule
                $amount = 10;
            }`,
            ...
        }
    ]
    if (trigger.data.deposit){
        $amount = trigger.output[[asset=base]];
    }
    $fee = round($amount * 0.01); // we can reference the $amount from above
    $data = trigger.data;
    $action = $data.params.action;
    $player_score = $data.params[$player_name || '_score'];
    $obj = {a: 3, b: 7 };
    $arr = [7, 2, 's', {a: 6}];
    $obj = {a: 3, b: 7 };
    $obj.a = 4; // modifies an existing field
    $obj.c = 10; // adds a new field
    delete($obj, 'b'); // deletes a field
    freeze($obj); // prohibits further mutations of the object
    
    $arr = [7, 2, 's', {a: 6}];
    $arr[0] = 8; // modifies an existing element
    $arr[] = 5; // adds a new element
    delete($arr, 1); // removes element 1
    freeze($arr); // prohibits further mutations of the array
    $f = ($x) => {
    	 $a = var['a'];
    	 $x * $a
    };
    
    // one-line syntax for functions that have only one expression
    $sq = $x => $x^2;
    
    $res = $f(2);
    $nine = $sq(3);
    ['autonomous agent', {
    	getters: `{
    		$sq = $x => $x^2;
    		$g = ($x, $y) => $x + 2*$y;
    		$h = ($x, $y) => $x||$y;
    		$r = ($acc, $x) => $x + $acc;
    	}`,
    	init: `{
    		// uncomment if the AA serves as library only
    		// bounce("library only");
    		...
    	}`,
    	...
    }]
    $nine = MXMEKGN37H5QO2AWHT7XRG6LHJVVTAWU.$sq(3);
    $remote_aa = "MXMEKGN37H5QO2AWHT7XRG6LHJVVTAWU";
    $nine = $remote_aa.$sq(3);
    $nine = $remote_aa#5.$sq(3);
    $max_complexity = 5;
    $nine = $remote_aa#$max_complexity.$sq(3);
    $base_aa = '3DGWRKKWWSC6SV4ZQDWEHYFRYB4TGPKX';
    $nine = $remote_aa#$base_aa.$sq(3);
    const { executeGetter } = require('ocore/formula/evaluation.js');
    const db = require('ocore/db.js');
    
    const args = ["arg1", "arg2"];
    const res = await executeGetter(db, aa_address, getter, args);
    // assigning current AAs state variable to local constant
    $my_var_name1 = var['var_name1'];
    
    // assigning other AAs state variable to local constant
    $their_var_name1 = var['JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY']['var_name1'];
    var['var_name1'] = 'var_value';
    var['var_name2'] = 10;
    var['var_name3'] += 10;
    var['var_name4'] = false;
    var['var_name5'] = {a:8, b:2};
    var['var_name5'] ||= {c:6};  // concat an object
    var['sent_back'] = $half_amount;
    var['count_investors'] += 1;
    var['amount_owed'] += trigger.output[[asset=base]];
    var['pending'] = false;
    $x = var['JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY']['var_name1'];
    response['key'] = 'text';
    response['message'] = "set exchange rate to 0.123 tokens/byte";
    response['deposit'] = 2250000;
    {
        "responseVars": {
            "message": "set exchange rate to 0.123 tokens/byte",
            "deposit": 2250000
        }
    }
    var trigger_address = '';
    
    const eventBus = require('ocore/event_bus.js');
    eventBus.on('aa_response_to_address-' + trigger_address, (objAAResponse) => {
        // handle event
    });
    var aa_address = '';
    
    const walletGeneral = require('ocore/wallet_general.js');
    const eventBus = require('ocore/event_bus.js');
    walletGeneral.addWatchedAddress(aa_address, () => {
        eventBus.on('aa_response_from_aa-' + aa_address, (objAAResponse) => {
            // handle event
        });
    });
    { 
        mci: 2385,
        trigger_address: '2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7',
        trigger_unit: 'f2S6Q3ufjzDyl9YcB51JUj2z9nE1sL4XL2VoYOrVRgQ=',
        aa_address: 'JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY',
        bounced: false,
        response_unit: 'JCJ1ZGkl2BtUlYoeu6U2yshp97pen/fIkTHvaKYjZa4=',
        objResponseUnit: {
            version: '2.0dev',
            alt: '3',
            timestamp: 1560939440,
            messages: [ ... ],
            authors: [ ... ],
            last_ball_unit: 'cxjDfHzWWgW8LqsC8yoDhgCYmwThmFfkdygGnDrxiFg=',
            last_ball: 'gjg8W2cE4WGFAIIsU2BvLOQKOpH/03Oo1SDS3/2SQDs=',
            witness_list_unit: '3gLI9EnI2xe3WJVPwRg8s4CB24ruetuddS0wYa2EI3c=',
            parent_units: [ 'f2S6Q3ufjzDyl9YcB51JUj2z9nE1sL4XL2VoYOrVRgQ=' ],
            headers_commission: 267,
            payload_commission: 157,
            unit: 'JCJ1ZGkl2BtUlYoeu6U2yshp97pen/fIkTHvaKYjZa4='
        },
        response: {} 
    }
    $amount = trigger.output[[asset=base]];
    var['sent_back'] = round($half_amount/2);
    response['message'] = "set exchange rate to 0.123 tokens/byte";
    if ($amount >= 42000)
        bounce("amount too large");
    if (balance[base] < 20000)
        return;
    $amount = trigger.output[[asset=base]];
    $action = trigger.data.action;
    $amount = trigger.output[[asset=base]];
    $action = trigger.data.action;
    $amount*2
    {
        output: {address: "BSPVULUCOVCNXQERIHIBUDLD7TIBIUHU", amount: 2e5}
    }
    {
        app: "payment",
        payload: {
            asset: "base",
            outputs: [
                `{trigger.data.output}`
            ]
        }
    }
    {
        app: "payment",
        payload: {
            asset: "base",
            outputs: [
                {
                    address: "BSPVULUCOVCNXQERIHIBUDLD7TIBIUHU",
                    amount: 2e5
                }
            ]
        }
    }
    return expr;
    return;
    if (condition){
        $x = 1;
        $y = 2 * $x;
    }
    else{
        $x = 2;
        $z = $x^3;
    }
    if (condition)
        $x = 1;
    $amount - 1000
    $amount^2 / 4
    'abc' || 'def'              // 'abcdef'
    [4, 6] || [3, 1]            // [4, 6, 3, 1]
    {x: 1, y: 7} || {y: 8, a:9} // {x: 1, y: 8, a:9}
    expr1 OTHERWISE expr2
    condition ? expr1 : expr2
    typeof(anything)
    sqrt(number)
    ln(number)
    abs(number)
    round(number [, decimal_places])
    ceil(number [, decimal_places])
    floor(number [, decimal_places])
    min(number1, [number2[, number3[, ...]]])
    max(number1, [number2[, number3[, ...]]])
    hypot(number1, [number2[, number3[, ...]]])
    substring(string, start_index)
    substring(string, start_index, length)
    index_of(string, search_string)
    starts_with(string, prefix)
    ends_with(string, suffix)
    contains(string, search_string)
    to_upper(string)
    to_lower(string)
    replace(str, search_str, replacement)
    has_only(str, allowed_chars)
    parse_date(ISO8601_date)
    parse_date(ISO8601_datetime)
    timestamp_to_string(timestamp)
    timestamp_to_string(timestamp, 'datetime')
    timestamp_to_string(timestamp, 'date')
    timestamp_to_string(timestamp, 'time')
    json_parse(string)
    json_stringify(object)
    $ar = [2, 5, 9];
    $ar2 = map($ar, 3, $x => $x^2);
    $f = $x => $x^2;
    $ar = [2, 5, 9];
    $ar2 = map($ar, 3, $f);
    $ar2 = map($ar, 3, $remote_aa.$f);
    $c = 3;
    $ar = [2, 5, 9];
    
    // sums all elements, will return 16
    $acc = reduce($ar, $c, ($acc, $x) => $acc + $x, 0);
    split("let-there-be-light", "-")  // ["let", "there", "be", "light"]
    join(["let", "there", "be", "light"], "-")  // "let-there-be-light"
    
    split("let-there-be-light", "-", 2)  // ["let", "there"]
    reverse([4, 8, 3])  // [3, 8, 4]
    keys({b: 3, a: 8}) // ['a', 'b']
    length(string|number|object|array)
    array_length(object)
    number_from_seed(string)
    number_from_seed(string, max)
    number_from_seed(string, min, max)
    sha256(string|number|boolean|object)
    sha256(string|number|boolean|object, 'base64')
    sha256(string|number|boolean|object, 'base32')
    sha256(string|number|boolean|object, 'hex')
    $definition = ["sig", {
      "pubkey": "Ald9tkgiUZQQ1djpZgv2ez7xf1ZvYAsTLhudhvn0931w"
    }];
    $address = chash160($definition);
    
    $aa_definition = ['autonomous agent', {
      ...
    }];
    $aa_address = chash160($aa_definition);
    exists(trigger.data.param)
    is_integer(number)
    is_array(object)
    is_assoc(object)
    is_valid_address(string)
    is_aa(string)
    is_valid_amount(number)
    is_valid_signed_package(signedPackage, address)
    {
        "signed_message": {
            "field1": "value1",
            "field2": "value2",
            ...
        },
        "authors": [
            {
                "address": "2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7",
                "authentifiers": {
                    "r": "MFZ0eFJeLAgAmm6BJdvbEzNt7x0H2Fb5RQBBpMSmyVFMLM2r2SX5chU9hbEWXExkz/T2hXAk1qHmxkAbbpZw8w=="
                }
            }
        ],
        "last_ball_unit": "izgjyn9bpbJjwpKQV7my0Dq1VUHbzrLpWLrdR0fDydw=",
        "version": "2.0"
    }
    var headlessWallet = require('headless-obyte');
    var signed_message = require('ocore/signed_message.js');
    
    signed_message.signMessage(message, address, headlessWallet.signer, true, function (err, signedPackage) {
        // handle result here
        trigger.data.signedPackage = signedPackage;
    });
    is_valid_sig(message, public_key, signature)
    -----BEGIN PUBLIC KEY-----
    MEowFAYHKoZIzj0CAQYJKyQDAwIIAQEEAzIABG7FrdP/Kqv8MZ4A097cEz0VuG1P\n\ebtdiWNfmIvnMC3quUpg3XQal7okD8HuqcuQCg==
    -----END PUBLIC KEY-----
    vrf_verify(seed, proof, pubkey)
    is_valid_merkle_proof(element, proof);
    bounce(string);
    require(condition, error_message);
    if (!condition) {
        bounce(error_message);
    }
    log(expr1, expr2, ...);
    const sig = require(‘ocore/signature.js’)
    var signature = sig.signMessageWithEcPemPrivKey(message, encoding, pem_key)
    const sig = require(‘ocore/signature.js’)
    var signature = sig.signMessageWithRsaPemPrivKey(message, encoding, pem_key)
    const sig = require(‘ocore/signature.js’)
    var proof = vrfGenerate(seed, pem_key)
    // this is a comment line
    $x = 1; // this part of line is a comment
    /*
    A comment block
    */
    trigger.output[[asset=assetID]].field
    trigger.output[[asset!=assetID]].field
    trigger.output[[asset=base]]
    trigger.output[[asset=base]].amount
    trigger.output[[asset='j52n7Bfec9jW']]
    trigger.output[[asset=$asset]]
    trigger.output[[asset!=base]]
    trigger.output[[asset!=base]].amount
    if (trigger.output[[asset!=base]].asset == 'ambiguous'){
        ...
    }
    trigger.outputs[assetID]
    $asset = 'base';
    trigger.outputs['base']
    trigger.outputs[$asset]
    trigger.outputs.base
    {
        "app": "data",
        "payload": {
            "field1": {
                "field2": "value2",
                "abc": 88
            },
            "abc": "def"
        },
        "payload_hash": "..."
    }
    {
        "field1": {
            "field2": "value2",
            "abc": 88
        },
        "abc": "def"
    }
    {
        "field2": "value2",
        "abc": 88
    }
    definition[trigger.address]
    definition[trigger.address][0] == 'autonomous agent'
    definition[trigger.address][1].base_aa == 'EXPECTED_BASE_AA'.
    asset[expr].field
    asset[expr][field_expr]
    asset[base].cap
    asset['base'].cap
    asset['abc'].exists
    asset['n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY='].is_issued
    asset['n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=']['is_' || 'issued']
    asset['n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=']['is_' || 'private']
    data_feed[[oracles=listOfOracles, feed_name=nameOfDataFeed, ...]]
    data_feed[[oracles='JPQKPRI5FMTQRJF4ZZMYZYDQVRD55OTC', feed_name='BTC_USD']]
    data_feed[[oracles=this_address, feed_name='score']]
    data_feed[[oracles='JPQKPRI5FMTQRJF4ZZMYZYDQVRD55OTC:I2ADHGP4HL6J37NQAD73J7E5SKFIXJOT', feed_name='timestamp']]
    in_data_feed[[oracles=listOfOracles, feed_name=nameOfDataFeed, feed_value>feedValue, ...]]
    in_data_feed[[oracles='JPQKPRI5FMTQRJF4ZZMYZYDQVRD55OTC', feed_name='BTC_USD', feed_value>12345.67]]
    in_data_feed[[oracles=this_address, feed_name='score', feed_value=$score]]
    in_data_feed[[oracles='JPQKPRI5FMTQRJF4ZZMYZYDQVRD55OTC:I2ADHGP4HL6J37NQAD73J7E5SKFIXJOT', feed_name='timestamp', feed_value>=1.5e9]]
    attestation[[attestors=listOfAttestors, address=attestedAddress, ...]].field
    attestation[[attestors=listOfAttestors, address=attestedAddress, ...]][field_expr]
    attestation[[attestors='UOYYSPEE7UUW3KJAB5F4Y4AWMYMDDB4Y', address='BI2MNEVU4EFWL4WSBILFK7GGMVNS2Q3Q']].email
    attestation[[attestors=this_address, address=trigger.address]]
    attestation[[attestors='JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725', address='TSXOWBIK2HEBVWYTFE6AH3UEAVUR2FIF', ifnone='anonymous']].steem_username
    attestation[[attestors='JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725', address='TSXOWBIK2HEBVWYTFE6AH3UEAVUR2FIF']].reputation
    balance[asset]
    balance[aa_address][asset]
    balance[base]
    balance['n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=']
    balance['JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY'][base]
    output[[asset=assetID, amount>minAmount, address=outputAddress]].field
    input[[asset=assetID, amount=amountValue, address=inputAddress]].field
    input[[asset=base]].amount
    output[[asset = base, address=GFK3RDAPQLLNCMQEVGGD2KCPZTLSG3HN]].amount
    output[[asset = base, address='GFK3RDAPQLLNCMQEVGGD2KCPZTLSG3HN']].amount
    output[[asset = 'n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=', amount>=100]].address
    unit[unit_hash] // returns an object that represents the requested unit
    unit[response_unit] // returns an object of response unit generated by the current AA invocation
    $x.messages[[.app = 'payment', .payload.asset != 'ff', .payload.outputs.0.amount > 10]].payload.asset
    unit['kGQZkIj3gRclCp2vw9LMg0jVnPKYCebXHxm8XYPxbQ4='].mci
    unit['kGQZkIj3gRclCp2vw9LMg0jVnPKYCebXHxm8XYPxbQ4='].timestamp
    unit['kGQZkIj3gRclCp2vw9LMg0jVnPKYCebXHxm8XYPxbQ4='].messages[[.app='payment']].payload.outputs[[.address='MYESSCFGDERS3YIEGNZDOG2BI5HKQHLU']].amount
    const headlessWallet = require('headless-obyte');
    const eventBus = require('ocore/event_bus.js');
    const objectHash = require('ocore/object_hash.js');
    
    function postAA() {
        let definition = ["autonomous agent", {
            bounce_fees: { base: 10000 },
            messages: [
                ...
            ]
        }];
        let payload = {
            address: objectHash.getChash160(definition),
            definition: definition
        };
        let objMessage = {
            app: 'definition',
            payload_location: 'inline',
            payload_hash: objectHash.getBase64Hash(payload),
            payload: payload
        };
        let opts = {
            messages: [objMessage]
        };
    
        ​headlessWallet.issueChangeAddressAndSendMultiPayment(opts, (err, unit) => {
            if (err){
                /*
                something went wrong,
                maybe put this transaction on a retry queue
                */
                return;
            }
            // handle successful payment
        });
    }
    
    eventBus.on('headless_wallet_ready', postAA);
    ["autonomous agent", {
        bounce_fees: { base: 10000 },
        messages: [
            {
                app: "definition",
                payload: {
                    definition: ["autonomous agent", {
                        base_aa: "BASE_AA_TEMPLATE_ADDRESS",
                        params: {
                            name1: "{trigger.data.name1}",
                            name2: "{trigger.data.name2}",
                            ...
                        }
                    }]
                }
            },
            ...
        ]
    }]
    -----BEGIN PUBLIC KEY-----
    MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANOJ1Y6/6uzcHDa7Q1gLO9z0KGOM51sO
    Pc2nfBF4RTobSVVpFnWtZF92r8iWCebwgSRSS9dEX6YIMIWNg11LbQ8CAwEAAQ==
    -----END PUBLIC KEY-----