Build the app
First we describe how to build our multi-chain dapp; then we explain what Cubist does during the build process.
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:
- The ABIs produced when
compiling the contracts with
solc
(within theartifacts
directory) and - 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.
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.