Skip to content

Asset Framework Application Development Guide

This page serves to guide developers through the various components that are included in and generated by the Asset Framework. This will allow developers to customize the app according to their needs following commonly used patterns. This guide assumes you have already generated an app with the Asset Framework.

Generated Project Overview

The Asset Framework generates a project capable of tracking the ownership and history of your assets.

It contains a backend JavaScript API, a React UI, and a Nginx proxy server to easily allow the API and UI to communicate with each other.

It also contains a host of scripts that allows it to be easily deployed and started in a dockerized environment, allowing to be run almost anywhere Docker can.

The backend directory contains the primary code of the application, from generated Asset smart contracts, the API server layer, a suite of middleware to facilitate the connection between the application API and the STRATO API, and wide range of helpers and utilities that provide useful smart contracts, constants, etc.

The ui directory is a standard React web application built using primarily out-of-the-box solutions like React state and context hooks, and the Shopify Polaris component library.

Smart Contracts

The bulk of most of the project's smart contracts and smart contract middleware logic is located within the backend/dapp/ directory. Each sub-directory is named after the primary contract that the code in it manages. For example the Car asset contract and middleware is located in the backend/dapp/Car/ directory, and the Private Dapp Realm is in backend/dapp/dapp.

You can also find many useful contracts in the backend/blockapps-sol/ directory. Import and inherit these base class libraries into your own contracts for easy to use functionality. Some of the contracts this library includes are:

  • User Membership Management
  • User Permission Management
  • Finite State Machine implementation (for use with lifecycles and status tracking)
  • HTTP error code constants

Since the framework uses blockapps-rest library, users can include import statements at the top of their smart contracts to add contract source code to the current contract when the contract is processed by the BA-rest library before being sent to the STRATO API. Filepaths may be a relative path from the current file, or an absolute path from the backend/ directory.

Dapp Contract/Privacy Realm

The Asset Framework generates a Dapp that is deployed to its own Private Dapp Realm. This serves as the launching point for all other contracts to be hosted in your app, such as permission managers, or assets, which are created as separate private shards. The deployment and contract code is located in the backend/dapp/dapp directory.

By default, the Dapp contract is very light, however as you require more app-level contracts, references can be added here to track those contracts. You may also add more app level functionality to this contract as needed. The Dapp contract also serves as the Governance contract for the Dapp Privacy realm, and thus contains the necessary basic functions to allow members to be added and removed from the realm.

As an example, if you were to add a permission manager to your app, you would create a new instance of it directly within the Dapp constructor, as follows:

import "/dapp/permissions/contracts/PermissionManager.sol"

contract Dapp {

  PermissionManager public permissionManager;

  ...

  constructor(...) {
    permissionManager = new PermissionManager();
  }
}

This will allow any other contract like Assets on their own shards, to access this Dapp's permission manager.

Asset Contracts

The following section briefly describes the generated output of Asset contracts from the Asset Framework based on the inputted data model. As an example, we will use the following simple “Car” data model to demonstrate the Framework’s capabilities.

Car VIN Color Model Weight
- text text text number

Assets are each created as the first (governance) contract of a new Privacy Shard (formerly "private chain"), this way their shard can individually be shared to different parties. Therefore only the members of the shard have knowledge of this asset’s existence. Typically only contracts directly related to the specific asset instance are posted to a specific asset shard. Because each asset exists on its own shard, this approach is called using “Asset Shards”.

State Variables

The Car contract will contain the state variables:

string  public vin;
string  public color;
string  public model;
int     public weight;

In order to track the ownership of an asset, each asset will also contain the fields:

address owner;
string appChainId;
string ownerOrganization;
string ownerOrganizationalUnit;
string ownerCommonName;

that represent the blockchain address and X.509 identity of the current user that owns the asset. The meaning of “ownership” in your app can be flexible and does not necessarily mean that the user truly “owns” the asset, but it could also represent the user that “manages” the asset.

Constructor

The Asset Contract constructor takes all of its user defined state fields to be provided as arguments. The asset owner will default to the tx.origin.

constructor(
    string _appChainId,
    string _vin,
    string _color,
    string model,
    int _weight
) {
    owner = msg.sender;
    appChainId _appChainId;
    vin = _vin;
    color = _color;
    model = _model;
    weight = _weight;
    ...
}

Field Updates

User provided state variables are updated through a single update function that takes a scheme value and all the new values for the asset.

function update(...fields, uint _scheme) returns (uint);

The scheme value is an integer bitmap of the position of which fields should be updated from the provided arguments. For the Car example, the vin field is the first (zeroth) state variable, and the weight is the last/third variable. In order to update just the color and model fields, we would pass in a value to the scheme argument that has a 1 in the 1st and 2nd positions, and zeros everywhere else:

0110 == 6

The backend API of the Asset Framework provides an easy utility to call the update function with only the new fields in a provided updates object, and it will calculate the correct scheme value and default values (for non-updated fields) to provide the function arguments.

Example updates object corresponding with the previous example field updates:

{
    color: 'red',
    model: 'Corolla',
}

Since the Framework provides a simple, generic update function, you will most likely want to change or completely remove this function so that it matches your app's required logic. Common patterns include restricting the call to certain users, only updating a subset of fields at a given time, or having another state variable be updated based on the arguments provided.

Ownership Transfer

The asset will have a transfer ownership function with the following signature:

transferOwnership(address _addr);

This function serves to facilitate the transfer of assets between users by address. It will set the owner to the provided address (_addr), given that it is an address with a valid identity. Since the asset is on its own shard, this function will also add the new owner to this shard, and remove the current owner, so they no longer have access to the asset. Developers can customize this behavior to suit their needs for what “transferring ownership” should mean in their app. Custom behaviors could include not wanting to remove the current owner from the shard upon transfer, or requiring a confirmation from the other party before it is fully transferred.

Asset Lifecycle

Tracking an asset's lifecycle is another common application pattern needed. For example, an asset might initially be created, then prepared, shipped, and finally delivered. Assets in the Asset Framework do not implement lifecycle tracking, however the framework provides some library contracts to make state management easier to implement out of the box. This is located in the backend/blockapps-sol/fsm directory.

Further References

For more information on SolidVM language features, refer to the SolidVM documentation: https://docs.blockapps.net/strato-vm/solid-vm/

For more information on using Private Shards and programming contracts on private shards, refer to the Private Shards (Chains) documentation: https://docs.blockapps.net/private-chains/

For more information on Identities on STRATO Mercata, refer to the Verified X.509 Identities Documentation: https://docs.blockapps.net/identity-x509

Accessing Asset Contract Data

Contract data should be accessed using the STRATO Cirrus API which stores the contract’s data in SQL tables for lightning fast access times. No transactions are needed to get a contract’s data.

An Asset is stored in a table with the name:

<Dapp-Creator-Org>-<Dapp-Name>-<AssetName>

Where the Dapp-Creator-Org prefix is the name of the organization of the user (developer) that created the Dapp. By default, the Dapp Creator's Organization and Name and is stored in the Dapp contract itself and in the deployment file generated. This way the value can be deterministically discovered by other users.

So for the Car asset, a query for a specific Car with the VIN of "12345" would look like:

GET /cirrus/search/BlockApps-Dapp-Car?vin=eq.12345

Standard SQL query terms can be added onto the request for more granular requests.

See the Cirrus API documentation here.

API/Dapp Middleware

The Asset Framework will generate basic API endpoints corresponding to common API functionality and the Asset Contract’s function calls. Along with each Asset defined are a collection of JavaScript functions that translate the API methods to STRATO API calls. This Javascript layer is called the Dapp middleware. The API listens for a request on a defined endpoint, and executes the prescribed Dapp middleware.

Dapp Middleware

The Dapp middleware associates logical abstractions of methods such as “creating a car” to the technical instructions needed to execute that operation on STRATO.

Below is a rough mapping of the logical operations to the technical operations for common methods:

Create an asset

  1. API handler validates arguments according to required Asset contract constructor arguments that must be provided by the user.
  2. Derive/fill in any generated arguments for asset creation such as IDs, etc.
  3. Check permissions of the user in the Dapp permission manager.
  4. Create a new private chain using the Asset contract as the governance contract. The Asset contract is predefined in the application backend code. Constructor arguments are provided from the API request.
  5. API responds according to success/failure of argument validation, permissions, and chain creation.

Update an asset’s data by ID

  1. API handler validates arguments according to required Asset contract function arguments that must be provided by the user.
  2. Find the asset’s contract address and chain ID if not provided in the API request.
  3. Call relevant update function on the asset smart contract (the contract that exists at address:chainId)
  4. API responds according to success/failure of argument validation, function call result, etc.

Get an individual asset by ID

  1. Query the STRATO Cirrus API on the Asset’s table, filtering by the provided asset ID, and any other filters provided in the API request.

Get an individual asset’s history by ID

  1. Query the STRATO Cirrus API on the Asset’s history table, filtering by the provided asset ID, and any other filters provided in the API request.

*Note this endpoint requires that the contract’s history be enabled upon initial creation.

Get all available instances of an asset type

  1. Query the STRATO Cirrus API on the Asset’s table, filtering by any parameters provided in the API request.

The Dapp middleware can be expanded upon to perform more complex operations, such as multiple STRATO API calls in sequence or as result of another call. However in general it is best practice to put as much application logic as possible inside an Asset’s smart contract methods, rather than in the Javascript backend API code, in order to ensure all application behavior is distributed on the blockchain.

Dapp Function “Binding”

The Asset Framework employs a technique of “binding” functionality to a specific Javascript object. This is also called “closure” in Javascript, where a function returns an object with other functionality attached to it, however those attached functions use values provided from the first-level function, not provided directly as function arguments.

The Asset Framework uses this to bind generic STRATO functionality, like calling a named smart contract function, to a specific instance of a contract.

For example, let's use the transferOwnership function on the Car smart contract:

Initially a generic transferOwnership JS function is defined to call the function of the same name through a STRATO transaction API call.

async function transferOwnership(admin, contract, options, newOwner) {
  const callArgs = {
      contract,
      method: 'transferOwnership',
      args: { _addr: newOwner },
    };
    const transferStatus = await rest.call(user, callArgs, options);
}

Note in this function the only things hardcoded are the function name, and function argument names. Then a generic function call is done through the BlockApps Rest JS library call function.

The contract object, which contains the contract name and address, is provided to us via arguments, so it must be provided by the function’s caller.

Next we “bind” a specific instance of a Car contract to the previous functionality:

function bind(admin, _contract, options) {
  const contract = {...contract}
  contract.transferOwnership = async function(newOwner) {
    return transferOwnership(admin, contract, options, newOwner)
  }
  return contract 
}

This gives us a contract object that has a function transferOwnership that we can easily call with just the newOwner argument, and not all of the technical details like the user calling it, or the contract address.

The bind function is called at the “start” of an interaction with a contract, such as during an API request that provides the contract address to be called. The provided address is passed to the bind function, and then the returned contract object is used to perform any required functionality.

The Asset Framework binds all Asset and basic Dapp functionality to a dapp object, so that the dapp object has attached functions like updateCar as well as transferOwnershipCar.

Adding Functionality to the Dapp

Whenever you add a new function to your contract, make sure to add the necessary middleware handler functions so that it can be invoked from the primary dapp object. Follow the pattern as referenced above for functions like transferOwnership.

Summary of all necessary actions needed:

  1. Define new function on your Asset Smart Contract (.sol file)
  2. Define a wrapper function in .js that calls the function through BlockApps Rest SDK, provided a contract address and arguments
    1. Add this function to the .js bind function so it is a method of the returned contract object.
    2. Export this function from the file
  3. Add a wrapper function to the main contract object returned from the bind function in dapp.js - this function will usually take a unique asset ID or address so that the app can either find the contract’s address based on its unique asset ID OR call the function directly using the provided address.
  4. This function can now be called from a bound dapp object, such as from the API layer.

API Endpoints

The following endpoints are generated for the Car asset are:

GET /car
Gets all cars

GET /car/:address
Gets a single car based on its Smart Contract address

GET /car/:address/audit
Gets the historical record of a single car based on its Smart Contract address

POST /car
Creates a car

PUT /car/update
Updates an existing car
The address of the car contract is provided in the request body

POST /car/transferOwnership
Transfers the ownership of the car to another user

The Framework will also generate basic endpoints for querying user identities on Mercata through various /user/ endpoints.

The full list of endpoints generated by the app are available to view in the backend/api/v1/endpoints.js file.

Managing Endpoints

If you have added a new function to your Dapp, or you simply need a new type of endpoint in your API, use this section as a helpful reference.

All of the server's endpoints are located in the backend/api/v1/endpoints.js file. Here they are separated by the entity they manage. Each entity object contains a prefix, typically the entity's name, and the names for each endpoint available for that entity. These names correspond with the actual strings that are the API endpoints. The API uses Express.js as its server framework, so refer to that documentation for endpoint naming and special matching characters. Add an entry here with the corresponding entity to start adding a new endpoint.

const MyEntity = {
  prefix: '/myEntity',
  ...,
  postMyFunction: '/myFunction',
}

Next, define a handler function for the API in the entities API controller. This defines how the API should respond when the endpoint is called. Navigate to backend/api/v1/<entity>/<entity>.controller,js, and a a new static function to the <entity>Controller class with a relevant name. The function should take an Express request, response, and next parameters.

class MyEntityController {

  ...

  static myFunction(req, res, next) {
    try {

      // handle the response for this endpoint
      const { dapp, body } = req
      const validationError = validateMyFunction(body)
      const result = await dapp.myFunction(body)
      return res.json(result).status(200)
    } catch (err) {
      next(err)
    }
  }

  static validateMyFunction(args) {
    const myFunctionSchema = Joi.object({
      ...schema
    });

    const validation = myFunctionSchema.validate(args);

    if (validation.error) {
      throw new rest.RestError(RestStatus.BAD_REQUEST, 'My Function Argument Validation Error', {
        message: `Missing args or bad format: ${validation.error.message}`,
      })
    }
  }

  ...
}

In this function, define how the server should handle the request, typically using a function already bound to the dapp object. This bound function takes care of the necessary technical details of completing the action on the Mercata Blockchain. Then this handler function should respond with the necessary data and HTTP response code as necessary from the returned value of the dapp function. It is in this file where body and parameter validation happens using the joi validation library. A separate body validation function is typically made for each handler function. This ensures that the Dapp functions receive the correct arguments and types.

Finally we attach the server to the handler using the defined endpoint name.In backend/api/v1/<entity>/index.js, add the HTTP route to the server, using the endpoint's route and handler as defined in previous steps.

router.post(
  // API endpoint to use
  MyEntity.myFunction,
  // ...middleware functions
  // the controller function to use
  MyEntityController.myFunction
)

API Request Middleware

The Asset Framework provides a set of Express.js middleware that provides basic functionality to make writing the core functionality of your app easier without having to worry about as much technical infrastructure and boilerplate.

Authentication Handler

Provides authentication on all requests with the configured OAuth provider. Also fetches the user’s token and STRATO address to use for STRATO API calls. The configured OAuth provider must necessarily be the same as the one configured with the STRATO node your app is connected to.

Load Dapp Handler

Creates a dapp JS object which contains the logical methods as functions. The dapp is “bound” to a specific Dapp deployment so that the caller of these functions does not have to provide the deployment information every call.

Error Handler

Handles errors thrown from Dapp middleware functions and translates them into corresponding HTTP responses.

While not provided out of the box, it is also a common pattern to include a “membershipHandler” middleware method that ensures that the request caller has a valid membership in the application. This is only relevant when your app has a concept of membership

UI

The UI generated by the Asset Framework provides all the necessary pages to display and manage the assets in your application. The UI is a standard React Application that uses minimal third-party libraries such as redux or axios.

This includes:

  • Left menu that allows user to select which asset type to view
  • Each asset view displays a table of those assets
  • Select a single asset to transfer ownership to another user on Mercata
  • Clicking on a row allows you to view an asset’s details
  • Update a single asset’s details, if you are the owner
  • Asset details also include a historical table of all of the asset’s data updates
  • Sign in/sign out through the configured identity provider.
  • Top right avatar allows users to signout from the application

The framework uses the Shopify Polaris component library for rapid and easy development of consistent components. See the library here: https://polaris.shopify.com/

App Deployment

In order to use your application, it needs to first be deployed to the Mercata network, and finally the necessary services need to be started so that the UI and API are accessible. For applications that are run in multi-node context (i.e the app must be accessible from different servers, and the Dapp Privacy realm is shared amongst multiple nodes on the Mercata network), you will typically only need to deploy the Private Dapp Realm once (from a "bootnode"), and any secondary nodes deployment process will be simply checking to make sure they have access to the Dapp realm, and posting any Node or Org specifc data needed to onboard a new Node to the Dapp.

Your application can be deployed and started from two different environments: ++locally++ or in Docker (Dockerized application).

The Asset Framework's included README has detailed steps for each deployment option, however here are general app deployment patterns that one must usually follow:

  • Provide configuration information to the app, instructing it what Mercata node it is connecting to, what OAuth provider to use, etc.
  • Run a pre-defined deployment script that creates the Private Dapp Realm and initializes the Dapp onto the network.
  • Start the Nginx proxy server
  • Start the backend API server
  • Start the UI

Now the application's UI will be accessible from the browser, which will allow you to make real transactions onto the Mercata Network via the backend API.