Advanced

Monitoring Your Channel

Accessing Channel State

Information about channel state can be accessed with getChannel(). This includes current node and client balances, availability of channel, and more.

Usage Example

Information about channel state retrieved with getChannel() can be used (for example) to stop execution if certain conditions are not met:

    var channelAvailable = (await channel.getChannel()).available
    if (!channelAvailable) {
      console.warn(`Channel not available yet.`);
      return;
    }

Event Monitoring

The Connext client is an event emitter. You can trigger actions such as transfer confirmations in your application by listening for events using connext.on(). connext.on() accepts a string representing the event you’d like to listen for, as well as a callback. The callback has a single parameter data which contains contextual data from the event. Available events are:

Channel Events:

CREATE_CHANNEL,
DEPOSIT_CONFIRMED,
DEPOSIT_FAILED,
DEPOSIT_STARTED,
WITHDRAWAL_CONFIRMED,
WITHDRAWAL_FAILED,
WITHDRAWAL_STARTED,

App Instance Events:

INSTALL,
INSTALL_VIRTUAL,
REJECT_INSTALL,
UNINSTALL,
UNINSTALL_VIRTUAL,
UPDATE_STATE,
PROPOSE_INSTALL,
REJECT_INSTALL_VIRTUAL,

Protocol Events:

PROTOCOL_MESSAGE_EVENT,

Transfer Events:

RECEIVE_TRANSFER_FAILED_EVENT,
RECEIVE_TRANSFER_FINISHED_EVENT,
RECEIVE_TRANSFER_STARTED_EVENT

Events exist in the types package as well, example:

import { ConnextEvents, DEPOSIT_STARTED_EVENT } from "@connext/types";

connext.on(DEPOSIT_STARTED_EVENT, (data) => {
  console.log("Your deposit has begun")
  const { txHash, value } = data;
  showDepositStarted(value);
  showTxStatus(txHash);
});

Controlling Deposit Flow

In some cases, an application will want to control exactly how funds are transferred to the multisig in order to add balance to a channel. The Connext client provides two methods for this, requestDepositRights and rescindDepositRights. When a client controls deposit rights in their channel, they can deposit into the multisig from any source.

An example use case is requesting deposit rights, then sending funds to a user to onboard them without requiring them to purchase ETH for gas.

Usage Example

Deposit rights can be requested by the client by using the method requestDepositRights. Once the rights have been requested, transfers to the multisig are credited to the client’s channel balance.

While deposit rights are held by the client, the node will not be able to make deposits (i.e. to collateralize the channel). The recommended approach is to rescind the rights after the transfer is received. Deposit rights are rescinded by using the method rescindDepositRights.

checkDepositRights is a convenience method to get the current state of the channel’s deposit rights.

// Transfer an ERC20 token manually
// create Ethers.js contract abstraction
const assetId = "0x..." // token address
const tokenContract = new Contract(
  assetId,
  erc20Abi,
  ethers.getDefaultProvider('homestead'), // mainnet
);
// request deposit rights
await client.requestDepositRights({ assetId });

// once rights are requested, it's safe to deposit
// this step can be completed by an external service at that point
const tx = await tokenContract.transfer(client.multisigAddress, parseEther("10"));

// wait for tx to confirm
await tx.wait();

// now it's safe to rescind deposit rights
await client.rescindDepositRights({ assetId });

Using a Custom Logger

Logger overhaul

The client accepts a logger option which must implement the ILogger interface:

interface ILogger {
  debug(msg: string): void
  info(msg: string): void
  warn(msg: string): void
  error(msg: string): void
}

Notice that console satisfies this interface on it’s own, so you could pass that in as-is:

import { connect } from "@connext/client";
const client = await connect({ logger: console, ...otherOptions });

But this is the default behavior & is what you’ll get if you omit the logger option entirely.

This option is useful if you’re using eg winston for more powerful logging or LogDNA to send logs to a remote service for further processing.

Note that winston loggers also satisfy the ILogger interface by default so you can also pass those in as-is just like console.

Creating a Custom Backup Service

Backup services store channel states on behalf of the client in case their store compromised or otherwise unavailable (i.e. for clearing localStorage in a browser, using incognito mode, or seamless multidevice channel usage). If a backup service is not available, the client will still function properly in these scenarios, but will rely on a trusted restore from the node’s version of the channel state.

Pisa hosts a backup service you can use as well, but is only currently active on rinkeby. If you would like to have backups on mainnet, you will have to create a custom implementation.

Interface

All custom backup services must implement the following interface:

export type StorePair = {
  path: string;
  value: any;
};

export interface IBackupServiceAPI {
  restore(): Promise<StorePair[]>;
  backup(pair: StorePair): Promise<void>;
}

The restore method will return an array of existing StorePair objects that should be used to populate the clients store using the set function. This function is called on connect if a problem with the store is detected on startup. Client users can also manually restore the state from back-up by calling await client.restoreState().

The backup method is called when set is called from the connextStore, here. If you are using a custom store module instead of the @connext/store package, you will want to make sure your set function includes similar logic for backing up pairs. By default, only updates to the main channel/ key will be automatically backed up.

Example Usage

To use a backup service with the client, simply instantiate the client with the backupService key populated:

/**
  * Imagine that there is an REST API available at some URL that has two endpoints, a
  * GET endpoint `restore` and a POST endpoint for `backup`.
  *
  * NOTE: This code has not been tested, and is designed to be purely illustrative.
  */

import { connect } from "@connext/client";
import { ClientOptions, IBackupServiceAPI, StorePair } from "@connext/types";
import * as axios from "axios";

class BackupService implements IBackupServiceAPI {
  private client: any;
  constructor(
    private readonly baseUrl: string,
  ) {
    this.client = axios.create({
      baseURL,
      responseType: 'json',
      headers: {
        'Content-Type': 'application/json'
      }
    });
  }

  restore = async (): Promise<StorePair[]> => {
    const res = await this.client.get("/restore");
    return res.data;
  };

  backup = async (pair: StorePair): Promise<void> => {
    await this.client.post("/backup", { pair });
  };
}

const connectOptions: ClientOptions = {
  backupService: new BackupService("https://myawesomebackup.com"),
  ethProviderUrl: "https://rinkeby.indra.connext.network/api/ethprovider",
  nodeUrl: "https://rinkeby.indra.connext.network/api/messaging",
  mnemonic:
    "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat"
};

const client = await connect(connectOptions);