Connext Documentation

Connext is an infrastructure layer on top of Ethereum that lets Ethereum wallets, applications and protocols do instant, high volume transactions with arbitrarily complex conditions for settlement. Projects that integrate Connext can enable users to batch up Ethereum interactions to get more transaction throughput per block.

These docs cover both the background of Connext and how to get started integrating Connext into your Ethereum wallet or application.


Community

Join the community to meet the team, discuss development, and hang out!


Contents

Introduction

How to get started

If you’re interested in:

  1. Integrating state channels into your dApp or wallet, see Getting Started

  2. Running your own node, see Running your own Node

  3. Helping build Connext, see our Contributor docs

What is Connext?

Connext is an infrastructure layer on top of Ethereum and any other EVM blockchains that enables instant, high volume, p2p transfers on and across chains. The goal of the Connext Network is to abstract away the complexities and cost of interacting with a given blockchain for p2p usecases. Projects in the space are already using Connext to enable instant wallet to wallet transfers, enable p2p micropayments, power marketplaces, and build games on the Ethereum mainnet!

Connext is built using state channels. State channels enable users to batch up normal Ethereum transactions without needing to trust intermediaries. State channels do not require any external custodians or add any additional functionality to Ethereum, they simply allow existing Ethereum interactions to occur more quickly and at lower cost by putting more interactions into each block.

If you’re unfamiliar with terms like smart contract and private key, please refer to a more general developer guide such as this one, compiled by the Ethereum community, before continuing.

State Channel Basics

State channels allow many off-chain commitments to be aggregated into just a few onchain transactions:

  1. A user opens a channel by depositing their money into a multisignature smart contract with a counterparty. Note that the smart contract runs entirely on the blockchain and so the user remains entirely in custody of their own funds.

  2. The user transacts by sending signed settlement instructions for how the counterparty can retrieve funds from the smart contract. Because the instructions give the counterparty irrevocable access to part of the funds, the user can make multiple “updates” to their balances while only paying the fees of the initial deposit.

  3. When either party is done transacting, they can take the latest update to the smart contract and unlock the finalized funds.

  4. Because there can be arbitrary conditionality to the settlement instructions, the above relationship can be extended to allow users to transact with more parties. For instance, if Alice wants to pay Charlie but only has a channel with Bob, Alice can pay Bob conditionally based on whether he pays Charlie. If the latter part of the transaction is not completed, then Alice’s transaction to Bob will not occur either - this makes transactions atomic and noncustodial.

  5. This arbitrary conditionality also applies to the activities that Alice and Bob can do - anything from simple transfers of Ethereum tokens, to prediction markets, auctions, and even chess games.

If you’re looking for more information, here are a few digestible resources on how they work:

Status

Connext’s Indra network is live on the Ethereum mainnet, rinkeby testnet, and support for a few other EVM-based chains is coming soon.

The current network features a hub-and-spoke topology over which transactions are routed. Anyone can run a Connext node as a service provider to connect users to each other. Connext clients open channels to this node and can then make commitments to any other client connected to the same node. In Connext, users’ funds annd transactions are entirely noncustodial - the node never holds your value at all.

Note that this iteration of Connext is not censorship resistant. It is currently possible for a node to block a user’s commitments to other users, thereby stopping the user from making instant/offchain transfers. Even if this were to happen, users are always able to withdraw their funds to any account or wallet on the base blockchain. For a detailed overview of the trust assumptions and limitations that exist at present, please read System Limitations.

The next major iteration of Connext will feature routing state updates between nodes, allowing clients to connect to many nodes concurrently, and the ability for anyone to connect their own node to the Connext Network.

The Basics of using a Connext Client

Like web3.js or ethers, the Connext client is a collection of libraries that allow you to interact with a local or remote Connext node.

This quickstart will guide you through instantiating the Connext client with a randomly generated private key in a web environment to get basic Connext functionality (deposits, swaps, transfers, withdrawals) working as quickly as possible.

Instantiating with a private key should not be used in production environments - once you get through this guide, we recommend looking through the React Native Integration guide for better patterns.

We will connect to a testnet (Rinkeby) node hosted at https://rinkeby.indra.connext.network/api/messaging using the Connext client. If you don’t have any Rinkeby ETH, we recommend you get some from a faucet before continuing with this guide.

Setting up a Channel

First install the client library in your project root directory using NPM or Yarn:

npm install --save @connext/client

# OR

yarn add @connext/client

Then import it and setup a channel by calling connext.connect()

import * as connext from "@connext/client";

const channel = await connext.connect("rinkeby")

This will create a channel for you using a private key randomly generated from inside the client.

If you’re using React, it can be helpful to set up your channel and save the instance to state in componentDidMount (or even better, in a React hook).

Depositing

After instantiating and starting Connext, you can deposit into a channel with channel.deposit with any Eth or ERC20 token. The default .deposit method will attempt to deposit value from the channel’s signer address, found using await channel.signerAddress(). Because of this, if you’re trying to deposit a token, ensure that the user has sufficient Eth in their signer address to pay gas for the deposit transaction.

// Making a deposit in ETH
import { AddressZero } from "ethers/constants";
import { parseEther } from "ethers/utils";

const payload: AssetAmount = {
  amount: parseEther("0.1").toString(), // in wei/wad (ethers.js methods are very convenient for getting wei amounts)
  assetId: AddressZero // Use the AddressZero constant from ethers.js to represent ETH, or enter the token address
};

channel.deposit(payload);

You can also deposit directly into the channel by bypassing the signer address with some additional work. For more info, see Controlling Deposit Flow

Swaps

Our hosted testnet node collateralizes test ETH and test Dai and allows you to swap between them in-channel. Say hello to instant and free exchanges. Exchange rates are pulled from the Dai medianizer.

Make an in-channel swap:

// Exchanging Wei for Dai
import { AddressZero } from "ethers/constants";
import { parseEther } from "ethers/utils";

const payload: SwapParams = {
  amount: parseEther("0.1").toString() // in wei (ethers.js methods are very convenient for getting wei amounts)
  toAssetId: "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359" // Dai
  fromAssetId: AddressZero // ETH
}

await channel.swap(payload)

Making a Transfer

You can now instantly make a transfer to any other client connected to the testnet node. Making a transfer is simple! Just call channel.transfer(). Recipient is identified by the counterparty’s public identifier, which you can find with channel.publicIdentifier.

// Transferring ETH
import { AddressZero } from "ethers/constants";
import { parseEther } from "ethers/utils";

const payload: TransferParams = {
  recipient: "indraZTSVFe...", // counterparty's public identifier
  meta: { value: "Metadata for transfer" }, // any arbitrary JSON data, or omit
  amount: parseEther("0.1").toString(), // in wei (ethers.js methods are very convenient for getting wei amounts)
  assetId: AddressZero // ETH
};

await channel.transfer(payload);

Withdrawing

Users can withdraw funds to any recipient address with channel.withdraw(). The specified assetId and amount must be part of the channel’s balance.

// Withdrawing ETH
import { AddressZero } from "ethers/constants";
import { parseEther } from "ethers/utils";

const payload: WithdrawParams = {
  recipient: // defaults to signer address but can be changed to withdraw to any recipient
  amount: parseEther("0.1").toString() // in wei (ethers.js methods are very convenient for getting wei amounts)
  assetId: AddressZero
}

await channel.withdraw(payload)

React Native

If you are interested in using Connext in react native, check out a sample implementation here based on the react native typescript template.

What’s next?

If you’re integrating Connext into a native wallet, check out the React Native Integration Guide.

If you’re building an application that uses Connext, check out DApp Integrations (docs coming soon!).

Additional Resources

Further documentation on the client (types, method reference, etc) can be found here.

A live mainnet implementation can be found here.

Client Instantiation

A Connext client is a non-routing implementation of the Connext protocols. You can think of it as an SPV node (whereas an indra node is a “full node”). Clients are currently entirely self reliant - they implement the protocols in their entirety, store channel state locally, and accept an ethProvider url so that they can query blockchain state without relying on the node.

To function correctly, the Connext client needs to be passed a Connext-specific signer API. These messages form the basis of security in the Connext protocol, but it is also assumed that the client itself is running within a secure environment - otherwise correct construction/validation of Connext protocol messages could be bypassed to force the signer to sign arbitrary (Connext-prefixed) messages.

For this reason, it is safest for the client to predominantly run inside wallets and pass around a channelProvider to dApps running in a less-secure web context. This pattern is exactly the same as how wallets handle an ethProvidercurrently. It is recommended that client implementers carefully read through all of the following sections to ensure that user funds remain secure.

Compatibility and React Native

At the moment, the Connext client - including the core protocol execution engine - is implemented only in Typescript. Other implementations are on the roadmap (we would love your help in building them!). The remainder of these docs will assume that the client is either being implemented within a browser, in a TS/JS server-side environment, or in React Native.

An example react native implementation can be found here.

An example REST API implementation can be found here

Client Options

Instantiating the client requires providing the following:

ClientOptions = {
  ethProviderUrl: string;
  nodeUrl?: string; // node's HTTP endpoint
  signer?: string | IChannelSigner;
  store?: IStoreService;
  storeType?: StoreTypes;
  backupService?: IBackupServiceAPI;
  channelProvider?: IChannelProvider;
  loggerService?: ILoggerService;
  logLevel?: number;
}

We’ll go through each of these options below.

Installing the Client for React Native

To integrate the Connext client in your React Native app, you must have installed react-native-crypto and polyfill NodeJS modules using rn-nodeify. You can find more information on NPM but here is how to install it.

// First install dependencies
npm install --save react-native-crypto react-native-randombytes

// Link native bindings
react-native link react-native-randombytes

// install latest rn-nodeify
npm install --save-dev tradle/rn-nodeify

// run this command as postinstall
./node_modules/.bin/rn-nodeify --hack --install

// import the generated shim into index.js (or index.ios.js or index.android.js)
// make sure you use `import` and not require! 
import './shim.js' // 

Now you can install the Connext client from NPM:

npm install --save @connext/client

Then include the fixes from our example ConnextReactNative app postinstall script which we recommend saving in the following path in your project: ./ops/post-install.sh. We have also included the rn-nodeify command from the previous step.

Add the postinstall script to your project’s package.json as follows:

{
  "scripts": {
    "postinstall": "bash ops/post-install.sh",
  },
}

Connecting to the Blockchain and to a Connext Node

Clients are unopinionated to however you choose to implement nodes for both Connext and the base chain. They simply expect to be able to connect to Ethereum (or any EVM chain) using some URL, and the Connext node over http.

For ethProviderUrl, this means you can use either an Infura or Alchemy url, or expose your own geth node.

For nodeUrl on testnet, you can either set up your own node or use our hosted testnet node on Rinkeby at https://rinkeby.indra.connext.network". For mainnet, we recommend running your own node – reach out to us directly about this on our discord.

Creating and Passing in a ChannelSigner

The Connext client must be instantiated with a signer API that uses a Connext-specific message prefix. This can be done unsafely by passing in a private key directly (the client will create an internal ChannelSigner) or by letting the client generate it’s own private key like we do in QuickStart. For production use, however, it is recommended that implementers create and pass in their own ChannelSigner.

The ChannelSigner must conform to the IChannelSigner API with the following specifications: // TODO

Setting up a Store

All clients have to be set up with some store that holds a copy of local state. We ship the client with a bunch of prebuilt default stores designed with different environments and usecases in mind:

StoreType Context
LocalStorage Browser local storage
AsyncStorage React Native local storage
Postgres Server-side database
File JSON File
Memory In-memory (for testing)

You can use a default store by passing in it’s StoreType as part of the client opts:

import AsyncStorage from "@react-native-community/async-storage";
import { StoreTypes } from "@connext/types";
import { ConnextStore } from "@connext/store";
import * as connext from "@connext/client";

const store = new ConnextStore(StoreTypes.AsyncStorage, { storage: AsyncStorage });
const channel = await connext.connect("rinkeby", { store });

It is also possible to create and pass in your own store implementation so long as it’s structured correctly. The default store implementations code should give an idea of how this should look.

Backing up State

The store module will save all the state channel state locally but it’s recommended that Wallets will backup this state remotely in a secure environment so that user’s could restore it easily with their mnemonic.

We provide an option to pass BackupServiceAPI which will hook up to the store module to maintain the state remotely in sync with the saved local state. The interface should match the following:

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

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

For more info, see Creating a Custom Backup Service.

ChannelProviders

A channel provider is an interface that allows an application to safely communicate with a remote Connext client over RPC. Wallets can inject a ChannelProvider into a browser context for web-dApps similarly to how they currently inject an existing ethProvider (without exposing keys or the signer to the dApp directly).

You can create a channelProvider by following along with our example implementation using WalletConnect in the Dai Card.

Logging

You may also provide a Logger to the client that corresponds to the ILoggerService interface:

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

// Designed to give devs power over log format & context switching
export interface ILoggerService extends ILogger {
  setContext(context: string): void;
  newContext(context: string): ILoggerService;
}

The client accepts a LogLevel of 1-5 where 1 corresponds to minimal logging (only errors) and 5 corresponds to oppressive logging. Note that this interface is consistent with logging services such as Winston.

Funding your Channel

Simple Deposits

Using the basic deposit function, client.deposit(), will attempt to deposit the amount/asset you specify from the client’s signer address.

For this to work, you must first send the user’s funds onchain to client.signerAddress. In the event that you’re depositing a token, you must also send a small amount of Eth for gas.

There is WIP proposal to abstract away the pain of having to pay gas for the second transaction without compromising the security of user funds.

Advanced: Controlling Deposit Flow

Sometimes, the above deposit mechanism is either not possible or has poor UX. An example of this is if the user is purchasing funds directly into their channel using Wyre or some other fiat->crypto onramp.

In these cases, it may make sense to access the lower level requestDepositRights and rescindDepositRights functions directly. When a client controls deposit rights in their channel for a given asset, they can deposit that asset into the channel from any source simply by sending funds to the channel multisig contract directly. Note that once the rights have been requested, all transfers of that asset to the multisig are credited to the client’s channel balance. This means that a node will not be able to deposit into a channel until the client explicitly calls rescindDepositRights.

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

For example:

// 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 });

Note that depositing this way has some additional security considerations:

  1. The transaction must be both sent and confirmed after deposit rights are requested but before they are rescinded.

  2. Sending funds directly to the multisig contract address without reqeusting deposit rights will result in the loss of those funds.

This makes requestDepositRights and rescindDepositRights only suitable for certain specific usecases where you can deterministically and reliably expect a deposit into the multisig only at a certain time.

Withdraw Commitments

A core user experience goal of Connext is abstracting away the process of paying gas (transaction fees) as much as possible. For client withdrawals, Connext uses a metatransaction pattern for where users sign commitments and have the node submit them to the blockchain on their behalf. The node may charge for this service using a user’s in-channel funds if they choose.

This pattern works because the channel’s offchain balance is atomically reduced based on whether a correctly signed commitment can be generated. There is no way for a node to steal a user’s funds, but it is possible for the node become unresponsive or otherwise censor the user by not following through on submitting the tx to the blockchain after the commitment is generated.

In these cases, the client implementer needs to recover and submit these commitments themselves whenever convenient. Fortunately, we also backup these commitments in the client’s store and they can be retrieved using:

client.store.getWithdrawalCommitments(client.multisigAddress);

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.

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);

Indra Architecture

_images/architecture.png

Indra

Indra is the core implementation monorepo for Connext. Indra contains ready-for-deployment code for core node services, client, the Dai Card (as a reference implementation), as well as scripts needed to deploy and operate a node in production. When deploying a node to production, the most recent docker images will be used.

For detailed instructions on how to run the code contained within the repository, see the guide: How to deploy an Indra Node.

There are several modules contained within the indra repository:

Client

The Connext Client package is a Typescript interface which is used to connect to a node and communicate over the Connext Network. The client package is available through npm.

Clients with their own keys are ideally integrated directly at the wallet layer. This is because they contain validator code which determines, on behalf of the user, whether a given state is safe to sign. Clients can also be used via a connextProvider in a frontend environment to make calls to a hooked client that is hosted in a more trusted environment such as a wallet.

Clients contain the following functionality:

  • Depositing to a channel

  • Installing any arbitrary application with any counterparty

  • Updating applications.

  • Uninstalling applications.

  • Prebuilt applications for common usecases, including simple transfers and swaps.

  • Withdrawing funds from a channel and automatically submitting the latest available mutually agreed update.

  • Handling a dispute.

  • Generating/signing/sending and validating/receiving state updates over NATS. The Client takes in the address of the server that is being used to pass messages in the constructor.

Further documentation on the client can be found here.

Node

Nodes can be thought of as an automated implementation of the client along with services to enable client to client interactions. Nodes have the same functionality outlined above, and forward state update packets between clients.

Contracts

Connext is built on Counterfactual’s contracts. Code is fully open source and a protocol description is available here.

Do not modify the contracts themselves before deploying - this could break the security model of the entire protocol

Database

Contains all the database migration information to be used by the docker images. Make sure that the migrations in the database module, and in the node/migrations folder are consistent if you are making changes here.

Proxy

Routing proxy and related configurations.

Dai Card

The Dai Card is a sample implementation of the client designed to help you bootstrap an application that integrates Connext. It contains an inpage wallet and payment interface, as well as a custom Web3 injection that automatically signs transactions using the inpage wallet. For developers just beginning to build their application, the card is a great way to get started; for developers looking to integrate with existing an existing app, it’s a good instructive resource for implementation and includes some components that you can easily copy over.

Counterfactual Protocol

The Node contains a TypeScript implementation of the Counterfactual protocol. It is responsible for executing the protocols and producing correctly constructed signed commitments that correspond to state transitions of the users’ state channels.

The main areas of interest with this implementation are:

  • src/machine/instruction-executor.ts: This is the entry point for running a protocol.

  • src/protocol is where all the protocols are implemented

  • src/ethereum is where the structure of the commitments is placed. It’s under this folder as these commitments adhere to the Ethereum network’s interface for transactions.

The specific design philosophy for this implementation is the middleware pattern. That is, all of these protocols are naturally broken down into steps, for each of which there is a middleware responsible for executing that step.

Given this design, it’s easy to extend this implementation to support additional protocols, replace a default middleware with an alternative implementation, and for the machine to rely on yet delegate more advanced logic to external services.

Some specific examples of this include:

  • delegating to a signing module that verifies whether a given action is safe to sign & countersign

  • storing state commitments (delegating to an arbitrary, possibly non-local service implementing a desired interface)

  • implementing a custom Write-Ahead-Log to tweak performance/security properties

We have some diagrams explaining the Node’s architecture and control flow.

Node Address and Signing Keys

In order for the Node to produce state-update commitments, it needs access to some signing keys.

There are two ways in which this is supported:

  1. An extended private key string is provided to the Node at the “EXTENDED_PRIVATE_KEY_PATH” key of the store service that is passed as an argument to the Node. If no such value exists for this key, the Node produces an extended private key and sets it at this key. This extended private key is then used to generate a “public identifer” for the Node (the address by which the Node is known by). It is also used to generate private keys which are specific to AppInstances.

  2. Instead of supplying a mnemonic, the Node operator supplies two other arguments:

  • an extended public key which will serve as the “public identifier” of the Node, and will be used to generate signer addresses at AppInstance-specific derivation paths for signature verification in the protocols.

  • a callback function that offers the generation of a private key given a specific derivation path. This enables the consumer of the Node (i.e. wallets) to not reveal any mnemonics but provide the ability to sign state isolated to specific AppInstances.

The Node package exposes a reference implementation of the second approach through a function named generatePrivateKeyGeneratorAndXPubPair which produces these 2 arguments given an extended private key.

Apps and OutcomeTypes

Each application that is installed in a channel has an OutcomeType that defines when the app reaches a terminal state and is about to be uninstalled how the funds allocated to it will be distributed.

The currently supported outcome types are:

  • TWO_PARTY_FIXED_OUTCOME

    • This is only used when the installed app is collateralized with ETH (for now) and indicates that the total amount allocated to the app will be sent to one of the two parties OR gets split evenly.

  • MULTI_ASSET_MULTI_PARTY_COIN_TRANSFER

    • This is used for transferring arbitrary amounts (limited by app collateral) of arbitrary asset classes (ETH or ERC20) to some addresses.

  • SINGLE_ASSET_TWO_PARTY_COIN_TRANSFER

    • This is used for transferring arbitrary amounts (limited by app collateral) of a single asset class (ETH or ERC20) to some addresses.

Note:

Any consumer of the Node should set up a handler for the event DEPOSIT_CONFIRMED so as to define how this Node behaves when a counter party has initiated a deposit and is asking this Node to make a counter deposit and collateralize the channel. The parameters passed with this event correspond to the same ones used by the initiator, tha is DepositParams (as defined in the @counterfactual/types packages).

If no such handler is defined, No event handler for counter depositing into channel <info> is printed indicating the Node does not know how to handle a counter deposit request.

Protocol Diagrams

These diagrams are available to help you understand the underlying architecture of the Node.

Ownership

arrows indicate “has a pointer to”

graph LR subgraph Node ProtocolRunner MessagingService RpcRouter --> RequestHandler RequestHandler --> RpcRouter RequestHandler --> StoreService RequestHandler --> MessagingService RequestHandler --> ProtocolRunner end

Control Flow

arrows mostly indicate “calls”

graph LR subgraph MessagingService onReceive send end subgraph RequestHandler callMethod end subgraph RpcRouter dispatch end subgraph StoreService storeServiceSet["set"] end subgraph NodeController_For_RPC rpcExecute["executeMethod"]-->storeServiceSet dispatch-->rpcExecute callMethod-->rpcExecute end subgraph Middleware IO_SEND_AND_WAIT IO_SEND OP_SIGN PERSIST_COMMITMENT-->storeServiceSet IO_SEND_AND_WAIT-->send IO_SEND-->send end subgraph Deferral ioSendDeferrals["resolve"] deferralCtor["constructor"] end subgraph Signer signDigest["signingKey.signDigest"] end subgraph Node onReceivedMessage onReceive-->onReceivedMessage onReceivedMessage-->ioSendDeferrals outgoing["Outgoing (EventEmitter)"] protocolMessageEventController-->|sends out events <br>after protocol finishes|outgoing OP_SIGN-->signDigest end subgraph NodeController_For_Events eventExecute["executeMethod"]-->storeServiceSet onReceivedMessage-->eventExecute end subgraph ProtocolRunner initiateProtocol runProtocolWithMessage protocolMessageEventController-->runProtocolWithMessage rpcExecute-->initiateProtocol runProtocol initiateProtocol-->runProtocol runProtocolWithMessage-->runProtocol ioSendDeferrals-->|resume|runProtocol IO_SEND_AND_WAIT-->deferralCtor runProtocol-->IO_SEND_AND_WAIT runProtocol-->IO_SEND runProtocol-->OP_SIGN runProtocol-->PERSIST_COMMITMENT end

Limitations & Assumptions

There are a few considerations to be aware of as an implementer.

Availability

The wallet must acknowledge every state update by cosigning that state. This means in order to update your channel, the user must be online. We’ve built several different conditional transfer types to help accommodate user availability constraints.

Default Conditional Transfers and UX Impacts

Autocollateralization

The node uses an autocollateralization mechanism that is triggered by any payment made, whether or not the payment was successful. Nodes determine amount of collateral needed based on user profiles (e.g., in a retail environment the cashier might be assigned a profile that gives them a large amount of collateral so they can receive many payments). Additionally, there are floors and ceilings implemented by node operators to minimize the amount of collateral that is locked in node channels, as well as set a minimum amount of collateral to be maintained in each channel.

Current Trust Assumptions

While the underlying protocol is completely noncustodial, there are trust assumptions which we want to make explicit. We are actively addressing these assumptions, so expect this section to change over the next few months.

Updates are censorable

Connext uses NATS - a highly scalable, lightweight, open source, p2p messaging system. NATS lets us have multiple independent copies of state hosted by the client, hub and independent backups. Unfortunately, it still requires that we implement a messaging server (currently hosted by the hub) to work properly. This means that while they’re p2p, messages in the centralized v2.0 hub are censorable for now.

The decision to use NATS specifically is a step towards solving this problem. NATS supports decentralized (mesh) messaging by clustering many messaging servers together. This means is that we can ship clustered NATS instances as part of the Connext nodes in our eventual decentralized network, to decentralize our messaging layer in tandem.

Transfers are censorable

In v2.0 of Connext, every user deposited into channels with the hub and then routes transfers to each other over the hub. This is a bootstrapping technique to create a reliable, easily iterable system while we collect user data and test a variety of different usecases. This also means, however, that our hub could be censored, DDoS’d or shut down, putting our payment service offline (though no user funds would be lost!).The addition of p2p messaging and generalized state is a huge step in towards eventually becoming decenralized, however. We expect to make steady progress towards a decentralized Connext, 2.x, now that we’re live.

Client proxies through hub’s interface to access Redlock

By decentralizing user state storage, we introduced complexity related to concurrently updating state. In centralized servers, concurrency is handled by locking/unlocking state on each operation. For our distributed paradigm, we integrated Redis’ distributed locks in the hub. Unfortunately, Redis isn’t natively supported in browsers and Webdis, the browser-based Redis interface, doesn’t support Redlock.

In the interest of shipping efficiently, we built a proxy interface hosted by the hub for the client to lock/unlock their state. In the short to mid term, we expect to reimplement or modify Webdis to use distributed locking as well.

FAQ

General

What Connext is:
  • A scalability solution for EVM-compatible blockchains.

  • A software company that builds open source technology infrastructure for Ethereum applications.

  • A non-custodial batching or “compression” layer on top of Ethereum which retains the security and self-sovereignty qualities of the base blockchain.

What Connext is not:
  • A custodial wallet or custody solution of any sort.

  • An exchange or other financial service provider.

  • A blockchain of its own.

Connext DOES NOT in any way take custody of user funds. We don’t even run the code, you do.

What problems does Connext solve?

Connext makes it possible to build scalable decentralized applications on the Ethereum blockchain.

The Ethereum blockchain already enables trust-minimized transactions between peers, but these transactions have high fees and slow confirmation times. This makes the Ethereum blockchain very suitable as a settlement layer, but not usable for day-to-day usecases. Connext reduces the number of transactions that need to be put onto Ethereum without changing the core decentralization and trust-minimization properties of Ethereum itself.

Does Connext have a token or native currency? Will Connext do an ICO/IEO?

No, Connext does not currently have its own native token. There are no plans for Connext to ever do an ICO/IEO.

Connext transactions can be made in any Ethereum ERC20 token.

How does Connext compare to other state channels solutions?

Connext is live on the Ethereum mainnet, is supported by a growing number of wallets/applications/stakeholders as the industry standard, and focuses largely on use cases related to conditional transfers.

Connext is also very easy to use as compared to other solutions! Check out Quick Start Basics to learn more.

How does Connext compare to Plasma/sidechains?

Plasma is a framework for scaling Ethereum capacity by using hierarchical sidechains with mechanisms to ensure economic finality. Plasma helps Ethereum scale out (i.e. allowing for more parallel processing) rather than scaling up (i.e. allowing for more transactions per block).

This means that plasma scales transactions around a specific ecosystem, rather than being massively horizontally scalable across the entirety of the Ethereum network (and other chains). Think of the difference between the transactional activity related to Uber vs the transactional activity related to Visa/Mastercard.

Plasma and Connext are mutually beneficial. Wallets that implement Connext at scale may need plasma-like solutions for seamless end user experience. Plasma chains, meanwhile, will need Connext to allow for interoperability and to allow users to utilize funds locked on the plasma chain without waiting for long exit timeouts.

Are there fees?

Connext itself does not collect any fees from the network. Nodes providing packet routing and storage services within the network may collect fees if they choose to do so.

How does Connext sustain itself?

For now, Connext is backed by VCs and grants from the Ethereum Foundation. Long term, the Connext team expects to monetize through selling ancillary services for reducing the operating costs of running nodes.

Is there a whitepaper?

No. A protocol specification for Connext v2.0 can be found in the Counterfactual framework documentation.

State Channels

What happens if two parties disagree about an offchain transaction?

In Connext, each state update must be signed by all channel parties and contain an update nonce. In the event that a disagreement occurs, either party can resolve the dispute on the blockchain by submitting their highest-nonce update to the channel dispute resolution contract.

Arbitrator contracts contain instructions on how a transaction should be resolved, based on the contents of the state update and/or external factors such as time or onchain events.

Why do you host a single node for v2.0?

One of the most difficult challenges of channelized networks is ensuring that there is enough capacity (or collateral) in a given channel to receive transactions without interrupting the flow of updates.

Let’s consider a simple transfer usecase in a hub and spoke system: if Alice wants to pay Bob 1 Eth through the Hub, Alice would first pay the Hub 1 Eth in her channel conditionally based on if the Hub would pay 1 Eth to Bob in his channel. To successfully complete this transfer, the Hub would need to already have had 1 Eth in Bob’s channel, which it could only have done if it knew beforehand that Bob would be the receiver of funds.

It turns out, this is largely a data science problem related to user behavior. Our goal with running a singular public node ourselves is to prioritize usability - by collecting data and coming up with a rebalancing/recollateralization protocol beforehand, we can improve the efficiency of collateral allocation for decentralized nodes in the future.

Doesn’t that mean you’re centralized?

Yes! This version of Connext is centralized. We fully acknowledge that this is the case and have been transparent about it from first deployment.

V2.x of Connext will enable support for transactions routed over multiple nodes, and will make it much simpler for any organization or individual to run their own node. At that point, we expect the network to become more and more decentralized.

Note that centralization here is completely different from being custodial. While we are currently the only entity routing and processing packets, this does not mean that we hold user funds in any way. The primary risk here is censorship of transactions themselves. Also note that our node implementation is fully open source, so anyone can come run their own node if they wanted to - in fact, many companies already do!

How To Deploy an Indra Node

Lets say you want to deploy an Indra payment node to https://indra.example.com (we’ll call this url $DOMAINNAME)

First step: get a server via AWS or DigitalOcean or whichever cloud provider is your favorite. For best results, use the most recent LTS version of Ubuntu. Note this new server’s IP address (we’ll call it $SERVER_IP). Make sure it’s able to connect to the internet via ports 80, 443, 4221, and 4222 (no action required on DigitalOcean, Security Group config needs to be setup properly on AWS).

Set up DNS so that $DOMAINNAME points to this server’s IP address.

Every Indra node needs access to a hot wallet, you should generate a fresh mnemonic for your node’s wallet that isn’t used anywhere else. You can generate a new mnemonic from a node console with ethers by doing something like this: require('ethers').Wallet.createRandom().

Save this mnemonic somewhere safe, copy it to your clipboard, and then run:

SSH_KEY=$HOME/.ssh/id_rsa bash ops/setup-ubuntu.sh $SERVER_IP

If this is a fresh Ubuntu server from DigitalOcean or AWS then the above script should:

  • configure an “ubuntu” user and disable root login (if enabled)

  • give an additional ssh public key login access if provided (useful for CD/auto-deployment)

  • install docker & make & other dependencies

  • upgrade everything to the latest version

  • save your mnemonic in a docker secret called indra_mnemonic

  • reboot

Note: this script is idempotent aka you can run it over and over again w/out causing any problems. In fact, re-running it every month or so will help keep things up-to-date (you can skip inputting the mnemonic on subsequent runs).

If you already have a server with docker & make installed, there’s another helper script you can use to easily load your mnemonic: bash ops/save-secret.sh. Run this on your prod server & copy/paste in your mnemonic.

For convenience’s sake, we recommend adding an entry to your ssh config to easily access this server. Add something that looks like the following to $HOME/.ssh/config:

Host new-indra
  Hostname $SERVER_IP
  User ubuntu
  IdentityFile ~/.ssh/id_rsa
  ServerAliveInterval 120

Now you can login to this server with just ssh new-indra. Once the server wakes up again after rebooting at the end of ops/setup-ubuntu, login to finish setup.

We need to add a couple env vars before launching our indra node. We’ll be pulling from the public default prod-mode env vars & updating a couple as needed.

cp prod.env .env

Ensure you’ve added correct values for two important env vars: INDRA_DOMAINNAME and INDRA_ETH_PROVIDER.

Upload the prod env vars to the indra server. If you’re using a custom address book, upload that too:

scp .env new-indra:~/indra/
scp address-book.json new-indra:~/indra/

Login to your prod server then run the following to launch your Indra node:

cd indra
git checkout master # staging is the default branch. It's cutting edge but maybe buggy.
make restart-prod

The above will download & run docker images associated with the commit/release you have checked out. If you want to launch a specific version of indra, checkout that version’s tag & restart:

git checkout indra-6.0.8 && make restart-prod

How To Integrate With a Browser App

The connext client @connext/client has built-in defaults that make it easy to use in a web browser. By default, the client will store it’s state in localStorage (Look for keys prefixed with INDRA_CLIENT_CF_CORE).

To get started, install the required connext package.

npm install @connext/client

Prerequisites

  1. You need access to an Indra node.

Connext exposes 2 Indra nodes for public use:

  • https://rinkeby.indra.connext.network/api: Good for testing & experimenting on the Rinkeby test net

  • https://indra.connext.network/api: To interact with mainnet channels in production.

You can also run your own Indra node locally by running the start command in an indra repo.

git clone https://github.com/connext/indra
cd indra
make start-headless

Once Indra wakes up after running the above, it’ll be available at http://localhost:8080 + a testnet eth provider will be available at http://localhost:8545 (you can send testnet ETH to any address by running bash ops/fund.sh 0xabc123... in the indra repo).

  1. You need access to an ethereum provider.

For small scale experiments, you can use a default ethers provider by passing in an optional first arg with the network name string (supported networks: “rinkeby” & “mainnet”).

In production, you’ll want to get an API key for alchemy, etherscan, or infura & use that URL.

  1. You need a key pair & you need to keep it safe.

Internally, we derive a signingAddress from a provided private key & funds at this Ethereum address will be used when calling deposit(). Alternatively, to protect the private key, you can wrap it in a ChannelSigner interface & inject that interface. See reference for more info.

Example Code

Create a state channel on a local testnet (Run make start-headless in indra first)

import { connect } from "@connext/client";
import { Wallet } from "ethers";

(async () => {

  const channel = await connect({
    ethProviderUrl: "http://localhost:8545",
    signer: Wallet.createRandom().privateKey,
    nodeUrl: "http://localhost:8080",
  });

  console.log(`Successfully connected channel with public id: ${channel.publicIdentifier}`);

})()

Create a state channel on Rinkeby

import { connect } from "@connext/client";
import { Wallet } from "ethers";

(async () => {

  const channel = await connect("rinkeby", {
    signer: Wallet.createRandom().privateKey,
    nodeUrl: "https://rinkeby.indra.connext.network/api",
  });

  console.log(`Successfully connected channel with public id: ${channel.publicIdentifier}`);

})()

Reference Implementations

DaiCard
  • Live at https://daicard.io

  • Code is open source at https://github.com/connext/indra/tree/staging/modules/daicard

How To Integrate With React Native

The Connext client assumes that it runs in a trusted environment because it needs to control the ability to automatically sign messages and transactions on behalf of a user. To ensure that user funds remain secure, it is recommended that implementers carefully read through all of the following sections:

Client Options

Instantiating the client requires providing the following:

Name Type Description Optional
ethProviderUrl String the Web3 provider URL used by the client no
nodeUrl String url of the node yes
mnemonic String Mnemonic of signing wallet yes
xpub String Extended Public Key of signing wallet yes
keyGen String Key Generation callback of signing wallet yes
store Store Module for storing local state yes
logLevel number Depth of logging yes
asyncStorage AsyncStorage AsyncStorage module for react-native yes
backupService IBackupServiceAPI Backup service module to store state remotely yes

Compatibility and React Native

At the moment, the Connext client - including the core protocol execution engine - is implemented only in Typescript. Other implementations are on the roadmap (we would love your help in building them!). The remainder of these docs will assume that the client is either being implemented in a TS/JS server-side environment or in React Native.

Pre-requirements

To integrate the Connext client in your React Native app, you must have installed react-native-crypto and polyfill NodeJS modules using rn-nodeify. You can find more information on NPM but here is how to install it.

// First install dependencies
npm install --save react-native-crypto react-native-randombytes

// Link native bindings
react-native link react-native-randombytes

// install latest rn-nodeify
npm install --save-dev tradle/rn-nodeify

// run this command as postinstall
./node_modules/.bin/rn-nodeify --hack --install

// import the generated shim into index.js (or index.ios.js or index.android.js)
// make sure you use `import` and not require! 
import './shim.js' // 

Installing

Now you can install the Connext client from NPM:

npm install --save @connext/client

Then include the fixes from our example ConnextReactNative app postinstall script which we recommend saving in the following path in your project: ./ops/post-install.sh. We have also included the rn-nodeify command from the previous step.

Add the postinstall script to your project’s package.json as follows:

{
  "scripts": {
    "postinstall": "bash ops/post-install.sh",
  },
}

Setting up a Channel

Finally you can import the Connext client inside your app and connect a channel using the same quick start instructions but also including the AsyncStorage package as an option for storing state locally in React Native.

import AsyncStorage from "@react-native-community/async-storage";
import * as connext from "@connext/client"

const channel = await connext.connect("rinkeby", { asyncStorage: AsyncStorage })

Backing up State

The store module will save all the state channel state locally but it’s recommended that Wallets will backup this state remotely in a secure environment so that user’s could restore it easily with their mnemonic.

We provide an option to pass BackupServiceAPI which will hook up to the store module to maintain the state remotely in sync with the saved local state. The interface should match the following:

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

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

How To Integrate With a NodeJS Server

To get started, install the required connext package.

npm install @connext/client

Prerequisites

  1. You need access to an Indra node.

Connext exposes 2 Indra nodes for public use:

  • https://rinkeby.indra.connext.network/api: Good for testing & experimenting on the Rinkeby test net

  • https://indra.connext.network/api: To interact with mainnet channels in production.

You can also run your own Indra node locally by running the start command in an indra repo.

git clone https://github.com/connext/indra
cd indra
make start-headless

Once Indra wakes up after running the above, it’ll be available at http://localhost:8080 + a testnet eth provider will be available at http://localhost:8545 (you can send testnet ETH to any address by running bash ops/fund.sh 0xabc123... in the indra repo).

  1. You need access to an ethereum provider.

For small scale experiments, you can use a default ethers provider by passing in an optional first arg with the network name string (supported networks: “rinkeby” & “mainnet”).

In production, you’ll want to get an API key for alchemy, etherscan, or infura & use that URL.

  1. You need a key pair & you need to keep it safe.

Internally, we derive a signingAddress from a provided private key & funds at this Ethereum address will be used when calling deposit(). Alternatively, to protect the private key, you can wrap it in a ChannelSigner interface & inject that. See reference for more info.

  1. You need to consider where you’re going to store your state.

The simplest option is save it as a file. Alternatively, to increase performance you can save it to a postgres database. The @connext/store reference has more info about the options available.

Example Code

Create a state channel on a local testnet (Run make start-headless in indra first). Save channel state to a simple file.

import { connect } from "@connext/client";
import { getFileStore } from "@connext/store";
import { Wallet } from "ethers";

(async () => {

  const channel = await connect({
    ethProviderUrl: "http://localhost:8545",
    signer: Wallet.createRandom().privateKey,
    nodeUrl: "http://localhost:8080",
    store: getFileStore(),
  });

  console.log(`Successfully connected channel with public id: ${channel.publicIdentifier}`);

})()

Create a state channel on Rinkeby & save state to a postgres database.

import { connect } from "@connext/client";
import { getPostgresStore } from "@connext/store";
import { Wallet } from "ethers";

(async () => {

  const channel = await connect("rinkeby", {
    signer: Wallet.createRandom().privateKey,
    nodeUrl: "https://rinkeby.indra.connext.network/api",
    store: getPostgresStore(`postgres://user:password@host:port/database`),
  });

  console.log(`Successfully connected channel with public id: ${channel.publicIdentifier}`);

})()

Reference Implementations

TipDai
  • Coming soon to twitter account @TipDai

  • Browse the code at https://gitlab.com/bohendo/tipdai

@connext/cf-core

Exported Classes:

Classes

CFCore

The Counterfactual protocol runner.

class CFCore {
  static create(
    messagingService: IMessagingService,
    storeService: IStoreService,
    networkContext: NetworkContext,
    nodeConfig: NodeConfig,
    provider: JsonRpcProvider,
    signer: IChannelSigner,
    lockService?: ILockService,
    blocksNeededForConfirmation?: number,
    logger?: ILoggerService,
  ): Promise<CFCore>;
}
Constructor Params

CFCore doesn’t have a traditional constructor because some required setup steps are asynchronous. Instead, all instances are created via the static method create. This method takes the parameters:

  1. messagingService: IMessagingService

  2. storeService: IStoreService

  3. networkContext: NetworkContext

  4. nodeConfig: NodeConfig: Basically just the store prefix which idk if this is even needed any more.

  5. provider: JsonRpcProvider

  6. signer: IChannelSigner

  7. lockService?:ILockService

  8. blocksNeededForConfirmation?:number: This should probably just be hard coded.

  9. logger?:ILoggerService

Properties
  • rpcRouter!: RpcRouter

  • networkContext: NetworkContext

  • blocksNeededForConfirmation: number

  • log: ILoggerService

  • signerAddress: string

  • publicIdentifie: string

Methods
  • injectMiddleware(opcode: Opcode, middleware: ValidationMiddleware): void

  • on(event: EventNames | MethodName, callback: (res: any) => void)

  • off(event: EventNames | MethodName, callback?: (res: any) => void)

  • once(event: EventNames | MethodName, callback: (res: any) => void)

  • emit(event: EventNames | MethodName, req: MethodRequest)

  • async call(method: MethodName, req: MethodRequest): Promise<MethodResponse>


Legacy Docs: Probably out of date



Message Format

A “message” is a return value of an RPC call or the value passed to an event listener. Messages have the following fields:

  • type: string

    • Name of the Method or Event that this message represents e.g. “getAppInstances”, “install”

  • requestId?: string

    • Unique ID for a Method request.

    • Only required for Methods. Leave empty for Events.

  • data: { [key: string]: any }

    • Data payload for this message.

    • See “Result” section of a Method and “Data” section of an Event for details.

Public RPC Methods

Method: getAppInstances

Returns all app instances currently installed on the Node.

NOTE: This is terrible from a security perspective. In the future this method will be changed or deprecated to fix the security flaw.

Params: None

Result:

  • appInstances:AppInstanceInfo[]

    • All the app instances installed on the Node

Method: proposeInstall

Requests that a peer start the install protocol for an app instance. At the same time, authorize the installation of that app instance, and generate and return a fresh ID for it. If the peer accepts and the install protocol completes, its ID should be the generated appIdentityHash.

Params:

  • proposedToIdentifier: string

    • Public identifier of the peer responding to the installation request of the app

  • appDefinition: string

    • On-chain address of App Definition contract

  • abiEncodings:AppABIEncodings

    • ABI encodings used for states and actions of this app

  • initiatorDeposit: BigNumber

    • Amount of the asset deposited by this user

  • initiatorDepositTokenAddress?: string

    • An optional string indicating whether an ERC20 token should be used for funding the proposer’s side of the app. If not specified, this defaults to ETH.

  • responderDeposit: BigNumber

    • Amount of the asset deposited by the counterparty

  • responderDepositTokenAddress?: string

    • An optional string indicating whether an ERC20 token should be used for funding the peer’s side of the app. If not specified, this defaults to ETH.

  • timeout: BigNumber

    • Number of blocks until a submitted state for this app is considered finalized

  • initialState:AppState

    • Initial state of app instance

Result:

  • appIdentityHash: string

    • Generated appIdentityHash

Errors: (TODO)

  • Not enough funds

Method: rejectInstall

Reject an app instance installation.

Params:

  • appIdentityHash: string

    • ID of the app instance to reject

Result: “OK”

Errors: (TODO)

  • Proposed app instance doesn’t exist

Method: install

Install an app instance.

Params:

  • appIdentityHash: string

    • ID of the app instance to install

    • Counterparty must have called proposedInstall and generated this ID

Result:

Errors: (TODO)

  • Counterparty rejected installation

Method: getState

Get the latest state of an app instance.

Params:

  • appIdentityHash: string

    • ID of the app instance to get state of

Result:

  • state:AppState

    • Latest state of the app instance

Errors: (TODO)

  • App not installed

Method: getAppInstanceDetails

Get details of an app instance.

Params:

  • appIdentityHash: string

    • ID of the app instance to get details of

Result:

Method: getProposedAppInstance

Get details of a proposed app instance.

Params:

  • appIdentityHash: string

    • ID of the app instance to get details of

Result:

Method: takeAction

Take action on current app state to advance it to a new state.

Params:

  • appIdentityHash: string

    • ID of the app instance for which to take action

  • action:SolidityValueType

    • Action to take on the current state

Result:

Errors: (TODO)

  • Illegal action

Method: uninstall

Uninstall an app instance, paying out users according to the latest signed state.

Params:

  • appIdentityHash: string

    • ID of the app instance to uninstall

Result: “OK”

Errors: (TODO)

  • App state not terminal

Method: proposeState

TODO

Method: acceptState

TODO

Method: rejectState

TODO

Method: createChannel

Creates a channel and returns the determined address of the multisig (does not deploy).

Params:

  • owners: string[]

    • the addresses who should be the owners of the multisig

Result:

  • CreateChannelTransactionResult

    • multisigAddress: string

      • the address which will hold the state deposits

Method: deployStateDepositHolder

Deploys a multisignature wallet contract for use by the channel participants. Required step before withdrawal.

Params:

  • multisigAddress: string

    • address of the multisignature wallet

  • retryCount?: number

    • the number of times to retry deploying the multisig using an expontential backoff period between each successive retry, starting with 1 second. This defaults to 3 if no retry count is provided.

Result:

  • DeployStateDepositHolderResult

    • transactionHash: string

      • the hash of the multisig deployment transaction

        • This can be used to either register a listener for when the transaction has been mined or await the mining.

Method: getChannelAddresses

Gets the (multisig) addresses of all the channels that are open on the Node.

Result:

  • addresses: string[]

    • the list of multisig addresses representing the open channels on the Node.

Method: deposit

If a token address is specified, deposits the specified amount of said token into the channel. Otherwise it defaults to ETH (denominated in Wei).

Params:

  • multisigAddress: string

  • amount: BigNumber

  • tokenAddress?: string

Result:

  • multisigBalance: BigNumber

    • the updated balance of the multisig

Error:

  • “Insufficient funds”

Method: getStateDepositHolderAddress

Retrieves the address for the state deposit used by the specified owners.

Params:

  • owners: string[]

    • the addresses who own the state deposit holder

Result:

  • multisigAddress: string

    • the address of the multisig (i.e. the state deposit holder)

Method: withdraw

If a token address is specified, withdraws the specified amount of said token from the channel. Otherwise it defaults to ETH (denominated in Wei). The address that the withdrawal is made to is either specified by the recipient parameter, or if none is specified defaults to ethers.utils.computeAddress(ethers.utils.HDNode.fromExtendedKey(nodePublicIdentifier).derivePath("0").publicKey). deployStateDepositHolder must be called before this method can be used to withdraw funds.

Params:

  • multisigAddress: string

  • amount: BigNumber

  • recipient?: string

  • tokenAddress?: string

Result:

  • recipient: string

    • The address to whom the withdrawal is made

  • txHash: string

    • The hash of the transaction in which the funds are transferred from the state deposit holder to the recipient

Error(s):

  • “Insufficient funds”

  • “Withdraw Failed”

Method: withdrawCommitment

This behaves similarly to the withdraw call, except it produces a commitment for the withdrawal and returns the commitment instead of sending the commitment to the network.

Params:

  • multisigAddress: string

  • amount: BigNumber

  • recipient?: string

  • tokenAddress?: string

Result:

  • transaction: { to: string; value: BigNumberish; data: string; }

Error(s):

  • “Insufficient funds”

Method: getFreeBalance

Gets the free balance AppInstance of the specified channel for the specified token. Defaults to ETH if no token is specified.

Params:

  • multisigAddress: string

  • tokenAddress?: string

Result:

{
    [s: string]: BigNumber;
};

Returns a mapping from address to balance in wei. The address of a node with public identifier publicIdentifier is defined as fromExtendedKey(publicIdentifier).derivePath("0").address.

Note: calling this with a specific token address will return Zero even if the channel has never had any deposits/withdrawals of that token.

Method: getTokenIndexedFreeBalanceStates

Gets the free balances for the ETH and ERC20 assets that have been deposited, indexed by token address.

Params:

  • multisigAddress: string

Result:

{
  [tokenAddress: string]: {
    [beneficiary: string]: BigNumber;
  }
};

Returns a doubly-nested mapping. The outer mapping is the address of the token for which there is free balance in the channel. The inner mapping is a mapping from address to balance in wei. The address of a node with public identifier publicIdentifier is defined as fromExtendedKey(publicIdentifier).derivePath("0").address.

Events

Event: depositEvent

Fired if a deposit has been made by a counter party.

Data:

  • multisigAddress: string

    • The address of the channel that the deposit was made into.

  • amount: BigNumber

    • The amount that was deposited by the counter party.

Event: installEvent

Fired if new app instance was successfully installed.

Data:

Event: rejectInstallEvent

Fired if installation of a new app instance was rejected.

Data:

Event: updateStateEvent

Fired if app state is successfully updated.

Data:

  • appIdentityHash: string

    • ID of app instance whose app state was updated

  • newState:AppState

  • action?:SolidityValueType

    • Optional action that was taken to advance from the old state to the new state

Event: uninstallEvent

Fired if app instance is successfully uninstalled

Data:

Event: proposeStateEvent

TODO

Event: rejectStateEvent

TODO

Event: createChannelEvent

Fired when a Node receives a message about a recently-created channel whose multisignature wallet’s list of owners includes this (i.e. the receiving) Node’s address.

Note: On the Node calling the creation of the channel, this event must have a registered callback to receive the multisig address before the channel creation call is made to prevent race conditions.

Data:

  • CreateChannelResult

    • counterpartyXpub: string

      • Xpub of the counterparty that the channel was opened with

    • multisigAddress: string

      • The address of the multisig that was created

    • owners: string[]

      • The list of multisig owners of the created channel

Data Types

Data Type: AppInstanceInfo

An instance of an installed app.

  • id: string

    • Opaque identifier used to refer to this app instance

    • No two distinct app instances (even in different channels) may share the same ID

  • appDefinition: string

    • On-chain address of App Definition contract

  • abiEncodings:AppABIEncodings

    • ABI encodings used for states and actions of this app

  • initiatorDeposit: BigNumber

    • Amount of the asset deposited by this user

  • responderDeposit: BigNumber

    • Amount of the asset deposited by the counterparty

  • timeout: BigNumber

    • Number of blocks until a submitted state for this app is considered finalized

Data Type: AppABIEncodings
  • stateEncoding: string

    • ABI encoding of the app state

      • For example, for the Tic Tac Toe App (https://github.com/counterfactual/monorepo/blob/master/packages/apps/contracts/TicTacToeApp.sol), the state encoding string is "tuple(uint256 versionNumber, uint256 winner, uint256[3][3] board)".

  • actionEncoding?: string

    • Optional ABI encoding of the app action

    • If left blank, instances of the app will only be able to update state using proposeState

    • If supplied, instances of this app will also be able to update state using takeAction

      • Again, for the Tic Tac Toe App, the action encoding string is "tuple(uint8 actionType, uint256 playX, uint256 playY, tuple(uint8 winClaimType, uint256 idx) winClaim)".

Data Type: AppState
  • Plain Old Javascript Object representation of the state of an app instance.

  • ABI encoded/decoded using the stateEncoding field on the instance’s AppABIEncodings.

Data Type: SolidityValueType
  • Plain Old Javascript Object representation of the action of an app instance.

  • ABI encoded/decoded using the actionEncoding field on the instance’s AppABIEncodings.

@connext/client

All methods return promises.

Management Methods

transfer

Makes a simple end-to-end transfer from one user’s balance to another.

transfer: (TransferParams) =>  Promise<ChannelState>
Example
const payload: TransferParams = {
  recipient: "xpub123abc...", // channel.publicIdentifier of recipient
  amount: "1000", // in Wei
  assetId: "0x0000000000000000000000000000000000000000" // represents ETH
}

await transfer(payload)
deposit

Deposits funds from a user’s onchain wallet to a channel.

deposit: (DepositParams) => Promise<ChannelState>
Example
// Making a deposit in ETH
const payload: AssetAmount = {
  amount: "1000", // in Wei
  assetId: "0x0000000000000000000000000000000000000000" // i.e. Eth
};

await deposit(payload);
swap

Conducts an in-channel swap from one asset to another.

swap: (SwapParams) => Promise<ChannelState>
Example
const payload: SwapParams = {
  amount: "100", // in Wei
  fromAssetId: "0x0000000000000000000000000000000000000000", // ETH
  toAssetId: "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359" // Dai
}

await swap(payload)
conditionalTransfer

Transfers with conditional logic determined by onchain app smart contract.

conditionalTransfer: (ConditionalTransferParameters) => Promise<ConditionalTransferResponse>
Condition Types
Linked Transfer

Generate a secret and allow receiver to asynchronously unlock payment without needing recipient online. Useful for onboarding / airdropping.

  • conditionType = “LINKED_TRANSFER”

  • params = LinkedTransferParameters

// linked transfer
const linkedParams: LinkedTransferParameters = {
  amount: parseEther("0.1").toString(),
  assetId: "0x0000000000000000000000000000000000000000" // ETH
  conditionType: "LINKED_TRANSFER",
  paymentId: createPaymentId(), // bytes32 hex string
  preImage: createPreImage(),// bytes32 hex string, shared secret
};

await conditionalTransfer(linkedParams);
Linked Transfer To Recipient

Generates a linked transfer which only the specified recipient can unlock. Useful for normal transfers where liveness is not guaranteed.

  • conditionType = “LINKED_TRANSFER_TO_RECIPIENT”

  • params = LinkedTransferToRecipientParameters

// linked transfer
const linkedParams: LinkedTransferToRecipientParameters = {
  amount: parseEther("0.1").toString(),
  assetId: "0x0000000000000000000000000000000000000000" // ETH
  conditionType: "LINKED_TRANSFER",
  paymentId: createPaymentId(), // bytes32 hex string
  preImage: createPreImage(),// bytes32 hex string, shared secret
  recipient: "xpub..." // recipient public identifier
};

await conditionalTransfer(linkedParams);
Fast Signed Transfer

Creates a persistent transfer app that reduces latency for micropayment use cases. Each transfer must specify a signer Ethereum address who can resolve the transfer with a signature on the payload, which consists of the paymentId hashed with arbitrary data.

export type FastSignedTransferParameters<T = string> = {
  conditionType: typeof FAST_SIGNED_TRANSFER;
  recipient: string;
  amount: T;
  assetId?: string;
  paymentId: string;
  maxAllocation?: T; // max amount to allocate to this app. if not specified, it will use the full channel balance
  signer: string;
  meta?: object;
};

export type FastSignedTransferResponse = {
  transferAppInstanceId: string; // app instance Id for installed application
};

// generate random payment ID
const paymentId = hexlify(randomBytes(32));
const { transferAppInstanceId } = (await client.conditionalTransfer({
  amount: parseEther("0.01").toString(),
  conditionType: FAST_SIGNED_TRANSFER,
  paymentId: ,
  recipient: "xpub...",
  signer: "0xAAA0000000000000000000000000000000000000",
  assetId: AddressZero,
  meta: { foo: "bar" },
} as FastSignedTransferParameters)) as FastSignedTransferResponse;
More conditional types are under active development, reach out for specific requests!
resolveCondition

Resolves a conditional transfer.

resolveCondition: (params: ResolveConditionParameters<string>) => Promise<ResolveConditionResponse>
Condition Types
Linked Transfer / Linked Transfer To Recipient
const resolveParams: ResolveLinkedTransferParameters = {
  amount: parseEther("0.1").toString(),
  assetId: "0x0000000000000000000000000000000000000000" // ETH
  conditionType: "LINKED_TRANSFER",
  paymentId: receivedPaymentId, // bytes32 hex string
  preImage: receivedPreImage // bytes32 hex string, shared secret
};

await resolveCondition(resolveParams);
Fast Signed Transfer
export type ResolveFastSignedTransferParameters = {
  conditionType: typeof FAST_SIGNED_TRANSFER;
  paymentId: string;
  data: string;
  signature: string;
};

export type ResolveFastSignedTransferResponse<T = string> = {
  appId: string;
  sender: string;
  paymentId: string;
  amount: T;
  assetId: string;
  signer: string;
  meta?: object;
};

// fast signed transfer has already been created 
const withdrawerSigningKey = new SigningKey(signerWallet.privateKey);
const digest = solidityKeccak256(["bytes32", "bytes32"], [data, paymentId]);
const signature = joinSignature(withdrawerSigningKey.signDigest(digest));

const res: ResolveFastSignedTransferResponse = await clientB.resolveCondition({
  conditionType: FAST_SIGNED_TRANSFER,
  paymentId,
  signature,
  data,
} as ResolveFastSignedTransferParameters);
requestDepositRights

Requests deposit rights to enable multisig transfers to top up channel balance.

requestDepositRights: (params: RequestDepositRightsParameters) => Promise<RequestDepositRightsResponse>
Example
const requestParams: RequestDepositRightsParameters = {
  assetId: "0x0000000000000000000000000000000000000000" // ETH
};

await requestDepositRights(requestParams);
rescindDepositRights

Rescinds deposit rights to “reclaim” deposited funds in free balance and allow node to request rights.

rescindDepositRights: (params: RescindDepositRightsParameters) => Promise<RescindDepositRightsResponse>
Example
const rescindParams: RescindDepositRightsParameters = {
  assetId: "0x0000000000000000000000000000000000000000" // ETH
};

await rescindDepositRights(rescindParams);
checkDepositRights

Checks the current status of the deposit rights on the channel.

checkDepositRights: (params: CheckDepositRightsParameters) => Promise<CheckDepositRightsResponse>
Example
const checkParams: CheckDepositRightsParameters = {
  assetId: "0x0000000000000000000000000000000000000000" // ETH
};

const depositRights = await checkDepositRights(rescindParams);
console.log("depositRights: ", depositRights);
on

Starts an event listener for channel events. See Advanced - Event Monitoring for a list of channel events.

on: (event: ConnextEvents, (cb: any => any) => void) => void
Example
connext.on("depositStartedEvent", () => {
  console.log("Your deposit has begun");
  this.showDepositStarted();
});
withdraw

Withdraws funds from a channel to a specified onchain recipient.

withdraw: (WithdrawParams) => Promise<ChannelState>
Example
const payload: WithdrawParams = {
  recipient: "0xe43...", // optional, defaults to accounts[0]
  amount: "100"
  assetId: "0x0000000000000000000000000000000000000000"
}

await withdraw(payload)

Generalized State Methods

getPaymentProfile
getPaymentProfile: () => Promise<PaymentProfile>
Example
await getPaymentProfile();
getAppState
getAppState: (appInstanceId: string) => Promise<GetStateResult>
Example
await getAppState("0xabc...");
getFreeBalance
getFreeBalance: (assetId: string) => Promise<GetFreeBalanceStateResult>
Example
// to get the ETH free balance in an object indexed by your
// freeBalanceAddress
await getFreeBalance("0x0000000000000000000000000000000000000000");

Low Level Channel API (mapped to CF node)

proposeInstallApp
proposeInstallApp: (params: ProposeInstallParams) => Promise<ProposeInstallResult>
Example
// initial state of your application, must match encoding
const initialState = {
  coinTransfers: [
    {
      amount: new BigNumber(1000),
      to: "xpub....",
    },
    {
      amount: new BigNumber(0),
      to: "xpub...",
    },
  ],
};

const params: ProposeInstallVirtualParams = {
  abiEncodings: { // encodings matching .sol file of app
    actionEncoding: "",
    stateEncoding: ""
  },
  appDefinition: "0xdef..." // create2 address of app
  initialState,
  initiatorDeposit: new BigNumber(1000), // wei units
  initiatorDepositTokenAddress: "0x0000...", // assetId, AddressZero for ethereum
  intermediaryIdentifier: "xpub...", // xpub of intermediary node, returned from config endpoint
  outcomeType: appInfo.outcomeType, // OutcomeType
  proposedToIdentifier: "0xabc...",
  responderDeposit: new BigNumber(0), // wei units
  responderDepositTokenAddress: "0x0000...", // assetId, AddressZero for ethereum,
  timeout: new BigNumber(0)
};

await proposeInstallApp(params);
installApp
installApp: (appInstanceId: string) => Promise<InstallResult>
Example
await installApp("0xabc...");
rejectInstallApp
rejectInstallApp: (appInstanceId: string) => Promise<UninstallResult>
Example
await rejectInstallApp("0xabc...");
uninstallApp
uninstallApp: (appInstanceId: string) => Promise<UninstallResult>
Example
await uninstallApp("0xabc...");
installVirtualApp
installVirtualApp: (appInstanceId: string) => Promise<InstallVirtualResult>
Example
await installVirtualApp("0xabc..");
takeAction
takeAction: (appInstanceId: string, action: SolidityValueType) => Promise<TakeActionResult>
Example
// action below is used in resolving linked transfers
const action = {
  preImage: "0xfec..."
};
await takeAction("0xabc...", action);
updateState
updateState: (appInstanceId: string, newState: SolidityValueType) => Promise<UpdateStateResult>
Example
await updateState("0xabc...", { preImage: createPreImage() });
getProposedAppInstance
getProposedAppInstance: (appInstanceId: string) => Promise<GetProposedAppInstanceResult | undefined>
Example
await getProposedAppInstance("0xabc...");

@connext/store

Exported Types:

Exported Classes:

Exported Functions:

Types

IAsyncStorage

The interface for React Native’s AsyncStorage. For example, what you get from importing @react-native-community/async-storage.

interface IAsyncStorage {
  getItem(key: string): Promise<string | null>;
  setItem(key: string, value: string): Promise<void>;
  removeItem(key: string): Promise<void>;
}
IBackupService

The interface implemented by backup services such as Pisa which provide a remote location for storing channel commitments (required for on-chain disputes). If an IBackupService is given to a store then it will send all generated commitments to that service for safe keeping or to enable it to act as a watchtower on your behalf.

interface IBackupService {
  restore(): Promise<Array<{ path: string, value: any }>>;
  backup(pair: { path: string, value: any }): Promise<void>;
}
IStoreService

The interface containing all the read/write methods that the core protocol needs to interact with your off-chain state. It’s relatively complicated, you can see the type definition at indra/modules/types/src/store.ts but you shouldn’t ever need to deal w this type directly, that’s what the functions exported by this lib are for. 😉

Classes

PisaBackupService

An IBackupService configured to work with Pisa’s state backup service.

class PisaBackupService implements IBackupService {
  constructor(pisaClient: string, wallet: Wallet)
}
Constructor Params
  1. pisaUrl: The URL that points to our Pisa backup service endpoint.

  2. wallet: an ethers Wallet, used to authenticate with Pisa.

Example Usage
import { PisaBackupService, getLocalStore } from "@connext/store";
import { connect } from "@connext/client";
import { Wallet } from "ethers";

const pisaUrl = "https://example.com";
const wallet = Wallet.createRandom();

const channel = connect({
  store: getLocalStore({ backupService: new PisaBackupService(pisaUrl, wallet) }),
});

channel.deposit({ ... });
channel.transfer({ ... });
channel.withdraw({ ... });

Functions

getAsyncStore
getAsyncStore(storage: IAsyncStorage, backupService?: IBackupService): IStoreService;
Params
  1. storage:: IAsyncStorage

  2. backupService: IBackupService (optional)

Returns

An IStoreService configured to save data in React Native’s Async Storage.

getFileStore
getFileStore(fileDir: string, backupService?: IBackupService): IStoreService;
Params
  1. fileDir:: string (optional) The path to a folder where the files containing store data will be saved. Defaults to ./.connext-store;

  2. backupService: IBackupService (optional)

Returns

An IStoreService configured to save data to a collection of files in fileDir.

getLocalStore
getLocalStore(backupService?: IBackupService): IStoreService;
Params
  1. backupService: IBackupService (optional)

Returns

An IStoreService configured to save data to a browser’s localStorage.

getMemoryStore
getMemoryStore(backupService?: IBackupService): IStoreService;
Params
  1. backupService: IBackupService (optional)

Returns

Returns an IStoreService configured to not save data & keep everything in memory. Good for testing, not good for managing real channel data.

getPostgresStore
getPostgresgStore(connectionUri: string, backupService?: IBackupService): IStoreService;
Params
  1. connectionUri:: string Should look something like postgres://user:password@host:port/database.

  2. backupService: IBackupService (optional)

Returns

Returns an IStoreService configured to save data to a postgres database.

@connext/types

ClientOptions

Object including the options to instantiate the connext client with.

The object contains the following fields:

Name Type Description
rpcProviderUrl String the Web3 provider URL used by the client
nodeUrl String url of the node
mnemonic? String (optional) Mnemonic of the signing wallet
externalWallet? any (optional) External wallet address
channelProvider? ChannelProvider (optional) Injected ChannelProvider
keyGen? () => Promise Function passed in by wallets to generate ephemeral keys
store? object Maps to set/get from CF. Defaults localStorage
logLevel? number Depth of logging
natsUrl? String Initially hardcoded
natsClusterId? String Initially hardcoded
natsToken? String Initially hardcoded

If the optional values are not provided, they will default to the ones that synchronize with the hub’s configuration values. However, you must pass at least one signing option (mnemonic, externalWallet, or channelProvider).

Object literals

Type aliases

BigNumber

BigNumber: BigNumber

CallbackStruct

CallbackStruct: object

Type declaration
ConditionResolvers

ConditionResolvers: object

Type declaration
ConditionalExecutors

ConditionalExecutors: object

Type declaration
InternalClientOptions

InternalClientOptions: ClientOptions & object

ProposalValidator

ProposalValidator: object

Type declaration

Variables

API_TIMEOUT

API_TIMEOUT: 5000 = 5000

BigNumber

BigNumber: BigNumber = utils.BigNumber

MAX_RETRIES

MAX_RETRIES: 20 = 20

createPaymentId

createPaymentId: createRandom32ByteHexString = createRandom32ByteHexString

createPreImage

createPreImage: createRandom32ByteHexString = createRandom32ByteHexString

baseAppValidation
  • baseAppValidation(app: AppInstanceInfo, registeredInfo: RegisteredAppDetails, isVirtual: boolean, connext: ConnextInternal): Promise

  • Defined in validation/appProposals.ts:125

Parameters
app: AppInstanceInfo
registeredInfo: RegisteredAppDetails
isVirtual: boolean
connext: ConnextInternal
Returns Promise
calculateExchange
Parameters
amount: BigNumber
swapRate: string
Returns BigNumber
capitalize
Parameters
str: string
Returns string
connect
  • connect(opts: ClientOptions): Promise<ConnextInternal>

  • Defined in connext.ts:62

    Creates a new client-node connection with node at specified url

Parameters
opts: ClientOptions
The options to instantiate the client with. At a minimum, must contain the nodeUrl and a client signing key or mnemonic
Returns Promise<ConnextInternal>
createLinkedHash
  • createLinkedHash(action: UnidirectionalLinkedTransferAppActionBigNumber): string

  • Defined in lib/utils.ts:65

Parameters
action: UnidirectionalLinkedTransferAppActionBigNumber
Returns string
createRandom32ByteHexString
Returns string
delay
Parameters
ms: number
Returns Promise<void>
falsy
Parameters
x: string | undefined
Returns boolean
freeBalanceAddressFromXpub
  • freeBalanceAddressFromXpub(xpub: string): string

  • Defined in lib/utils.ts:61

Parameters
xpub: string
Returns string
insertDefault
  • insertDefault(val: string, obj: any, keys: string[]): any

  • Defined in lib/utils.ts:39

Parameters
val: string
obj: any
keys: string[]
Returns any
invalid32ByteHexString
Parameters
value: any
Returns string | undefined
invalidAddress
Parameters
value: string
Returns string | undefined
invalidXpub
Parameters
value: string
Returns string | undefined
isValidAddress
Parameters
value: any
Returns boolean
notBigNumber
Parameters
value: any
Returns string | undefined
notBigNumberish
Parameters
value: any
Returns string | undefined
notGreaterThan
  • notGreaterThan(value: any, ceil: utils.BigNumberish): string | undefined

  • Defined in validation/bn.ts:31

Parameters
value: any
ceil: utils.BigNumberish
Returns string | undefined
notGreaterThanOrEqualTo
  • notGreaterThanOrEqualTo(value: any, ceil: utils.BigNumberish): string | undefined

  • Defined in validation/bn.ts:40

Parameters
value: any
ceil: utils.BigNumberish
Returns string | undefined
notLessThan
  • notLessThan(value: any, floor: utils.BigNumberish): string | undefined

  • Defined in validation/bn.ts:50

Parameters
value: any
floor: utils.BigNumberish
Returns string | undefined
notLessThanOrEqualTo
  • notLessThanOrEqualTo(value: any, floor: utils.BigNumberish): string | undefined

  • Defined in validation/bn.ts:59

Parameters
value: any
floor: utils.BigNumberish
Returns string | undefined
notNegative
Parameters
value: any
Returns string | undefined
notPositive
Parameters
value: any
Returns string | undefined
objMap
  • objMap<T, F, R>(obj: T, func: function): object

  • Defined in lib/utils.ts:13

Type parameters
T
  • F: keyof T
  • R
    Parameters
    obj: T
    func: function
    *   *   (val: T\[F\], field: F): R
    
        *   #### Parameters
    
            ##### val: T\[F\]
    
            ##### field: F
    
    
            #### Returns R
    
    Returns object
    objMapPromise
    • objMapPromise<T, F, R>(obj: T, func: function): Promise

    • Defined in lib/utils.ts:26

    Type parameters
    T
  • F: keyof T
  • R
    Parameters
    obj: T
    func: function
    *   *   (val: T\[F\], field: F): Promise<R\>
    
        *   #### Parameters
    
            ##### val: T\[F\]
    
            ##### field: F
    
    
            #### Returns Promise<R\>
    
    Returns Promise<object>
    prettyLog
    Parameters
    app: AppInstanceInfo
    Returns string
    publicIdentifierToAddress
    • publicIdentifierToAddress(publicIdentifier: string): string

    • Defined in lib/utils.ts:57

    Parameters
    publicIdentifier: string
    Returns string
    replaceBN
    Parameters
    key: string
    value: any
    Returns any
    validateLinkedTransferApp
    • validateLinkedTransferApp(app: AppInstanceInfo, registeredInfo: RegisteredAppDetails, isVirtual: boolean, connext: ConnextInternal): Promise

    • Defined in validation/appProposals.ts:100

    Parameters
    app: AppInstanceInfo
    registeredInfo: RegisteredAppDetails
    isVirtual: boolean
    connext: ConnextInternal
    Returns Promise
    validateSimpleTransferApp
    • validateSimpleTransferApp(app: AppInstanceInfo, registeredInfo: RegisteredAppDetails, isVirtual: boolean, connext: ConnextInternal): Promise

    • Defined in validation/appProposals.ts:68

    Parameters
    app: AppInstanceInfo
    registeredInfo: RegisteredAppDetails
    isVirtual: boolean
    connext: ConnextInternal
    Returns Promise
    validateSwapApp
    • validateSwapApp(app: AppInstanceInfo, registeredInfo: RegisteredAppDetails, isVirtual: boolean, connext: ConnextInternal): Promise

    • Defined in validation/appProposals.ts:18

    Parameters
    app: AppInstanceInfo
    registeredInfo: RegisteredAppDetails
    isVirtual: boolean
    connext: ConnextInternal
    Returns Promise
    validateTransferApp
    • validateTransferApp(app: AppInstanceInfo, registeredInfo: RegisteredAppDetails, isVirtual: boolean, connext: ConnextInternal): Promise

    • Defined in validation/appProposals.ts:37

    Parameters
    app: AppInstanceInfo
    registeredInfo: RegisteredAppDetails
    isVirtual: boolean
    connext: ConnextInternal
    Returns Promise

    Object literals

    appProposalValidation

    appProposalValidation: object

    SimpleTransferApp

    SimpleTransferApp: validateSimpleTransferApp = validateSimpleTransferApp

    SimpleTwoPartySwapApp

    SimpleTwoPartySwapApp: validateSwapApp = validateSwapApp

    UnidirectionalLinkedTransferApp

    UnidirectionalLinkedTransferApp: validateLinkedTransferApp = validateLinkedTransferApp

    UnidirectionalTransferApp

    UnidirectionalTransferApp: validateTransferApp = validateTransferApp

    @connext/utils

    Exported Classes:

    Classes

    ChannelSigner

    An extended ethers Signer interface configured to add support for encrypting/decrypting messages and remove support for signing generic Ethereum messages.

    import { Signer } from "ethers";
    
    class ChannelSigner extends Signer{
      constructor(privateKey: string, ethProviderUrl?: string);
    
      // Properties
      address: string;
      publicKey: string;
      publicIdentifier: string;
    
      // Methods
      decrypt(message: string): Promise<string>;
      encrypt(message: string, publicKey: string): Promise<string>;
      signMessage(message: string): Promise<string>;
    }
    

    Contributing

    All source code for the contracts, as well as information about contributing to Counterfactual, can be found here.

    For comprehensive documentation on the Counterfactual Framework, please refer to the CF Protocol Specification.

    Guide

    We value curiosity, enthusiasm, and determination. Anyone who is excited about state channels and our vision is welcome to contribute to Connext. While we’re always looking for developers, there are plenty of other ways to contribute through documentation, design, marketing, and even translation. If you’re not sure how best to contribute, reach out to a team member and we’ll be happy to find a way for you to help out.

    Our documentation includes information on Ethereum and state channels for both part-time and core contributors to the project, while LearnChannels provides a comprehensive introduction to state channel technology.

    Feel free to fork our repo and start creating PR’s after assigning yourself to an issue in the repo. We are always chatting on Discord, so drop us a line there if you want to get more involved or have any questions on our implementation!

    We encourage contributors to:

    • Engage in Discord conversations and questions on how to begin contributing to the project

    • Open up github issues to express interest in code to implement

    • Open up PRs referencing any open issue in the repo. PRs should include:

      • Detailed context of what would be required for merge

      • Tests that are consistent with how other tests are written in our implementation

      • Proper labels, milestones, and projects (see other closed PRs for reference)

    • Follow up on open PRs

    • Have an estimated timeframe to completion and let the core contributors know if a PR will take longer than expected

    • We do not expect all part-time contributors to be experts on all the latest documentation, but all contributors should at least be familiarized with our documentation and LearnChannels.

    Contribution Steps

    When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change.

    Please note we have a code of conduct, please follow it in all your interactions with the project.

    Our pull request workflow is as follows:

    1. Ensure any install or build dependencies are removed before the last commit.

    2. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters.

    3. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is SemVer.

    4. You should sign your commits if possible with GPG.

    5. Commits should be atomic changes with clear code comments and commit messages.

    6. All builds must be passing, new functionality should be covered with tests.

    7. Once you commit your changes, please tag a core team member to review them.

    8. You may merge the Pull Request in once you have the sign-off of at least one core team member, or if you do not have permission to do that, you may request the reviewer to merge it for you.

    Code of Conduct

    Our Pledge

    In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.

    Our Standards

    Examples of behavior that contributes to creating a positive environment include:

    • Using welcoming and inclusive language

    • Being respectful of differing viewpoints and experiences

    • Gracefully accepting constructive criticism

    • Focusing on what is best for the community

    • Showing empathy towards other community members

    Examples of unacceptable behavior by participants include:

    • The use of sexualized language or imagery and unwelcome sexual attention or advances

    • Trolling, insulting/derogatory comments, and personal or political attacks

    • Public or private harassment

    • Publishing others’ private information, such as a physical or electronic address, without explicit permission

    • Other conduct which could reasonably be considered inappropriate in a professional setting

    Our Responsibilities

    Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.

    Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

    Scope

    This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.

    Enforcement

    Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@connext.network. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.

    Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project’s leadership.

    Attribution

    This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available here

    Workflow Protocols

    These are guidelines that the Connext team uses internally to coordinate our development. Suggestions for improvements are welcome!

    Goals

    Coordinate several devs working across several time zones to maximize energy spent on useful stuff & minimize wasted work.

    “Useful” work to be maximized:

    • Building new features

    • fixing existing bugs w/out introducing new ones

    • Reviewing & providing feedback on work other teammates have done

    “Wasteful” work to be minimized:

    • Putting out fires caused by new bugs making it to prod

    • Re-doing any work that’s already been done in another branch

    • Fixing new bugs that someone else pushed/merged w/out noticing (the person who introduces a bug probably has more context and is the one who can fix it the fastest)

    • Resolving ugly merge conflicts

    Protocols

    Merging code to sync up development (aka CI)
    • Merge feature -> staging frequently. CI must pass before merging and either CD must pass or someone else must have reviewed your code ie CI && (CD || Review)

    • If staging CD is failing, branch off to fix it (ie don’t push commits directly to staging). These CD-hotfix branches should be merged into staging as soon as their ready w/out necessarily waiting for a review.

    • Code reviews: at the start of each day, everyone should review and merge other people’s “pending review” PRs. Once the “pending review” queue is cleared (or only contains our own PRs), then we’re free to work on our own feature branches for the day.

    Deploying to staging/prod (aka CD)
    • Note: Staging CD tests a local copy of the code (doesn’t use any @connext/* npm packages). Master CD tests/deploys connext packages from the npm registry.

    • run bash ops/npm-publish.sh after staging CD has passed & before merging into master to trigger deploying to prod.

    • Don’t manually merge staging into master, use this helper script: bash ops/deploy-indra.sh. This script will:

      • Detect & abort if any merge conflicts exist between staging & master.

      • Ask you for a version number & run some sanity checks to make sure this version is valid

      • Update the version in the root package.json

      • Merge staging into master & then fast-forward merge master into staging so they’re at the same point.

      • Tag & push this new merge commit

    • Once master has been pushed, the CD pipeline will automatically be activated.

    Handling Backwards Incompatibilities
    • A set of checks are in place to partially check for backwards incompatible changes. As we build out our integration tests, future checks will be more complete.

    • There’s a version hard-coded in Makefile (look for a variable called backwards_compatible_version). If this variable is set to 2.3.20, for example then the CD test suite for indra v2.4.5 will test it against both of:

      • indra_test_runner:2.3.20

      • indra_test_runner:2.4.5

    • To get the backwards compatibility check to pass after introducing a breaking change, you must change the backwards_compatible_version in Makefile to a compatible version. Ideally, you’d increment the major version eg 2.4.4 ->3.0.0 and then set the backwards_compatible_version to 3.0.0 so that, moving forward, all 3.x.x versions are tested for compatibility w 3.0.0. (an automated calculation is commented out but later could be activated to always run test-runner version a.0.0 for any version a.b.c)

    • Continuing the wild-west-style version management: if you want to increment 2.4.4 -> 2.4.5 after introducing a breaking change, you can also set backwards_compatible_version to be 2.4.5.

    Rolling back prod

    Situation: recent prod release is broken & we want to roll-back to an old version

    • ssh onto prod server & cd indra

    • git fetch --all

    • git checkout <target-version>

    • make restart-prod Important note: restarting locally on the server (instead of automatically deploying via CD) means the repo’s secrets (see github -> indra -> settings -> secrets) don’t get injected. In this situation, the env vars used come from ~/.bashrc instead so verify that this file has the env vars we need.

    Hot-fixing prod while staging is broken (via CD)

    Situation: staging needs some repairs before it’s ready to be deployed but there’s something urgent we need to fix in prod.

    • Create hotfix branch directly off of master & develop/cherry-pick the hotfix here

    • Push hotfix branch & make sure it passes all CI/CD steps

    • Merge hotfix branch into master if it looks good.

    • Wait ~20 mins for CD to deploy change to prod.

    • Check out hotfix & make sure it does what you want. See rollback instructions if it makes things more broken.

    Hot-fixing (skip CD)

    Situation: we need to get a change deployed to prod as quickly as possible.

    • Write hotfix and push change to master.

    • Build/push images: If you have a beefy laptop & fast internet you can do this manually via make prod && make push-release (or use make push-staging to hotfix staging). Otherwise, it’s probably faster to just wait for CD to build/push stuff as part of the build step.

    • Once images are pushed:

      • ssh onto the target server & cd into indra clone

      • git fetch --``all

      • git checkout <target commit/release>

      • make restart-prod

    • Check to make sure your hotfix got deployed & does what we expect. Be aware that CD is probably still testing this commit in the background and it will be redeployed maybe 10 mins after you manually deployed it.

    Tagging docker images

    Docker images are tagged & pushed automatically during CD, you shouldn’t ever have to push/pull images manually. There are 3 important image tag types:

    • Commit tags eg 3dffdc17, these are built & pushed during the first step of either feature or staging CD. Later steps of feature/staging CD will pull & run tests against these images (and deploy them if staging and tests pass). These are built using local code (ie local modules aren’t pulled from the npm registry).

    • Release tags eg 1.2.3, these are built & pushed during the first step of master CD and then tested/deployed during later steps. These images use code from the npm registry, not local code.

    • latest tag is always pushed when pushing either commit or release tagged images, these latest images are only used in CD as a cache to make building go faster so if they’re corrupted then everything’ll be fine but building will take longer. These images will be overwritten frequently so don’t pull them expecting anything specific (if you want specific images, use commit-tagged ones). Local images built & run by make start will be tagged latest so beware: they will be overwritten if you make pull-latest (which shouldn’t ever be necessary to do during normal dev workflows) Under the hood: the helper scripts ops/push-images and ops/pull-image are used by make commands, they:

    • Both accept one arg: the version to push or pull eg 3dffdc17 or 1.2.3

    • Contain a list of all the images to push/pull (eg node, database, proxy, etc)

    • Push latest images too whenever we ask to push commit or release tagged images

    • Protect us from overwriting an already pushed image eg can’t push images tagged 3dffdc17 more than once.

    Setting up CI/CD

    All auto-deployment config can be found in .github/workflows/. See GitHub Actions Documentation for docs.

    The auto-deployer needs an ssh key so it can login to the prod server, we should use a fresh one instead of re-using existing ssh keys. Run this command to generate a new ssh key pair: ssh-keygen -t rsa -b 4096 -C "autodeployer" -m pem -f .ssh/autodeployer. The ops/setup-ubuntu.sh script will look for a public key called $HOME/.ssh/autodeployer.pub and try to add it to the server’s ~/.ssh/authorized_keys. If we ever change the autodeployer’s ssh key, we can add the new keys to our servers by re-running bash ops/setup-ubuntu.sh $SERVER_IP.

    Env vars controlling CD are store in: GitHub -> Indra Repo -> Settings -> Secrets. The following env vars are used:

    • DOCKER_USER & DOCKER_PASSWORD: Login credentials for someone with push access to the docker repository specified by the registry vars at the top of the Makefile & ops/start-prod.sh.

    • INDRA_ADMIN_TOKEN: an admin token for controlling access to sensitive parts of the dashboard.

    • INDRA_AWS_ACCESS_KEY_ID & INDRA_AWS_SECRET_ACCESS_KEY: Login credentials for an AWS storage repo, if provided the database will automatically send DB snapshots to AWS.

    • INDRA_LOGDNA_KEY: Credentials for LogDNA. If provided, all logs will be sent to this service for further analysis.

    • MAINNET_ETH_PROVIDER & RINKEBY_ETH_PROVIDER: Ethereum RPC Urls eg Alchemy or Infura that let us read from/write to blockchain.

    • SSH_KEY: The autodeployer private ssh key

    Documentation Documentation

    If you find a mistake in our documentation, we’d be thrilled to receive a pull-request from you with the fix.

    To get started, fork Indra & browse the files in indra/docs/src.

    You can build our docs with the following command:

    make docs
    

    Once the docs are build, you can view them locally by opening docs/build/html/index.html in a web browser. Refresh the page to display changes after re-building.

    If the docs look good locally, push changes to your fork & submit a PR to our repo. Thanks!