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:
Integrating state channels into your dApp or wallet, see Getting Started
Running your own node, see Running your own Node
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:
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.
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.
When either party is done transacting, they can take the latest update to the smart contract and unlock the finalized funds.
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.
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!).
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 ethProvider
currently. 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:
The transaction must be both sent and confirmed after deposit rights are requested but before they are rescinded.
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¶
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 implementedsrc/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:
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 theNode
. If no such value exists for this key, theNode
produces an extended private key and sets it at this key. This extended private key is then used to generate a “public identifer” for theNode
(the address by which theNode
is known by). It is also used to generate private keys which are specific toAppInstance
s.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 atAppInstance
-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 specificAppInstance
s.
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”
Control Flow¶
arrows mostly indicate “calls”
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¶
Link¶
Link transfers let you to create a preloaded link that can be redeemed for a given amount of funds. When you create a link, the amount of the link is deducted from your channel balance and the funds are locked by the node pending the receipt of a secret. Next, a link is generated with that secret. It cannot be regenerated, so don’t lose it or you’ll lose those funds! This link (and only this link) can be used to unlock those funds.
Link payments are a useful tool for on-boarding new users and/or creating prepaid accounts.
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¶
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 nethttps://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).
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.
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}`);
})()
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¶
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 nethttps://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).
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.
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.
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}`);
})()
@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:
messagingService
:IMessagingService
storeService
:IStoreService
networkContext
:NetworkContext
nodeConfig
:NodeConfig
: Basically just the store prefix which idk if this is even needed any more.provider
:JsonRpcProvider
signer
:IChannelSigner
lockService
?:ILockService
blocksNeededForConfirmation
?:number
: This should probably just be hard coded.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:
appInstance:
AppInstanceInfo
Successfully installed app instance
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:
appInstance:
AppInstanceInfo
App instance details
Method: getProposedAppInstance
¶
Get details of a proposed app instance.
Params:
appIdentityHash: string
ID of the app instance to get details of
Result:
appInstance:
AppInstanceInfo
App instance details
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:
newState:
AppState
New app state
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:
appInstance:
AppInstanceInfo
Newly installed app instance
Event: rejectInstallEvent
¶
Fired if installation of a new app instance was rejected.
Data:
appInstance:
AppInstanceInfo
Rejected app instance
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:
appInstance:
AppInstanceInfo
Uninstalled app instance
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’sAppABIEncodings
.
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’sAppABIEncodings
.
@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¶
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);
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);
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;
resolveCondition¶
Resolves a conditional transfer.
resolveCondition: (params: ResolveConditionParameters<string>) => Promise<ResolveConditionResponse>
Condition Types¶
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);
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();
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);
@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¶
pisaUrl
: The URL that points to our Pisa backup service endpoint.wallet
: an ethersWallet
, 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¶
storage:
:IAsyncStorage
backupService
:IBackupService
(optional)
Returns¶
An IStoreService
configured to save data in React Native’s Async Storage.
getFileStore¶
getFileStore(fileDir: string, backupService?: IBackupService): IStoreService;
Params¶
fileDir:
:string
(optional) The path to a folder where the files containing store data will be saved. Defaults to./.connext-store
;backupService
:IBackupService
(optional)
Returns¶
An IStoreService
configured to save data to a collection of files in fileDir
.
getLocalStore¶
getLocalStore(backupService?: IBackupService): IStoreService;
Params¶
backupService
:IBackupService
(optional)
Returns¶
An IStoreService
configured to save data to a browser’s localStorage
.
getMemoryStore¶
getMemoryStore(backupService?: IBackupService): IStoreService;
Params¶
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¶
connectionUri:
:string
Should look something likepostgres://user:password@host:port/database
.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).
Type aliases¶
Types¶
Object literals¶
Type aliases¶
ConditionResolvers¶
ConditionResolvers: object
Defined in controllers/ResolveConditionController.ts:14
Type declaration¶
ProposalValidator¶
ProposalValidator: object
Defined in validation/appProposals.ts:9
Type declaration¶
Variables¶
createPaymentId¶
createPaymentId: createRandom32ByteHexString = createRandom32ByteHexString
Defined in lib/utils.ts:78
createPreImage¶
createPreImage: createRandom32ByteHexString = createRandom32ByteHexString
Defined in lib/utils.ts:79
baseAppValidation¶
baseAppValidation(app: AppInstanceInfo, registeredInfo: RegisteredAppDetails, isVirtual: boolean, connext: ConnextInternal): Promise
Defined in validation/appProposals.ts:125
Parameters¶
Returns Promise¶
calculateExchange¶
calculateExchange(amount: BigNumber, swapRate: string): BigNumber
Defined in controllers/SwapController.ts:19
Returns BigNumber¶
connect¶
connect(opts: ClientOptions): Promise<ConnextInternal>
Defined in connext.ts:62
Creates a new client-node connection with node at specified url
Parameters¶
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
Returns string¶
createRandom32ByteHexString¶
createRandom32ByteHexString(): string
Defined in lib/utils.ts:74
Returns string¶
freeBalanceAddressFromXpub¶
freeBalanceAddressFromXpub(xpub: string): string
Defined in lib/utils.ts:61
Returns string¶
insertDefault¶
insertDefault(val: string, obj: any, keys: string[]): any
Defined in lib/utils.ts:39
Returns any¶
invalid32ByteHexString¶
invalid32ByteHexString(value: any): string | undefined
Defined in validation/hexStrings.ts:4
Returns string | undefined¶
invalidAddress¶
invalidAddress(value: string): string | undefined
Defined in validation/addresses.ts:24
Returns string | undefined¶
invalidXpub¶
invalidXpub(value: string): string | undefined
Defined in validation/addresses.ts:16
Returns string | undefined¶
isValidAddress¶
isValidAddress(value: any): boolean
Defined in validation/addresses.ts:4
Returns boolean¶
notBigNumber¶
notBigNumber(value: any): string | undefined
Defined in validation/bn.ts:15
Returns string | undefined¶
notBigNumberish¶
notBigNumberish(value: any): string | undefined
Defined in validation/bn.ts:21
Returns string | undefined¶
notGreaterThan¶
notGreaterThan(value: any, ceil: utils.BigNumberish): string | undefined
Defined in validation/bn.ts:31
Returns string | undefined¶
notGreaterThanOrEqualTo¶
notGreaterThanOrEqualTo(value: any, ceil: utils.BigNumberish): string | undefined
Defined in validation/bn.ts:40
Returns string | undefined¶
notLessThan¶
notLessThan(value: any, floor: utils.BigNumberish): string | undefined
Defined in validation/bn.ts:50
Returns string | undefined¶
notLessThanOrEqualTo¶
notLessThanOrEqualTo(value: any, floor: utils.BigNumberish): string | undefined
Defined in validation/bn.ts:59
Returns string | undefined¶
notNegative¶
notNegative(value: any): string | undefined
Defined in validation/bn.ts:72
Returns string | undefined¶
notPositive¶
notPositive(value: any): string | undefined
Defined in validation/bn.ts:68
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¶
* * (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¶
* * (val: T\[F\], field: F): Promise<R\>
* #### Parameters
##### val: T\[F\]
##### field: F
#### Returns Promise<R\>
Returns Promise<object>¶
prettyLog¶
prettyLog(app: AppInstanceInfo): string
Defined in validation/appProposals.ts:116
Returns string¶
publicIdentifierToAddress¶
publicIdentifierToAddress(publicIdentifier: string): string
Defined in lib/utils.ts:57
Returns string¶
validateLinkedTransferApp¶
validateLinkedTransferApp(app: AppInstanceInfo, registeredInfo: RegisteredAppDetails, isVirtual: boolean, connext: ConnextInternal): Promise
Defined in validation/appProposals.ts:100
Parameters¶
Returns Promise¶
validateSimpleTransferApp¶
validateSimpleTransferApp(app: AppInstanceInfo, registeredInfo: RegisteredAppDetails, isVirtual: boolean, connext: ConnextInternal): Promise
Defined in validation/appProposals.ts:68
Parameters¶
Returns Promise¶
validateSwapApp¶
validateSwapApp(app: AppInstanceInfo, registeredInfo: RegisteredAppDetails, isVirtual: boolean, connext: ConnextInternal): Promise
Defined in validation/appProposals.ts:18
Parameters¶
Returns Promise¶
validateTransferApp¶
validateTransferApp(app: AppInstanceInfo, registeredInfo: RegisteredAppDetails, isVirtual: boolean, connext: ConnextInternal): Promise
Defined in validation/appProposals.ts:37
Parameters¶
Returns Promise¶
Object literals¶
SimpleTransferApp¶
SimpleTransferApp: validateSimpleTransferApp = validateSimpleTransferApp
Defined in validation/appProposals.ts:110
SimpleTwoPartySwapApp¶
SimpleTwoPartySwapApp: validateSwapApp = validateSwapApp
Defined in validation/appProposals.ts:111
UnidirectionalLinkedTransferApp¶
UnidirectionalLinkedTransferApp: validateLinkedTransferApp = validateLinkedTransferApp
Defined in validation/appProposals.ts:112
UnidirectionalTransferApp¶
UnidirectionalTransferApp: validateTransferApp = validateTransferApp
Defined in validation/appProposals.ts:113
@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:
Ensure any install or build dependencies are removed before the last commit.
Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters.
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.
You should sign your commits if possible with GPG.
Commits should be atomic changes with clear code comments and commit messages.
All builds must be passing, new functionality should be covered with tests.
Once you commit your changes, please tag a core team member to review them.
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 to2.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 thebackwards_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 usemake 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 bymake start
will be taggedlatest
so beware: they will be overwritten if youmake pull-latest
(which shouldn’t ever be necessary to do during normal dev workflows) Under the hood: the helper scriptsops/push-images
andops/pull-image
are used bymake
commands, they:Both accept one arg: the version to push or pull eg
3dffdc17
or1.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 theregistry
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!