Skip to content

Commit

Permalink
feat: add a way to automatically compile and deploy a contract (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
itegulov authored Mar 30, 2022
1 parent 64bf467 commit 4c4c4a0
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 6 deletions.
10 changes: 6 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Add wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: Check with stable features
run: cargo check --verbose
- name: Run tests with unstable features
run: cargo test --verbose --features unstable
3 changes: 3 additions & 0 deletions workspaces/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ Library for automating workflows and testing NEAR smart contracts.

[dependencies]
async-trait = "0.1"
async-process = { version = "1.3.0", optional = true }
anyhow = "1.0"
base64 = "0.13"
borsh = "0.9"
cargo_metadata = { version = "0.14.2", optional = true }
chrono = "0.4.19"
dirs = "3.0.2"
hex = "0.4.2"
Expand Down Expand Up @@ -49,3 +51,4 @@ tracing-subscriber = { version = "0.3.5", features = ["env-filter"] }
[features]
default = ["install"]
install = [] # Install the sandbox binary during compile time
unstable = ["cargo_metadata", "async-process"]
101 changes: 101 additions & 0 deletions workspaces/src/cargo/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use anyhow::anyhow;
use async_process::Command;
use cargo_metadata::{Message, Metadata, MetadataCommand};
use std::env;
use std::fmt::Debug;
use std::fs;
use std::path::Path;
use std::process::Stdio;
use tracing::debug;

fn cargo_bin() -> Command {
match env::var_os("CARGO") {
Some(cargo) => Command::new(cargo),
None => Command::new("cargo"),
}
}

/// Fetch current project's metadata (i.e. project invoking this method, not the one that we are
/// trying to compile).
fn root_cargo_metadata() -> anyhow::Result<Metadata> {
MetadataCommand::new().exec().map_err(Into::into)
}

async fn build_cargo_project<P: AsRef<Path> + Debug>(
project_path: P,
) -> anyhow::Result<Vec<Message>> {
let metadata = root_cargo_metadata()?;
let output = cargo_bin()
.args([
"build",
"--target",
"wasm32-unknown-unknown",
"--release",
"--message-format=json",
"--target-dir",
metadata.target_directory.as_str(),
])
.current_dir(&project_path)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await?;
debug!(
target: "workspaces",
"Building project '{:?}' resulted in status {:?}",
&project_path, output.status
);
if output.status.success() {
let reader = std::io::BufReader::new(output.stdout.as_slice());
Ok(Message::parse_stream(reader).map(|m| m.unwrap()).collect())
} else {
Err(anyhow!(
"Failed to build project '{:?}'.\n\
Stderr:\n\
{}\n\
Stdout:\n\
{}",
project_path,
String::from_utf8(output.stderr)?,
String::from_utf8(output.stdout)?,
))
}
}

/// Builds the cargo project located at `project_path` and returns the generated wasm file contents.
///
/// NOTE: This function does not check whether the resulting wasm file is a valid smart
/// contract or not.
pub async fn compile_project(project_path: &str) -> anyhow::Result<Vec<u8>> {
let messages = build_cargo_project(fs::canonicalize(project_path)?).await?;
// We find the last compiler artifact message which should contain information about the
// resulting .wasm file
let compile_artifact = messages
.iter()
.filter_map(|m| match m {
cargo_metadata::Message::CompilerArtifact(artifact) => Some(artifact),
_ => None,
})
.last()
.ok_or(anyhow!(
"Cargo failed to produce any compilation artifacts. \
Please check that your project contains a NEAR smart contract."
))?;
// The project could have generated many auxiliary files, we are only interested in .wasm files
let wasm_files = compile_artifact
.filenames
.iter()
.filter(|f| f.as_str().ends_with(".wasm"))
.collect::<Vec<_>>();
match wasm_files.as_slice() {
[] => Err(anyhow!(
"Compilation resulted in no '.wasm' target files. \
Please check that your project contains a NEAR smart contract."
)),
[file] => Ok(tokio::fs::read(file.canonicalize()?).await?),
_ => Err(anyhow!(
"Compilation resulted in more than one '.wasm' target file: {:?}",
wasm_files
)),
}
}
5 changes: 5 additions & 0 deletions workspaces/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#[cfg(feature = "unstable")]
mod cargo;
#[cfg(feature = "unstable")]
pub use cargo::compile_project;

mod network;
mod rpc;
mod types;
Expand Down
27 changes: 27 additions & 0 deletions workspaces/tests/deploy_project.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#![cfg(feature = "unstable")]
#![recursion_limit = "256"]
use test_log::test;
use workspaces::prelude::*;

#[test(tokio::test)]
async fn test_dev_deploy_project() -> anyhow::Result<()> {
let worker = workspaces::sandbox();
let wasm = workspaces::compile_project("./tests/test-contracts/status-message").await?;
let contract = worker.dev_deploy(&wasm).await?;

let _res = contract
.call(&worker, "set_status")
.args_json(("foo",))?
.max_gas()
.transact()
.await?;

let res = contract
.call(&worker, "get_status")
.args_json((contract.id(),))?
.view()
.await?;
assert_eq!(res.json::<String>()?, "foo");

Ok(())
}
4 changes: 2 additions & 2 deletions workspaces/tests/optional_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async fn test_empty_args_error() -> anyhow::Result<()> {

let res = contract
.call(&worker, "storage_unregister")
.gas(300_000_000_000_000)
.max_gas()
.deposit(1)
.transact()
.await;
Expand All @@ -47,7 +47,7 @@ async fn test_optional_args_present() -> anyhow::Result<()> {
.args_json(serde_json::json!({
"force": true
}))?
.gas(300_000_000_000_000)
.max_gas()
.deposit(1)
.transact()
.await?;
Expand Down
23 changes: 23 additions & 0 deletions workspaces/tests/test-contracts/status-message/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "test-contract-status-message"
version = "0.0.0"
authors = ["Near Inc <[email protected]>"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
near-sdk = "4.0.0-pre.7"
near-contract-standards = "4.0.0-pre.7"

[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "z"
lto = true
debug = false
panic = "abort"
overflow-checks = true

[workspace]
24 changes: 24 additions & 0 deletions workspaces/tests/test-contracts/status-message/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{env, log, metadata, near_bindgen, AccountId};
use std::collections::HashMap;

#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct StatusMessage {
records: HashMap<AccountId, String>,
}

#[near_bindgen]
impl StatusMessage {
#[payable]
pub fn set_status(&mut self, message: String) {
let account_id = env::signer_account_id();
log!("{} set_status with message {}", account_id, message);
self.records.insert(account_id, message);
}

pub fn get_status(&self, account_id: AccountId) -> Option<String> {
log!("get_status for account_id {}", account_id);
self.records.get(&account_id).cloned()
}
}

0 comments on commit 4c4c4a0

Please sign in to comment.