GuidesAPI Reference
DocumentationLog In

How to Write a Rosetta DSL file

Overview

When writing check:construction tests to test with the rosetta-cli tool, it’s best to write them in the Rosetta Constructor Domain-Specific Language (DSL).

The Rosetta DSL allows you to to write Workflows for the constructor package.

This DSL is most commonly used for writing check:construction tests for the rosetta-cli tool. The Rosetta DSL filename's extension is .ros.

Prerequisites

  • The Rosetta DSL file is a prerequisite to write a configuration file for the rosetta-cli tool’s check:construction test. For more information, read our How to Write a Configuration File for rosetta-cli Testing documentation.
  • We recommend that you write your Rosetta DSL file as you write your Rosetta API configuration file.
  • Use Rosetta-supported curve types. The create_account and transfer workflows include this value. Using any other curve types will cause test failures with the rosetta-cli tool.
  • The request_funds workflow requires the use of an account with test funds. You can provide that with a pre-funded account or with a URL to an account with test funds. See the Account Funding section for more information.

Terminology

The rosetta-cli tool's check:construction test uses the following hierarchy of concerns:

Workflows -> Jobs
  Scenarios
    Actions

Workflows contain collections of Scenarios to execute. Scenarios are executed atomically in database transactions (rolled back if execution fails) and culminate in an optional broadcast. This means that a single Workflow could contain multiple broadcasts (which can be useful for orchestrating staking-related transactions that affect a single account).

To perform a Workflow, you create a Job. The Job has a unique identifier and stores the state for all Scenarios in the Workflow. The state is shared across an entire Job so that Actions in a Scenario can access the output of Actions in other Scenarios. The syntax for accessing this shared state is called GJSON Path.

Actions are discrete operations that can be performed in the context of a Scenario. You can find a full list of all the Actions that can be performed in the Rosetta SDK's ActionType package.

If you have suggestions to add more actions, you can open an issue in the rosetta-sdk-go repository.

Syntax

When broken down into simple elements, the Rosetta DSL syntax looks like the following example:

// line comment
<workflow name>(<concurrency>){
  <scenario 1 name>{
    <output path> = <action type>(<input>); // another comment
  },
  <scenario 2 name>{
    <output path 2> = <action type>(<input>);
  }                    
}

Elements

  • line comment - Follows the JavaScript single-line comment notation of two slashes (//) at the start of a line or at the end of a line of code.
  • workflow name - Your name for the workflow. For example, create_account.
    • Each workflow must have a unique name; no two workflows can have the same name.
  • concurrency - The order in which you want the workflow to run. You must provide a concurrency value when you define a workflow.
  • scenario name - Your name for the scenarios you’d like to add to the workflow.
    • You can add one or more scenarios to a workflow. They must be separated by a comma.
    • You must define the scenarios within a workflow.
    • As with the workflow name, each scenario name must be unique. No two scenarios inside the same workflow can have the same name.
  • output path - This variable stores the results of a scenario for later use.
  • action type - The reason why you want to run this scenario.
  • input - The input for all functions is a JSON blob that will be evaluated by the Worker.
    • It is possible to reference other variables in an input using the syntax {{var}} where var must follow the GJSON syntax. The Rosetta DSL compiler will automatically check that referenced variables are previously defined.

Account Funding

If you plan to run the check:construction test in continuous integration (CI), we recommend that you provide a value for prefunded accounts when running the test. Otherwise, you would need to manually fund generated accounts.

Optionally, you can also provide a return_funds workflow that will be invoked when exiting check:construction. This can be useful in CI when you want to return all funds to a single account or faucet (instead of black-holing them in all the addresses created during testing).

To use the check:construction test without prefunded accounts, you must implement two required Workflows:

  • If you don't implement these Workflows, processing could stall.
  • Please note that create_account can contain a transaction broadcast if on-chain origination is required for new accounts on your blockchain.

Broadcast Invocation

If you'd like to broadcast a transaction at the end of a Scenario, you must populate the following fields:

  • <scenario>.network
  • <scenario>.operations
  • <scenario>.confirmation_depth (allows for stake-related transactions to complete before marking as a success)

Optionally, you can populate the <scenario>.preprocess_metadata field.

Once a transaction is confirmed on-chain (after the provided <scenario>.confirmation_depth), check:construction stores it at <scenario>.transaction for access by other Scenarios in the same Job.

Dry Runs

In UTXO-based blockchains, it may be necessary to amend the operations stored in <scenario>.operations based on the suggested_fee returned in /construction/metadata. The check:construction test supports running a dry run of a transaction broadcast if you set the <scenario>.dry_run field to true. The suggested fee will then be stored as <scenario>.suggested_fee for use by other Scenarios in the same Job. You can find an example of this in the Bitcoin config).

If you do not populate this field or set it to false, the transaction will be constructed, signed, and broadcast.

Functions

In the Rosetta DSL, it is possible to invoke functions, where the function name is written as an action type (Action.Type).

  • The Rosetta DSL provides optional "native invocation" support for the math and set_variable action types. Read the Native Invocation section below for details.
  • Function invocations can span multiple lines (if you "pretty print" the JSON blob). However, each function call line must end with a semicolon (;).
  • Currently, it is not possible to define your own functions. The types.go file lists all the available functions.

Recursive Calls

It is not possible to invoke a function from the input of another function. There MUST be exactly one function call per line. For example, this syntax is not allowed:

a = 1 + load_env("value");

Adding this incorrect syntax to your code will produce errors.

Native Invocation

The Rosetta DSL provides optional "native invocation" support for the math and set_variable action types. "Native invocation" in this case means that the caller does not need to invoke the action type in the normal format:

<output path> = <function name>(<input>);
math

You can invoke math with the following syntax:

<output path> = <left side> <operator> <right side>;

A native invocation of a simple addition would look like the following code:

a = 10 + {{fee}};

The normal invocation would look like the following code:

a = math({"operation":"addition","left_side":"10","right_side":{{fee}}});
set_variable

You can invoke set_variable with the following syntax:

<output path> = <input>

A native invocation of the set_variable code would look like:

a = {"message": "hello"};

The normal invocation would look like the following code:

a = set_variable({"message": "hello"});

Testing

Workflows are part of a Rosetta API implementation, which you can test with the rosetta.cli tool.

Example Workflows

Create an Account

This Workflow is required if you are testing without prefunded accounts.

The following example is for a Workflow to create an account. It includes the create_account scenario and the save_account scenario. For more information, read the source file.

create_account(1){
  create_account{
    network = {"network":"Testnet3", "blockchain":"Bitcoin"};
    key = generate_key({"curve_type":"secp256k1"});
    account = derive({
      "network_identifier": {{network}},
      "public_key": {{key.public_key}}
    });
    save_account({
      "account_identifier": {{account.account_identifier}},
      "keypair": {{key}}
    });
  }
}

Request Funds

This Workflow is required if you are testing without prefunded accounts.

The following example is for a Workflow to request funds. It includes the find_account scenario and the request scenario. For more information, read the source file.

request_funds(1){
  find_account{
    currency = {
      "symbol":"tBTC",
      "decimals":8
    };
    random_account = find_balance({
      "minimum_balance":{
        "value": "0",
        "currency": {{currency}}
      },
      "create_limit":1
    });
  },
  request{
    min_balance = load_env("MIN_BALANCE");
    adjusted_min = {{min_balance}} + 600;
    loaded_account = find_balance({
      "account_identifier": {{random_account.account_identifier}},
      "minimum_balance":{
        "value": {{adjusted_min}},
        "currency": {{currency}}
      }
    });
  }
}

Transfer

The following example is for a Workflow to transfer funds. It includes the transfer scenario. For more information, read the source file.

transfer(10){
  transfer{
    transfer.network = {"network":"Testnet", "blockchain":"ICON"};
    currency = {"symbol":"ICX", "decimals":18};
    sender = find_balance({
      "minimum_balance":{
        "value": "10000000000000000",
        "currency": {{currency}}
      }
    });

    // Set the recipient_amount as some value <= sender.balance-max_fee
    max_fee = "10000000000000000";
    available_amount = {{sender.balance.value}} - {{max_fee}};
    recipient_amount = random_number({"minimum": "1", "maximum": {{available_amount}}});
    print_message({"recipient_amount":{{recipient_amount}}});

    // Find recipient and construct operations
    sender_amount = 0 - {{recipient_amount}};
    recipient = find_balance({
      "not_account_identifier":[{{sender.account_identifier}}],
      "minimum_balance":{
        "value": "0",
        "currency": {{currency}}
      },
      "create_limit": 100,
      "create_probability": 50
    });
    transfer.confirmation_depth = "1";
    transfer.operations = [
      {
        "operation_identifier":{"index":0},
        "type":"TRANSFER",
        "account":{{sender.account_identifier}},
        "amount":{
          "value":{{sender_amount}},
          "currency":{{currency}}
        }
      },
      {
        "operation_identifier":{"index":1},
        "type":"TRANSFER",
        "account":{{recipient.account_identifier}},
        "amount":{
          "value":{{recipient_amount}},
          "currency":{{currency}}
        }
      }
    ];
  }
}

Example DSL File

Destination Tag Support

The following example is for a DSL file to test whether the Rosetta implementation supports destination tags. It includes the request_funds, create_account, and transfer workflows. For more information, read the Testing Destination Tag Blockchains section in the How to Test your Rosetta Implementation document.

request_funds(1){
  find_account{
    currency = {"symbol":"STX", "decimals":6};
    random_account = find_balance({
      "minimum_balance":{
        "value": "0",
        "currency": {{currency}}
      },
      "create_limit":1
    });
  },
  // Create a separate scenario to request funds so that
  // the address we are using to request funds does not
  // get rolled back if funds do not yet exist.
  request{
    loaded_account = find_balance({
      "account_identifier": {{random_account.account_identifier}},
      "minimum_balance":{
        "value": "1000000000",
        "currency": {{currency}}
      }
    });
  }
}
create_account(1){
  create{
    network = {"network":"testnet", "blockchain":"stacks"};
    key = generate_key({"curve_type": "secp256k1"});
    account = derive({
      "network_identifier": {{network}},
      "public_key": {{key.public_key}}
    });
    // If the account is not saved, the key will be lost!
    save_account({
      "account_identifier": {{account.account_identifier}},
      "keypair": {{key}}
    });
       print_message({"--- created": {{key}}});
  }
}
transfer(1){
  transfer{
    transfer.network = {"network":"testnet", "blockchain":"stacks"};
    currency = {"symbol":"STX", "decimals":6};
    
    recipient_amount = "50";
    sender_amount = 0 - {{recipient_amount}};
    print_message({"recipient_amount":{{recipient_amount}}});
    print_message({"sender_amount":{{sender_amount}}});
    
    sender_account = {"address":"ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y"};
    recipient_account = {"address":"STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6"};
    transfer.confirmation_depth = "1";
    transfer.operations = [
         {
        "operation_identifier":{"index":0},
        "type":"fee",
        "account":{{sender_account}},
        "amount":{
          "value":"-180",
          "currency":{{currency}}
        }
      },
      {
        "operation_identifier":{"index":1},
        "type":"token_transfer",
        "account":{{sender_account}},
        "amount":{
          "value":{{sender_amount}},
          "currency":{{currency}}
        }
      },
      {
        "operation_identifier":{"index":2},
        "type":"token_transfer",
        "account":{{recipient_account}},
        "amount":{
          "value":{{recipient_amount}},
          "currency":{{currency}}
        }
      }
    ];
    transfer.preprocess_metadata = {"memo":"testing"};
  }
}