Skip to main content

Build the app

First we describe how to build our multi-chain dapp; then we explain what Cubist does during the build process.

note

The following instructions assume that you have installed Cubist.

Build instructions

In the cubist-config.json we specify the target chains on which each contract should run:

...
"contracts": {
"root_dir": "contracts",
"targets": {
"avalanche": {
"files": [
"./contracts/Sender1.sol",
"./contracts/Receiver1.sol"
]
},
"ava_subnet" : {
"files": [
"./contracts/Channel.sol"
]
},
"ethereum": {
"files": [
"./contracts/Sender2.sol"
]
},
"polygon": {
"files": [
"./contracts/Receiver2.sol"
]
}
}
}
...

root_dir tells Cubist the directory in which your contracts live. To change chains, simply alter targets to specify different chains for different files. Note that Cubist only allows you to assign chains at a per file granularity---not per contract. If you have two contracts in the same file and want to deploy them on different chains, you'll have to put each contract in its own file.

Now that we've specified where our contracts should run, we build the project using Cubist:

cubist build

What's going on behind the scenes

After we run build, Cubist generates files in both the build directory and our app's Rust src directory. The next sections describe the Cubist-generated code in each location.

The build directory

When you invoke build, Cubist generates new files in the build directory:

build
├── ava_subnet // Compiled contracts and metadata for everything to be deployed on an Avalanche subnet
├── ava // Compiled contracts and metadata for everything to be deployed on Avalanche
├── ethereum // ...etc...
└── polygon // ...etc...

Within each target directory (e.g., ava_subnet), Cubist saves:

  1. The ABIs produced when compiling the contracts with solc (within the artifacts directory) and
  2. The original and shim source files (within the contracts directory).

Cubist copies all files from the original contracts directory into each target directory; if shims are required on a given target, the <target>/contracts directory includes shim files instead of original source files.

Let's look a little closer at the ava_subnet directory:

ava_subnet 
├── artifacts // ABI and metadata directory
| ├── Channel.sol
| | └── Channel.json
| ├── Receiver1.sol
| | └── Receiver1.json
| └── Receiver2.sol
| └── Receiver2.json
├── build_infos // Info for the Cubist tool
├── cache // Cubist build cache
└── contracts // All contracts from the original project, or their shims
├── Channel.sol // Original Channel.sol contract
├── Receiver1.sol // Cubist-generated Receiver1 shim
├── Receiver1.bridge.json // Receiver1 configuration for the Cubist relayer
├── Receiver2.sol // Cubist-generated Receiver2 shim
├── Receiver2.bridge.json // Receiver2 configuration for the Cubist relayer
└── ... // More contracts

The artifacts directory contains metadata for each contract actually deployed on the Avalanche subnet. The Channel contract exists in the artifacts directory because the channel is deployed on an Avalanche subnet. To understand why the Receiver1 and Receiver2 shims end up on the subnet, too, let's take a look back at the Channel code:

import './Receiver1.sol';
import './Receiver2.sol';

contract Channel {
uint256 number;
R1 r1; // deployed on Avalanche
R2 r2; // deployed on Polygon
...
function send(uint256 num) public {
r1.store(num); // cross-chain call
r2.store(num); // cross-chain call
number = num;
}
...
}

Since the Channel interacts with R1 and R2, which are both deployed on different chains, Cubist must generate shims for Receiver1 and Receiver2 for deployment on the Avalanche subnet. These shims let Channel call r1.store(num) and r2.store(num); when they're called, the shim contract emits events that tell the Cubist off-chain relayer to relay the function calls to the true receiver contracts on Avalanche and Polygon.

Here's an example of one of the generated shim contracts, the one for Receiver1:

contract R1 {

event __cubist_event_R1_store(uint256 num);
...
function store(uint256 num) public onlyCaller {
emit __cubist_event_R1_store(num);
}

}

When Channel.sol calls send, the shim contract emits the ___cubist_event_R1_store event, which tells the relayer to invoke send on the "real" Receiver1 contract (deployed on Avalanche).

cubist build also generates the information that lets the Cubist relayer do its job. That's stored in the contracts directory, within the Receiver1.bridge.json and Receiver2.bridge.json files.

danger

You should not modify any of the files in the build directory. These files are automatically generated.

The src directory

At build time, Cubist also generates the code that allows our Rust dapp to interact with the sender contracts, receiver contracts, and channel. After running build, our source directory looks like this:

src
├── cubist_gen // Directory with contract-specific bindings
│ ├── channel.rs // Channel contract bindings
│ ├── r1.rs // Receiver one contract bindings
│ ├── r2.rs // ...etc...
│ ├── s1.rs
│ └── s2.rs
├── cubist_gen.rs // Binding code for interacting with on-chain contracts
└── main.rs // Application code

The next sections will go into detail about how to use cubist_gen.rs to interact with our contracts.