Meet Strato, a concise yet powerful SDK alternative for JS Devs
Apr 01, 2022
by Buidler Labs

Hedera Strato JS is an unofficial, stratospheric (hence its name) implementation of Hedera’s JavaScript SDK looking to speed up development of web3 dApps.

Quick Start

Here is a quick start example to using Strato:

Code Snippet Background
const { session } = await ApiSession.default();
const contract = await Contract.newFrom({ code: read({ contract: ‘hello_world’) });
const liveContract = await session.upload(contract);
console.log(await liveContract.greet());

The above code will:

  • Load the .env file to bootstrap an ApiSession. It uses sensible defaults if values are not specified;

  • Create an UploadableEntity (Contract) by reading and compiling the specified Solidity file;

  • Create a LiveContract by uploading the Contract, dynamically binding its ABI to the JS instance;

  • Call the greet function available in the contract and prints the result;

For more quickstart contract samples we invite you to check our live examples in the project docs.

A Bit of History, Context and Motivation

The library was born during the Hedera - Filecoin grant received for the MyTaskbar gig-economy platform. That was the moment when the Buidler Labs dev team, the one working on the grant, first came together and saw the need for development tooling which would help speed up our efforts.

As such, Strato came into being with the hope that it will be used in all our ongoing and future Hedera work. This also includes the Headstarter Launchpad, our current drive and focus.

At its essence, Strato strives to simplify the development of dApps, bringing the experience closer to what a web3 developer is usually accustomed to, by abstracting away the verbosity of Hedera SDK JS with little to no functionality sacrificed in the process. In the end, what we get is not only a piece of code that makes dApp development super-easy and fast to write, but also something that’s tailored to benefit from the particularities of the platform. And here is where we think the library has the potential to shine.

The end-goal of the library is to fuel and power dApp development on Hedera both as a standalone endeavor and as part of a broader suite of other open-source solutions which are in the works. We hope that these products will constitute a way of helping and growing the Hedera development community by bringing quality tooling designed with the developer needs in mind.

Architecture and Features

Figure 1 shows both a layered architectural view (showing its relationship with the rest of the hashgraph’s open-source initiatives) and describes the areas we are looking to integrate with the services and 3rd party products that are being actively developed:

2022 Strato Library Image 1

Fig. 1 – The Layered View

In it, boxes with the solid outline represent the functionality offered by Hedera, the Hashgraph Consensus, the Services running on top of it, as well as the APIs, SDKs and the Mirror Node from which we can query historical data.

Boxes with dashed outline are meant to represent future, immediate, upcoming feature support for HIP-338 wallets, Hedera Consensus (Topic) and general File Services.

Lastly, blue background boxes represent the functionalities offered by Buidler Labs that integrate all the Services available on Hedera.

Currently, the well established Strato layers (mostly hard blue background) consist of the Hedera-Strato-JS library itself which is meant be used on the client side. Currently, at v0.7.3, the main functionalities of the library are to abstract the Hedera Services, like the Smart Contract Service, Token Service and File Service, into Entities and LiveEntities offering features for uploading, creating and interacting with these services. We have also set the groundwork for supporting different Wallet Providers similar to HashConnect when made available, which we plan to deliver as soon as possible following the adoption of HIP-338. Further integrations with Hedera Consensus Service are planned.

Another product which we are working on is integration of our dockerized-hedera-services repo. With an added support for the ingestion of data from the File System, we hope to be able to offer a local development environment similar to the Ganache counterpart.

The last product currently in development is called Hedera-Strato-Net which integrates the local environment and Hedera-Strato-JS to offer a Command Line Interface from where users can run and deploy their functionalities on local, non-official, Hedera environments

NFTs: A Showcase

To illustrate the utility of the Hedera Strato JS library we are showcasing an example of minting and transferring NFTs through Smart Contracts 2.0, explaining what the library is doing and how it behaves in comparison to the main SDK.

The tutorial will make use of the following smart contract called NFTShop:

Code Snippet Background
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
 
import "../../hedera/contracts/hip-206/IHederaTokenService.sol";
import "../../hedera/contracts/hip-206/HederaResponseCodes.sol";
 
// @title NFTShop - a simple minting and transferring contract
// @author Buidler Labs
// @dev The functions implemented make use of Hedera Token Service precompiled contract
contract NFTShop is HederaResponseCodes {
   // @dev Hedera Token Service precompiled address
   address constant precompileAddress = address(0x167);
 
   // @dev The address of the Non-Fungible token
   address tokenAddress;
 
   // @dev The address of the token treasury, the address which receives tokens once they are minted
   address tokenTreasury;
 
   // @dev The price for a mint
   uint64 mintPrice;
 
   // @dev The metadata which the minted tokens will contain
   bytes metadata;
 
   // @dev Constructor is the only place where the tokenAddress, tokenTreasury, mintPrice and metadata are being set
   constructor(
       address _tokenAddress,
       address _tokenTreasury,
       uint64 _mintPrice,
       bytes memory _metadata
   ) {
       tokenAddress = _tokenAddress;
       tokenTreasury = _tokenTreasury;
       mintPrice = _mintPrice;
       metadata = _metadata;
   }
 
   // @notice Error used when reverting the minting function if it doesn't receive the required payment amount
   error InsufficientPay();
 
   // @dev Error used to revert if an error occurred during HTS mint
   error MintError(int32 errorCode);
 
   // @dev Error used to revert if an error occurred during HTS transfer
   error TransferError(int32 errorCode);
 
   // @dev event used if a mint was successful
   event NftMint(address indexed tokenAddress, int64[] serialNumbers);
 
   // @dev event used after tokens have been transferred
   event NftTransfer(address indexed tokenAddress, address indexed from, address indexed to, int64[] serialNumbers);
 
   // @dev Modifier to test if while minting, the necessary amount of hbars is paid
   modifier isPaymentCovered(uint256 pieces) {
       if (uint256(mintPrice) * pieces > msg.value) {
           revert InsufficientPay();
       }
       _;
   }
 
   // @dev Main minting and transferring function
   // @param to The address to which the tokens are transferred after being minted
   // @param amount The number of tokens to be minted
   // @return The serial numbers of the tokens which have been minted
   function mint(address to, uint256 amount)
       external
       payable
       isPaymentCovered(amount)
       returns (int64[] memory)
   {
       bytes[] memory nftMetadatas = generateBytesArrayForHTS(
           metadata,
           amount
       );
 
       (bool success, bytes memory result) = precompileAddress.call(
           abi.encodeWithSelector(
               IHederaTokenService.mintToken.selector,
               tokenAddress,
               0,
               nftMetadatas
           )
       );
       (int32 responseCode, , int64[] memory serialNumbers) = success
           ? abi.decode(result, (int32, uint64, int64[]))
           : (HederaResponseCodes.UNKNOWN, 0, new int64[](0));
 
       if (responseCode != HederaResponseCodes.SUCCESS) {
           revert MintError(responseCode);
       }
 
       emit NftMint(tokenAddress, serialNumbers);
 
       address[] memory tokenTreasuryArray = generateAddressArrayForHTS(
           tokenTreasury,
           amount
       );
 
       address[] memory minterArray = generateAddressArrayForHTS(to, amount);
 
       (bool successTransfer, bytes memory resultTransfer) = precompileAddress
           .call(
               abi.encodeWithSelector(
                   IHederaTokenService.transferNFTs.selector,
                   tokenAddress,
                   tokenTreasuryArray,
                   minterArray,
                   serialNumbers
               )
           );
       responseCode = successTransfer
           ? abi.decode(resultTransfer, (int32))
           : HederaResponseCodes.UNKNOWN;
 
       if (responseCode != HederaResponseCodes.SUCCESS) {
           revert TransferError(responseCode);
       }
 
       emit NftTransfer(tokenAddress, tokenTreasury, to, serialNumbers);
 
       return serialNumbers;
   }
 
   // @dev Helper function which generates array of addresses required for HTSPrecompiled
   function generateAddressArrayForHTS(address _address, uint256 _items)
       internal
       pure
       returns (address[] memory _addresses)
   {
       _addresses = new address[](_items);
       for (uint256 i = 0; i < _items; i++) {
           _addresses[i] = _address;
       }
   }
 
   // @dev Helper function which generates array required for metadata by HTSPrecompiled
   function generateBytesArrayForHTS(bytes memory _bytes, uint256 _items)
       internal
       pure
       returns (bytes[] memory _bytesArray)
   {
       _bytesArray = new bytes[](_items);
       for (uint256 i = 0; i < _items; i++) {
           _bytesArray[i] = _bytes;
       }
   }
}

Upon creation, the contract initializes the tokenAddress (address of the token where new NFTs will be minted), tokenTreasury (address of the treasury of the token), mintPrice (the price of each NFT in tinyBars) and the metadata (the IPFS hash which will be referenced by the NFTs).

The only external function which can be called is the ‘mint’ function. This function mints the specified amount of NFTs to the address, both of which are received as arguments.

Let’s jump right in and see how we can use Strato and how it is going to help us interacting with this contract:

1. Defining the Environment Variables, Describing the Network and Operator, and Initializing the Session

For the purpose of this demo, we use the dockerized-hedera-services repo locally, where the network exposes default addresses to connect to:

Code Snippet Background
HEDERAS_NETWORK=customnet
HEDERAS_NODES='127.0.0.1:50211#3'
HEDERAS_OPERATOR_ID=0.0.2
HEDERAS_OPERATOR_KEY=91132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137

‘customnet’ is one of the network flavors supported by Strato, besides previewnet, testnet and mainnet. Each local network which is being spawned is going to contain the operator ID, that is being controlled by the same key found above.

Code Snippet Background
const nftPriceInHbar = new Hbar(10);
const amountToMint = 5;
const metadata = "Qmbp4hqKpwNDYjqQxsAAm38wgueSY8U2BSJumL74wyX2Dy";
 
const { session } = await ApiSession.default();

ApiSession.default() returns a ControlledSession, composed of an ApiSession and a ClientController.

  • The ApiSession is both the gateway and the gatekeeper to the Strato library. It is being built from environment parameters or loaded from a .env file. It allows for Uploading of UploadableEntities (eg. Contract, plain JSON objects) and the creation of CreatableEntities (Token, Account)

  • The ClientController can be used to switch the session between different accounts or network providers (this makes up for a more sophisticated use-case which, although not discussed here, will make the basis of future wallet integrations)

Console output:

Code Snippet Background
Created a new stated strato-client of type 'Hedera'

2. Defining the CreatableEntities and the UploadableEntities

Code Snippet Background
const account = new Account({ maxAutomaticTokenAssociations: 1 });
const defaultNonFungibleTokenFeatures: TokenFeatures = {
  decimals: 0,
  initialSupply: 0,
  maxSupply: 10,
  name: "hbarRocks",
  symbol: "HROKs",
  supplyType: TokenSupplyType.Finite,
  type: TokenTypes.NonFungibleUnique,
  keys: {
    kyc: null
  },
};
const token = new Token(defaultNonFungibleTokenFeatures);
const contract = await Contract.newFrom({ path: './NFTShop.sol' });

Think of Entities as offline blueprints of Live instances. Another way of thinking about them is in object oriented programing terms, where an Entity is a class while its Live counterpart is its instance:

  • Account
    • Offers the ability to use existing keys, or generates the keys by itself when being created via the session

    • It allows fine tuning similar to the contract or token.

  • Token
    • It pre-populates the keys required in a CreateTokenTransaction with the operator account.

    • It allows to fine tune all the options available when for HTS tokens;

  • Contract
    • Provides various ways to get from Solidity code to a compiled representation of the underlying contracts;

    • It allows for serialization/deserialization;

    • When uploading via an ApiSession, meta-arguments allow to customize the HFS/Contract deploy transactions, and of course, permits constructor arguments.

    • It exposes the ABI Interface, the bytecode and the name of the referenced Solidity contract

3. Making the Entities Live

Code Snippet Background
const aliceLiveAccount = await session.create(account);
const liveToken = await session.create(token);
const liveContract = await session.upload(
   contract,
   { _contract: { gas: 200_000 } },
   liveToken,
   session,
   nftPriceInHbar._valueInTinybar,
   metadata
);

Next up, we are going to create and upload the entities to the network bringing them, in essence, to Live.

All LiveEntities implement the SolidityAddressable interface. Thus, if a contract call expects an address as one of its parameters, sending a LiveEntity will have it mapped to a Solidity address allowing for the call to go through.

In the code above, creating the liveAccount instance will have a new ECDSA key generated followed by a new account created on the network.

Using some TokenFeatures, a liveToken is also created on the Hedera network.

The liveContract is being created using meta-arguments for gas, along with the required constructor arguments, the address of the newly created token, the address of the treasury account, the price of an NFT and the metadata for the NFT.

By uploading the contract through the ApiSession, the resulting instance will allow for fluent on-chain contract-calls as the ABI interface is dynamically binded on the JS instance. Afterwards, all the function calls which are being made are agnostic to the method’s mutability grade as it abstracts away the ContractCallQuery and ContractExecuteTransaction calls.

Console output:

Code Snippet Background
Creating a new Hedera Account
A new 'ECDSA' key has been created: 3030020100300706052b8104000a04220420af781dcc513e3da1607fa7194c0c2746caec61be485517d757a01381a954e951 . Copy it since this is only time you'll see it.
Successfully created Account id 0.0.1450
Creating a new Hedera Token
Successfully created Token id 0.0.1451
Uploading a new NFTShop-Contract to Hedera File Service (HFS).
Uploaded content to HFS resulting in file id 0.0.1452
Appending the remaining content with a total of 1 file-append transactions.
Done appending. Content has been successfully uploaded and is available at HFS id 0.0.1452
Successfully created a NFTShop-Contract id 0.0.1453.

4. Assigning Supply Control to the Live Contract

Code Snippet Background
liveToken.assignSupplyControlTo(liveContract);

We update the token to allow for the address of the liveContract to be able to control the supply. This is required if we want to mint through the contract.

5. Registering for LiveContract Events

Code Snippet Background
liveContract.onEvent("NftMint", ({tokenAddress, serialNumbers}) => {
  console.log("NFTs minted", tokenAddress, serialNumbers);
});
 
liveContract.onEvent("NftTransfer", ({tokenAddress, from, to, serialNumbers}) => {
  console.log("NFTs transferred", tokenAddress, serialNumbers, from, to );
});

The LiveContract makes use of the ABI Interface to parse the contract call response for events, to which we can register by giving the name of the event of interest and the callback to be executed when the event gets fired.

6. Calling the Mint Function on the Live Contract

    Code Snippet Background
    const serialNumbers = await liveContract.mint(
       {
           amount: new Hbar(nftPriceInHbar.toBigNumber().toNumber() * amountToMint).toBigNumber().toNumber(),
           gas: 1_500_000
       },
       aliceLiveAccount,
       amountToMint
    );
     
    console.log("Serial numbers minted by the smart contract: ", serialNumbers.map(item => item.toNumber()));
    

    In the mint call above, we customize the contract call by setting meta-arguments, for payable amount, being the price of the NFTs times the amount we want to mint, and the gas cost of the transaction. The last two arguments are the ones received by the Solidity function, being the account of Alice and the number of NFTs to mint.

    Here we can also notice that the dynamically injected ABI functions know how to handle Strato LiveEntities and to map them to a Solidity address.

    Because we previously subscribed to the events on the LiveContract, calling the method triggers these events which are being handled by our registered callbacks.

    Also, being a function which returns a vector of int64 values, these are being mapped from the received receipt.

    Console output:

    Code Snippet Background
    NFTs minted, ["0x00000000000000000000000000000000000005b7",[1,2,3,4,5]]
    NFTs transferred, ["0x00000000000000000000000000000000000005b7",[1,2,3,4,5],"0x0000000000000000000000000000000000000002","0x00000000000000000000000000000000000005B6"]
    2022-02-16T18:25:23.899Z - info: Serial numbers minted by the smart contract, [[1,2,3,4,5]]
    

    7. Getting Info about Alice's Account and the Contract

    Code Snippet Background
    const aliceInfo = await aliceLiveAccount.getLiveEntityInfo();
    const contractInfo = await liveContract.getLiveEntityInfo();
     
    console.log(`Number of NFTs owned by Alice: ${aliceInfo.ownedNfts.toNumber()}`);
    console.log(`HBar balance of contract: ${contractInfo.balance.toBigNumber().toNumber()}`);
    

    As a last step, we want to get the info for Alice and the contract, to check that the contract has done what we expected.

    We can see that:

    1. the number of NFTs now owned by Alice is equal to the amountToMint defined at step one and used when calling the mint function and

    2. the HBar required for the minting of the 5 NFTs has been transferred to the contract balance.

    Console output:

    Code Snippet Background
    Number of NFTs owned by Alice: 5
    HBar balance of contract: 50
    

    And That’s It!

    In the above showcase we have seen how we can use Strato to interact with the Hedera Services, how to create accounts, tokens, contracts and how to interact with each of them. For a quick dive into a working version of the above code, please head over to Github, clone and run the hedera-strato-demo repo. Or, better yet, through the magic of Gitpod, how about auto-doing all these steps directly in browser? Just click here and let magic do its work.

    Although Strato is currently in alpha, it’s at least usable to play around with as it's being actively developed. Expect for more functionalities and integrations to be provided in upcoming releases.

    If you do decide to give it a spin and have any kind of feedback (good or bad), we would love to hear all of it on our Discord channel.

    Interested to find more about Buidler Labs? Come check our GitHub repos. Of course, any contributions and/or feedback are highly welcomed and appreciated.

    If you want us to prioritize a feature, head over to our issues page and either bump an existing ticket or create a new one and make your case. We take each one into serious consideration.

    With love from Buidler Labs team