Implement Custom Provider

Build custom Providers that provides an API to access and process information provided by services.

As of writing (Dec 2022), Mesh offers three options: Blockfrost, Tangocrypto, or Koios (see a list of Providers) . These blockchain providers allow developers to access the Cardano blockchain and retrieve intricate data. For example, they can be used to query the UTXO of a smart contract and use it as part of a transaction, or to submit a signed transaction to the chain.

You can customize a provider to utilize GraphQL, cardano-cli, or websocket with Mesh SDK. Whatever the query method used to obtain the data, it will work perfectly with Mesh SDK so long as the output of the function is compatible with the interface.

This guide will show us how to make a custom provider and how to integrate it with Mesh so that it works in tandem with the transaction builder.

How Does It Work?

JavaScript interfaces are structures that define the interface of an application: they are used to define the syntax for classes to follow. Thus, any classes which are based on an interface must abide by the structure laid out in the interface.

All providers have one or more interface(s). For example, the KoiosProvider Class implements the IFetcher and ISubmitter interfaces, thus KoiosProvider needs to strictly conform to the structure of these two interfaces.

IFetcher and ISubmitter are implemented in the KoiosProvider class using the implement keyword:

export class KoiosProvider implements IFetcher, ISubmitter {}

To see the latest up-to-date list of interfaces used by Mesh, visit the GitHub repo, packages/module/src/common/contracts.

To create a custom provider class, one must create functions with the same name, input parameters, and return type as the list of defined methods for each interface. Doing so will allow the functions to work as expected when building transactions and any of the other many functions provided in Mesh.

For example, as of writing, IFetcher has 6 functions (see packages/module/src/common/contracts/fetcher.ts for latest implemention):

import type { AccountInfo, AssetMetadata, Protocol, UTxO } from '@mesh/common/types';

export interface IFetcher {
  fetchAccountInfo(address: string): Promise<AccountInfo>;
  fetchAddressUTxOs(address: string, asset?: string): Promise<UTxO[]>;
  fetchAssetAddresses(asset: string): Promise<{ address: string; quantity: string }[]>;
  fetchAssetMetadata(asset: string): Promise<AssetMetadata>;
  fetchHandleAddress(handle: string): Promise<string>;
  fetchProtocolParameters(epoch: number): Promise<Protocol>;
}

As such, KoiosProvider must implement these functions as defined in IFetcher.

Implement Your Own Provider

If you want to begin utilizing your own provider, looking at one of the existing providers is the ideal way to get started. Visit the GitHub repo, packages/module/src/providers to see a list of providers.

This code base below can be used as a starting point:

import { IFetcher, ISubmitter } from "@mesh/common/contracts";
import { parseHttpError } from "@mesh/common/utils";
import type {
  AccountInfo,
  AssetMetadata,
  Protocol,
  UTxO,
} from "@mesh/common/types";

export class NAMEProvider implements IFetcher, ISubmitter {
  constructor(network: "") {
    // init variables and other Javascript libraries needed
  }

  async fetchAccountInfo(address: string): Promise<AccountInfo> {
    try {
      // return <AccountInfo>{
      //   ...
      // };
    } catch (error) {
      throw parseHttpError(error);
    }
  }

  async fetchAddressUTxOs(address: string, asset?: string): Promise<UTxO[]> {
    try {
      // return <UTxO[]>[
      //   ...
      // ];
    } catch (error) {
      throw parseHttpError(error);
    }
  }

  async fetchAssetAddresses(
    asset: string
  ): Promise<{ address: string; quantity: string }[]> {
    try {
      // return AssetAddresses;
    } catch (error) {
      throw parseHttpError(error);
    }
  }

  async fetchAssetMetadata(asset: string): Promise<AssetMetadata> {
    try {
      // return <AssetMetadata>[
      //   ...
      // ];
    } catch (error) {
      throw parseHttpError(error);
    }
  }

  async fetchHandleAddress(handle: string): Promise<string> {
    try {
      // return handleAddress;
    } catch (error) {
      throw parseHttpError(error);
    }
  }

  async fetchProtocolParameters(epoch = Number.NaN): Promise<Protocol> {
    try {
      // return <Protocol>{
      //   ...
      // };
    } catch (error) {
      throw parseHttpError(error);
    }
  }

  async submitTx(tx: string): Promise<string> {
    try {
      // if (status === 200)
      //   return txHash;
    } catch (error) {
      throw parseHttpError(error);
    }
  }
}

However, please note that it may no longer be valid when the interface is updated. It is also important to note that the interface you require may not be IFetcher or ISubmitter, but rather other interfaces, depending on the purpose of the provider you are implementing.

Implement Constructor and Functions

To start, we want to define the constructor.

A constructor is a special function that creates and initializes a class. This constructor gets called when an object is created using the new keyword. The purpose of a constructor is to create a new object and set values for any existing object properties.

When setting up a provider, it is usually necessary to provide some basic information, such as which network it should be connected to and if an API key is required.

In the case of KoiosProvider, we want the users to define the network and version (optional):

private readonly _axiosInstance: AxiosInstance;

constructor(network: 'api' | 'preview' | 'preprod' | 'guild', version = 0) {
  this._axiosInstance = axios.create({
    baseURL: `https://${network}.koios.rest/api/v${version}`,
  });
}

This constructor initializes the Axios instance, with the parameters provided by the user.

Next, we can define each function that is required by the interface. To do this, you must understand the answers to the following:

  • how to query the blockchain provider?
  • what are the input parameters of the interface?
  • what are the input parameters needed to query the blockchain provider?
  • what are the expected outputs of the interface?
  • what is being returned by the blockchain provider?

By knowing the inputs and outputs of both the interface and the blockchain provider, one can create the functions that map the data correctly from the blockchain provider to the interface's required data type.

For example, below we have implemeted the fetchProtocolParameters() for KoiosProvider to map the responses returned from Koios, transforming the output into the required Protocol data type. This function is used for fetching protocol parameters:

async fetchProtocolParameters(epoch: number): Promise<Protocol> {
  try {
    const { data, status } = await this._axiosInstance.get(
      `epoch_params?_epoch_no=${epoch}`,
    );

    if (status === 200)
      return <Protocol>{
        coinsPerUTxOSize: data[0].coins_per_utxo_size,
        collateralPercent: data[0].collateral_percent,
        decentralisation: data[0].decentralisation,
        epoch: data[0].epoch_no,
        keyDeposit: data[0].key_deposit,
        maxBlockExMem: data[0].max_block_ex_mem.toString(),
        maxBlockExSteps: data[0].max_block_ex_steps.toString(),
        maxBlockHeaderSize: data[0].max_bh_size,
        maxBlockSize: data[0].max_block_size,
        maxCollateralInputs: data[0].max_collateral_inputs,
        maxTxExMem: data[0].max_tx_ex_mem.toString(),
        maxTxExSteps: data[0].max_tx_ex_steps.toString(),
        maxTxSize: data[0].max_tx_size,
        maxValSize: data[0].max_val_size.toString(),
        minFeeA: data[0].min_fee_a,
        minFeeB: data[0].min_fee_b,
        minPoolCost: data[0].min_pool_cost,
        poolDeposit: data[0].pool_deposit,
        priceMem: data[0].price_mem,
        priceStep: data[0].price_step,
      };

    throw parseHttpError(data);
  } catch (error) {
    throw parseHttpError(error);
  }
}

To complete implementation of your custom provider, simply do the same for every function specified by the interface and test that they work as expected.

If you think that the provider you have implemented will benefit the Cardano developer community, create a pull request.