Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions contract/src/expiry/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ExpiryError {
#[error("document not anchored")]
DocumentNotAnchored,
#[error("invalid date range")]
InvalidDateRange,
#[error("record not found")]
RecordNotFound,
#[error("already expired")]
AlreadyExpired,
#[error("storage error")]
StorageError,
#[error("stellar anchor failed")]
StellarAnchorFailed,
}
9 changes: 9 additions & 0 deletions contract/src/expiry/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pub mod error;
pub mod repository;
pub mod service;
pub mod types;

pub use error::ExpiryError;
pub use repository::ExpiryRepository;
pub use service::DocumentExpiryService;
pub use types::{DocumentExpiryRecord, ExpiryPolicy, ExpiryStatus};
64 changes: 64 additions & 0 deletions contract/src/expiry/repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use crate::cache::CacheBackend;
use crate::expiry::error::ExpiryError;
use crate::expiry::types::{DocumentExpiryRecord, ExpiryStatus};

const RECORD_PREFIX: &str = "expiry:record:";
const HISTORY_PREFIX: &str = "expiry:history:";
const RECORD_TTL: u64 = 30 * 24 * 60 * 60;

#[derive(Clone)]
pub struct ExpiryRepository {
cache: CacheBackend,
}

impl ExpiryRepository {
pub fn new(cache: CacheBackend) -> Self {
Self { cache }
}

pub async fn save_record(&self, record: &DocumentExpiryRecord) -> Result<(), ExpiryError> {
let key = format!("{}{}", RECORD_PREFIX, record.document_hash);
self.cache
.set(&key, record, RECORD_TTL)
.await
.map_err(|_| ExpiryError::StorageError)
}

pub async fn load_record(&self, document_hash: &str) -> Result<Option<DocumentExpiryRecord>, ExpiryError> {
let key = format!("{}{}", RECORD_PREFIX, document_hash);
self.cache.get(&key).await.map_err(|_| ExpiryError::StorageError)
}

pub async fn update_status(&self, document_hash: &str, status: ExpiryStatus, stellar_memo: Option<String>) -> Result<(), ExpiryError> {
let mut record = self.load_record(document_hash).await?.ok_or(ExpiryError::RecordNotFound)?;
record.status = status;
record.stellar_memo = stellar_memo;
self.save_record(&record).await
}
}

impl ExpiryRepository {
pub fn new(cache: CacheBackend) -> Self {
Self { cache }
}

pub async fn save_record(&self, record: &DocumentExpiryRecord) -> Result<(), ExpiryError> {
let key = format!("{}{}", RECORD_PREFIX, record.document_hash);
self.cache
.set(&key, record, RECORD_TTL)
.await
.map_err(|_| ExpiryError::StorageError)
}

pub async fn load_record(&self, document_hash: &str) -> Result<Option<DocumentExpiryRecord>, ExpiryError> {
let key = format!("{}{}", RECORD_PREFIX, document_hash);
self.cache.get(&key).await.map_err(|_| ExpiryError::StorageError)
}

pub async fn update_status(&self, document_hash: &str, status: ExpiryStatus, stellar_memo: Option<String>) -> Result<(), ExpiryError> {
let mut record = self.load_record(document_hash).await?.ok_or(ExpiryError::RecordNotFound)?;
record.status = status;
record.stellar_memo = stellar_memo;
self.save_record(&record).await
}
}
45 changes: 45 additions & 0 deletions contract/src/expiry/service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use crate::expiry::error::ExpiryError;
use crate::expiry::repository::ExpiryRepository;
use crate::expiry::types::{DocumentExpiryRecord, ExpiryPolicy, ExpiryStatus};
use chrono::Utc;

pub struct DocumentExpiryService {
repository: ExpiryRepository,
}

impl DocumentExpiryService {
pub fn new(repository: ExpiryRepository) -> Self {
Self { repository }
}

pub async fn register_expiry(&self, document_hash: String, policy: ExpiryPolicy) -> Result<DocumentExpiryRecord, ExpiryError> {
if policy.expires_at <= policy.issued_at {
return Err(ExpiryError::InvalidDateRange);
}
let record = DocumentExpiryRecord::new(document_hash.clone(), &policy);
self.repository.save_record(&record).await?;
Ok(record)
}

pub async fn check_status(&self, document_hash: &str) -> Result<DocumentExpiryRecord, ExpiryError> {
let mut record = self.repository.load_record(document_hash).await?.ok_or(ExpiryError::RecordNotFound)?;
let now = Utc::now().timestamp();
if record.is_expired(now) && record.status != ExpiryStatus::Expired {
record.status = ExpiryStatus::Expired;
self.repository.update_status(document_hash, ExpiryStatus::Expired, None).await?;
}
Ok(record)
}

pub async fn renew(&self, document_hash: &str, new_expires_at: i64) -> Result<DocumentExpiryRecord, ExpiryError> {
let mut record = self.repository.load_record(document_hash).await?.ok_or(ExpiryError::RecordNotFound)?;
if new_expires_at <= record.expires_at {
return Err(ExpiryError::InvalidDateRange);
}
record.expires_at = new_expires_at;
record.renewed_at = Some(Utc::now().timestamp());
record.status = ExpiryStatus::Renewed;
self.repository.save_record(&record).await?;
Ok(record)
}
}
44 changes: 44 additions & 0 deletions contract/src/expiry/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpiryPolicy {
pub issued_at: i64,
pub expires_at: i64,
pub renewable: bool,
pub grace_period_days: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ExpiryStatus {
Active,
Expired,
GracePeriod,
Renewed,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentExpiryRecord {
pub document_hash: String,
pub issued_at: i64,
pub expires_at: i64,
pub status: ExpiryStatus,
pub stellar_memo: Option<String>,
pub renewed_at: Option<i64>,
}

impl DocumentExpiryRecord {
pub fn new(document_hash: String, policy: &ExpiryPolicy) -> Self {
Self {
document_hash,
issued_at: policy.issued_at,
expires_at: policy.expires_at,
status: ExpiryStatus::Active,
stellar_memo: None,
renewed_at: None,
}
}

pub fn is_expired(&self, now: i64) -> bool {
now > self.expires_at
}
}
3 changes: 3 additions & 0 deletions contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
pub mod cache;
pub mod config;
pub mod event;
pub mod expiry;
pub mod hash_validator;
pub mod metrics;
pub mod multi_party;
pub mod rate_limit;
pub mod stellar;

Expand Down
19 changes: 19 additions & 0 deletions contract/src/multi_party/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use thiserror::Error;

#[derive(Debug, Error)]
pub enum MultiPartyError {
#[error("session not found")]
SessionNotFound,
#[error("threshold not reached")]
ThresholdNotReached,
#[error("verifier not allowed")]
VerifierNotAllowed,
#[error("duplicate signature")]
DuplicateSignature,
#[error("invalid signature")]
InvalidSignature,
#[error("already finalized")]
AlreadyFinalized,
#[error("storage error")]
StorageError,
}
11 changes: 11 additions & 0 deletions contract/src/multi_party/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pub mod error;
pub mod policy;
pub mod repository;
pub mod service;
pub mod types;

pub use error::MultiPartyError;
pub use policy::{default_internal_review_policy, default_land_sale_policy, from_policy_name};
pub use repository::MultiPartyRepository;
pub use service::MultiPartyVerificationService;
pub use types::{CoSigningSession, MultiPartyPolicy, SignatureRecord, VerifierProfile};
39 changes: 39 additions & 0 deletions contract/src/multi_party/policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use crate::multi_party::error::MultiPartyError;
use crate::multi_party::types::MultiPartyPolicy;
use std::collections::HashSet;

pub fn default_land_sale_policy() -> MultiPartyPolicy {
let mut allowed = HashSet::new();
allowed.insert("seller".to_string());
allowed.insert("buyer".to_string());
allowed.insert("notary".to_string());

MultiPartyPolicy {
required_signatures: 3,
allowed_verifiers: allowed,
policy_name: "land_sale".to_string(),
}
}

pub fn default_internal_review_policy() -> MultiPartyPolicy {
let mut allowed = HashSet::new();
allowed.insert("dept_lead_1".to_string());
allowed.insert("dept_lead_2".to_string());
allowed.insert("dept_lead_3".to_string());
allowed.insert("dept_lead_4".to_string());
allowed.insert("dept_lead_5".to_string());

MultiPartyPolicy {
required_signatures: 2,
allowed_verifiers: allowed,
policy_name: "internal_review".to_string(),
}
}

pub fn from_policy_name(name: &str) -> Result<MultiPartyPolicy, MultiPartyError> {
match name {
"land_sale" => Ok(default_land_sale_policy()),
"internal_review" => Ok(default_internal_review_policy()),
_ => Err(MultiPartyError::StorageError),
}
}
52 changes: 52 additions & 0 deletions contract/src/multi_party/repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::cache::CacheBackend;
use crate::multi_party::error::MultiPartyError;
use crate::multi_party::types::{CoSigningSession, MultiPartyPolicy, SignatureRecord};
use chrono::Utc;
use std::collections::HashSet;

const SESSION_PREFIX: &str = "multi_party:session:";
const HISTORY_PREFIX: &str = "multi_party:history:";
const SESSION_TTL: u64 = 24 * 60 * 60;

#[derive(Clone)]
pub struct MultiPartyRepository {
cache: CacheBackend,
}

impl MultiPartyRepository {
pub fn new(cache: CacheBackend) -> Self {
Self { cache }
}

pub async fn save_session(&self, session: &CoSigningSession) -> Result<(), MultiPartyError> {
let key = format!("{}{}", SESSION_PREFIX, session.document_hash);
self.cache
.set(&key, session, SESSION_TTL)
.await
.map_err(|_| MultiPartyError::StorageError)?;
Ok(())
}

pub async fn load_session(&self, document_hash: &str) -> Result<Option<CoSigningSession>, MultiPartyError> {
let key = format!("{}{}", SESSION_PREFIX, document_hash);
self.cache.get(&key).await.map_err(|_| MultiPartyError::StorageError)
}

pub async fn append_signature(&self, document_hash: &str, record: &SignatureRecord) -> Result<(), MultiPartyError> {
let key = format!("{}{}", HISTORY_PREFIX, document_hash);
let mut history: Vec<SignatureRecord> = self.cache.get(&key).await.map_err(|_| MultiPartyError::StorageError)?.unwrap_or_default();
history.push(record.clone());
self.cache.set(&key, &history, SESSION_TTL).await.map_err(|_| MultiPartyError::StorageError)?;
Ok(())
}

pub async fn get_history(&self, document_hash: &str) -> Result<Vec<SignatureRecord>, MultiPartyError> {
let key = format!("{}{}", HISTORY_PREFIX, document_hash);
self.cache.get(&key).await.map_err(|_| MultiPartyError::StorageError).map(|v| v.unwrap_or_default())
}

pub async fn delete_session(&self, document_hash: &str) -> Result<(), MultiPartyError> {
let key = format!("{}{}", SESSION_PREFIX, document_hash);
self.cache.delete(&key).await.map_err(|_| MultiPartyError::StorageError)
}
}
57 changes: 57 additions & 0 deletions contract/src/multi_party/service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::multi_party::error::MultiPartyError;
use crate::multi_party::repository::MultiPartyRepository;
use crate::multi_party::types::{CoSigningSession, MultiPartyPolicy, SignatureRecord};
use chrono::Utc;

pub struct MultiPartyVerificationService {
repository: MultiPartyRepository,
}

impl MultiPartyVerificationService {
pub fn new(repository: MultiPartyRepository) -> Self {
Self { repository }
}

pub async fn initiate_signing(&self, document_hash: String, policy: MultiPartyPolicy) -> Result<CoSigningSession, MultiPartyError> {
let session = CoSigningSession::new(document_hash.clone(), policy);
self.repository.save_session(&session).await?;
Ok(session)
}

pub async fn add_signature(&self, document_hash: &str, verifier_id: String, signature_blob: String) -> Result<CoSigningSession, MultiPartyError> {
let mut session = self.repository.load_session(document_hash).await?
.ok_or(MultiPartyError::SessionNotFound)?;

let signature = SignatureRecord {
verifier_id,
signature_blob,
timestamp: Utc::now().timestamp(),
transaction_id: None,
};

session.add_signature(signature.clone())?;
self.repository.save_session(&session).await?;
self.repository.append_signature(document_hash, &signature).await?;

Ok(session)
}

pub async fn check_status(&self, document_hash: &str) -> Result<CoSigningSession, MultiPartyError> {
self.repository.load_session(document_hash).await?.ok_or(MultiPartyError::SessionNotFound)
}

pub async fn finalize_if_complete(&self, document_hash: &str) -> Result<CoSigningSession, MultiPartyError> {
let mut session = self.repository.load_session(document_hash).await?
.ok_or(MultiPartyError::SessionNotFound)?;

if !session.is_complete() {
return Err(MultiPartyError::ThresholdNotReached);
}

session.finalized = true;
self.repository.save_session(&session).await?;
self.repository.delete_session(document_hash).await?;

Ok(session)
}
}
Loading