cargo_tangle/
deploy.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
use alloy_provider::network::TransactionBuilder;
use alloy_provider::Provider;
pub use alloy_signer_local::PrivateKeySigner;
use color_eyre::eyre::{self, Context, ContextCompat, OptionExt, Result};
use gadget_blueprint_proc_macro_core::{BlueprintManager, ServiceBlueprint};
use gadget_sdk::clients::tangle::runtime::TangleConfig;
pub use k256;
use std::fmt::Debug;
use std::path::PathBuf;
use tangle_subxt::subxt;
use tangle_subxt::subxt::ext::sp_core;
use tangle_subxt::subxt::tx::PairSigner;
use tangle_subxt::tangle_testnet_runtime::api as TangleApi;
use tangle_subxt::tangle_testnet_runtime::api::services::calls::types;

pub type TanglePairSigner = PairSigner<TangleConfig, sp_core::sr25519::Pair>;

#[derive(Clone)]
pub struct Opts {
    /// The name of the package to deploy (if the workspace has multiple packages)
    pub pkg_name: Option<String>,
    /// The RPC URL of the Tangle Network
    pub rpc_url: String,
    /// The path to the manifest file
    pub manifest_path: std::path::PathBuf,
    /// The signer for deploying the blueprint
    pub signer: Option<TanglePairSigner>,
    /// The signer for deploying the smart contract
    pub signer_evm: Option<PrivateKeySigner>,
}

impl Debug for Opts {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Opts")
            .field("pkg_name", &self.pkg_name)
            .field("rpc_url", &self.rpc_url)
            .field("manifest_path", &self.manifest_path)
            .finish()
    }
}

pub async fn generate_service_blueprint<P: Into<PathBuf>, T: AsRef<str>>(
    manifest_metadata_path: P,
    pkg_name: Option<&String>,
    rpc_url: T,
    signer_evm: Option<PrivateKeySigner>,
) -> Result<types::create_blueprint::Blueprint> {
    let manifest_path = manifest_metadata_path.into();
    let metadata = cargo_metadata::MetadataCommand::new()
        .manifest_path(manifest_path)
        .no_deps()
        .exec()
        .context("Getting Metadata about the workspace")?;

    let package = find_package(&metadata, pkg_name)?.clone();
    let package_clone = &package.clone();
    let mut blueprint =
        tokio::task::spawn_blocking(move || load_blueprint_metadata(&package)).await??;
    build_contracts_if_needed(package_clone, &blueprint).context("Building contracts")?;
    deploy_contracts_to_tangle(rpc_url.as_ref(), package_clone, &mut blueprint, signer_evm).await?;

    bake_blueprint(blueprint)
}

pub async fn deploy_to_tangle(
    Opts {
        pkg_name,
        rpc_url,
        manifest_path,
        signer,
        signer_evm,
    }: Opts,
) -> Result<u64> {
    // Load the manifest file into cargo metadata
    let blueprint =
        generate_service_blueprint(&manifest_path, pkg_name.as_ref(), &rpc_url, signer_evm).await?;

    let signer = if let Some(signer) = signer {
        signer
    } else {
        crate::signer::load_signer_from_env()?
    };

    let my_account_id = signer.account_id();
    let client = subxt::OnlineClient::from_url(rpc_url).await?;

    let create_blueprint_tx = TangleApi::tx().services().create_blueprint(blueprint);

    let progress = client
        .tx()
        .sign_and_submit_then_watch_default(&create_blueprint_tx, &signer)
        .await?;
    let result = progress.wait_for_finalized_success().await?;
    let event = result
        .find::<TangleApi::services::events::BlueprintCreated>()
        .flatten()
        .find(|e| e.owner.0 == my_account_id.0)
        .context("Finding the `BlueprintCreated` event")
        .map_err(|e| {
            eyre::eyre!(
                "Trying to find the `BlueprintCreated` event with your account Id: {:?}",
                e
            )
        })?;
    println!(
        "Blueprint #{} created successfully by {} with extrinsic hash: {}",
        event.blueprint_id,
        event.owner,
        result.extrinsic_hash(),
    );

    Ok(event.blueprint_id)
}

pub fn load_blueprint_metadata(
    package: &cargo_metadata::Package,
) -> Result<ServiceBlueprint<'static>> {
    let blueprint_json_path = package
        .manifest_path
        .parent()
        .map(|p| p.join("blueprint.json"))
        .unwrap();

    if !blueprint_json_path.exists() {
        eprintln!("Could not find blueprint.json; running `cargo build`...");
        // Need to run cargo build for the current package.
        escargot::CargoBuild::new()
            .manifest_path(&package.manifest_path)
            .package(&package.name)
            .run()
            .context("Failed to build the package")?;
    }
    // should have the blueprint.json
    let blueprint_json =
        std::fs::read_to_string(blueprint_json_path).context("Reading blueprint.json file")?;
    let blueprint = serde_json::from_str(&blueprint_json)?;
    Ok(blueprint)
}

async fn deploy_contracts_to_tangle(
    rpc_url: &str,
    package: &cargo_metadata::Package,
    blueprint: &mut ServiceBlueprint<'_>,
    signer_evm: Option<PrivateKeySigner>,
) -> Result<()> {
    enum ContractKind {
        Manager,
    }
    let rpc_url = rpc_url.replace("ws", "http").replace("wss", "https");
    let contract_paths = match blueprint.manager {
        BlueprintManager::Evm(ref path) => vec![(ContractKind::Manager, path)],
        _ => {
            eprintln!("Unsupported blueprint manager kind");
            vec![]
        }
    };

    let abs_contract_paths: Vec<_> = contract_paths
        .into_iter()
        .map(|(kind, path)| (kind, resolve_path_relative_to_package(package, path)))
        .collect();

    let contracts = abs_contract_paths
        .iter()
        .flat_map(|(kind, path)| {
            std::fs::read_to_string(path).map(|content| {
                (
                    kind,
                    path.file_stem().unwrap_or_default().to_string_lossy(),
                    content,
                )
            })
        })
        .flat_map(|(kind, contract_name, json)| {
            serde_json::from_str::<alloy_json_abi::ContractObject>(&json)
                .map(|contract| (kind, contract_name, contract))
        })
        .collect::<Vec<_>>();

    if contracts.is_empty() {
        return Ok(());
    }

    let signer = if let Some(signer) = signer_evm {
        signer
    } else {
        crate::signer::load_evm_signer_from_env()?
    };

    let wallet = alloy_provider::network::EthereumWallet::from(signer);
    let provider = alloy_provider::ProviderBuilder::new()
        .with_recommended_fillers()
        .wallet(wallet)
        .on_http(rpc_url.parse()?);

    let chain_id = provider.get_chain_id().await?;
    eprintln!("Chain ID: {chain_id}");

    for (kind, name, contract) in contracts {
        eprintln!("Deploying contract: {name} ...");
        let Some(bytecode) = contract.bytecode.clone() else {
            eprintln!("Contract {name} does not have deployed bytecode! Skipping ...");
            continue;
        };
        let tx = alloy_rpc_types::TransactionRequest::default().with_deploy_code(bytecode);
        // Deploy the contract.
        let receipt = provider.send_transaction(tx).await?.get_receipt().await?;
        // Check the receipt status.
        if receipt.status() {
            let contract_address =
                alloy_network::ReceiptResponse::contract_address(&receipt).unwrap();
            eprintln!("Contract {name} deployed at: {contract_address}");
            match kind {
                ContractKind::Manager => {
                    blueprint.manager = BlueprintManager::Evm(contract_address.to_string());
                }
            }
        } else {
            eprintln!("Contract {name} deployment failed!");
            eprintln!("Receipt: {receipt:#?}");
        }
    }
    Ok(())
}

/// Checks if the contracts need to be built and builds them if needed.
fn build_contracts_if_needed(
    package: &cargo_metadata::Package,
    blueprint: &ServiceBlueprint,
) -> Result<()> {
    let pathes_to_check = match blueprint.manager {
        BlueprintManager::Evm(ref path) => vec![path],
        _ => {
            eprintln!("Unsupported blueprint manager kind");
            vec![]
        }
    };

    let abs_pathes_to_check: Vec<_> = pathes_to_check
        .into_iter()
        .map(|path| resolve_path_relative_to_package(package, path))
        .collect();

    let needs_build = abs_pathes_to_check.iter().any(|path| !path.exists());

    if needs_build {
        let mut foundry = crate::foundry::FoundryToolchain::new();
        foundry.check_installed_or_exit();
        foundry.forge.build()?;
    }

    Ok(())
}

/// Converts the ServiceBlueprint to a format that can be sent to the Tangle Network.
fn bake_blueprint(
    blueprint: ServiceBlueprint,
) -> Result<TangleApi::runtime_types::tangle_primitives::services::ServiceBlueprint> {
    let mut blueprint_json = serde_json::to_value(&blueprint)?;
    convert_to_bytes_or_null(&mut blueprint_json["metadata"]["name"]);
    convert_to_bytes_or_null(&mut blueprint_json["metadata"]["description"]);
    convert_to_bytes_or_null(&mut blueprint_json["metadata"]["author"]);
    convert_to_bytes_or_null(&mut blueprint_json["metadata"]["license"]);
    convert_to_bytes_or_null(&mut blueprint_json["metadata"]["website"]);
    convert_to_bytes_or_null(&mut blueprint_json["metadata"]["code_repository"]);
    convert_to_bytes_or_null(&mut blueprint_json["metadata"]["category"]);
    for job in blueprint_json["jobs"].as_array_mut().unwrap() {
        convert_to_bytes_or_null(&mut job["metadata"]["name"]);
        convert_to_bytes_or_null(&mut job["metadata"]["description"]);
    }

    // Retrieves the Gadget information from the blueprint.json file.
    // From this data, we find the sources of the Gadget.
    let (_, gadget) = blueprint_json["gadget"]
        .as_object_mut()
        .expect("Bad gadget value")
        .iter_mut()
        .next()
        .expect("Should be at least one gadget");
    let sources = gadget["sources"].as_array_mut().expect("Should be a list");

    // The source includes where to fetch the Blueprint, the name of the blueprint, and
    // the name of the binary. From this data, we create the blueprint's [`ServiceBlueprint`]
    for (idx, source) in sources.iter_mut().enumerate() {
        if let Some(fetchers) = source["fetcher"].as_object_mut() {
            let fetcher_fields = fetchers
                .iter_mut()
                .next()
                .expect("Should be at least one fetcher")
                .1;
            for (_key, value) in fetcher_fields
                .as_object_mut()
                .expect("Fetcher should be a map")
            {
                if value.is_array() {
                    let xs = value.as_array_mut().expect("Value should be an array");
                    for x in xs {
                        if x.is_object() {
                            convert_to_bytes_or_null(&mut x["name"]);
                        }
                    }
                } else {
                    convert_to_bytes_or_null(value);
                }
            }
        } else {
            panic!("The source at index {idx} does not have a valid fetcher");
        }
    }

    println!("Job: {blueprint_json}");
    let blueprint = serde_json::from_value(blueprint_json)?;
    Ok(blueprint)
}

/// Recursively converts a JSON string (or array of JSON strings) to bytes.
///
/// Empty strings are converted to nulls.
fn convert_to_bytes_or_null(v: &mut serde_json::Value) {
    if let serde_json::Value::String(s) = v {
        *v = serde_json::Value::Array(s.bytes().map(serde_json::Value::from).collect());
        return;
    }

    if let serde_json::Value::Array(vals) = v {
        for val in vals {
            convert_to_bytes_or_null(val);
        }
    }
}

/// Resolves a path relative to the package manifest.
fn resolve_path_relative_to_package(
    package: &cargo_metadata::Package,
    path: &str,
) -> std::path::PathBuf {
    if path.starts_with('/') {
        std::path::PathBuf::from(path)
    } else {
        package.manifest_path.parent().unwrap().join(path).into()
    }
}

/// Finds a package in the workspace to deploy.
fn find_package<'m>(
    metadata: &'m cargo_metadata::Metadata,
    pkg_name: Option<&String>,
) -> Result<&'m cargo_metadata::Package, eyre::Error> {
    match metadata.workspace_members.len() {
        0 => Err(eyre::eyre!("No packages found in the workspace")),
        1 => metadata
            .packages
            .iter()
            .find(|p| p.id == metadata.workspace_members[0])
            .ok_or_eyre("No package found in the workspace"),
        _more_than_one if pkg_name.is_some() => metadata
            .packages
            .iter()
            .find(|p| pkg_name.is_some_and(|v| &p.name == v))
            .ok_or_eyre("No package found in the workspace with the specified name"),
        _otherwise => {
            eprintln!("Please specify the package to deploy:");
            for package in metadata.packages.iter() {
                eprintln!("Found: {}", package.name);
            }
            eprintln!();
            Err(eyre::eyre!(
                "The workspace has multiple packages, please specify the package to deploy"
            ))
        }
    }
}