Quellcode durchsuchen

Improve amount parsing

Cesar Rodas vor 1 Jahr
Ursprung
Commit
5150413438

+ 50 - 0
Cargo.lock

@@ -757,6 +757,19 @@ dependencies = [
 ]
 
 [[package]]
+name = "env_logger"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
 name = "equivalent"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1148,6 +1161,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
 
 [[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
 name = "iana-time-zone"
 version = "0.1.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1242,6 +1261,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "is-terminal"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
+dependencies = [
+ "hermit-abi",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
 name = "itertools"
 version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1301,6 +1331,7 @@ name = "ledger"
 version = "0.1.0"
 dependencies = [
  "actix-web",
+ "env_logger",
  "ledger-utxo",
  "serde",
  "serde_json",
@@ -1318,6 +1349,7 @@ dependencies = [
  "futures",
  "hex",
  "serde",
+ "serde_json",
  "sha2",
  "sqlx",
  "strum",
@@ -2612,6 +2644,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "termcolor"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
 name = "thiserror"
 version = "1.0.49"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3019,6 +3060,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
 [[package]]
+name = "winapi-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
 name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"

+ 1 - 0
Cargo.toml

@@ -14,3 +14,4 @@ actix-web = "3"
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 tokio = { version = "1.32.0", features = ["full"] }
+env_logger = "0.10.0"

+ 26 - 11
src/main.rs

@@ -1,4 +1,7 @@
-use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
+use actix_web::{
+    error::InternalError, get, middleware::Logger, post, web, App, HttpResponse, HttpServer,
+    Responder,
+};
 use ledger_utxo::{AccountId, AssetDefinition, AssetManager, Status, TransactionId};
 use serde::{Deserialize, Serialize};
 use serde_json::json;
@@ -17,6 +20,7 @@ pub struct Deposit {
     pub amount: String,
     pub asset: String,
     pub memo: String,
+    pub status: Status,
 }
 
 impl Deposit {
@@ -24,13 +28,11 @@ impl Deposit {
         self,
         ledger: &Ledger,
     ) -> Result<ledger_utxo::Transaction, ledger_utxo::Error> {
-        /*
+        let amount = ledger._inner.parse_amount(&self.asset, &self.amount)?;
         ledger
             ._inner
-            .deposit(self.account, self.amount, self.asset, self.memo)
+            .deposit(&self.account, amount, self.status, self.memo)
             .await
-            */
-        todo!()
     }
 }
 
@@ -83,12 +85,13 @@ async fn get_transaction(
 async fn deposit(item: web::Json<Deposit>, ledger: web::Data<Ledger>) -> impl Responder {
     // Insert the item into a database or another data source.
     // For this example, we'll just echo the received item.
-    if let Ok(tx) = item.into_inner().to_ledger_transaction(&ledger).await {
-        // Insert the item into a database or another data source.
-        // For this example, we'll just echo the received item.
-        HttpResponse::Created().json(tx)
-    } else {
-        HttpResponse::Created().json(json!({"test": "true"}))
+    match item.into_inner().to_ledger_transaction(&ledger).await {
+        Ok(tx) => {
+            // Insert the item into a database or another data source.
+            // For this example, we'll just echo the received item.
+            HttpResponse::Created().json(tx)
+        }
+        Err(e) => HttpResponse::Created().json(e),
     }
 }
 
@@ -117,6 +120,8 @@ async fn main() -> std::io::Result<()> {
         AssetDefinition::new(2, "USD", 4),
     ]);
 
+    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
+
     let rt = tokio::runtime::Builder::new_multi_thread()
         .worker_threads(10)
         .enable_all()
@@ -136,7 +141,17 @@ async fn main() -> std::io::Result<()> {
             let storage = ledger_utxo::Sqlite::new(pool.clone(), asset_manager.clone());
             let ledger = ledger_utxo::Ledger::new(storage, asset_manager.clone());
             App::new()
+                .wrap(Logger::default())
                 .data(Ledger { _inner: ledger })
+                .app_data(web::JsonConfig::default().error_handler(|err, _req| {
+                    InternalError::from_response(
+                        "",
+                        HttpResponse::BadRequest()
+                            .content_type("application/json")
+                            .body(format!(r#"{{"error":"{}"}}"#, err)),
+                    )
+                    .into()
+                }))
                 .service(get_transaction)
                 .service(deposit)
                 .service(create_transaction)

+ 1 - 0
utxo/Cargo.toml

@@ -11,6 +11,7 @@ chrono = { version = "0.4.31", features = ["serde"] }
 futures = "0.3.28"
 hex = "0.4.3"
 serde = { version = "1.0.188", features = ["derive"] }
+serde_json = "1.0.107"
 sha2 = "0.10.7"
 sqlx = { version = "0.7.1", features = ["runtime-tokio", "tls-native-tls", "sqlite", "chrono"] }
 strum = "0.25.0"

+ 72 - 2
utxo/src/amount.rs

@@ -1,8 +1,15 @@
 use crate::Asset;
 use serde::{Serialize, Serializer};
 
+/// The raw storage for cents, the more the better
 pub type AmountCents = i128;
 
+#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
+pub enum Error {
+    #[error("{0} is not a valid number")]
+    NoANumber(String),
+}
+
 /// Amount
 ///
 /// The cents are stored in their lowest denomination, or their "cents".  For
@@ -40,6 +47,44 @@ impl Amount {
         Self { asset, cents }
     }
 
+    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() {
+            match i {
+                '-' => {
+                    if pos != 0 {
+                        return Err(Error::NoANumber(human_amount.to_owned()));
+                    }
+                }
+                '.' => {
+                    if dot_at.is_some() {
+                        return Err(Error::NoANumber(human_amount.to_owned()));
+                    }
+                    dot_at = Some(pos);
+                }
+                '0'..='9' => {}
+                _ => {
+                    return Err(Error::NoANumber(human_amount.to_owned()));
+                }
+            }
+        }
+
+        let (whole, fractional_part) = if let Some(dot_at) = dot_at {
+            let (whole, fractional_part) = human_amount.split_at(dot_at);
+            (whole, fractional_part[1..].to_owned())
+        } else {
+            (human_amount, "".to_owned())
+        };
+
+        let fractional_part = fractional_part + &"0".repeat(asset.precision.into());
+
+        let cents = (whole.to_owned() + &fractional_part[..asset.precision.into()])
+            .parse::<AmountCents>()
+            .map_err(|_| Error::NoANumber(format!("{}.{}", whole, fractional_part)))?;
+
+        Ok(Self { asset, cents })
+    }
+
     #[inline]
     pub fn asset(&self) -> &Asset {
         &self.asset
@@ -64,7 +109,7 @@ impl Amount {
 
 impl ToString for Amount {
     fn to_string(&self) -> String {
-        let str = self.cents.to_string();
+        let str = self.cents.abs().to_string();
         let precision: usize = self.asset.precision.into();
         let str = if str.len() < precision + 1 {
             format!("{}{}", "0".repeat(precision - str.len() + 1), str)
@@ -73,7 +118,12 @@ impl ToString for Amount {
         };
 
         let (left, right) = str.split_at(str.len() - precision);
-        format!("{}.{}", left, right)
+        format!(
+            "{}{}.{}",
+            if self.cents.is_negative() { "-" } else { "" },
+            left,
+            right
+        )
     }
 }
 
@@ -109,4 +159,24 @@ mod test {
             "2.00000000"
         );
     }
+
+    #[test]
+    fn from_human() {
+        let btc = Asset {
+            id: 1,
+            precision: 8,
+        };
+        let parsed_amount = Amount::from_human(btc, "0.1").expect("valid amount");
+        assert_eq!(parsed_amount.to_string(), "0.10000000");
+        let parsed_amount = Amount::from_human(btc, "-0.1").expect("valid amount");
+        assert_eq!(parsed_amount.to_string(), "-0.10000000");
+        let parsed_amount = Amount::from_human(btc, "-0.000001").expect("valid amount");
+        assert_eq!(parsed_amount.to_string(), "-0.00000100");
+        let parsed_amount = Amount::from_human(btc, "-0.000000001").expect("valid amount");
+        assert_eq!(parsed_amount.to_string(), "0.00000000");
+        let parsed_amount = Amount::from_human(btc, "0.000001").expect("valid amount");
+        assert_eq!(parsed_amount.to_string(), "0.00000100");
+        let parsed_amount = Amount::from_human(btc, "-0.000000100001").expect("valid amount");
+        assert_eq!(parsed_amount.to_string(), "-0.00000010");
+    }
 }

+ 5 - 8
utxo/src/asset_manager.rs

@@ -39,15 +39,12 @@ impl AssetManager {
             .ok_or(Error::AssetIdNotFound(id))
     }
 
-    pub fn amount_by_name(&self, name: &str, amount: &str) -> Result<Amount, Error> {
-        self.asset_names
+    pub fn human_amount_by_name(&self, name: &str, human_amount: &str) -> Result<Amount, Error> {
+        Ok(self
+            .asset_names
             .get(name)
-            .map(|asset| {
-                //let amount = AmountCents::from_str(amount)?;
-                //Amount::new(asset.asset, amount)
-                todo!()
-            })
-            .ok_or(Error::AssetNotFound(name.to_owned()))
+            .map(|asset| Amount::from_human(asset.asset, human_amount))
+            .ok_or(Error::AssetNotFound(name.to_owned()))??)
     }
 
     pub fn amount_by_and_cents(&self, id: AssetId, cents: AmountCents) -> Result<Amount, Error> {

+ 15 - 1
utxo/src/error.rs

@@ -1,4 +1,5 @@
-use crate::{asset::AssetId, storage, transaction, AccountId};
+use crate::{amount, asset::AssetId, storage, transaction, AccountId};
+use serde::{Serialize, Serializer};
 
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
@@ -16,4 +17,17 @@ pub enum Error {
 
     #[error("Not enough funds (asset {1}) for account {0}")]
     InsufficientBalance(AccountId, AssetId),
+
+    #[error("Invalid amount: {0}")]
+    InvalidAmount(#[from] amount::Error),
+}
+
+impl Serialize for Error {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let serialized = self.to_string();
+        serializer.serialize_str(&serialized)
+    }
 }

+ 1 - 1
utxo/src/id.rs

@@ -11,7 +11,7 @@ pub enum Error {
 
 macro_rules! Id {
     ($id:ident, $suffix:expr) => {
-        #[derive(Clone, Debug, Eq, Hash, PartialEq)]
+        #[derive(Clone, Debug, Eq, PartialOrd, Ord, Hash, PartialEq)]
         pub struct $id {
             bytes: [u8; 32],
         }

+ 4 - 3
utxo/src/ledger.rs

@@ -29,8 +29,8 @@ where
         }
     }
 
-    pub fn parse_amount(&self, amount: &str, asset: &str) -> Result<Amount, Error> {
-        Ok(self.asset_manager.amount_by_name(amount, asset)?)
+    pub fn parse_amount(&self, asset: &str, amount: &str) -> Result<Amount, Error> {
+        Ok(self.asset_manager.human_amount_by_name(asset, amount)?)
     }
 
     /// Selects the unspent payments to be used as inputs of the new transaction.
@@ -107,7 +107,7 @@ where
                         )
                         .await?;
                         // Spend the new payment
-                        payments.push(split_input.created()[0].clone());
+                        payments.push(split_input.creates()[0].clone());
                         // Return the split payment transaction to be executed
                         // later as a pre-requisite for the new transaction
                         change_transactions.push(split_input);
@@ -191,6 +191,7 @@ where
     ) -> Result<Transaction, Error> {
         let mut transaction =
             Transaction::new_external_deposit(reference, status, vec![(account.clone(), amount)])?;
+        println!("{}", serde_json::to_string_pretty(&transaction).unwrap());
         transaction.persist(&self.storage).await?;
         Ok(transaction)
     }

+ 12 - 2
utxo/src/payment.rs

@@ -1,12 +1,22 @@
 use crate::{AccountId, Amount, Status, TransactionId};
-use serde::Serialize;
+use serde::{Serialize, Serializer};
 
-#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
+#[derive(Clone, Debug, Eq, Ord, PartialOrd, PartialEq)]
 pub struct PaymentId {
     pub transaction: TransactionId,
     pub position: usize,
 }
 
+impl Serialize for PaymentId {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let serialized = self.to_string();
+        serializer.serialize_str(&serialized)
+    }
+}
+
 impl ToString for PaymentId {
     fn to_string(&self) -> String {
         format!("{}:{}", self.transaction, self.position)

+ 6 - 6
utxo/src/sqlite/batch.rs

@@ -126,20 +126,20 @@ impl<'a> storage::Batch<'a> for Batch<'a> {
                 INSERT INTO "transactions"("transaction_id", "status", "type", "reference", "created_at", "updated_at")
                 VALUES(?, ?, ?, ?, ?, ?)
                 ON CONFLICT("transaction_id")
-                    DO UPDATE SET "status" = excluded."status", "reference" = excluded."reference", "updated_at" = excluded."updated_at"
+                    DO UPDATE SET "status" = excluded."status", "updated_at" = excluded."updated_at"
             "#,
         )
         .bind(transaction.id().to_string())
         .bind::<u32>(transaction.status().into())
         .bind::<u32>(transaction.typ().into())
         .bind(transaction.reference())
-        .bind(transaction.created_at().timestamp())
-        .bind(transaction.updated_at().timestamp())
+        .bind(transaction.created_at())
+        .bind(transaction.updated_at())
         .execute(&mut *self.inner)
         .await
         .map_err(|e| Error::Storage(e.to_string()))?;
 
-        for payment in transaction.spent().iter() {
+        for payment in transaction.spends().iter() {
             sqlx::query(
                 r#"
             INSERT INTO "transaction_input_payments"("transaction_id", "payment_transaction_id", "payment_position_id")
@@ -175,8 +175,8 @@ impl<'a> storage::Batch<'a> for Batch<'a> {
         .bind(transaction.id().to_string())
         .bind(account.to_string())
         .bind::<u32>(transaction.typ().into())
-        .bind(transaction.created_at().timestamp())
-        .bind(transaction.updated_at().timestamp())
+        .bind(transaction.created_at())
+        .bind(transaction.updated_at())
         .execute(&mut *self.inner)
         .await
         .map_err(|e| Error::Storage(e.to_string()))?;

+ 2 - 2
utxo/src/sqlite/mod.rs

@@ -333,9 +333,9 @@ impl<'a> Storage<'a, Batch<'a>> for Sqlite<'a> {
                 "p"."status",
                 "p"."spent_by"
             FROM
-                "payments" "p"
-            INNER JOIN
                 "transaction_input_payments" "tp"
+            INNER JOIN
+                "payments" "p"
                 ON (
                     "tp"."payment_transaction_id" = "p"."transaction_id"
                     AND "tp"."payment_position_id" = "p"."position_id"

+ 1 - 0
utxo/src/status.rs

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

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

@@ -265,7 +265,7 @@ async fn balance_decreases_while_pending_spending_and_failed() {
         ledger.get_balance(&source).await.expect("balance")
     );
 
-    let id = ledger
+    let tx = ledger
         .new_transaction(
             "Exchange one".to_owned(),
             Status::Pending,
@@ -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(&id, Status::Processing)
+        .change_status(&tx, Status::Processing)
         .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(&id, Status::Cancelled)
+            .change_status(&tx, Status::Cancelled)
             .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(&id, Status::Failed)
+        .change_status(&tx, Status::Failed)
         .await
         .expect("valid");
 

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

@@ -25,7 +25,7 @@ pub async fn get_instance() -> (
 
     let db = Sqlite::new(pool, assets.clone());
     db.setup().await.expect("setup");
-    (assets, Ledger::new(db))
+    (assets.clone(), Ledger::new(db, assets))
 }
 
 pub async fn withdrawal(

+ 38 - 25
utxo/src/transaction/inner.rs

@@ -39,11 +39,11 @@ use std::collections::HashMap;
 #[derive(Debug, Clone, Serialize)]
 pub struct Transaction {
     id: TransactionId,
-    spend: Vec<Payment>,
+    spends: Vec<Payment>,
     #[allow(dead_code)]
     reference: String,
     typ: Type,
-    create: Vec<Payment>,
+    creates: Vec<Payment>,
     status: Status,
     #[serde(with = "ts_milliseconds")]
     created_at: DateTime<Utc>,
@@ -57,9 +57,11 @@ impl Transaction {
         status: Status,
         spend: Vec<Payment>,
     ) -> Result<Transaction, Error> {
+        let created_at = Utc::now();
         let id = Self::calculate_hash(
             spend.iter().map(|t| &t.id).collect::<Vec<&PaymentId>>(),
             vec![],
+            created_at,
         )?;
         let spend = spend
             .into_iter()
@@ -70,12 +72,12 @@ impl Transaction {
             .collect();
         Ok(Self {
             id,
-            spend,
-            create: vec![],
+            spends: spend,
+            creates: vec![],
             typ: Type::Withdrawal,
             reference,
             status,
-            created_at: Utc::now(),
+            created_at,
             updated_at: Utc::now(),
         })
     }
@@ -85,12 +87,14 @@ impl Transaction {
         status: Status,
         pay_to: Vec<(AccountId, Amount)>,
     ) -> Result<Transaction, Error> {
+        let created_at = Utc::now();
         let id = Self::calculate_hash(
             vec![],
             pay_to
                 .iter()
                 .map(|t| (&t.0, &t.1))
                 .collect::<Vec<(&AccountId, &Amount)>>(),
+            created_at,
         )?;
         let create = pay_to
             .into_iter()
@@ -109,12 +113,12 @@ impl Transaction {
 
         Ok(Self {
             id,
-            spend: vec![],
-            create,
+            spends: vec![],
+            creates: create,
             reference,
             typ: Type::Deposit,
             status,
-            created_at: Utc::now(),
+            created_at,
             updated_at: Utc::now(),
         })
     }
@@ -126,12 +130,14 @@ impl Transaction {
         spend: Vec<Payment>,
         pay_to: Vec<(AccountId, Amount)>,
     ) -> Result<Transaction, Error> {
+        let created_at = Utc::now();
         let id = Self::calculate_hash(
             spend.iter().map(|t| &t.id).collect::<Vec<&PaymentId>>(),
             pay_to
                 .iter()
                 .map(|t| (&t.0, &t.1))
                 .collect::<Vec<(&AccountId, &Amount)>>(),
+            created_at,
         )?;
 
         for (i, input) in spend.iter().enumerate() {
@@ -165,11 +171,11 @@ impl Transaction {
         Ok(Self {
             id,
             reference,
-            spend,
+            spends: spend,
             typ,
-            create,
+            creates: create,
             status,
-            created_at: Utc::now(),
+            created_at,
             updated_at: Utc::now(),
         })
     }
@@ -177,8 +183,13 @@ impl Transaction {
     fn calculate_hash(
         spend: Vec<&PaymentId>,
         create: Vec<(&AccountId, &Amount)>,
+        created_at: DateTime<Utc>,
     ) -> Result<TransactionId, Error> {
         let mut hasher = Sha256::new();
+        let mut spend = spend;
+
+        spend.sort();
+
         for id in spend.into_iter() {
             hasher.update(&bincode::serialize(id)?);
         }
@@ -186,6 +197,7 @@ impl Transaction {
             hasher.update(&bincode::serialize(account)?);
             hasher.update(&bincode::serialize(amount)?);
         }
+        hasher.update(&created_at.timestamp_millis().to_le_bytes());
         Ok(TransactionId::new(hasher.finalize().into()))
     }
 
@@ -201,13 +213,13 @@ impl Transaction {
     #[inline]
     pub fn change_status(&mut self, new_status: Status) -> Result<(), Error> {
         if self.status.can_transition_to(&new_status) {
-            self.spend.iter_mut().for_each(|payment| {
+            self.spends.iter_mut().for_each(|payment| {
                 payment.status = new_status.clone();
                 if new_status.is_rollback() {
                     payment.spent_by = None;
                 }
             });
-            self.create.iter_mut().for_each(|payment| {
+            self.creates.iter_mut().for_each(|payment| {
                 payment.status = new_status.clone();
             });
             self.status = new_status;
@@ -239,11 +251,12 @@ impl Transaction {
 
     pub(crate) fn validate(&self) -> Result<(), Error> {
         let calculated_id = Self::calculate_hash(
-            self.spend.iter().map(|p| &p.id).collect::<Vec<_>>(),
-            self.create
+            self.spends.iter().map(|p| &p.id).collect::<Vec<_>>(),
+            self.creates
                 .iter()
                 .map(|p| (&p.to, &p.amount))
                 .collect::<Vec<_>>(),
+            self.created_at,
         )?;
 
         if calculated_id != self.id {
@@ -253,7 +266,7 @@ impl Transaction {
         let mut debit = HashMap::<Asset, AmountCents>::new();
         let mut credit = HashMap::<Asset, AmountCents>::new();
 
-        for (i, input) in self.spend.iter().enumerate() {
+        for (i, input) in self.spends.iter().enumerate() {
             if input.spent_by.is_some() && input.spent_by.as_ref() != Some(&self.id) {
                 return Err(Error::SpentPayment(i));
             }
@@ -276,7 +289,7 @@ impl Transaction {
             return Ok(());
         }
 
-        for (i, output) in self.create.iter().enumerate() {
+        for (i, output) in self.creates.iter().enumerate() {
             if output.spent_by.is_some() {
                 return Err(Error::SpentPayment(i));
             }
@@ -311,12 +324,12 @@ impl Transaction {
         Ok(())
     }
 
-    pub fn spent(&self) -> &[Payment] {
-        &self.spend
+    pub fn spends(&self) -> &[Payment] {
+        &self.spends
     }
 
-    pub fn created(&self) -> &[Payment] {
-        &self.create
+    pub fn creates(&self) -> &[Payment] {
+        &self.creates
     }
 
     pub fn id(&self) -> &TransactionId {
@@ -357,13 +370,13 @@ impl Transaction {
         self.validate()?;
         self.updated_at = Utc::now();
         batch.store_transaction(self).await?;
-        for payment in self.create.iter() {
+        for payment in self.creates.iter() {
             batch.store_new_payment(payment).await?;
             batch
                 .relate_account_to_transaction(&self, &payment.to)
                 .await?;
         }
-        for input in self.spend.iter() {
+        for input in self.spends.iter() {
             batch
                 .spend_payment(&input.id, self.status.clone(), &self.id)
                 .await?;
@@ -383,8 +396,8 @@ impl TryFrom<from_db::Transaction> for Transaction {
         let tx = Transaction {
             id: value.id,
             typ: value.typ,
-            spend: value.spend,
-            create: value.create,
+            spends: value.spend,
+            creates: value.create,
             reference: value.reference,
             status: value.status,
             created_at: value.created_at,

+ 1 - 0
utxo/src/transaction/typ.rs

@@ -7,6 +7,7 @@ pub enum Error {
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
 pub enum Type {
     Deposit,
     Withdrawal,