Cesar Rodas 1 year ago
parent
commit
a8b38f0c7e

+ 20 - 19
Cargo.lock

@@ -1520,28 +1520,10 @@ version = "0.1.0"
 dependencies = [
  "actix-web",
  "env_logger",
- "ledger-utxo",
  "serde",
  "serde_json",
  "tokio 1.32.0",
-]
-
-[[package]]
-name = "ledger-utxo"
-version = "0.1.0"
-dependencies = [
- "async-trait",
- "bincode",
- "chrono",
- "futures",
- "hex",
- "serde",
- "sha2",
- "sqlx",
- "strum",
- "strum_macros",
- "thiserror",
- "tokio 1.32.0",
+ "verax",
 ]
 
 [[package]]
@@ -3209,6 +3191,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
 [[package]]
+name = "verax"
+version = "0.1.0"
+dependencies = [
+ "async-trait",
+ "bincode",
+ "chrono",
+ "futures",
+ "hex",
+ "rand 0.8.5",
+ "serde",
+ "sha2",
+ "sqlx",
+ "strum",
+ "strum_macros",
+ "thiserror",
+ "tokio 1.32.0",
+]
+
+[[package]]
 name = "version_check"
 version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"

+ 1 - 1
Cargo.toml

@@ -8,7 +8,7 @@ edition = "2021"
 members = ["utxo"]
 
 [dependencies]
-ledger-utxo = { path = "utxo", features = ["sqlite"] }
+verax = { path = "utxo", features = ["sqlite"] }
 actix-web = "3"
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"

+ 38 - 7
client.js

@@ -2,7 +2,7 @@
 const addr1 = "foo";
 const addr2 = "bar";
 const fee = "fee";
-const percentage = 0.01;
+const percentage = 0.005;
 
 async function deposit(account, amount, asset) {
 	const response = (await fetch("http://127.0.0.1:8080/deposit", {
@@ -14,14 +14,14 @@ async function deposit(account, amount, asset) {
 			memo: "deposit",
 			account,
 			amount: amount.toString(),
-			status: 'settled',
+			status: 'pending',
 			asset,
 		})
 	}));
 	return response.json();
 }
 
-async function trade(amount, asset, from, to) {
+async function trade(amount, asset, from, amount_to, asset_to, to) {
 	const response = (await fetch("http://127.0.0.1:8080/tx", {
 		method: "POST",
 		headers: {
@@ -34,6 +34,11 @@ async function trade(amount, asset, from, to) {
 					account: from,
 					amount: amount.toString(),
 					asset,
+				},
+				{
+					account: to,
+					amount: amount_to.toString(),
+					asset: asset_to,
 				}
 			],
 			credit: [
@@ -43,21 +48,47 @@ async function trade(amount, asset, from, to) {
 					asset,
 				},
 				{
+					account: from,
+					amount: amount_to.toString(),
+					asset: asset_to,
+				},
+				{
 					account: fee,
 					amount: (amount * percentage).toString(),
 					asset,
 				}
 			],
-			status: 'settled',
+			status: 'pending',
 			asset,
 		})
 	}));
 	return response.json();
 }
 
+async function change_status(id, s_status) {
+	const response = (await fetch(`http://127.0.0.1:8080/${id}`, {
+		method: "POST",
+		headers: {
+			"Content-Type": "application/json",
+		},
+		body: JSON.stringify({
+			status: s_status,
+			memo: `change status to ${s_status}`,
+		})
+	}));
+	return response.json();
+}
+
 async function test() {
-	console.log(await deposit(addr1, 100, "BTC"));
-	console.log(await trade(10, "BTC", addr1, addr2))
+	let d = (await deposit(addr1, 100, "BTC"));
+	console.log(await change_status(d.id, 'settled'));
+	d = (await deposit(addr2, 1000000, "USD"));
+	console.log(await change_status(d.id, 'settled'));
+
+	const t = await trade(1, "BTC", addr1, 26751.11, "USD", addr2);
+	console.log(t);
+	console.log(await change_status(t.id, 'processing',));
+	console.log(await change_status(t.id, 'settled'));
 }
 
-test()
+test()

+ 48 - 19
src/main.rs

@@ -2,9 +2,10 @@ use actix_web::{
     error::InternalError, get, middleware::Logger, post, web, App, HttpResponse, HttpServer,
     Responder,
 };
-use ledger_utxo::{AccountId, AnyId, AssetDefinition, AssetManager, Status, TransactionId};
 use serde::{Deserialize, Serialize};
+use serde_json::json;
 use std::sync::Arc;
+use verax::{AccountId, AnyId, AssetDefinition, AssetManager, Status, TransactionId, Type};
 
 #[derive(Deserialize)]
 pub struct Movement {
@@ -26,7 +27,7 @@ impl Deposit {
     pub async fn to_ledger_transaction(
         self,
         ledger: &Ledger,
-    ) -> Result<ledger_utxo::Transaction, ledger_utxo::Error> {
+    ) -> Result<verax::Transaction, verax::Error> {
         let amount = ledger._inner.parse_amount(&self.asset, &self.amount)?;
         ledger
             ._inner
@@ -54,8 +55,11 @@ impl UpdateTransaction {
         self,
         id: &TransactionId,
         ledger: &Ledger,
-    ) -> Result<ledger_utxo::Transaction, ledger_utxo::Error> {
-        ledger._inner.change_status(id, self.status).await
+    ) -> Result<verax::Transaction, verax::Error> {
+        ledger
+            ._inner
+            .change_status(id, self.status, self.memo)
+            .await
     }
 }
 
@@ -63,7 +67,7 @@ impl Transaction {
     pub async fn to_ledger_transaction(
         self,
         ledger: &Ledger,
-    ) -> Result<ledger_utxo::Transaction, ledger_utxo::Error> {
+    ) -> Result<verax::Transaction, verax::Error> {
         let from = self
             .debit
             .into_iter()
@@ -117,16 +121,17 @@ async fn get_balance(info: web::Path<AccountId>, ledger: web::Data<Ledger>) -> i
                         .get_name(amount.asset().id)
                         .map(|asset| AccountResponse {
                             amount: amount.to_string(),
-                            asset: asset,
+                            asset,
                         })
                 })
                 .collect::<Result<Vec<_>, _>>()
             {
                 Ok(balances) => HttpResponse::Ok().json(balances),
-                Err(e) => HttpResponse::BadRequest().json(e),
+                Err(err) => HttpResponse::InternalServerError()
+                    .json(json!({ "text": err.to_string(), "err": err})),
             }
         }
-        Err(e) => HttpResponse::BadRequest().json(e),
+        Err(err) => HttpResponse::BadRequest().json(json!({ "text": err.to_string(), "err": err})),
     }
 }
 
@@ -135,7 +140,15 @@ async fn get_info(info: web::Path<AnyId>, ledger: web::Data<Ledger>) -> impl Res
     let result = match info.0 {
         AnyId::Account(account_id) => ledger
             ._inner
-            .get_transactions(&account_id, None)
+            .get_transactions(
+                &account_id,
+                vec![
+                    Type::Deposit,
+                    Type::Withdrawal,
+                    Type::Exchange,
+                    Type::Transaction,
+                ],
+            )
             .await
             .map(|transactions| HttpResponse::Ok().json(transactions)),
         AnyId::Transaction(transaction_id) => ledger
@@ -143,11 +156,18 @@ async fn get_info(info: web::Path<AnyId>, ledger: web::Data<Ledger>) -> impl Res
             .get_transaction(&transaction_id)
             .await
             .map(|tx| HttpResponse::Ok().json(tx)),
+        AnyId::Payment(payment_id) => ledger
+            ._inner
+            .get_payment_info(&payment_id)
+            .await
+            .map(|tx| HttpResponse::Ok().json(tx)),
     };
 
     match result {
         Ok(x) => x,
-        Err(err) => HttpResponse::InternalServerError().json(err),
+        Err(err) => {
+            HttpResponse::InternalServerError().json(json!({ "text": err.to_string(), "err": err}))
+        }
     }
 }
 
@@ -161,7 +181,7 @@ async fn deposit(item: web::Json<Deposit>, ledger: web::Data<Ledger>) -> impl Re
             // For this example, we'll just echo the received item.
             HttpResponse::Created().json(tx)
         }
-        Err(e) => HttpResponse::Created().json(e),
+        Err(err) => HttpResponse::BadRequest().json(json!({ "text": err.to_string(), "err": err})),
     }
 }
 
@@ -172,7 +192,9 @@ async fn create_transaction(
 ) -> impl Responder {
     match item.into_inner().to_ledger_transaction(&ledger).await {
         Ok(tx) => HttpResponse::Accepted().json(tx),
-        Err(err) => HttpResponse::Created().json(err),
+        Err(err) => {
+            HttpResponse::InternalServerError().json(json!({ "text": err.to_string(), "err": err}))
+        }
     }
 }
 
@@ -188,12 +210,14 @@ async fn update_status(
         .await
     {
         Ok(tx) => HttpResponse::Accepted().json(tx),
-        Err(err) => HttpResponse::Created().json(err),
+        Err(err) => {
+            HttpResponse::InternalServerError().json(json!({ "text": err.to_string(), "err": err}))
+        }
     }
 }
 
 pub struct Ledger {
-    _inner: ledger_utxo::Ledger<'static, ledger_utxo::Batch<'static>, ledger_utxo::Sqlite<'static>>,
+    _inner: verax::Ledger<'static, verax::Batch<'static>, verax::SQLite<'static>>,
 }
 
 #[actix_web::main]
@@ -208,17 +232,22 @@ async fn main() -> std::io::Result<()> {
         AssetDefinition::new(2, "USD", 4),
     ]);
 
-    let pool = ledger_utxo::sqlite::SqlitePoolOptions::new()
-        .connect("sqlite:./test.db")
+    let settings = "sqlite:./test.db"
+        .parse::<verax::sqlite::SqliteConnectOptions>()
+        .expect("valid settings")
+        .create_if_missing(true);
+
+    let pool = verax::sqlite::SqlitePoolOptions::new()
+        .connect_with(settings)
         .await
         .expect("pool");
 
-    let storage = ledger_utxo::Sqlite::new(pool.clone(), asset_manager.clone());
+    let storage = verax::SQLite::new(pool.clone(), asset_manager.clone());
     storage.setup().await.expect("setup");
 
     HttpServer::new(move || {
-        let storage = ledger_utxo::Sqlite::new(pool.clone(), asset_manager.clone());
-        let ledger = ledger_utxo::Ledger::new(storage, asset_manager.clone());
+        let storage = verax::SQLite::new(pool.clone(), asset_manager.clone());
+        let ledger = verax::Ledger::new(storage, asset_manager.clone());
         App::new()
             .wrap(Logger::default())
             .app_data(web::Data::new(Ledger { _inner: ledger }))

+ 9 - 3
utxo/Cargo.toml

@@ -1,9 +1,8 @@
 [package]
-name = "ledger-utxo"
+name = "verax"
 version = "0.1.0"
 edition = "2021"
 
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 [dependencies]
 async-trait = "0.1.73"
 bincode = { version = "1.3.3", features = ["i128"] }
@@ -25,7 +24,14 @@ thiserror = "1.0.48"
 tokio = { version = "1.32.0", features = ["full"] }
 
 [dev-dependencies]
-sqlx = { version = "0.7.1", features = ["sqlite"] }
+rand = "0.8.5"
+sqlx = { version = "0.7.1", features = [
+    "runtime-tokio",
+    "runtime-async-std-native-tls",
+    "tls-native-tls",
+    "sqlite",
+    "chrono",
+] }
 
 [features]
 default = []

+ 27 - 11
utxo/src/amount.rs

@@ -1,5 +1,5 @@
 use crate::Asset;
-use serde::{Serialize, Serializer};
+use serde::{ser::SerializeMap, Serialize, Serializer};
 
 /// The raw storage for cents, the more the better
 pub type AmountCents = i128;
@@ -26,7 +26,7 @@ pub enum Error {
 ///
 /// The `cents` and `Asset.id` must be used to store amounts in the storage
 /// layer. Float or string representations should be used to display
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, Eq, PartialEq)]
 pub struct Amount {
     asset: Asset,
     cents: AmountCents,
@@ -37,16 +37,22 @@ impl Serialize for Amount {
     where
         S: Serializer,
     {
-        let serialized = self.to_string();
-        serializer.serialize_str(&serialized)
+        let amount = self.to_string();
+        let mut s = serializer.serialize_map(Some(2))?;
+        s.serialize_entry("amount", &amount)?;
+        s.serialize_entry("asset", &self.asset.name)?;
+        s.end()
     }
 }
 
 impl Amount {
+    /// Creates a new amount from an asset and cents
     pub fn new(asset: Asset, cents: AmountCents) -> Self {
         Self { asset, cents }
     }
 
+    /// Creates a new amount from a human amount (a floating number serialized
+    /// as string to avoid loss precision)
     pub fn from_human(asset: Asset, human_amount: &str) -> Result<Self, Error> {
         let mut dot_at = None;
         for (pos, i) in human_amount.chars().enumerate() {
@@ -86,22 +92,26 @@ impl Amount {
     }
 
     #[inline]
+    /// Returns the asset for this amount
     pub fn asset(&self) -> &Asset {
         &self.asset
     }
 
     #[inline]
+    /// Return the cents
     pub fn cents(&self) -> AmountCents {
         self.cents
     }
 
+    /// Attempts to do a checked addition of two amounts
+    /// This will fails if assets are not the same or it overflows or underflows
     pub fn checked_add(&self, other: &Self) -> Option<Self> {
         if self.asset != other.asset {
             return None;
         }
 
         self.cents.checked_add(other.cents).map(|cents| Self {
-            asset: self.asset,
+            asset: self.asset.clone(),
             cents,
         })
     }
@@ -139,6 +149,7 @@ mod test {
         let usd = Asset {
             id: 1,
             precision: 4,
+            name: "BTC".into(),
         };
         let amount = usd.new_amount(1022100);
         assert_eq!(amount.to_string(), "102.21");
@@ -149,6 +160,7 @@ mod test {
         let btc = Asset {
             id: 1,
             precision: 8,
+            name: "BTC".into(),
         };
         assert_eq!(btc.new_amount(1022100).to_string(), "0.010221");
         assert_eq!(btc.new_amount(10).to_string(), "0.0000001");
@@ -169,18 +181,22 @@ mod test {
         let btc = Asset {
             id: 1,
             precision: 8,
+            name: "BTC".into(),
         };
-        let parsed_amount = Amount::from_human(btc, "0.1").expect("valid amount");
+        let parsed_amount = Amount::from_human(btc.clone(), "0.1").expect("valid amount");
         assert_eq!(parsed_amount.to_string(), "0.1");
-        let parsed_amount = Amount::from_human(btc, "-0.1").expect("valid amount");
+        let parsed_amount = Amount::from_human(btc.clone(), "-0.1").expect("valid amount");
         assert_eq!(parsed_amount.to_string(), "-0.1");
-        let parsed_amount = Amount::from_human(btc, "-0.000001").expect("valid amount");
+        let parsed_amount = Amount::from_human(btc.clone(), "-0.000001").expect("valid amount");
         assert_eq!(parsed_amount.to_string(), "-0.000001");
-        let parsed_amount = Amount::from_human(btc, "-0.000000001").expect("valid amount");
+        let parsed_amount = Amount::from_human(btc.clone(), "-0.000000001").expect("valid amount");
         assert_eq!(parsed_amount.to_string(), "0");
-        let parsed_amount = Amount::from_human(btc, "0.000001").expect("valid amount");
+        let parsed_amount = Amount::from_human(btc.clone(), "0.000001").expect("valid amount");
         assert_eq!(parsed_amount.to_string(), "0.000001");
-        let parsed_amount = Amount::from_human(btc, "-0.000000100001").expect("valid amount");
+        let parsed_amount =
+            Amount::from_human(btc.clone(), "-0.000000100001").expect("valid amount");
         assert_eq!(parsed_amount.to_string(), "-0.0000001");
+        let parsed_amount = Amount::from_human(btc, "100000").expect("valid amount");
+        assert_eq!(parsed_amount.to_string(), "100000");
     }
 }

+ 8 - 3
utxo/src/asset.rs

@@ -1,6 +1,6 @@
 use crate::{amount::AmountCents, Amount};
 use serde::{Deserialize, Serialize};
-use std::fmt::Display;
+use std::{fmt::Display, sync::Arc};
 
 pub type AssetId = u32;
 
@@ -12,15 +12,20 @@ pub type AssetId = u32;
 /// The asset struct also has a precision, which is a u8 number (from 0-255)
 /// which is used by amount when converting the inner_amount to a human readable
 /// format.
-#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
 pub struct Asset {
+    /// The Asset id
     pub id: AssetId,
+    /// The name of the asset
+    pub name: Arc<str>,
+    /// The precision
     pub(crate) precision: u8,
 }
 
 impl Asset {
+    /// Creates a new amount from the asset
     pub fn new_amount(&self, cents: AmountCents) -> Amount {
-        Amount::new(*self, cents)
+        Amount::new(self.clone(), cents)
     }
 }
 

+ 27 - 4
utxo/src/asset_manager.rs

@@ -1,20 +1,33 @@
 use crate::{amount::AmountCents, asset::AssetId, Amount, Asset, Error};
 use std::{collections::HashMap, sync::Arc};
 
+/// Asset manager
+///
+/// The Verax asset manager is a simple structure that holds the list of
+/// supported assets.
+///
+/// The asset manager is used to convert from human readable amounts to an
+/// internal representation which is safe to use and operate.
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct AssetManager {
     assets: Arc<HashMap<AssetId, AssetDefinition>>,
     asset_names: Arc<HashMap<Arc<str>, AssetDefinition>>,
 }
 
+/// The asset definition
+///
+/// It is a simple structure that holds the asset id (an internal number), name
+/// (a human name) and the asset precision.
 #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
 pub struct AssetDefinition {
+    /// The asset definition
     #[serde(flatten)]
     pub asset: Asset,
     name: Arc<str>,
 }
 
 impl AssetManager {
+    /// Creates a new instance of the asset manager
     pub fn new(assets: Vec<AssetDefinition>) -> Self {
         Self {
             assets: Arc::new(
@@ -32,6 +45,7 @@ impl AssetManager {
         }
     }
 
+    /// Get the name of an asset by their asset id
     pub fn get_name(&self, id: AssetId) -> Result<Arc<str>, Error> {
         self.assets
             .get(&id)
@@ -39,31 +53,40 @@ impl AssetManager {
             .ok_or(Error::AssetIdNotFound(id))
     }
 
+    /// Get an asset definition by their id
     pub fn asset(&self, id: AssetId) -> Result<Asset, Error> {
         self.assets
             .get(&id)
-            .map(|asset| asset.asset)
+            .map(|asset| asset.asset.clone())
             .ok_or(Error::AssetIdNotFound(id))
     }
 
+    /// Get an asset definition by their human name
     pub fn human_amount_by_name(&self, name: &str, human_amount: &str) -> Result<Amount, Error> {
         Ok(self
             .asset_names
             .get(name)
-            .map(|asset| Amount::from_human(asset.asset, human_amount))
+            .map(|asset| Amount::from_human(asset.asset.clone(), human_amount))
             .ok_or(Error::AssetNotFound(name.to_owned()))??)
     }
 
+    /// Get an Amount by their asset Id and cents
     pub fn amount_by_and_cents(&self, id: AssetId, cents: AmountCents) -> Result<Amount, Error> {
         self.asset(id).map(|asset| Amount::new(asset, cents))
     }
 }
 
 impl AssetDefinition {
+    /// Creates a new asset definition
     pub fn new(id: u32, name: &str, precision: u8) -> Self {
+        let name: Arc<str> = name.into();
         Self {
-            asset: Asset { id, precision },
-            name: name.into(),
+            asset: Asset {
+                id,
+                precision,
+                name: name.clone(),
+            },
+            name,
         }
     }
 }

+ 108 - 0
utxo/src/changelog.rs

@@ -0,0 +1,108 @@
+use chrono::{serde::ts_milliseconds, DateTime, Utc};
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+use std::collections::{HashMap, VecDeque};
+
+#[derive(thiserror::Error, Debug, Serialize)]
+pub enum Error {
+    #[error("Missing change {0:?}")]
+    MissingChange(Vec<u8>),
+
+    #[error("Unexpected changes {0:?}")]
+    UnexpectedChanges(Vec<Vec<u8>>),
+
+    #[error("Bincode: {0}")]
+    #[serde(serialize_with = "crate::error::serialize_to_string")]
+    Bincode(#[from] bincode::Error),
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(bound = "T: Serialize + DeserializeOwned")]
+pub struct Changelog<T: DeserializeOwned + Serialize + Send + Sync> {
+    #[serde(skip)]
+    pub previous: Option<Vec<u8>>,
+    #[serde(skip)]
+    pub object_id: Vec<u8>,
+    #[serde(flatten)]
+    pub change: T,
+    #[serde(with = "ts_milliseconds")]
+    pub updated_at: DateTime<Utc>,
+}
+
+impl<T: DeserializeOwned + Serialize + Send + Sync> Changelog<T> {
+    pub fn new(previous: Option<Vec<u8>>, object_id: Vec<u8>, change: T) -> Changelog<T> {
+        Self {
+            previous,
+            object_id,
+            change,
+            updated_at: Utc::now(),
+        }
+    }
+
+    pub fn new_from_db(
+        previous: Option<Vec<u8>>,
+        object_id: Vec<u8>,
+        change: T,
+        created_at: DateTime<Utc>,
+    ) -> Changelog<T> {
+        Self {
+            previous,
+            object_id,
+            change,
+            updated_at: created_at,
+        }
+    }
+
+    pub fn id(&self) -> Result<Vec<u8>, Error> {
+        let mut hasher = Sha256::new();
+        hasher.update(&self.object_id);
+        hasher.update(if let Some(v) = self.previous.as_ref() {
+            v.clone()
+        } else {
+            vec![0, 0]
+        });
+        hasher.update(&bincode::serialize(&self.change)?);
+        hasher.update(&bincode::serialize(&self.updated_at)?);
+        Ok(hasher.finalize().to_vec())
+    }
+}
+
+pub fn sort_changes<T: DeserializeOwned + Serialize + Send + Sync>(
+    changes: Vec<Changelog<T>>,
+    last_change: Vec<u8>,
+) -> Result<Vec<Changelog<T>>, Error> {
+    let mut changes_by_id = changes
+        .into_iter()
+        .map(|a| a.id().map(|id| (id, a)))
+        .collect::<Result<HashMap<Vec<u8>, Changelog<T>>, _>>()?;
+
+    let mut sorted_changes = VecDeque::new();
+
+    let last_change = match changes_by_id.remove(&last_change) {
+        Some(change) => change,
+        None => return Err(Error::MissingChange(last_change)),
+    };
+
+    sorted_changes.push_front(last_change);
+
+    loop {
+        let first_element = sorted_changes.get(0).unwrap();
+        if let Some(id) = first_element.previous.as_ref() {
+            let last_change = match changes_by_id.remove(id) {
+                Some(change) => change,
+                None => return Err(Error::MissingChange(id.clone())),
+            };
+            sorted_changes.push_front(last_change);
+        } else {
+            break;
+        }
+    }
+
+    if !changes_by_id.is_empty() {
+        return Err(Error::UnexpectedChanges(
+            changes_by_id.into_keys().collect(),
+        ));
+    }
+
+    Ok(sorted_changes.into())
+}

+ 17 - 1
utxo/src/error.rs

@@ -1,23 +1,39 @@
 use crate::{amount, asset::AssetId, storage, transaction, AccountId};
-use serde::Serialize;
+use serde::{Serialize, Serializer};
+use std::fmt::Display;
 
+/// The errors that can happen in the Verax crate
 #[derive(thiserror::Error, Debug, Serialize)]
 pub enum Error {
+    /// A transaction error
     #[error("Transaction: {0}")]
     Transaction(#[from] transaction::Error),
 
+    /// A storage error
     #[error("Storage: {0}")]
     Storage(#[from] storage::Error),
 
+    /// The asset is not defined
     #[error("Asset {0} is not defined")]
     AssetIdNotFound(AssetId),
 
+    /// The asset is not found
     #[error("Asset {0} is not defined")]
     AssetNotFound(String),
 
+    /// The account has not enough balance to perform the operation
     #[error("Not enough funds (asset {1}) for account {0}")]
     InsufficientBalance(AccountId, AssetId),
 
+    /// The amount is invalid
     #[error("Invalid amount: {0}")]
     InvalidAmount(#[from] amount::Error),
 }
+
+pub fn serialize_to_string<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+    T: ToString + Display,
+{
+    serializer.serialize_str(&value.to_string())
+}

+ 34 - 1
utxo/src/id.rs

@@ -1,25 +1,36 @@
 use serde::{Deserialize, Deserializer, Serialize};
 use sha2::{Digest, Sha256};
 use std::fmt::Display;
+use std::num::ParseIntError;
 use std::str::FromStr;
 
+use crate::PaymentId;
+
+/// Error
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
+    /// The number of bytes is invalid
     #[error("Invalid length for {0}: {1} (expected: {2})")]
     InvalidLength(String, usize, usize),
 
+    /// The provided prefix on the string ID is unknown
     #[error("Unknown prefix {0}")]
     UnknownPrefix(String),
+
+    #[error("Invalid PaymentId: {0}")]
+    InvalidPaymentId(#[from] ParseIntError),
 }
 
 macro_rules! Id {
     ($id:ident, $suffix:expr) => {
         #[derive(Clone, Debug, Eq, PartialOrd, Ord, Hash, PartialEq)]
+        /// A unique identifier for $id
         pub struct $id {
             bytes: [u8; 32],
         }
 
         impl $id {
+            /// Creates a new instance of $id from the raw bytes
             pub fn new(bytes: [u8; 32]) -> Self {
                 Self { bytes }
             }
@@ -135,16 +146,30 @@ macro_rules! Id {
                 &self.bytes
             }
         }
+
+        impl std::ops::Deref for $id {
+            type Target = [u8; 32];
+            fn deref(&self) -> &Self::Target {
+                &self.bytes
+            }
+        }
     };
 }
 
 Id!(AccountId, "account");
 Id!(TransactionId, "tx");
 
+/// A generic ID wrapper
+///
+/// This enum can be used whenever a human ID is passed and needs to be parsed and validated.
 #[derive(Debug)]
 pub enum AnyId {
+    /// AccountId
     Account(AccountId),
+    /// TransactionId
     Transaction(TransactionId),
+    /// Payment
+    Payment(PaymentId),
 }
 
 impl FromStr for AnyId {
@@ -154,7 +179,15 @@ impl FromStr for AnyId {
         if value.starts_with("account") {
             Ok(Self::Account(value.parse()?))
         } else if value.starts_with("tx") {
-            Ok(Self::Transaction(value.parse()?))
+            if let Some(at) = value.find(":") {
+                let (tx, pos) = value.split_at(at);
+                Ok(Self::Payment(PaymentId {
+                    transaction: tx.parse()?,
+                    position: (&pos[1..]).parse()?,
+                }))
+            } else {
+                Ok(Self::Transaction(value.parse()?))
+            }
         } else {
             Err(Error::UnknownPrefix(value.to_owned()))
         }

+ 33 - 7
utxo/src/ledger.rs

@@ -1,10 +1,11 @@
 use crate::{
     storage::{Batch, Storage},
     transaction::Type,
-    AccountId, Amount, AssetManager, Error, Payment, Status, Transaction, TransactionId,
+    AccountId, Amount, AssetManager, Error, Payment, PaymentId, Status, Transaction, TransactionId,
 };
 use std::{cmp::Ordering, collections::HashMap};
 
+/// The Verax ledger
 #[derive(Clone, Debug)]
 pub struct Ledger<'a, B, S>
 where
@@ -21,6 +22,7 @@ where
     B: Batch<'a>,
     S: Storage<'a, B> + Sync + Send,
 {
+    /// Creates a new ledger instance
     pub fn new(storage: S, asset_manager: AssetManager) -> Self {
         Self {
             storage,
@@ -29,10 +31,12 @@ where
         }
     }
 
+    /// Parses a human amount (float amounts serialized as string to avoid precision loss)
     pub fn parse_amount(&self, asset: &str, amount: &str) -> Result<Amount, Error> {
         Ok(self.asset_manager.human_amount_by_name(asset, amount)?)
     }
 
+    /// Returns a reference to the asset manager
     pub fn asset_manager(&self) -> &AssetManager {
         &self.asset_manager
     }
@@ -52,7 +56,7 @@ where
         let mut to_spend = HashMap::new();
 
         for (account_id, amount) in payments.into_iter() {
-            let id = (account_id, *amount.asset());
+            let id = (account_id, amount.asset().clone());
             if let Some(value) = to_spend.get_mut(&id) {
                 *value += amount.cents();
             } else {
@@ -102,7 +106,7 @@ where
                             // otherwise it would be locked until the main
                             // transaction settles.
                             Status::Settled,
-                            Type::Internal,
+                            Type::Exchange,
                             vec![input],
                             vec![
                                 (account.clone(), asset.new_amount(cents - to_spend_cents)),
@@ -186,6 +190,11 @@ where
         Ok(self.storage.get_balance(account).await?)
     }
 
+    /// Creates an external deposit
+    ///
+    /// Although a deposit can have multiple output payments, to different
+    /// accounts and amounts, to keep the upstream API simple, this function
+    /// only accepts a single account and amount to credit
     pub async fn deposit(
         &'a self,
         account: &AccountId,
@@ -199,6 +208,12 @@ where
         Ok(transaction)
     }
 
+    /// Creates a new withdrawal transaction and returns it.
+    ///
+    /// Although a transaction supports multiple inputs to be burned, from
+    /// different accounts, to keep things simple, this function only supports a
+    /// single input (single account and single amount). This is because the
+    /// natural behaviour is to have withdrawals from a single account.
     pub async fn withdrawal(
         &'a self,
         account: &AccountId,
@@ -217,6 +232,11 @@ where
         Ok(transaction)
     }
 
+    /// Returns the payment object by a given payment id
+    pub async fn get_payment_info(&'a self, payment_id: &PaymentId) -> Result<Payment, Error> {
+        Ok(self.storage.get_payment(payment_id).await?)
+    }
+
     /// Returns the transaction object by a given transaction id
     pub async fn get_transaction(
         &'a self,
@@ -235,11 +255,16 @@ where
     pub async fn get_transactions(
         &'a self,
         account_id: &AccountId,
-        typ: Option<Type>,
+        types: Vec<Type>,
     ) -> Result<Vec<Transaction>, Error> {
+        let types = if types.is_empty() {
+            vec![Type::Transaction, Type::Deposit, Type::Withdrawal]
+        } else {
+            types
+        };
         let r = self
             .storage
-            .get_transactions(account_id, typ)
+            .get_transactions(account_id, types)
             .await?
             .into_iter()
             .map(|x| x.try_into())
@@ -248,19 +273,20 @@ where
         Ok(r)
     }
 
-    /// Attemps to change the status of a given transaction id. On success the
+    /// Attempts to change the status of a given transaction id. On success the
     /// new transaction object is returned, otherwise an error is returned.
     pub async fn change_status(
         &'a self,
         transaction_id: &TransactionId,
         new_status: Status,
+        reason: String,
     ) -> Result<Transaction, Error> {
         let mut tx: Transaction = self
             .storage
             .get_transaction(transaction_id)
             .await?
             .try_into()?;
-        tx.change_status(new_status)?;
+        tx.change_status(new_status, reason)?;
         tx.persist(&self.storage).await?;
         Ok(tx)
     }

+ 27 - 2
utxo/src/lib.rs

@@ -1,6 +1,29 @@
+//! # Verax: A simple ledger database
+//!
+//! It is a simple Ledger database which aims to be a building block for
+//! financial products.
+//!
+//! Verax uses a UTXO model (unspent transaction output), heavily inspired by
+//! Bitcoin. Having UTXO each account balance is isolated from the rest and it's
+//! easy to track the history of each payment. In Verax the terminology is
+//! slighly different, there are unspent payments instead of UTXO.
+//!
+//! The data model is simple, any transaction spends one or many payments. Those
+//! payments cannot be spend again and are marked as spent. The transaction can
+//! be settled or not, if it's not settled it can be rolled back. Each
+//! transaction creates new payments that can be spend in the future by other
+//! transactions.
+//!
+//! Verax aims to be simple, auditable, cryptographically provable and human
+//! auditable friendly.
+
+#![deny(missing_docs)]
+#![allow(warnings)]
+
 mod amount;
 mod asset;
 mod asset_manager;
+mod changelog;
 mod error;
 mod id;
 mod ledger;
@@ -13,6 +36,8 @@ pub mod storage;
 mod tests;
 mod transaction;
 
+#[cfg(test)]
+pub use self::storage::test as storage_test;
 pub use self::{
     amount::Amount,
     asset::Asset,
@@ -22,8 +47,8 @@ pub use self::{
     ledger::Ledger,
     payment::{Payment, PaymentId},
     status::Status,
-    transaction::Transaction,
+    transaction::{Transaction, Type},
 };
 
 #[cfg(any(feature = "sqlite", test))]
-pub use self::sqlite::{Batch, Sqlite};
+pub use self::sqlite::{Batch, SQLite};

+ 76 - 6
utxo/src/payment.rs

@@ -1,12 +1,30 @@
-use crate::{AccountId, Amount, Status, TransactionId};
-use serde::{Serialize, Serializer};
+use crate::{changelog::Changelog, AccountId, Amount, Error, Status, TransactionId};
+use serde::{Deserialize, Serialize, Serializer};
+use std::ops::Deref;
 
 #[derive(Clone, Debug, Eq, Ord, PartialOrd, PartialEq)]
+/// PaymentID
+///
+/// This is the payment ID. The payment ID has two public members, which is
+/// basically the transaction which created this payment and the position in the
+/// transaction.
 pub struct PaymentId {
+    /// Transaction which created this payment
     pub transaction: TransactionId,
+    /// This payment position inside the transaction
     pub position: usize,
 }
 
+impl PaymentId {
+    /// Returns the bytes representation of the PaymentId
+    pub fn bytes(&self) -> [u8; 40] {
+        let mut bytes = [0u8; 40];
+        bytes[0..32].copy_from_slice(self.transaction.deref());
+        bytes[32..40].copy_from_slice(&self.position.to_be_bytes());
+        bytes
+    }
+}
+
 impl Serialize for PaymentId {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
@@ -23,19 +41,71 @@ impl ToString for PaymentId {
     }
 }
 
+#[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Clone)]
+pub struct ChangelogEntry {
+    pub reason: String,
+    pub status: Status,
+    pub spent: Option<SpentInfo>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+pub struct SpentInfo {
+    pub by: TransactionId,
+    pub status: Status,
+}
+
+/// Payment
 #[derive(Clone, Debug, Eq, PartialEq, Serialize)]
 pub struct Payment {
+    /// The payment ID
     pub id: PaymentId,
+    /// Which account can spend this new payment
     pub to: AccountId,
+    /// The amount of the payment
     pub amount: Amount,
-    #[serde(skip)]
+    /// The status of the payment. This status is just the status of identical
+    /// to the status of the transaction which created this payment.
     pub status: Status,
-    #[serde(skip)]
-    pub spent_by: Option<TransactionId>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    /// What is the spending status of the payment. If the payment is not spent it will be None
+    pub spent: Option<SpentInfo>,
+    /// The changelog of this payment
+    pub changelog: Vec<Changelog<ChangelogEntry>>,
 }
 
 impl Payment {
+    /// Returns true if the payment is spendable
+    #[inline]
     pub fn is_spendable(&self) -> bool {
-        self.spent_by.is_none() && self.status == Status::Settled
+        self.spent.is_none() && self.status == Status::Settled
+    }
+
+    /// Returns true if the payment is spendable or was spent by a given transaction
+    pub fn is_spendable_or_was_by(&self, by: &TransactionId) -> bool {
+        if let Some(spent) = self.spent.as_ref() {
+            if &spent.by != by {
+                return false;
+            }
+        }
+        self.status == Status::Settled
+    }
+
+    /// Appends a new changelog taking a snapshot of the current object
+    pub fn new_changelog(&mut self, reference: &str) {
+        let previous_id = if let Some(Ok(previous_id)) = self.changelog.last().map(|x| x.id()) {
+            Some(previous_id)
+        } else {
+            None
+        };
+
+        self.changelog.push(Changelog::new(
+            previous_id,
+            self.id.bytes().to_vec(),
+            ChangelogEntry {
+                status: self.status.clone(),
+                reason: reference.to_owned(),
+                spent: self.spent.clone(),
+            },
+        ));
     }
 }

+ 74 - 17
utxo/src/sqlite/batch.rs

@@ -1,16 +1,23 @@
 use crate::{
+    changelog::Changelog,
     storage::{self, Error},
-    AccountId, Payment, PaymentId, Status, Transaction, TransactionId,
+    transaction, AccountId, Payment, PaymentId, Status, Transaction, TransactionId,
 };
+use serde::{de::DeserializeOwned, Serialize};
 use sqlx::{Row, Sqlite, Transaction as SqlxTransaction};
 use std::marker::PhantomData;
 
+/// Creates a new Batch for SQLite
+///
+/// Batches are a group of updates to the databases, in which all of the are
+/// executed or none. Same concept as a rdbms transaction.
 pub struct Batch<'a> {
     inner: SqlxTransaction<'a, Sqlite>,
     x: PhantomData<&'a ()>,
 }
 
 impl<'a> Batch<'a> {
+    /// Creates a new instance
     pub fn new(inner: SqlxTransaction<'a, Sqlite>) -> Batch<'a> {
         Self {
             inner,
@@ -35,26 +42,74 @@ impl<'a> storage::Batch<'a> for Batch<'a> {
             .map_err(|e| Error::Storage(e.to_string()))
     }
 
-    async fn spend_payment(
+    async fn store_changelogs<T: DeserializeOwned + Serialize + Send + Sync>(
+        &mut self,
+        changelog: &[Changelog<T>],
+    ) -> Result<(), Error> {
+        for change in changelog.iter() {
+            let change_bytes =
+                bincode::serialize(&change.change).map_err(|e| Error::Storage(e.to_string()))?;
+
+            sqlx::query(
+                r#"
+                INSERT INTO "changelog"("id", "previous", "object_id", "change", "created_at")
+                VALUES(?, ?, ?, ?, ?)
+                ON CONFLICT("id")
+                    DO NOTHING
+            "#,
+            )
+            .bind(change.id().map_err(|e| Error::Storage(e.to_string()))?)
+            .bind(&change.previous)
+            .bind(&change.object_id)
+            .bind(change_bytes)
+            .bind(change.updated_at)
+            .execute(&mut *self.inner)
+            .await
+            .map_err(|e| Error::Storage(e.to_string()))?;
+        }
+        Ok(())
+    }
+
+    async fn update_payment(
         &mut self,
         payment_id: &PaymentId,
-        status: Status,
-        transaction_id: &TransactionId,
+        spent_by: &TransactionId,
+        spent_status: Status,
     ) -> Result<(), Error> {
+        let settled: u32 = Status::Settled.into();
+        let spent_by_val = if spent_status.is_rollback() {
+            None
+        } else {
+            Some(spent_by.to_string())
+        };
+        let spent_by_status_val: Option<u32> = if spent_status.is_rollback() {
+            None
+        } else {
+            Some(spent_status.into())
+        };
+
         let result = sqlx::query(
             r#"
-                UPDATE payments SET "spent_by" = ?
-                WHERE "transaction_id" = ? AND "position_id" = ? AND ("spent_by" IS NULL OR "spent_by" = ?)
+                UPDATE 
+                    "payments" 
+                SET 
+                    "spent_by" = ?,
+                    "spent_by_status" = ?
+                WHERE 
+                    "transaction_id" = ?
+                    AND "position_id" = ?
+                    AND "status" = ? 
+                    AND ("spent_by_status" IS NULL OR "spent_by_status" != ?)
+                    AND ("spent_by" = ? OR "spent_by" IS NULL)
             "#,
         )
-        .bind(if status.is_rollback() {
-            None
-        } else {
-            Some(transaction_id.to_string())
-        })
+        .bind(spent_by_val)
+        .bind(spent_by_status_val)
         .bind(payment_id.transaction.to_string())
         .bind(payment_id.position.to_string())
-        .bind(transaction_id.to_string())
+        .bind(settled)
+        .bind(settled)
+        .bind(spent_by.to_string())
         .execute(&mut *self.inner)
         .await
         .map_err(|e| Error::SpendPayment(e.to_string()))?;
@@ -102,8 +157,8 @@ impl<'a> storage::Batch<'a> for Batch<'a> {
     async fn store_new_payment(&mut self, payment: &Payment) -> Result<(), Error> {
         sqlx::query(
                 r#"
-                INSERT INTO payments("transaction_id", "position_id", "to", "cents", "asset_id", "status")
-                VALUES (?, ?, ?, ?, ?, ?)
+                INSERT INTO payments("transaction_id", "position_id", "to", "cents", "asset_id", "status", "spent_by_status")
+                VALUES (?, ?, ?, ?, ?, ?, ?)
                 ON CONFLICT("transaction_id", "position_id")
                     DO UPDATE SET "status" = excluded."status"
             "#,
@@ -114,6 +169,7 @@ impl<'a> storage::Batch<'a> for Batch<'a> {
             .bind(payment.amount.cents().to_string())
             .bind(payment.amount.asset().id)
             .bind::<u32>((&payment.status).into())
+            .bind::<Option<u32>>(None)
             .execute(&mut *self.inner)
             .await
             .map_err(|e| Error::Storage(e.to_string()))?;
@@ -123,16 +179,17 @@ impl<'a> storage::Batch<'a> for Batch<'a> {
     async fn store_transaction(&mut self, transaction: &Transaction) -> Result<(), Error> {
         sqlx::query(
             r#"
-                INSERT INTO "transactions"("transaction_id", "status", "type", "reference", "created_at", "updated_at")
-                VALUES(?, ?, ?, ?, ?, ?)
+                INSERT INTO "transactions"("transaction_id", "status", "type", "reference", "last_version", "created_at", "updated_at")
+                VALUES(?, ?, ?, ?, ?, ?, ?)
                 ON CONFLICT("transaction_id")
-                    DO UPDATE SET "status" = excluded."status", "updated_at" = excluded."updated_at"
+                    DO UPDATE SET "status" = excluded."status", "updated_at" = excluded."updated_at", "last_version" = excluded."last_version"
             "#,
         )
         .bind(transaction.id().to_string())
         .bind::<u32>(transaction.status().into())
         .bind::<u32>(transaction.typ().into())
         .bind(transaction.reference())
+        .bind(transaction.last_version())
         .bind(transaction.created_at())
         .bind(transaction.updated_at())
         .execute(&mut *self.inner)

+ 197 - 56
utxo/src/sqlite/mod.rs

@@ -1,27 +1,33 @@
+//! SQLite storage layer for Verax
 use crate::{
     amount::AmountCents,
     asset::AssetId,
+    changelog::Changelog,
+    payment::{self, SpentInfo},
     storage::{Error, Storage},
-    transaction::{from_db, Type},
+    transaction::{self, from_db, Type},
     AccountId, Amount, Asset, AssetManager, Payment, PaymentId, Status, TransactionId,
 };
 use chrono::NaiveDateTime;
 use futures::TryStreamExt;
-use sqlx::{sqlite::SqliteRow, Executor, Row};
-use std::{collections::HashMap, marker::PhantomData};
+use serde::{de::DeserializeOwned, Serialize};
+use sqlx::{pool::PoolConnection, sqlite::SqliteRow, Executor, Row, Sqlite, SqliteConnection};
+use std::{collections::HashMap, marker::PhantomData, ops::Deref};
 
 mod batch;
 
 pub use batch::Batch;
-pub use sqlx::sqlite::SqlitePoolOptions;
+pub use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
 
-pub struct Sqlite<'a> {
+/// SQLite storage layer for Verax
+pub struct SQLite<'a> {
     db: sqlx::SqlitePool,
     asset_manager: AssetManager,
     _phantom: PhantomData<&'a ()>,
 }
 
-impl<'a> Sqlite<'a> {
+impl<'a> SQLite<'a> {
+    /// Creates a new Verax SQLite storage layer. It takes an instance of the asset_manager
     pub fn new(db: sqlx::SqlitePool, asset_manager: AssetManager) -> Self {
         Self {
             db,
@@ -30,6 +36,7 @@ impl<'a> Sqlite<'a> {
         }
     }
 
+    /// Creates all the tables and indexes that are needed
     pub async fn setup(&self) -> Result<(), sqlx::Error> {
         let mut x = self.db.begin().await?;
         x.execute(
@@ -41,6 +48,7 @@ impl<'a> Sqlite<'a> {
                 "status" INTEGER NOT NULL,
                 "to" VARCHAR(71) NOT NULL,
                 "spent_by" TEXT,
+                "spent_by_status" INTEGER DEFAULT NULL,
                 "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
                 "updated_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
                 PRIMARY KEY ("transaction_id", "position_id")
@@ -51,6 +59,7 @@ impl<'a> Sqlite<'a> {
                 "status" INTEGER NOT NULL,
                 "type" INTEGER NOT NULL,
                 "reference" TEXT NOT NULL,
+                "last_version" BLOB NOT NULL,
                 "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
                 "updated_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
                 PRIMARY KEY ("transaction_id")
@@ -72,6 +81,15 @@ impl<'a> Sqlite<'a> {
                 "updated_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
                 PRIMARY KEY ("transaction_id", "payment_transaction_id", "payment_position_id")
             );
+            CREATE TABLE IF NOT EXISTS "changelog" (
+                "id" BLOB NOT NULL,
+                "object_id" BLOB NOT NULL,
+                "previous" BLOB DEFAULT NULL,
+                "change" BLOB NOT NULL,
+                "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
+                PRIMARY KEY ("id")
+            );
+            CREATE INDEX IF NOT EXISTS "changelog_object_id" ON "changelog" ("object_id", "created_at");
         "#,
         )
         .await
@@ -81,7 +99,46 @@ impl<'a> Sqlite<'a> {
     }
 
     #[inline]
-    fn sql_row_to_payment(&self, row: SqliteRow) -> Result<Payment, Error> {
+    async fn get_changelogs_internal<T: DeserializeOwned + Serialize + Send + Sync>(
+        &self,
+        conn: &mut SqliteConnection,
+        object_id: Vec<u8>,
+    ) -> Result<Vec<Changelog<T>>, Error> {
+        let rows = sqlx::query(
+            r#"SELECT  "previous", "change", "created_at", "id" FROM "changelog" WHERE "object_id" = ? ORDER BY "created_at" ASC"#,
+        ).bind(&object_id).fetch_all(&mut *conn).await.map_err(|e| Error::Storage(e.to_string()))?;
+
+        let mut results = vec![];
+
+        for row in rows.into_iter() {
+            let change: T = bincode::deserialize(
+                &row.try_get::<Vec<u8>, usize>(1)
+                    .map_err(|_| Error::Storage("Invalid change content".to_string()))?,
+            )
+            .map_err(|e| Error::Storage(e.to_string()))?;
+            let created_at = row
+                .try_get::<NaiveDateTime, usize>(2)
+                .map_err(|e| Error::InvalidDate(e.to_string()))?
+                .and_utc();
+
+            results.push(Changelog::new_from_db(
+                row.try_get::<Option<Vec<u8>>, usize>(0)
+                    .map_err(|_| Error::Storage("Invalid previous content".to_string()))?,
+                object_id.clone(),
+                change,
+                created_at,
+            ));
+        }
+
+        Ok(results)
+    }
+
+    #[inline]
+    async fn sql_row_to_payment(
+        &self,
+        conn: &mut SqliteConnection,
+        row: SqliteRow,
+    ) -> Result<Payment, Error> {
         let id = PaymentId {
             transaction: row
                 .try_get::<String, usize>(0)
@@ -101,6 +158,31 @@ impl<'a> Sqlite<'a> {
             .map_err(|_| Error::Storage("Invalid cents".to_string()))?
             .into();
 
+        let changelog = self
+            .get_changelogs_internal::<payment::ChangelogEntry>(conn, id.bytes().to_vec())
+            .await?;
+
+        let spent_by: Option<TransactionId> = row
+            .try_get::<Option<String>, usize>(6)
+            .map_err(|_| Error::Storage("Invalid spent_by".to_string()))?
+            .map(|s| s.as_str().try_into())
+            .transpose()
+            .map_err(|_| Error::Storage("Invalid spent_by".to_string()))?;
+
+        let spent_status = row
+            .try_get::<Option<u32>, usize>(7)
+            .map_err(|_| Error::Storage("Invalid spent_by_status".to_string()))?
+            .map(|status| Status::try_from(status))
+            .transpose()
+            .map_err(|_| Error::Storage("Invalid spent_by_status".to_string()))?;
+
+        if spent_by.is_some() != spent_status.is_some() {
+            panic!("{:?} {:?}", spent_by, spent_status);
+            return Err(Error::Storage(
+                "Invalid spent_by and spent_by_status combination".to_string(),
+            ));
+        }
+
         Ok(Payment {
             id,
             amount: self
@@ -124,18 +206,17 @@ impl<'a> Sqlite<'a> {
                 .map_err(|_| Error::Storage("Invalid `status`".to_string()))?
                 .try_into()
                 .map_err(|_| Error::Storage("Invalid status".to_string()))?,
-            spent_by: row
-                .try_get::<Option<String>, usize>(6)
-                .map_err(|_| Error::Storage("Invalid spent_by".to_string()))?
-                .map(|s| s.as_str().try_into())
-                .transpose()
-                .map_err(|_| Error::Storage("Invalid spent_by".to_string()))?,
+            changelog,
+            spent: spent_by.map(|status| SpentInfo {
+                by: status,
+                status: spent_status.unwrap_or_default(),
+            }),
         })
     }
 }
 
 #[async_trait::async_trait]
-impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
+impl<'a> Storage<'a, Batch<'a>> for SQLite<'a> {
     async fn begin(&'a self) -> Result<Batch<'a>, Error> {
         self.db
             .begin()
@@ -144,7 +225,7 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
             .map_err(|x| Error::Storage(x.to_string()))
     }
 
-    async fn get_payment(&self, id: PaymentId) -> Result<Payment, Error> {
+    async fn get_payment(&self, id: &PaymentId) -> Result<Payment, Error> {
         let mut conn = self
             .db
             .acquire()
@@ -160,7 +241,8 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
                 "p"."cents",
                 "p"."to",
                 "p"."status",
-                "p"."spent_by"
+                "p"."spent_by",
+                "p"."spent_by_status"
             FROM
                 "payments" "p"
             WHERE
@@ -176,7 +258,7 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
         .map_err(|e| Error::Storage(e.to_string()))?
         .ok_or(Error::NotFound)?;
 
-        self.sql_row_to_payment(row)
+        Ok(self.sql_row_to_payment(&mut *conn, row).await?)
     }
 
     async fn get_balance(&self, account: &AccountId) -> Result<Vec<Amount>, Error> {
@@ -249,7 +331,7 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
             .acquire()
             .await
             .map_err(|e| Error::Storage(e.to_string()))?;
-        let mut result = sqlx::query(
+        let results = sqlx::query(
             r#"
             SELECT
                 "p"."transaction_id",
@@ -258,7 +340,8 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
                 "p"."cents",
                 "p"."to",
                 "p"."status",
-                "p"."spent_by"
+                "p"."spent_by",
+                "p"."spent_by_status"
             FROM
                 "payments" as "p"
             WHERE
@@ -269,15 +352,13 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
         .bind(account.to_string())
         .bind(asset.to_string())
         .bind::<u32>(Status::Settled.into())
-        .fetch(&mut *conn);
+        .fetch_all(&mut *conn)
+        .await
+        .map_err(|e| Error::Storage(e.to_string()))?;
 
         let mut to_return = vec![];
-        while let Some(row) = result
-            .try_next()
-            .await
-            .map_err(|e| Error::Storage(e.to_string()))?
-        {
-            let row = self.sql_row_to_payment(row)?;
+        for row in results.into_iter() {
+            let row = self.sql_row_to_payment(&mut *conn, row).await?;
             target_amount -= row.amount.cents();
             to_return.push(row);
             if target_amount <= 0 {
@@ -308,6 +389,7 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
                 "t"."status",
                 "t"."type",
                 "t"."reference",
+                "t"."last_version",
                 "t"."created_at",
                 "t"."updated_at"
             FROM
@@ -322,7 +404,7 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
         .map_err(|e| Error::Storage(e.to_string()))?
         .ok_or(Error::NotFound)?;
 
-        let mut spend_result = sqlx::query(
+        let results = sqlx::query(
             r#"
             SELECT
                 "p"."transaction_id",
@@ -331,7 +413,8 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
                 "p"."cents",
                 "p"."to",
                 "p"."status",
-                "p"."spent_by"
+                "p"."spent_by",
+                "p"."spent_by_status"
             FROM
                 "transaction_input_payments" "tp"
             INNER JOIN
@@ -345,21 +428,16 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
             "#,
         )
         .bind(transaction_id.to_string())
-        .fetch(&mut *conn);
+        .fetch_all(&mut *conn)
+        .await
+        .map_err(|e| Error::Storage(e.to_string()))?;
 
         let mut spend = vec![];
-
-        while let Some(row) = spend_result
-            .try_next()
-            .await
-            .map_err(|e| Error::Storage(e.to_string()))?
-        {
-            spend.push(self.sql_row_to_payment(row)?);
+        for row in results.into_iter() {
+            spend.push(self.sql_row_to_payment(&mut *conn, row).await?);
         }
 
-        drop(spend_result);
-
-        let mut create_result = sqlx::query(
+        let results = sqlx::query(
             r#"
             SELECT
                 "p"."transaction_id",
@@ -368,7 +446,8 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
                 "p"."cents",
                 "p"."to",
                 "p"."status",
-                "p"."spent_by"
+                "p"."spent_by",
+                "p"."spent_by_status"
             FROM
                 "payments" "p"
             WHERE
@@ -376,16 +455,14 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
             "#,
         )
         .bind(transaction_id.to_string())
-        .fetch(&mut *conn);
+        .fetch_all(&mut *conn)
+        .await
+        .map_err(|e| Error::Storage(e.to_string()))?;
 
         let mut create = vec![];
 
-        while let Some(row) = create_result
-            .try_next()
-            .await
-            .map_err(|e| Error::Storage(e.to_string()))?
-        {
-            create.push(self.sql_row_to_payment(row)?);
+        for row in results.into_iter() {
+            create.push(self.sql_row_to_payment(&mut *conn, row).await?);
         }
 
         let status = transaction_row
@@ -403,21 +480,34 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
             .try_get::<String, usize>(2)
             .map_err(|_| Error::Storage("Invalid reference".to_string()))?;
 
+        let last_change = transaction_row
+            .try_get::<Vec<u8>, usize>(3)
+            .map_err(|_| Error::Storage("Invalid last_version".to_string()))?;
+
         let created_at = transaction_row
-            .try_get::<NaiveDateTime, usize>(3)
+            .try_get::<NaiveDateTime, usize>(4)
             .map_err(|e| Error::InvalidDate(e.to_string()))?
             .and_utc();
 
         let updated_at = transaction_row
-            .try_get::<NaiveDateTime, usize>(4)
+            .try_get::<NaiveDateTime, usize>(5)
             .map_err(|e| Error::InvalidDate(e.to_string()))?
             .and_utc();
 
+        let changelog = self
+            .get_changelogs_internal::<transaction::ChangelogEntry>(
+                &mut *conn,
+                transaction_id.deref().to_vec(),
+            )
+            .await?;
+
         Ok(from_db::Transaction {
             id: transaction_id.clone(),
             spend,
             create,
             typ,
+            last_change,
+            changelog,
             status,
             reference,
             created_at,
@@ -428,7 +518,7 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
     async fn get_transactions(
         &self,
         account: &AccountId,
-        typ: Option<Type>,
+        types: Vec<Type>,
     ) -> Result<Vec<from_db::Transaction>, Error> {
         let mut conn = self
             .db
@@ -436,14 +526,24 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
             .await
             .map_err(|e| Error::Storage(e.to_string()))?;
 
-        let sql = sqlx::query(if typ.is_some() {
-            r#"SELECT "transaction_id" FROM "transaction_accounts" WHERE "account_id" = ? AND "type" = ?" ORDER BY "created_at" DESC"#
+        let sql = if types.is_empty() {
+            r#"SELECT "transaction_id" FROM "transaction_accounts" WHERE "account_id" = ? ORDER BY "created_at" DESC"#.to_owned()
         } else {
-            r#"SELECT "transaction_id" FROM "transaction_accounts" WHERE "account_id" = ? ORDER BY "created_at" DESC"#
-        }).bind(account.to_string());
+            let params = format!("?{}", ", ?".repeat(types.len() - 1));
+            format!(
+                r#"SELECT "transaction_id" FROM "transaction_accounts" WHERE "account_id" = ? AND "type" IN ({}) ORDER BY "created_at" DESC"#,
+                params
+            )
+        };
 
-        let ids = if let Some(typ) = typ {
-            sql.bind::<u32>(typ.into())
+        let sql = sqlx::query(&sql).bind(account.to_string());
+
+        let ids = if !types.is_empty() {
+            let mut sql = sql;
+            for typ in types.into_iter() {
+                sql = sql.bind::<u32>(typ.into());
+            }
+            sql
         } else {
             sql
         }
@@ -471,4 +571,45 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
 
         Ok(transactions)
     }
+
+    async fn get_changelogs<T: DeserializeOwned + Serialize + Send + Sync>(
+        &self,
+        object_id: Vec<u8>,
+    ) -> Result<Vec<Changelog<T>>, Error> {
+        let mut conn = self
+            .db
+            .acquire()
+            .await
+            .map_err(|e| Error::Storage(e.to_string()))?;
+
+        self.get_changelogs_internal(&mut *conn, object_id).await
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::{storage_test_suite, AssetDefinition};
+    use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
+    use std::{fs::remove_file, path::Path};
+
+    async fn get_instance(assets: AssetManager, test: &str) -> SQLite<'static> {
+        let path = format!("/tmp/{}.db", test);
+        let _ = remove_file(&path);
+        let settings = path
+            .parse::<SqliteConnectOptions>()
+            .expect("valid settings")
+            .create_if_missing(true);
+
+        let pool = SqlitePoolOptions::new()
+            .connect_with(settings)
+            .await
+            .expect("pool");
+
+        let db = SQLite::new(pool, assets);
+        db.setup().await.expect("setup");
+        db
+    }
+
+    storage_test_suite!();
 }

+ 2 - 2
utxo/src/status.rs

@@ -1,8 +1,8 @@
 use serde::{Deserialize, Serialize};
-use strum_macros::Display;
+use strum_macros::{Display, EnumIter};
 
 /// Transaction status
-#[derive(Clone, Eq, PartialEq, Debug, Display, Serialize, Deserialize)]
+#[derive(Clone, Eq, PartialEq, Debug, Display, Serialize, EnumIter, Deserialize)]
 #[serde(rename_all = "snake_case")]
 pub enum Status {
     /// Pending status

+ 346 - 8
utxo/src/storage.rs

@@ -1,55 +1,99 @@
+//! Storage layer trait
 use crate::{
     amount::AmountCents,
     asset::AssetId,
+    changelog::{self, Changelog},
     transaction::{from_db, Type},
     AccountId, Amount, Payment, PaymentId, Status, Transaction, TransactionId,
 };
-use serde::Serialize;
+use serde::{de::DeserializeOwned, Serialize};
 
 #[derive(thiserror::Error, Debug, Serialize)]
+/// Storage error
 pub enum Error {
     #[error("Storage error: {0}")]
+    /// Any error related to the storage layer, with string
     Storage(String),
 
     #[error("Invalid date format: {0}")]
+    /// The date format is invalid
     InvalidDate(String),
 
     #[error("Spend payment: {0}")]
+    /// The requested payment has been spent already
     SpendPayment(String),
 
     #[error("No storage update when expecting")]
+    /// No update was performed when expecting one
     NoUpdate,
 
     #[error("Record not found")]
+    /// The requested record was not found
     NotFound,
 
-    /// TODO: Convert the AmountCents to Amount for better error reporting upstream
+    #[error("Error while parsing changelog: {0}")]
+    /// Error while parsing changelog
+    Changelog(#[from] changelog::Error),
+
     #[error("Not enough unspent payments (missing {0} cents)")]
+    /// TODO: Convert the AmountCents to Amount for better error reporting upstream
     NotEnoughUnspentPayments(AmountCents),
 }
 
 #[async_trait::async_trait]
+/// Batches
+///
+/// Batches are a group of updates to the databases, in which all of the are
+/// executed or none. Same concept as a rdbms transaction.
 pub trait Batch<'a> {
+    /// The current batch is discarded
     async fn rollback(self) -> Result<(), Error>;
 
+    /// The batch is commited into the storage layer and it is consumed.
     async fn commit(self) -> Result<(), Error>;
 
-    async fn spend_payment(
+    /// Stores a changelogs event
+    async fn store_changelogs<T: DeserializeOwned + Serialize + Send + Sync>(
+        &mut self,
+        changelog: &[Changelog<T>],
+    ) -> Result<(), Error>;
+
+    /// Spends a payment.
+    ///
+    /// Under normal circumstances the transaction_id is required, and the
+    /// payment is marked as spent.
+    ///
+    /// If the transaction_id is None, the payment is released and can be spent
+    /// again. This will only happen when the transaction that spent this
+    /// payment was either cancelled or failed. The original transaction will
+    /// exists forever either as failed or cancelled and will reference the
+    /// original payment, but the underlying payment that was just released will
+    /// become spendable again
+    async fn update_payment(
         &mut self,
         payment_id: &PaymentId,
-        status: Status,
-        transaction_id: &TransactionId,
+        spent_by: &TransactionId,
+        spent_status: Status,
     ) -> Result<(), Error>;
 
+    /// Returns the stats of a payment from the point of view of the on-going transaction
     async fn get_payment_status(
         &mut self,
         transaction_id: &TransactionId,
     ) -> Result<Option<Status>, Error>;
 
+    /// Stores a payment (that was created by a transaction)
     async fn store_new_payment(&mut self, payment: &Payment) -> Result<(), Error>;
 
+    /// Stores a transaction
     async fn store_transaction(&mut self, transaction: &Transaction) -> Result<(), Error>;
 
+    /// Creates a relationship between an account and a transaction.
+    ///
+    /// This is a chance to build an index which relates accounts to
+    /// transactions (spending or receiving accounts). This information is used
+    /// to query ledger information per account and to enhance any future cache
+    /// layer.
     async fn relate_account_to_transaction(
         &mut self,
         transaction: &Transaction,
@@ -58,6 +102,7 @@ pub trait Batch<'a> {
 }
 
 #[async_trait::async_trait]
+/// Main storage layer
 pub trait Storage<'a, B>
 where
     B: Batch<'a>,
@@ -74,14 +119,15 @@ where
     async fn begin(&'a self) -> Result<B, Error>;
 
     /// Returns a payment object by ID.
-    async fn get_payment(&self, id: PaymentId) -> Result<Payment, Error>;
+    async fn get_payment(&self, id: &PaymentId) -> Result<Payment, Error>;
 
     /// Similar to get_payment() but errors if the requested payment is not spendable.
-    async fn get_unspent_payment(&self, id: PaymentId) -> Result<Payment, Error> {
+    async fn get_unspent_payment(&self, id: &PaymentId) -> Result<Payment, Error> {
         let payment = self.get_payment(id).await?;
         if payment.is_spendable() {
             Ok(payment)
         } else {
+            println!("{:?}", payment);
             Err(Error::NotFound)
         }
     }
@@ -115,6 +161,298 @@ where
     async fn get_transactions(
         &self,
         account: &AccountId,
-        typ: Option<Type>,
+        types: Vec<Type>,
     ) -> Result<Vec<from_db::Transaction>, Error>;
+
+    /// Returns all changelogs associated with a given object_id. The result should be sorted from oldest to newest.
+    async fn get_changelogs<T: DeserializeOwned + Serialize + Send + Sync>(
+        &self,
+        object_id: Vec<u8>,
+    ) -> Result<Vec<Changelog<T>>, Error>;
+}
+
+#[cfg(test)]
+pub mod test {
+    use super::*;
+    use crate::{payment::SpentInfo, AssetDefinition, AssetManager};
+    use rand::Rng;
+    use strum::IntoEnumIterator;
+
+    #[macro_export]
+    macro_rules! storage_unit_test {
+        ($name:ident) => {
+            #[tokio::test]
+            async fn $name() {
+                let assets = AssetManager::new(vec![
+                    AssetDefinition::new(1, "BTC", 8),
+                    AssetDefinition::new(2, "USD", 4),
+                ]);
+                let ledger = get_instance(assets.clone(), stringify!($name)).await;
+                $crate::storage::test::$name(&ledger, assets).await;
+            }
+        };
+    }
+
+    #[macro_export]
+    macro_rules! storage_test_suite {
+        () => {
+            $crate::storage_unit_test!(transaction);
+            $crate::storage_unit_test!(transaction_not_available_until_commit);
+            $crate::storage_unit_test!(sorted_unspent_payments);
+            $crate::storage_unit_test!(does_not_update_spent_payments);
+            $crate::storage_unit_test!(does_not_spend_unspendable_payments);
+            $crate::storage_unit_test!(relate_account_to_transaction);
+        };
+    }
+
+    pub async fn transaction<'a, T, B>(storage: &'a T, assets: AssetManager)
+    where
+        T: Storage<'a, B>,
+        B: Batch<'a>,
+    {
+        let deposit = Transaction::new_external_deposit(
+            "test reference".to_owned(),
+            Status::Settled,
+            vec![(
+                "alice".parse().expect("account"),
+                assets
+                    .human_amount_by_name("USD", "100.99")
+                    .expect("valid amount"),
+            )],
+        )
+        .expect("valid tx");
+        let mut storing = storage.begin().await.expect("valid tx");
+        storing.store_transaction(&deposit).await;
+        storing.rollback().await;
+        assert!(storage.get_transaction(&deposit.id()).await.is_err());
+
+        let mut storing = storage.begin().await.expect("valid tx");
+        storing.store_transaction(&deposit).await;
+        storing.commit().await;
+        assert!(storage.get_transaction(&deposit.id()).await.is_ok());
+    }
+
+    pub async fn transaction_not_available_until_commit<'a, T, B>(
+        storage: &'a T,
+        assets: AssetManager,
+    ) where
+        T: Storage<'a, B>,
+        B: Batch<'a>,
+    {
+        let deposit = Transaction::new_external_deposit(
+            "test reference".to_owned(),
+            Status::Settled,
+            vec![(
+                "alice".parse().expect("account"),
+                assets
+                    .human_amount_by_name("USD", "100.99")
+                    .expect("valid amount"),
+            )],
+        )
+        .expect("valid tx");
+        let mut storing = storage.begin().await.expect("valid tx");
+        storing.store_transaction(&deposit).await;
+        assert!(storage.get_transaction(&deposit.id()).await.is_err());
+        storing.rollback().await;
+        assert!(storage.get_transaction(&deposit.id()).await.is_err());
+
+        let mut storing = storage.begin().await.expect("valid tx");
+        storing.store_transaction(&deposit).await;
+        storing.commit().await;
+        assert!(storage.get_transaction(&deposit.id()).await.is_ok());
+    }
+
+    pub async fn does_not_update_spent_payments<'a, T, B>(storage: &'a T, assets: AssetManager)
+    where
+        T: Storage<'a, B>,
+        B: Batch<'a>,
+    {
+        let mut writer = storage.begin().await.expect("writer");
+        let mut rng = rand::thread_rng();
+        let account = "account0".parse::<AccountId>().expect("account");
+        let amount = assets
+            .human_amount_by_name("USD", &format!("{}", rng.gen_range(-1000.0..1000.0)))
+            .expect("valid amount");
+
+        let payment_id = PaymentId {
+            transaction: vec![0u8; 32].try_into().expect("valid tx id"),
+            position: 0,
+        };
+
+        writer
+            .store_new_payment(&Payment {
+                id: payment_id.clone(),
+                to: account.clone(),
+                amount,
+                status: Status::Settled,
+                changelog: vec![],
+                spent: None,
+            })
+            .await
+            .expect("valid payment");
+
+        // Alter state
+        assert!(writer
+            .update_payment(&payment_id, &payment_id.transaction, Status::Processing)
+            .await
+            .is_ok());
+
+        // Alter state, again
+        assert!(writer
+            .update_payment(&payment_id, &payment_id.transaction, Status::Cancelled)
+            .await
+            .is_ok());
+
+        // Payment is forever used
+        assert!(writer
+            .update_payment(&payment_id, &payment_id.transaction, Status::Settled)
+            .await
+            .is_ok());
+
+        for status in Status::iter() {
+            assert!(
+                writer
+                    .update_payment(&payment_id, &payment_id.transaction, status.clone())
+                    .await
+                    .is_err(),
+                "status: {:?}",
+                status
+            );
+        }
+    }
+
+    pub async fn does_not_spend_unspendable_payments<'a, T, B>(storage: &'a T, assets: AssetManager)
+    where
+        T: Storage<'a, B>,
+        B: Batch<'a>,
+    {
+        let mut writer = storage.begin().await.expect("writer");
+        let mut rng = rand::thread_rng();
+        let account = "account0".parse::<AccountId>().expect("account");
+        let amount = assets
+            .human_amount_by_name("USD", &format!("{}", rng.gen_range(-1000.0..1000.0)))
+            .expect("valid amount");
+
+        for status in Status::iter() {
+            if status == Status::Settled {
+                continue;
+            }
+            let status_id: u32 = status.clone().into();
+            let payment_id = PaymentId {
+                transaction: vec![status_id as u8; 32].try_into().expect("valid tx id"),
+                position: 0,
+            };
+
+            writer
+                .store_new_payment(&Payment {
+                    id: payment_id.clone(),
+                    to: account.clone(),
+                    amount: amount.clone(),
+                    status,
+                    changelog: vec![],
+                    spent: Some(SpentInfo {
+                        by: payment_id.transaction.clone(),
+                        status: Status::Settled,
+                    }),
+                })
+                .await
+                .expect("valid payment");
+            for status in Status::iter() {
+                if status != Status::Settled {
+                    assert!(writer
+                        .update_payment(&payment_id, &payment_id.transaction, Status::Pending)
+                        .await
+                        .is_err());
+                }
+            }
+        }
+    }
+
+    pub async fn sorted_unspent_payments<'a, T, B>(storage: &'a T, assets: AssetManager)
+    where
+        T: Storage<'a, B>,
+        B: Batch<'a>,
+    {
+        let mut writer = storage.begin().await.expect("writer");
+        let accounts: Vec<AccountId> = (0..10)
+            .map(|i| format!("account{}", i).parse().expect("account"))
+            .collect();
+
+        let mut rng = rand::thread_rng();
+        let mut position = 0;
+        let target_inputs_per_account = 20;
+
+        for account in &accounts {
+            for i in 0..target_inputs_per_account {
+                let amount = assets
+                    .human_amount_by_name("USD", &format!("{}", rng.gen_range(-1000.0..1000.0)))
+                    .expect("valid amount");
+                writer
+                    .store_new_payment(&Payment {
+                        id: PaymentId {
+                            transaction: vec![0u8; 32].try_into().expect("valid tx id"),
+                            position,
+                        },
+                        to: account.clone(),
+                        amount,
+                        changelog: vec![],
+                        status: Status::Settled,
+                        spent: None,
+                    })
+                    .await
+                    .expect("valid payment");
+                position += 1;
+            }
+        }
+        writer.commit().await.expect("valid commit");
+        let mut at_least_one_negative_amount = false;
+        for account in &accounts {
+            let balance = &storage.get_balance(&account).await.expect("valid balance")[0];
+            if balance.cents() < 0 {
+                continue;
+            }
+            let all_unspent_amounts = storage
+                .get_unspent_payments(account, balance.asset().id, balance.cents())
+                .await
+                .expect("valid unspent payments")
+                .into_iter()
+                .map(|x| x.amount.cents())
+                .collect::<Vec<_>>();
+
+            let mut sorted = all_unspent_amounts.clone();
+            sorted.sort();
+
+            assert_eq!(target_inputs_per_account, all_unspent_amounts.len());
+            assert_eq!(sorted, all_unspent_amounts);
+            if !at_least_one_negative_amount {
+                at_least_one_negative_amount = sorted[0] < 0;
+            }
+        }
+        assert!(at_least_one_negative_amount);
+    }
+
+    pub async fn relate_account_to_transaction<'a, T, B>(storage: &'a T, assets: AssetManager)
+    where
+        T: Storage<'a, B>,
+        B: Batch<'a>,
+    {
+        let account: AccountId = "alice".parse().expect("account");
+        let deposit = Transaction::new_external_deposit(
+            "test reference".to_owned(),
+            Status::Settled,
+            vec![(
+                account.clone(),
+                assets
+                    .human_amount_by_name("USD", "100.99")
+                    .expect("valid amount"),
+            )],
+        )
+        .expect("valid tx");
+        let mut storing = storage.begin().await.expect("valid tx");
+        storing.store_transaction(&deposit).await;
+        storing
+            .relate_account_to_transaction(&deposit, &account)
+            .await
+            .expect("relationship");
+    }
 }

+ 6 - 6
utxo/src/tests/deposit.rs

@@ -24,7 +24,7 @@ async fn pending_deposit_and_failure() {
         .is_empty());
 
     ledger
-        .change_status(&id, Status::Failed)
+        .change_status(&id, Status::Failed, "failed due test".to_owned())
         .await
         .expect("valid tx");
 
@@ -153,7 +153,7 @@ async fn balance_decreases_while_pending_spending_and_confirm() {
     assert!(ledger.get_balance(&fee).await.expect("balance").is_empty());
 
     ledger
-        .change_status(&id, Status::Settled)
+        .change_status(&id, Status::Settled, "ready".to_owned())
         .await
         .expect("valid tx");
 
@@ -228,7 +228,7 @@ async fn balance_decreases_while_pending_spending_and_cancel() {
     assert!(ledger.get_balance(&fee).await.expect("balance").is_empty());
 
     ledger
-        .change_status(&id, Status::Cancelled)
+        .change_status(&id, Status::Cancelled, "cancelled by test".to_owned())
         .await
         .expect("valid tx");
 
@@ -297,7 +297,7 @@ async fn balance_decreases_while_pending_spending_and_failed() {
     assert!(ledger.get_balance(&fee).await.expect("balance").is_empty());
 
     ledger
-        .change_status(&tx, Status::Processing)
+        .change_status(&tx, Status::Processing, "processing now".to_owned())
         .await
         .expect("valid tx");
 
@@ -311,7 +311,7 @@ async fn balance_decreases_while_pending_spending_and_failed() {
     assert_eq!(
         "Transaction: Status transition from Processing to Cancelled is not allowed".to_owned(),
         ledger
-            .change_status(&tx, Status::Cancelled)
+            .change_status(&tx, Status::Cancelled, "cancelled by user".to_owned())
             .await
             .unwrap_err()
             .to_string()
@@ -325,7 +325,7 @@ async fn balance_decreases_while_pending_spending_and_failed() {
     assert!(ledger.get_balance(&fee).await.expect("balance").is_empty());
 
     ledger
-        .change_status(&tx, Status::Failed)
+        .change_status(&tx, Status::Failed, "it has failed".to_owned())
         .await
         .expect("valid");
 

+ 5 - 5
utxo/src/tests/mod.rs

@@ -1,13 +1,13 @@
 use crate::{
     asset_manager::AssetDefinition,
-    sqlite::{Batch, Sqlite},
+    sqlite::{Batch, SQLite},
     AccountId, Amount, AssetManager, Error, Ledger, Status, TransactionId,
 };
 use sqlx::sqlite::SqlitePoolOptions;
 
 pub async fn get_instance() -> (
     AssetManager,
-    Ledger<'static, Batch<'static>, Sqlite<'static>>,
+    Ledger<'static, Batch<'static>, SQLite<'static>>,
 ) {
     let pool = SqlitePoolOptions::new()
         .max_connections(1)
@@ -23,13 +23,13 @@ pub async fn get_instance() -> (
         AssetDefinition::new(2, "USD", 4),
     ]);
 
-    let db = Sqlite::new(pool, assets.clone());
+    let db = SQLite::new(pool, assets.clone());
     db.setup().await.expect("setup");
     (assets.clone(), Ledger::new(db, assets))
 }
 
 pub async fn withdrawal(
-    ledger: &Ledger<'static, Batch<'static>, Sqlite<'static>>,
+    ledger: &Ledger<'static, Batch<'static>, SQLite<'static>>,
     account_id: &AccountId,
     status: Status,
     amount: Amount,
@@ -42,7 +42,7 @@ pub async fn withdrawal(
 }
 
 pub async fn deposit(
-    ledger: &Ledger<'static, Batch<'static>, Sqlite<'static>>,
+    ledger: &Ledger<'static, Batch<'static>, SQLite<'static>>,
     account_id: &AccountId,
     amount: Amount,
 ) -> TransactionId {

+ 1 - 1
utxo/src/tests/withdrawal.rs

@@ -252,7 +252,7 @@ async fn cancelled_withdrawal() {
     );
 
     ledger
-        .change_status(&tx_id, Status::Cancelled)
+        .change_status(&tx_id, Status::Cancelled, "cancelled by test".to_owned())
         .await
         .expect("valid tx");
 

+ 8 - 0
utxo/src/transaction/changelog.rs

@@ -0,0 +1,8 @@
+use crate::Status;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct ChangelogEntry {
+    pub status: Status,
+    pub reason: String,
+}

+ 5 - 12
utxo/src/transaction/error.rs

@@ -1,7 +1,5 @@
-use std::fmt::Display;
-
 use crate::{storage, Amount, Asset, Status, TransactionId};
-use serde::{Serialize, Serializer};
+use serde::Serialize;
 
 #[derive(thiserror::Error, Debug, Serialize)]
 pub enum Error {
@@ -33,20 +31,15 @@ pub enum Error {
     Storage(#[from] storage::Error),
 
     #[error("Internal error at serializing: {0}")]
-    #[serde(serialize_with = "serialize_to_string")]
+    #[serde(serialize_with = "crate::error::serialize_to_string")]
     Internal(#[from] Box<bincode::ErrorKind>),
 
+    #[error("Invalid changelog: {0}")]
+    Changelog(#[from] crate::changelog::Error),
+
     #[error("Invalid calculated id {0} (expected {1})")]
     InvalidTransactionId(TransactionId, TransactionId),
 
     #[error("Overflow")]
     Overflow,
 }
-
-fn serialize_to_string<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
-where
-    S: Serializer,
-    T: ToString + Display,
-{
-    serializer.serialize_str(&value.to_string())
-}

+ 4 - 2
utxo/src/transaction/from_db.rs

@@ -1,5 +1,5 @@
-use super::Type;
-use crate::{Payment, Status, TransactionId};
+use super::{ChangelogEntry, Type};
+use crate::{changelog::Changelog, Payment, Status, TransactionId};
 use chrono::{DateTime, Utc};
 
 pub struct Transaction {
@@ -9,6 +9,8 @@ pub struct Transaction {
     pub reference: String,
     pub typ: Type,
     pub status: Status,
+    pub last_change: Vec<u8>,
+    pub changelog: Vec<Changelog<ChangelogEntry>>,
     pub created_at: DateTime<Utc>,
     pub updated_at: DateTime<Utc>,
 }

+ 181 - 65
utxo/src/transaction/inner.rs

@@ -1,5 +1,7 @@
 use crate::{
     amount::AmountCents,
+    changelog::{sort_changes, Changelog},
+    payment::{self, SpentInfo},
     storage::{Batch, Storage},
     transaction::*,
     AccountId, Amount, Asset, Payment, PaymentId, Status, TransactionId,
@@ -7,7 +9,7 @@ use crate::{
 use chrono::{serde::ts_milliseconds, DateTime, Utc};
 use serde::Serialize;
 use sha2::{Digest, Sha256};
-use std::collections::HashMap;
+use std::{collections::HashMap, ops::Deref};
 
 /// Transactions
 ///
@@ -47,6 +49,7 @@ pub struct Transaction {
     typ: Type,
     #[serde(skip_serializing_if = "Vec::is_empty")]
     creates: Vec<Payment>,
+    changelog: Vec<Changelog<ChangelogEntry>>,
     status: Status,
     #[serde(with = "ts_milliseconds")]
     created_at: DateTime<Utc>,
@@ -55,79 +58,126 @@ pub struct Transaction {
 }
 
 impl Transaction {
-    pub fn new_external_withdrawal(
+    /// 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,
-        spend: Vec<Payment>,
+        pay_to: Vec<(AccountId, Amount)>,
     ) -> Result<Transaction, Error> {
         let created_at = Utc::now();
-        let id = Self::calculate_hash(
+        let id = Self::calculate_id(
             &reference,
-            spend.iter().map(|t| &t.id).collect::<Vec<&PaymentId>>(),
             vec![],
+            pay_to
+                .iter()
+                .map(|t| (&t.0, &t.1))
+                .collect::<Vec<(&AccountId, &Amount)>>(),
             created_at,
         )?;
-        let spend = spend
+        let create = pay_to
             .into_iter()
-            .map(|mut input| {
-                input.spent_by = Some(id.clone());
-                input
+            .enumerate()
+            .map(|(position, (to, amount))| {
+                let id = crate::PaymentId {
+                    transaction: id.clone(),
+                    position,
+                };
+                let id_bytes = id.bytes().to_vec();
+                Payment {
+                    id,
+                    to,
+                    amount,
+                    spent: None,
+                    changelog: vec![Changelog::new(
+                        None,
+                        id_bytes,
+                        payment::ChangelogEntry {
+                            status: status.clone(),
+                            reason: reference.clone(),
+                            spent: None,
+                        },
+                    )],
+                    status: status.clone(),
+                }
             })
             .collect();
+
+        let changelog: Vec<Changelog<changelog::ChangelogEntry>> = vec![Changelog::new(
+            None,
+            id.deref().to_vec(),
+            ChangelogEntry {
+                status: status.clone(),
+                reason: reference.clone(),
+            },
+        )];
+
         Ok(Self {
             id,
-            spends: spend,
-            creates: vec![],
-            typ: Type::Withdrawal,
+            spends: vec![],
+            creates: create,
             reference,
+            typ: Type::Deposit,
             status,
+            changelog,
             created_at,
             updated_at: Utc::now(),
         })
     }
 
-    pub fn new_external_deposit(
+    /// Creates a new external withdrawal transaction
+    ///
+    /// Burns assets to reflect external withdrawals
+    pub fn new_external_withdrawal(
         reference: String,
         status: Status,
-        pay_to: Vec<(AccountId, Amount)>,
+        spend: Vec<Payment>,
     ) -> Result<Transaction, Error> {
         let created_at = Utc::now();
-        let id = Self::calculate_hash(
+        let id = Self::calculate_id(
             &reference,
+            spend.iter().map(|t| &t.id).collect::<Vec<&PaymentId>>(),
             vec![],
-            pay_to
-                .iter()
-                .map(|t| (&t.0, &t.1))
-                .collect::<Vec<(&AccountId, &Amount)>>(),
             created_at,
         )?;
-        let create = pay_to
+        let spend = spend
             .into_iter()
-            .enumerate()
-            .map(|(position, (to, amount))| Payment {
-                id: crate::PaymentId {
-                    transaction: id.clone(),
-                    position,
-                },
-                to,
-                amount,
-                spent_by: None,
-                status: status.clone(),
+            .map(|mut payment| {
+                payment.spent = Some(SpentInfo {
+                    by: id.clone(),
+                    status: status.clone(),
+                });
+                payment.new_changelog(&reference);
+                payment
             })
             .collect();
 
+        let changelog: Vec<Changelog<changelog::ChangelogEntry>> = vec![Changelog::new(
+            None,
+            id.deref().to_vec(),
+            ChangelogEntry {
+                status: status.clone(),
+                reason: reference.clone(),
+            },
+        )];
+
         Ok(Self {
             id,
-            spends: vec![],
-            creates: create,
+            spends: spend,
+            creates: vec![],
+            typ: Type::Withdrawal,
             reference,
-            typ: Type::Deposit,
             status,
+            changelog,
             created_at,
             updated_at: Utc::now(),
         })
     }
 
+    /// Creates a new transaction
     pub async fn new(
         reference: String,
         status: Status,
@@ -136,7 +186,7 @@ impl Transaction {
         pay_to: Vec<(AccountId, Amount)>,
     ) -> Result<Transaction, Error> {
         let created_at = Utc::now();
-        let id = Self::calculate_hash(
+        let id = Self::calculate_id(
             &reference,
             spend.iter().map(|t| &t.id).collect::<Vec<&PaymentId>>(),
             pay_to
@@ -147,46 +197,65 @@ impl Transaction {
         )?;
 
         for (i, input) in spend.iter().enumerate() {
-            if input.spent_by.as_ref() != Some(&id) && !input.is_spendable() {
+            if !input.is_spendable_or_was_by(&id) {
                 return Err(Error::InvalidPaymentStatus(i, input.status.clone()));
             }
         }
         let spend = spend
             .into_iter()
-            .map(|mut input| {
-                input.spent_by = Some(id.clone());
-                input
+            .map(|mut payment| {
+                payment.spent = Some(SpentInfo {
+                    by: id.clone(),
+                    status: status.clone(),
+                });
+                payment.new_changelog(&reference);
+                payment
             })
             .collect();
 
         let create = pay_to
             .into_iter()
             .enumerate()
-            .map(|(position, (to, amount))| Payment {
-                id: crate::PaymentId {
-                    transaction: id.clone(),
-                    position,
-                },
-                to,
-                amount,
-                spent_by: None,
-                status: status.clone(),
+            .map(|(position, (to, amount))| {
+                let mut payment = Payment {
+                    id: crate::PaymentId {
+                        transaction: id.clone(),
+                        position,
+                    },
+                    to,
+                    amount,
+                    spent: None,
+                    changelog: vec![],
+                    status: status.clone(),
+                };
+                payment.new_changelog(&reference);
+                payment
             })
             .collect();
 
+        let changelog: Vec<Changelog<changelog::ChangelogEntry>> = vec![Changelog::new(
+            None,
+            id.deref().to_vec(),
+            ChangelogEntry {
+                status: status.clone(),
+                reason: reference.clone(),
+            },
+        )];
+
         Ok(Self {
-            id,
+            id: id,
             reference,
             spends: spend,
             typ,
             creates: create,
             status,
+            changelog,
             created_at,
             updated_at: Utc::now(),
         })
     }
 
-    fn calculate_hash(
+    fn calculate_id(
         reference: &str,
         spend: Vec<&PaymentId>,
         create: Vec<(&AccountId, &Amount)>,
@@ -209,27 +278,48 @@ impl Transaction {
         Ok(TransactionId::new(hasher.finalize().into()))
     }
 
-    pub async fn settle<'a, B, S>(&mut self, storage: &'a S) -> Result<(), Error>
+    /// Settles the current transaction
+    ///
+    /// This is equivalent to changes the status to Settle and to persist
+    pub async fn settle<'a, B, S>(&mut self, storage: &'a S, reason: String) -> Result<(), Error>
     where
         B: Batch<'a>,
         S: Storage<'a, B> + Sync + Send,
     {
-        self.change_status(Status::Settled)?;
+        self.change_status(Status::Settled, reason)?;
         self.persist::<B, S>(storage).await
     }
 
+    /// Changes the status of a given transaction
     #[inline]
-    pub fn change_status(&mut self, new_status: Status) -> Result<(), Error> {
+    pub fn change_status(&mut self, new_status: Status, reason: String) -> Result<(), Error> {
         if self.status.can_transition_to(&new_status) {
-            self.spends.iter_mut().for_each(|payment| {
-                payment.status = new_status.clone();
-                if new_status.is_rollback() {
-                    payment.spent_by = None;
-                }
-            });
-            self.creates.iter_mut().for_each(|payment| {
+            for payment in self.spends.iter_mut() {
+                payment.spent = if new_status.is_rollback() {
+                    None
+                } else {
+                    Some(SpentInfo {
+                        by: self.id.clone(),
+                        status: new_status.clone(),
+                    })
+                };
+                payment.new_changelog(&reason);
+            }
+
+            for payment in self.creates.iter_mut() {
                 payment.status = new_status.clone();
-            });
+                payment.new_changelog(&reason);
+            }
+
+            let previous = self.changelog.last().map(|x| x.id()).transpose()?;
+            self.changelog.push(Changelog::new(
+                previous,
+                self.id.deref().to_vec(),
+                ChangelogEntry {
+                    status: new_status.clone(),
+                    reason,
+                },
+            ));
             self.status = new_status;
             Ok(())
         } else {
@@ -241,6 +331,7 @@ impl Transaction {
     }
 
     #[inline]
+    /// Checks if the transaction attempts to spend any negative amount (which is no allowed)
     fn check_no_negative_amounts_are_spent(
         &self,
         debit: &HashMap<Asset, i128>,
@@ -257,8 +348,9 @@ impl Transaction {
         Ok(())
     }
 
+    /// Validates the transaction before storing
     pub(crate) fn validate(&self) -> Result<(), Error> {
-        let calculated_id = Self::calculate_hash(
+        let calculated_id = Self::calculate_id(
             &self.reference,
             self.spends.iter().map(|p| &p.id).collect::<Vec<_>>(),
             self.creates
@@ -276,7 +368,7 @@ impl Transaction {
         let mut credit = HashMap::<Asset, AmountCents>::new();
 
         for (i, input) in self.spends.iter().enumerate() {
-            if input.spent_by.is_some() && input.spent_by.as_ref() != Some(&self.id) {
+            if !input.is_spendable_or_was_by(&self.id) {
                 return Err(Error::SpentPayment(i));
             }
             if let Some(value) = debit.get_mut(input.amount.asset()) {
@@ -286,7 +378,7 @@ impl Transaction {
                     .checked_add(*value)
                     .ok_or(Error::Overflow)?;
             } else {
-                debit.insert(*input.amount.asset(), input.amount.cents());
+                debit.insert(input.amount.asset().clone(), input.amount.cents());
             }
         }
 
@@ -306,7 +398,7 @@ impl Transaction {
                     .checked_add(*value)
                     .ok_or(Error::Overflow)?;
             } else {
-                credit.insert(*output.amount.asset(), output.amount.cents());
+                credit.insert(output.amount.asset().clone(), output.amount.cents());
             }
         }
 
@@ -330,38 +422,57 @@ impl Transaction {
         Ok(())
     }
 
+    /// Returns the list of payments that were used to create this transaction
     pub fn spends(&self) -> &[Payment] {
         &self.spends
     }
 
+    /// Returns the list of payments that were created by this transaction
     pub fn creates(&self) -> &[Payment] {
         &self.creates
     }
 
+    /// Returns the transaction ID
     pub fn id(&self) -> &TransactionId {
         &self.id
     }
 
+    /// Returns the transaction status
     pub fn status(&self) -> &Status {
         &self.status
     }
 
+    /// Returns the transaction type
     pub fn typ(&self) -> &Type {
         &self.typ
     }
 
+    /// Returns the reference of this transaction
     pub fn reference(&self) -> &str {
         &self.reference
     }
 
+    /// Returns the last version of this transaction (the ID of the last entry from the changelog)
+    pub fn last_version(&self) -> Option<Vec<u8>> {
+        if let Some(Ok(x)) = self.changelog.last().map(|x| x.id()) {
+            Some(x)
+        } else {
+            None
+        }
+    }
+
+    /// Returns the time when this transaction was created
     pub fn created_at(&self) -> DateTime<Utc> {
         self.created_at
     }
 
+    /// Returns the time when this transaction was last updated
     pub fn updated_at(&self) -> DateTime<Utc> {
         self.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, B, S>(&mut self, storage: &'a S) -> Result<(), Error>
     where
         B: Batch<'a>,
@@ -381,15 +492,19 @@ impl Transaction {
             batch
                 .relate_account_to_transaction(&self, &payment.to)
                 .await?;
+            batch.store_changelogs(&payment.changelog).await?;
         }
         for input in self.spends.iter() {
             batch
-                .spend_payment(&input.id, self.status.clone(), &self.id)
+                .update_payment(&input.id, &self.id, self.status.clone())
                 .await?;
             batch
                 .relate_account_to_transaction(&self, &input.to)
                 .await?;
+            batch.store_changelogs(&input.changelog).await?;
         }
+
+        batch.store_changelogs(&self.changelog).await?;
         batch.commit().await?;
         Ok(())
     }
@@ -406,6 +521,7 @@ impl TryFrom<from_db::Transaction> for Transaction {
             creates: value.create,
             reference: value.reference,
             status: value.status,
+            changelog: sort_changes(value.changelog, value.last_change)?,
             created_at: value.created_at,
             updated_at: value.updated_at,
         };

+ 2 - 1
utxo/src/transaction/mod.rs

@@ -1,6 +1,7 @@
+mod changelog;
 mod error;
 pub mod from_db;
 mod inner;
 mod typ;
 
-pub use self::{error::Error, inner::Transaction, typ::Type};
+pub use self::{changelog::ChangelogEntry, error::Error, inner::Transaction, typ::Type};

+ 13 - 4
utxo/src/transaction/typ.rs

@@ -8,17 +8,26 @@ pub enum Error {
 
 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all = "snake_case")]
+/// Transaction types
 pub enum Type {
+    /// Deposit
     Deposit,
+    /// Withdrawal
     Withdrawal,
+    /// Transaction
     Transaction,
-    Internal,
+    /// Internal / Exchange Transactiojn
+    Exchange,
 }
 
 impl Type {
     #[inline]
+    /// Is it a transaction
+    ///
+    /// Transactions have a constraint that the amounts and assets must be equal
+    /// on both sides. Withdrawals and deposits don't have this constraint.
     pub fn is_transaction(&self) -> bool {
-        matches!(self, Self::Transaction | Self::Internal)
+        matches!(self, Self::Transaction | Self::Exchange)
     }
 }
 
@@ -29,7 +38,7 @@ impl TryFrom<u32> for Type {
             0 => Ok(Self::Transaction),
             1 => Ok(Self::Deposit),
             2 => Ok(Self::Withdrawal),
-            1000 => Ok(Self::Internal),
+            1000 => Ok(Self::Exchange),
             _ => Err(Error::InvalidStatus(value)),
         }
     }
@@ -47,7 +56,7 @@ impl From<&Type> for u32 {
             Type::Transaction => 0,
             Type::Deposit => 1,
             Type::Withdrawal => 2,
-            Type::Internal => 1000,
+            Type::Exchange => 1000,
         }
     }
 }