use serde::{Deserialize, Serialize};
use thiserror::Error;
use bsv::wallet::interfaces::{ReviewActionResult, SendWithResult};
use bsv::wallet::types::OutpointString;
pub type WalletResult<T> = Result<T, WalletError>;
#[derive(Debug, Error)]
pub enum WalletError {
#[error("WERR_INTERNAL: {0}")]
Internal(String),
#[error("WERR_INVALID_PARAMETER: The {parameter} parameter must be {must_be}")]
InvalidParameter {
parameter: String,
must_be: String,
},
#[error("WERR_NOT_IMPLEMENTED: {0}")]
NotImplemented(String),
#[error("WERR_BAD_REQUEST: {0}")]
BadRequest(String),
#[error("WERR_UNAUTHORIZED: {0}")]
Unauthorized(String),
#[error("WERR_NOT_ACTIVE: {0}")]
NotActive(String),
#[error("WERR_INVALID_OPERATION: {0}")]
InvalidOperation(String),
#[error("WERR_MISSING_PARAMETER: The required {0} parameter is missing.")]
MissingParameter(String),
#[error("WERR_INSUFFICIENT_FUNDS: {message}")]
InsufficientFunds {
message: String,
total_satoshis_needed: i64,
more_satoshis_needed: i64,
},
#[error("WERR_BROADCAST_UNAVAILABLE: Unable to broadcast transaction at this time.")]
BroadcastUnavailable,
#[error("WERR_NETWORK_CHAIN: {0}")]
NetworkChain(String),
#[error("WERR_INVALID_PUBLIC_KEY: {message}")]
InvalidPublicKey {
message: String,
key: String,
},
#[error("WERR_REVIEW_ACTIONS: {message}")]
ReviewActions {
message: String,
review_action_results: Vec<ReviewActionResult>,
send_with_results: Vec<SendWithResult>,
txid: Option<String>,
tx: Option<Vec<u8>>,
no_send_change: Vec<OutpointString>,
},
#[error("WERR_INVALID_MERKLE_ROOT: {message}")]
InvalidMerkleRoot {
message: String,
block_hash: String,
block_height: u32,
merkle_root: String,
txid: Option<String>,
},
#[error("WERR_INTERNAL: {0}")]
Sqlx(#[from] sqlx::Error),
#[error("WERR_INTERNAL: {0}")]
Io(#[from] std::io::Error),
#[error("WERR_INTERNAL: {0}")]
SerdeJson(#[from] serde_json::Error),
}
impl WalletError {
pub fn code(&self) -> &'static str {
match self {
Self::Internal(_) | Self::Sqlx(_) | Self::Io(_) | Self::SerdeJson(_) => "WERR_INTERNAL",
Self::InvalidParameter { .. } => "WERR_INVALID_PARAMETER",
Self::NotImplemented(_) => "WERR_NOT_IMPLEMENTED",
Self::BadRequest(_) => "WERR_BAD_REQUEST",
Self::Unauthorized(_) => "WERR_UNAUTHORIZED",
Self::NotActive(_) => "WERR_NOT_ACTIVE",
Self::InvalidOperation(_) => "WERR_INVALID_OPERATION",
Self::MissingParameter(_) => "WERR_MISSING_PARAMETER",
Self::InsufficientFunds { .. } => "WERR_INSUFFICIENT_FUNDS",
Self::BroadcastUnavailable => "WERR_BROADCAST_UNAVAILABLE",
Self::NetworkChain(_) => "WERR_NETWORK_CHAIN",
Self::InvalidPublicKey { .. } => "WERR_INVALID_PUBLIC_KEY",
Self::ReviewActions { .. } => "WERR_REVIEW_ACTIONS",
Self::InvalidMerkleRoot { .. } => "WERR_INVALID_MERKLE_ROOT",
}
}
pub fn to_wallet_error_object(&self) -> WalletErrorObject {
let mut obj = WalletErrorObject {
is_error: true,
name: self.code().to_string(),
message: self.to_string(),
code: None,
parameter: None,
total_satoshis_needed: None,
more_satoshis_needed: None,
review_action_results: None,
send_with_results: None,
txid: None,
tx: None,
no_send_change: None,
block_hash: None,
block_height: None,
merkle_root: None,
key: None,
};
match self {
Self::InvalidParameter { parameter, .. } => {
obj.parameter = Some(parameter.clone());
}
Self::MissingParameter(p) => {
obj.parameter = Some(p.clone());
}
Self::InsufficientFunds {
total_satoshis_needed,
more_satoshis_needed,
..
} => {
obj.total_satoshis_needed = Some(*total_satoshis_needed);
obj.more_satoshis_needed = Some(*more_satoshis_needed);
}
Self::InvalidPublicKey { key, .. } => {
obj.key = Some(key.clone());
}
Self::ReviewActions {
review_action_results,
send_with_results,
txid,
tx,
no_send_change,
..
} => {
obj.code = Some(5);
obj.review_action_results = Some(review_action_results.clone());
obj.send_with_results = Some(send_with_results.clone());
obj.txid = txid.clone();
obj.tx = tx.clone();
obj.no_send_change = Some(no_send_change.clone());
}
Self::InvalidMerkleRoot {
block_hash,
block_height,
merkle_root,
txid,
..
} => {
obj.code = Some(8);
obj.block_hash = Some(block_hash.clone());
obj.block_height = Some(*block_height);
obj.merkle_root = Some(merkle_root.clone());
obj.txid = txid.clone();
}
_ => {}
}
obj
}
}
pub fn wallet_error_from_object(obj: WalletErrorObject) -> WalletError {
match obj.name.as_str() {
"WERR_INVALID_PARAMETER" => WalletError::InvalidParameter {
parameter: obj.parameter.unwrap_or_default(),
must_be: obj.message,
},
"WERR_NOT_IMPLEMENTED" => WalletError::NotImplemented(obj.message),
"WERR_BAD_REQUEST" => WalletError::BadRequest(obj.message),
"WERR_UNAUTHORIZED" => WalletError::Unauthorized(obj.message),
"WERR_NOT_ACTIVE" => WalletError::NotActive(obj.message),
"WERR_INVALID_OPERATION" => WalletError::InvalidOperation(obj.message),
"WERR_MISSING_PARAMETER" => {
WalletError::MissingParameter(obj.parameter.unwrap_or_else(|| obj.message.clone()))
}
"WERR_INSUFFICIENT_FUNDS" => WalletError::InsufficientFunds {
message: obj.message,
total_satoshis_needed: obj.total_satoshis_needed.unwrap_or(0),
more_satoshis_needed: obj.more_satoshis_needed.unwrap_or(0),
},
"WERR_BROADCAST_UNAVAILABLE" => WalletError::BroadcastUnavailable,
"WERR_NETWORK_CHAIN" => WalletError::NetworkChain(obj.message),
"WERR_INVALID_PUBLIC_KEY" => WalletError::InvalidPublicKey {
message: obj.message,
key: obj.key.or(obj.parameter).unwrap_or_default(),
},
"WERR_REVIEW_ACTIONS" => WalletError::ReviewActions {
message: obj.message,
review_action_results: obj.review_action_results.unwrap_or_default(),
send_with_results: obj.send_with_results.unwrap_or_default(),
txid: obj.txid,
tx: obj.tx,
no_send_change: obj.no_send_change.unwrap_or_default(),
},
"WERR_INVALID_MERKLE_ROOT" => WalletError::InvalidMerkleRoot {
message: obj.message,
block_hash: obj.block_hash.unwrap_or_default(),
block_height: obj.block_height.unwrap_or(0),
merkle_root: obj.merkle_root.unwrap_or_default(),
txid: obj.txid,
},
_ => WalletError::Internal(obj.message),
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletErrorObject {
pub is_error: bool,
pub name: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_satoshis_needed: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub more_satoshis_needed: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub review_action_results: Option<Vec<ReviewActionResult>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub send_with_results: Option<Vec<SendWithResult>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub txid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tx: Option<Vec<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_send_change: Option<Vec<OutpointString>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub block_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub block_height: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub merkle_root: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use bsv::wallet::interfaces::{
ActionResultStatus, ReviewActionResult, ReviewActionResultStatus, SendWithResult,
};
#[test]
fn test_review_actions_error_roundtrip() {
let err = WalletError::ReviewActions {
message: "Undelayed results require review.".to_string(),
review_action_results: vec![ReviewActionResult {
txid: "aabb".to_string(),
status: ReviewActionResultStatus::DoubleSpend,
competing_txs: Some(vec!["ccdd".to_string()]),
competing_beef: None,
}],
send_with_results: vec![SendWithResult {
txid: "eeff".to_string(),
status: ActionResultStatus::Sending,
}],
txid: Some("aabb".to_string()),
tx: None,
no_send_change: vec![],
};
assert_eq!(err.code(), "WERR_REVIEW_ACTIONS");
let obj = err.to_wallet_error_object();
assert_eq!(obj.code, Some(5));
assert!(obj.review_action_results.is_some());
let err2 = wallet_error_from_object(obj);
assert_eq!(err2.code(), "WERR_REVIEW_ACTIONS");
}
#[test]
fn test_invalid_merkle_root_error_roundtrip() {
let err = WalletError::InvalidMerkleRoot {
message: "Invalid merkleRoot abc for block def at height 100".to_string(),
block_hash: "def".to_string(),
block_height: 100,
merkle_root: "abc".to_string(),
txid: Some("1234".to_string()),
};
assert_eq!(err.code(), "WERR_INVALID_MERKLE_ROOT");
let obj = err.to_wallet_error_object();
assert_eq!(obj.code, Some(8));
assert_eq!(obj.block_hash.as_deref(), Some("def"));
let err2 = wallet_error_from_object(obj);
assert_eq!(err2.code(), "WERR_INVALID_MERKLE_ROOT");
}
#[test]
fn test_invalid_public_key_key_field_roundtrip() {
let err = WalletError::InvalidPublicKey {
message: "not on curve".to_string(),
key: "02abcdef".to_string(),
};
let obj = err.to_wallet_error_object();
assert_eq!(obj.key.as_deref(), Some("02abcdef"));
let json = serde_json::to_string(&obj).unwrap();
assert!(json.contains("\"key\":\"02abcdef\""));
let obj2: WalletErrorObject = serde_json::from_str(&json).unwrap();
let err2 = wallet_error_from_object(obj2);
match err2 {
WalletError::InvalidPublicKey { key, .. } => assert_eq!(key, "02abcdef"),
_ => panic!("Expected InvalidPublicKey"),
}
}
}