Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 37 additions & 18 deletions crates/sage-database/src/tables/coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use chia_wallet_sdk::{
chia::puzzle_types::{LineageProof, Proof},
prelude::*,
};
use sqlx::{Row, SqliteExecutor, query};
use sqlx::{QueryBuilder, Row, Sqlite, SqliteExecutor, SqlitePool, query};

use crate::{
AssetKind, Convert, Database, DatabaseError, DatabaseTx, Result, SerializedDid,
Expand Down Expand Up @@ -619,7 +619,7 @@ async fn coins_by_ids(conn: impl SqliteExecutor<'_>, coin_ids: &[String]) -> Res
}

async fn coin_records(
conn: impl SqliteExecutor<'_>,
conn: &SqlitePool,
asset_filter: AssetFilter,
limit: u32,
offset: u32,
Expand All @@ -635,7 +635,7 @@ async fn coin_records(
CoinFilterMode::Clawback => "clawback_coins",
};

let mut query = sqlx::QueryBuilder::new(format!(
let mut query = QueryBuilder::new(format!(
"
SELECT
parent_coin_hash, puzzle_hash, amount,
Expand All @@ -647,18 +647,7 @@ async fn coin_records(
",
));

match asset_filter {
AssetFilter::Id(asset_id) => {
query.push(" WHERE asset_hash = ");
query.push_bind(asset_id.to_vec());
}
AssetFilter::Nfts => {
query.push(" WHERE asset_kind = 1");
}
AssetFilter::Dids => {
query.push(" WHERE asset_kind = 2");
}
}
push_asset_filter(&mut query, asset_filter);

query.push(" ORDER BY ");
match sort_mode {
Expand All @@ -680,9 +669,24 @@ async fn coin_records(
query.push_bind(offset as i64);

let rows = query.build().fetch_all(conn).await?;
let total_count = rows
.first()
.map_or(Ok(0), |row| row.get::<i64, _>("total_count").try_into())?;
let total_count = if let Some(row) = rows.first() {
row.get::<i64, _>("total_count").try_into()?
} else {
// COUNT(*) OVER() has no row from which to read the total when the
// requested offset is beyond the end of the filtered result set.
// Run the equivalent count query so callers can recover to the last
// available page.
let mut count_query =
QueryBuilder::new(format!("SELECT COUNT(*) AS total_count FROM {table}"));
push_asset_filter(&mut count_query, asset_filter);

count_query
.build()
.fetch_one(conn)
.await?
.get::<i64, _>("total_count")
.try_into()?
};
let coins = rows
.into_iter()
.map(|row| {
Expand Down Expand Up @@ -720,6 +724,21 @@ async fn coin_records(
Ok((coins, total_count))
}

fn push_asset_filter(query: &mut QueryBuilder<'_, Sqlite>, asset_filter: AssetFilter) {
match asset_filter {
AssetFilter::Id(asset_id) => {
query.push(" WHERE asset_hash = ");
query.push_bind(asset_id.to_vec());
}
AssetFilter::Nfts => {
query.push(" WHERE asset_kind = 1");
}
AssetFilter::Dids => {
query.push(" WHERE asset_kind = 2");
}
}
}

async fn selectable_xch_coins(conn: impl SqliteExecutor<'_>) -> Result<Vec<Coin>> {
query!("SELECT parent_coin_hash, puzzle_hash, amount FROM selectable_coins WHERE asset_id = 0")
.fetch_all(conn)
Expand Down
11 changes: 11 additions & 0 deletions src/components/OwnedCoinsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,19 @@ export function OwnedCoinsCard({
filter_mode: includeSpentCoins ? 'spent' : 'owned',
})
.then((res) => {
// Ignore a late response for a page the user has already left.
if (page !== currentPageRef.current) return;

setCoins(res.coins);
setTotalCoins(res.total);

// A combine can remove enough coins to make the current page
// invalid. Move to the last page that still exists after the
// refreshed total is known; the page effect will load it.
const lastPage = Math.max(0, Math.ceil(res.total / pageSize) - 1);
if (page > lastPage) {
setCurrentPage(lastPage);
}
})
.catch(addError);
},
Expand Down
Loading