Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI update/tx details link #596

Merged
merged 2 commits into from
Jan 24, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Cross from '@assets/svgX/close.svg';
import Arrow from '@assets/svgX/arrow-right.svg';
import Button from '@popup/popupX/shared/Button';
import Page from '@popup/popupX/shared/Page';
import { absoluteRoutes, transactionLogRoute } from '@popup/popupX/constants/routes';
import { absoluteRoutes, transactionDetailsRoute } from '@popup/popupX/constants/routes';
import Card from '@popup/popupX/shared/Card';
import { useAsyncMemo } from 'wallet-common-helpers';
import {
Expand Down Expand Up @@ -320,7 +320,7 @@ export default function SubmittedTransaction() {
label={t('detailsButton')}
className="submitted-tx__details-btn"
leftLabel
onClick={() => nav(transactionLogRoute(status.summary.sender))}
onClick={() => nav(transactionDetailsRoute(status.summary.sender, status.summary.hash))}
/>
)}
<Page.Footer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
display: flex;
flex-direction: column;
word-break: break-word;
white-space: pre-line;
margin-bottom: rem(4px);

.capture__main_small {
color: $color-mineral-3;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Navigate, useLocation, useParams } from 'react-router-dom';
import clsx from 'clsx';
import { TimeStampUnit, dateFromTimestamp, displayAsCcd } from 'wallet-common-helpers';
import { TimeStampUnit, dateFromTimestamp, displayAsCcd, noOp, useAsyncMemo } from 'wallet-common-helpers';
import { useAtomValue } from 'jotai';
import { TransactionHash, TransactionStatusEnum } from '@concordium/web-sdk';

import Copy from '@assets/svgX/copy.svg';
import ArrowSquareOut from '@assets/svgX/arrow-square-out.svg';
Expand All @@ -12,9 +14,13 @@ import Page from '@popup/popupX/shared/Page';
import Button from '@popup/popupX/shared/Button';
import * as CcdScan from '@popup/shared/utils/ccdscan';
import { useCopyToClipboard } from '@popup/popupX/shared/utils/hooks';
import { BrowserWalletTransaction, TransactionStatus } from '@popup/shared/utils/transaction-history-types';
import { useCredential } from '@popup/shared/utils/account-helpers';
import { WalletCredential } from '@shared/storage/types';
import {
BrowserWalletTransaction,
TransactionStatus,
toBrowserWalletTransaction,
} from '@popup/shared/utils/transaction-history-types';
import { grpcClientAtom } from '@popup/store/settings';

import { onlyTime, onlyDate, TransactionLogParams, mapTypeToText, hasAmount } from '../util';

/** State passed as part of the navigation */
Expand All @@ -25,20 +31,30 @@ export type TransactionDetailsLocationState = {

type TransactionDetailsProps = {
transaction: BrowserWalletTransaction;
account: WalletCredential;
account: string;
};

type Params = TransactionLogParams & {
transactionHash: string;
};

function TransactionDetails({ transaction, account }: TransactionDetailsProps) {
const { t } = useTranslation('x', { keyPrefix: 'transactionLogX' });
const copy = useCopyToClipboard();
const copyTransactionHash = useCallback(() => copy(transaction.transactionHash), []);
const copyTransactionHash = useCallback(() => {
if (transaction.transactionHash !== undefined) {
copy(transaction.transactionHash);
}
}, [transaction.transactionHash]);
const seeOnCcdScan = useCallback(() => {
CcdScan.openTransaction(transaction.transactionHash);
}, []);
if (transaction.transactionHash !== undefined) {
CcdScan.openTransaction(transaction.transactionHash);
}
}, [transaction.transactionHash]);

const isSender = account === transaction.fromAddress;
// Flip the amount if selected account is sender, and amount is positive. We expect the transaction list endpoint to sign the amount based on this,
// but this is not the case for pending transactions. This seeks to emulate the behaviour of the transaction list endpoint.
const isSender = account.address === transaction.fromAddress;
const amount =
isSender && transaction.status === TransactionStatus.Pending && transaction.amount > 0n
? -transaction.amount
Expand Down Expand Up @@ -92,7 +108,7 @@ function TransactionDetails({ transaction, account }: TransactionDetailsProps) {
<Card.RowDetails title={t('details.tHash')} value={transaction.transactionHash} />
)}
<Card.RowDetails title={t('details.bHash')} value={transaction.blockHash} />
{transaction.events !== undefined && (
{transaction.events !== undefined && transaction.events.length !== 0 && (
<Card.Row>
<div className="transaction-details__card_row">
<Text.Capture>{t('details.events')}</Text.Capture>
Expand All @@ -110,19 +126,40 @@ function TransactionDetails({ transaction, account }: TransactionDetailsProps) {
);
}

export default function Loader() {
const params = useParams<TransactionLogParams>();
const account = useCredential(params.account);
export default function Container() {
const { account, transactionHash } = useParams<Params>();
const location = useLocation();
if (
typeof location.state !== 'object' ||
location.state === null ||
!('transaction' in location.state) ||
account === undefined
) {
const grpc = useAtomValue(grpcClientAtom);

const transaction = useAsyncMemo(
async () => {
if (typeof location.state === 'object' && location.state !== null && 'transaction' in location.state) {
return (location.state as TransactionDetailsLocationState).transaction;
}

if (transactionHash === undefined || account === undefined) {
return null;
}

const blockItem = await grpc.getBlockItemStatus(TransactionHash.fromHexString(transactionHash));
if (blockItem.status !== TransactionStatusEnum.Finalized) {
return null;
}
return toBrowserWalletTransaction(blockItem, account, grpc);
},
noOp,
[grpc, transactionHash]
);

if (transaction === null || !account) {
// Necessary state not available
return <Navigate to="../" />;
}
const state = location.state as TransactionDetailsLocationState;
return <TransactionDetails account={account} transaction={state.transaction} />;

if (transaction === undefined) {
// We're still waiting for response
return null;
}

return <TransactionDetails account={account} transaction={transaction} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { WalletCredential } from '@shared/storage/types';
import Text from '@popup/popupX/shared/Text';
import Page from '@popup/popupX/shared/Page';
import { useCredential } from '@popup/shared/utils/account-helpers';
import { relativeRoutes } from '@popup/popupX/constants/routes';
import { mainLayoutScrollContext } from '@popup/popupX/page-layouts/MainLayout/MainLayout';

import useTransactionGroups, { TransactionLogParams } from './util';
Expand Down Expand Up @@ -330,7 +329,7 @@ export default function Loader() {
const state: TransactionDetailsLocationState = {
transaction,
};
nav(relativeRoutes.home.transactionLog.details.path, { state });
nav(transaction.transactionHash ?? '__special__', { state });
};

return <TransactionList account={account} onTransactionClick={navToTransactionDetails} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
import { AccountTransactionType } from '@concordium/web-sdk';
import {
AccountTransactionSummary,
AccountTransactionType,
ConcordiumGRPCWebClient,
ContractTraceEvent,
BakerEvent,
DelegationEvent,
FinalizedBlockItem,
TransactionEvent,
TransactionEventTag,
TransactionKindString,
TransactionSummaryType,
getTransactionRejectReason,
} from '@concordium/web-sdk';
import JSONBig from 'json-bigint';

/**
* The interface for the result of a query to get historical transactions for
Expand Down Expand Up @@ -28,6 +42,66 @@ export enum RewardType {

export enum SpecialTransactionType {
Malformed = 'Malformed',
Other = 'Other',
}

function mapTransactionKindStringToTransactionType(
kind: TransactionKindString
): AccountTransactionType | RewardType | SpecialTransactionType {
switch (kind) {
case TransactionKindString.DeployModule:
return AccountTransactionType.DeployModule;
case TransactionKindString.InitContract:
return AccountTransactionType.InitContract;
case TransactionKindString.Update:
return AccountTransactionType.Update;
case TransactionKindString.Transfer:
return AccountTransactionType.Transfer;
case TransactionKindString.AddBaker:
return AccountTransactionType.AddBaker;
case TransactionKindString.RemoveBaker:
return AccountTransactionType.RemoveBaker;
case TransactionKindString.UpdateBakerStake:
return AccountTransactionType.UpdateBakerStake;
case TransactionKindString.UpdateBakerRestakeEarnings:
return AccountTransactionType.UpdateBakerRestakeEarnings;
case TransactionKindString.UpdateBakerKeys:
return AccountTransactionType.UpdateBakerKeys;
case TransactionKindString.UpdateCredentialKeys:
return AccountTransactionType.UpdateCredentialKeys;
case TransactionKindString.BakingReward:
return RewardType.BakingReward;
case TransactionKindString.BlockReward:
return RewardType.BlockReward;
case TransactionKindString.FinalizationReward:
return RewardType.FinalizationReward;
case TransactionKindString.EncryptedAmountTransfer:
return AccountTransactionType.EncryptedAmountTransfer;
case TransactionKindString.TransferToEncrypted:
return AccountTransactionType.TransferToEncrypted;
case TransactionKindString.TransferToPublic:
return AccountTransactionType.TransferToPublic;
case TransactionKindString.TransferWithSchedule:
return AccountTransactionType.TransferWithSchedule;
case TransactionKindString.UpdateCredentials:
return AccountTransactionType.UpdateCredentials;
case TransactionKindString.RegisterData:
return AccountTransactionType.RegisterData;
case TransactionKindString.TransferWithMemo:
return AccountTransactionType.TransferWithMemo;
case TransactionKindString.EncryptedAmountTransferWithMemo:
return AccountTransactionType.EncryptedAmountTransferWithMemo;
case TransactionKindString.TransferWithScheduleAndMemo:
return AccountTransactionType.TransferWithScheduleAndMemo;
case TransactionKindString.ConfigureBaker:
return AccountTransactionType.ConfigureBaker;
case TransactionKindString.ConfigureDelegation:
return AccountTransactionType.ConfigureDelegation;
case TransactionKindString.StakingReward:
return RewardType.StakingReward;
default:
throw Error(`Unknown transaction kind was encounted: ${kind}`);
}
}

/**
Expand All @@ -54,6 +128,133 @@ export interface BrowserWalletAccountTransaction extends BrowserWalletTransactio
type: AccountTransactionType;
}

function getTransactionAmount(account: string, summary: AccountTransactionSummary): bigint {
switch (summary.transactionType) {
case TransactionKindString.InitContract:
return summary.contractInitialized.amount.microCcdAmount;
case TransactionKindString.Update:
return summary.events.reduce((acc: bigint, e) => {
switch (e.tag) {
case TransactionEventTag.Updated:
return acc - e.amount.microCcdAmount;
case TransactionEventTag.Transferred: {
if (e.to.address === account) {
return acc + e.amount.microCcdAmount;
}
return acc;
}
default:
return acc;
}
}, 0n);
case TransactionKindString.Transfer:
case TransactionKindString.TransferWithMemo:
return summary.sender.address === account
? -summary.transfer.amount.microCcdAmount
: summary.transfer.amount.microCcdAmount;
case TransactionKindString.TransferWithSchedule: {
const amount = summary.event.amount.reduce((acc, release) => acc + release.amount.microCcdAmount, 0n);
return summary.sender.address === account ? -amount : amount;
}
case TransactionKindString.TransferWithScheduleAndMemo: {
const amount = summary.transfer.amount.reduce((acc, release) => acc + release.amount.microCcdAmount, 0n);
return summary.sender.address === account ? -amount : amount;
}
default:
return 0n;
}
}

function getTransactionReceiver(summary: AccountTransactionSummary): string | undefined {
switch (summary.transactionType) {
case TransactionKindString.Transfer:
case TransactionKindString.TransferWithMemo:
return summary.transfer.to.address;
default:
return undefined;
}
}

function getTransactionMemo(summary: AccountTransactionSummary): string | undefined {
switch (summary.transactionType) {
case TransactionKindString.TransferWithMemo:
case TransactionKindString.TransferWithScheduleAndMemo:
case TransactionKindString.EncryptedAmountTransferWithMemo:
return summary.memo.memo;
default:
return undefined;
}
}

function getTransactionEvents(summary: AccountTransactionSummary): string[] | undefined {
const toString = ({ tag, ...rest }: TransactionEvent | ContractTraceEvent | BakerEvent | DelegationEvent) =>
`${tag}\n${JSONBig.stringify(rest)}`;

switch (summary.transactionType) {
case TransactionKindString.Update:
case TransactionKindString.ConfigureBaker:
case TransactionKindString.ConfigureDelegation:
return summary.events.map(toString);
default:
return undefined;
}
}

export async function toBrowserWalletTransaction(
{ outcome: { blockHash, summary } }: FinalizedBlockItem,
account: string,
grpc: ConcordiumGRPCWebClient
): Promise<BrowserWalletTransaction> {
const block = await grpc.getBlockInfo(blockHash);
const time = BigInt(Math.round(block.blockSlotTime.getTime() / 1000));
const id = -1;

if (summary.type !== TransactionSummaryType.AccountTransaction) {
return {
blockHash: blockHash.toString(),
transactionHash: summary.hash.toString(),
fromAddress: undefined,
toAddress: undefined,
amount: 0n,
id,
type: SpecialTransactionType.Other,
time,
status: TransactionStatus.Finalized,
};
}

let type: AccountTransactionType | RewardType | SpecialTransactionType;
if (summary.transactionType === TransactionKindString.Failed && summary.failedTransactionType === undefined) {
type = SpecialTransactionType.Malformed;
} else {
type = mapTransactionKindStringToTransactionType(
summary.transactionType === TransactionKindString.Failed && summary.failedTransactionType !== undefined
? summary.failedTransactionType
: summary.transactionType
);
}
const status =
summary.transactionType === TransactionKindString.Failed
? TransactionStatus.Failed
: TransactionStatus.Finalized;

return {
blockHash: blockHash.toString(),
transactionHash: summary.hash.toString(),
fromAddress: summary.sender.address,
toAddress: getTransactionReceiver(summary),
amount: getTransactionAmount(account, summary),
id,
type,
time,
status,
cost: summary.cost,
rejectReason: getTransactionRejectReason(summary)?.tag,
memo: getTransactionMemo(summary),
events: getTransactionEvents(summary),
};
}

export function isAccountTransaction(
transaction: BrowserWalletTransaction
): transaction is BrowserWalletAccountTransaction {
Expand Down
Loading