Skip to content

naik-ai/nxtp

 
 

Repository files navigation

Logo

Test Sops Deploy Testnet Build Test Deploy

Discord Twitter


About Connext

Connext is a bridging protocol which allows for safe transfer of tokens and data between EVM compatible blockchains.

NXTP

Noncustodial Xdomain Transfer Protocol.

Useful Links
Explore the docs »

View Mainnet Bridge
View Testnet Bridge (Connext Amarok)

Report Bug
Bug Bounty Program

Request Feature

Table of Contents
  1. About NXTP
  2. Getting Started
  3. Usage
  4. Roadmap
  5. Contributing
  6. License
  7. Contact
  8. Acknowledgments

Connext Architecture

The Connext architecture can be seen as a layered system, as follows:

Layer Protocol/Stakeholders
Application Layer Crosschain Applications (xApps)
Liquidity Layer NXTP
Gateway/Routing Layer Interchain Gateway Protocol
Messaging Layer Nomad
Transport Layer Connext Routers

About NXTP

Nxtp is a liquidity layer and a developer interface on top of the nomad optimisitic briding protocol.

(back to top)

Structure of this repo

This repo is set up as a Yarn workspace monorepo:

All main level scripts are defined in the main package.json at the root of the repo

Internal Design Principles

These are important and everyone must adhere to them:

  1. Keep it simple, stupid.

  2. Follow the Unix philosophy for every file and function. For instance, a listeners.ts file should only handle setting up listeners and then route to a corresponding handler. This keeps all business logic consolidated, making it easy to test and read.

  3. Every file and function should be unit tested. The scope of this codebase is very small, so it shouldn't be difficult to do this.

  4. Build for future hires and contributors. Every function should have a top-level comment that describes what it does, and internal comments breaking down each step. Files should have comments delineating their reponsibilities. Remember: Good code is never surprising.

Core Flow of a transaction through Connext

A transaction flowing through Connext will now have the following lifecycle:

A User will:

Initiate the transaction by calling a xcall function on our contracts, passing in funds, gas details, arbitrary data, and a target address object (includes chain info). Note that xcall is meant to mimic solidity's lower level call as best as possible.

Our contracts will:

-If needed, swap the passed in token to the Nomad version of the same asset. Call the Nomad contracts with a hash of the tx details to initiate the 30-60m message across chains.

-Emit an event with the tx details. Routers observing the origin chain with funds on the destination chain will: Simulate the transaction (if this fails, the assumption is that this is a more "expressive" crosschain message that requires authentication of the call and verification of the data, and so it must go through the slow Nomad process only).

Routers (Active Liquidity Providers) will:

-Prepare a signed transaction object using funds on the receiving chain. Post this object (a "bid") to the auctioneer. Note: if the router does not have enough funds for the transfer, they may also provide only part of the transfer's value, which gets split across multiple routers in the network.

-The sequencer collects bids from routers and allows routers 30 seconds per transfer to send bids. After this time, sequencer will select a bid randomly (selection process TBD) and submit the payload which contains the router's signature to the relayer network to be submitted to chain.

-When a given bid is submitted to chain, the contracts will do the following: Check that there are enough funds available for the transaction.

-Swap the router's Nomad-flavored funds for the canonical asset of the chain if needed. Send the swapped funds to the correct target (if it is a contract, this will also execute calldata against the target).

-Hash the router's params and store a mapping of this hash to the router's address in the contract.

--> At this point, the user's tx has already been completed!

Later, when the Nomad message arrives, a heavily batched tx can be submitted to take all pending hashes received over Nomad and look up whether they have corresponding router addresses in the hash -> router address mapping. If they do, then Nomad assets are minted and given to the router.

Note: if the router gives the incorrect amount of funds to a user or if they execute the wrong calldata, then the router's param hash will not match the hash coming over Nomad and the router will not get reimbursed. This is the core security mechanism that ensures that routers behave correctly.

Note 2: Routers will take a 30-60 minute lockup on their funds when relaying transactions. While this theoretically reduces capital efficiency compared to the existing system, in practice the lack of need to rebalance will mean that routers have more capital available more often regardless.

Architecture

  • adapters - Wrappers around external modules. These adapters can be shared between different packages.

    • Cache is a wrapper around all the redis based caches that are used.
    • Subrgaph contains all the code to read from the subgraph
    • TxService resiliently attempts to send transactions to chain (with retries, etc.) and is used to read and write to RPC providers, and has fallback providers if needed. Fallbacks can be defined as arrays and this way we can provide resiliency in case of failure
    • Web3Signer is a wrapper around Web3Signer, which is a secure way of signing which does not require to include mnemonics in the app itself.
  • agents - Any kind of backend service that we have running

    • Cartograpber is our chain indexer, which indexes from subgraph
    • Lighthouse is our implementation of a Watchtower, when transfers are fulfilled using slowpath, they are not executed by a router. So they need to be executed by another agent, and that's what lighthouse does
    • [Relayer](https://github.com/connext/nxtp/tree/main/packages/agents/relayer is an implementatino of a relayer in case we can't use Gelato
    • Router - listens for events from messaging service and subgraph, and then dispatches transactions to txService
    • SDK - is a JS wrapper around the contract calls themselves and can be used by integrations
    • Sequencer - is the agent module which is in charge of sourcing bids from routers and puts fast liquidity bids onto the chain itself.
  • Contracts - hold funds for all network participants, and lock/unlock based on data submitted by users and routers

  • deployments - These are things which we deploy

    • Contracts are the contracts that we deploy and the deployment scripts
    • Subgraph is all the subgraph source code to define all the mappings and contains all the configurations to deploy to different graph hosted services or third party graph providers
  • examples - these are not used in production, but contains ways to use the SDK that are illustrative of how to integrate NXTP

  • integration - Utilities for integration test

  • utils - A catchall for different types of helper functions that are shared thoughout the different packages

(back to top)

(back to top)

SDK Quickstart

The Connext SDK allows developers to interact with the Connext protocol in standard Node.js or web environments. This quickstart will go through how to build on top of Connext using the TypeScript SDK.

These examples (and others) can be found in our xApp Starter Kit, under src/sdk-interactions.

xApp Starter Kit


Cross-Chain Transfer

In this quickstart, we'll demonstrate how to execute an xcall to transfer funds from a wallet on Kovan to a destination address on Rinkeby.

1. Setup project

If you have an existing project, you can skip to Install dependencies.

Create the project folder and initialize the package.

mkdir node-examples && cd node-examples
yarn init

We'll use TypeScript / Node.js in this example.

yarn add @types/node typescript
yarn add -D @types/chai
yarn tsc --init

We want to use top-level await so we'll set the compiler options accordingly.

{
  "compilerOptions": {
    "outDir": "./dist",
    "target": "es2017",
    "module": "esnext",
    "moduleResolution": "node"
  },
  "exclude": ["node_modules"]
}

And add the following to package.json:

"type": "module",
"scripts": {
  "build": "tsc",
  "xtransfer": "node dist/xtransfer.js"
}

Create xtransfer.ts in project directory, where we will write all the code in this example.

mkdir src && touch src/xtransfer.ts

2. Install dependencies

Install the SDK.

yarn add @connext/nxtp-sdk

Also, install ethers.

yarn add ethers

3. Pull in imports

We only need a few imports for this example.

import { create, NxtpSdkConfig } from "@connext/nxtp-sdk";
import { ethers } from "ethers";

The rest of this guide will be working with this file.

4. Create a Signer

Use a wallet (i.e. MetaMask) as a Signer.

const privateKey = "<wallet_private_key>";
let signer = new ethers.Wallet(privateKey);

And connect it to a Provider on the sending chain (Infura, Alchemy, etc).

const provider = new ethers.providers.JsonRpcProvider("<kovan_rpc_url>");
signer = signer.connect(provider);
const signerAddress = await signer.getAddress();

5. Construct the NxtpSdkConfig

Fill in the placeholders with the appropriate provider URLs.

const nxtpConfig: NxtpSdkConfig = {
  logLevel: "info",
  signerAddress: signerAddress,
  chains: {
    "1111": {
      providers: ["<rinkeby_rpc_url>"],
      assets: [
        {
          name: "TEST",
          address: "0xB7b1d3cC52E658922b2aF00c5729001ceA98142C",
        },
      ],
    },
    "2221": {
      providers: ["<kovan_rpc_url>"],
      assets: [
        {
          name: "TEST",
          address: "0xB5AabB55385bfBe31D627E2A717a7B189ddA4F8F",
        },
      ],
    },
  },
};

Not sure where those IDs came from? They refer to the Nomad Domain IDs which are a custom mapping of ID to specific execution environment (not always equivalent to "chain", hence we have Domain IDs).

6. Create the SDK

Simply call create() with the config from above.

const { nxtpSdkBase } = await create(nxtpConfig);

7. Construct the xCallArgs

Now, we construct the arguments that will be passed into the xcall.

const callParams = {
  to: "<destination_address>", // the address that should receive the funds
  callData: "0x", // empty calldata for a simple transfer
  originDomain: "2221", // send from Kovan
  destinationDomain: "1111", // to Rinkeby
  recovery: "<destination_address>", // fallback address to send funds to if execution fails on destination side
  callback: ethers.constants.AddressZero, // zero address because we don't expect a callback for a simple transfer
  callbackFee: "0", // relayers on testnet don't take a fee
  forceSlow: false, // option that allows users to take the Nomad slow path (~30 mins) instead of paying routers a 0.05% fee on their transaction
  receiveLocal: false, // option for users to receive the local Nomad-flavored asset instead of the adopted asset on the destination side
};

const xCallArgs = {
  params: callParams,
  transactingAssetId: "0xB5AabB55385bfBe31D627E2A717a7B189ddA4F8F", // the Kovan Test Token
  amount: "1000000000000000000", // amount to send (1 TEST)
  relayerFee: "0", // relayers on testnet don't take a fee
};

8. Approve the asset transfer

This is necessary because funds will first be sent to the ConnextHandler contract before being bridged.

approveIfNeeded() is a helper function that finds the right contract address and executes the standard "increase allowance" flow for an asset.

const approveTxReq = await nxtpSdkBase.approveIfNeeded(
  xCallArgs.params.originDomain,
  xCallArgs.transactingAssetId,
  xCallArgs.amount,
);
const approveTxReceipt = await signer.sendTransaction(approveTxReq);
const approveResult = await approveTxReceipt.wait();

9. Send it!

Send the xcall.

const xcallTxReq = await nxtpSdkBase.xcall(xCallArgs);
xcallTxReq.gasLimit = ethers.BigNumber.from("30000000");
const xcallTxReceipt = await signer.sendTransaction(xcallTxReq);
console.log(xcallTxReceipt); // so we can see the transaction hash
const xcallResult = await xcallTxReceipt.wait();

Finally, run the following to fire off the cross-chain transfer!

yarn build
yarn xtransfer

10. Track the xcall

We can use the transaction hash from the transaction receipt we logged above to track the status of the xcall, following instructions here.

Tracking an xcall

After the DestinationTransfer shows up on the Rinkeby side, the freshly transferred tokens should show up in the destination wallet.


Cross-Chain Mint (unauthenticated)

We can also send arbitrary calldata, along with the xcall, to be executed on the destination domain.

In this example, we're going to construct some calldata targeting an existing contract function to avoid having to deploy a new contract. We'll aim for the mint function of the Test ERC20 Token (TEST) contract to demonstrate this.

Minting usually requires verification of the data but the Test Token has a public mint function (callable by anyone!) that we can leverage for this example. Hence, this is an "unauthenticated" xcall with unverified calldata - nothing extra needs to be done on the destination side.

7. Encode the calldata

After creating the SDK (steps 1-6 above), we have to create and encode the calldata.

To do this, we'll just grab the Test Token contract's ABI (we only care about the mint function here) and encode the calldata with the correct arguments.

const contractABI = ["function mint(address account, uint256 amount)"];
const iface = new ethers.utils.Interface(contractABI);

const calldata = iface.encodeFunctionData("mint", [
  "0x6d2A06543D23Cc6523AE5046adD8bb60817E0a94", // address to mint tokens for
  ethers.BigNumber.from("100000000000000000000"), // amount to mint (100 TEST)
]);

8. Construct the xCallArgs

Now with the calldata ready, we supply it to the xCallArgs.

const callParams = {
  to: "0xB7b1d3cC52E658922b2aF00c5729001ceA98142C", // Rinkeby Test Token - this is the contract we are targeting
  //highlight-next-line
  callData: calldata,
  originDomain: "2221", // send from Kovan
  destinationDomain: "1111", // to Rinkeby
  forceSlow: false, // option that allows users to take the Nomad slow path (~30 mins) instead of paying routers a 0.05% fee on their transaction
  receiveLocal: false, // option for users to receive the local Nomad-flavored asset instead of the adopted asset on the destination side
};

const xCallArgs = {
  params: callParams,
  transactingAssetId: "0xB5AabB55385bfBe31D627E2A717a7B189ddA4F8F", // the Kovan Test Token
  amount: "0", // not sending any funds
  relayerFee: "0", // relayers on testnet don't take a fee
};

9. Send it!

Notice that we specified amount: "0" above so we're not sending any funds with this xcall. Therefore, we can skip the approval dance and just send the transaction.

const xcallTxReq = await nxtpSdkBase.xcall(xCallArgs);
xcallTxReq.gasLimit = ethers.BigNumber.from("30000000");
const xcallTxReceipt = await signer.sendTransaction(xcallTxReq);
console.log(xcallTxReceipt); // so we can see the transaction hash
const xcallResult = await xcallTxReceipt.wait();

Add a new script to package.json:

"scripts": {
  "xmint": "node dist/xmint.js"
}

Finally, run the following to fire off the cross-chain mint!

yarn build
yarn xmint

10. Track the xcall

Again, we use the transaction hash from the transaction receipt to track the status of the xcall and we can check the destination wallet to make sure the right amount of funds were minted.

(back to top)

Roadmap

  • Testnet Launch
  • Mainnet Launch

See the open issues for a full list of proposed features (and known issues).

(back to top)

Contributing

Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.

If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

(back to top)

License

Distributed under the MIT License. See LICENSE.txt for more information.

(back to top)

Project Link: https://github.com/connext/nxtp

(back to top)

Local Dev

We are using a Yarn 2 Workspace-based monorepo structure. The individual workspaces are within the packages/ directory. This repo structure was heavily inspired by create-ts-project. The goal is to minimize 3rd party dependencies, and expose the configurations so that they can be transparently managed.

There are a few top-level scripts that can be used:

  • lint:all - Lints all packages.
  • test:all - Tests all packages.
  • clean:all - Removes build artifacts.
  • build:all - Builds all packages.
  • verify:all - Tests, builds, and lints all packages.
  • version:all - Sets versions for packages.
  • purge:all - Cleans and removes node_modules, etc.

Individual commands can be run against workspaces as so (example for nxtp-utils package):

yarn workspace @connext/nxtp-utils test

You should be able to do everything from the root and not need to go into the individual package dirs. For example, adding an npm package:

yarn workspace @connext/nxtp-txservice add ethers

First time setup

Make sure you are on the latest yarn version:

  • yarn set version berry

Try running yarn to update everything. If you have issues, try deleting node_modules and yarn.lock. After deleting yarn.lock run touch yarn.lock since it does not like if there is no lock file.

Common Tasks

  • yarn: Install deps, create symlinks, hoist packages.
  • yarn build:all: Build all packages.

Run router:

  • yarn workspace @connext/nxtp-router dev - Runs router in hot-reload mode.

Run test-ui:

  • yarn workspace @connext/nxtp-test-ui dev - Runs test-ui in hot-reload mode.

Running Test

  • yarn: Install deps, create symlinks, hoist packages.
  • yarn build:all: Build all packages. or
  • yarn workspace @connext/nxtp-contracts build: Build the specific package.

Run test:

  • yarn workspace @connext/nxtp-contracts test - Runs test.

Adding Packages

To add a new package that can be shared by the rest of the repo, you can use some convenience scripts that we have installed:

yarn tsp create @connext/test-lib --template node-lib

Note: The tsp tool is not required, it just makes boilerplate generation easier. If you want, you can copy paste stuff from other packages. Documentation on the tool is here.

To add the lib to be a dependency of a consuming app (i.e. the router):

yarn tsp add @connext/test-lib --cwd packages/router

Again, this can all be done without the tool, all it does is add some files and make some config changes.

Note: We use node-lib as the template for all the packages. There are some other included templates like browser-lib which didn't work with our bundling. We might need to revisit things for bundling reqs.

Publishing Packages

  • Update the CHANGELOG.md.
  • Run yarn version:all X.X.X where X.X.X is the full version string of the NPM version to deploy (i.e. 0.0.1).
  • Run git push --follow-tags.
  • The GitHub action will publish the packages by recognizing the version tag.

Integration

Information about common integration flows.

Setup Router for Connext

There's an easy hardhat script to run that sets up a router, adds assets, and adds liquidity. Run it by calling:

yarn workspace @connext/nxtp-contracts hardhat setup-test-router --router 0x0EC26F03e3dBA9bb5162D28fD5a3378A25f168d1 --network rinkeby

There are other configurable options. Note: You must use the deployer mnemonic. If you don't have it, ask a team member.

Running Test UI and Router Locally Against Live Chains

  • Spin up local messaging (optional but good idea to not use prod messaging):
yarn workspace @connext/nxtp-integration docker:messaging:up
  • Create router packages/router/config.json. Configure for live chains and the desired messaging. If you have not set up your mnemonic with the Connext, see the instructions:
{
  "adminToken": "blahblah",
  "chainConfig": {
    "4": {
      "providers": [
        "https://rinkeby.infura.io/v3/...",
        "https://rinkeby.infura.io/v3/...",
        "https://rinkeby.infura.io/v3/..."
      ],
      "confirmations": 1,
      "subgraph": "https://api.thegraph.com/subgraphs/name/connext/nxtp-rinkeby"
    },
    "5": {
      "providers": [
        "https://goerli.infura.io/v3/...",
        "https://goerli.infura.io/v3/...",
        "https://goerli.infura.io/v3/..."
      ],
      "confirmations": 1,
      "subgraph": "https://api.thegraph.com/subgraphs/name/connext/nxtp-goerli"
    }
  },
  "logLevel": "info",
  "natsUrl": "nats://localhost:4222",
  "authUrl": "http://localhost:5040",
  "mnemonic": "...", // use your own mnemonic!
  "swapPools": [
    {
      "name": "TEST",
      "assets": [
        { "chainId": 4, "assetId": "0x9aC2c46d7AcC21c881154D57c0Dc1c55a3139198" },
        { "chainId": 5, "assetId": "0x8a1Cad3703E0beAe0e0237369B4fcD04228d1682" }
      ]
    }
  ]
}
  • Spin up local router:
yarn workspace @connext/nxtp-router dev
  • Create packages/test-ui/.env. Configure for live chains and the desired messaging:
REACT_APP_CHAIN_CONFIG='{"1":{"providers":["https://mainnet.infura.io/v3/...","https://mainnet.infura.io/v3/...","https://mainnet.infura.io/v3/..."]},"4":{"providers":["https://rinkeby.infura.io/v3/...","https://rinkeby.infura.io/v3/...","https://rinkeby.infura.io/v3/..."]},"5":{"providers":["https://goerli.infura.io/v3/...","https://goerli.infura.io/v3/...","https://goerli.infura.io/v3/..."]}}'
REACT_APP_SWAP_CONFIG='[{"name":"TEST","assets":{"4":"0x9aC2c46d7AcC21c881154D57c0Dc1c55a3139198","5":"0x8a1Cad3703E0beAe0e0237369B4fcD04228d1682"}}]'
#REACT_APP_NATS_URL_OVERRIDE=ws://localhost:4221
#REACT_APP_AUTH_URL_OVERRIDE=http://localhost:5040
  • Spin up local test-ui:
yarn workspace @connext/nxtp-test-ui dev

Local Messaging and Chains

In some cases it is desirable to develop against local blockchains and messaging services. To do that, run:

  • yarn docker:local:services

The above command runs local chains and messaging and take care of local deployment. Modify packages/router/config.json to look similar to the following:

{
  "adminToken": "blahblah",
  "chainConfig": {
    "1337": {
      "providers": ["http://localhost:8545"],
      "confirmations": 1,
      "subgraph": "http://localhost:8010/subgraphs/name/connext/nxtp",
      "transactionManagerAddress": "0x8CdaF0CD259887258Bc13a92C0a6dA92698644C0",
      "safeRelayerFee": "100"
    },
    "1338": {
      "providers": ["http://localhost:8546"],
      "confirmations": 1,
      "subgraph": "http://localhost:9010/subgraphs/name/connext/nxtp",
      "transactionManagerAddress": "0x8CdaF0CD259887258Bc13a92C0a6dA92698644C0",
      "safeRelayerFee": "100"
    }
  },
  "logLevel": "info",
  "natsUrl": "nats://localhost:4222",
  "authUrl": "http://localhost:5040",
  "mnemonic": "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat",
  "swapPools": [
    {
      "name": "TEST",
      "assets": [
        { "chainId": 1337, "assetId": "0x345cA3e014Aaf5dcA488057592ee47305D9B3e10" },
        { "chainId": 1338, "assetId": "0x345cA3e014Aaf5dcA488057592ee47305D9B3e10" }
      ]
    }
  ]
}

Run the router locally with:

yarn workspace @connext/nxtp-router dev

The router will now hot reload and allow easy testing/debug.

Now you can run yarn workspace @connext/nxtp-integration test to run integration tests against a local machine.

When you are done, you can run yarn docker:stop:all to halt all running services.

About

Nxtp is a lightweight protocol for generalized crosschain transfers.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 72.5%
  • Solidity 19.0%
  • JavaScript 6.5%
  • HCL 1.2%
  • Ruby 0.3%
  • Dockerfile 0.2%
  • Other 0.3%