|  | @@ -1,10 +1,9 @@
 | 
	
		
			
				|  |  |  //! SQLite storage layer for Verax
 | 
	
		
			
				|  |  |  use crate::{
 | 
	
		
			
				|  |  |      amount::AmountCents,
 | 
	
		
			
				|  |  | -    storage::{Error, FilterId, Storage},
 | 
	
		
			
				|  |  | +    storage::{Error, Storage},
 | 
	
		
			
				|  |  |      transaction::{Revision, Type},
 | 
	
		
			
				|  |  | -    AccountId, Amount, Asset, BaseTx, Filter, PaymentFrom, PaymentId, RevId, Tag, Transaction,
 | 
	
		
			
				|  |  | -    TxId,
 | 
	
		
			
				|  |  | +    AccountId, Amount, Asset, BaseTx, Filter, PaymentFrom, PaymentId, RevId, TxId,
 | 
	
		
			
				|  |  |  };
 | 
	
		
			
				|  |  |  use borsh::from_slice;
 | 
	
		
			
				|  |  |  use futures::TryStreamExt;
 | 
	
	
		
			
				|  | @@ -23,6 +22,7 @@ pub struct SQLite {
 | 
	
		
			
				|  |  |      db: sqlx::SqlitePool,
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +#[derive(Debug, Clone)]
 | 
	
		
			
				|  |  |  enum PrimaryFilter {
 | 
	
		
			
				|  |  |      Id(Vec<TxId>),
 | 
	
		
			
				|  |  |      Revision(Vec<RevId>),
 | 
	
	
		
			
				|  | @@ -31,16 +31,17 @@ enum PrimaryFilter {
 | 
	
		
			
				|  |  |      Stream,
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +#[derive(Debug, Clone)]
 | 
	
		
			
				|  |  |  struct Cursor {
 | 
	
		
			
				|  |  |      primary_filter: PrimaryFilter,
 | 
	
		
			
				|  |  |      filter: Filter,
 | 
	
		
			
				|  |  | -    skip: usize,
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  /// Receives a cursor and does the initial loading of transactions that may match the requested
 | 
	
		
			
				|  |  |  /// filter.
 | 
	
		
			
				|  |  |  ///
 | 
	
		
			
				|  |  |  /// The function will load the full transaction of candidates and return them.
 | 
	
		
			
				|  |  | +#[allow(warnings)]
 | 
	
		
			
				|  |  |  async fn find_candidates<'e, 'c: 'e, E>(
 | 
	
		
			
				|  |  |      executor: E,
 | 
	
		
			
				|  |  |      cursor: &mut Cursor,
 | 
	
	
		
			
				|  | @@ -61,6 +62,34 @@ where
 | 
	
		
			
				|  |  |      };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      let rows = match &cursor.primary_filter {
 | 
	
		
			
				|  |  | +        PrimaryFilter::Revision(rev_ids) => {
 | 
	
		
			
				|  |  | +            let sql = format!(
 | 
	
		
			
				|  |  | +                r#"
 | 
	
		
			
				|  |  | +                    SELECT
 | 
	
		
			
				|  |  | +                        "bt"."blob",
 | 
	
		
			
				|  |  | +                        "b"."blob"
 | 
	
		
			
				|  |  | +                    FROM
 | 
	
		
			
				|  |  | +                        "base_transactions" as "bt",
 | 
	
		
			
				|  |  | +                        "revisions" as "b"
 | 
	
		
			
				|  |  | +                    WHERE
 | 
	
		
			
				|  |  | +                        "b"."revision_id" IN ({})
 | 
	
		
			
				|  |  | +                        AND "b"."transaction_id" = "bt"."transaction_id"
 | 
	
		
			
				|  |  | +                        {} {}
 | 
	
		
			
				|  |  | +                    ORDER BY "b"."created_at" DESC
 | 
	
		
			
				|  |  | +                    LIMIT {} OFFSET {}
 | 
	
		
			
				|  |  | +                    "#,
 | 
	
		
			
				|  |  | +                "?,".repeat(rev_ids.len()).trim_end_matches(","),
 | 
	
		
			
				|  |  | +                since,
 | 
	
		
			
				|  |  | +                until,
 | 
	
		
			
				|  |  | +                limit,
 | 
	
		
			
				|  |  | +                cursor.filter.skip
 | 
	
		
			
				|  |  | +            );
 | 
	
		
			
				|  |  | +            let mut query = sqlx::query(&sql);
 | 
	
		
			
				|  |  | +            for id in rev_ids.iter() {
 | 
	
		
			
				|  |  | +                query = query.bind(id.to_string());
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            query.fetch_all(executor).await
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  |          PrimaryFilter::Id(ids) => {
 | 
	
		
			
				|  |  |              let sql = format!(
 | 
	
		
			
				|  |  |                  r#"
 | 
	
	
		
			
				|  | @@ -77,11 +106,13 @@ where
 | 
	
		
			
				|  |  |                          AND "t"."transaction_id" = "bt"."transaction_id"
 | 
	
		
			
				|  |  |                          {} {}
 | 
	
		
			
				|  |  |                      ORDER BY "t"."created_at" DESC
 | 
	
		
			
				|  |  | -                    LIMIT ? OFFSET ?
 | 
	
		
			
				|  |  | +                    LIMIT {} OFFSET {}
 | 
	
		
			
				|  |  |                      "#,
 | 
	
		
			
				|  |  |                  "?,".repeat(ids.len()).trim_end_matches(","),
 | 
	
		
			
				|  |  |                  since,
 | 
	
		
			
				|  |  | -                until
 | 
	
		
			
				|  |  | +                until,
 | 
	
		
			
				|  |  | +                limit,
 | 
	
		
			
				|  |  | +                cursor.filter.skip
 | 
	
		
			
				|  |  |              );
 | 
	
		
			
				|  |  |              let mut query = sqlx::query(&sql);
 | 
	
		
			
				|  |  |              for id in ids.iter() {
 | 
	
	
		
			
				|  | @@ -89,41 +120,41 @@ where
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |              query
 | 
	
		
			
				|  |  |                  .bind(limit as i64)
 | 
	
		
			
				|  |  | -                .bind(cursor.skip as i64)
 | 
	
		
			
				|  |  | +                .bind(cursor.filter.skip as i64)
 | 
	
		
			
				|  |  |                  .fetch_all(executor)
 | 
	
		
			
				|  |  |                  .await
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -        PrimaryFilter::Revision(rev_ids) => {
 | 
	
		
			
				|  |  | +        PrimaryFilter::Account(account_ids) => {
 | 
	
		
			
				|  |  |              let sql = format!(
 | 
	
		
			
				|  |  |                  r#"
 | 
	
		
			
				|  |  |                      SELECT
 | 
	
		
			
				|  |  |                          "bt"."blob",
 | 
	
		
			
				|  |  |                          "b"."blob"
 | 
	
		
			
				|  |  |                      FROM
 | 
	
		
			
				|  |  | +                        "transaction_accounts" as "ta",
 | 
	
		
			
				|  |  |                          "transactions" as "t",
 | 
	
		
			
				|  |  |                          "base_transactions" as "bt",
 | 
	
		
			
				|  |  |                          "revisions" as "b"
 | 
	
		
			
				|  |  |                      WHERE
 | 
	
		
			
				|  |  | -                        "b"."revision_id" IN ({})
 | 
	
		
			
				|  |  | +                        "ta"."account_id" IN ({})
 | 
	
		
			
				|  |  | +                        AND "t"."transaction_id" = "ta"."transaction_id"
 | 
	
		
			
				|  |  |                          AND "t"."revision_id" = "b"."revision_id"
 | 
	
		
			
				|  |  |                          AND "t"."transaction_id" = "bt"."transaction_id"
 | 
	
		
			
				|  |  |                          {} {}
 | 
	
		
			
				|  |  | -                    ORDER BY "t"."created_at" DESC
 | 
	
		
			
				|  |  | -                    LIMIT ? OFFSET ?
 | 
	
		
			
				|  |  | +                    ORDER BY "ta"."created_at" DESC
 | 
	
		
			
				|  |  | +                    LIMIT {} OFFSET {}
 | 
	
		
			
				|  |  |                      "#,
 | 
	
		
			
				|  |  | -                "?,".repeat(rev_ids.len()).trim_end_matches(","),
 | 
	
		
			
				|  |  | +                "?,".repeat(account_ids.len()).trim_end_matches(","),
 | 
	
		
			
				|  |  |                  since,
 | 
	
		
			
				|  |  | -                until
 | 
	
		
			
				|  |  | +                until,
 | 
	
		
			
				|  |  | +                limit,
 | 
	
		
			
				|  |  | +                cursor.filter.skip
 | 
	
		
			
				|  |  |              );
 | 
	
		
			
				|  |  |              let mut query = sqlx::query(&sql);
 | 
	
		
			
				|  |  | -            for id in rev_ids.iter() {
 | 
	
		
			
				|  |  | +            for id in account_ids.iter() {
 | 
	
		
			
				|  |  |                  query = query.bind(id.to_string());
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  | -            query
 | 
	
		
			
				|  |  | -                .bind(limit as i64)
 | 
	
		
			
				|  |  | -                .bind(cursor.skip as i64)
 | 
	
		
			
				|  |  | -                .fetch_all(executor)
 | 
	
		
			
				|  |  | -                .await
 | 
	
		
			
				|  |  | +            query.fetch_all(executor).await
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |          _ => todo!(),
 | 
	
		
			
				|  |  |      };
 | 
	
	
		
			
				|  | @@ -147,7 +178,7 @@ where
 | 
	
		
			
				|  |  |          })
 | 
	
		
			
				|  |  |          .collect::<Result<Vec<_>, Error>>()?;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    cursor.skip += results.len();
 | 
	
		
			
				|  |  | +    cursor.filter.skip += results.len();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      Ok(results)
 | 
	
		
			
				|  |  |  }
 | 
	
	
		
			
				|  | @@ -192,7 +223,6 @@ impl SQLite {
 | 
	
		
			
				|  |  |          CREATE TABLE IF NOT EXISTS "transactions_by_tags" (
 | 
	
		
			
				|  |  |              "transaction_id" VARCHAR(67),
 | 
	
		
			
				|  |  |              "tag" VARCHAR(250) NOT NULL,
 | 
	
		
			
				|  |  | -            "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
 | 
	
		
			
				|  |  |              PRIMARY KEY ("transaction_id", "tag")
 | 
	
		
			
				|  |  |          );
 | 
	
		
			
				|  |  |          CREATE TABLE IF NOT EXISTS "payments" (
 | 
	
	
		
			
				|  | @@ -463,64 +493,6 @@ impl Storage for SQLite {
 | 
	
		
			
				|  |  |          .collect::<Result<Result<Vec<_>, _>, _>>()??)
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    async fn get_transaction_and_revision(
 | 
	
		
			
				|  |  | -        &self,
 | 
	
		
			
				|  |  | -        id: FilterId,
 | 
	
		
			
				|  |  | -    ) -> Result<(BaseTx, Revision), Error> {
 | 
	
		
			
				|  |  | -        let mut conn = self
 | 
	
		
			
				|  |  | -            .db
 | 
	
		
			
				|  |  | -            .acquire()
 | 
	
		
			
				|  |  | -            .await
 | 
	
		
			
				|  |  | -            .map_err(|e| Error::Storage(e.to_string()))?;
 | 
	
		
			
				|  |  | -        let row = sqlx::query(match id {
 | 
	
		
			
				|  |  | -            FilterId::LatestRevision(_) => {
 | 
	
		
			
				|  |  | -                r#"
 | 
	
		
			
				|  |  | -            SELECT
 | 
	
		
			
				|  |  | -                "bt"."blob",
 | 
	
		
			
				|  |  | -                "b"."blob"
 | 
	
		
			
				|  |  | -            FROM
 | 
	
		
			
				|  |  | -                "transactions" as "t",
 | 
	
		
			
				|  |  | -                "base_transactions" as "bt",
 | 
	
		
			
				|  |  | -                "revisions" as "b"
 | 
	
		
			
				|  |  | -            WHERE
 | 
	
		
			
				|  |  | -                "t"."transaction_id" = ?
 | 
	
		
			
				|  |  | -                AND "t"."revision_id" = "b"."revision_id"
 | 
	
		
			
				|  |  | -                AND "t"."transaction_id" = "bt"."transaction_id"
 | 
	
		
			
				|  |  | -        "#
 | 
	
		
			
				|  |  | -            }
 | 
	
		
			
				|  |  | -            FilterId::RevisionId(_) => {
 | 
	
		
			
				|  |  | -                r#"
 | 
	
		
			
				|  |  | -            SELECT
 | 
	
		
			
				|  |  | -                "bt"."blob",
 | 
	
		
			
				|  |  | -                "b"."blob"
 | 
	
		
			
				|  |  | -            FROM
 | 
	
		
			
				|  |  | -                "base_transactions" as "bt",
 | 
	
		
			
				|  |  | -                "revisions" as "b"
 | 
	
		
			
				|  |  | -            WHERE
 | 
	
		
			
				|  |  | -                "b"."revision_id" = ?
 | 
	
		
			
				|  |  | -                AND "b"."transaction_id" = "bt"."transaction_id"
 | 
	
		
			
				|  |  | -                "#
 | 
	
		
			
				|  |  | -            }
 | 
	
		
			
				|  |  | -        })
 | 
	
		
			
				|  |  | -        .bind(id.to_string())
 | 
	
		
			
				|  |  | -        .fetch_one(&mut *conn)
 | 
	
		
			
				|  |  | -        .await
 | 
	
		
			
				|  |  | -        .map_err(|e| Error::Storage(e.to_string()))?;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        let transaction = row
 | 
	
		
			
				|  |  | -            .try_get::<Vec<u8>, usize>(0)
 | 
	
		
			
				|  |  | -            .map_err(|e| Error::Storage(e.to_string()))?;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        let revision = row
 | 
	
		
			
				|  |  | -            .try_get::<Vec<u8>, usize>(1)
 | 
	
		
			
				|  |  | -            .map_err(|e| Error::Storage(e.to_string()))?;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        Ok((
 | 
	
		
			
				|  |  | -            from_slice::<BaseTx>(&transaction)?,
 | 
	
		
			
				|  |  | -            from_slice::<Revision>(&revision)?,
 | 
	
		
			
				|  |  | -        ))
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |      async fn find_base_tx_and_revision(
 | 
	
		
			
				|  |  |          &self,
 | 
	
		
			
				|  |  |          mut filter: Filter,
 | 
	
	
		
			
				|  | @@ -552,7 +524,6 @@ impl Storage for SQLite {
 | 
	
		
			
				|  |  |          let mut cursor = Cursor {
 | 
	
		
			
				|  |  |              primary_filter,
 | 
	
		
			
				|  |  |              filter,
 | 
	
		
			
				|  |  | -            skip: 0,
 | 
	
		
			
				|  |  |          };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          let candidate_limits = 20;
 | 
	
	
		
			
				|  | @@ -573,6 +544,20 @@ impl Storage for SQLite {
 | 
	
		
			
				|  |  |                  {
 | 
	
		
			
				|  |  |                      continue;
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  | +                if !cursor.filter.revisions.is_empty()
 | 
	
		
			
				|  |  | +                    && cursor
 | 
	
		
			
				|  |  | +                        .filter
 | 
	
		
			
				|  |  | +                        .revisions
 | 
	
		
			
				|  |  | +                        .binary_search(&candidate.1.rev_id().expect("vv"))
 | 
	
		
			
				|  |  | +                        .is_err()
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    continue;
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                if !cursor.filter.kind.is_empty()
 | 
	
		
			
				|  |  | +                    && cursor.filter.kind.binary_search(&candidate.0.typ).is_err()
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    continue;
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  |                  if !cursor.filter.tags.is_empty() {
 | 
	
		
			
				|  |  |                      let mut found = false;
 | 
	
		
			
				|  |  |                      for tag in candidate.1.tags.iter() {
 | 
	
	
		
			
				|  | @@ -595,91 +580,10 @@ impl Storage for SQLite {
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        Ok(if cursor.filter.limit != 0 {
 | 
	
		
			
				|  |  | -            results.split_off(cursor.filter.limit)
 | 
	
		
			
				|  |  | -        } else {
 | 
	
		
			
				|  |  | -            results
 | 
	
		
			
				|  |  | -        })
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    async fn get_transactions_with_latest_revision(
 | 
	
		
			
				|  |  | -        &self,
 | 
	
		
			
				|  |  | -        account: &AccountId,
 | 
	
		
			
				|  |  | -        types: &[Type],
 | 
	
		
			
				|  |  | -        _tags: &[Tag],
 | 
	
		
			
				|  |  | -    ) -> Result<Vec<(BaseTx, Revision)>, Error> {
 | 
	
		
			
				|  |  | -        let mut conn = self
 | 
	
		
			
				|  |  | -            .db
 | 
	
		
			
				|  |  | -            .acquire()
 | 
	
		
			
				|  |  | -            .await
 | 
	
		
			
				|  |  | -            .map_err(|e| Error::Storage(e.to_string()))?;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        let sql = if types.is_empty() {
 | 
	
		
			
				|  |  | -            r#"SELECT
 | 
	
		
			
				|  |  | -                "t"."transaction_id",
 | 
	
		
			
				|  |  | -                "t"."revision_id",
 | 
	
		
			
				|  |  | -                "bt"."blob",
 | 
	
		
			
				|  |  | -                "b"."blob"
 | 
	
		
			
				|  |  | -            FROM
 | 
	
		
			
				|  |  | -                "transaction_accounts" as "ta",
 | 
	
		
			
				|  |  | -                "base_transactions" as "bt",
 | 
	
		
			
				|  |  | -                "transactions" as "t",
 | 
	
		
			
				|  |  | -                "revisions" as "b"
 | 
	
		
			
				|  |  | -            WHERE
 | 
	
		
			
				|  |  | -                "ta"."account_id" = ?
 | 
	
		
			
				|  |  | -                AND "t"."transaction_id" = "ta"."transaction_id"
 | 
	
		
			
				|  |  | -                AND "t"."revision_id" = "b"."revision_id"
 | 
	
		
			
				|  |  | -                AND "t"."transaction_id" = "bt"."transaction_id"
 | 
	
		
			
				|  |  | -            ORDER BY "ta"."id" DESC"#
 | 
	
		
			
				|  |  | -                .to_owned()
 | 
	
		
			
				|  |  | -        } else {
 | 
	
		
			
				|  |  | -            let types = types
 | 
	
		
			
				|  |  | -                .into_iter()
 | 
	
		
			
				|  |  | -                .map(|t| u32::from(t).to_string())
 | 
	
		
			
				|  |  | -                .collect::<Vec<_>>()
 | 
	
		
			
				|  |  | -                .join(",");
 | 
	
		
			
				|  |  | -            format!(
 | 
	
		
			
				|  |  | -                r#"SELECT
 | 
	
		
			
				|  |  | -                    "t"."transaction_id",
 | 
	
		
			
				|  |  | -                    "t"."revision_id",
 | 
	
		
			
				|  |  | -                    "bt"."blob",
 | 
	
		
			
				|  |  | -                    "b"."blob"
 | 
	
		
			
				|  |  | -                FROM
 | 
	
		
			
				|  |  | -                    "transaction_accounts" as "ta",
 | 
	
		
			
				|  |  | -                    "base_transactions" as "bt",
 | 
	
		
			
				|  |  | -                    "transactions" as "t",
 | 
	
		
			
				|  |  | -                    "revisions" as "b"
 | 
	
		
			
				|  |  | -                WHERE
 | 
	
		
			
				|  |  | -                    "account_id" = ?
 | 
	
		
			
				|  |  | -                    AND "t"."transaction_id" = "ta"."transaction_id"
 | 
	
		
			
				|  |  | -                    AND "t"."revision_id" = "b"."revision_id"
 | 
	
		
			
				|  |  | -                    AND "t"."transaction_id" = "bt"."transaction_id"
 | 
	
		
			
				|  |  | -                    AND "ta"."type" IN ({types})
 | 
	
		
			
				|  |  | -                ORDER BY "ta"."id" DESC"#,
 | 
	
		
			
				|  |  | -            )
 | 
	
		
			
				|  |  | -        };
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        sqlx::query(&sql)
 | 
	
		
			
				|  |  | -            .bind(account.to_string())
 | 
	
		
			
				|  |  | -            .fetch_all(&mut *conn)
 | 
	
		
			
				|  |  | -            .await
 | 
	
		
			
				|  |  | -            .map_err(|e| Error::Storage(e.to_string()))?
 | 
	
		
			
				|  |  | -            .into_iter()
 | 
	
		
			
				|  |  | -            .map(|row| {
 | 
	
		
			
				|  |  | -                let base_tx = row
 | 
	
		
			
				|  |  | -                    .try_get::<Vec<u8>, usize>(2)
 | 
	
		
			
				|  |  | -                    .map_err(|e| Error::Storage(e.to_string()))?;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -                let revision = row
 | 
	
		
			
				|  |  | -                    .try_get::<Vec<u8>, usize>(3)
 | 
	
		
			
				|  |  | -                    .map_err(|e| Error::Storage(e.to_string()))?;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -                Ok((
 | 
	
		
			
				|  |  | -                    from_slice::<BaseTx>(&base_tx)?,
 | 
	
		
			
				|  |  | -                    from_slice::<Revision>(&revision)?,
 | 
	
		
			
				|  |  | -                ))
 | 
	
		
			
				|  |  | -            })
 | 
	
		
			
				|  |  | -            .collect::<Result<Vec<_>, _>>()
 | 
	
		
			
				|  |  | +        if cursor.filter.limit != 0 {
 | 
	
		
			
				|  |  | +            results.truncate(cursor.filter.limit);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        Ok(results)
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 |