Skip to content

Commit

Permalink
feat/1442 - Extension: Updates to MASP Tx Details (#1506)
Browse files Browse the repository at this point in the history
* fix: better detection for transfer type

* feat: notes inputs and outputs in details

* fix: wrapper fee payer type

---------

Co-authored-by: Mateusz Jasiuk <[email protected]>
  • Loading branch information
jurevans and mateuszjasiuk authored Jan 27, 2025
1 parent aa522c1 commit 4ee780f
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 30 deletions.
8 changes: 6 additions & 2 deletions apps/extension/src/Approvals/ApproveSignTx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,14 @@ export const ApproveSignTx: React.FC<Props> = ({ details, setDetails }) => {
{!displayTransactionData && (
<Stack gap={1}>
{details?.txDetails?.map(
({ commitments }) =>
({ commitments, wrapperFeePayer }) =>
commitments?.length &&
commitments.map((tx, i) => (
<Commitment key={`${tx.hash}-${i}`} commitment={tx} />
<Commitment
key={`${tx.hash}-${i}`}
commitment={tx}
wrapperFeePayer={wrapperFeePayer}
/>
))
)}
</Stack>
Expand Down
35 changes: 21 additions & 14 deletions apps/extension/src/Approvals/Commitment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,12 @@ import { FaVoteYea } from "react-icons/fa";
import { FaRegEye, FaWallet } from "react-icons/fa6";
import { GoStack } from "react-icons/go";
import { PiDotsNineBold } from "react-icons/pi";
import {
hasShieldedSection,
parseTransferType,
ShieldedPoolLabel,
} from "utils";
import { isShieldedPool, parseTransferType, ShieldedPoolLabel } from "utils";
import { TransactionCard } from "./TransactionCard";

type CommitmentProps = {
commitment: CommitmentDetailProps;
wrapperFeePayer: string;
};

const IconMap: Record<TxType, React.ReactNode> = {
Expand Down Expand Up @@ -59,7 +56,13 @@ const TitleMap: Record<TxType, string> = {
const formatAddress = (address: string): string =>
shortenAddress(address, 6, 6);

const renderContent = (tx: CommitmentDetailProps): ReactNode => {
const formatTransferAddress = (address: string): string =>
isShieldedPool(address) ? ShieldedPoolLabel : formatAddress(address);

const renderContent = (
tx: CommitmentDetailProps,
wrapperFeePayer: string
): ReactNode => {
switch (tx.txType) {
case TxType.Bond:
const bondTx = tx as BondProps;
Expand Down Expand Up @@ -116,13 +119,11 @@ const renderContent = (tx: CommitmentDetailProps): ReactNode => {

case TxType.Transfer:
const transferTx = tx as TransferProps;
const { source, target } = parseTransferType(transferTx);
const { source, target } = parseTransferType(transferTx, wrapperFeePayer);
return (
<>
Transfer from {formatAddress(source)} to{" "}
{hasShieldedSection(transferTx) ?
ShieldedPoolLabel
: formatAddress(target)}
Transfer from {formatTransferAddress(source)} to{" "}
{formatTransferAddress(target)}
</>
);

Expand All @@ -132,20 +133,26 @@ const renderContent = (tx: CommitmentDetailProps): ReactNode => {
}
};

export const Commitment = ({ commitment }: CommitmentProps): JSX.Element => {
export const Commitment = ({
commitment,
wrapperFeePayer,
}: CommitmentProps): JSX.Element => {
const txType: TxType = commitment.txType as TxType;
let title = TitleMap[txType];

if (commitment.txType === TxType.Transfer) {
// Determine specific transfer type
const { type } = parseTransferType(commitment as TransferProps);
const { type } = parseTransferType(
commitment as TransferProps,
wrapperFeePayer
);
title = `${type} ${title}`;
}

return (
<TransactionCard
title={title}
content={renderContent(commitment)}
content={renderContent(commitment, wrapperFeePayer)}
icon={IconMap[txType]}
/>
);
Expand Down
3 changes: 2 additions & 1 deletion apps/extension/src/Approvals/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ export type TransferType =
| "Transparent"
| "Shielding"
| "Shielded"
| "Unshielding";
| "Unshielding"
| "Unknown";
43 changes: 36 additions & 7 deletions apps/extension/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,28 +133,57 @@ export const ShieldedPoolLabel = "the shielded pool";
export const hasShieldedSection = (tx: TransferProps): boolean => {
return Boolean(tx.shieldedSectionHash);
};
export const isShieldedPool = (address: string): boolean => {
const shieldedPoolRegex = /qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/;
return Boolean(address.match(shieldedPoolRegex));
};

/**
* Create label to indicate specific type of Transfer
* @param tx TransferProps
* @returns string label
*/
export const parseTransferType = (
tx: TransferProps
tx: TransferProps,
wrapperFeePayer?: string
): { source: string; target: string; type: TransferType } => {
const { sources, targets } = tx;
const source = sources[0].owner;
const target = targets[0].owner;
const fromMasp = isShieldedPool(source);

const unshieldingToPayFees = Boolean(
fromMasp && targets.find((t) => t.owner === wrapperFeePayer)
);
const isUnshielding =
fromMasp && unshieldingToPayFees ?
// If we're unshielding to pay fees, we should have one more target
targets.length === sources.length + 1
// Otherwise, we should have the same number of targets as sources
: targets.length === sources.length;

const isShieldedTransfer =
fromMasp && unshieldingToPayFees ?
// If we're unshielding to pay fees, we should have exactly one source and one target
targets.length === 1 && sources.length === 1
// Otherwise, we should have no targets and no sources, as everything is in the shielded pool
: targets.length === 0 && sources.length === 0;

let type: TransferType = "Transparent";
const txHasShieldedSection = hasShieldedSection(tx);

if (source.startsWith("tnam") && txHasShieldedSection) {
type = "Shielding";
} else if (source.startsWith("znam") && txHasShieldedSection) {
type = "Shielded";
} else if (source.startsWith("znam") && target.startsWith("tnam")) {
type = "Unshielding";
if (txHasShieldedSection) {
if (isShieldedPool(source)) {
if (isShieldedTransfer) {
type = "Shielded";
} else if (isUnshielding) {
type = "Unshielding";
} else {
type = "Unknown";
}
} else if (isShieldedPool(target)) {
type = "Shielding";
}
}

return {
Expand Down
6 changes: 5 additions & 1 deletion packages/sdk/src/tx/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,12 +466,16 @@ export class Tx {

return {
...wrapperTx,
// Wrapper fee payer is always defined at this point
wrapperFeePayer: wrapperTx.wrapperFeePayer!,
commitments: commitments.map(
({ txType, hash, txCodeId, data, memo }) => ({
({ txType, hash, txCodeId, data, memo, maspTxIn, maspTxOut }) => ({
txType: txType as TxType,
hash,
txCodeId,
memo,
maspTxIn,
maspTxOut,
...getProps(txType, data),
})
),
Expand Down
1 change: 1 addition & 0 deletions packages/shared/lib/src/sdk/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,7 @@ pub struct IbcTransferMsg {
}

impl IbcTransferMsg {
#[allow(clippy::too_many_arguments)]
pub fn new(
source: String,
receiver: String,
Expand Down
12 changes: 9 additions & 3 deletions packages/shared/lib/src/sdk/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct ProgressStart {

impl ProgressStart {
pub fn to_json(&self) -> JsValue {
let json = serde_json::to_value(&self).unwrap();
let json = serde_json::to_value(self).unwrap();
JsValue::from_str(&json.to_string())
}
}
Expand All @@ -24,7 +24,7 @@ pub struct ProgressFinish {

impl ProgressFinish {
pub fn to_json(&self) -> JsValue {
let json = serde_json::to_value(&self).unwrap();
let json = serde_json::to_value(self).unwrap();
JsValue::from_str(&json.to_string())
}
}
Expand All @@ -39,7 +39,7 @@ pub struct ProgressIncrement {

impl ProgressIncrement {
pub fn to_json(&self) -> JsValue {
let json = serde_json::to_value(&self).unwrap();
let json = serde_json::to_value(self).unwrap();
JsValue::from_str(&json.to_string())
}
}
Expand Down Expand Up @@ -149,3 +149,9 @@ impl EventDispatcher {
self.dispatch_custom_event(event)
}
}

impl Default for EventDispatcher {
fn default() -> Self {
Self::new()
}
}
2 changes: 1 addition & 1 deletion packages/shared/lib/src/sdk/masp/masp_web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl WebShieldedUtils {

/// Clear all shielded context data from the database.
pub async fn clear(chain_id: &str) -> Result<(), Error> {
let db = Self::build_database(&chain_id).await?;
let db = Self::build_database(chain_id).await?;
let entry = format!("{}-{}", SHIELDED_CONTEXT_PREFIX, chain_id);

db.transaction(&[entry.clone()], TransactionMode::ReadWrite)?
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/lib/src/sdk/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl TransactionKind {
pub fn from(tx_type: TxType, data: &[u8]) -> Self {
match tx_type {
TxType::Transfer => TransactionKind::Transfer(
Transfer::try_from_slice(data).expect("Cannot deserialize TransparentTransfer"),
Transfer::try_from_slice(data).expect("Cannot deserialize Transfer"),
),
TxType::Bond => {
TransactionKind::Bond(Bond::try_from_slice(data).expect("Cannot deserialize Bond"))
Expand Down
89 changes: 89 additions & 0 deletions packages/shared/lib/src/sdk/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ use std::str::FromStr;
use gloo_utils::format::JsValueSerdeExt;
use namada_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use namada_sdk::masp_primitives::transaction::components::sapling::builder::StoredBuildParams;
use namada_sdk::masp_primitives::transaction::components::sapling::fees::{InputView, OutputView};
use namada_sdk::masp_primitives::zip32::ExtendedFullViewingKey;
use namada_sdk::signing::SigningTxData;
use namada_sdk::token::{Amount, DenominatedAmount};
use namada_sdk::tx::data::compute_inner_tx_hash;
use namada_sdk::tx::either::Either;
use namada_sdk::tx::{
Expand All @@ -14,6 +16,7 @@ use namada_sdk::tx::{
};
use namada_sdk::uint::Uint;
use namada_sdk::{address::Address, key::common::PublicKey};
use namada_sdk::{ExtendedViewingKey, PaymentAddress};
use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue};

use super::args::WrapperTxMsg;
Expand Down Expand Up @@ -253,6 +256,22 @@ pub fn deserialize_tx(tx_bytes: Vec<u8>, wasm_hashes: JsValue) -> Result<Vec<u8>
Ok(borsh::to_vec(&tx)?)
}

#[derive(BorshSerialize, BorshDeserialize)]
#[borsh(crate = "namada_sdk::borsh")]
pub struct TxIn {
pub token: String,
pub value: String,
pub owner: String,
}

#[derive(BorshSerialize, BorshDeserialize)]
#[borsh(crate = "namada_sdk::borsh")]
pub struct TxOut {
pub token: String,
pub value: String,
pub address: String,
}

#[derive(BorshSerialize, BorshDeserialize)]
#[borsh(crate = "namada_sdk::borsh")]
pub struct Commitment {
Expand All @@ -261,6 +280,8 @@ pub struct Commitment {
tx_code_id: String,
data: Vec<u8>,
memo: Option<String>,
masp_tx_in: Option<Vec<TxIn>>,
masp_tx_out: Option<Vec<TxOut>>,
}

#[derive(BorshSerialize, BorshDeserialize)]
Expand Down Expand Up @@ -326,12 +347,16 @@ impl TxDetails {
let tx_kind = transaction::TransactionKind::from(tx_type, &tx_data);
let data = tx_kind.to_bytes()?;

let (inputs, outputs) = get_masp_details(&tx, &tx_kind);

commitments.push(Commitment {
tx_type,
hash,
tx_code_id,
data,
memo,
masp_tx_out: outputs,
masp_tx_in: inputs,
});
}
}
Expand All @@ -347,6 +372,70 @@ impl TxDetails {
}
}

fn get_masp_details(
tx: &tx::Tx,
tx_kind: &transaction::TransactionKind,
) -> (Option<Vec<TxIn>>, Option<Vec<TxOut>>) {
match tx_kind {
transaction::TransactionKind::Transfer(transfer) => {
if let Some(shielded_hash) = transfer.shielded_section_hash {
let masp_builder = tx
.get_masp_builder(&shielded_hash)
.expect("Masp builder to exist");

let asset_types = &masp_builder.asset_types;

let inputs = masp_builder
.builder
.sapling_inputs()
.iter()
.map(|input| {
let asset_data = asset_types
.iter()
.find(|ad| ad.encode().unwrap() == input.asset_type())
.expect("Asset data to exist");

let amount = Amount::from_u64(input.value());
let denominated_amount = DenominatedAmount::new(amount, asset_data.denom);

TxIn {
token: asset_data.token.to_string(),
value: denominated_amount.to_string(),
owner: ExtendedViewingKey::from(*input.key()).to_string(),
}
})
.collect::<Vec<_>>();

let outputs = masp_builder
.builder
.sapling_outputs()
.iter()
.map(|output| {
let asset_data = asset_types
.iter()
.find(|ad| ad.encode().unwrap() == output.asset_type())
.expect("Asset data to exist");

let amount = Amount::from_u64(output.value());
let denominated_amount = DenominatedAmount::new(amount, asset_data.denom);

TxOut {
token: { asset_data.token.to_string() },
value: denominated_amount.to_string(),
address: PaymentAddress::from(output.address()).to_string(),
}
})
.collect::<Vec<_>>();

(Some(inputs), Some(outputs))
} else {
(None, None)
}
}
_ => (None, None),
}
}

#[wasm_bindgen]
#[derive(BorshSerialize, BorshDeserialize)]
#[borsh(crate = "namada_sdk::borsh")]
Expand Down
Loading

0 comments on commit 4ee780f

Please sign in to comment.