Skip to content

Commit 6bf8d47

Browse files
authored
feat(ui): Flag addresses as own or foreign [LW-10061] (#998)
* feat(ui): add address tag ui component LW-10061 * feat(extension): flag own, address book and foreign addresses in dapp txs LW-10061 * feat(extension): flag own, address book and foreign addresses in activity details LW-10061
1 parent e511860 commit 6bf8d47

File tree

26 files changed

+409
-67
lines changed

26 files changed

+409
-67
lines changed

apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Flex } from '@lace/ui';
55
import { useViewsFlowContext } from '@providers/ViewFlowProvider';
66

77
import { Wallet } from '@lace/cardano';
8-
import { withAddressBookContext } from '@src/features/address-book/context';
8+
import { useAddressBookContext, withAddressBookContext } from '@src/features/address-book/context';
99
import { useWalletStore } from '@stores';
1010
import { useFetchCoinPrice, useChainHistoryProvider } from '@hooks';
1111
import {
@@ -23,6 +23,7 @@ import { useCurrencyStore, useAppSettingsContext } from '@providers';
2323
import { logger } from '@lib/wallet-api-ui';
2424
import { useComputeTxCollateral } from '@hooks/useComputeTxCollateral';
2525
import { utxoAndBackendChainHistoryResolver } from '@src/utils/utxo-chain-history-resolver';
26+
import { AddressBookSchema, useDbStateValue } from '@lib/storage';
2627

2728
interface DappTransactionContainerProps {
2829
errorMessage?: string;
@@ -43,6 +44,10 @@ export const DappTransactionContainer = withAddressBookContext(
4344
walletState
4445
} = useWalletStore();
4546

47+
const ownAddresses = useObservable(inMemoryWallet.addresses$)?.map((a) => a.address);
48+
const { list: addressBook } = useAddressBookContext() as useDbStateValue<AddressBookSchema>;
49+
const addressToNameMap = new Map(addressBook?.map((entry) => [entry.address as string, entry.name]));
50+
4651
const { fiatCurrency } = useCurrencyStore();
4752
const { priceResult } = useFetchCoinPrice();
4853

@@ -146,6 +151,8 @@ export const DappTransactionContainer = withAddressBookContext(
146151
errorMessage={errorMessage}
147152
toAddress={toAddressTokens}
148153
collateral={txCollateral}
154+
ownAddresses={ownAddresses}
155+
addressToNameMap={addressToNameMap}
149156
/>
150157
) : (
151158
<Skeleton loading />

apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ import { DappTransactionContainer } from '../DappTransactionContainer';
3030
import '@testing-library/jest-dom';
3131
import { BehaviorSubject } from 'rxjs';
3232
import { act } from 'react-dom/test-utils';
33-
import { buildMockTx } from '@src/utils/mocks/tx';
33+
import { buildMockTx, sendingAddress } from '@src/utils/mocks/tx';
3434
import { Wallet } from '@lace/cardano';
3535
import { SignTxData } from '../types';
3636
import { getWrapper } from '../testing.utils';
3737
import { TransactionWitnessRequest } from '@cardano-sdk/web-extension';
3838
import { cardanoCoin } from '@src/utils/constants';
39+
import { AddressBookSchema } from '@lib/storage';
3940

4041
const { Cardano, Crypto } = Wallet;
4142

@@ -51,6 +52,7 @@ const mockedAssetsInfo = new Map([['id', 'data']]);
5152
const assetInfo$ = new BehaviorSubject(mockedAssetsInfo);
5253
const available$ = new BehaviorSubject([]);
5354
const signed$ = new BehaviorSubject([]);
55+
const addresses$ = new BehaviorSubject([sendingAddress]);
5456
const rewardAccounts$ = new BehaviorSubject([
5557
{
5658
// eslint-disable-next-line unicorn/consistent-destructuring
@@ -65,6 +67,7 @@ const protocolParameters$ = new BehaviorSubject({
6567
});
6668

6769
const inMemoryWallet = {
70+
addresses$,
6871
assetInfo$,
6972
balance: {
7073
utxo: {
@@ -133,12 +136,12 @@ jest.mock('react-i18next', () => {
133136
};
134137
});
135138

136-
const addressList = ['addressList'];
139+
const addressBook: AddressBookSchema[] = [];
137140
jest.mock('@src/features/address-book/context', () => ({
138141
// eslint-disable-next-line @typescript-eslint/no-explicit-any
139142
...jest.requireActual<any>('@src/features/address-book/context'),
140143
withAddressBookContext: mockWithAddressBookContext,
141-
useAddressBookContext: () => ({ list: addressList })
144+
useAddressBookContext: () => ({ list: addressBook })
142145
}));
143146

144147
jest.mock('antd', () => {
@@ -332,7 +335,9 @@ describe('Testing DappTransactionContainer component', () => {
332335
errorMessage,
333336
coinSymbol: 'ADA',
334337
collateral: BigInt(1_000_000),
335-
txInspectionDetails
338+
txInspectionDetails,
339+
ownAddresses: [sendingAddress.address],
340+
addressToNameMap: new Map()
336341
},
337342
{}
338343
);

apps/browser-extension-wallet/src/lib/translations/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,8 @@
13681368
"core.receive.usedAddresses.copy": "Copy Address",
13691369
"core.receive.usedAddresses.addressCopied": "Address copied",
13701370
"core.receive.showUsedAddresses": "Show used addresses",
1371+
"core.addressTags.own": "own",
1372+
"core.addressTags.foreign": "foreign",
13711373
"addressesDiscovery.overlay.title": "Your wallet is syncing, this might take a few minutes",
13721374
"addressesDiscovery.toast.errorText": "Wallet failed to sync",
13731375
"addressesDiscovery.toast.successText": "Wallet synced successfully",

apps/browser-extension-wallet/src/utils/mocks/tx.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/* eslint-disable no-magic-numbers */
22
import { Wallet } from '@lace/cardano';
33

4-
const sendingAddress = Wallet.Cardano.PaymentAddress(
5-
'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g'
6-
);
4+
export const sendingAddress = {
5+
address: Wallet.Cardano.PaymentAddress(
6+
'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g'
7+
)
8+
} as Wallet.KeyManagement.GroupedAddress;
79

8-
const receivingAddress = Wallet.Cardano.PaymentAddress(
10+
export const receivingAddress = Wallet.Cardano.PaymentAddress(
911
'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz'
1012
);
1113

@@ -34,7 +36,7 @@ export const buildMockTx = (
3436
]),
3537
inputs: args.inputs ?? [
3638
{
37-
address: sendingAddress,
39+
address: sendingAddress.address,
3840
index: 0,
3941
txId: Wallet.Cardano.TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0')
4042
}
@@ -64,7 +66,7 @@ export const buildMockTx = (
6466
}
6567
},
6668
{
67-
address: sendingAddress,
69+
address: sendingAddress.address,
6870
value: {
6971
assets: new Map([
7072
[Wallet.Cardano.AssetId('659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41'), BigInt(1)]

apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { TxDirections } from '@src/types';
99
import { APP_MODE_POPUP } from '@src/utils/constants';
1010
import { config } from '@src/config';
1111
import { PostHogAction } from '@providers/AnalyticsProvider/analyticsTracker';
12+
import { useObservable } from '@lace/common';
1213

1314
type TransactionDetailsProxyProps = {
1415
name: string;
@@ -21,13 +22,23 @@ export const TransactionDetailsProxy = withAddressBookContext(
2122
({ name, activityInfo, direction, status, amountTransformer }: TransactionDetailsProxyProps): ReactElement => {
2223
const analytics = useAnalyticsContext();
2324
const {
25+
inMemoryWallet,
2426
walletInfo,
2527
environmentName,
2628
walletUI: { cardanoCoin, appMode }
2729
} = useWalletStore();
2830
const isPopupView = appMode === APP_MODE_POPUP;
2931
const openExternalLink = useExternalLinkOpener();
32+
33+
// Prepare own addresses of active account
34+
const walletAddresses = useObservable(inMemoryWallet.addresses$)?.map((a) => a.address);
35+
36+
// Prepare address book data as Map<address, name>
3037
const { list: addressList } = useAddressBookContext();
38+
const addressToNameMap = useMemo(
39+
() => new Map<string, string>(addressList?.map((item: AddressListType) => [item.address, item.name])),
40+
[addressList]
41+
);
3142

3243
const { CEXPLORER_BASE_URL, CEXPLORER_URL_PATHS } = config();
3344
const explorerBaseUrl = useMemo(
@@ -72,11 +83,6 @@ export const TransactionDetailsProxy = withAddressBookContext(
7283
externalLink && status === ActivityStatus.SUCCESS && openExternalLink(externalLink);
7384
};
7485

75-
const addressToNameMap = useMemo(
76-
() => new Map<string, string>(addressList?.map((item: AddressListType) => [item.address, item.name])),
77-
[addressList]
78-
);
79-
8086
return (
8187
// eslint-disable-next-line react/jsx-pascal-case
8288
<TransactionDetails
@@ -95,6 +101,7 @@ export const TransactionDetailsProxy = withAddressBookContext(
95101
amountTransformer={amountTransformer}
96102
headerDescription={getHeaderDescription() || cardanoCoin.symbol}
97103
txSummary={txSummary}
104+
ownAddresses={walletAddresses}
98105
addressToNameMap={addressToNameMap}
99106
coinSymbol={cardanoCoin.symbol}
100107
isPopupView={isPopupView}

packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -102,31 +102,16 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid-
102102
font-weight: 500;
103103
line-height: 17px;
104104
}
105+
106+
&.addressTag {
107+
gap: size_unit(1);
108+
}
105109
}
106110

107111
.timestamp {
108112
flex: 0 0 35%;
109113
}
110114

111-
.amount {
112-
display: flex;
113-
flex-direction: column;
114-
width: 100%;
115-
align-items: flex-end;
116-
117-
.ada {
118-
color: var(--text-color-primary, #ffffff);
119-
}
120-
121-
.fiat {
122-
color: var(--text-color-secondary, #878e9e);
123-
}
124-
125-
.addrName {
126-
margin-bottom: size_unit(1);
127-
}
128-
}
129-
130115
.addressDetail {
131116
font-size: var(--bodySmall, 14px);
132117
font-weight: 400;

packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
/* eslint-disable no-magic-numbers */
33
import React from 'react';
44
import cn from 'classnames';
5-
import { TransactionDetailAsset, TransactionMetadataProps, TxOutputInput, TxSummary } from './TransactionDetailAsset';
5+
66
import { Ellipsis, toast } from '@lace/common';
77
import { Box } from '@lace/ui';
8-
import { useTranslate } from '@src/ui/hooks';
8+
import { useTranslate } from '@ui/hooks';
9+
import { getAddressTagTranslations, renderAddressTag } from '@ui/utils';
10+
11+
import { TransactionDetailAsset, TransactionMetadataProps, TxOutputInput, TxSummary } from './TransactionDetailAsset';
912
import CopyToClipboard from 'react-copy-to-clipboard';
1013
import { ActivityStatus } from '../Activity';
1114
import styles from './TransactionDetails.module.scss';
@@ -76,6 +79,7 @@ export interface TransactionDetailsProps {
7679
txSummary?: TxSummary[];
7780
coinSymbol: string;
7881
tooltipContent?: string;
82+
ownAddresses: string[];
7983
addressToNameMap: Map<string, string>;
8084
isPopupView?: boolean;
8185
openExternalLink?: (url: string) => void;
@@ -122,6 +126,7 @@ export const TransactionDetails = ({
122126
txSummary = [],
123127
coinSymbol,
124128
pools,
129+
ownAddresses,
125130
addressToNameMap,
126131
isPopupView,
127132
openExternalLink,
@@ -353,22 +358,19 @@ export const TransactionDetails = ({
353358
</div>
354359
)}
355360
{(summary.addr as string[]).map((addr) => {
356-
const addrName = addressToNameMap?.get(addr);
357361
const address = isPopupView ? (
358362
<Ellipsis className={cn(styles.addr, styles.fiat)} text={addr} ellipsisInTheMiddle />
359363
) : (
360364
<span className={cn(styles.addr, styles.fiat)}>{addr}</span>
361365
);
362366
return (
363-
<div key={addr} data-testid="tx-to-detail" className={cn(styles.addr, styles.detail)}>
364-
{addrName ? (
365-
<div className={styles.amount}>
366-
<span className={cn(styles.ada, styles.addrName)}>{addrName}</span>
367-
{address}
368-
</div>
369-
) : (
370-
address
371-
)}
367+
<div
368+
key={addr}
369+
data-testid="tx-to-detail"
370+
className={cn([styles.detail, styles.addr, styles.addressTag])}
371+
>
372+
{address}
373+
{renderAddressTag(addr, getAddressTagTranslations(t), ownAddresses, addressToNameMap)}
372374
</div>
373375
);
374376
})}
@@ -590,6 +592,8 @@ export const TransactionDetails = ({
590592
coinSymbol={coinSymbol}
591593
withSeparatorLine
592594
sendAnalytics={sendAnalyticsInputs}
595+
ownAddresses={ownAddresses}
596+
addressToNameMap={addressToNameMap}
593597
/>
594598
)}
595599
{addrOutputs?.length > 0 && (
@@ -604,6 +608,8 @@ export const TransactionDetails = ({
604608
}}
605609
coinSymbol={coinSymbol}
606610
sendAnalytics={sendAnalyticsOutputs}
611+
ownAddresses={ownAddresses}
612+
addressToNameMap={addressToNameMap}
607613
/>
608614
)}
609615
{metadata?.length > 0 && (

packages/core/src/ui/components/ActivityDetail/TransactionInputOutput.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
import React, { useState } from 'react';
33
import { Tooltip } from 'antd';
44
import cn from 'classnames';
5-
import { addEllipsis, Button } from '@lace/common';
65

76
import { InfoCircleOutlined, DownOutlined } from '@ant-design/icons';
7+
import { addEllipsis, Button } from '@lace/common';
8+
89
import { TxOutputInput } from './TransactionDetailAsset';
910
import { TranslationsFor } from '../../utils/types';
1011

1112
import { ReactComponent as BracketDown } from '../../assets/icons/bracket-down.component.svg';
1213
import styles from './TransactionInputOutput.module.scss';
14+
import { Flex } from '@lace/ui';
15+
import { getAddressTagTranslations, renderAddressTag } from '@ui/utils';
16+
import { useTranslate } from '@ui/hooks';
1317

1418
const rotateOpen: React.CSSProperties = {
1519
transform: 'rotate(180deg)',
@@ -30,6 +34,8 @@ export interface TransactionInputOutputProps {
3034
translations: TranslationsFor<'address' | 'sent'>;
3135
coinSymbol: string;
3236
withSeparatorLine?: boolean;
37+
ownAddresses: string[];
38+
addressToNameMap: Map<string, string>;
3339
sendAnalytics?: () => void;
3440
}
3541

@@ -42,9 +48,12 @@ export const TransactionInputOutput = ({
4248
translations,
4349
coinSymbol,
4450
withSeparatorLine,
51+
ownAddresses,
52+
addressToNameMap,
4553
sendAnalytics
4654
}: TransactionInputOutputProps): React.ReactElement => {
4755
const [isVisible, setIsVisible] = useState<boolean>();
56+
const { t } = useTranslate();
4857

4958
const animation = isVisible ? rotateOpen : rotateClose;
5059
const Icon = BracketDown ? <BracketDown className={styles.bracket} style={{ ...animation }} /> : <DownOutlined />;
@@ -79,9 +88,12 @@ export const TransactionInputOutput = ({
7988
<div className={styles.addressContainer} key={`${inputAddress}-${idx}`}>
8089
<div className={styles.row}>
8190
<div className={styles.label}>{translations.address}</div>
82-
<div data-testid="tx-address" className={cn(styles.addressDetail, styles.content)}>
83-
<Tooltip title={inputAddress}>{addEllipsis(inputAddress, 8, 8)}</Tooltip>
84-
</div>
91+
<Flex flexDirection="column" alignItems="flex-end" gap="$8">
92+
<div data-testid="tx-address" className={cn(styles.addressDetail, styles.content)}>
93+
<Tooltip title={inputAddress}>{addEllipsis(inputAddress, 8, 8)}</Tooltip>
94+
</div>
95+
{renderAddressTag(inputAddress, getAddressTagTranslations(t), ownAddresses, addressToNameMap)}
96+
</Flex>
8597
</div>
8698

8799
<div className={styles.row}>

packages/core/src/ui/components/ActivityDetail/__tests__/TransactionDetails.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe('Testing ActivityDetailsBrowser component', () => {
3535
],
3636
amountTransformer: (amount) => `${amount} $`,
3737
coinSymbol: 'ADA',
38+
ownAddresses: [],
3839
addressToNameMap: new Map()
3940
};
4041

@@ -83,4 +84,22 @@ describe('Testing ActivityDetailsBrowser component', () => {
8384
const { queryByTestId: query } = render(<TransactionDetails {...addrListProps} />);
8485
expect(query('tx-metadata')).not.toBeInTheDocument();
8586
});
87+
88+
test('should show address tag for inputs', async () => {
89+
// use empty addrOutputs (so we get only one toggle button for inputs)
90+
const { findByTestId } = render(<TransactionDetails {...addrListProps} addrOutputs={[]} />);
91+
const inputsSectionToggle = await findByTestId('tx-addr-list_toggle');
92+
fireEvent.click(inputsSectionToggle);
93+
94+
expect(await findByTestId('address-tag')).toBeVisible();
95+
});
96+
97+
test('should show address tag for outputs', async () => {
98+
// use empty addrOutputs (so we get only one toggle button for outputs)
99+
const { findByTestId } = render(<TransactionDetails {...addrListProps} addrOutputs={[]} />);
100+
const outputsSectionToggle = await findByTestId('tx-addr-list_toggle');
101+
fireEvent.click(outputsSectionToggle);
102+
103+
expect(await findByTestId('address-tag')).toBeVisible();
104+
});
86105
});

0 commit comments

Comments
 (0)