瀏覽代碼

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
             "#,
         )