diff --git a/.gitignore b/.gitignore index 8be8e8a..f4d2a7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .direnv result* /target -/wasm/target +/wasm/**/target .envrc.local infra/wallet .vscode/* diff --git a/.vscode/settings.json.default b/.vscode/settings.json.default index 90d9cc3..4f407bb 100644 --- a/.vscode/settings.json.default +++ b/.vscode/settings.json.default @@ -1,6 +1,6 @@ { "rust-analyzer.linkedProjects": [ "Cargo.toml", - "wasm/Cargo.toml" + "wasm/simple-tx/Cargo.toml" ] } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 25b8f3b..237ea93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,6 +1095,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "firefly-cardano-deploy-contract" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "hex", + "reqwest", + "serde", + "tokio", + "uuid", + "wat", + "wit-component", +] + [[package]] name = "firefly-cardanoconnect" version = "0.1.0" @@ -2753,7 +2768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.4.1", + "heck 0.5.0", "itertools 0.12.1", "log", "multimap", @@ -3575,6 +3590,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spdx" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae30cc7bfe3656d60ee99bf6836f472b0c53dddcbf335e253329abb16e535a2" +dependencies = [ + "smallvec", +] + [[package]] name = "spin" version = "0.9.8" @@ -3690,7 +3714,7 @@ dependencies = [ "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4453,6 +4477,22 @@ dependencies = [ "wasmparser 0.220.0", ] +[[package]] +name = "wasm-metadata" +version = "0.220.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3e5f5920c5abfc45573c89b07b38efdaae1515ef86f83dad12d60e50ecd62b" +dependencies = [ + "anyhow", + "indexmap 2.6.0", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder 0.220.0", + "wasmparser 0.220.0", +] + [[package]] name = "wasmparser" version = "0.212.0" @@ -4473,8 +4513,11 @@ version = "0.220.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e246c2772ce3ebc83f89a2d4487ac5794cad6c309b2071818a88c7db7c36d87b" dependencies = [ + "ahash", "bitflags 2.6.0", + "hashbrown 0.14.5", "indexmap 2.6.0", + "semver", ] [[package]] @@ -4585,7 +4628,7 @@ dependencies = [ "syn 2.0.85", "wasmtime-component-util", "wasmtime-wit-bindgen", - "wit-parser", + "wit-parser 0.212.0", ] [[package]] @@ -4741,7 +4784,7 @@ dependencies = [ "anyhow", "heck 0.4.1", "indexmap 2.6.0", - "wit-parser", + "wit-parser 0.212.0", ] [[package]] @@ -4973,6 +5016,25 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-component" +version = "0.220.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ccedf54cc65f287da268d64d2bf4f7530d2cfb2296ffbe3ad5f65567e4cf53" +dependencies = [ + "anyhow", + "bitflags 2.6.0", + "indexmap 2.6.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.220.0", + "wasm-metadata", + "wasmparser 0.220.0", + "wit-parser 0.220.0", +] + [[package]] name = "wit-parser" version = "0.212.0" @@ -4991,6 +5053,24 @@ dependencies = [ "wasmparser 0.212.0", ] +[[package]] +name = "wit-parser" +version = "0.220.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7117ce3adc0b4354b46dc1cf3190b00b333e65243d244c613ffcc58bdec84d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.6.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.220.0", +] + [[package]] name = "xattr" version = "1.3.1" diff --git a/Cargo.toml b/Cargo.toml index 20d4e6b..4972fc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ members = [ "firefly-cardanosigner", "firefly-server", "scripts/demo", + "scripts/deploy-contract", ] +exclude = ["wasm"] + resolver = "2" diff --git a/firefly-cardanoconnect/src/contracts.rs b/firefly-cardanoconnect/src/contracts.rs index e14cb2f..23d9d75 100644 --- a/firefly-cardanoconnect/src/contracts.rs +++ b/firefly-cardanoconnect/src/contracts.rs @@ -21,6 +21,7 @@ pub struct ContractsConfig { } pub struct ContractManager { + components_path: Option, runtime: Option>, } @@ -29,12 +30,29 @@ impl ContractManager { fs::create_dir_all(&config.components_path).await?; let runtime = Self::new_runtime(config, blockfrost_key).await?; Ok(Self { + components_path: Some(config.components_path.clone()), runtime: Some(RwLock::new(runtime)), }) } pub fn none() -> Self { - Self { runtime: None } + Self { + components_path: None, + runtime: None, + } + } + + pub async fn deploy(&self, id: &str, contract: &[u8]) -> Result<()> { + let Some(components_path) = self.components_path.as_deref() else { + bail!("No contract directory configured"); + }; + let path = components_path.join(format!("{id}.wasm")); + fs::write(&path, contract).await?; + if let Some(rt_lock) = &self.runtime { + let mut runtime = rt_lock.write().await; + runtime.register_worker(id, path, json!(null)).await?; + } + Ok(()) } pub async fn invoke( diff --git a/firefly-cardanoconnect/src/main.rs b/firefly-cardanoconnect/src/main.rs index 1c432cd..330015b 100644 --- a/firefly-cardanoconnect/src/main.rs +++ b/firefly-cardanoconnect/src/main.rs @@ -14,7 +14,7 @@ use operations::OperationsManager; use routes::{ chain::get_chain_tip, health::health, - operations::{get_operation_status, invoke_contract}, + operations::{deploy_contract, get_operation_status, invoke_contract}, streams::{ create_listener, create_stream, delete_listener, delete_stream, get_listener, get_stream, list_listeners, list_streams, update_stream, @@ -102,6 +102,7 @@ async fn main() -> Result<()> { let router = ApiRouter::new() .api_route("/health", get(health)) + .api_route("/contracts/deploy", post(deploy_contract)) .api_route("/contracts/invoke", post(invoke_contract)) .api_route("/transactions", post(submit_transaction)) .api_route("/transactions/id", get(get_operation_status)) diff --git a/firefly-cardanoconnect/src/operations/manager.rs b/firefly-cardanoconnect/src/operations/manager.rs index 8a4fb7f..fefe968 100644 --- a/firefly-cardanoconnect/src/operations/manager.rs +++ b/firefly-cardanoconnect/src/operations/manager.rs @@ -34,6 +34,27 @@ impl OperationsManager { } } + pub async fn deploy(&self, id: OperationId, name: &str, contract: &[u8]) -> ApiResult<()> { + let mut op = Operation { + id, + status: OperationStatus::Pending, + tx_id: None, + }; + self.persistence.write_operation(&op).await?; + match self.contracts.deploy(name, contract).await { + Ok(()) => { + op.status = OperationStatus::Succeeded; + self.persistence.write_operation(&op).await?; + Ok(()) + } + Err(err) => { + op.status = OperationStatus::Failed(err.to_string()); + self.persistence.write_operation(&op).await?; + Err(err.into()) + } + } + } + pub async fn invoke( &self, id: OperationId, diff --git a/firefly-cardanoconnect/src/routes/operations.rs b/firefly-cardanoconnect/src/routes/operations.rs index 42f8cbb..ac0f34f 100644 --- a/firefly-cardanoconnect/src/routes/operations.rs +++ b/firefly-cardanoconnect/src/routes/operations.rs @@ -23,6 +23,21 @@ pub struct InvokeRequest { pub params: Vec, } +#[derive(Deserialize, JsonSchema)] +pub struct DeployRequest { + /// The FireFly operation ID of this request. + pub id: String, + /// A hex-encoded WASM component. + pub contract: String, + /// A description of the schema for this contract. + pub definition: ABIContract, +} + +#[derive(Deserialize, JsonSchema)] +pub struct ABIContract { + pub name: String, +} + #[derive(Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct ABIMethod { @@ -70,6 +85,19 @@ pub struct OperationReceipt { pub protocol_id: Option, } +pub async fn deploy_contract( + State(AppState { operations, .. }): State, + Json(req): Json, +) -> ApiResult { + let id = req.id.into(); + let name = &req.definition.name; + let contract = hex::decode(req.contract)?; + match operations.deploy(id, name, &contract).await { + Ok(()) => Ok(NoContent), + Err(error) => Err(error.with_field("submissionRejected", true)), + } +} + pub async fn invoke_contract( State(AppState { operations, .. }): State, Json(req): Json, diff --git a/scripts/deploy-contract/Cargo.toml b/scripts/deploy-contract/Cargo.toml new file mode 100644 index 0000000..1ea280c --- /dev/null +++ b/scripts/deploy-contract/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "firefly-cardano-deploy-contract" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +hex = "0.4" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["full"] } +uuid = { version = "1", features = ["v4"] } +wat = "1" +wit-component = "0.220" diff --git a/scripts/deploy-contract/src/firefly.rs b/scripts/deploy-contract/src/firefly.rs new file mode 100644 index 0000000..e6ded45 --- /dev/null +++ b/scripts/deploy-contract/src/firefly.rs @@ -0,0 +1,53 @@ +use anyhow::{bail, Result}; +use reqwest::{Client, Response}; +use serde::Serialize; +use uuid::Uuid; + +pub struct FireflyCardanoClient { + client: Client, + base_url: String, +} + +impl FireflyCardanoClient { + pub fn new(base_url: &str) -> Self { + Self { + client: Client::new(), + base_url: base_url.to_string(), + } + } + + pub async fn deploy_contract(&self, name: &str, contract: &str) -> Result<()> { + let url = format!("{}/contracts/deploy", self.base_url); + let req = DeployContractRequest { + id: Uuid::new_v4().to_string(), + contract: contract.to_string(), + definition: ABIContract { + name: name.to_string(), + }, + }; + let res = self.client.post(url).json(&req).send().await?; + Self::extract_error(res).await?; + Ok(()) + } + + async fn extract_error(res: Response) -> Result { + if !res.status().is_success() { + let default_msg = res.status().to_string(); + let message = res.text().await.unwrap_or(default_msg); + bail!("request failed: {}", message); + } + Ok(res) + } +} + +#[derive(Serialize)] +struct DeployContractRequest { + pub id: String, + pub contract: String, + pub definition: ABIContract, +} + +#[derive(Serialize)] +struct ABIContract { + pub name: String, +} diff --git a/scripts/deploy-contract/src/main.rs b/scripts/deploy-contract/src/main.rs new file mode 100644 index 0000000..4b8b938 --- /dev/null +++ b/scripts/deploy-contract/src/main.rs @@ -0,0 +1,77 @@ +use std::{path::PathBuf, process::Command}; + +use anyhow::{bail, Result}; +use clap::Parser; +use firefly::FireflyCardanoClient; +use wit_component::ComponentEncoder; + +mod firefly; + +#[derive(Parser)] +struct Args { + #[arg(long)] + contract_path: PathBuf, + #[arg(long, default_value = "http://localhost:5018")] + firefly_cardano_url: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + let Some(name) = args.contract_path.file_name() else { + bail!("couldn't find contract name"); + }; + let Some(name) = name.to_str() else { + bail!("invalid contract name"); + }; + + println!("Compiling {name}..."); + + Command::new("cargo") + .arg("build") + .arg("--target") + .arg("wasm32-unknown-unknown") + .arg("--release") + .current_dir(&args.contract_path) + .exec()?; + + let filename = format!("{}.wasm", name.replace("-", "_")); + let path = args + .contract_path + .join("target") + .join("wasm32-unknown-unknown") + .join("release") + .join(filename); + + println!("Bundling {name} as WASM component..."); + let module = wat::Parser::new().parse_file(path)?; + let component = ComponentEncoder::default() + .validate(true) + .module(&module)? + .encode()?; + let contract = hex::encode(&component); + + println!("Deploying {name} to FireFly..."); + let firefly = FireflyCardanoClient::new(&args.firefly_cardano_url); + firefly.deploy_contract(name, &contract).await?; + + Ok(()) +} + +trait CommandExt { + fn exec(&mut self) -> Result<()>; +} + +impl CommandExt for Command { + fn exec(&mut self) -> Result<()> { + let output = self.output()?; + if !output.stderr.is_empty() { + eprintln!("{}", String::from_utf8(output.stderr)?); + } + if !output.status.success() { + bail!("command failed: {}", output.status); + } + Ok(()) + } +} diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml deleted file mode 100644 index 7c60fef..0000000 --- a/wasm/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[workspace] - -members = [ - "simple-tx", -] - -resolver = "2" \ No newline at end of file diff --git a/wasm/Cargo.lock b/wasm/simple-tx/Cargo.lock similarity index 99% rename from wasm/Cargo.lock rename to wasm/simple-tx/Cargo.lock index ca0838d..0d261c4 100644 --- a/wasm/Cargo.lock +++ b/wasm/simple-tx/Cargo.lock @@ -165,9 +165,9 @@ checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "shlex", ] @@ -999,9 +999,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "spdx" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47317bbaf63785b53861e1ae2d11b80d6b624211d42cb20efcd210ee6f8a14bc" +checksum = "bae30cc7bfe3656d60ee99bf6836f472b0c53dddcbf335e253329abb16e535a2" dependencies = [ "smallvec", ]