diff --git a/packages/query/src/block-processor.ts b/packages/query/src/block-processor.ts index 44c5671922..d609cc64e0 100644 --- a/packages/query/src/block-processor.ts +++ b/packages/query/src/block-processor.ts @@ -84,7 +84,7 @@ export class BlockProcessor implements BlockProcessorInterface { async identifyTransactions( spentNullifiers: Set, - commitmentRecords: Map, + commitmentRecordsByStateCommitment: Map, blockTx: Transaction[], ) { const relevantTx = new Map(); @@ -115,25 +115,25 @@ export class BlockProcessor implements BlockProcessorInterface { } }); - for (const spentNf of spentNullifiers) { - if (txNullifiers.some(txNf => spentNf.equals(txNf))) { + for (const spentNullifier of spentNullifiers) { + if (txNullifiers.some(txNullifier => spentNullifier.equals(txNullifier))) { txId = new TransactionId({ inner: await sha256Hash(tx.toBinary()) }); relevantTx.set(txId, tx); - spentNullifiers.delete(spentNf); + spentNullifiers.delete(spentNullifier); } } - for (const [recCom, record] of commitmentRecords) { - if (txCommitments.some(txCom => recCom.equals(txCom))) { + for (const [stateCommitment, spendableNoteRecord] of commitmentRecordsByStateCommitment) { + if (txCommitments.some(txCommitment => stateCommitment.equals(txCommitment))) { txId ??= new TransactionId({ inner: await sha256Hash(tx.toBinary()) }); relevantTx.set(txId, tx); - if (blankTxSource.equals(record.source)) { - record.source = new CommitmentSource({ + if (blankTxSource.equals(spendableNoteRecord.source)) { + spendableNoteRecord.source = new CommitmentSource({ source: { case: 'transaction', value: { id: txId.inner } }, }); - recordsWithSources.push(record); + recordsWithSources.push(spendableNoteRecord); } - commitmentRecords.delete(recCom); + commitmentRecordsByStateCommitment.delete(stateCommitment); } } } @@ -208,8 +208,10 @@ export class BlockProcessor implements BlockProcessorInterface { // - update idb await this.identifyNewAssets(flush.newNotes); - for (const nr of flush.newNotes) recordsByCommitment.set(nr.noteCommitment!, nr); - for (const sr of flush.newSwaps) recordsByCommitment.set(sr.swapCommitment!, sr); + for (const spendableNoteRecord of flush.newNotes) + recordsByCommitment.set(spendableNoteRecord.noteCommitment!, spendableNoteRecord); + for (const swapRecord of flush.newSwaps) + recordsByCommitment.set(swapRecord.swapCommitment!, swapRecord); } // nullifiers on this block may match notes or swaps from db @@ -290,9 +292,9 @@ export class BlockProcessor implements BlockProcessorInterface { else throw new Error('Unexpected record type'); } - private async identifyNewAssets(newNotes: SpendableNoteRecord[]) { - for (const n of newNotes) { - const assetId = n.note?.value?.assetId; + private async identifyNewAssets(notes: SpendableNoteRecord[]) { + for (const note of notes) { + const assetId = note.note?.value?.assetId; if (!assetId) continue; await this.saveAndReturnMetadata(assetId); @@ -317,13 +319,13 @@ export class BlockProcessor implements BlockProcessorInterface { private async resolveNullifiers(nullifiers: Nullifier[], height: bigint) { const spentNullifiers = new Set(); - for (const nf of nullifiers) { + for (const nullifier of nullifiers) { const record = - (await this.indexedDb.getSpendableNoteByNullifier(nf)) ?? - (await this.indexedDb.getSwapByNullifier(nf)); + (await this.indexedDb.getSpendableNoteByNullifier(nullifier)) ?? + (await this.indexedDb.getSwapByNullifier(nullifier)); if (!record) continue; - spentNullifiers.add(nf); + spentNullifiers.add(nullifier); if (record instanceof SpendableNoteRecord) { record.heightSpent = height; diff --git a/packages/types/src/indexed-db.ts b/packages/types/src/indexed-db.ts index 9330cec7f4..f5952c0615 100644 --- a/packages/types/src/indexed-db.ts +++ b/packages/types/src/indexed-db.ts @@ -218,4 +218,5 @@ export const IDB_TABLES: Tables = { epochs: 'EPOCHS', prices: 'PRICES', validator_infos: 'VALIDATOR_INFOS', + transactions: 'TRANSACTIONS', }; diff --git a/packages/ui/components/ui/tx/view/swap-claim.tsx b/packages/ui/components/ui/tx/view/swap-claim.tsx index 28d70bbe4b..15a586d5a7 100644 --- a/packages/ui/components/ui/tx/view/swap-claim.tsx +++ b/packages/ui/components/ui/tx/view/swap-claim.tsx @@ -2,15 +2,35 @@ import { ViewBox } from './viewbox'; import { SwapClaimView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/dex/v1/dex_pb'; import { JsonViewer } from '../../json-viewer'; import { JsonObject } from '@bufbuild/protobuf'; +import { TransactionIdComponent } from './transaction-id'; +import { SquareArrowRight } from 'lucide-react'; export const SwapClaimViewComponent = ({ value }: { value: SwapClaimView }) => { if (value.swapClaimView.case === 'visible') { + const swapTxId = value.swapClaimView.value.swapTx; + return ( + <> + {swapTxId && ( +
+ + Swap + + + } + transactionId={swapTxId} + shaClassName='font-mono ml-1' + /> +
+ )} + {/** @todo: Make a real UI for swap claims -- web#424 */} + + } /> ); diff --git a/packages/ui/components/ui/tx/view/swap.tsx b/packages/ui/components/ui/tx/view/swap.tsx index a36a4ae302..5e3cfbeb4b 100644 --- a/packages/ui/components/ui/tx/view/swap.tsx +++ b/packages/ui/components/ui/tx/view/swap.tsx @@ -5,6 +5,8 @@ import { uint8ArrayToBase64 } from '@penumbra-zone/types/src/base64'; import { ActionDetails } from './action-details'; import { AddressView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; import { AddressViewComponent } from './address-view'; +import { TransactionIdComponent } from './transaction-id'; +import { SquareArrowRight } from 'lucide-react'; export const SwapViewComponent = ({ value }: { value: SwapView }) => { if (value.swapView.case === 'visible') { @@ -15,11 +17,27 @@ export const SwapViewComponent = ({ value }: { value: SwapView }) => { addressView: { case: 'decoded', value: { address: claimAddress } }, }); + const swapClaimTxId = value.swapView.value.claimTx; + return ( + {swapClaimTxId && ( +
+ + Swap claim + + + } + transactionId={swapClaimTxId} + shaClassName='font-mono ml-1' + /> +
+ )} {uint8ArrayToBase64(tradingPair!.asset1!.inner)} diff --git a/packages/ui/components/ui/tx/view/transaction-id.tsx b/packages/ui/components/ui/tx/view/transaction-id.tsx new file mode 100644 index 0000000000..b0e93ed5d6 --- /dev/null +++ b/packages/ui/components/ui/tx/view/transaction-id.tsx @@ -0,0 +1,28 @@ +import { TransactionId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/txhash/v1/txhash_pb'; +import { Pill } from '../../pill'; +import { uint8ArrayToHex } from '@penumbra-zone/types/src/hex'; +import { shorten } from '@penumbra-zone/types/src/string'; +import { ReactNode } from 'react'; + +/** + * Renders a SHA-256 hash of a transaction ID in a pill. + */ +export const TransactionIdComponent = ({ + transactionId, + prefix, + shaClassName, +}: { + transactionId: TransactionId; + /** Anything to render before the SHA, like a label and/or icon */ + prefix?: ReactNode; + /** Classes to apply to the wrapping the SHA */ + shaClassName?: string; +}) => { + const sha = uint8ArrayToHex(transactionId.inner); + return ( + + {prefix} + {shorten(sha, 8)} + + ); +}; diff --git a/packages/wasm/crate/src/storage.rs b/packages/wasm/crate/src/storage.rs index c13b371dad..205e0b4f6d 100644 --- a/packages/wasm/crate/src/storage.rs +++ b/packages/wasm/crate/src/storage.rs @@ -8,10 +8,10 @@ use indexed_db_futures::{ use penumbra_asset::asset::{self, Id, Metadata}; use penumbra_keys::keys::AddressIndex; use penumbra_num::Amount; -use penumbra_proto::core::{app::v1::AppParameters, component::sct::v1::Epoch}; use penumbra_proto::{ + core::{app::v1::AppParameters, component::sct::v1::Epoch}, crypto::tct::v1::StateCommitment, - view::v1::{NotesRequest, SwapRecord}, + view::v1::{NotesRequest, SwapRecord, TransactionInfo}, DomainType, }; use penumbra_sct::Nullifier; @@ -40,6 +40,7 @@ pub struct Tables { pub app_parameters: String, pub gas_prices: String, pub epochs: String, + pub transactions: String, } pub struct IndexedDBStorage { @@ -269,6 +270,25 @@ impl IndexedDBStorage { .map(serde_wasm_bindgen::from_value) .transpose()?) } + + pub async fn get_swap_by_nullifier( + &self, + nullifier: &Nullifier, + ) -> WasmResult> { + let tx = self.db.transaction_on_one(&self.constants.tables.swaps)?; + let store = tx.object_store(&self.constants.tables.swaps)?; + + Ok(store + .index("nullifier")? + .get_owned(base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + &nullifier.to_proto().inner, + ))? + .await? + .map(serde_wasm_bindgen::from_value) + .transpose()?) + } + pub async fn get_fmd_params(&self) -> WasmResult> { let tx = self .db @@ -316,4 +336,21 @@ impl IndexedDBStorage { .await? .and_then(|cursor| serde_wasm_bindgen::from_value(cursor.value()).ok())) } + + pub async fn get_transaction_infos(&self) -> WasmResult> { + let tx = self + .db + .transaction_on_one(&self.constants.tables.transactions)?; + let store = tx.object_store(&self.constants.tables.transactions)?; + + let mut records = Vec::new(); + let raw_values = store.get_all()?.await?; + + for raw_value in raw_values { + let record: TransactionInfo = serde_wasm_bindgen::from_value(raw_value)?; + records.push(record); + } + + Ok(records) + } } diff --git a/packages/wasm/crate/src/tx.rs b/packages/wasm/crate/src/tx.rs index d9659eaa0a..cab2b4f075 100644 --- a/packages/wasm/crate/src/tx.rs +++ b/packages/wasm/crate/src/tx.rs @@ -8,8 +8,10 @@ use penumbra_keys::FullViewingKey; use penumbra_proto::core::transaction::v1 as pb; use penumbra_proto::core::transaction::v1::{TransactionPerspective, TransactionView}; use penumbra_proto::DomainType; -use penumbra_tct::{Proof, StateCommitment}; +use penumbra_sct::{CommitmentSource, Nullifier}; +use penumbra_tct::{Position, Proof, StateCommitment}; use penumbra_transaction::plan::TransactionPlan; +use penumbra_transaction::txhash::TransactionId; use penumbra_transaction::Action; use penumbra_transaction::{AuthorizationData, Transaction, WitnessData}; use rand_core::OsRng; @@ -18,7 +20,7 @@ use serde_wasm_bindgen::Error; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; -use crate::error::WasmResult; +use crate::error::{WasmError, WasmResult}; use crate::storage::IndexedDBStorage; use crate::storage::IndexedDbConstants; use crate::utils; @@ -273,7 +275,44 @@ pub async fn transaction_info_inner( .insert(nullifier, spendable_note_record.note.clone()); } } + Action::Swap(swap) => { + let commitment = swap.body.payload.commitment; + + let swap_position_option = storage + .get_swap_by_commitment(commitment.into()) + .await? + .map(|swap_record| Position::from(swap_record.position)); + + if let Some(swap_position) = swap_position_option { + add_swap_info_to_perspective( + &storage, + &fvk, + &mut txp, + &commitment, + swap_position, + ) + .await?; + } + } Action::SwapClaim(claim) => { + let nullifier = claim.body.nullifier; + + storage + .get_swap_by_nullifier(&nullifier) + .await? + .and_then(|swap_record| swap_record.source) + .into_iter() + .try_for_each(|source| { + let commitment_source: Result = + source.try_into(); + if let Some(id) = commitment_source.unwrap().id() { + txp.creation_transaction_ids_by_nullifier + .insert(nullifier, TransactionId(id)); + } + + Some(()) + }); + let output_1_record = storage .get_note(&claim.body.output_1_commitment) .await? @@ -370,3 +409,48 @@ pub async fn transaction_info_inner( }; Ok(response) } + +async fn add_swap_info_to_perspective( + storage: &IndexedDBStorage, + fvk: &FullViewingKey, + txp: &mut penumbra_transaction::TransactionPerspective, + commitment: &StateCommitment, + swap_position: Position, +) -> Result<(), WasmError> { + let derived_nullifier_from_swap = + Nullifier::derive(fvk.nullifier_key(), swap_position, commitment); + + let transaction_infos = storage.get_transaction_infos().await?; + + for transaction_info in transaction_infos { + transaction_info + .transaction + .and_then(|transaction| transaction.body) + .iter() + .for_each(|body| { + for action in body.actions.iter() { + let tranasction_id = action + .action + .as_ref() + .and_then(|action| match action { + penumbra_proto::core::transaction::v1::action::Action::SwapClaim( + swap_claim, + ) => swap_claim.body.as_ref(), + _ => None, + }) + .and_then(|body| body.nullifier.as_ref()) + .filter(|&nullifier| nullifier == &derived_nullifier_from_swap.to_proto()) + .and(transaction_info.id.as_ref()) + .and_then(|id| TransactionId::try_from(id.clone()).ok()); + + if let Some(transaction_id) = tranasction_id { + txp.nullification_transaction_ids_by_commitment + .insert(*commitment, transaction_id); + break; + } + } + }); + } + + Ok(()) +} diff --git a/packages/wasm/crate/tests/build.rs b/packages/wasm/crate/tests/build.rs index 187742cf27..ce976a271f 100644 --- a/packages/wasm/crate/tests/build.rs +++ b/packages/wasm/crate/tests/build.rs @@ -92,6 +92,7 @@ mod tests { app_parameters: String, gas_prices: String, epochs: String, + transactions: String, } // Define `IndexDB` table parameters and constants. @@ -104,6 +105,7 @@ mod tests { app_parameters: "APP_PARAMETERS".to_string(), gas_prices: "GAS_PRICES".to_string(), epochs: "EPOCHS".to_string(), + transactions: "TRANSACTIONS".to_string(), }; let constants: IndexedDbConstants = IndexedDbConstants {