From 4eeacc930f7e7957df21addd49cfeb64acd6868c Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 27 Aug 2024 01:37:41 +0000 Subject: [PATCH 1/3] init --- Cargo.lock | 2 + crates/cli/Cargo.toml | 2 + crates/cli/src/commands/mod.rs | 3 +- crates/cli/src/commands/words/mod.rs | 327 +++++++++++++++++++++++++++ crates/cli/src/lib.rs | 5 +- 5 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 crates/cli/src/commands/words/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 092bde54f..4f3539c61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6306,7 +6306,9 @@ dependencies = [ "chrono", "clap", "comfy-table", + "csv", "httpmock", + "rain-metadata 0.0.2-alpha.6", "rain_orderbook_app_settings", "rain_orderbook_bindings", "rain_orderbook_common", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a9b297476..9f1e96d50 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -26,6 +26,7 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ['env-filter'] } comfy-table = { workspace = true } chrono = { workspace = true } +csv = { workspace = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] tokio = { workspace = true, features = ["full"] } @@ -36,3 +37,4 @@ tokio = { workspace = true, features = ["sync", "macros", "io-util", "rt", "time [dev-dependencies] httpmock = "0.7.0" serde_json = { workspace = true } +rain-metadata = { workspace = true } diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index a48eb6eb6..c3a540def 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -3,5 +3,6 @@ mod order; mod order_take; mod quote; mod vault; +mod words; -pub use self::{chart::Chart, order::Order, order_take::OrderTake, vault::Vault}; +pub use self::{chart::Chart, order::Order, order_take::OrderTake, vault::Vault, words::Words}; diff --git a/crates/cli/src/commands/words/mod.rs b/crates/cli/src/commands/words/mod.rs new file mode 100644 index 000000000..78633708d --- /dev/null +++ b/crates/cli/src/commands/words/mod.rs @@ -0,0 +1,327 @@ +use crate::execute::Execute; +use anyhow::anyhow; +use anyhow::Result; +use clap::{ArgAction, Args, Parser}; +use csv::Writer; +use rain_orderbook_app_settings::config_source::ConfigSource; +use rain_orderbook_app_settings::Config; +use rain_orderbook_common::dotrain::RainDocument; +use rain_orderbook_common::dotrain_order::AuthoringMetaV2; +use std::{fs::read_to_string, path::PathBuf}; + +/// Get words of a deployer contract from the given inputs +#[derive(Debug, Parser, PartialEq)] +pub struct Words { + #[command(flatten)] + pub input: Input, + + /// Deployer key to get its associating words + #[arg(short = 'e', long)] + pub deployer: String, + + /// Optional metaboard subgraph url, will override the metaboard in + /// frontmatter or if frontmatter has no metaboard specified inside + #[arg(short = 's', long, value_name = "URL")] + pub metaboard_subgraph: Option, + + /// Optional output file path to write the result into + #[arg(short = 'o', long)] + pub output: Option, + + /// Print the result on console + #[arg(short, long, action = ArgAction::SetTrue)] + pub print: bool, +} + +/// Group of possible input files, at least one of dotrain file or +/// setting yml file or both +#[derive(Args, Clone, Debug, PartialEq)] +#[group(required = true, multiple = true)] +pub struct Input { + /// Path to the .rain file specifying the order + #[arg(short = 'f', long, value_name = "PATH")] + pub dotrain_file: Option, + + /// Path to the settings yaml file + #[arg(short = 'c', long, value_name = "PATH")] + pub settings_file: Option, +} + +impl Execute for Words { + async fn execute(&self) -> Result<()> { + let dotrain_frontmatter = self + .input + .dotrain_file + .as_ref() + .and_then(|v| read_to_string(v).ok()) + .and_then(|v| RainDocument::get_front_matter(&v).map(|f| f.to_string())); + let settings = self + .input + .settings_file + .as_ref() + .and_then(|v| read_to_string(v).ok()); + + let config: Config = if dotrain_frontmatter.is_some() && settings.is_some() { + let mut frontmatter_config = + ConfigSource::try_from_string(dotrain_frontmatter.unwrap()).await?; + frontmatter_config.merge(ConfigSource::try_from_string(settings.unwrap()).await?)?; + frontmatter_config.try_into()? + } else if dotrain_frontmatter.is_some() { + ConfigSource::try_from_string(dotrain_frontmatter.unwrap()) + .await? + .try_into()? + } else if settings.is_some() { + ConfigSource::try_from_string(settings.unwrap()) + .await? + .try_into()? + } else { + // clap doesnt allow this to happen since at least 1 input is required + panic!("cant happen!") + }; + + let deployer = config + .deployers + .get(&self.deployer) + .ok_or(anyhow!("undefined deployer!"))?; + + let metaboard_url = if let Some(v) = &self.metaboard_subgraph { + v.to_string() + } else if let Some(v) = config.metaboards.get(&deployer.network.name) { + v.to_string() + } else { + config + .networks + .iter() + .find(|(_, v)| **v == deployer.network) + .ok_or(anyhow!("undefined metaboard subgraph url")) + .and_then(|(k, _)| { + Ok(config + .metaboards + .get(k) + .ok_or(anyhow!("undefined metaboard subgraph url"))? + .to_string()) + })? + }; + + let results = AuthoringMetaV2::fetch_for_contract( + deployer.address, + deployer.network.rpc.to_string(), + metaboard_url, + ) + .await? + .words; + + let mut csv_writer = Writer::from_writer(vec![]); + for item in results.clone().into_iter() { + csv_writer.serialize(item)?; + } + let text = String::from_utf8(csv_writer.into_inner()?)?; + + if let Some(output) = &self.output { + std::fs::write(output, &text)?; + } + if self.print { + println!("{}", text); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::{hex::encode_prefixed, primitives::B256, sol, sol_types::SolValue}; + use alloy_ethers_typecast::rpc::Response; + use clap::CommandFactory; + use httpmock::MockServer; + use rain_metadata::{KnownMagic, RainMetaDocumentV1Item}; + use serde_bytes::ByteBuf; + + sol!( + struct AuthoringMetaV2Sol { + bytes32 word; + string description; + } + ); + + #[test] + fn verify_command() { + Words::command().debug_assert(); + } + + #[tokio::test] + async fn test_execute_happy() { + let server = MockServer::start(); + // contract calls + server.mock(|when, then| { + when.path("/rpc").body_contains("0x01ffc9a701ffc9a7"); + then.body( + Response::new_success(1, &B256::left_padding_from(&[1]).to_string()) + .to_json_string() + .unwrap(), + ); + }); + server.mock(|when, then| { + when.path("/rpc").body_contains("0x01ffc9a7ffffffff"); + then.body( + Response::new_success(1, &B256::left_padding_from(&[0]).to_string()) + .to_json_string() + .unwrap(), + ); + }); + server.mock(|when, then| { + when.path("/rpc").body_contains("0x01ffc9a7"); + then.body( + Response::new_success(1, &B256::left_padding_from(&[1]).to_string()) + .to_json_string() + .unwrap(), + ); + }); + server.mock(|when, then| { + when.path("/rpc").body_contains("0x6f5aa28d"); + then.body( + Response::new_success(1, &B256::random().to_string()) + .to_json_string() + .unwrap(), + ); + }); + + // mock sg query + let mocked_words = vec![AuthoringMetaV2Sol { + word: B256::right_padding_from("some-word".as_bytes()), + description: "some-desc".to_string(), + }] + .abi_encode(); + let meta_bytes = RainMetaDocumentV1Item { + payload: ByteBuf::from(mocked_words), + magic: KnownMagic::AuthoringMetaV2, + content_type: rain_metadata::ContentType::OctetStream, + content_encoding: rain_metadata::ContentEncoding::None, + content_language: rain_metadata::ContentLanguage::None, + } + .cbor_encode() + .unwrap(); + server.mock(|when, then| { + when.path("/sg"); // You need to tailor this to the actual body sent + then.status(200).json_body_obj(&{ + serde_json::json!({ + "data": { + "metaV1S": [ + { + "meta": encode_prefixed(meta_bytes), + "metaHash": "0x00", + "sender": "0x00", + "id": "0x00", + "metaBoard": { + "id": "0x00", + "metas": [], + "address": "0x00", + }, + "subject": "0x00", + } + ] + } + }) + }); + }); + + let dotrain_content = format!( + " +networks: + some-network: + rpc: {} + chain-id: 123 + network-id: 123 + currency: ETH + +metaboards: + some-network: {} + +deployers: + some-deployer: + network: some-network + address: 0xF14E09601A47552De6aBd3A0B165607FaFd2B5Ba +--- +#binding +:;", + server.url("/rpc"), + server.url("/sg") + ); + let dotrain_path = "./test_dotrain_words_happy.rain"; + std::fs::write(dotrain_path, dotrain_content).unwrap(); + + let words = Words { + input: Input { + dotrain_file: Some(dotrain_path.into()), + settings_file: None, + }, + deployer: "some-deployer".to_string(), + metaboard_subgraph: None, + output: None, + print: true, + }; + + // should execute successfully + assert!(words.execute().await.is_ok()); + + // remove test file + std::fs::remove_file(dotrain_path).unwrap(); + } + + #[tokio::test] + async fn test_execute_unhappy() { + let server = MockServer::start(); + // doesn implement IDescribeByMetaV1 + server.mock(|when, then| { + when.path("/rpc").body_contains("0x01ffc9a701ffc9a7"); + then.body( + Response::new_success(1, &B256::left_padding_from(&[0]).to_string()) + .to_json_string() + .unwrap(), + ); + }); + + let dotrain_content = format!( + " +networks: + some-network: + rpc: {} + chain-id: 123 + network-id: 123 + currency: ETH + +metaboards: + some-network: {} + +deployers: + some-deployer: + network: some-network + address: 0xF14E09601A47552De6aBd3A0B165607FaFd2B5Ba +--- +#binding +:;", + server.url("/rpc"), + server.url("/sg") + ); + let dotrain_path = "./test_dotrain_words_unhappy.rain"; + std::fs::write(dotrain_path, dotrain_content).unwrap(); + + let words = Words { + input: Input { + dotrain_file: Some(dotrain_path.into()), + settings_file: None, + }, + deployer: "some-deployer".to_string(), + metaboard_subgraph: None, + output: None, + print: true, + }; + + // should execute successfully + assert!(words.execute().await.is_err()); + + // remove test file + std::fs::remove_file(dotrain_path).unwrap(); + } +} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 9b38d0d10..4f8720104 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,4 +1,4 @@ -use crate::commands::{Chart, Order, OrderTake, Vault}; +use crate::commands::{Chart, Order, OrderTake, Vault, Words}; use crate::execute::Execute; use anyhow::Result; use clap::Subcommand; @@ -25,6 +25,8 @@ pub enum Orderbook { Chart(Chart), Quote(Quoter), + + Words(Words), } impl Orderbook { @@ -35,6 +37,7 @@ impl Orderbook { Orderbook::OrderTake(order_take) => (order_take).execute().await, Orderbook::Chart(chart) => chart.execute().await, Orderbook::Quote(quote) => quote.execute().await, + Orderbook::Words(words) => words.execute().await, } } } From 8e9009c20846914aaba02e222e554e13fbadcf49 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 27 Aug 2024 01:44:30 +0000 Subject: [PATCH 2/3] Update mod.rs --- crates/cli/src/commands/words/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cli/src/commands/words/mod.rs b/crates/cli/src/commands/words/mod.rs index 78633708d..b0d252bdb 100644 --- a/crates/cli/src/commands/words/mod.rs +++ b/crates/cli/src/commands/words/mod.rs @@ -318,7 +318,7 @@ deployers: print: true, }; - // should execute successfully + // should fail assert!(words.execute().await.is_err()); // remove test file From d63f7692d9a5a0e1d9a040d41d688dc3470c9879 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 27 Aug 2024 04:41:27 +0000 Subject: [PATCH 3/3] Update mod.rs --- crates/cli/src/commands/words/mod.rs | 372 +++++++++++++++++---------- 1 file changed, 238 insertions(+), 134 deletions(-) diff --git a/crates/cli/src/commands/words/mod.rs b/crates/cli/src/commands/words/mod.rs index b0d252bdb..7462c12cc 100644 --- a/crates/cli/src/commands/words/mod.rs +++ b/crates/cli/src/commands/words/mod.rs @@ -1,26 +1,23 @@ use crate::execute::Execute; -use anyhow::anyhow; -use anyhow::Result; +use anyhow::{anyhow, Result}; use clap::{ArgAction, Args, Parser}; use csv::Writer; -use rain_orderbook_app_settings::config_source::ConfigSource; -use rain_orderbook_app_settings::Config; -use rain_orderbook_common::dotrain::RainDocument; -use rain_orderbook_common::dotrain_order::AuthoringMetaV2; +use rain_orderbook_app_settings::{config::Config, config_source::ConfigSource}; +use rain_orderbook_common::{dotrain::RainDocument, dotrain_order::AuthoringMetaV2}; use std::{fs::read_to_string, path::PathBuf}; /// Get words of a deployer contract from the given inputs -#[derive(Debug, Parser, PartialEq)] +#[derive(Debug, Parser)] pub struct Words { #[command(flatten)] pub input: Input, /// Deployer key to get its associating words - #[arg(short = 'e', long)] + #[arg(short = 'd', long)] pub deployer: String, /// Optional metaboard subgraph url, will override the metaboard in - /// frontmatter or if frontmatter has no metaboard specified inside + /// inputs or if inputs has no metaboard specified inside #[arg(short = 's', long, value_name = "URL")] pub metaboard_subgraph: Option, @@ -28,9 +25,9 @@ pub struct Words { #[arg(short = 'o', long)] pub output: Option, - /// Print the result on console - #[arg(short, long, action = ArgAction::SetTrue)] - pub print: bool, + /// Print the result on console (send result to std out) + #[arg(long, action = ArgAction::SetTrue)] + pub stdout: bool, } /// Group of possible input files, at least one of dotrain file or @@ -49,59 +46,64 @@ pub struct Input { impl Execute for Words { async fn execute(&self) -> Result<()> { - let dotrain_frontmatter = self - .input - .dotrain_file - .as_ref() - .and_then(|v| read_to_string(v).ok()) - .and_then(|v| RainDocument::get_front_matter(&v).map(|f| f.to_string())); - let settings = self - .input - .settings_file - .as_ref() - .and_then(|v| read_to_string(v).ok()); - - let config: Config = if dotrain_frontmatter.is_some() && settings.is_some() { - let mut frontmatter_config = - ConfigSource::try_from_string(dotrain_frontmatter.unwrap()).await?; - frontmatter_config.merge(ConfigSource::try_from_string(settings.unwrap()).await?)?; - frontmatter_config.try_into()? - } else if dotrain_frontmatter.is_some() { - ConfigSource::try_from_string(dotrain_frontmatter.unwrap()) + // handle and build Config from inputs + let config: Config = + if self.input.dotrain_file.is_some() && self.input.settings_file.is_some() { + let mut config = ConfigSource::try_from_string( + RainDocument::get_front_matter(&read_to_string( + self.input.dotrain_file.as_ref().unwrap(), + )?) + .unwrap_or("") + .to_string(), + ) + .await?; + config.merge( + ConfigSource::try_from_string(read_to_string( + self.input.settings_file.as_ref().unwrap(), + )?) + .await?, + )?; + config.try_into()? + } else if self.input.dotrain_file.is_some() { + ConfigSource::try_from_string( + RainDocument::get_front_matter(&read_to_string( + self.input.dotrain_file.as_ref().unwrap(), + )?) + .unwrap_or("") + .to_string(), + ) .await? .try_into()? - } else if settings.is_some() { - ConfigSource::try_from_string(settings.unwrap()) + } else if self.input.settings_file.is_some() { + ConfigSource::try_from_string(read_to_string( + self.input.settings_file.as_ref().unwrap(), + )?) .await? .try_into()? - } else { - // clap doesnt allow this to happen since at least 1 input is required - panic!("cant happen!") - }; + } else { + // clap doesnt allow this to happen since at least 1 input + // is required which is enforced and catched by clap + panic!("undefined input") + }; + // get deployer from config let deployer = config .deployers .get(&self.deployer) .ok_or(anyhow!("undefined deployer!"))?; - let metaboard_url = if let Some(v) = &self.metaboard_subgraph { - v.to_string() - } else if let Some(v) = config.metaboards.get(&deployer.network.name) { - v.to_string() - } else { - config - .networks - .iter() - .find(|(_, v)| **v == deployer.network) - .ok_or(anyhow!("undefined metaboard subgraph url")) - .and_then(|(k, _)| { - Ok(config - .metaboards - .get(k) - .ok_or(anyhow!("undefined metaboard subgraph url"))? - .to_string()) - })? - }; + // get metaboard subgraph url + let metaboard_url = self + .metaboard_subgraph + .as_ref() + .map(|v| v.to_string()) + .or_else(|| { + config + .metaboards + .get(&deployer.network.name) + .map(|v| v.to_string()) + }) + .ok_or(anyhow!("undefined metaboard subgraph url"))?; let results = AuthoringMetaV2::fetch_for_contract( deployer.address, @@ -120,7 +122,7 @@ impl Execute for Words { if let Some(output) = &self.output { std::fs::write(output, &text)?; } - if self.print { + if self.stdout { println!("{}", text); } @@ -151,81 +153,8 @@ mod tests { } #[tokio::test] - async fn test_execute_happy() { - let server = MockServer::start(); - // contract calls - server.mock(|when, then| { - when.path("/rpc").body_contains("0x01ffc9a701ffc9a7"); - then.body( - Response::new_success(1, &B256::left_padding_from(&[1]).to_string()) - .to_json_string() - .unwrap(), - ); - }); - server.mock(|when, then| { - when.path("/rpc").body_contains("0x01ffc9a7ffffffff"); - then.body( - Response::new_success(1, &B256::left_padding_from(&[0]).to_string()) - .to_json_string() - .unwrap(), - ); - }); - server.mock(|when, then| { - when.path("/rpc").body_contains("0x01ffc9a7"); - then.body( - Response::new_success(1, &B256::left_padding_from(&[1]).to_string()) - .to_json_string() - .unwrap(), - ); - }); - server.mock(|when, then| { - when.path("/rpc").body_contains("0x6f5aa28d"); - then.body( - Response::new_success(1, &B256::random().to_string()) - .to_json_string() - .unwrap(), - ); - }); - - // mock sg query - let mocked_words = vec![AuthoringMetaV2Sol { - word: B256::right_padding_from("some-word".as_bytes()), - description: "some-desc".to_string(), - }] - .abi_encode(); - let meta_bytes = RainMetaDocumentV1Item { - payload: ByteBuf::from(mocked_words), - magic: KnownMagic::AuthoringMetaV2, - content_type: rain_metadata::ContentType::OctetStream, - content_encoding: rain_metadata::ContentEncoding::None, - content_language: rain_metadata::ContentLanguage::None, - } - .cbor_encode() - .unwrap(); - server.mock(|when, then| { - when.path("/sg"); // You need to tailor this to the actual body sent - then.status(200).json_body_obj(&{ - serde_json::json!({ - "data": { - "metaV1S": [ - { - "meta": encode_prefixed(meta_bytes), - "metaHash": "0x00", - "sender": "0x00", - "id": "0x00", - "metaBoard": { - "id": "0x00", - "metas": [], - "address": "0x00", - }, - "subject": "0x00", - } - ] - } - }) - }); - }); - + async fn test_execute_happy_with_dotrain() { + let server = mock_server(); let dotrain_content = format!( " networks: @@ -259,20 +188,113 @@ deployers: deployer: "some-deployer".to_string(), metaboard_subgraph: None, output: None, - print: true, + stdout: true, + }; + + // should execute successfully + assert!(words.execute().await.is_ok()); + + // remove test file + std::fs::remove_file(dotrain_path).unwrap(); + } + + #[tokio::test] + async fn test_execute_happy_with_settings() { + let server = mock_server(); + let settings_content = format!( + " +networks: + some-network: + rpc: {} + chain-id: 123 + network-id: 123 + currency: ETH + +metaboards: + some-network: {} + +deployers: + some-deployer: + network: some-network + address: 0xF14E09601A47552De6aBd3A0B165607FaFd2B5Ba", + server.url("/rpc"), + server.url("/sg") + ); + let settings_path = "./test_settings_words_happy.rain"; + std::fs::write(settings_path, settings_content).unwrap(); + + let words = Words { + input: Input { + settings_file: Some(settings_path.into()), + dotrain_file: None, + }, + deployer: "some-deployer".to_string(), + metaboard_subgraph: None, + output: None, + stdout: true, }; // should execute successfully assert!(words.execute().await.is_ok()); // remove test file + std::fs::remove_file(settings_path).unwrap(); + } + + #[tokio::test] + async fn test_execute_happy_all() { + let server = mock_server(); + let dotrain_content = format!( + " +metaboards: + some-network: {} +--- +#binding\n:;", + server.url("/sg") + ); + let settings_content = format!( + " +networks: + some-network: + rpc: {} + chain-id: 123 + network-id: 123 + currency: ETH + +deployers: + some-deployer: + network: some-network + address: 0xF14E09601A47552De6aBd3A0B165607FaFd2B5Ba", + server.url("/rpc"), + ); + let settings_path = "./test_settings_words_happy_all.rain"; + std::fs::write(settings_path, settings_content).unwrap(); + let dotrain_path = "./test_dotrain_words_happy_all.rain"; + std::fs::write(dotrain_path, dotrain_content).unwrap(); + + let words = Words { + input: Input { + settings_file: Some(settings_path.into()), + dotrain_file: Some(dotrain_path.into()), + }, + deployer: "some-deployer".to_string(), + metaboard_subgraph: None, + output: None, + stdout: true, + }; + + // should execute successfully + assert!(words.execute().await.is_ok()); + + // remove test files + std::fs::remove_file(settings_path).unwrap(); std::fs::remove_file(dotrain_path).unwrap(); } #[tokio::test] async fn test_execute_unhappy() { let server = MockServer::start(); - // doesn implement IDescribeByMetaV1 + // mock contract calls that doesnt implement IDescribeByMetaV1 server.mock(|when, then| { when.path("/rpc").body_contains("0x01ffc9a701ffc9a7"); then.body( @@ -315,7 +337,7 @@ deployers: deployer: "some-deployer".to_string(), metaboard_subgraph: None, output: None, - print: true, + stdout: true, }; // should fail @@ -324,4 +346,86 @@ deployers: // remove test file std::fs::remove_file(dotrain_path).unwrap(); } + + // helper function to mock rpc and sg response + fn mock_server() -> MockServer { + let server = MockServer::start(); + // mock contract calls + server.mock(|when, then| { + when.path("/rpc").body_contains("0x01ffc9a701ffc9a7"); + then.body( + Response::new_success(1, &B256::left_padding_from(&[1]).to_string()) + .to_json_string() + .unwrap(), + ); + }); + server.mock(|when, then| { + when.path("/rpc").body_contains("0x01ffc9a7ffffffff"); + then.body( + Response::new_success(1, &B256::left_padding_from(&[0]).to_string()) + .to_json_string() + .unwrap(), + ); + }); + server.mock(|when, then| { + when.path("/rpc").body_contains("0x01ffc9a7"); + then.body( + Response::new_success(1, &B256::left_padding_from(&[1]).to_string()) + .to_json_string() + .unwrap(), + ); + }); + server.mock(|when, then| { + when.path("/rpc").body_contains("0x6f5aa28d"); + then.body( + Response::new_success(1, &B256::random().to_string()) + .to_json_string() + .unwrap(), + ); + }); + + // mock sg query + server.mock(|when, then| { + when.path("/sg"); // You need to tailor this to the actual body sent + then.status(200).json_body_obj(&serde_json::json!({ + "data": { + "metaV1S": [{ + "meta": encode_prefixed( + RainMetaDocumentV1Item { + payload: ByteBuf::from( + vec![ + AuthoringMetaV2Sol { + word: B256::right_padding_from("some-word".as_bytes()), + description: "some-desc".to_string(), + }, + AuthoringMetaV2Sol { + word: B256::right_padding_from("some-other-word".as_bytes()), + description: "some-other-desc".to_string(), + } + ] + .abi_encode(), + ), + magic: KnownMagic::AuthoringMetaV2, + content_type: rain_metadata::ContentType::OctetStream, + content_encoding: rain_metadata::ContentEncoding::None, + content_language: rain_metadata::ContentLanguage::None, + } + .cbor_encode() + .unwrap() + ), + "metaHash": "0x00", + "sender": "0x00", + "id": "0x00", + "metaBoard": { + "id": "0x00", + "metas": [], + "address": "0x00", + }, + "subject": "0x00", + }] + } + })); + }); + server + } }