Skip to content

Commit

Permalink
See list of transactions done by an address (MystenLabs#2276)
Browse files Browse the repository at this point in the history
* lists transactions sent from/to address

* improves wording

* lists transactions as clickable links

* deduplicate transactions list

* creates TxForID component

* creates test and static data

* adds license

* improve error text

* improves handling when no to/from field

* adds more info on types

* refactoring

* removes split into transaction groups

* displays TxId, TxType and Status for each Tx - note static mode suspended

* improves handling when method not defined

* stylizes tx table

* improves txtype format

* enables static mode to show what fail looks like with success

* adds from/to

* truncates addresses

* update tests
  • Loading branch information
apburnie authored Jun 10, 2022
1 parent a7ec54b commit 38e1200
Show file tree
Hide file tree
Showing 12 changed files with 393 additions and 83 deletions.
12 changes: 12 additions & 0 deletions explorer/client/src/__tests__/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,4 +402,16 @@ describe('End-to-end Tests', () => {
).toBe('Balance200');
});
});
describe('Transactions for ID', () => {
it('are displayed deduplicated from and to', async () => {
const address = 'ownsAllAddress';
await page.goto(`${BASE_URL}/addresses/${address}`);
const fromResults = await cssInteract(page)
.with('#tx')
.get.textContent();
expect(fromResults.replace(/\s/g, '')).toBe(
'TxIdTxTypeStatusAddressesDa4vHc9IwbvOYblE8LnrVsqXwryt2Kmms+xnJ7Zx5E4=Transfer\u2714From:senderAddressTo:receiv...dressGHTP9gcFmF5KTspnz3KxXjvSH8Bx0jv68KFhdqfpdK8=Transfer\u2716From:senderAddressTo:receiv...dressXHTP9gcFmF5KTspnz3KxXjvSH8Bx0jv68KFhdqfpdK8=Transfer\u2714From:senderAddressTo:receiv...dress'
);
});
});
});
84 changes: 7 additions & 77 deletions explorer/client/src/components/transaction-card/RecentTxCard.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import {
getExecutionStatusType,
getTotalGasUsed,
getTransactions,
getTransactionDigest,
getTransactionKindName,
getTransferCoinTransaction,
} from '@mysten/sui.js';
import cl from 'classnames';
import { useEffect, useState, useContext } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
Expand All @@ -19,16 +11,16 @@ import theme from '../../styles/theme.module.css';
import {
DefaultRpcClient as rpc,
type Network,
getDataOnTxDigests,
} from '../../utils/api/DefaultRpcClient';
import { IS_STATIC_ENV } from '../../utils/envUtil';
import { getAllMockTransaction } from '../../utils/static/searchUtil';
import { truncate } from '../../utils/stringUtils';
import ErrorResult from '../error-result/ErrorResult';
import Pagination from '../pagination/Pagination';

import type {
CertifiedTransaction,
GetTxnDigestsResponse,
TransactionEffectsResponse,
ExecutionStatusType,
TransactionKindName,
} from '@mysten/sui.js';
Expand Down Expand Up @@ -88,81 +80,19 @@ async function getRecentTransactions(
if (endGatewayTxSeqNumber < 0) {
throw new Error('Invalid transaction number');
}
const transactions = await rpc(network)
return (await rpc(network)
.getTransactionDigestsInRange(
startGatewayTxSeqNumber,
endGatewayTxSeqNumber
)
.then((res: GetTxnDigestsResponse) => res);

const digests = transactions.map((tx) => tx[1]);

const txLatest = await rpc(network)
.getTransactionWithEffectsBatch(digests)
.then((txEffs: TransactionEffectsResponse[]) => {
return txEffs.map((txEff, i) => {
const [seq, digest] = transactions.filter(
(transactionId) =>
transactionId[1] ===
getTransactionDigest(txEff.certificate)
)[0];
const res: CertifiedTransaction = txEff.certificate;
// TODO: handle multiple transactions
const txns = getTransactions(res);
if (txns.length > 1) {
console.error(
'Handling multiple transactions is not yet supported',
txEff
);
return null;
}
const txn = txns[0];
const txKind = getTransactionKindName(txn);
const recipient =
getTransferCoinTransaction(txn)?.recipient;

return {
seq,
txId: digest,
status: getExecutionStatusType(txEff),
txGas: getTotalGasUsed(txEff),
kind: txKind,
From: res.data.sender,
...(recipient
? {
To: recipient,
}
: {}),
};
});
});

// Remove failed transactions and sort by sequence number
return txLatest
.filter((itm) => itm)
.sort((a, b) => b!.seq - a!.seq) as TxnData[];
.then((res: GetTxnDigestsResponse) =>
getDataOnTxDigests(network, res)
)) as TxnData[];
} catch (error) {
throw error;
}
}

function truncate(fullStr: string, strLen: number, separator: string) {
if (fullStr.length <= strLen) return fullStr;

separator = separator || '...';

const sepLen = separator.length,
charsToShow = strLen - sepLen,
frontChars = Math.ceil(charsToShow / 2),
backChars = Math.floor(charsToShow / 2);

return (
fullStr.substr(0, frontChars) +
separator +
fullStr.substr(fullStr.length - backChars)
);
}

function LatestTxView({
results,
}: {
Expand Down Expand Up @@ -211,7 +141,7 @@ function LatestTxView({
styles.txstatus
)}
>
{tx.status === 'success' ? '' : ''}
{tx.status === 'success' ? '\u2714' : '\u2716'}
</div>
<div className={styles.txgas}>{tx.txGas}</div>
<div className={styles.txadd}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.txheader {
@apply hidden bg-offblack text-offwhite py-2;
}

.txheader,
.txrow {
@apply md:flex;
}

.txheader > div,
.txrow > div {
@apply md:ml-[2vw];
}

.txid {
@apply md:w-[35vw];
}

.txtype {
@apply md:w-[10vw];
}

.txstatus {
@apply md:w-[5vw];
}

.txadd a {
@apply no-underline text-sky-600 hover:text-sky-900 cursor-pointer break-all;
}

.failure {
@apply text-red-300;
}

.success {
@apply text-green-400;
}
183 changes: 183 additions & 0 deletions explorer/client/src/components/transactions-for-id/TxForID.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import {
type GetTxnDigestsResponse,
type ExecutionStatusType,
type TransactionKindName,
} from '@mysten/sui.js';
import cl from 'classnames';
import { useState, useEffect, useContext } from 'react';
import { Link } from 'react-router-dom';

import { NetworkContext } from '../../context';
import {
DefaultRpcClient as rpc,
getDataOnTxDigests,
} from '../../utils/api/DefaultRpcClient';
import { IS_STATIC_ENV } from '../../utils/envUtil';
import { deduplicate } from '../../utils/searchUtil';
import { findTxfromID, findTxDatafromID } from '../../utils/static/searchUtil';
import { truncate } from '../../utils/stringUtils';
import ErrorResult from '../error-result/ErrorResult';
import Longtext from '../longtext/Longtext';

import styles from './TxForID.module.css';

const DATATYPE_DEFAULT = {
loadState: 'pending',
};

type TxnData = {
seq: number;
txId: string;
status: ExecutionStatusType;
kind: TransactionKindName | undefined;
From: string;
To?: string;
};

const getTx = async (
id: string,
network: string,
category: 'address'
): Promise<GetTxnDigestsResponse> => rpc(network).getTransactionsForAddress(id);

function TxForIDView({ showData }: { showData: TxnData[] | undefined }) {
if (!showData || showData.length === 0) return <></>;

return (
<>
<div>
<div>Transactions</div>
<div id="tx">
<div className={styles.txheader}>
<div className={styles.txid}>TxId</div>
<div className={styles.txtype}>TxType</div>
<div className={styles.txstatus}>Status</div>
<div className={styles.txadd}>Addresses</div>
</div>

{showData.map((x, index) => (
<div key={`txid-${index}`} className={styles.txrow}>
<div className={styles.txid}>
<Longtext
text={x.txId}
category="transactions"
isLink={true}
/>
</div>
<div className={styles.txtype}>{x.kind}</div>
<div
className={cl(
styles.txstatus,
styles[x.status.toLowerCase()]
)}
>
{x.status === 'success' ? '\u2714' : '\u2716'}
</div>
<div className={styles.txadd}>
<div>
From:
<Link
className={styles.txlink}
to={'addresses/' + x.From}
>
{truncate(x.From, 14, '...')}
</Link>
</div>
{x.To && (
<div>
To :
<Link
className={styles.txlink}
to={'addresses/' + x.To}
>
{truncate(x.To, 14, '...')}
</Link>
</div>
)}
</div>
</div>
))}
</div>
</div>
</>
);
}

function TxForIDStatic({ id, category }: { id: string; category: 'address' }) {
const data = deduplicate(
findTxfromID(id)?.data as [number, string][] | undefined
)
.map((id) => findTxDatafromID(id))
.filter((x) => x !== undefined) as TxnData[];
if (!data) return <></>;
return <TxForIDView showData={data} />;
}

function TxForIDAPI({ id, category }: { id: string; category: 'address' }) {
const [showData, setData] =
useState<{ data?: TxnData[]; loadState: string }>(DATATYPE_DEFAULT);
const [network] = useContext(NetworkContext);
useEffect(() => {
getTx(id, network, category).then((transactions) => {
//If the API method does not exist, the transactions will be undefined
if (!transactions?.[0]) {
setData({
loadState: 'loaded',
});
} else {
getDataOnTxDigests(network, transactions)
.then((data) => {
const subData = data.map((el) => ({
seq: el!.seq,
txId: el!.txId,
status: el!.status,
kind: el!.kind,
From: el!.From,
To: el!.To,
}));
setData({
data: subData,
loadState: 'loaded',
});
})
.catch((error) => {
console.log(error);
setData({ ...DATATYPE_DEFAULT, loadState: 'fail' });
});
}
});
}, [id, network, category]);

if (showData.loadState === 'pending') {
return <div>Loading ...</div>;
}

if (showData.loadState === 'loaded') {
const data = showData.data;
return <TxForIDView showData={data} />;
}

return (
<ErrorResult
id={id}
errorMsg="Transactions could not be extracted on the following specified ID"
/>
);
}

export default function TxForID({
id,
category,
}: {
id: string;
category: 'address';
}) {
return IS_STATIC_ENV ? (
<TxForIDStatic id={id} category={category} />
) : (
<TxForIDAPI id={id} category={category} />
);
}
2 changes: 2 additions & 0 deletions explorer/client/src/pages/address-result/AddressResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useParams } from 'react-router-dom';
import ErrorResult from '../../components/error-result/ErrorResult';
import Longtext from '../../components/longtext/Longtext';
import OwnedObjects from '../../components/ownedobjects/OwnedObjects';
import TxForID from '../../components/transactions-for-id/TxForID';
import theme from '../../styles/theme.module.css';

type DataType = {
Expand Down Expand Up @@ -38,6 +39,7 @@ function AddressResult() {
/>
</div>
</div>
<TxForID id={addressID} category="address" />
<div>
<div>Owned Objects</div>
<div>
Expand Down
Loading

0 comments on commit 38e1200

Please sign in to comment.