Skip to content

Hello World

Simple Storage: The “Hello World” of Blockchain

Before we dive further into application development, let’s start with walking through the most basic example for application development using the Simple Storage Contract. Think of this as the “Hello World” for the blockchain environment. The Simple Storage contract stores an unsigned integer on the blockchain and contains methods to set and fetch the state of this unsigned integer. The Solidity code for this smart contract looks something like this:

contract SimpleStorage {
    uint storedData;

    function set(uint x) {
        storedData = x;
    }

    function get() returns (uint) {
        return storedData;
    }
}

For more information on Solidity, you can read the docs here: https://solidity.readthedocs.io/

We will now look at how to use the blockapps-rest SDK (read more about this here) to:

  1. Retrieve an OAuth token and create a STRATO account
  2. Upload this contract to a STRATO blockchain
  3. Change the state of this contract by calling the set method in the smart contract
  4. Retrieve the state of the contract by calling the get method in the smart contract
  5. Retrieve the state of the contract by querying STRATO's relational mapping of on-chain data (cirrus)

Setup

You will need:

  1. A strato node. See deployment instructions here.
  2. A text editor
  3. nodejs, npm and yarn

Setup the project using the following commands:

mkdir strato-hello-world
cd strato-hello-world
yarn init

Install blockapps-rest and Babel:

yarn add blockapps-rest
yarn add @babel/core @babel/cli @babel/preset-env @babel/node

Create a file .babelrc with the following content:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": true
        }
      }
    ]
  ]
}

Copy the solidity code above into a file SimpleStorage.sol.

Create a config.yaml file with the following structure and replace the url field and oauth details with settings that match your STRATO node:

apiDebug: true
timeout: 600000

# WARNING - extra strict syntax
# DO NOT change the nodes order
# node 0 is the default url for all single node api calls
nodes:
  - id: 0
    url: "http://localhost"
    publicKey: "6d8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0"
    port: 30303
    oauth:
      appTokenCookieName: "ba_rest_test_session"
      scope: "email openid"
      appTokenCookieMaxAge: 7776000000 # 90 days: 90 * 24 * 60 * 60 * 1000
      clientId: "PLACEHOLDER"
      clientSecret: "PLACEHOLDER"
      openIdDiscoveryUrl: "PLACEHOLDER"
      redirectUri: "http://localhost/callback"
      logoutRedirectUri: "http://localhost"

At this point your directory structure should look like this:

./strato-hello-world
|-- node_modules/
|-- SimpleStorage.sol
|-- config.yaml
|-- package.json
|-- yarn.lock
|-- .babelrc

Create a STRATO Account Using OAuth

Before we can do anything, we need to create a STRATO account (read more about accounts here. We will be using the client-credential flow. Type the following code into your index.js:

import { assert, rest, fsUtil, oauthUtil } from "blockapps-rest";

const globalConfig = fsUtil.getYaml("config.yaml");

// Sanity checks for the config
assert.isDefined(globalConfig, "Config should be defined");
assert.isArray(globalConfig.nodes, "Config should have a nodes array member");
assert.isAtLeast(
  globalConfig.nodes.length,
  1,
  "Atleast one node should be defined in the config"
);
assert.isDefined(
  globalConfig.nodes[0].oauth,
  "OAuth configuration should be defined for the first node"
);

const main = async config => {
  // Setup options
  const options = { config: globalConfig, logger: console };

  // Initialize ba-rest oaut-utility
  const oauth = oauthUtil.init(globalConfig.nodes[0].oauth);

  // Get token using client-credential flow
  const tokenResponse = await oauth.getAccessTokenByClientSecret();

  // Extract token from token response
  const oauthUser = { token: tokenResponse.token.access_token };

  // Create or get the public ethereum address corresponding to this OAuth identity
  const stratoUser = await rest.createUser(oauthUser, options);
};

main(globalConfig);

The above piece of code does the following:

  1. It reads in the config file and performs some checks to ensure the config is valid
  2. It sets up an options object that will be used by the rest sdk
  3. It initializes an oauth helper to get auth tokens for the identity configured in the config file.
  4. Its uses the Oauth identity to create a STRATO identity (public-private key pair).

rest.createUser makes the following API call if a key pair does not exist for the user identified by the token provided. You can also make the following API call directly without using the SDK.

curl -X POST \
   -H "Authorization: Bearer <token>"
  http://<strato_host_address>/strato/v2.3/key

Upload the Contract

Now that we have an account, we will use this STRATO identity to upload our SimpleStorage contract. Add the following lines of code to our main function:

  // read Solidity source
  const simpleStorageSrc = fsUtil.get("SimpleStorage.sol");

  const contractArgs = {
    name: 'SimpleStorage',
    source: simpleStorageSrc,
    args: {}, // Any constructor args would go here. We dont have any.
  }

  // Use the STRATO identity to upload our contract
  const contract = await rest.createContract (stratoUser, contractArgs, options)

The above lines allow our code to:

  1. Read in a solidity source file
  2. Upload it to the STRATO blockchain using the STRATO identiy we created previously
  3. Return a contract object that we can now use to call functions against this contract

You can console.log the contract object above. It should look something like this:

{ name: 'SimpleStorage',
  address: '9c8bd2bffc3b55265fc4b9474e2337654f8c9327' }

To upload a contract directly using the API, you can use the following POST request:

curl -X POST \
  -H "authorization: Bearer <token>" \
  --data '{
    "txs": [
        {
          "payload": {
            "contract": "SimpleStorage",
            "src": "<contract-src>",
            "args": {}
          },
          "metadata": {
            "history": ""
          },
          "type": "CONTRACT"
        }
      ],
      "txParams": {
        "gasLimit": 32100000000,
        "gasPrice": 1
      }
    }' \
  "http://<strato_host_address>/strato/v2.3/transaction?resolve=true"

Call Methods in the Contract

To call the set method in our contract, add the following lines of code to our main function:

  // Call the SimpleStorage set method with the argument x=10
  const callArgs = {
    contract,
    method: "set",
    args: {
      x: 10
    }
  };

  await rest.call(stratoUser, callArgs, options);

The above code, calls the set method in the SimpleStorage smart contract.

To call a method directly using the API, the call is:

curl -X POST \
  -H "authorization: Bearer <token>" \
  --data '{
    "txs": [
      {
        "payload": {
          "contractName": "SimpleStorage",
          "contractAddress": "9cf2969960f1947d8b523f2044ff39e380fed144",
          "method": "set",
          "args": {
            "x": 10
          },
          "metadata": {
            "history": ""
          },
        },
        "type": "FUNCTION"
      }
    ],
    "txParams": {
      "gasLimit": 32100000000,
      "gasPrice": 1
    }
  }' \
  "http://<strato_host_address>/strato/v2.3/transaction?resolve=true"

Query for Contract State

There are multiple ways to query for state in STRATO. You can fetch the state of a contract directly from the chain using rest.getState, or, you can query all contracts of the same type (same code hash), using rest.search.

Use rest.getState to Retrieve State from the Chain

To retrieve the state of a specific SimpleStorage contract add the following lines to the main function:

  const state = await rest.getState(stratoUser, contract, options);

  console.log(state);

This should result in the following output:

{
  "get": "function () returns (uint)",
  "set": "function (uint) returns ()",
  "storedData": "10"
}

The equivalent API call for this method is:

curl -X GET
  -H "authorization: Bearer <token>" \
  http://<strato_host_address>/bloc/v2.2/contracts/SimpleStorage/:contractAddress/state?resolve=true

Note that everytime you run this code, a new instance of the SimpleStorage smart contract is uploaded to the chain. Using getState we can only get state for a specific instance specified by the contract address in the contract argument. The next section deals with how to retrieve state for all contracts of type SimpleStorage in one call.

Use rest.search to Retrieve State from Relational Mirror

In order to query for all SimpleStorage contracts, add the following code to the main function:

  const searchResults = await rest.search(stratoUser, contract, options);
  console.log(searchResults);

The equivalent API call is:

curl -X GET
  -H "authorization: Bearer <token>" \
  http://<strato_host_address>/cirrus/search/SimpleStorage

The above call will display all contracts of type SimpleStorage. The output should look something like:

[
  {
    "address": "9c8bd2bffc3b55265fc4b9474e2337654f8c9327",
    "chainId": "",
    "block_hash": "1bf02e95cdc7235b05544cf3862c9006eb49720081ef111bddd2d4b601a49ee9",
    "block_timestamp": "2020-05-07 00:34:10 UTC",
    "block_number": "2",
    "transaction_hash": "ad8e2f2f1af0fe016439f89ecb8d5a343c13683a2d515a588e7a8b340bdf6afb",
    "transaction_sender": "cae004d5d1feb7f2a28f408cfd722799c12f08f5",
    "transaction_function_name": "",
    "storedData": 0
  },
  {
    "address": "edeaf9914103b73e64741b8f366798aae58194c7",
    "chainId": "",
    "block_hash": "81c1087462e369acea84b76431dd0e7052ebf7868e5d3b2e21a7b2be0c5084df",
    "block_timestamp": "2020-05-07 17:25:33 UTC",
    "block_number": "4",
    "transaction_hash": "ef983f5aeba69498e8bb8cd9ed9d2546b8eafea53131d75ed5513a746f18b0c9",
    "transaction_sender": "cae004d5d1feb7f2a28f408cfd722799c12f08f5",
    "transaction_function_name": "",
    "storedData": 10
  },
  {
    "address": "63688d2e1b2b58eb576d0fa0119057be5f349524",
    "chainId": "",
    "block_hash": "d7173d96ebb97ad4accbfc20d9f29afc247ce79149c5b1e9f4fed05d36415590",
    "block_timestamp": "2020-05-07 17:26:38 UTC",
    "block_number": "6",
    "transaction_hash": "47c593042145cba3b8d546c285c2c84212f3c91997a07b80435626cac442dc5d",
    "transaction_sender": "cae004d5d1feb7f2a28f408cfd722799c12f08f5",
    "transaction_function_name": "",
    "storedData": 10
  }
]

The search method allows full sql like querying capabilities. Some possibilities are listed below.

To query for a specific SimpleStorage contract:

    const queryOptions = {
    ...options,
    query: { address: `eq.${contract.address}` }
  };
  const searchResults = await rest.search(stratoUser, contract, queryOptions);

The equivalent API call is:

curl -X GET
  -H "authorization: Bearer <token>" \
  http://<strato_host_address>/cirrus/search/SimpleStorage?address=eq.<address>

To query for contracts where block_timestamp is greater than some date:

  const queryOptions = {
    ...options,
    query: { block_timestamp: `gte.2020-05-07` }
  };
  const searchResults = await rest.search(stratoUser, contract, queryOptions);

The equivalent API call is:

curl -X GET
  -H "authorization: Bearer <token>" \
  http://<strato_host_address>/cirrus/search/SimpleStorage?block_timestamp=gte.<date>

To query for contracts where block_timestamp is greater than some date and the value of storedData is greater than equal to 10:

  const queryOptions = {
    ...options,
    query: { block_timestamp: `gte.2020-05-07`, storedData: `gte.10` }
  };
  const searchResults = await rest.search(stratoUser, contract, queryOptions);

The equivalent API call is:

curl -X GET
  -H "authorization: Bearer <token>" \
  http://<strato_host_address>/cirrus/search/SimpleStorage?address=eq.<address>&block_timestamp=gte.<date>

More details about sql like querying are available here