Skip to content
Open
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
4 changes: 3 additions & 1 deletion crates/sage-api/src/records/pending_transaction.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};

use crate::Amount;
use crate::{Amount, TransactionCoinRecord};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "tauri", derive(specta::Type))]
Expand All @@ -9,4 +9,6 @@ pub struct PendingTransactionRecord {
pub transaction_id: String,
pub fee: Amount,
pub submitted_at: Option<u64>,
pub spent: Vec<TransactionCoinRecord>,
pub created: Vec<TransactionCoinRecord>,
}
110 changes: 108 additions & 2 deletions crates/sage-database/src/tables/mempool_items.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use chia_wallet_sdk::prelude::*;
use sqlx::{SqliteConnection, SqliteExecutor, query};
use sqlx::{Row, SqliteConnection, SqliteExecutor, query};

use crate::{Convert, Database, DatabaseTx, Result};
use crate::tables::transactions::create_transaction_coin;
use crate::{Convert, Database, DatabaseTx, Result, TransactionCoin};

#[derive(Debug, Clone)]
pub struct MempoolItem {
Expand All @@ -11,6 +12,15 @@ pub struct MempoolItem {
pub submitted_timestamp: Option<u64>,
}

#[derive(Debug, Clone)]
pub struct MempoolItemWithCoins {
pub hash: Bytes32,
pub fee: u64,
pub submitted_timestamp: Option<u64>,
pub spent: Vec<TransactionCoin>,
pub created: Vec<TransactionCoin>,
}

impl Database {
pub async fn mempool_items_to_submit(
&self,
Expand All @@ -31,6 +41,10 @@ impl Database {
pub async fn mempool_items(&self) -> Result<Vec<MempoolItem>> {
mempool_items(&self.pool).await
}

pub async fn mempool_items_with_coins(&self) -> Result<Vec<MempoolItemWithCoins>> {
mempool_items_with_coins(&self.pool).await
}
}

impl DatabaseTx<'_> {
Expand Down Expand Up @@ -330,3 +344,95 @@ async fn mempool_items(conn: impl SqliteExecutor<'_>) -> Result<Vec<MempoolItem>
})
.collect()
}

async fn mempool_items_with_coins(
conn: impl SqliteExecutor<'_>,
) -> Result<Vec<MempoolItemWithCoins>> {
use std::collections::HashMap;

let rows = sqlx::query(
"
SELECT
mempool_items.hash AS item_hash,
mempool_items.fee AS item_fee,
mempool_items.submitted_timestamp,
coins.puzzle_hash,
coins.parent_coin_hash,
coins.amount,
mempool_coins.is_input,
mempool_coins.is_output,
p2_puzzles.hash AS p2_puzzle_hash,
assets.hash AS asset_hash,
assets.name AS asset_name,
assets.ticker AS asset_ticker,
assets.precision AS asset_precision,
assets.icon_url AS asset_icon_url,
assets.kind AS asset_kind,
assets.description AS asset_description,
assets.is_visible AS asset_is_visible,
assets.is_sensitive_content AS asset_is_sensitive_content,
assets.hidden_puzzle_hash AS asset_hidden_puzzle_hash
FROM mempool_items
INNER JOIN mempool_coins ON mempool_coins.mempool_item_id = mempool_items.id
INNER JOIN coins ON coins.id = mempool_coins.coin_id
INNER JOIN assets ON assets.id = coins.asset_id
LEFT JOIN p2_puzzles ON p2_puzzles.id = coins.p2_puzzle_id
ORDER BY mempool_items.submitted_timestamp DESC, mempool_items.hash ASC
",
)
.fetch_all(conn)
.await?;

// Group rows by mempool item hash, preserving order
let mut order: Vec<Bytes32> = Vec::new();
#[allow(clippy::type_complexity)]
let mut items: HashMap<
Bytes32,
(u64, Option<u64>, Vec<TransactionCoin>, Vec<TransactionCoin>),
> = HashMap::new();

for row in &rows {
let item_hash: Bytes32 = row.get::<Vec<u8>, _>("item_hash").convert()?;
let is_input: bool = row.get("is_input");
let is_output: bool = row.get("is_output");

let transaction_coin = create_transaction_coin(row)?;

if !items.contains_key(&item_hash) {
let fee: u64 = row.get::<Vec<u8>, _>("item_fee").convert()?;
let submitted_timestamp: Option<i64> = row.get("submitted_timestamp");
order.push(item_hash);
items.insert(
item_hash,
(
fee,
submitted_timestamp.map(|ts| ts as u64),
Vec::new(),
Vec::new(),
),
);
}

let entry = items.get_mut(&item_hash).unwrap();
if is_input {
entry.2.push(transaction_coin.clone());
}
if is_output {
entry.3.push(transaction_coin);
}
}

Ok(order
.into_iter()
.map(|hash| {
let (fee, submitted_timestamp, spent, created) = items.remove(&hash).unwrap();
MempoolItemWithCoins {
hash,
fee,
submitted_timestamp,
spent,
created,
}
})
.collect())
}
6 changes: 3 additions & 3 deletions crates/sage-database/src/tables/transactions.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use chia_wallet_sdk::prelude::*;
use sqlx::{Row, SqliteExecutor};

use crate::{Asset, Convert, Database, Result};
use crate::{Asset, AssetKind, Convert, Database, Result};

#[derive(Debug, Clone)]
pub struct Transaction {
Expand Down Expand Up @@ -35,7 +35,7 @@ impl Database {
}

// Helper function to create a TransactionCoin from a database row
fn create_transaction_coin(row: &sqlx::sqlite::SqliteRow) -> Result<TransactionCoin> {
pub(crate) fn create_transaction_coin(row: &sqlx::sqlite::SqliteRow) -> Result<TransactionCoin> {
let coin = Coin::new(
row.get::<Vec<u8>, _>("parent_coin_hash").convert()?,
row.get::<Vec<u8>, _>("puzzle_hash").convert()?,
Expand All @@ -55,7 +55,7 @@ fn create_transaction_coin(row: &sqlx::sqlite::SqliteRow) -> Result<TransactionC
.get::<Option<i64>, _>("asset_kind")
.map(Convert::convert)
.transpose()?
.unwrap_or(crate::AssetKind::Token),
.unwrap_or(AssetKind::Token),
hidden_puzzle_hash: row
.get::<Option<Vec<u8>>, _>("asset_hidden_puzzle_hash")
.convert()?,
Expand Down
22 changes: 17 additions & 5 deletions crates/sage/src/endpoints/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,14 +494,26 @@ impl Sage {

let transactions = wallet
.db
.mempool_items()
.mempool_items_with_coins()
.await?
.into_iter()
.map(|tx| {
.map(|item| {
let spent = item
.spent
.into_iter()
.map(|c| self.transaction_coin(c))
.collect::<Result<Vec<_>>>()?;
let created = item
.created
.into_iter()
.map(|c| self.transaction_coin(c))
.collect::<Result<Vec<_>>>()?;
Result::Ok(PendingTransactionRecord {
transaction_id: hex::encode(tx.hash),
fee: Amount::u64(tx.fee),
submitted_at: tx.submitted_timestamp,
transaction_id: hex::encode(item.hash),
fee: Amount::u64(item.fee),
submitted_at: item.submitted_timestamp,
spent,
created,
})
})
.collect::<Result<Vec<_>>>()?;
Expand Down
2 changes: 1 addition & 1 deletion src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2030,7 +2030,7 @@ export type OptionAssets = { underlying_asset: Asset; underlying_amount: Amount;
export type OptionRecord = { launcher_id: string; name: string | null; visible: boolean; coin_id: string; address: string; amount: Amount; underlying_asset: Asset; underlying_amount: Amount; underlying_coin_id: string; strike_asset: Asset; strike_amount: Amount; expiration_seconds: number; created_height: number | null; created_timestamp: number | null }
export type OptionSortMode = "name" | "created_height" | "expiration_seconds"
export type PeerRecord = { ip_addr: string; port: number; peak_height: number; user_managed: boolean }
export type PendingTransactionRecord = { transaction_id: string; fee: Amount; submitted_at: number | null }
export type PendingTransactionRecord = { transaction_id: string; fee: Amount; submitted_at: number | null; spent: TransactionCoinRecord[]; created: TransactionCoinRecord[] }
/**
* Perform database maintenance operations
*/
Expand Down
31 changes: 22 additions & 9 deletions src/components/TransactionColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ import { AssetIcon } from './AssetIcon';
import { AssetLink } from './AssetLink';

export interface FlattenedTransaction {
transactionHeight: number;
type: AssetKind;
iconUrl?: string | null;
amount: string;
address: string | null;
itemId: string;
displayName: string;
transactionHeight: number;
timestamp: number | null;
precision: number;
groupKey: string; // unique group identifier: String(height) for confirmed, transactionId for pending
isPending?: boolean;
transactionId?: string;
}

export const columns: ColumnDef<FlattenedTransaction>[] = [
Expand All @@ -39,17 +42,27 @@ export const columns: ColumnDef<FlattenedTransaction>[] = [
enableSorting: false,
size: 140,
cell: ({ row, table }) => {
// Get all rows data
const rows = table.options.data as FlattenedTransaction[];

// Check if this is the first row for this transaction height
// Show only once per group (first row in the group)
const isFirstInGroup =
rows.findIndex(
(tx) => tx.transactionHeight === row.original.transactionHeight,
) === rows.indexOf(row.original);
rows.findIndex((tx) => tx.groupKey === row.original.groupKey) ===
rows.indexOf(row.original);

// Only show block number for first row in group
return isFirstInGroup ? (
if (!isFirstInGroup) return null;

if (row.original.isPending) {
return (
<div className='flex items-center gap-1.5 animate-pulse'>
<div className='h-2 w-2 rounded-full bg-amber-500' />
<span className='text-amber-600 text-sm font-medium'>
<Trans>Pending</Trans>
</span>
</div>
);
}

return (
<Link
to={`/transactions/${row.getValue('transactionHeight')}`}
className='hover:underline'
Expand All @@ -61,7 +74,7 @@ export const columns: ColumnDef<FlattenedTransaction>[] = [
{formatTimestamp(row.original?.timestamp, 'short', 'short') ||
row.getValue('transactionHeight')}
</Link>
) : null;
);
},
},
{
Expand Down
Loading
Loading