diff --git a/explorer/client/src/__tests__/e2e.test.ts b/explorer/client/src/__tests__/e2e.test.ts
index d00a2aa8aff41..afe4345417d56 100644
--- a/explorer/client/src/__tests__/e2e.test.ts
+++ b/explorer/client/src/__tests__/e2e.test.ts
@@ -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'
+ );
+ });
+ });
});
diff --git a/explorer/client/src/components/transaction-card/RecentTxCard.tsx b/explorer/client/src/components/transaction-card/RecentTxCard.tsx
index d701442f0d80b..f0d1f3ac28dfd 100644
--- a/explorer/client/src/components/transaction-card/RecentTxCard.tsx
+++ b/explorer/client/src/components/transaction-card/RecentTxCard.tsx
@@ -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';
@@ -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';
@@ -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,
}: {
@@ -211,7 +141,7 @@ function LatestTxView({
styles.txstatus
)}
>
- {tx.status === 'success' ? '✔' : '✖'}
+ {tx.status === 'success' ? '\u2714' : '\u2716'}
{tx.txGas}
diff --git a/explorer/client/src/components/transactions-for-id/TxForID.module.css b/explorer/client/src/components/transactions-for-id/TxForID.module.css
new file mode 100644
index 0000000000000..4aad411667144
--- /dev/null
+++ b/explorer/client/src/components/transactions-for-id/TxForID.module.css
@@ -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;
+}
diff --git a/explorer/client/src/components/transactions-for-id/TxForID.tsx b/explorer/client/src/components/transactions-for-id/TxForID.tsx
new file mode 100644
index 0000000000000..d70fa8f67a41b
--- /dev/null
+++ b/explorer/client/src/components/transactions-for-id/TxForID.tsx
@@ -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
=> rpc(network).getTransactionsForAddress(id);
+
+function TxForIDView({ showData }: { showData: TxnData[] | undefined }) {
+ if (!showData || showData.length === 0) return <>>;
+
+ return (
+ <>
+
+
Transactions
+
+
+
TxId
+
TxType
+
Status
+
Addresses
+
+
+ {showData.map((x, index) => (
+
+
+
+
+
{x.kind}
+
+ {x.status === 'success' ? '\u2714' : '\u2716'}
+
+
+
+ From:
+
+ {truncate(x.From, 14, '...')}
+
+
+ {x.To && (
+
+ To :
+
+ {truncate(x.To, 14, '...')}
+
+
+ )}
+
+
+ ))}
+
+
+ >
+ );
+}
+
+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 ;
+}
+
+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 Loading ...
;
+ }
+
+ if (showData.loadState === 'loaded') {
+ const data = showData.data;
+ return ;
+ }
+
+ return (
+
+ );
+}
+
+export default function TxForID({
+ id,
+ category,
+}: {
+ id: string;
+ category: 'address';
+}) {
+ return IS_STATIC_ENV ? (
+
+ ) : (
+
+ );
+}
diff --git a/explorer/client/src/pages/address-result/AddressResult.tsx b/explorer/client/src/pages/address-result/AddressResult.tsx
index 111ffe4782199..ca5fa0e3f061e 100644
--- a/explorer/client/src/pages/address-result/AddressResult.tsx
+++ b/explorer/client/src/pages/address-result/AddressResult.tsx
@@ -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 = {
@@ -38,6 +39,7 @@ function AddressResult() {
/>
+
Owned Objects
diff --git a/explorer/client/src/utils/api/DefaultRpcClient.ts b/explorer/client/src/utils/api/DefaultRpcClient.ts
index 042fecc537f21..ea8f2ae969919 100644
--- a/explorer/client/src/utils/api/DefaultRpcClient.ts
+++ b/explorer/client/src/utils/api/DefaultRpcClient.ts
@@ -1,10 +1,25 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
-import { JsonRpcProvider } from '@mysten/sui.js';
+import {
+ getExecutionStatusType,
+ getTotalGasUsed,
+ getTransactions,
+ getTransactionDigest,
+ getTransactionKindName,
+ getTransferCoinTransaction,
+ JsonRpcProvider,
+} from '@mysten/sui.js';
+import { deduplicate } from '../searchUtil';
import { getEndpoint, Network } from './rpcSetting';
+import type {
+ CertifiedTransaction,
+ TransactionEffectsResponse,
+ GetTxnDigestsResponse,
+} from '@mysten/sui.js';
+
// TODO: Remove these types with SDK types
export type AddressBytes = number[];
export type AddressOwner = { AddressOwner: AddressBytes };
@@ -16,3 +31,53 @@ export { Network, getEndpoint };
export const DefaultRpcClient = (network: Network | string) =>
new JsonRpcProvider(getEndpoint(network));
+
+export const getDataOnTxDigests = (
+ network: Network | string,
+ transactions: GetTxnDigestsResponse
+) =>
+ DefaultRpcClient(network)
+ .getTransactionWithEffectsBatch(deduplicate(transactions))
+ .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
+ .filter((itm) => itm)
+ .sort((a, b) => b!.seq - a!.seq)
+ );
+ });
diff --git a/explorer/client/src/utils/searchUtil.ts b/explorer/client/src/utils/searchUtil.ts
index 38f61df0ae04f..1b0e3cd49c9d0 100644
--- a/explorer/client/src/utils/searchUtil.ts
+++ b/explorer/client/src/utils/searchUtil.ts
@@ -1,5 +1,11 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
+const deduplicate = (results: [number, string][] | undefined) =>
+ results
+ ? results
+ .map((result) => result[1])
+ .filter((value, index, self) => self.indexOf(value) === index)
+ : [];
let navigateWithUnknown: Function;
let overrideTypeChecks = false;
@@ -15,4 +21,4 @@ if (process.env.REACT_APP_DATA === 'static') {
);
}
-export { navigateWithUnknown, overrideTypeChecks };
+export { navigateWithUnknown, overrideTypeChecks, deduplicate };
diff --git a/explorer/client/src/utils/static/latest_transactions.json b/explorer/client/src/utils/static/latest_transactions.json
index 5c4f3d93d9dcd..742446e0a34f6 100644
--- a/explorer/client/src/utils/static/latest_transactions.json
+++ b/explorer/client/src/utils/static/latest_transactions.json
@@ -5,7 +5,7 @@
"To": "receiverAddress",
"kind": "Transfer",
"seq": 7787,
- "status": "Success",
+ "status": "success",
"txGas": 41,
"txId": "Da4vHc9IwbvOYblE8LnrVsqXwryt2Kmms+xnJ7Zx5E4="
},
@@ -14,9 +14,18 @@
"To": "receiverAddress",
"kind": "Transfer",
"seq": 7787,
- "status": "Failure",
+ "status": "failure",
"txGas": 41,
"txId": "GHTP9gcFmF5KTspnz3KxXjvSH8Bx0jv68KFhdqfpdK8="
+ },
+ {
+ "From": "senderAddress",
+ "To": "receiverAddress",
+ "kind": "Transfer",
+ "seq": 7787,
+ "status": "success",
+ "txGas": 41,
+ "txId": "XHTP9gcFmF5KTspnz3KxXjvSH8Bx0jv68KFhdqfpdK8="
}
]
}
diff --git a/explorer/client/src/utils/static/searchUtil.ts b/explorer/client/src/utils/static/searchUtil.ts
index 440a13363cd1d..217bcb038ac4b 100644
--- a/explorer/client/src/utils/static/searchUtil.ts
+++ b/explorer/client/src/utils/static/searchUtil.ts
@@ -4,6 +4,7 @@
import latestTxData from './latest_transactions.json';
import mockData from './mock_data.json';
import mockOwnedObjectData from './owned_object.json';
+import mockTxData from './tx_for_id.json';
const navigateWithUnknown = async (
input: string,
@@ -34,9 +35,17 @@ const findOwnedObjectsfromID = (targetID: string | undefined) =>
const getAllMockTransaction = () => latestTxData.data;
+const findTxfromID = (targetID: string | undefined) =>
+ mockTxData!.data!.find(({ id }) => id === targetID);
+
+const findTxDatafromID = (targetID: string | undefined) =>
+ latestTxData!.data!.find(({ txId }) => txId === targetID);
+
export {
findDataFromID,
navigateWithUnknown,
findOwnedObjectsfromID,
+ findTxfromID,
+ findTxDatafromID,
getAllMockTransaction,
};
diff --git a/explorer/client/src/utils/static/tx_for_id.json b/explorer/client/src/utils/static/tx_for_id.json
new file mode 100644
index 0000000000000..8c32769d6bb50
--- /dev/null
+++ b/explorer/client/src/utils/static/tx_for_id.json
@@ -0,0 +1,16 @@
+{
+ "data": [
+ {
+ "id": "ownsAllAddress",
+ "data": [
+ [0, "Da4vHc9IwbvOYblE8LnrVsqXwryt2Kmms+xnJ7Zx5E4="],
+ [1, "Da4vHc9IwbvOYblE8LnrVsqXwryt2Kmms+xnJ7Zx5E4="],
+ [0, "GHTP9gcFmF5KTspnz3KxXjvSH8Bx0jv68KFhdqfpdK8="],
+ [1, "GHTP9gcFmF5KTspnz3KxXjvSH8Bx0jv68KFhdqfpdK8="],
+ [2, "Da4vHc9IwbvOYblE8LnrVsqXwryt2Kmms+xnJ7Zx5E4="],
+ [3, "GHTP9gcFmF5KTspnz3KxXjvSH8Bx0jv68KFhdqfpdK8="],
+ [4, "XHTP9gcFmF5KTspnz3KxXjvSH8Bx0jv68KFhdqfpdK8="]
+ ]
+ }
+ ]
+}
diff --git a/explorer/client/src/utils/stringUtils.ts b/explorer/client/src/utils/stringUtils.ts
index 4e53fde9d2f48..99ee995a903d0 100644
--- a/explorer/client/src/utils/stringUtils.ts
+++ b/explorer/client/src/utils/stringUtils.ts
@@ -40,6 +40,23 @@ function transformURL(url: string) {
return `https://ipfs.io/ipfs/${found[1]}`;
}
+export 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)
+ );
+}
+
/* Currently unused but potentially useful:
*
* export const isValidHttpUrl = (url: string) => {
diff --git a/sdk/typescript/src/providers/json-rpc-provider.ts b/sdk/typescript/src/providers/json-rpc-provider.ts
index 7fd06eca44176..9eaf2b8a4dbf4 100644
--- a/sdk/typescript/src/providers/json-rpc-provider.ts
+++ b/sdk/typescript/src/providers/json-rpc-provider.ts
@@ -8,7 +8,7 @@ import {
isGetOwnedObjectsResponse,
isGetTxnDigestsResponse,
isTransactionEffectsResponse,
- isTransactionResponse,
+ isTransactionResponse
} from '../index.guard';
import {
GatewayTxSeqNumber,
@@ -17,7 +17,7 @@ import {
SuiObjectInfo,
TransactionDigest,
TransactionEffectsResponse,
- TransactionResponse,
+ TransactionResponse
} from '../types';
const isNumber = (val: any): val is number => typeof val === 'number';
@@ -91,6 +91,30 @@ export class JsonRpcProvider extends Provider {
}
}
+ //Addresses
+ async getTransactionsForAddress(addressID: string): Promise {
+ const requests = [
+ {
+ method: 'sui_getTransactionsToAddress',
+ args: [addressID]
+ },
+ {
+ method: 'sui_getTransactionsFromAddress',
+ args: [addressID]
+ }
+ ]
+
+ try {
+ const results = await this.client.batchRequestWithType(
+ requests,
+ isGetTxnDigestsResponse
+ )
+ return [...results[0], ...results[1]];
+ } catch (err) {
+ throw new Error(`Error getting transactions for address: ${err} for id ${addressID}`)
+ }
+ }
+
// Transactions
async getTransactionWithEffects(
digest: TransactionDigest