diff --git a/src/api/construction_derive.rs b/src/api/construction_derive.rs index 5255373..6441bbe 100644 --- a/src/api/construction_derive.rs +++ b/src/api/construction_derive.rs @@ -3,9 +3,8 @@ use coinbase_mesh::models::{AccountIdentifier, ConstructionDeriveRequest, Constr use mina_signer::{BaseField, CompressedPubKey}; use o1_utils::FieldHelpers; use serde_json::{json, Value}; -use sha2::Digest; -use crate::{util::DEFAULT_TOKEN_ID, MinaMesh, MinaMeshError}; +use crate::{base58::validate_base58_with_checksum, util::DEFAULT_TOKEN_ID, MinaMesh, MinaMeshError}; /// https://github.com/MinaProtocol/mina/blob/985eda49bdfabc046ef9001d3c406e688bc7ec45/src/app/rosetta/lib/construction.ml#L162 impl MinaMesh { @@ -88,27 +87,6 @@ fn decode_token_id(metadata: Option) -> Result { } /// Validates the token ID format (base58 and checksum) -/// https://github.com/MinaProtocol/mina/blob/985eda49bdfabc046ef9001d3c406e688bc7ec45/src/lib/base58_check/base58_check.ml fn validate_base58_token_id(token_id: &str) -> Result<(), MinaMeshError> { - // Decode the token ID using base58 - let bytes = bs58::decode(token_id) - .with_alphabet(bs58::Alphabet::BITCOIN) - .into_vec() - .map_err(|_| MinaMeshError::MalformedPublicKey("Token_id not valid base58".to_string()))?; - - // Check the length (e.g., must include version and checksum) - if bytes.len() < 5 { - return Err(MinaMeshError::MalformedPublicKey("Token_id too short".to_string())); - } - - // Split into payload and checksum - let (payload, checksum) = bytes.split_at(bytes.len() - 4); - - // Recompute checksum - let computed_checksum = sha2::Sha256::digest(sha2::Sha256::digest(payload)); - if &computed_checksum[.. 4] != checksum { - return Err(MinaMeshError::MalformedPublicKey("Token_id checksum mismatch".to_string())); - } - - Ok(()) + validate_base58_with_checksum(token_id, None) } diff --git a/src/api/construction_preprocess.rs b/src/api/construction_preprocess.rs index b1ba68d..28347fd 100644 --- a/src/api/construction_preprocess.rs +++ b/src/api/construction_preprocess.rs @@ -1,15 +1,284 @@ use anyhow::Result; -use coinbase_mesh::models::{ConstructionPreprocessRequest, ConstructionPreprocessResponse}; +use coinbase_mesh::models::{ConstructionPreprocessRequest, ConstructionPreprocessResponse, Operation}; +use serde_json::{json, Map, Value}; -use crate::MinaMesh; +use crate::{ + base58::validate_base58_with_checksum, + util::DEFAULT_TOKEN_ID, + MinaMesh, MinaMeshError, + OperationType::{self, *}, + PartialReason, PreprocessMetadata, UserCommandType, +}; -/// https://github.com/MinaProtocol/mina/blob/985eda49bdfabc046ef9001d3c406e688bc7ec45/src/app/rosetta/lib/construction.ml#L392 impl MinaMesh { pub async fn construction_preprocess( &self, request: ConstructionPreprocessRequest, - ) -> Result { + ) -> Result { self.validate_network(&request.network_identifier).await?; - Ok(ConstructionPreprocessResponse::new()) + + let metadata = PreprocessMetadata::from_json(request.metadata)?; + let partial_command = PartialUserCommand::from_operations(&request.operations, metadata)?; + + validate_base58_public_key(partial_command.fee_payer.as_str())?; + validate_base58_public_key(partial_command.source.as_str())?; + validate_base58_public_key(partial_command.receiver.as_str())?; + + Ok(ConstructionPreprocessResponse { + options: Some(make_response_options(partial_command)), + required_public_keys: Some(vec![]), + }) + } +} + +fn make_response_options(partial_command: PartialUserCommand) -> Value { + let mut options = Map::new(); + + options.insert("sender".to_string(), json!(partial_command.fee_payer)); + options.insert("receiver".to_string(), json!(partial_command.receiver)); + options.insert("token_id".to_string(), json!(partial_command.token)); + + if let Some(valid_until) = partial_command.valid_until { + options.insert("valid_until".to_string(), json!(valid_until)); + } + + if let Some(memo) = partial_command.memo { + options.insert("memo".to_string(), json!(memo)); + } + + json!(options) +} + +fn validate_base58_public_key(token_id: &str) -> Result<(), MinaMeshError> { + validate_base58_with_checksum(token_id, None).map_err(|e| MinaMeshError::PublicKeyFormatNotValid(e.to_string())) +} + +#[allow(dead_code)] +#[derive(Debug)] +pub struct PartialUserCommand { + pub kind: UserCommandType, + pub fee_payer: String, + pub source: String, + pub receiver: String, + pub fee_token: String, + pub token: String, + pub fee: i64, + pub amount: Option, + pub valid_until: Option, + pub memo: Option, +} + +impl PartialUserCommand { + pub fn from_operations( + operations: &[Operation], + metadata: Option, + ) -> Result { + let mut errors = Vec::new(); + let metadata = metadata.unwrap_or_default(); + let valid_until = metadata.valid_until; + let memo = metadata.memo; + + match operations.len() { + 3 => Self::parse_payment_operations(operations, valid_until, memo).map_err(|err| { + if let MinaMeshError::OperationsNotValid(reasons) = &err { + errors.extend(reasons.clone()); + } + MinaMeshError::OperationsNotValid(errors.clone()) + }), + 2 => Self::parse_delegation_operations(operations, valid_until, memo).map_err(|err| { + if let MinaMeshError::OperationsNotValid(reasons) = &err { + errors.extend(reasons.clone()); + } + MinaMeshError::OperationsNotValid(errors.clone()) + }), + _ => { + errors.push(PartialReason::LengthMismatch(format!( + "Expected 2 operations for delegation or 3 for payment, got {}", + operations.len() + ))); + Err(MinaMeshError::OperationsNotValid(errors)) + } + } + } + + fn parse_payment_operations( + operations: &[Operation], + valid_until: Option, + memo: Option, + ) -> Result { + let mut errors = Vec::new(); + + let fee_payment = Self::find_operation(operations, FeePayment).inspect_err(|e| { + errors.push(e.clone()); + }); + + let source_dec = Self::find_operation(operations, PaymentSourceDec).inspect_err(|e| { + errors.push(e.clone()); + }); + + let receiver_inc = Self::find_operation(operations, PaymentReceiverInc).inspect_err(|e| { + errors.push(e.clone()); + }); + + if !errors.is_empty() { + return Err(MinaMeshError::OperationsNotValid(errors)); + } + + let fee_payment = fee_payment.unwrap(); + let source_dec = source_dec.unwrap(); + let receiver_inc = receiver_inc.unwrap(); + + let fee_token = Self::token_id_from_operation(fee_payment); + let token = Self::token_id_from_operation(source_dec); + + if fee_payment.account != source_dec.account { + errors.push(PartialReason::FeePayerAndSourceMismatch); + } + + //Validate source and receiver amounts match + let source_amt = Self::parse_amount_as_i64(source_dec).map_err(|e| { + errors.push(e.clone()); + MinaMeshError::OperationsNotValid(errors.clone()) + })?; + let receiver_amt = Self::parse_amount_as_i64(receiver_inc).map_err(|e| { + errors.push(e.clone()); + MinaMeshError::OperationsNotValid(errors.clone()) + })?; + if (source_amt + receiver_amt) != 0 { + errors.push(PartialReason::AmountIncDecMismatch); + } + + // Validate the fee + let fee = Self::parse_amount_as_i64(fee_payment).map_err(|e| { + errors.push(e.clone()); + MinaMeshError::OperationsNotValid(errors.clone()) + })?; + if fee >= 0 { + errors.push(PartialReason::FeeNotNegative); + } + + if !errors.is_empty() { + return Err(MinaMeshError::OperationsNotValid(errors)); + } + + Ok(PartialUserCommand { + kind: UserCommandType::Payment, + fee_payer: Self::address_from_operation(fee_payment), + source: Self::address_from_operation(source_dec), + receiver: Self::address_from_operation(receiver_inc), + fee_token, + token, + fee, + amount: Some(receiver_amt), + valid_until, + memo, + }) + } + + fn parse_delegation_operations( + operations: &[Operation], + valid_until: Option, + memo: Option, + ) -> Result { + let mut errors = Vec::new(); + + let fee_payment = Self::find_operation(operations, FeePayment).inspect_err(|e| { + errors.push(e.clone()); + }); + + let delegate_change = Self::find_operation(operations, DelegateChange).inspect_err(|e| { + errors.push(e.clone()); + }); + + if !errors.is_empty() { + return Err(MinaMeshError::OperationsNotValid(errors)); + } + + let fee_payment = fee_payment.unwrap(); + let delegate_change = delegate_change.unwrap(); + + let fee_token = Self::token_id_from_operation(fee_payment); + let token = Self::token_id_from_operation(delegate_change); + + if fee_payment.account != delegate_change.account { + errors.push(PartialReason::FeePayerAndSourceMismatch); + } + + // Validate the fee + let fee = Self::parse_amount_as_i64(fee_payment).map_err(|e| { + errors.push(e.clone()); + MinaMeshError::OperationsNotValid(errors.clone()) + })?; + if fee >= 0 { + errors.push(PartialReason::FeeNotNegative); + } + + if let Some(metadata) = &delegate_change.metadata { + // Validate the delegate_change_target is present + if metadata.get("delegate_change_target").is_none() { + errors.push(PartialReason::InvalidMetadata( + "Missing delegate_change_target in delegate_change metadata".to_string(), + )); + } + } else { + errors.push(PartialReason::InvalidMetadata( + "Missing delegate_change metadata with delegate_change_target".to_string(), + )); + } + + if !errors.is_empty() { + return Err(MinaMeshError::OperationsNotValid(errors)); + } + + Ok(PartialUserCommand { + kind: UserCommandType::Delegation, + fee_payer: Self::address_from_operation(fee_payment), + source: Self::address_from_operation(fee_payment), + receiver: Self::address_from_operation(delegate_change), + fee_token, + token, + fee, + amount: None, + valid_until, + memo, + }) + } + + fn find_operation(operations: &[Operation], op_type: OperationType) -> Result<&Operation, PartialReason> { + operations + .iter() + .find(|op| op.r#type == op_type.to_string()) + .ok_or(PartialReason::CanNotFindKind(op_type.to_string())) + } + + fn parse_amount_as_i64(operation: &Operation) -> Result { + operation + .amount + .as_ref() + .ok_or(PartialReason::AmountNotSome) + .and_then(|amount| amount.value.parse::().map_err(|_| PartialReason::AmountNotValid)) + } + + fn token_id_from_operation(operation: &Operation) -> String { + operation + .account + .as_ref() + .and_then(|account| account.metadata.as_ref()) + .and_then(|meta| meta.get("token_id").and_then(|t| t.as_str())) + .unwrap_or(DEFAULT_TOKEN_ID) + .to_string() + } + + fn address_from_operation(operation: &Operation) -> String { + if operation.r#type == DelegateChange.to_string() { + operation + .metadata + .as_ref() + .and_then(|meta| meta.get("delegate_change_target").and_then(|t| t.as_str())) + .unwrap_or_default() + .to_string() + } else { + operation.account.as_ref().map_or_else(String::new, |acc| acc.address.clone()) + } } } diff --git a/src/base58.rs b/src/base58.rs new file mode 100644 index 0000000..ce53239 --- /dev/null +++ b/src/base58.rs @@ -0,0 +1,49 @@ +use sha2::Digest; + +use crate::MinaMeshError; + +/// https://github.com/MinaProtocol/mina/blob/985eda49bdfabc046ef9001d3c406e688bc7ec45/src/lib/base58_check/base58_check.ml +/// +/// Validates a base58-encoded string with checksum and optional version byte. +/// +/// # Arguments +/// * `input` - The base58-encoded string to validate. +/// * `expected_version` - An optional expected version byte for validation. +/// +/// # Returns +/// * `Ok(())` if the input is valid. +/// * `Err(MinaMeshError)` if the input is invalid. +pub fn validate_base58_with_checksum(input: &str, expected_version: Option) -> Result<(), MinaMeshError> { + // Decode the input using base58 + let bytes = bs58::decode(input) + .with_alphabet(bs58::Alphabet::BITCOIN) + .into_vec() + .map_err(|_| MinaMeshError::MalformedPublicKey("Input not valid base58".to_string()))?; + + // Check the length (must include at least version and checksum) + if bytes.len() < 5 { + return Err(MinaMeshError::MalformedPublicKey("Input too short".to_string())); + } + + // Split into version, payload, and checksum + let (version, rest) = bytes.split_at(1); + let (payload, checksum) = rest.split_at(rest.len() - 4); + + // Validate version byte if specified + if let Some(expected) = expected_version { + if version[0] != expected { + return Err(MinaMeshError::MalformedPublicKey(format!( + "Unexpected version byte: expected {}, got {}", + expected, version[0] + ))); + } + } + + // Recompute checksum + let computed_checksum = sha2::Sha256::digest(sha2::Sha256::digest([version, payload].concat())); + if &computed_checksum[.. 4] != checksum { + return Err(MinaMeshError::MalformedPublicKey("Checksum mismatch".to_string())); + } + + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index b9571c8..2eb4961 100644 --- a/src/error.rs +++ b/src/error.rs @@ -41,7 +41,7 @@ pub enum MinaMeshError { #[error("Block not found")] BlockMissing(Option, Option), - #[error("Malformed public key")] + #[error("Malformed public key: {0}")] MalformedPublicKey(String), #[error("Cannot convert operations to valid transaction")] @@ -54,7 +54,7 @@ pub enum MinaMeshError { SignatureMissing, #[error("Invalid public key format")] - PublicKeyFormatNotValid, + PublicKeyFormatNotValid(String), #[error("No options provided")] NoOptionsProvided, @@ -95,12 +95,13 @@ pub enum MinaMeshError { #[derive(Clone, Debug, PartialEq, Serialize)] pub enum PartialReason { - LengthMismatch, + LengthMismatch(String), FeePayerAndSourceMismatch, FeeNotNegative, AmountNotSome, + AmountNotValid, AccountNotSome, - InvalidMetadata, + InvalidMetadata(String), IncorrectTokenId, AmountIncDecMismatch, StatusNotPending, @@ -123,7 +124,7 @@ impl MinaMeshError { MinaMeshError::OperationsNotValid(vec![]), MinaMeshError::UnsupportedOperationForConstruction, MinaMeshError::SignatureMissing, - MinaMeshError::PublicKeyFormatNotValid, + MinaMeshError::PublicKeyFormatNotValid("Error message".to_string()), MinaMeshError::NoOptionsProvided, MinaMeshError::Exception("Unexpected error".to_string()), MinaMeshError::SignatureInvalid, @@ -155,7 +156,7 @@ impl MinaMeshError { MinaMeshError::OperationsNotValid(_) => 11, MinaMeshError::UnsupportedOperationForConstruction => 12, MinaMeshError::SignatureMissing => 13, - MinaMeshError::PublicKeyFormatNotValid => 14, + MinaMeshError::PublicKeyFormatNotValid(_) => 14, MinaMeshError::NoOptionsProvided => 15, MinaMeshError::Exception(_) => 16, MinaMeshError::SignatureInvalid => 17, @@ -221,6 +222,13 @@ impl MinaMeshError { MinaMeshError::MalformedPublicKey(err) => json!({ "error": err, }), + MinaMeshError::PublicKeyFormatNotValid(err) => json!({ + "error": err, + }), + MinaMeshError::OperationsNotValid(reasons) => json!({ + "error": "We could not convert those operations to a valid transaction.", + "reasons": reasons, + }), MinaMeshError::BlockMissing(index, hash) => { let block_identifier = match (index, hash) { (Some(idx), Some(hsh)) => format!("index={}, hash={}", idx, hsh), @@ -275,7 +283,7 @@ impl MinaMeshError { "The operation is not supported for transaction construction.".to_string() } MinaMeshError::SignatureMissing => "Your request is missing a signature.".to_string(), - MinaMeshError::PublicKeyFormatNotValid => "The public key you provided had an invalid format.".to_string(), + MinaMeshError::PublicKeyFormatNotValid(_) => "The public key you provided had an invalid format.".to_string(), MinaMeshError::NoOptionsProvided => "Your request is missing options.".to_string(), MinaMeshError::Exception(_) => "An internal exception occurred.".to_string(), MinaMeshError::SignatureInvalid => "Your request has an invalid signature.".to_string(), @@ -317,7 +325,7 @@ impl IntoResponse for MinaMeshError { MinaMeshError::OperationsNotValid(_) => StatusCode::BAD_REQUEST, MinaMeshError::UnsupportedOperationForConstruction => StatusCode::BAD_REQUEST, MinaMeshError::SignatureMissing => StatusCode::BAD_REQUEST, - MinaMeshError::PublicKeyFormatNotValid => StatusCode::BAD_REQUEST, + MinaMeshError::PublicKeyFormatNotValid(_) => StatusCode::BAD_REQUEST, MinaMeshError::NoOptionsProvided => StatusCode::BAD_REQUEST, MinaMeshError::Exception(_) => StatusCode::INTERNAL_SERVER_ERROR, MinaMeshError::SignatureInvalid => StatusCode::BAD_REQUEST, diff --git a/src/lib.rs b/src/lib.rs index 407143c..e03cd84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ mod api; +pub mod base58; mod commands; mod config; mod create_router; diff --git a/src/types.rs b/src/types.rs index 9c8f606..14b0a17 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,9 +1,11 @@ -use convert_case::{Case, Casing}; use derive_more::derive::Display; -use serde::Serialize; +use serde::{Deserialize, Serialize}; +use serde_json::Value; use sqlx::{FromRow, Type}; use strum::IntoEnumIterator; -use strum_macros::EnumIter; +use strum_macros::{Display as StrumDisplay, EnumIter, EnumString}; + +use crate::MinaMeshError; #[derive(Type, Debug, PartialEq, Eq, Serialize)] #[sqlx(type_name = "chain_status_type", rename_all = "lowercase")] @@ -50,7 +52,8 @@ impl From for OperationStatus { } } -#[derive(Debug, Display, EnumIter)] +#[derive(Debug, StrumDisplay, EnumIter, EnumString)] +#[strum(serialize_all = "snake_case")] pub enum OperationType { FeePayerDec, FeeReceiverInc, @@ -66,7 +69,7 @@ pub enum OperationType { } pub fn operation_types() -> Vec { - OperationType::iter().map(|variant| format!("{:?}", variant).to_case(Case::Snake)).collect() + OperationType::iter().map(|variant| variant.to_string()).collect() } #[derive(Type, Debug, PartialEq, Eq, Serialize, Display)] @@ -382,3 +385,29 @@ impl InternalCommandOperationsData for InternalCommandMetadata { pub enum CacheKey { NetworkId, } + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct PreprocessMetadata { + pub valid_until: Option, + pub memo: Option, +} + +impl PreprocessMetadata { + pub fn from_json(metadata: Option) -> Result, MinaMeshError> { + if let Some(meta) = metadata { + serde_json::from_value(meta) + .map(Some) + .map_err(|e| MinaMeshError::JsonParse(Some(format!("Failed to parse metadata: {}", e)))) + } else { + Ok(None) + } + } + + pub fn to_json(&self) -> Value { + serde_json::to_value(self).unwrap_or_default() + } + + pub fn new(valid_until: Option, memo: Option) -> Self { + Self { valid_until, memo } + } +} diff --git a/tests/compare_to_ocaml.rs b/tests/compare_to_ocaml.rs index 9d40f4d..071bde4 100644 --- a/tests/compare_to_ocaml.rs +++ b/tests/compare_to_ocaml.rs @@ -98,3 +98,9 @@ async fn construction_derive() -> Result<()> { let (subpath, reqs) = fixtures::construction_derive(); assert_responses_eq(subpath, &reqs).await } + +#[tokio::test] +async fn construction_preprocess() -> Result<()> { + let (subpath, reqs) = fixtures::construction_preprocess(); + assert_responses_eq(subpath, &reqs).await +} diff --git a/tests/construction_preprocess.rs b/tests/construction_preprocess.rs new file mode 100644 index 0000000..c5a86d2 --- /dev/null +++ b/tests/construction_preprocess.rs @@ -0,0 +1,323 @@ +use anyhow::Result; +use insta::assert_debug_snapshot; +use mina_mesh::{ + models::{AccountIdentifier, Amount, ConstructionPreprocessRequest, Currency, Operation, OperationIdentifier}, + test::network_id, + MinaMeshConfig, + OperationType::*, + PreprocessMetadata, +}; +use serde_json::json; + +#[tokio::test] +async fn construction_preprocess_empty() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let request = ConstructionPreprocessRequest::new(network_id(), vec![]); + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_err()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_payment() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = payment_operations( + // cspell:disable + ("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk", "-1010"), + ("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk", "50000"), + ("B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDiv", "-50000"), + // cspell:enable + ); + let request = ConstructionPreprocessRequest::new(network_id(), operations); + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_ok()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_payment_with_metadata() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = payment_operations( + // cspell:disable + ("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk", "-1010"), + ("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk", "50000"), + ("B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDiv", "-50000"), + // cspell:enable + ); + let metadata = PreprocessMetadata::new(Some("20000".into()), Some("hello".into())); + let request = ConstructionPreprocessRequest { + network_identifier: network_id().into(), + operations, + metadata: Some(metadata.to_json()), + }; + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_ok()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_payment_fee_not_negative() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = payment_operations( + // cspell:disable + ("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk", "1010"), + ("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk", "50000"), + ("B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDiv", "-50000"), + // cspell:enable + ); + let request = ConstructionPreprocessRequest::new(network_id(), operations); + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_err()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_payment_dec_inc_mismatch() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = payment_operations( + // cspell:disable + ("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk", "-1010"), + ("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk", "50000"), + ("B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDiv", "50000"), + // cspell:enable + ); + let request = ConstructionPreprocessRequest::new(network_id(), operations); + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_err()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_payment_invalid_pk() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = payment_operations( + // cspell:disable + ("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk", "-1010"), + ("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk", "50000"), + ("B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDivkk", "-50000"), + // cspell:enable + ); + let request = ConstructionPreprocessRequest::new(network_id(), operations); + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_err()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_payment_fee_payer_mismatch() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = payment_operations( + // cspell:disable + ("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk", "-1010"), + ("B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDiv", "50000"), + ("B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDiv", "-50000"), + // cspell:enable + ); + let request = ConstructionPreprocessRequest::new(network_id(), operations); + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_err()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_delegation() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = delegation_operations( + // cspell:disable + "B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvB", + "-10100000", + "B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvB", + "B62qiburnzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzmp7r7UN6X", + // cspell:enable + ); + let request = ConstructionPreprocessRequest::new(network_id(), operations); + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_ok()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_delegation_with_metadata() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = delegation_operations( + // cspell:disable + "B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvB", + "-10100000", + "B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvB", + "B62qiburnzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzmp7r7UN6X", + // cspell:enable + ); + let metadata = PreprocessMetadata::new(Some("20000".into()), Some("hello".into())); + let request = ConstructionPreprocessRequest { + network_identifier: network_id().into(), + operations, + metadata: Some(metadata.to_json()), + }; + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_ok()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_delegation_fee_not_negative() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = delegation_operations( + // cspell:disable + "B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvB", + "10100000", + "B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvB", + "B62qiburnzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzmp7r7UN6X", + // cspell:enable + ); + let request = ConstructionPreprocessRequest::new(network_id(), operations); + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_err()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_delegation_fee_amt_invalid() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = delegation_operations( + // cspell:disable + "B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvB", + "xxxx", + "B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvB", + "B62qiburnzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzmp7r7UN6X", + // cspell:enable + ); + let request = ConstructionPreprocessRequest::new(network_id(), operations); + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_err()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_delegation_fee_payer_mismatch() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = delegation_operations( + // cspell:disable + "B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvB", + "-1", + "B62qiburnzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzmp7r7UN6X", + "B62qiburnzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzmp7r7UN6X", + // cspell:enable + ); + let request = ConstructionPreprocessRequest::new(network_id(), operations); + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_err()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn construction_preprocess_delegation_invalid_pk() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let operations = delegation_operations( + // cspell:disable + "B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvBx", + "-1", + "B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvBx", + "B62qiburnzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzmp7r7UN6X", + // cspell:enable + ); + let request = ConstructionPreprocessRequest::new(network_id(), operations); + let response = mina_mesh.construction_preprocess(request).await; + assert!(response.is_err()); + assert_debug_snapshot!(response); + Ok(()) +} + +fn payment_operations( + (fee_act, fee_amt): (&str, &str), + (sender_act, sender_amt): (&str, &str), + (receiver_act, receiver_amt): (&str, &str), +) -> Vec { + vec![ + Operation { + operation_identifier: OperationIdentifier::new(0).into(), + related_operations: None, + r#type: FeePayment.to_string(), + account: Some( + AccountIdentifier { address: fee_act.into(), sub_account: None, metadata: json!({ "token_id": "1" }).into() } + .into(), + ), + amount: Some(Box::new(Amount::new(fee_amt.into(), Currency::new("MINA".into(), 9)))), + coin_change: None, + metadata: None, + status: None, + }, + Operation { + operation_identifier: OperationIdentifier::new(1).into(), + related_operations: None, + r#type: PaymentSourceDec.to_string(), + account: Some( + AccountIdentifier { + address: sender_act.into(), + sub_account: None, + metadata: json!({ "token_id": "1" }).into(), + } + .into(), + ), + amount: Some(Box::new(Amount::new(sender_amt.into(), Currency::new("MINA".into(), 9)))), + coin_change: None, + metadata: None, + status: None, + }, + Operation { + operation_identifier: OperationIdentifier::new(2).into(), + related_operations: vec![OperationIdentifier::new(1)].into(), + r#type: PaymentReceiverInc.to_string(), + account: Some( + AccountIdentifier { + address: receiver_act.into(), + sub_account: None, + metadata: json!({ "token_id": "1" }).into(), + } + .into(), + ), + amount: Some(Box::new(Amount::new(receiver_amt.into(), Currency::new("MINA".into(), 9)))), + coin_change: None, + status: None, + metadata: None, + }, + ] +} + +fn delegation_operations(fee_act: &str, fee_amt: &str, source_act: &str, delegate_target_act: &str) -> Vec { + vec![ + Operation { + operation_identifier: OperationIdentifier::new(0).into(), + related_operations: None, + r#type: FeePayment.to_string(), + account: Some(AccountIdentifier::new(fee_act.into()).into()), + amount: Some(Box::new(Amount::new(fee_amt.into(), Currency::new("MINA".into(), 9)))), + coin_change: None, + metadata: None, + status: None, + }, + Operation { + operation_identifier: OperationIdentifier::new(1).into(), + related_operations: None, + r#type: DelegateChange.to_string(), + account: Some(AccountIdentifier::new(source_act.into()).into()), + amount: None, + coin_change: None, + metadata: Some(json!({ + "delegate_change_target": delegate_target_act + })), + status: None, + }, + ] +} diff --git a/tests/error.rs b/tests/error.rs index 4bb0b1a..03710e7 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -93,7 +93,13 @@ async fn test_error_properties() { StatusCode::BAD_REQUEST, ), (SignatureMissing, 13, "Your request is missing a signature.", false, StatusCode::BAD_REQUEST), - (PublicKeyFormatNotValid, 14, "The public key you provided had an invalid format.", false, StatusCode::BAD_REQUEST), + ( + PublicKeyFormatNotValid("error message".to_string()), + 14, + "The public key you provided had an invalid format.", + false, + StatusCode::BAD_REQUEST, + ), (NoOptionsProvided, 15, "Your request is missing options.", false, StatusCode::BAD_REQUEST), ( Exception("Unexpected error".to_string()), diff --git a/tests/fixtures/construction_preprocess.rs b/tests/fixtures/construction_preprocess.rs new file mode 100644 index 0000000..c8d6b72 --- /dev/null +++ b/tests/fixtures/construction_preprocess.rs @@ -0,0 +1,113 @@ +use mina_mesh::{ + models::{AccountIdentifier, Amount, ConstructionPreprocessRequest, Currency, Operation, OperationIdentifier}, + test::network_id, + OperationType::*, + PreprocessMetadata, +}; +use serde_json::json; + +use super::CompareGroup; + +pub fn construction_preprocess<'a>() -> CompareGroup<'a> { + let payment_operations = vec![ + Operation { + operation_identifier: OperationIdentifier::new(0).into(), + related_operations: None, + r#type: FeePayment.to_string(), + account: Some( + AccountIdentifier { + // cspell:disable-next-line + address: "B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk".into(), + sub_account: None, + metadata: json!({ "token_id": "1" }).into(), + } + .into(), + ), + amount: Some(Box::new(Amount::new("-100000".into(), Currency::new("MINA".into(), 9)))), + coin_change: None, + metadata: None, + status: None, + }, + Operation { + operation_identifier: OperationIdentifier::new(1).into(), + related_operations: None, + r#type: PaymentSourceDec.to_string(), + account: Some( + AccountIdentifier { + // cspell:disable-next-line + address: "B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk".into(), + sub_account: None, + metadata: json!({ "token_id": "1" }).into(), + } + .into(), + ), + amount: Some(Box::new(Amount::new("-9000000".into(), Currency::new("MINA".into(), 9)))), + coin_change: None, + metadata: None, + status: None, + }, + Operation { + operation_identifier: OperationIdentifier::new(2).into(), + related_operations: vec![OperationIdentifier::new(1)].into(), + r#type: PaymentReceiverInc.to_string(), + account: Some( + AccountIdentifier { + // cspell:disable-next-line + address: "B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDiv".into(), + sub_account: None, + metadata: json!({ "token_id": "1" }).into(), + } + .into(), + ), + amount: Some(Box::new(Amount::new("9000000".into(), Currency::new("MINA".into(), 9)))), + coin_change: None, + status: None, + metadata: None, + }, + ]; + + let delegation_operations = vec![ + Operation { + operation_identifier: OperationIdentifier::new(0).into(), + related_operations: None, + r#type: FeePayment.to_string(), + // cspell:disable-next-line + account: Some(AccountIdentifier::new("B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDiv".into()).into()), + amount: Some(Box::new(Amount::new("-500".into(), Currency::new("MINA".into(), 9)))), + coin_change: None, + metadata: None, + status: None, + }, + Operation { + operation_identifier: OperationIdentifier::new(1).into(), + related_operations: None, + r#type: DelegateChange.to_string(), + // cspell:disable-next-line + account: Some(AccountIdentifier::new("B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDiv".into()).into()), + amount: None, + coin_change: None, + metadata: Some(json!({ + // cspell:disable-next-line + "delegate_change_target": "B62qiburnzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzmp7r7UN6X" + })), + status: None, + }, + ]; + + let metadata = PreprocessMetadata::new(Some("70000".into()), Some("test memo OK".into())); + + ("/construction/preprocess", vec![ + Box::new(ConstructionPreprocessRequest::new(network_id(), payment_operations.clone())), + Box::new(ConstructionPreprocessRequest::new(network_id(), delegation_operations.clone())), + Box::new(ConstructionPreprocessRequest { + network_identifier: network_id().into(), + operations: payment_operations, + metadata: Some(metadata.to_json()), + }), + Box::new(ConstructionPreprocessRequest { + network_identifier: network_id().into(), + operations: delegation_operations, + metadata: Some(metadata.to_json()), + }), + ]) +} diff --git a/tests/fixtures/mod.rs b/tests/fixtures/mod.rs index f475157..abd663e 100644 --- a/tests/fixtures/mod.rs +++ b/tests/fixtures/mod.rs @@ -3,6 +3,7 @@ use erased_serde::Serialize as ErasedSerialize; mod account_balance; mod block; mod construction_derive; +mod construction_preprocess; mod mempool; mod network; mod search_transactions; @@ -10,6 +11,7 @@ mod search_transactions; pub use account_balance::*; pub use block::*; pub use construction_derive::*; +pub use construction_preprocess::*; pub use mempool::*; pub use network::*; pub use search_transactions::*; diff --git a/tests/snapshots/construction_derive__construction_derive_token_id_fail.snap b/tests/snapshots/construction_derive__construction_derive_token_id_fail.snap index 829f286..6320662 100644 --- a/tests/snapshots/construction_derive__construction_derive_token_id_fail.snap +++ b/tests/snapshots/construction_derive__construction_derive_token_id_fail.snap @@ -4,6 +4,6 @@ expression: response --- Err( MalformedPublicKey( - "Token_id too short", + "Input too short", ), ) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_delegation.snap b/tests/snapshots/construction_preprocess__construction_preprocess_delegation.snap new file mode 100644 index 0000000..52857bd --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_delegation.snap @@ -0,0 +1,18 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Ok( + ConstructionPreprocessResponse { + options: Some( + Object { + "receiver": String("B62qiburnzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzmp7r7UN6X"), + "sender": String("B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvB"), + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + required_public_keys: Some( + [], + ), + }, +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_delegation_fee_amt_invalid.snap b/tests/snapshots/construction_preprocess__construction_preprocess_delegation_fee_amt_invalid.snap new file mode 100644 index 0000000..722e214 --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_delegation_fee_amt_invalid.snap @@ -0,0 +1,11 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Err( + OperationsNotValid( + [ + AmountNotValid, + ], + ), +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_delegation_fee_not_negative.snap b/tests/snapshots/construction_preprocess__construction_preprocess_delegation_fee_not_negative.snap new file mode 100644 index 0000000..c5318db --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_delegation_fee_not_negative.snap @@ -0,0 +1,11 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Err( + OperationsNotValid( + [ + FeeNotNegative, + ], + ), +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_delegation_fee_payer_mismatch.snap b/tests/snapshots/construction_preprocess__construction_preprocess_delegation_fee_payer_mismatch.snap new file mode 100644 index 0000000..3226c05 --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_delegation_fee_payer_mismatch.snap @@ -0,0 +1,11 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Err( + OperationsNotValid( + [ + FeePayerAndSourceMismatch, + ], + ), +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_delegation_invalid_pk.snap b/tests/snapshots/construction_preprocess__construction_preprocess_delegation_invalid_pk.snap new file mode 100644 index 0000000..351b6ab --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_delegation_invalid_pk.snap @@ -0,0 +1,9 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Err( + PublicKeyFormatNotValid( + "Malformed public key: Checksum mismatch", + ), +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_delegation_with_metadata.snap b/tests/snapshots/construction_preprocess__construction_preprocess_delegation_with_metadata.snap new file mode 100644 index 0000000..529d8b6 --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_delegation_with_metadata.snap @@ -0,0 +1,20 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Ok( + ConstructionPreprocessResponse { + options: Some( + Object { + "memo": String("hello"), + "receiver": String("B62qiburnzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzmp7r7UN6X"), + "sender": String("B62qkXajxfnicuCNtaurdAhQpkFsqjoyPJuw53aeJP848bsa3Ne3RvB"), + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + "valid_until": String("20000"), + }, + ), + required_public_keys: Some( + [], + ), + }, +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_empty.snap b/tests/snapshots/construction_preprocess__construction_preprocess_empty.snap new file mode 100644 index 0000000..7f0ef37 --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_empty.snap @@ -0,0 +1,13 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Err( + OperationsNotValid( + [ + LengthMismatch( + "Expected 2 operations for delegation or 3 for payment, got 0", + ), + ], + ), +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_payment.snap b/tests/snapshots/construction_preprocess__construction_preprocess_payment.snap new file mode 100644 index 0000000..c9ec1d3 --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_payment.snap @@ -0,0 +1,18 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Ok( + ConstructionPreprocessResponse { + options: Some( + Object { + "receiver": String("B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDiv"), + "sender": String("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk"), + "token_id": String("1"), + }, + ), + required_public_keys: Some( + [], + ), + }, +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_payment_dec_inc_mismatch.snap b/tests/snapshots/construction_preprocess__construction_preprocess_payment_dec_inc_mismatch.snap new file mode 100644 index 0000000..bb2db48 --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_payment_dec_inc_mismatch.snap @@ -0,0 +1,11 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Err( + OperationsNotValid( + [ + AmountIncDecMismatch, + ], + ), +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_payment_fee_not_negative.snap b/tests/snapshots/construction_preprocess__construction_preprocess_payment_fee_not_negative.snap new file mode 100644 index 0000000..c5318db --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_payment_fee_not_negative.snap @@ -0,0 +1,11 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Err( + OperationsNotValid( + [ + FeeNotNegative, + ], + ), +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_payment_fee_payer_mismatch.snap b/tests/snapshots/construction_preprocess__construction_preprocess_payment_fee_payer_mismatch.snap new file mode 100644 index 0000000..3226c05 --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_payment_fee_payer_mismatch.snap @@ -0,0 +1,11 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Err( + OperationsNotValid( + [ + FeePayerAndSourceMismatch, + ], + ), +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_payment_invalid_pk.snap b/tests/snapshots/construction_preprocess__construction_preprocess_payment_invalid_pk.snap new file mode 100644 index 0000000..351b6ab --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_payment_invalid_pk.snap @@ -0,0 +1,9 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Err( + PublicKeyFormatNotValid( + "Malformed public key: Checksum mismatch", + ), +) diff --git a/tests/snapshots/construction_preprocess__construction_preprocess_payment_with_metadata.snap b/tests/snapshots/construction_preprocess__construction_preprocess_payment_with_metadata.snap new file mode 100644 index 0000000..df5728f --- /dev/null +++ b/tests/snapshots/construction_preprocess__construction_preprocess_payment_with_metadata.snap @@ -0,0 +1,20 @@ +--- +source: tests/construction_preprocess.rs +expression: response +--- +Ok( + ConstructionPreprocessResponse { + options: Some( + Object { + "memo": String("hello"), + "receiver": String("B62qoDWfBZUxKpaoQCoFqr12wkaY84FrhxXNXzgBkMUi2Tz4K8kBDiv"), + "sender": String("B62qkUHaJUHERZuCHQhXCQ8xsGBqyYSgjQsKnKN5HhSJecakuJ4pYyk"), + "token_id": String("1"), + "valid_until": String("20000"), + }, + ), + required_public_keys: Some( + [], + ), + }, +) diff --git a/tests/snapshots/network_options__network_options.snap b/tests/snapshots/network_options__network_options.snap index da55699..dddd0c1 100644 --- a/tests/snapshots/network_options__network_options.snap +++ b/tests/snapshots/network_options__network_options.snap @@ -151,7 +151,7 @@ Allow { }, Error { code: 10, - message: "Malformed public key", + message: "Malformed public key: Error message", description: Some( "The provided public key is malformed.", ), @@ -170,7 +170,10 @@ Allow { ), retriable: false, details: Some( - String(""), + Object { + "error": String("We could not convert those operations to a valid transaction."), + "reasons": Array [], + }, ), }, Error { @@ -203,7 +206,9 @@ Allow { ), retriable: false, details: Some( - String(""), + Object { + "error": String("Error message"), + }, ), }, Error {