Skip to main content

Run the app

First, we're going to use Cubist to set up local chains and the cross-chain relayer. Then, we'll interact with the storage contracts from inside our Rust application.

Start Cubist chains and relayer

Now that we've built our contracts, we want to deploy them and then interact with them. We're going to do this locally; the config (cubist-config.json) already specifies the URLs on which the chains will run. The app config will look something like this:

{
...
"network_profiles": {
"default": {
"ethereum": { "url": "http://127.0.0.1:8545" },
"polygon": { "url": "http://127.0.0.1:9545" }
}
}
}

You can alter the config to specify other URLs if you choose.

To start both the local chains and the Cubist relayer, use:

cubist start

Note that this command may take a while (i.e., a minute) the first time you run it. This is because Cubist needs to download and build the local chain running services before it can start the chains.

Once you've run cubist start, you'll see output that looks something like this:

Launching chains
ethereum ✔ [ 0s] [....................] http://localhost:8545/
polygon ✔ [ 1s] [....................] http://localhost:9545/ All servers available
Watching <path>/cubist-deploy dir

This output shows where the localnets are running (e.g., localhost:8545), and how long they took to become initialized and available. The output also lets us know that the local relayer is successfully watching for the events that tell it to relay information from one chain to another.

tip

Alternatively, you can start the chains and the relayer seperately with cubist chains start and cubist relayer start. For more information on these and other Cubist CLI commands, try cubist help, or check out the CLI reference.

caution

Right now, Avalanche and Avalanche-subnet localnets are slower to start up than other networks. We're working on it!

Interact with the contracts from Rust

The example Rust storage app can be run two different ways:

  1. As a standalone binary that takes no arguments
  2. Via the command line.

We'll walk through both versions of the application next.

The standalone application (main.rs)

To run the example main application, from the top-level app example repo invoke:

cargo run

This command may take a while when first run! That's because it has to download and build all the app dependencies. When it's finally done, it will yield something like this:

Now let's take a look at the app code that generated this output; the app in src/main.rs uses a combination of the Cubist Rust SDK and code generated by Cubist at build time---both exposed via cubist_gen---to interface with the contracts. The next sections walk through deploying the contracts and interacting with them.

Deploying the contracts

First, we create a new Cubist instance and deploy a sender receiver contract and a sender contract:

let cubist = cubist().await?;
// Deploy receiver contract and shims
let receiver_one = StorageReceiver::deploy(U256::from(1)).await?;
// Deploy sender contract
let sender_one =
StorageSender::deploy((U256::from(2), receiver_one.addr(StorageSender::target()))).await?;

The StorageReceiver and StorageSender types represent both of our contracts, and were automatically generated in the storage_receiver.rs and storage_sender.rs files within the cubist_gen directory. We deploy these contracts using deploy function; note that the constructor arguments---a number for StorageReceiver and a number and an address for StorageSender---match the smart contract constructor parameters. We get the address to which the StorageReceiver contract was deployed using the generated addr function. addr takes a target chain (e.g., Ethereum or Polygon) as input; in this case, we've asked for StorageReceiver's address on Polygon indirectly on line 4, by calling:

receiver_one.addr(StorageSender::target())

StorageSender::target returns Polygon, since that's the chain that was specified as its target in the config file. Note that StorageReceiver was not explicitly deployed on Polygon; however, since it's called cross-chain by StorageSender, Cubist generates and deploys a Polygon shim for StorageReceiver. It's the address of this shim that is returned by the code snippet above.

Making sure the relayer is running

Next, the code in main.rs makes sure the relayer is running properly:

assert!(cubist.when_bridged(None).await);

This assertion calls when_bridged on cubist with no (optional) delay parameters; functions like when_bridged are all described in the Rust SDK docs.

Deploying the same contract twice

Next, we create a new Cubist instance in order to deploy StorageSender and StorageReceiver again:

let cubist = new_cubist().await?;
let receiver_two = cubist.storage_receiver().deploy(U256::from(10)).await?;
let sender_two = cubist
.storage_sender()
.deploy((U256::from(20), receiver_two.addr(StorageSender::target())))
.await?;

This code uses the automatically generated storage_receiver and storage_sender functions on the Cubist instance to create a deployable instance of each contract. Earlier, we used the automatically generated contract types StorageReceiver (from storage_receiver.rs) and StorageSender (from storage_sender.rs) to do the same thing.

Interacting with the contracts

Once we've deployed both sets of contracts and checked both relayers using when_bridged, we can interact with our cross-chain contracts. First, we retrieve the stored value using the retrieve function; retrieve is a Cubist-generated function that invokes retrieve on a given smart contract:

assert_eq!(U256::from(10), receiver_two.retrieve().call().await?);
assert_eq!(U256::from(20), sender_two.retrieve().call().await?);

We indeed get back the number each contract was constructed to contain.

We can also store values on each of our two StorageSender contracts---sender_three is three and sender_thirty is thirty (converted from Rust to Solidity values using the ethers library):

let sender_three = U256::from(3);
let sender_thirty = U256::from(30);
let call = sender_one.store(sender_three);
call.send().await?.await?;
let call = sender_two.store(sender_thirty);
call.send().await?.await?;

store is a Cubist-generated function that will invoke StorageSender's store function. It returns an ethers ContractCall type that can be invoked using the send function.

Recall that StorageSender's store function makes a (cross-chain) call to StorageReceiver's store function; therefore, any stores to StorageSender are reflected on StorageReceiver. Accounting for delay (code omitted), our main function checks that this is the case:

let retrieved_one = receiver_one.retrieve().call().await?;
let retrieved_two = receiver_two.retrieve().call().await?;
println!("Retrieved {retrieved_one:?}, {retrieved_two:?}");
assert_eq!(sender_three, retrieved_one);
assert_eq!(sender_thirty, retrieved_two);

It uses the generated retrieve function to retrieve the value from StorageReceiver, and then checks that the StorageReceiver values are indeed equivalent to the StorageSender values.

The command line interface (cli.rs)

Since the command line interface uses the same Cubist-generated functions that the main application does, we describe the CLI in less detail. The CLI exposes different commands:

enum Command {
/// Deploy both 'StorageSender' and 'StorageReceiver',
/// configuring the sender to forward values to the receiver
Deploy(DeployArgs),
/// List the current deployments. Only one instance of 'StorageSender'
/// and 'StorageReceiver' contracts may exist at a time.
List,
/// Store a value to 'StorageSender'; the relayer will automatically
/// forward that value to its 'StorageReceiver' contract.
StoreSender(StoreArgs),
// ... more ...
}

We're going to look at StoreSender, which invokes the store_sender function. The key difference between store_sender and main is that store_sender must load already deployed contracts from their deployment receipts; in main, we had deployed the contracts within the same function they were used, so nothing had to be re-loaded.

Here's the code for store_sender:

async fn store_sender(args: &StoreArgs) -> Result<()> {
let sender = StorageSender::deployed()
.await
.context("Contracts not deployed; call 'cargo run -- deploy' first")?;
...
sender.store(U256::from(args.val)).send().await?.await?;
Ok(())
}

The second line loads the StorageSender contract automatically from saved deployment receipts (and fails if StorageSender has not yet been deployed). Once we've loaded the contract, we can call the its store function directly from Rust.

Shut down

Running cubist stop will shut down both the chains and the relayer. Once the chains and the relayer are shut down, trying to call contract functions from within your dapp will result in errors.