diff --git a/migrations/create_tables_001.sql b/migrations/create_tables_001.sql index 129055e..2a202b4 100644 --- a/migrations/create_tables_001.sql +++ b/migrations/create_tables_001.sql @@ -65,5 +65,6 @@ CREATE TABLE IF NOT EXISTS cashed_cheques FOREIGN KEY (underlying_security) REFERENCES securities (id), FOREIGN KEY (drawer_user_id) REFERENCES users (id), - FOREIGN KEY (drawer_account_id) REFERENCES accounts (id) + FOREIGN KEY (drawer_account_id) REFERENCES accounts (id), + CONSTRAINT double_spending UNIQUE (drawer_user_id, cheque_number) -- no double spending ); diff --git a/src/db/_accounts.rs b/src/db/_accounts.rs index c40d044..dffb253 100644 --- a/src/db/_accounts.rs +++ b/src/db/_accounts.rs @@ -26,6 +26,10 @@ impl Account { security: Security::from_row(row)?, }); } + + pub fn raw_amount(&self) -> u32 { + self.amount + } } pub fn list_accounts<'a>(conn: &Connection) -> Result> { @@ -48,7 +52,7 @@ pub fn list_accounts<'a>(conn: &Connection) -> Result> { return Ok(ret); } -pub fn get_account_by_id(tx: &Transaction, id: u32) -> Result { +pub fn get_account_by_id(tx: &Transaction, id: &AccountId) -> Result { let q = "SELECT acc.id AS acc_id, acc.amount AS acc_amount, UNIXEPOCH(acc.timestamp) AS acc_timestamp, @@ -66,7 +70,8 @@ pub fn create_account(tx: &Transaction, sec: &SecurityId, user: &UserId) -> Resu let q = "INSERT INTO accounts(security, owner, amount, timestamp) VALUES (?1, ?2, 0, CURRENT_TIMESTAMP)"; tx.execute(q, (sec, user))?; - return get_account_by_id(tx, tx.last_insert_rowid() as u32); + let id = AccountId::from_id(tx.last_insert_rowid() as u32); + return get_account_by_id(tx, &id); } pub fn is_owner(tx: &Transaction, account_id: &AccountId, user_id: &UserId) -> Result { diff --git a/src/db/_external_id.rs b/src/db/_external_id.rs index 6421fce..31cb56a 100644 --- a/src/db/_external_id.rs +++ b/src/db/_external_id.rs @@ -1,4 +1,5 @@ use std::io::Read; +use std::fmt; use regex::{Captures, Regex}; @@ -17,16 +18,12 @@ pub trait Id { fn get_id(&self) -> u32; } - -impl ToString for SafeId { - fn to_string(&self) -> String { - format!("{}-{}", - hex::encode(self.encrypted_id), - hex::encode(self.signature)) +impl fmt::Display for SafeId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}-{}", hex::encode(self.encrypted_id), hex::encode(self.signature)) } } - impl Id for SafeId { type Output = Self; @@ -64,9 +61,9 @@ impl Id for SafeId { macro_rules! impl_ids { ($T:ident, $prefix:literal) => { - impl ToString for $T { - fn to_string(&self) -> String { - format!("{}-{}", $prefix, self.0.to_string()) + impl fmt::Display for $T { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}-{}", $prefix, self.0) } } diff --git a/src/services/_transactions.rs b/src/services/_transactions.rs index 35ada41..51ea41a 100644 --- a/src/services/_transactions.rs +++ b/src/services/_transactions.rs @@ -1,4 +1,4 @@ -use rusqlite::{params, Params, Transaction}; +use rusqlite::{ffi, params, Params, Transaction}; use crate::db::accounts::{Account, get_account_by_id}; use crate::db::cheques::SecurityEmitterCheque; @@ -29,21 +29,29 @@ fn _add_cheque_to_cashed_cheques_log(tx: &TransactionHelper, ) VALUES(?1, ?2, ?3, ?4) "; - tx.execute(q, params![cheque.drawer_user_id, + let ret = tx.execute(q, params![cheque.drawer_user_id, cheque.cheque_number, cheque.amount, - cheque.security_id])?; - Ok(()) + cheque.security_id]); + match ret { + Ok(_) => Ok(()), + Err(Error::SqlError(rusqlite::Error::SqliteFailure(ffi::Error { code: _, extended_code: 2067 }, _))) => + Err(Error::DoubleSpendingError(format!("Cheque number {} from user {} has already been cached.", cheque.cheque_number, cheque.drawer_user_id))), + Err(a) => Err(a) + } } fn cash_cheque(tx: &Transaction, cheque: &SecurityEmitterCheque, account_id: &AccountId) -> CustomResult<()> { - // move money to new account + if !securities::is_owner(tx, &cheque.security_id, &cheque.drawer_user_id).map_err(Error::SqlError)? { return Err(Error::InvalidDrawer(format!("The drawer is the security owner: {:?}", cheque))); } + // todo: + // 1. payee is owner of the account. + let myx = TransactionHelper::new(tx); _add_securities_to_account(&myx, cheque, account_id)?; @@ -54,51 +62,103 @@ fn cash_cheque(tx: &Transaction, #[cfg(test)] -mod test { +mod test_caching_cheques { use rusqlite::{Connection, params}; use crate::db::accounts::create_account; - use crate::db::test_utils; + use crate::db::{accounts, test_utils}; + use crate::db::external_id::{Id, SecurityId, UserId}; + use crate::db::users::User; use crate::migration; use super::*; - fn setup() -> Connection { + fn setup_conn() -> Connection { let mut conn = Connection::open_in_memory().unwrap(); migration::apply_migrations(&mut conn).unwrap(); return conn; } - - #[test] - fn test_basic() -> Result<(), Error> { - let mut conn = setup(); + fn setup_basic() -> (Connection, UserId, SecurityId, UserId, AccountId) { + let mut conn = setup_conn(); let tx = conn.transaction().unwrap(); let (users, securities, accounts) = test_utils::setup_basic_environment(&tx).unwrap(); let sec = securities.get(0).unwrap(); let drawer = users.get(0).unwrap(); let payee = users.get(1).unwrap(); - let amount: u32 = 1000; - let payee_account = create_account(&tx, & sec.id(), & payee.id()).unwrap(); + let payee_account = create_account(&tx, &sec.id(), &payee.id()).unwrap(); tx.commit().unwrap(); + (conn, drawer.id(), sec.id(), payee.id(), payee_account.id()) + } + + + #[test] + fn test_basic_happy_path() -> CustomResult<()> { + let (mut conn, drawer, sec, payee, payee_account) = setup_basic(); let tx = conn.transaction().unwrap(); - - // let tx = conn.transaction()?; - // SecurityEmitterCheque::new(securities.get()) let cheque = SecurityEmitterCheque::new( - drawer.id(), - sec.id(), - payee.id(), - amount, + drawer, + sec, + payee, + 1000, 99, ); - - cash_cheque(&tx, &cheque, &payee_account.id())?; - + cash_cheque(&tx, &cheque, &payee_account)?; tx.commit().unwrap(); + + let tx = conn.transaction().unwrap(); + let acc = get_account_by_id(&tx, &payee_account).unwrap(); + assert_eq!(acc.raw_amount(), 1000); + Ok(()) } + + #[test] + fn test_double_spend() -> CustomResult<()> { + let (mut conn, drawer, sec, payee, payee_account) = setup_basic(); + let amount: u32 = 1000; + let tx = conn.transaction().unwrap(); + + let cheque = SecurityEmitterCheque::new( + drawer, + sec, + payee, + 1000, + 99, + ); + + cash_cheque(&tx, &cheque, &payee_account)?; + tx.commit().unwrap(); + let tx = conn.transaction().unwrap(); + + let ret = cash_cheque(&tx, &cheque, &payee_account); + match ret { + Err(Error::DoubleSpendingError(_)) => Ok(()), + _ => Err(Error::Testing("Expected double spending error here.".to_string())) + } + } + + #[test] + fn test_invalid_drawer() -> CustomResult<()> { + let (mut conn, drawer, sec, payee, payee_account) = setup_basic(); + let amount: u32 = 1000; + let tx = conn.transaction().unwrap(); + let not_currency_owner = UserId::from_id(drawer.get_id() + 72623); + let cheque = SecurityEmitterCheque::new( + not_currency_owner, + sec, + payee, + 1000, + 99, + ); + + let ret = cash_cheque(&tx, &cheque, &payee_account); + match ret { + Err(Error::InvalidDrawer(_)) => Ok(()), + _ => Err(Error::Testing("Expected invalid drawer here.".to_string())) + } + } } \ No newline at end of file diff --git a/src/util.rs b/src/util.rs index 40e9e23..f11ff37 100644 --- a/src/util.rs +++ b/src/util.rs @@ -4,6 +4,8 @@ use rusqlite::{Params, Transaction}; pub enum Error { SqlError(rusqlite::Error), InvalidDrawer(String), + DoubleSpendingError(String), + Testing(String), } pub type CustomResult = Result;