Crate cubist_sdk

source ·
Expand description

SDK for working with Cubist projects.

Background

Cubist makes it easy to develop cross-chain dApps by making them look and feel like single-chain dApps.

A user writes contracts as if they were all going to be deployed on the same chain. For example, one contract may directly call a method on another contract, as if they were on the same chain (even though they will not be).

Next, the user provides a configuration file, where, among other things, the user assigns each contract source file to a single chain.

Finally, to compile such a cross-chain dApp, for each chain from the config file Cubist generates (behind the scenes) a separate “target project”, i.e., a standard single-chain project amenable to existing web3 tooling. To facilitate cross-chain interactions1 between contracts, however, Cubist generates (again, behind the scenes, without requiring any user interaction) a shim contract for every cross-chain callee contract and places it in the target project for the chain on which that contract is called2.

Example

Consider the following extremely simple multi-chain storage dApp consisting of two contracts:

  • Receiver, which exposes a simple interface for storing a number,
    contract Receiver {
      uint256 _number;
     
      function store(uint256 num) public {
        _number = num;
      }
    
      function retrieve() public view returns (uint256) {
        return _number;
      }
    }
    
  • Sender, which exposes the same interface but it stores a given number in two contracts: itself and an instance of Receiver.
    import './Receiver.sol';
     
    contract Sender {
      Receiver _receiver;
      uint256 _number;
     
      constructor (Receiver addr) {
        _receiver = addr;
      }
     
      function store(uint256 num) public {
        _number = num;
        _receiver.store(_number);
      }
    
      function retrieve() public view returns (uint256) {
        return _number;
      }
    }
    

Let’s also assume that we want the Receiver contract deployed on Ethereum and the Sender contract deployed on Polygon3.

When instructed to build this dApp (e.g., by a user running cubist build from the command line), Cubist generates two target projects, one for each chain:

  • the Ethereum project contains only the Receiver contract (unchanged),

  • the Polygon project contains the Sender contract (unchanged) as well as an automatically generated Receiver shim contract; the shim contract has exactly the same interface as the original receiver contract (so that Sender can remain unchanged). The key difference, however, is that the shim contract’s store method now only generates an event (containing the method argument in its field). This event is automatically picked up by the relayer and relayed to the original Receiver contract deployed on Ethereum.

Once Cubist has created the shims in each target project, it individually builds each target project using a native contract compiler (currently, solc is the only supported compiler for contracts written in Solidity).

Once the contracts are compiled, we still need to write an app to interact with them (e.g., deploy them, invoke methods, run tests, etc.). For apps written in Rust, this SDK provides the necessary abstractions.

API

This crate exposes a number of abstractions for interacting with a Cubist project:

  • CubistInfo contains metadata about a Cubist dApp, e.g.,

    • which contracts target which chains (e.g., Receiver targets Ethereum and Sender targets Polygon),
    • which contracts have shims on which chains (e.g., only Receiver has a shim on Polygon)
    • general project configuration, etc.
  • TargetProjectInfo contains metadata about a single-chain target project, e.g., its target chain, compiler settings, chain endpoint configuration, etc.

  • ContractInfo contains metadata about a single contract, e.g., full name, source code information, ABI, and compiled bytecode.

To communicate and interact with an actual on-chain endpoint node, those abstractions must first be instantiated with a concrete middleware (either Http or Ws), i.e.,

API Examples

Instantiate Cubist<M> for a given dApp

use cubist_sdk::*;
use cubist_config::Config;

async {
  // expects to find 'cubist-config.json' in any of the parent folders
  let cfg = || Config::nearest().expect("cubist-config.json not found");

  // create a Cubist instance over HTTP
  let cubist_http = Cubist::<Http>::new(cfg()).await.unwrap();

  // create a cubist instance over WebSockets
  let cubist_ws = Cubist::<Ws>::new(cfg()).await.unwrap();

  // create an un-instantiated (not connected to the endpoint) 'CubistInfo' instance
  let cubist = CubistInfo::new(cfg()).unwrap();
};

Deploy Sender and Receiver contracts then call store

use cubist_sdk::*;
use cubist_config::Config;
use ethers::types::U256;

async {
  // expects to find 'cubist-config.json' in any of the parent folders
  let cfg = || Config::nearest().expect("cubist-config.json not found");

  // create a Cubist instance over HTTP
  let cubist = Cubist::<Http>::new(cfg()).await.unwrap();

  // find contracts by their names
  let receiver = cubist.contract("Receiver").expect("Contract 'Receiver' not found");
  let sender = cubist.contract("Sender").expect("Contract 'Sender' not found");

  // deploy first 'Receiver' then 'Sender'
  receiver.deploy(()).await.unwrap();
  sender.deploy(receiver.address_on(sender.target())).await.unwrap();

  // call 'store' on the sender
  let val = U256::from(123);
  sender.send("store", val).await.unwrap();

  // call 'retrieve' on both 'Sender' and 'Receiver'
  assert_eq!(val, sender.call("retrieve", ()).await.unwrap());

  // if the relayer is running, it will automatically propagate the value to 'Receiver'
  tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  assert_eq!(val, receiver.call("retrieve", ()).await.unwrap());
};

Load already deployed contracts from existing deployment receipts

use cubist_sdk::*;
use cubist_config::Config;
use ethers::types::U256;

async {
  // expects to find 'cubist-config.json' in any of the parent folders
  let cfg = || Config::nearest().expect("cubist-config.json not found");

  // create a Cubist instance over HTTP
  let cubist = Cubist::<Http>::new(cfg()).await.unwrap();

  // find contracts by their names
  let receiver = cubist.contract("Receiver").expect("Contract 'Receiver' not found");
  let sender = cubist.contract("Sender").expect("Contract 'Sender' not found");

  // if 'Receiver' and 'Sender' were previously deployed using Cubist, and the deployment
  // receipts are still on disk (in 'deploy' directory, by default), we can reload just them
  receiver.deployed().await.unwrap();
  sender.deployed().await.unwrap();

  // call 'store' on the sender
  let val = U256::from(123);
  sender.send("store", val).await.unwrap();

  // call 'retrieve' on both 'Sender' and 'Receiver'
  assert_eq!(val, sender.call("retrieve", ()).await.unwrap());

  // if the relayer is running, it will automatically propagate the value to 'Receiver'
  tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  assert_eq!(val, receiver.call("retrieve", ()).await.unwrap());
};

  1. Cubist automatically discovers all cross-chain dependencies by statically analyzing the contract source files. 

  2. A separate component, called relayer, continuously monitors events triggered by shim contracts and automatically relays them to their final destinations. 

  3. Refer to cubist_config::Config for details on how to configure a Cubist dApp and assign contracts to different chains. 

  4. The current Contract<M> API is somewhat weird in that it is stateful, i.e., that some methods (like send and call) may only be called after deploy. This is subject to change! The Contract<M> type will likely be decoupled into “ContractFactory” and “Contract”. 

Modules

Contract and project management data structures.
The interface (shim) contract generator.
Utilities for parsing contract files

Structs

Re-export some very commonly used types. Deployable contract.
Re-export some very commonly used types. Contract compiled into abi and bytecode.
Re-export some very commonly used types. Top-level type to use to manage configured contracts and target chains.
Re-export some very commonly used types. Top-level type to use to manage configured contracts and target chains.
Re-export some very commonly used types. Generic project targeting a single chain either via HTTP or WS.
Re-export some very commonly used types. Metadata about a project target a single chain.

Enums

Errors raised by this crate.
Custom error type wrapping various third-party errors

Type Definitions

Type alias for middleware stack over HTTP
Result with error type defaulting to CubistSdkError.
Type alias for middleware stack over Web Sockets