use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use crate::error::{CoreError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SubmarineSendState {
Committed,
Revealable,
Revealed,
Executed,
Expired,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmarineCommitment {
pub id: String,
pub commitment_hash: Vec<u8>,
pub committed_at: DateTime<Utc>,
pub reveal_after: DateTime<Utc>,
pub reveal_before: DateTime<Utc>,
pub committer: String,
pub bond_amount: Option<Decimal>,
pub state: SubmarineSendState,
}
impl SubmarineCommitment {
pub fn new(
id: String,
commitment_hash: Vec<u8>,
committer: String,
commit_delay_seconds: i64,
reveal_window_seconds: i64,
bond_amount: Option<Decimal>,
) -> Self {
let now = Utc::now();
let reveal_after = now + chrono::Duration::seconds(commit_delay_seconds);
let reveal_before = reveal_after + chrono::Duration::seconds(reveal_window_seconds);
Self {
id,
commitment_hash,
committed_at: now,
reveal_after,
reveal_before,
committer,
bond_amount,
state: SubmarineSendState::Committed,
}
}
pub fn can_reveal(&self, current_time: DateTime<Utc>) -> bool {
(self.state == SubmarineSendState::Committed
|| self.state == SubmarineSendState::Revealable)
&& current_time >= self.reveal_after
&& current_time <= self.reveal_before
}
pub fn is_expired(&self, current_time: DateTime<Utc>) -> bool {
self.state == SubmarineSendState::Committed && current_time > self.reveal_before
}
pub fn mark_revealable(&mut self) {
if self.state == SubmarineSendState::Committed {
self.state = SubmarineSendState::Revealable;
}
}
pub fn mark_revealed(&mut self) {
if self.state == SubmarineSendState::Committed
|| self.state == SubmarineSendState::Revealable
{
self.state = SubmarineSendState::Revealed;
}
}
pub fn mark_executed(&mut self) {
if self.state == SubmarineSendState::Revealed {
self.state = SubmarineSendState::Executed;
}
}
pub fn mark_expired(&mut self) {
if self.state == SubmarineSendState::Committed
|| self.state == SubmarineSendState::Revealable
{
self.state = SubmarineSendState::Expired;
}
}
pub fn mark_cancelled(&mut self) {
if self.state == SubmarineSendState::Committed
|| self.state == SubmarineSendState::Revealable
{
self.state = SubmarineSendState::Cancelled;
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmarineSend {
pub id: String,
pub commitment_id: String,
pub from: String,
pub to: String,
pub token_id: String,
pub amount: Decimal,
pub nonce: Vec<u8>,
pub data: Vec<u8>,
pub revealed_at: DateTime<Utc>,
pub executed_at: Option<DateTime<Utc>>,
}
impl SubmarineSend {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
commitment_id: String,
from: String,
to: String,
token_id: String,
amount: Decimal,
nonce: Vec<u8>,
data: Vec<u8>,
) -> Self {
Self {
id,
commitment_id,
from,
to,
token_id,
amount,
nonce,
data,
revealed_at: Utc::now(),
executed_at: None,
}
}
pub fn compute_commitment_hash(&self) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(self.from.as_bytes());
hasher.update(self.to.as_bytes());
hasher.update(self.token_id.as_bytes());
hasher.update(self.amount.to_string().as_bytes());
hasher.update(&self.nonce);
hasher.update(&self.data);
hasher.finalize().to_vec()
}
pub fn verify_commitment(&self, commitment_hash: &[u8]) -> bool {
self.compute_commitment_hash() == commitment_hash
}
pub fn mark_executed(&mut self) {
self.executed_at = Some(Utc::now());
}
pub fn is_executed(&self) -> bool {
self.executed_at.is_some()
}
}
pub struct SubmarineSendBuilder {
pub from: String,
pub to: String,
pub token_id: String,
pub amount: Decimal,
pub data: Vec<u8>,
}
impl SubmarineSendBuilder {
pub fn new(from: String, to: String, token_id: String, amount: Decimal) -> Self {
Self {
from,
to,
token_id,
amount,
data: Vec::new(),
}
}
pub fn with_data(mut self, data: Vec<u8>) -> Self {
self.data = data;
self
}
fn generate_nonce() -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(Utc::now().timestamp_nanos_opt().unwrap_or(0).to_le_bytes());
hasher.update(rand::random::<[u8; 32]>());
hasher.finalize()[0..32].to_vec()
}
pub fn build(
self,
commitment_id: String,
tx_id: String,
commit_delay_seconds: i64,
reveal_window_seconds: i64,
bond_amount: Option<Decimal>,
) -> (SubmarineCommitment, SubmarineSend) {
let nonce = Self::generate_nonce();
let tx = SubmarineSend::new(
tx_id,
commitment_id.clone(),
self.from.clone(),
self.to,
self.token_id,
self.amount,
nonce,
self.data,
);
let commitment_hash = tx.compute_commitment_hash();
let commitment = SubmarineCommitment::new(
commitment_id,
commitment_hash,
self.from,
commit_delay_seconds,
reveal_window_seconds,
bond_amount,
);
(commitment, tx)
}
}
#[derive(Debug)]
pub struct SubmarineSendManager {
pub commitments: HashMap<String, SubmarineCommitment>,
pub revealed_txs: HashMap<String, SubmarineSend>,
pub executed_txs: Vec<SubmarineSend>,
pub default_commit_delay: i64,
pub default_reveal_window: i64,
pub require_bond: bool,
pub min_bond_amount: Decimal,
}
impl SubmarineSendManager {
pub fn new(
default_commit_delay: i64,
default_reveal_window: i64,
require_bond: bool,
min_bond_amount: Decimal,
) -> Self {
Self {
commitments: HashMap::new(),
revealed_txs: HashMap::new(),
executed_txs: Vec::new(),
default_commit_delay,
default_reveal_window,
require_bond,
min_bond_amount,
}
}
pub fn submit_commitment(&mut self, commitment: SubmarineCommitment) -> Result<()> {
if self.commitments.contains_key(&commitment.id) {
return Err(CoreError::Validation(format!(
"Commitment {} already exists",
commitment.id
)));
}
if self.require_bond {
match commitment.bond_amount {
Some(amount) if amount >= self.min_bond_amount => {}
_ => {
return Err(CoreError::Validation(format!(
"Bond amount must be at least {}",
self.min_bond_amount
)));
}
}
}
self.commitments.insert(commitment.id.clone(), commitment);
Ok(())
}
pub fn reveal_transaction(
&mut self,
tx: SubmarineSend,
current_time: DateTime<Utc>,
) -> Result<()> {
let commitment = self.commitments.get_mut(&tx.commitment_id).ok_or_else(|| {
CoreError::Validation(format!("Commitment {} not found", tx.commitment_id))
})?;
if !commitment.can_reveal(current_time) {
return Err(CoreError::Validation(format!(
"Cannot reveal commitment {} at this time",
tx.commitment_id
)));
}
if !tx.verify_commitment(&commitment.commitment_hash) {
return Err(CoreError::Validation(
"Transaction does not match commitment".to_string(),
));
}
commitment.mark_revealed();
self.revealed_txs.insert(tx.commitment_id.clone(), tx);
Ok(())
}
pub fn execute_transaction(&mut self, commitment_id: &str) -> Result<SubmarineSend> {
let mut tx = self.revealed_txs.remove(commitment_id).ok_or_else(|| {
CoreError::Validation(format!(
"No revealed transaction for commitment {}",
commitment_id
))
})?;
let commitment = self.commitments.get_mut(commitment_id).ok_or_else(|| {
CoreError::Validation(format!("Commitment {} not found", commitment_id))
})?;
if commitment.state != SubmarineSendState::Revealed {
return Err(CoreError::Validation(format!(
"Commitment {} is not in revealed state",
commitment_id
)));
}
tx.mark_executed();
commitment.mark_executed();
self.executed_txs.push(tx.clone());
Ok(tx)
}
pub fn cancel_commitment(&mut self, commitment_id: &str, requester: &str) -> Result<()> {
let commitment = self.commitments.get_mut(commitment_id).ok_or_else(|| {
CoreError::Validation(format!("Commitment {} not found", commitment_id))
})?;
if commitment.committer != requester {
return Err(CoreError::Validation(
"Only committer can cancel".to_string(),
));
}
if commitment.state != SubmarineSendState::Committed
&& commitment.state != SubmarineSendState::Revealable
{
return Err(CoreError::Validation(
"Cannot cancel commitment in current state".to_string(),
));
}
commitment.mark_cancelled();
Ok(())
}
pub fn expire_old_commitments(&mut self, current_time: DateTime<Utc>) -> Vec<String> {
let mut expired = Vec::new();
for (id, commitment) in &mut self.commitments {
if commitment.is_expired(current_time)
&& commitment.state == SubmarineSendState::Committed
{
commitment.mark_expired();
expired.push(id.clone());
}
}
expired
}
pub fn update_revealable(&mut self, current_time: DateTime<Utc>) {
for commitment in self.commitments.values_mut() {
if commitment.state == SubmarineSendState::Committed
&& current_time >= commitment.reveal_after
&& current_time <= commitment.reveal_before
{
commitment.mark_revealable();
}
}
}
pub fn get_commitment(&self, commitment_id: &str) -> Option<&SubmarineCommitment> {
self.commitments.get(commitment_id)
}
pub fn get_revealed_transaction(&self, commitment_id: &str) -> Option<&SubmarineSend> {
self.revealed_txs.get(commitment_id)
}
pub fn stats(&self) -> SubmarineSendStats {
let total_commitments = self.commitments.len();
let revealed_count = self
.commitments
.values()
.filter(|c| c.state == SubmarineSendState::Revealed)
.count();
let executed_count = self.executed_txs.len();
let expired_count = self
.commitments
.values()
.filter(|c| c.state == SubmarineSendState::Expired)
.count();
let pending_count = self
.commitments
.values()
.filter(|c| c.state == SubmarineSendState::Committed)
.count();
SubmarineSendStats {
total_commitments,
pending_commitments: pending_count,
revealable_commitments: self
.commitments
.values()
.filter(|c| c.state == SubmarineSendState::Revealable)
.count(),
revealed_transactions: revealed_count,
executed_transactions: executed_count,
expired_commitments: expired_count,
}
}
}
impl Default for SubmarineSendManager {
fn default() -> Self {
Self::new(
300, 600, false, Decimal::ZERO,
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmarineSendStats {
pub total_commitments: usize,
pub pending_commitments: usize,
pub revealable_commitments: usize,
pub revealed_transactions: usize,
pub executed_transactions: usize,
pub expired_commitments: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_submarine_commitment_lifecycle() {
let commitment = SubmarineCommitment::new(
"commit-1".to_string(),
vec![1, 2, 3],
"user1".to_string(),
60, 120, Some(dec!(10)),
);
assert_eq!(commitment.state, SubmarineSendState::Committed);
assert!(!commitment.can_reveal(Utc::now()));
let after_delay = Utc::now() + chrono::Duration::seconds(61);
assert!(commitment.can_reveal(after_delay));
let after_expiry = Utc::now() + chrono::Duration::seconds(200);
assert!(commitment.is_expired(after_expiry));
}
#[test]
fn test_submarine_send_builder() {
let (commitment, tx) = SubmarineSendBuilder::new(
"user1".to_string(),
"user2".to_string(),
"token1".to_string(),
dec!(100),
)
.with_data(vec![1, 2, 3])
.build(
"commit-1".to_string(),
"tx-1".to_string(),
60,
120,
Some(dec!(10)),
);
assert_eq!(commitment.committer, "user1");
assert_eq!(tx.amount, dec!(100));
assert!(tx.verify_commitment(&commitment.commitment_hash));
}
#[test]
fn test_submarine_send_manager() {
let mut manager = SubmarineSendManager::default();
let (commitment, tx) = SubmarineSendBuilder::new(
"user1".to_string(),
"user2".to_string(),
"token1".to_string(),
dec!(100),
)
.build(
"commit-1".to_string(),
"tx-1".to_string(),
1, 60,
None,
);
manager.submit_commitment(commitment).unwrap();
let stats = manager.stats();
assert_eq!(stats.total_commitments, 1);
assert_eq!(stats.pending_commitments, 1);
std::thread::sleep(std::time::Duration::from_secs(2));
let reveal_time = Utc::now();
manager.update_revealable(reveal_time);
manager.reveal_transaction(tx, reveal_time).unwrap();
let executed = manager.execute_transaction("commit-1").unwrap();
assert!(executed.is_executed());
let stats = manager.stats();
assert_eq!(stats.executed_transactions, 1);
}
}