diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 0ff1a58fb62b..ab1e3e300740 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -4372,6 +4372,9 @@
"recoveryPhraseReminderTitle": {
"message": "Protect your funds"
},
+ "redeposit": {
+ "message": "Redeposit"
+ },
"refreshList": {
"message": "Refresh list"
},
diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json
index 4ecab44a1a2f..96d15277911e 100644
--- a/app/_locales/en_GB/messages.json
+++ b/app/_locales/en_GB/messages.json
@@ -4372,6 +4372,9 @@
"recoveryPhraseReminderTitle": {
"message": "Protect your funds"
},
+ "redeposit": {
+ "message": "Redeposit"
+ },
"refreshList": {
"message": "Refresh list"
},
diff --git a/app/images/bitcoin-testnet-logo.svg b/app/images/bitcoin-testnet-logo.svg
new file mode 100644
index 000000000000..336e8600e893
--- /dev/null
+++ b/app/images/bitcoin-testnet-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shared/constants/multichain/networks.ts b/shared/constants/multichain/networks.ts
index acecbcebbeaf..ee1b7c0907da 100644
--- a/shared/constants/multichain/networks.ts
+++ b/shared/constants/multichain/networks.ts
@@ -28,6 +28,7 @@ export type MultichainProviderConfig = ProviderConfigWithImageUrl & {
// NOTE: For now we use a callback to check if the address is compatible with
// the given network or not
isAddressCompatible: (address: string) => boolean;
+ decimals: number;
};
export type MultichainNetworkIds = `${MultichainNetworks}`;
@@ -55,6 +56,8 @@ export const MULTICHAIN_NETWORK_TO_NICKNAME: Record = {
};
export const BITCOIN_TOKEN_IMAGE_URL = './images/bitcoin-logo.svg';
+export const BITCOIN_TESTNET_TOKEN_IMAGE_URL =
+ './images/bitcoin-testnet-logo.svg';
export const SOLANA_TOKEN_IMAGE_URL = './images/solana-logo.svg';
export const BITCOIN_BLOCK_EXPLORER_URL = 'https://mempool.space';
@@ -94,6 +97,7 @@ export const MULTICHAIN_NETWORK_BLOCK_EXPLORER_FORMAT_URLS_MAP: Record<
export const MULTICHAIN_TOKEN_IMAGE_MAP: Record = {
[MultichainNetworks.BITCOIN]: BITCOIN_TOKEN_IMAGE_URL,
+ [MultichainNetworks.BITCOIN_TESTNET]: BITCOIN_TESTNET_TOKEN_IMAGE_URL,
[MultichainNetworks.SOLANA]: SOLANA_TOKEN_IMAGE_URL,
} as const;
@@ -111,6 +115,7 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record<
nickname: 'Bitcoin',
id: 'btc-mainnet',
type: 'rpc',
+ decimals: 8,
rpcPrefs: {
imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.BITCOIN],
blockExplorerUrl:
@@ -131,8 +136,9 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record<
nickname: 'Bitcoin (testnet)',
id: 'btc-testnet',
type: 'rpc',
+ decimals: 8,
rpcPrefs: {
- imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.BITCOIN],
+ imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.BITCOIN_TESTNET],
blockExplorerUrl:
MULTICHAIN_NETWORK_BLOCK_EXPLORER_FORMAT_URLS_MAP[
MultichainNetworks.BITCOIN_TESTNET
@@ -154,6 +160,7 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record<
nickname: 'Solana',
id: 'solana-mainnet',
type: 'rpc',
+ decimals: 5,
rpcPrefs: {
imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.SOLANA],
blockExplorerUrl:
@@ -174,6 +181,7 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record<
nickname: 'Solana (devnet)',
id: 'solana-devnet',
type: 'rpc',
+ decimals: 5,
rpcPrefs: {
imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.SOLANA],
blockExplorerUrl:
@@ -194,6 +202,7 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record<
nickname: 'Solana (testnet)',
id: 'solana-testnet',
type: 'rpc',
+ decimals: 5,
rpcPrefs: {
imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.SOLANA],
blockExplorerUrl:
diff --git a/shared/constants/transaction.ts b/shared/constants/transaction.ts
index ac4bb77b070a..f95e8d119226 100644
--- a/shared/constants/transaction.ts
+++ b/shared/constants/transaction.ts
@@ -118,6 +118,10 @@ export enum TransactionGroupCategory {
* where the final token is sent to another chain.
*/
bridge = 'bridge',
+ /**
+ * Transaction group representing a redeposit (a send to ourselves), mainly used for consolidation.
+ */
+ redeposit = 'redeposit',
}
/**
diff --git a/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.stories.tsx b/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.stories.tsx
index 9279233e6123..63ee12422e81 100644
--- a/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.stories.tsx
+++ b/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.stories.tsx
@@ -6,45 +6,57 @@ export default {
};
const mockTransaction = {
- type: 'Send BTC',
- status: 'Confirmed',
+ type: 'send',
+ status: 'confirmed',
timestamp: new Date('Sep 30 2023 12:56').getTime(),
id: 'b93ea2cb4eed0f9e13284ed8860bcfc45de2488bb6a8b0b2a843c4b2fbce40f3',
- from: [{
- address: "bc1p7atgm33ak04ntsq9366mvym42ecrk4y34ssysc99340a39eq9arq0pu9uj",
- asset: {
- amount: '1.2',
- unit: 'BTC',
- }
- }],
- to: [{
- address: "bc1p3t7744qewy262ym5afgeuqlwswtpfe22y7c4lwv0a7972p2k73msee7rr3",
- asset: {
- amount: '1.2',
- unit: 'BTC',
- }
- }],
- fees: [{
- type: 'base',
- asset: {
- amount: '1.0001',
- unit: 'BTC',
- }
- }]
+ chain: 'bip122:000000000019d6689c085ae165831e93',
+ account: 'test-account-id',
+ from: [
+ {
+ address: 'bc1p7atgm33ak04ntsq9366mvym42ecrk4y34ssysc99340a39eq9arq0pu9uj',
+ asset: {
+ amount: '1.2',
+ unit: 'BTC',
+ fungible: true,
+ },
+ },
+ ],
+ to: [
+ {
+ address: 'bc1p3t7744qewy262ym5afgeuqlwswtpfe22y7c4lwv0a7972p2k73msee7rr3',
+ asset: {
+ amount: '1.2',
+ unit: 'BTC',
+ fungible: true,
+ },
+ },
+ ],
+ fees: [
+ {
+ type: 'priority',
+ asset: {
+ amount: '1.0001',
+ unit: 'BTC',
+ fungible: true,
+ },
+ },
+ ],
};
export const Default = {
args: {
transaction: mockTransaction,
onClose: () => console.log('Modal closed'),
- multichainNetwork: {
+ userAddress:
+ 'bc1p7atgm33ak04ntsq9366mvym42ecrk4y34ssysc99340a39eq9arq0pu9uj',
+ networkConfig: {
nickname: 'Bitcoin',
isEvmNetwork: false,
chainId: 'bip122:000000000019d6689c085ae165831e93',
- network: {
- chainId: 'bip122:000000000019d6689c085ae165831e93',
- ticker: 'BTC',
- },
+ decimals: 8,
+ ticker: 'BTC',
+ id: 'btc-mainnet',
},
},
};
diff --git a/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.test.tsx b/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.test.tsx
index b3fd120540b1..b4436e9b0de4 100644
--- a/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.test.tsx
+++ b/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.test.tsx
@@ -11,7 +11,9 @@ import { renderWithProvider } from '../../../../test/lib/render-helpers';
import { MOCK_ACCOUNT_SOLANA_MAINNET } from '../../../../test/data/mock-accounts';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import {
+ MULTICHAIN_PROVIDER_CONFIGS,
MultichainNetworks,
+ MultichainProviderConfig,
SOLANA_BLOCK_EXPLORER_URL,
} from '../../../../shared/constants/multichain/networks';
import mockState from '../../../../test/data/mock-state.json';
@@ -117,6 +119,7 @@ const mockProps = {
transaction: mockTransaction,
onClose: jest.fn(),
userAddress: MOCK_ACCOUNT_SOLANA_MAINNET.address,
+ networkConfig: MULTICHAIN_PROVIDER_CONFIGS[MultichainNetworks.BITCOIN],
};
describe('MultichainTransactionDetailsModal', () => {
@@ -136,6 +139,7 @@ describe('MultichainTransactionDetailsModal', () => {
transaction: Transaction;
onClose: jest.Mock;
userAddress: string;
+ networkConfig: MultichainProviderConfig;
} = mockProps,
) => {
const store = configureStore(mockState.metamask);
@@ -299,13 +303,14 @@ describe('MultichainTransactionDetailsModal', () => {
transaction: mockSwapTransaction,
onClose: jest.fn(),
userAddress,
+ networkConfig: MULTICHAIN_PROVIDER_CONFIGS[MultichainNetworks.SOLANA],
};
renderComponent(swapProps);
expect(screen.getByText('Swap')).toBeInTheDocument();
expect(screen.getByTestId('transaction-amount')).toHaveTextContent(
- '2.5 SOL',
+ '100 USDC',
);
const addressStart = userAddress.substring(0, 6);
diff --git a/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.tsx b/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.tsx
index f941cad336f3..b0fbda32a5ab 100644
--- a/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.tsx
+++ b/ui/components/app/multichain-transaction-details-modal/multichain-transaction-details-modal.tsx
@@ -1,6 +1,10 @@
import React, { useContext } from 'react';
import { capitalize } from 'lodash';
-import { Transaction, TransactionStatus } from '@metamask/keyring-api';
+import {
+ Transaction,
+ TransactionStatus,
+ TransactionType,
+} from '@metamask/keyring-api';
import {
Display,
FlexDirection,
@@ -42,6 +46,7 @@ import {
KEYRING_TRANSACTION_STATUS_KEY,
useMultichainTransactionDisplay,
} from '../../../hooks/useMultichainTransactionDisplay';
+import { MultichainProviderConfig } from '../../../../shared/constants/multichain/networks';
import {
formatTimestamp,
getTransactionUrl,
@@ -53,28 +58,20 @@ export type MultichainTransactionDetailsModalProps = {
transaction: Transaction;
onClose: () => void;
userAddress: string;
+ networkConfig: MultichainProviderConfig;
};
export function MultichainTransactionDetailsModal({
transaction,
onClose,
userAddress,
+ networkConfig,
}: MultichainTransactionDetailsModalProps) {
const t = useI18nContext();
const trackEvent = useContext(MetaMetricsContext);
- const {
- id,
- type,
- timestamp,
- chain,
- status,
- from,
- to,
- baseFee,
- priorityFee,
- asset,
- } = useMultichainTransactionDisplay({ transaction, userAddress });
+ const { assetInputs, assetOutputs, isRedeposit, baseFee, priorityFee } =
+ useMultichainTransactionDisplay(transaction, networkConfig);
const getStatusColor = (txStatus: string) => {
switch (txStatus.toLowerCase()) {
@@ -88,7 +85,68 @@ export function MultichainTransactionDetailsModal({
return TextColor.textDefault;
}
};
- const statusKey = KEYRING_TRANSACTION_STATUS_KEY[status];
+ const statusKey = KEYRING_TRANSACTION_STATUS_KEY[transaction.status];
+
+ const accountComponent = (title: string, address?: string) =>
+ address ? (
+
+
+ {title}
+
+
+
+ {shortenAddress(address)}
+
+ navigator.clipboard.writeText(
+ getAddressUrl(address as string, transaction.chain),
+ )
+ }
+ />
+
+
+
+ ) : null;
+
+ const amountComponent = (
+ {
+ amount,
+ unit,
+ }: {
+ amount: string;
+ unit: string;
+ },
+ title: string,
+ dataTestId: string,
+ ) => (
+
+
+ {title}
+
+
+
+ {amount} {unit}
+
+
+
+ );
return (
- {capitalize(type)}
+ {capitalize(isRedeposit ? t('redeposit') : transaction.type)}
- {formatTimestamp(timestamp)}
+ {formatTimestamp(transaction.timestamp)}
@@ -136,7 +194,10 @@ export function MultichainTransactionDetailsModal({
{t('status')}
-
+
{capitalize(t(statusKey))}
@@ -162,9 +223,9 @@ export function MultichainTransactionDetailsModal({
}}
as="a"
externalLink
- href={getTransactionUrl(id, chain)}
+ href={getTransactionUrl(transaction.id, transaction.chain)}
>
- {shortenTransactionId(id)}
+ {shortenTransactionId(transaction.id)}
navigator.clipboard.writeText(
- getTransactionUrl(id, chain),
+ getTransactionUrl(transaction.id, transaction.chain),
)
}
/>
@@ -191,172 +252,33 @@ export function MultichainTransactionDetailsModal({
gap={4}
>
{/* From */}
- {from?.address && (
-
-
- {t('from')}
-
-
-
- {shortenAddress(from.address)}
-
- navigator.clipboard.writeText(
- getAddressUrl(from.address as string, chain),
- )
- }
- />
-
-
-
- )}
+ {transaction.type === TransactionType.Send
+ ? accountComponent(t('from'), userAddress)
+ : assetInputs.map((input) =>
+ accountComponent(t('from'), input.address),
+ )}
- {/* To */}
- {to?.address && (
-
-
- {t('to')}
-
-
-
- {shortenAddress(to.address)}
-
- navigator.clipboard.writeText(
- getAddressUrl(to.address as string, chain),
- )
- }
- />
-
-
-
- )}
+ {/* Amounts per token */}
+ {assetOutputs.map((output) => (
+ <>
+ {accountComponent(t('to'), output.address)}
+ {amountComponent(output, t('amount'), 'transaction-amount')}
+ >
+ ))}
- {/* Amount */}
- {asset && (
-
-
- {t('amount')}
-
-
-
- {asset?.amount} {asset?.unit}
-
-
-
+ {/* Base Fees */}
+ {baseFee.map((fee) =>
+ amountComponent(fee, t('networkFee'), 'transaction-base-fee'),
)}
- {/* Network Fees */}
- {baseFee ? (
-
-
- {t('networkFee')}
-
-
-
- {baseFee.amount} {baseFee.unit}
-
-
-
- ) : null}
-
- {priorityFee ? (
-
-
- {t('priorityFee')}
-
-
-
- {priorityFee.amount} {priorityFee.unit}
-
-
-
- ) : null}
+ {/* Priority Fees */}
+ {priorityFee.map((fee) =>
+ amountComponent(
+ fee,
+ t('priorityFee'),
+ 'transaction-priority-fee',
+ ),
+ )}
@@ -371,7 +293,7 @@ export function MultichainTransactionDetailsModal({
variant={ButtonVariant.Link}
onClick={() => {
global.platform.openTab({
- url: getTransactionUrl(id, chain),
+ url: getTransactionUrl(transaction.id, transaction.chain),
});
trackEvent({
@@ -380,7 +302,9 @@ export function MultichainTransactionDetailsModal({
properties: {
link_type: MetaMetricsEventLinkType.AccountTracker,
location: 'Transaction Details',
- url_domain: getURLHostName(getTransactionUrl(id, chain)),
+ url_domain: getURLHostName(
+ getTransactionUrl(transaction.id, transaction.chain),
+ ),
},
});
}}
diff --git a/ui/components/app/transaction-icon/transaction-icon.js b/ui/components/app/transaction-icon/transaction-icon.js
index 6051862aae26..b1a23f7f9a39 100644
--- a/ui/components/app/transaction-icon/transaction-icon.js
+++ b/ui/components/app/transaction-icon/transaction-icon.js
@@ -22,6 +22,7 @@ const ICON_MAP = {
[TransactionGroupCategory.swap]: IconName.SwapHorizontal,
[TransactionGroupCategory.swapAndSend]: IconName.Arrow2UpRight,
[TransactionGroupCategory.bridge]: IconName.Bridge,
+ [TransactionGroupCategory.redeposit]: IconName.Refresh,
};
const COLOR_MAP = {
diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js
index be0cf22a657e..cc530da16f36 100644
--- a/ui/components/app/transaction-list/transaction-list.component.js
+++ b/ui/components/app/transaction-list/transaction-list.component.js
@@ -40,12 +40,7 @@ import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions';
import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP } from '../../../../shared/constants/swaps';
import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
import { useMultichainSelector } from '../../../hooks/useMultichainSelector';
-import {
- getSelectedInternalAccount,
- ///: BEGIN:ONLY_INCLUDE_IF(multichain)
- isSelectedInternalAccountSolana,
- ///: END:ONLY_INCLUDE_IF
-} from '../../../selectors/accounts';
+import { getSelectedInternalAccount } from '../../../selectors/accounts';
import {
getMultichainNetwork,
///: BEGIN:ONLY_INCLUDE_IF(multichain)
@@ -63,6 +58,8 @@ import {
IconName,
BadgeWrapper,
AvatarNetwork,
+ AvatarNetworkSize,
+ BadgeWrapperAnchorElementShape,
///: END:ONLY_INCLUDE_IF
} from '../../component-library';
///: BEGIN:ONLY_INCLUDE_IF(multichain)
@@ -73,6 +70,7 @@ import { formatTimestamp } from '../multichain-transaction-details-modal/helpers
///: END:ONLY_INCLUDE_IF
import {
///: BEGIN:ONLY_INCLUDE_IF(multichain)
+ BackgroundColor,
Display,
///: END:ONLY_INCLUDE_IF
TextColor,
@@ -92,16 +90,11 @@ import { openBlockExplorer } from '../../multichain/menu-items/view-explorer-men
import { getMultichainAccountUrl } from '../../../helpers/utils/multichain/blockExplorer';
import { ActivityListItem } from '../../multichain';
import { MetaMetricsContext } from '../../../contexts/metametrics';
-import {
- MULTICHAIN_PROVIDER_CONFIGS,
- MultichainNetworks,
- SOLANA_TOKEN_IMAGE_URL,
- BITCOIN_TOKEN_IMAGE_URL,
-} from '../../../../shared/constants/multichain/networks';
import {
KEYRING_TRANSACTION_STATUS_KEY,
useMultichainTransactionDisplay,
} from '../../../hooks/useMultichainTransactionDisplay';
+import { TransactionGroupCategory } from '../../../../shared/constants/transaction';
///: END:ONLY_INCLUDE_IF
import { endTrace, TraceName } from '../../../../shared/lib/trace';
@@ -251,6 +244,7 @@ export default function TransactionList({
const isTokenNetworkFilterEqualCurrentNetwork = useSelector(
getIsTokenNetworkFilterEqualCurrentNetwork,
);
+ const selectedAccount = useSelector(getSelectedAccount);
///: BEGIN:ONLY_INCLUDE_IF(multichain)
const [selectedTransaction, setSelectedTransaction] = useState(null);
@@ -305,7 +299,6 @@ export default function TransactionList({
]);
const chainId = useSelector(getCurrentChainId);
- const selectedAccount = useSelector(getSelectedAccount);
const account = useSelector(getSelectedInternalAccount);
const { isEvmNetwork } = useMultichainSelector(getMultichainNetwork, account);
@@ -482,6 +475,7 @@ export default function TransactionList({
transaction={selectedTransaction}
onClose={() => toggleShowDetails(null)}
userAddress={selectedAccount.address}
+ networkConfig={multichainNetwork.network}
/>
)}
@@ -502,13 +496,12 @@ export default function TransactionList({
>
{dateGroup.date}
- {dateGroup.transactionGroups.map((transaction, index) => (
+ {dateGroup.transactionGroups.map((transaction) => (
))}
@@ -659,61 +652,88 @@ export default function TransactionList({
///: BEGIN:ONLY_INCLUDE_IF(multichain)
const MultichainTransactionListItem = ({
transaction,
- userAddress,
+ networkConfig,
toggleShowDetails,
}) => {
const t = useI18nContext();
- const isSolanaAccount = useSelector(isSelectedInternalAccountSolana);
-
- const { type, status, to, from, asset } = useMultichainTransactionDisplay({
- transaction,
- userAddress,
- });
-
- let title = capitalize(type);
- const statusKey = KEYRING_TRANSACTION_STATUS_KEY[status];
-
- if (type === TransactionType.swap) {
- title = `${t('swap')} ${from.asset.unit} ${'to'} ${to.asset.unit}`;
+ const { assetInputs, assetOutputs, isRedeposit } =
+ useMultichainTransactionDisplay(transaction, networkConfig);
+ let title = capitalize(transaction.type);
+ const statusKey = KEYRING_TRANSACTION_STATUS_KEY[transaction.status];
+
+ // A redeposit transaction is a special case where the outputs list is emtpy because we are sending to ourselves and only pay the fees
+ // Mainly used for consolidation transactions
+ if (isRedeposit) {
+ return (
+ toggleShowDetails(transaction)}
+ icon={
+
+ }
+ >
+
+
+ }
+ title={t('redeposit')}
+ // eslint-disable-next-line react/jsx-no-duplicate-props
+ subtitle={
+
+ }
+ />
+ );
}
- return (
- toggleShowDetails(transaction)}
- icon={
-
- }
- display="block"
- positionObj={{ right: -4, top: -4 }}
- >
-
-
- }
- rightContent={
- <>
+ return assetOutputs.map((output, index) => {
+ if (transaction.type === TransactionType.swap) {
+ title = `${t('swap')} ${assetInputs[index].unit} ${'to'} ${output.unit}`;
+ }
+
+ return (
+ toggleShowDetails(transaction)}
+ icon={
+
+ }
+ >
+
+
+ }
+ rightContent={
- {asset?.amount} {asset?.unit}
+ {output.amount} {output.unit}
- >
- }
- title={transaction.isBridgeTx ? t('bridge') : title}
- // eslint-disable-next-line react/jsx-no-duplicate-props
- subtitle={
- transaction.isBridgeTx && transaction.bridgeInfo ? (
- <>
+ }
+ title={transaction.isBridgeTx ? t('bridge') : title}
+ // eslint-disable-next-line react/jsx-no-duplicate-props
+ subtitle={
+ transaction.isBridgeTx && transaction.bridgeInfo ? (
+ <>
+
+
+ {`${t('to')} ${transaction.bridgeInfo.destAsset?.symbol} ${t(
+ 'on',
+ )} ${
+ // Use the pre-computed chain name from our hook, or fall back to chain ID
+ transaction.bridgeInfo.destChainName ||
+ transaction.bridgeInfo.destChainId
+ }`}
+
+ >
+ ) : (
-
- {`${t('to')} ${transaction.bridgeInfo.destAsset?.symbol} ${t(
- 'on',
- )} ${
- // Use the pre-computed chain name from our hook, or fall back to chain ID
- transaction.bridgeInfo.destChainName ||
- transaction.bridgeInfo.destChainId
- }`}
-
- >
- ) : (
-
- )
- }
- >
- );
+ )
+ }
+ />
+ );
+ });
};
+
MultichainTransactionListItem.propTypes = {
transaction: PropTypes.object.isRequired,
- userAddress: PropTypes.string.isRequired,
+ networkConfig: PropTypes.object.isRequired,
toggleShowDetails: PropTypes.func.isRequired,
};
diff --git a/ui/components/app/transaction-list/transaction-list.test.js b/ui/components/app/transaction-list/transaction-list.test.js
index 8918a245ab39..6496fd3c149e 100644
--- a/ui/components/app/transaction-list/transaction-list.test.js
+++ b/ui/components/app/transaction-list/transaction-list.test.js
@@ -57,7 +57,17 @@ const btcState = {
type: 'send',
account: MOCK_ACCOUNT_BIP122_P2WPKH.id,
from: [],
- to: [],
+ to: [
+ {
+ address: MOCK_ACCOUNT_BIP122_P2WPKH.address,
+ asset: {
+ fungible: true,
+ type: '',
+ unit: 'BTC',
+ amount: '0.000000723',
+ },
+ },
+ ],
fees: [],
events: [],
},
@@ -93,15 +103,6 @@ const solanaSwapState = {
status: 'confirmed',
type: 'swap',
from: [
- {
- address: '8kR2HTHzPtTJuzpFZ8jtGCQ9TpahPaWbZfTNRs2GJdxq',
- asset: {
- fungible: true,
- type: '',
- unit: 'SOL',
- amount: '0.000073111',
- },
- },
{
address: MOCK_ACCOUNT_SOLANA_MAINNET.address,
asset: {
@@ -111,44 +112,8 @@ const solanaSwapState = {
amount: '0.01',
},
},
- {
- address: 'HUCjBnmd4FoUjCCMYQ9xFz1ce1r8vWAd8uMhUQakE2FR',
- asset: {
- fungible: true,
- type: '',
- unit: 'BONK',
- amount: '2583.728601',
- },
- },
- {
- address: '3msVd34R5KxonDzyNSV5nT19UtUeJ2RF1NaQhvVPNLxL',
- asset: {
- fungible: true,
- type: '',
- unit: 'SOL',
- amount: '0.000073111',
- },
- },
],
to: [
- {
- address: 'CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM',
- asset: {
- fungible: true,
- type: '',
- unit: 'SOL',
- amount: '0.000000723',
- },
- },
- {
- address: 'HUCjBnmd4FoUjCCMYQ9xFz1ce1r8vWAd8uMhUQakE2FR',
- asset: {
- fungible: true,
- type: '',
- unit: 'SOL',
- amount: '0.00007238',
- },
- },
{
address: MOCK_ACCOUNT_SOLANA_MAINNET.address,
asset: {
@@ -158,15 +123,6 @@ const solanaSwapState = {
amount: '2583.72',
},
},
- {
- address: '3msVd34R5KxonDzyNSV5nT19UtUeJ2RF1NaQhvVPNLxL',
- asset: {
- fungible: true,
- type: '',
- unit: 'SOL',
- amount: '0.01',
- },
- },
],
fees: [
{
@@ -374,7 +330,7 @@ describe('TransactionList', () => {
expect(getByTestId('activity-list-item')).toBeInTheDocument();
- expect(getByText('-0.01 SOL')).toBeInTheDocument();
+ expect(getByText('2,583.72 BONK')).toBeInTheDocument();
const viewOnExplorerBtn = getByRole('button', {
name: 'View on block explorer',
diff --git a/ui/hooks/useMultichainTransactionDisplay.ts b/ui/hooks/useMultichainTransactionDisplay.ts
index 7efc4d705c29..11d858ee5c87 100644
--- a/ui/hooks/useMultichainTransactionDisplay.ts
+++ b/ui/hooks/useMultichainTransactionDisplay.ts
@@ -8,9 +8,7 @@ import { useSelector } from 'react-redux';
import { formatWithThreshold } from '../components/app/assets/util/formatWithThreshold';
import { getIntlLocale } from '../ducks/locale/locale';
import { TransactionGroupStatus } from '../../shared/constants/transaction';
-
-type Fee = Transaction['fees'][0]['asset'];
-type Token = Transaction['from'][0]['asset'];
+import { MultichainProviderConfig } from '../../shared/constants/multichain/networks';
export const KEYRING_TRANSACTION_STATUS_KEY = {
[KeyringTransactionStatus.Failed]: TransactionStatus.failed,
@@ -19,112 +17,109 @@ export const KEYRING_TRANSACTION_STATUS_KEY = {
[KeyringTransactionStatus.Submitted]: TransactionStatus.submitted,
};
-export function useMultichainTransactionDisplay({
- transaction,
- userAddress,
-}: {
- transaction: Transaction;
- userAddress: string;
-}) {
+type Asset = {
+ unit: string;
+ type: `${string}:${string}/${string}:${string}`;
+ amount: string;
+ fungible: true;
+};
+
+type Movement = {
+ asset: Asset;
+ address?: string;
+};
+
+export function useMultichainTransactionDisplay(
+ transaction: Transaction,
+ networkConfig: MultichainProviderConfig,
+) {
const locale = useSelector(getIntlLocale);
+ const isNegative = transaction.type === TransactionType.Send;
- const transactionFromEntry = transaction.from?.find(
- (entry) => entry?.address === userAddress,
+ const assetInputs = aggregateAmount(
+ transaction.from as Movement[],
+ isNegative,
+ locale,
+ networkConfig.decimals,
);
- const transactionToEntry = transaction.to?.find(
- (entry) => entry?.address === userAddress,
+ const assetOutputs = aggregateAmount(
+ transaction.to as Movement[],
+ isNegative,
+ locale,
+ networkConfig.decimals,
+ );
+ const baseFee = aggregateAmount(
+ transaction.fees.filter((fee) => fee.type === 'base') as Movement[],
+ isNegative,
+ locale,
+ );
+ const priorityFee = aggregateAmount(
+ transaction.fees.filter((fee) => fee.type === 'priority') as Movement[],
+ isNegative,
+ locale,
);
-
- const baseFee = transaction?.fees?.find((fee) => fee.type === 'base') ?? null;
- const priorityFee =
- transaction?.fees?.find((fee) => fee.type === 'priority') ?? null;
-
- let from = null;
- let to = null;
-
- switch (transaction.type) {
- case TransactionType.Swap:
- from = transactionFromEntry ?? null;
- to = transactionToEntry ?? null;
- break;
- case TransactionType.Send:
- from = transactionFromEntry ?? transaction.from?.[0] ?? null;
- to = transaction.to?.[0] ?? null;
- break;
- case TransactionType.Receive:
- from = transaction.from?.[0] ?? null;
- to = transactionToEntry ?? transaction.to?.[0] ?? null;
- break;
- default:
- from = transaction.from?.[0] ?? null;
- to = transaction.to?.[0] ?? null;
- }
-
- const asset = {
- [TransactionType.Send]: parseAssetWithThreshold(
- from?.asset ?? null,
- '0.00001',
- { locale, isNegative: true },
- ),
- [TransactionType.Receive]: parseAssetWithThreshold(
- to?.asset ?? null,
- '0.00001',
- { locale, isNegative: false },
- ),
- [TransactionType.Swap]: parseAssetWithThreshold(
- from?.asset ?? null,
- '0.00001',
- { locale, isNegative: true },
- ),
- }[transaction.type];
return {
- ...transaction,
- from,
- to,
- asset,
- baseFee: parseAssetWithThreshold(baseFee?.asset ?? null, '0.0000001', {
- locale,
- isNegative: false,
- }),
- priorityFee: parseAssetWithThreshold(
- priorityFee?.asset ?? null,
- '0.0000001',
- { locale, isNegative: false },
- ),
+ assetInputs,
+ assetOutputs,
+ baseFee,
+ priorityFee,
+ isRedeposit: assetOutputs.length === 0,
};
}
-function parseAssetWithThreshold(
- asset: Token | Fee | null,
- threshold: string,
- { locale, isNegative }: { locale: string; isNegative: boolean },
+function aggregateAmount(
+ movement: Movement[],
+ isNegative: boolean,
+ locale: string,
+ decimals?: number,
) {
- if (asset?.fungible) {
- const numberOfDecimals = threshold.split('.')?.[1]?.length ?? 0;
+ const amountByAsset: Record = {};
- const amount = formatWithThreshold(
- Number(asset?.amount),
- Number(threshold),
- locale,
- {
- minimumFractionDigits: 0,
- maximumFractionDigits: numberOfDecimals,
- },
- );
-
- if (isNegative && !amount.startsWith('<')) {
- return {
- ...asset,
- amount: `-${amount}`,
- };
+ for (const mv of movement) {
+ if (!mv?.asset.fungible) {
+ continue;
+ }
+ const assetId = mv.asset.type;
+ if (!amountByAsset[assetId]) {
+ amountByAsset[assetId] = mv;
+ continue;
}
- return {
- ...asset,
- amount,
- };
+ amountByAsset[assetId].asset.amount += Number(mv.asset.amount || 0);
+ }
+
+ // Convert to a proper display array.
+ return Object.entries(amountByAsset).map(([_, mv]) =>
+ parseAsset(mv, locale, isNegative, decimals),
+ );
+}
+
+function parseAsset(
+ movement: Movement,
+ locale: string,
+ isNegative: boolean,
+ decimals?: number,
+) {
+ const displayAmount = formatWithThreshold(
+ Number(movement.asset.amount),
+ 0.00000001,
+ locale,
+ {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: decimals || 8,
+ },
+ );
+
+ let finalAmount = displayAmount;
+ if (isNegative && !displayAmount.startsWith('<')) {
+ finalAmount = `-${displayAmount}`;
}
- return null;
+ return {
+ amount: finalAmount,
+ unit: movement.asset.unit,
+ // It is not strictly correct to use the address here but we do not support sending multiple assets to multiple addresses
+ address: movement.address,
+ };
}