فهرست منبع

Do not expect payments to be sorted

But make sure that all negative deposits are included
Cesar Rodas 11 ماه پیش
والد
کامیت
bbe2377e5b
4فایلهای تغییر یافته به همراه135 افزوده شده و 15 حذف شده
  1. 12 2
      utxo/src/storage/cache/mod.rs
  2. 61 7
      utxo/src/storage/mod.rs
  3. 5 3
      utxo/src/storage/sqlite/batch.rs
  4. 57 3
      utxo/src/storage/sqlite/mod.rs

+ 12 - 2
utxo/src/storage/cache/mod.rs

@@ -148,14 +148,24 @@ where
         }
     }
 
-    async fn get_unspent_payments(
+    async fn get_negative_unspent_payments(
+        &self,
+        account: &AccountId,
+        asset: &Asset,
+    ) -> Result<Vec<PaymentFrom>, Error> {
+        self.inner
+            .get_negative_unspent_payments(account, asset)
+            .await
+    }
+
+    async fn get_positive_unspent_payments(
         &self,
         account: &AccountId,
         asset: &Asset,
         target_amount: AmountCents,
     ) -> Result<Vec<PaymentFrom>, Error> {
         self.inner
-            .get_unspent_payments(account, asset, target_amount)
+            .get_positive_unspent_payments(account, asset, target_amount)
             .await
     }
 

+ 61 - 7
utxo/src/storage/mod.rs

@@ -277,6 +277,16 @@ pub trait Storage {
     /// Returns the balances for a given account
     async fn get_balance(&self, account: &AccountId) -> Result<Vec<Amount>, Error>;
 
+    /// Returns all the negative payments that are unspent by any account
+    ///
+    /// If any unspent negative deposit exists with the given account and asset, it must be spend in
+    /// the transaction.
+    async fn get_negative_unspent_payments(
+        &self,
+        account: &AccountId,
+        asset: &Asset,
+    ) -> Result<Vec<PaymentFrom>, Error>;
+
     /// Returns a list of unspent payments for a given account and asset.
     ///
     /// The payments should be returned sorted by ascending amount, this bit is
@@ -284,13 +294,39 @@ pub trait Storage {
     /// account. It will also improve the database by using many small payments
     /// instead of a few large ones, which will make the database faster leaving
     /// fewer unspent payments when checking for balances.
-    async fn get_unspent_payments(
+    async fn get_positive_unspent_payments(
         &self,
         account: &AccountId,
         asset: &Asset,
         target_amount: AmountCents,
     ) -> Result<Vec<PaymentFrom>, Error>;
 
+    /// Returns a list of unspent payments for a given account and asset.
+    ///
+    /// This list includes all the negative unspent payments and the list of positive unspent
+    /// payments to cover the target_amount
+    async fn get_unspent_payments(
+        &self,
+        account: &AccountId,
+        asset: &Asset,
+        target_amount: AmountCents,
+    ) -> Result<Vec<PaymentFrom>, Error> {
+        let mut payments = self.get_negative_unspent_payments(account, asset).await?;
+        let target_amount = target_amount
+            + payments
+                .iter()
+                .map(|payment| payment.amount.cents())
+                .sum::<AmountCents>()
+                .abs();
+
+        payments.extend(
+            self.get_positive_unspent_payments(account, asset, target_amount)
+                .await?
+                .into_iter(),
+        );
+        Ok(payments)
+    }
+
     /*
     ///
     async fn get_revision(&self, revision_id: &TxId) -> Result<Transaction, Error>;
@@ -363,6 +399,8 @@ pub trait Storage {
 
 #[cfg(test)]
 pub mod test {
+    use std::collections::HashMap;
+
     use super::*;
     use crate::{config::Config, status::StatusManager, Transaction};
     use rand::Rng;
@@ -384,7 +422,7 @@ pub mod test {
             $crate::storage_unit_test!(transaction);
             $crate::storage_unit_test!(transaction_does_not_update_stale_transactions);
             $crate::storage_unit_test!(transaction_not_available_until_commit);
-            $crate::storage_unit_test!(sorted_unspent_payments);
+            $crate::storage_unit_test!(payments_always_include_negative_amounts);
             $crate::storage_unit_test!(does_not_update_spent_payments);
             $crate::storage_unit_test!(does_not_spend_unspendable_payments);
             $crate::storage_unit_test!(spend_spendable_payments);
@@ -713,7 +751,7 @@ pub mod test {
         }
     }
 
-    pub async fn sorted_unspent_payments<T>(storage: T)
+    pub async fn payments_always_include_negative_amounts<T>(storage: T)
     where
         T: Storage + Send + Sync,
     {
@@ -726,13 +764,21 @@ pub mod test {
         let target_inputs_per_account = 20;
         let usd: Asset = "USD/2".parse().expect("valid asset");
 
+        let mut negative_payments_per_account = HashMap::new();
+
         for (index, account) in accounts.iter().enumerate() {
             let transaction_id: TxId = vec![index as u8; 32].try_into().expect("valid tx id");
+            let mut negative_payments: usize = 0;
             let recipients = (0..target_inputs_per_account)
                 .map(|_| {
                     let amount = usd
                         .from_human(&format!("{}", rng.gen_range(-1000.0..1000.0)))
                         .expect("valid amount");
+
+                    if amount.cents().is_negative() {
+                        negative_payments += 1;
+                    }
+
                     PaymentTo {
                         to: account.clone(),
                         amount,
@@ -740,6 +786,8 @@ pub mod test {
                 })
                 .collect::<Vec<_>>();
 
+            negative_payments_per_account.insert(account.clone(), negative_payments);
+
             writer
                 .create_payments(
                     &transaction_id,
@@ -765,13 +813,19 @@ pub mod test {
                 .map(|x| x.amount.cents())
                 .collect::<Vec<_>>();
 
-            let mut sorted = all_unspent_amounts.clone();
-            sorted.sort();
+            let expected_negative_payments = all_unspent_amounts
+                .iter()
+                .filter(|x| x.is_negative())
+                .count();
 
+            assert_eq!(
+                Some(expected_negative_payments),
+                negative_payments_per_account.get(account).cloned()
+            );
             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;
+                at_least_one_negative_amount = expected_negative_payments > 0;
             }
         }
         assert!(at_least_one_negative_amount);

+ 5 - 3
utxo/src/storage/sqlite/batch.rs

@@ -111,10 +111,11 @@ impl<'a> storage::Batch<'a> for Batch<'a> {
         status: ReceivedPaymentStatus,
     ) -> Result<(), Error> {
         for (pos, recipient) in recipients.iter().enumerate() {
+            let cents = recipient.amount.cents();
             sqlx::query(
                 r#"
-                INSERT INTO "payments"("payment_id", "to", "status", "asset", "cents")
-                VALUES(?, ?, ?, ?, ? )
+                INSERT INTO "payments"("payment_id", "to", "status", "asset", "cents", "is_negative")
+                VALUES(?, ?, ?, ?, ?, ?)
             "#,
             )
             .bind(
@@ -129,7 +130,8 @@ impl<'a> storage::Batch<'a> for Batch<'a> {
             .bind(recipient.to.to_string())
             .bind::<u32>(status.into())
             .bind(recipient.amount.asset().to_string())
-            .bind(recipient.amount.cents().to_string())
+            .bind(cents.to_string())
+            .bind(if cents.is_negative() { 1 } else { 0 })
             .execute(&mut *self.inner)
             .await
             .map_err(|e| Error::Storage(e.to_string()))?;

+ 57 - 3
utxo/src/storage/sqlite/mod.rs

@@ -65,11 +65,12 @@ impl SQLite {
             "status" INT NOT NULL,
             "asset" VARCHAR(10) NOT NULL,
             "cents" STRING NOT NULL,
+            "is_negative" INT DEFAULT '0',
             "spent_by" VARCHAR(64) DEFAULT NULL,
             "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
             "updated_at" DATETIME DEFAULT CURRENT_TIMESTAMP
         );
-        CREATE INDEX IF NOT EXISTS "spent_by" ON "payments" ("to", "status", "spent_by");
+        CREATE INDEX IF NOT EXISTS "spent_by" ON "payments" ("to", "status", "spent_by", "is_negative");
         CREATE TABLE IF NOT EXISTS "transaction_accounts" (
             "id" INTEGER PRIMARY KEY AUTOINCREMENT,
             "account_id" VARCHAR(64) NOT NULL,
@@ -183,7 +184,56 @@ impl Storage for SQLite {
         Ok(balances.into_values().collect())
     }
 
-    async fn get_unspent_payments(
+    async fn get_negative_unspent_payments(
+        &self,
+        account: &AccountId,
+        asset: &Asset,
+    ) -> Result<Vec<PaymentFrom>, Error> {
+        let mut conn = self
+            .db
+            .acquire()
+            .await
+            .map_err(|e| Error::Storage(e.to_string()))?;
+        sqlx::query(
+            r#"
+            SELECT
+                "p"."payment_id",
+                "p"."asset",
+                "p"."cents",
+                "p"."to"
+            FROM
+                "payments" as "p"
+            WHERE
+                "p"."to" = ?
+                AND "p"."asset" = ?
+                AND "p"."status" = ?
+                AND "p"."spent_by" IS NULL
+                AND "p"."is_negative" = 1
+            "#,
+        )
+        .bind(account.to_string())
+        .bind(asset.to_string())
+        .bind::<u32>(ReceivedPaymentStatus::Spendable.into())
+        .fetch_all(&mut *conn)
+        .await
+        .map_err(|e| Error::Storage(e.to_string()))?
+        .into_iter()
+        .map(|row| {
+            let amount = Self::sql_row_to_amount(&row, 1)?;
+            Ok(PaymentFrom {
+                id: Self::sql_row_to_payment_id(&row, 0)?,
+                from: row
+                    .try_get::<String, usize>(3)
+                    .map_err(|_| Error::Storage("Invalid from".to_string()))?
+                    .parse()
+                    .map_err(|_| Error::Encoding("Invalid account encoding".to_owned()))?,
+                amount,
+            })
+        })
+        .collect::<Result<Vec<_>, Error>>()
+    }
+
+    async fn get_positive_unspent_payments(
         &self,
         account: &AccountId,
         asset: &Asset,
@@ -204,7 +254,11 @@ impl Storage for SQLite {
             FROM
                 "payments" as "p"
             WHERE
-                "p"."to" = ? AND "p"."asset" = ? AND status = ? AND "p"."spent_by" IS NULL
+                "p"."to" = ?
+                AND "p"."asset" = ?
+                AND "p"."status" = ?
+                AND "p"."spent_by" IS NULL
+                AND "p"."is_negative" = 0
             ORDER BY cents ASC
             "#,
         )