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 ofReceiver
.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 generatedReceiver
shim contract; the shim contract has exactly the same interface as the original receiver contract (so thatSender
can remain unchanged). The key difference, however, is that the shim contract’sstore
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 originalReceiver
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 andSender
targets Polygon), - which contracts have shims on which chains (e.g., only
Receiver
has a shim on Polygon) - general project configuration, etc.
- which contracts target which chains (e.g.,
-
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.,
-
Cubist<M>
is a grouping of instantiated target projects and contracts. Within this grouping, each contract may be deployed at most once (i.e.,Contract<M>::deploy
is idempotent). When multiple deployments per contract are needed, multipleCubist<M>
instances may be created within the same app. -
TargetProject<M>
is a wrapper around TargetProjectInfo which additionally contains an ethers provider used to talk to the chain endpoint -
Contract<M>
is a wrapper aroundContractInfo
which can additionally be deployed; once deployed, its methods may be called via send or call4
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());
};
Cubist automatically discovers all cross-chain dependencies by statically analyzing the contract source files. ↩
A separate component, called relayer, continuously monitors events triggered by shim contracts and automatically relays them to their final destinations. ↩
Refer to cubist_config::Config for details on how to configure a Cubist dApp and assign contracts to different chains. ↩
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! TheContract<M>
type will likely be decoupled into “ContractFactory” and “Contract”. ↩
Modules
Structs
Enums
Type Definitions
CubistSdkError
.