Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Construction preprocess #95

Merged
merged 12 commits into from
Jan 20, 2025
26 changes: 2 additions & 24 deletions src/api/construction_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -88,27 +87,6 @@ fn decode_token_id(metadata: Option<Value>) -> Result<String, MinaMeshError> {
}

/// 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)
}
279 changes: 274 additions & 5 deletions src/api/construction_preprocess.rs
Original file line number Diff line number Diff line change
@@ -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<ConstructionPreprocessResponse> {
) -> Result<ConstructionPreprocessResponse, MinaMeshError> {
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<i64>,
pub valid_until: Option<String>,
pub memo: Option<String>,
}

impl PartialUserCommand {
pub fn from_operations(
operations: &[Operation],
metadata: Option<PreprocessMetadata>,
) -> Result<Self, MinaMeshError> {
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<String>,
memo: Option<String>,
) -> Result<Self, MinaMeshError> {
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<String>,
memo: Option<String>,
) -> Result<Self, MinaMeshError> {
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<i64, PartialReason> {
operation
.amount
.as_ref()
.ok_or(PartialReason::AmountNotSome)
.and_then(|amount| amount.value.parse::<i64>().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())
}
}
}
49 changes: 49 additions & 0 deletions src/base58.rs
Original file line number Diff line number Diff line change
@@ -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<u8>) -> 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(())
}
Loading
Loading