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’scheck: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
andtransfer
workflows include this value. Using any other curve types will cause test failures with therosetta-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 aconcurrency
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 theWorker
.- It is possible to reference other variables in an input using the syntax
{{var}}
wherevar
must follow the GJSON syntax. The Rosetta DSL compiler will automatically check that referenced variables are previously defined.
- It is possible to reference other variables in an input using the syntax
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
andset_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"};
}
}