use std::collections::HashMap;
use std::sync::Arc;
pub type ThreadId = String;
pub type RemittanceOptionId = String;
pub type UnixMillis = u64;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
pub enum RemittanceThreadState {
#[cfg_attr(feature = "network", serde(rename = "new"))]
New,
#[cfg_attr(feature = "network", serde(rename = "identityRequested"))]
IdentityRequested,
#[cfg_attr(feature = "network", serde(rename = "identityResponded"))]
IdentityResponded,
#[cfg_attr(feature = "network", serde(rename = "identityAcknowledged"))]
IdentityAcknowledged,
#[cfg_attr(feature = "network", serde(rename = "invoiced"))]
Invoiced,
#[cfg_attr(feature = "network", serde(rename = "settled"))]
Settled,
#[cfg_attr(feature = "network", serde(rename = "receipted"))]
Receipted,
#[cfg_attr(feature = "network", serde(rename = "terminated"))]
Terminated,
#[cfg_attr(feature = "network", serde(rename = "errored"))]
Errored,
}
impl std::fmt::Display for RemittanceThreadState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::New => "new",
Self::IdentityRequested => "identityRequested",
Self::IdentityResponded => "identityResponded",
Self::IdentityAcknowledged => "identityAcknowledged",
Self::Invoiced => "invoiced",
Self::Settled => "settled",
Self::Receipted => "receipted",
Self::Terminated => "terminated",
Self::Errored => "errored",
};
f.write_str(s)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
pub enum RemittanceKind {
#[cfg_attr(feature = "network", serde(rename = "invoice"))]
Invoice,
#[cfg_attr(feature = "network", serde(rename = "identityVerificationRequest"))]
IdentityVerificationRequest,
#[cfg_attr(feature = "network", serde(rename = "identityVerificationResponse"))]
IdentityVerificationResponse,
#[cfg_attr(
feature = "network",
serde(rename = "identityVerificationAcknowledgment")
)]
IdentityVerificationAcknowledgment,
#[cfg_attr(feature = "network", serde(rename = "settlement"))]
Settlement,
#[cfg_attr(feature = "network", serde(rename = "receipt"))]
Receipt,
#[cfg_attr(feature = "network", serde(rename = "termination"))]
Termination,
}
impl std::fmt::Display for RemittanceKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Invoice => "invoice",
Self::IdentityVerificationRequest => "identityVerificationRequest",
Self::IdentityVerificationResponse => "identityVerificationResponse",
Self::IdentityVerificationAcknowledgment => "identityVerificationAcknowledgment",
Self::Settlement => "settlement",
Self::Receipt => "receipt",
Self::Termination => "termination",
};
f.write_str(s)
}
}
pub fn allowed_transitions(state: &RemittanceThreadState) -> &'static [RemittanceThreadState] {
use RemittanceThreadState::*;
match state {
New => &[IdentityRequested, Invoiced, Settled, Terminated, Errored],
IdentityRequested => &[
IdentityResponded,
IdentityAcknowledged,
Invoiced,
Settled,
Terminated,
Errored,
],
IdentityResponded => &[IdentityAcknowledged, Invoiced, Settled, Terminated, Errored],
IdentityAcknowledged => &[Invoiced, Settled, Terminated, Errored],
Invoiced => &[
IdentityRequested,
IdentityResponded,
IdentityAcknowledged,
Settled,
Terminated,
Errored,
],
Settled => &[Receipted, Terminated, Errored],
Receipted => &[Terminated, Errored],
Terminated => &[Errored],
Errored => &[],
}
}
pub fn is_valid_transition(from: &RemittanceThreadState, to: &RemittanceThreadState) -> bool {
allowed_transitions(from).contains(to)
}
pub trait LoggerLike: Send + Sync {
fn log(&self, args: &[&dyn std::fmt::Debug]);
fn warn(&self, args: &[&dyn std::fmt::Debug]);
fn error(&self, args: &[&dyn std::fmt::Debug]);
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct Unit {
pub namespace: String,
pub code: String,
#[cfg_attr(feature = "network", serde(skip_serializing_if = "Option::is_none"))]
pub decimals: Option<u32>,
}
pub fn sat_unit() -> Unit {
Unit {
namespace: "bsv".into(),
code: "sat".into(),
decimals: Some(0),
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct Amount {
pub value: String,
pub unit: Unit,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct LineItem {
#[cfg_attr(feature = "network", serde(skip_serializing_if = "Option::is_none"))]
pub id: Option<String>,
pub description: String,
#[cfg_attr(feature = "network", serde(skip_serializing_if = "Option::is_none"))]
pub quantity: Option<String>,
#[cfg_attr(feature = "network", serde(skip_serializing_if = "Option::is_none"))]
pub unit_price: Option<Amount>,
#[cfg_attr(feature = "network", serde(skip_serializing_if = "Option::is_none"))]
pub amount: Option<Amount>,
#[cfg_attr(feature = "network", serde(skip_serializing_if = "Option::is_none"))]
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct InstrumentBase {
pub thread_id: String,
pub payee: String,
pub payer: String,
#[cfg_attr(feature = "network", serde(skip_serializing_if = "Option::is_none"))]
pub note: Option<String>,
pub line_items: Vec<LineItem>,
pub total: Amount,
pub invoice_number: String,
pub created_at: u64,
#[cfg_attr(feature = "network", serde(skip_serializing_if = "Option::is_none"))]
pub arbitrary: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct Invoice {
pub kind: RemittanceKind,
#[cfg_attr(feature = "network", serde(skip_serializing_if = "Option::is_none"))]
pub expires_at: Option<u64>,
pub options: HashMap<String, serde_json::Value>,
#[cfg_attr(feature = "network", serde(flatten))]
pub base: InstrumentBase,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct IdentityRequest {
pub types: HashMap<String, Vec<String>>,
pub certifiers: Vec<String>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct IdentityVerificationRequest {
pub kind: RemittanceKind,
pub thread_id: String,
pub request: IdentityRequest,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct RemittanceCertificate {
#[cfg_attr(feature = "network", serde(rename = "type"))]
pub cert_type: String,
pub certifier: String,
pub subject: String,
pub fields: HashMap<String, String>,
pub signature: String,
pub serial_number: String,
pub revocation_outpoint: String,
pub keyring_for_verifier: HashMap<String, String>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct IdentityVerificationResponse {
pub kind: RemittanceKind,
pub thread_id: String,
pub certificates: Vec<RemittanceCertificate>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct IdentityVerificationAcknowledgment {
pub kind: RemittanceKind,
pub thread_id: String,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct Settlement {
pub kind: RemittanceKind,
pub thread_id: String,
pub module_id: String,
pub option_id: String,
pub sender: String,
pub created_at: u64,
pub artifact: serde_json::Value,
#[cfg_attr(feature = "network", serde(skip_serializing_if = "Option::is_none"))]
pub note: Option<String>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct Receipt {
pub kind: RemittanceKind,
pub thread_id: String,
pub module_id: String,
pub option_id: String,
pub payee: String,
pub payer: String,
pub created_at: u64,
pub receipt_data: serde_json::Value,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct Termination {
pub code: String,
pub message: String,
#[cfg_attr(feature = "network", serde(skip_serializing_if = "Option::is_none"))]
pub details: Option<serde_json::Value>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct PeerMessage {
pub message_id: String,
pub sender: String,
pub recipient: String,
pub message_box: String,
pub body: String,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "network", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "network", serde(rename_all = "camelCase"))]
pub struct RemittanceEnvelope {
pub v: u8,
pub id: String,
pub kind: RemittanceKind,
pub thread_id: String,
pub created_at: u64,
pub payload: serde_json::Value,
}
#[derive(Clone)]
pub struct ModuleContext {
pub wallet: Arc<dyn crate::wallet::interfaces::WalletInterface>,
pub originator: Option<String>,
pub now: Arc<dyn Fn() -> u64 + Send + Sync>,
pub logger: Option<Arc<dyn LoggerLike>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_thread_state_serialization() {
use RemittanceThreadState::*;
let cases = vec![
(New, r#""new""#),
(IdentityRequested, r#""identityRequested""#),
(IdentityResponded, r#""identityResponded""#),
(IdentityAcknowledged, r#""identityAcknowledged""#),
(Invoiced, r#""invoiced""#),
(Settled, r#""settled""#),
(Receipted, r#""receipted""#),
(Terminated, r#""terminated""#),
(Errored, r#""errored""#),
];
for (variant, expected) in cases {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, expected, "serialization mismatch for {:?}", variant);
}
}
#[test]
fn test_thread_state_deserialization() {
use RemittanceThreadState::*;
let cases = vec![
(r#""new""#, New),
(r#""identityRequested""#, IdentityRequested),
(r#""identityResponded""#, IdentityResponded),
(r#""identityAcknowledged""#, IdentityAcknowledged),
(r#""invoiced""#, Invoiced),
(r#""settled""#, Settled),
(r#""receipted""#, Receipted),
(r#""terminated""#, Terminated),
(r#""errored""#, Errored),
];
for (json, expected) in cases {
let parsed: RemittanceThreadState = serde_json::from_str(json).unwrap();
assert_eq!(parsed, expected, "deserialization mismatch for {}", json);
}
}
#[test]
fn test_kind_serialization() {
use RemittanceKind::*;
let cases = vec![
(Invoice, r#""invoice""#),
(
IdentityVerificationRequest,
r#""identityVerificationRequest""#,
),
(
IdentityVerificationResponse,
r#""identityVerificationResponse""#,
),
(
IdentityVerificationAcknowledgment,
r#""identityVerificationAcknowledgment""#,
),
(Settlement, r#""settlement""#),
(Receipt, r#""receipt""#),
(Termination, r#""termination""#),
];
for (variant, expected) in cases {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, expected, "serialization mismatch for {:?}", variant);
}
}
#[test]
fn test_kind_deserialization() {
use RemittanceKind::*;
let cases = vec![
(r#""invoice""#, Invoice),
(
r#""identityVerificationRequest""#,
IdentityVerificationRequest,
),
(
r#""identityVerificationResponse""#,
IdentityVerificationResponse,
),
(
r#""identityVerificationAcknowledgment""#,
IdentityVerificationAcknowledgment,
),
(r#""settlement""#, Settlement),
(r#""receipt""#, Receipt),
(r#""termination""#, Termination),
];
for (json, expected) in cases {
let parsed: RemittanceKind = serde_json::from_str(json).unwrap();
assert_eq!(parsed, expected, "deserialization mismatch for {}", json);
}
}
#[test]
fn test_valid_transitions() {
use RemittanceThreadState::*;
assert!(is_valid_transition(&New, &IdentityRequested));
assert!(is_valid_transition(&New, &Invoiced));
assert!(is_valid_transition(&New, &Settled));
assert!(is_valid_transition(&New, &Terminated));
assert!(is_valid_transition(&New, &Errored));
assert!(is_valid_transition(&IdentityRequested, &IdentityResponded));
assert!(is_valid_transition(&IdentityRequested, &Invoiced));
assert!(is_valid_transition(
&IdentityResponded,
&IdentityAcknowledged
));
assert!(is_valid_transition(&IdentityResponded, &Invoiced));
assert!(is_valid_transition(&IdentityAcknowledged, &Invoiced));
assert!(is_valid_transition(&IdentityAcknowledged, &Settled));
assert!(is_valid_transition(&Settled, &Receipted));
assert!(is_valid_transition(&Settled, &Terminated));
assert!(is_valid_transition(&Receipted, &Terminated));
assert!(is_valid_transition(&Receipted, &Errored));
assert!(is_valid_transition(&Terminated, &Errored));
}
#[test]
fn test_invalid_transitions() {
use RemittanceThreadState::*;
assert!(!is_valid_transition(&Receipted, &New));
assert!(!is_valid_transition(&Errored, &New));
assert!(!is_valid_transition(&Errored, &Settled));
assert!(!is_valid_transition(&New, &Receipted));
assert!(!is_valid_transition(&Settled, &Invoiced));
assert!(!is_valid_transition(&Terminated, &Settled));
}
#[test]
fn test_invoiced_back_transitions() {
use RemittanceThreadState::*;
assert!(is_valid_transition(&Invoiced, &IdentityRequested));
assert!(is_valid_transition(&Invoiced, &IdentityResponded));
assert!(is_valid_transition(&Invoiced, &IdentityAcknowledged));
}
#[test]
fn test_errored_is_terminal() {
let transitions = allowed_transitions(&RemittanceThreadState::Errored);
assert!(
transitions.is_empty(),
"Errored should be a terminal state with no transitions"
);
}
#[test]
fn test_logger_like_is_object_safe() {
struct TestLogger;
impl LoggerLike for TestLogger {
fn log(&self, args: &[&dyn std::fmt::Debug]) {
let _ = args;
}
fn warn(&self, args: &[&dyn std::fmt::Debug]) {
let _ = args;
}
fn error(&self, args: &[&dyn std::fmt::Debug]) {
let _ = args;
}
}
let logger = TestLogger;
let dyn_logger: &dyn LoggerLike = &logger;
dyn_logger.log(&[&"test message"]);
dyn_logger.warn(&[&"warning"]);
dyn_logger.error(&[&"error"]);
}
#[test]
fn test_thread_state_display() {
assert_eq!(RemittanceThreadState::New.to_string(), "new");
assert_eq!(
RemittanceThreadState::IdentityRequested.to_string(),
"identityRequested"
);
assert_eq!(RemittanceThreadState::Invoiced.to_string(), "invoiced");
assert_eq!(RemittanceThreadState::Errored.to_string(), "errored");
}
}