123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- use crate::{
- amount::AmountCents,
- config::Config,
- payment::PaymentTo,
- storage::{self, Batch, Storage},
- transaction::*,
- AccountId, Amount, Asset, PaymentFrom, Status, TransactionId,
- };
- use chrono::{serde::ts_milliseconds, DateTime, Utc};
- use serde::{Deserialize, Serialize};
- use sha2::{Digest, Sha256};
- use std::collections::HashMap;
- /// Transaction Inner
- ///
- /// This is the transaction details, as described bellow, it is a Transaction but without the ID nor
- /// the revision ID.
- ///
- /// This seperated struct is used to calculate the ID of the transaction, and to be able to
- /// serialize the transaction without the ID.
- ///
- /// Since the transaction ID is calculated from the transaction itself, to provide cryptographic
- /// security that its content was not altered.
- #[derive(Debug, Clone, Deserialize, Serialize)]
- pub struct TransactionInner {
- /// A pointer to the first revision of the transaction.
- first_revision: Option<TransactionId>,
- /// Any previous transaction that this transaction is replacing.
- previous: Option<TransactionId>,
- /// A human-readable description of the transaction changes.
- changelog: String,
- spends: Vec<PaymentFrom>,
- creates: Vec<PaymentTo>,
- #[allow(dead_code)]
- reference: String,
- #[serde(rename = "type")]
- typ: Type,
- status: Status,
- tags: Vec<String>,
- #[serde(with = "ts_milliseconds")]
- created_at: DateTime<Utc>,
- #[serde(with = "ts_milliseconds")]
- updated_at: DateTime<Utc>,
- }
- impl TransactionInner {
- pub fn calculate_id(&self) -> Result<TransactionId, Error> {
- let mut hasher = Sha256::new();
- let bytes = bincode::serialize(self)?;
- hasher.update(bytes);
- Ok(TransactionId::new(hasher.finalize().into()))
- }
- /// The transaction fingerprint is a hash of the properties that are not allowed to be updated
- /// in a transaction.
- pub fn transaction_fingerprint(&self) -> Result<TransactionId, Error> {
- let mut hasher = Sha256::new();
- hasher.update(&bincode::serialize(&self.spends)?);
- hasher.update(&bincode::serialize(&self.creates)?);
- hasher.update(&self.typ.to_string());
- hasher.update(&self.reference);
- hasher.update(&self.created_at.timestamp_millis().to_string());
- Ok(TransactionId::new(hasher.finalize().into()))
- }
- /// Validates the transaction input and output (debit and credit)
- ///
- /// The total sum of debits and credits should always be zero in transactions, unless they are
- /// deposits or withdrawals.
- ///
- /// Negative amounts can be used in transactions, but the total sum of debits and credits should
- /// always be zero, and the debit amount should be positive numbers
- pub fn validate(&self) -> Result<(), Error> {
- let mut debit = HashMap::<Asset, AmountCents>::new();
- let mut credit = HashMap::<Asset, AmountCents>::new();
- for input in self.spends.iter() {
- if let Some(value) = debit.get_mut(input.amount.asset()) {
- *value = input
- .amount
- .cents()
- .checked_add(*value)
- .ok_or(Error::Overflow)?;
- } else {
- debit.insert(input.amount.asset().clone(), input.amount.cents());
- }
- }
- for (asset, amount) in debit.iter() {
- if *amount <= 0 {
- return Err(Error::InvalidAmount(
- asset.new_amount(*amount),
- asset.new_amount(*amount),
- ));
- }
- }
- if !self.typ.is_transaction() {
- // We don't care input/output balance in external operations
- // (withdrawals/deposits), because these operations are inbalanced
- return Ok(());
- }
- for output in self.creates.iter() {
- if let Some(value) = credit.get_mut(output.amount.asset()) {
- *value = output
- .amount
- .cents()
- .checked_add(*value)
- .ok_or(Error::Overflow)?;
- } else {
- credit.insert(output.amount.asset().clone(), output.amount.cents());
- }
- }
- for (asset, credit_amount) in credit.into_iter() {
- if let Some(debit_amount) = debit.remove(&asset) {
- if debit_amount != credit_amount {
- return Err(Error::InvalidAmount(
- asset.new_amount(debit_amount),
- asset.new_amount(credit_amount),
- ));
- }
- } else {
- return Err(Error::MissingSpendingAsset(asset));
- }
- }
- if let Some((asset, _)) = debit.into_iter().next() {
- return Err(Error::MissingPaymentAsset(asset));
- }
- Ok(())
- }
- }
- /// Transactions
- ///
- /// A transaction is a set of payments being spent, to create a new set of payments. Payments can be
- /// spent only once. This simple model is inspired by Bitcoin's Unspent Transaction output model. In
- /// every transaction, the sum of the spends must equal the sum of the creates. Any difference will
- /// result in an error.
- ///
- /// Every payment has a target account and the amount and asset.
- ///
- /// Transactions are immutable, but since this is an append-only database, a newer version of the
- /// transaction can replace a previous version, as long as the transaction is not finalized.
- /// Previous transaction versions are kept forever and never pruned. The spend and create fields are
- /// not updatable, and the state of the transaction has a transition rule that will be enforced in
- /// each update. Furthermore, all new revisions must have a description of their update, inspired by
- /// a git commit message.
- ///
- /// A Finalized transaction will either be settled (i.e. spendable) or reverted, in which case it is
- /// void but it is kept for historical reasons.
- ///
- /// Although there is no concept of balances or accounts at this layer, the balance associated with
- /// an account is a sum of all received payments that were not spent.
- ///
- /// The transaction ID, and the revision ID, are the cryptographic hash of the transactions
- #[derive(Debug, Clone, Serialize)]
- pub struct Transaction {
- /// The TransactionID is the RevisionID of the first revision of the transaction.
- pub id: TransactionId,
- /// Current Revision ID.
- pub revision: TransactionId,
- /// The transaction inner details
- #[serde(flatten)]
- inner: TransactionInner,
- }
- impl TryFrom<TransactionInner> for Transaction {
- type Error = Error;
- fn try_from(inner: TransactionInner) -> Result<Self, Self::Error> {
- let id = inner.calculate_id()?;
- Ok(Transaction {
- id: inner.first_revision.clone().unwrap_or_else(|| id.clone()),
- revision: id,
- inner,
- })
- }
- }
- impl Transaction {
- /// Creates a new external deposit transaction
- ///
- /// All transactions must be balanced, same amounts that are spent should be
- /// created. There are two exceptions, external deposits and withdrawals.
- /// The idea is to mimic external operations, where new assets enter the system.
- pub fn new_external_deposit(
- reference: String,
- status: Status,
- pay_to: Vec<(AccountId, Amount)>,
- ) -> Result<Transaction, Error> {
- TransactionInner {
- first_revision: None,
- changelog: "".to_owned(),
- previous: None,
- spends: vec![],
- creates: pay_to
- .into_iter()
- .map(|(to, amount)| PaymentTo { to, amount })
- .collect(),
- reference,
- typ: Type::Deposit,
- tags: Vec::new(),
- status,
- created_at: Utc::now(),
- updated_at: Utc::now(),
- }
- .try_into()
- }
- /// Returns a unique list of accounts involved in this transaction.
- ///
- /// Accounts are sorted and unique, and they include the accounts that spent and that receives
- pub fn accounts(&self) -> Vec<AccountId> {
- let mut accounts = self
- .inner
- .creates
- .iter()
- .map(|x| x.to.clone())
- .collect::<Vec<_>>();
- accounts.extend(
- self.inner
- .spends
- .iter()
- .map(|x| x.from.clone())
- .collect::<Vec<_>>(),
- );
- accounts.sort();
- accounts.dedup();
- accounts
- }
- /// Creates a new external withdrawal transaction
- ///
- /// Burns assets to reflect external withdrawals
- pub fn new_external_withdrawal(
- reference: String,
- status: Status,
- spend: Vec<PaymentFrom>,
- ) -> Result<Transaction, Error> {
- TransactionInner {
- first_revision: None,
- changelog: "".to_owned(),
- previous: None,
- spends: spend,
- creates: vec![],
- reference,
- typ: Type::Withdrawal,
- tags: Vec::new(),
- status,
- created_at: Utc::now(),
- updated_at: Utc::now(),
- }
- .try_into()
- }
- /// Gets the inner transaction
- pub fn inner(&self) -> &TransactionInner {
- &self.inner
- }
- /// Creates a new transaction
- pub async fn new(
- reference: String,
- status: Status,
- typ: Type,
- spends: Vec<PaymentFrom>,
- pay_to: Vec<(AccountId, Amount)>,
- ) -> Result<Transaction, Error> {
- // for (i, input) in spends.iter().enumerate() {
- // if !input.is_spendable_or_was_by(&id) {
- // return Err(Error::InvalidPaymentStatus(i, input.status.clone()));
- // }
- // }
- let create = pay_to
- .into_iter()
- .map(|(to, amount)| PaymentTo { to, amount })
- .collect();
- TransactionInner {
- first_revision: None,
- changelog: "".to_owned(),
- previous: None,
- spends,
- creates: create,
- reference,
- typ,
- tags: Vec::new(),
- status,
- created_at: Utc::now(),
- updated_at: Utc::now(),
- }
- .try_into()
- }
- /// Prepares a transaction ammend to update its status.
- ///
- /// If the status transaction is not allowed, it will return an error.
- ///
- /// The returned transaction is the newest version which is already persisted. The previous
- /// version is not longer in memory
- #[inline]
- pub async fn change_status<S>(
- self,
- config: &Config<S>,
- new_status: Status,
- reason: String,
- ) -> Result<Self, Error>
- where
- S: Storage + Sync + Send,
- {
- config
- .status
- .is_valid_transition(&self.inner.status, &new_status)?;
- let mut inner = self.inner;
- inner.changelog = reason;
- if inner.first_revision.is_none() {
- inner.first_revision = Some(self.id);
- }
- inner.updated_at = Utc::now();
- inner.previous = Some(self.revision);
- inner.status = new_status;
- let mut x: Transaction = inner.try_into()?;
- x.persist(config).await?;
- Ok(x)
- }
- /// Validates the transaction before storing
- pub(crate) fn validate(&self) -> Result<(), Error> {
- let calculated_revision_id = self.inner.calculate_id()?;
- if self.revision != calculated_revision_id
- || (self.inner.previous.is_none() && self.id != calculated_revision_id)
- {
- return Err(Error::InvalidTransactionId(
- self.id.clone(),
- calculated_revision_id,
- ));
- }
- self.inner.validate()
- }
- /// Returns the list of payments that were used to create this transaction
- pub fn spends(&self) -> &[PaymentFrom] {
- &self.inner.spends
- }
- /// Returns the list of payments that were created by this transaction
- pub fn creates(&self) -> &[PaymentTo] {
- &self.inner.creates
- }
- /// Returns the transaction ID
- pub fn id(&self) -> &TransactionId {
- &self.id
- }
- /// Returns the transaction status
- pub fn status(&self) -> &Status {
- &self.inner.status
- }
- /// Returns the transaction type
- pub fn typ(&self) -> Type {
- self.inner.typ
- }
- /// Returns the reference of this transaction
- pub fn reference(&self) -> &str {
- &self.inner.reference
- }
- /// Returns the time when this transaction was created
- pub fn created_at(&self) -> DateTime<Utc> {
- self.inner.created_at
- }
- /// Returns the time when this transaction was last updated
- pub fn updated_at(&self) -> DateTime<Utc> {
- self.inner.updated_at
- }
- /// Persists the changes done to this transaction object.
- /// This method is not idempotent, and it will fail if the transaction if the requested update
- /// is not allowed.
- pub async fn persist<'a, S>(&mut self, config: &'a Config<S>) -> Result<(), Error>
- where
- S: Storage + Sync + Send,
- {
- self.validate()?;
- let mut batch = config.storage.begin().await?;
- if let Some(previous_id) = &self.inner.previous {
- // Make sure this update is updating the last revision and the status is not final
- let current_transaction = batch.get_transaction(&self.id).await?;
- if current_transaction.revision != *previous_id
- || config.status.is_final(¤t_transaction.inner.status)
- || self.inner.transaction_fingerprint()?
- != current_transaction.inner.transaction_fingerprint()?
- {
- return Err(Error::TransactionUpdatesNotAllowed);
- }
- // Updates all the spends to reflect the new status.
- let (updated_created, updated_spent) = if config.status.is_reverted(&self.inner.status)
- {
- // Release all the previously spent payments since the whole transaction is being
- // reverted due a failure or cancellation.
- batch
- .update_transaction_payments(
- &self.id,
- storage::Status::Failed,
- storage::Status::Spendable,
- )
- .await?
- } else if config.status.is_spendable(&self.inner.status) {
- // Spend all the payments that were used to create this transaction
- batch
- .update_transaction_payments(
- &self.id,
- storage::Status::Spendable,
- storage::Status::Spent,
- )
- .await?
- } else {
- // Lock both the spent transaction and the created transaction, since this
- // transaction is still not finalized
- batch
- .update_transaction_payments(
- &self.id,
- storage::Status::Locked,
- storage::Status::Locked,
- )
- .await?
- };
- if updated_created != self.inner.creates.len()
- || updated_spent != self.inner.spends.len()
- {
- return Err(Error::NoUpdate);
- }
- } else {
- let spends = self
- .inner
- .spends
- .iter()
- .map(|x| x.id.clone())
- .collect::<Vec<_>>();
- let (spent_payment_status, creates_payment_status) =
- if config.status.is_spendable(&self.inner.status) {
- (storage::Status::Spent, storage::Status::Spendable)
- } else {
- (storage::Status::Locked, storage::Status::Locked)
- };
- batch
- .spend_payments(&self.id, spends, spent_payment_status)
- .await?;
- batch
- .create_payments(&self.id, &self.inner.creates, creates_payment_status)
- .await?;
- for account in self.accounts() {
- batch
- .relate_account_to_transaction(&self.id, &account, self.typ())
- .await?;
- }
- }
- batch.store_transaction(self).await?;
- //batch.tag_transaction(self, &self.inner.tags).await?;
- batch.commit().await?;
- Ok(())
- }
- }
|