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.
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.
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:
- As a standalone binary that takes no arguments
- 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.