Skip to content

Commit

Permalink
feat: Add e2e canister tests (eigerco#29)
Browse files Browse the repository at this point in the history
* feat: Add e2e canister tests

* take canister name as an argument

* handle both candid types and argument encoders in call invocations

* Add no arguments variant of call

* Correct the result types in tests

* make separate module for u256 and address in interface

* make setup eth canister a standalone function

* use wrap_err_with where needed

* import more aggresively
  • Loading branch information
zvolin authored Aug 11, 2023
1 parent aaa6dc5 commit 1b19410
Show file tree
Hide file tree
Showing 8 changed files with 370 additions and 122 deletions.
3 changes: 3 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[build]
target = "wasm32-unknown-unknown"

[test]
target = "x86_64-unknown-linux-gnu"
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ ethers-contract = { version = "2.0.8", default-features = false, features = [
] }
ethers-core = "2.0.8"
eyre = "0.6.8"
helios_client = { package = "client", path = "../helios/client" }
helios_common = { package = "common", path = "../helios/common" }
helios_config = { package = "config", path = "../helios/config" }
helios_execution = { package = "execution", path = "../helios/execution" }
helios_client = { package = "client", git = "https://github.com/eigerco/helios", rev = "a7173b2" }
helios_common = { package = "common", git = "https://github.com/eigerco/helios", rev = "a7173b2" }
helios_config = { package = "config", git = "https://github.com/eigerco/helios", rev = "a7173b2" }
helios_execution = { package = "execution", git = "https://github.com/eigerco/helios", rev = "a7173b2" }
interface = { path = "src/interface" }

[profile.release]
Expand Down
54 changes: 54 additions & 0 deletions src/ethereum-canister/tests/canister.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use candid::Nat;

mod test_canister;

use crate::test_canister::{call, setup_ethereum_canister};

#[test]
fn get_block_number() {
let canister = setup_ethereum_canister();

let block_num: (Nat,) = call!(canister, "get_block_number").unwrap();
assert!(block_num.0 > 17880732u128);
}

mod erc20 {
use interface::{Erc20OwnerOfRequest, U256};

use super::*;

#[test]
fn balance_of() {
let canister = setup_ethereum_canister();

let request = Erc20OwnerOfRequest {
contract: "0xdAC17F958D2ee523a2206206994597C13D831ec7" // usdt
.parse()
.unwrap(),
account: "0xF977814e90dA44bFA03b6295A0616a897441aceC"
.parse()
.unwrap(),
};
let _: (U256,) = call!(canister, "erc20_balance_of", request).unwrap();
}
}

mod erc721 {
use interface::{Address, Erc721OwnerOfRequest};

use super::*;

#[test]
fn owner_of() {
let canister = setup_ethereum_canister();

let request = Erc721OwnerOfRequest {
contract: "0x5Af0D9827E0c53E4799BB226655A1de152A425a5" // milady
.parse()
.unwrap(),
token_id: 7773_u32.into(),
};

let _: (Address,) = call!(canister, "erc721_owner_of", request).unwrap();
}
}
134 changes: 134 additions & 0 deletions src/ethereum-canister/tests/test_canister/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use std::process::Command;
use std::str;

use candid::utils::{encode_args, ArgumentEncoder};
use candid::IDLArgs;
use eyre::{ensure, Result, WrapErr};
use interface::SetupRequest;
use temp_dir::TempDir;

const DEFAULT_CONSENSUS_RPC: &str = "https://www.lightclientdata.org";
const DEFAULT_EXECUTION_RPC: &str = "https://ethereum.publicnode.com";

#[derive(Debug)]
pub struct TestCanister {
name: String,
temp_dir: TempDir,
}

impl TestCanister {
pub fn deploy(name: &str) -> Self {
let temp_dir = TempDir::new().unwrap();

// setup the tempdir
make_symlink(&temp_dir, "src");
make_symlink(&temp_dir, "target");
make_symlink(&temp_dir, "Cargo.toml");
make_symlink(&temp_dir, "Cargo.lock");
make_symlink(&temp_dir, "dfx.json");

// deploy
let canister = TestCanister {
temp_dir,
name: name.to_owned(),
};
canister.run_dfx(&["deploy", name]).unwrap();

canister
}

pub fn call(&self, method: &str, args: impl ArgumentEncoder) -> Result<Vec<u8>> {
// convert arguments into format understood by `dfx`
let args = encode_args(args).wrap_err("encoding args")?;
let args = IDLArgs::from_bytes(&args).wrap_err("decoding dfx args")?;

let stdout = self
.run_dfx(&["canister", "call", &self.name, method, &args.to_string()])
.wrap_err_with(|| format!("calling '{method} {args}'"))?;

// convert results from the format understood by `dfx`
// to the candid binary representation
// note: decoding here to the correct type is not possible as decoding
// binds the result's lifetime to the lifetime of output
// this is because decoding supports also reference types
let stdout = str::from_utf8(&stdout).wrap_err("decoding output")?;
let output: IDLArgs = stdout.parse().wrap_err("parsing output")?;
output.to_bytes().wrap_err("encoding to candid")
}

fn remove(&self) {
self.run_dfx(&["canister", "stop", &self.name])
.expect("Stopping failed");
self.run_dfx(&["canister", "delete", &self.name])
.expect("Deleting failed");
}

fn run_dfx(&self, args: &[&str]) -> Result<Vec<u8>> {
let output = Command::new("dfx")
.args(args)
.current_dir(self.temp_dir.path())
.output()
.wrap_err_with(|| format!("executing dfx {args:?}"))?;
ensure!(
output.status.success(),
"dfx {args:?} failed: {}",
str::from_utf8(&output.stderr)?
);
Ok(output.stdout)
}
}

impl Drop for TestCanister {
fn drop(&mut self) {
self.remove()
}
}

pub fn setup_ethereum_canister() -> TestCanister {
let canister = TestCanister::deploy("ethereum_canister");
let request = SetupRequest {
consensus_rpc_url: DEFAULT_CONSENSUS_RPC.to_owned(),
execution_rpc_url: DEFAULT_EXECUTION_RPC.to_owned(),
};
let _: () = call!(canister, "setup", request).unwrap();
canister
}

/// A helper macro that allows calling canister methods with single and multiple arguments
/// and decodes the results.
macro_rules! call {
($canister:expr, $method:expr, ($($arg:expr),*)) => {{
let result = $canister.call($method, ($($arg),*));
crate::test_canister::call!(@decode, result)
}};
($canister:expr, $method:expr, $arg:expr) => {{
let result = $canister.call($method, ($arg,));
crate::test_canister::call!(@decode, result)
}};
($canister:expr, $method:expr) => {{
let result = $canister.call($method, ());
crate::test_canister::call!(@decode, result)
}};
(@decode, $result:expr) => {
$result.and_then(|output| {
::candid::utils::decode_args(&output).map_err(|err| ::eyre::eyre!(err))
})
}
}

fn make_symlink(temp_dir: &TempDir, name: &str) {
#[cfg(not(target_family = "unix"))]
{
_ = temp_dir;
_ = name;
panic!("unsupported test target")
}
#[cfg(target_family = "unix")]
{
let path = format!("{}/../../{name}", env!("CARGO_MANIFEST_DIR"));
std::os::unix::fs::symlink(path, temp_dir.child(name)).unwrap();
}
}

// this has to come after macro definitions
pub(crate) use call;
59 changes: 59 additions & 0 deletions src/interface/src/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::str::FromStr;

use candid::types::{Compound, Serializer, Type};
use candid::CandidType;
use ethers_core::types::Address as EthersAddress;
use serde::Deserialize;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Address(EthersAddress);

impl CandidType for Address {
fn _ty() -> Type {
<String as CandidType>::ty()
}

fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
where
S: Serializer,
{
let s = format!("{:?}", &self.0);
let mut ser = serializer.serialize_struct()?;
Compound::serialize_element(&mut ser, &s)?;
Ok(())
}
}

impl<'de> Deserialize<'de> for Address {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let addr = s
.parse::<EthersAddress>()
.map_err(serde::de::Error::custom)?;
Ok(Address(addr))
}
}

impl From<EthersAddress> for Address {
fn from(value: EthersAddress) -> Self {
Address(value)
}
}

impl From<Address> for EthersAddress {
fn from(value: Address) -> Self {
value.0
}
}

impl FromStr for Address {
type Err = <EthersAddress as FromStr>::Err;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let addr: EthersAddress = s.parse()?;
Ok(Self(addr))
}
}
Loading

0 comments on commit 1b19410

Please sign in to comment.