Skip to content

Commit

Permalink
feat: implement an endpoint+script to deploy contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
SupernaviX committed Nov 18, 2024
1 parent 9602c50 commit b706578
Show file tree
Hide file tree
Showing 13 changed files with 308 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.direnv
result*
/target
/wasm/target
/wasm/**/target
.envrc.local
infra/wallet
.vscode/*
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json.default
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"rust-analyzer.linkedProjects": [
"Cargo.toml",
"wasm/Cargo.toml"
"wasm/simple-tx/Cargo.toml"
]
}
88 changes: 84 additions & 4 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ members = [
"firefly-cardanosigner",
"firefly-server",
"scripts/demo",
"scripts/deploy-contract",
]

exclude = ["wasm"]

resolver = "2"
20 changes: 19 additions & 1 deletion firefly-cardanoconnect/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct ContractsConfig {
}

pub struct ContractManager {
components_path: Option<PathBuf>,
runtime: Option<RwLock<Runtime>>,
}

Expand All @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion firefly-cardanoconnect/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Expand Down
21 changes: 21 additions & 0 deletions firefly-cardanoconnect/src/operations/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions firefly-cardanoconnect/src/routes/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ pub struct InvokeRequest {
pub params: Vec<Value>,
}

#[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 {
Expand Down Expand Up @@ -70,6 +85,19 @@ pub struct OperationReceipt {
pub protocol_id: Option<String>,
}

pub async fn deploy_contract(
State(AppState { operations, .. }): State<AppState>,
Json(req): Json<DeployRequest>,
) -> ApiResult<NoContent> {
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<AppState>,
Json(req): Json<InvokeRequest>,
Expand Down
15 changes: 15 additions & 0 deletions scripts/deploy-contract/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
53 changes: 53 additions & 0 deletions scripts/deploy-contract/src/firefly.rs
Original file line number Diff line number Diff line change
@@ -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<Response> {
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,
}
Loading

0 comments on commit b706578

Please sign in to comment.