Skip to content

Commit

Permalink
Link swaps and swap claims in the UI (#773)
Browse files Browse the repository at this point in the history
* Rename some vars

* Create new Pill and TransactionId components

* Populate transaction_ids_by_nullifier

* Render a link to the original swap from the swap claim

* Use new TransactionPerspective propertY

* Rewrite a bit

* Rewrite more concisely

* Refactor a bit

* Add todo

* Revert unneeded change

* Fix use of maps

* Fix typings

* Fix import issue

* Bump IDB version

* Revert unneeded changes

* Remove unneeded comment

* Revert idb version bump

* Address linting issues

* Add transactions table

* Incorporate Gabe's refactors

* Fix linter complaint
  • Loading branch information
jessepinho authored Mar 29, 2024
1 parent e273093 commit e202610
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 25 deletions.
40 changes: 21 additions & 19 deletions packages/query/src/block-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class BlockProcessor implements BlockProcessorInterface {

async identifyTransactions(
spentNullifiers: Set<Nullifier>,
commitmentRecords: Map<StateCommitment, SpendableNoteRecord | SwapRecord>,
commitmentRecordsByStateCommitment: Map<StateCommitment, SpendableNoteRecord | SwapRecord>,
blockTx: Transaction[],
) {
const relevantTx = new Map<TransactionId, Transaction>();
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -317,13 +319,13 @@ export class BlockProcessor implements BlockProcessorInterface {
private async resolveNullifiers(nullifiers: Nullifier[], height: bigint) {
const spentNullifiers = new Set<Nullifier>();

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;
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/indexed-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,5 @@ export const IDB_TABLES: Tables = {
epochs: 'EPOCHS',
prices: 'PRICES',
validator_infos: 'VALIDATOR_INFOS',
transactions: 'TRANSACTIONS',
};
24 changes: 22 additions & 2 deletions packages/ui/components/ui/tx/view/swap-claim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ViewBox
label='Swap Claim'
visibleContent={
/** @todo: Make a real UI for swap claims -- web#424 */
<JsonViewer jsonObj={value.swapClaimView.value.toJson() as JsonObject} />
<>
{swapTxId && (
<div>
<TransactionIdComponent
prefix={
<>
Swap
<SquareArrowRight size={16} className='ml-1' />
</>
}
transactionId={swapTxId}
shaClassName='font-mono ml-1'
/>
</div>
)}
{/** @todo: Make a real UI for swap claims -- web#424 */}
<JsonViewer jsonObj={value.swapClaimView.value.toJson() as JsonObject} />
</>
}
/>
);
Expand Down
18 changes: 18 additions & 0 deletions packages/ui/components/ui/tx/view/swap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -15,11 +17,27 @@ export const SwapViewComponent = ({ value }: { value: SwapView }) => {
addressView: { case: 'decoded', value: { address: claimAddress } },
});

const swapClaimTxId = value.swapView.value.claimTx;

return (
<ViewBox
label='Swap'
visibleContent={
<div className='flex flex-col gap-8'>
{swapClaimTxId && (
<div>
<TransactionIdComponent
prefix={
<>
Swap claim
<SquareArrowRight size={16} className='ml-1' />
</>
}
transactionId={swapClaimTxId}
shaClassName='font-mono ml-1'
/>
</div>
)}
<ActionDetails label='Asset 1'>
<ActionDetails.Row label='ID' truncate>
{uint8ArrayToBase64(tradingPair!.asset1!.inner)}
Expand Down
28 changes: 28 additions & 0 deletions packages/ui/components/ui/tx/view/transaction-id.tsx
Original file line number Diff line number Diff line change
@@ -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 <span> wrapping the SHA */
shaClassName?: string;
}) => {
const sha = uint8ArrayToHex(transactionId.inner);
return (
<Pill to={`/tx/${sha}`}>
{prefix}
<span className={shaClassName}>{shorten(sha, 8)}</span>
</Pill>
);
};
41 changes: 39 additions & 2 deletions packages/wasm/crate/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,6 +40,7 @@ pub struct Tables {
pub app_parameters: String,
pub gas_prices: String,
pub epochs: String,
pub transactions: String,
}

pub struct IndexedDBStorage {
Expand Down Expand Up @@ -269,6 +270,25 @@ impl IndexedDBStorage {
.map(serde_wasm_bindgen::from_value)
.transpose()?)
}

pub async fn get_swap_by_nullifier(
&self,
nullifier: &Nullifier,
) -> WasmResult<Option<SwapRecord>> {
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<Option<fmd::Parameters>> {
let tx = self
.db
Expand Down Expand Up @@ -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<Vec<TransactionInfo>> {
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)
}
}
88 changes: 86 additions & 2 deletions packages/wasm/crate/src/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<CommitmentSource, anyhow::Error> =
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?
Expand Down Expand Up @@ -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(())
}
2 changes: 2 additions & 0 deletions packages/wasm/crate/tests/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ mod tests {
app_parameters: String,
gas_prices: String,
epochs: String,
transactions: String,
}

// Define `IndexDB` table parameters and constants.
Expand All @@ -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 {
Expand Down

0 comments on commit e202610

Please sign in to comment.