Skip to content

Commit aa63be9

Browse files
authored
Merge pull request #14863 from LedgerHQ/feat/hide-address-on-shielded-ops
feat(zcash): hide zcash shielded address if discreet mode is on [LIVE-26508]
2 parents 929dcf6 + 4be0c86 commit aa63be9

File tree

5 files changed

+240
-91
lines changed

5 files changed

+240
-91
lines changed

.changeset/red-guests-eat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ledger-live-desktop": minor
3+
---
4+
5+
Hide Zcash operation address if discreet mode is on

apps/ledger-live-desktop/src/renderer/components/OperationsList/AddressCell.tsx

Lines changed: 4 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,12 @@
11
import React, { PureComponent } from "react";
2-
import styled from "styled-components";
32
import type { Operation } from "@ledgerhq/types-live";
43
import type { Currency } from "@ledgerhq/types-cryptoassets";
54
import Box from "~/renderer/components/Box";
65
import { getLLDCoinFamily } from "~/renderer/families";
6+
import { Address, Cell } from "./AddressCellShared";
7+
export { Address, Cell, splitAddress, SplitAddress } from "./AddressCellShared";
78

8-
export const splitAddress = (value: string): { left: string; right: string } => {
9-
let left, right;
10-
if (value.includes(".")) {
11-
const parts = value.split(".");
12-
left = parts[0] + ".";
13-
right = parts.slice(1).join(".");
14-
} else {
15-
const third = Math.round(value.length / 3);
16-
left = value.slice(0, third);
17-
right = value.slice(third, value.length);
18-
}
19-
return { left, right };
20-
};
21-
22-
export const SplitAddress = ({
23-
value,
24-
color,
25-
ff,
26-
fontSize,
27-
}: {
28-
value: string;
29-
color?: string;
30-
ff?: string;
31-
fontSize?: number;
32-
}) => {
33-
if (!value) {
34-
return <Box />;
35-
}
36-
const boxProps = {
37-
color,
38-
ff,
39-
fontSize,
40-
};
41-
42-
const { left, right } = splitAddress(value);
43-
44-
return (
45-
<Box horizontal {...boxProps}>
46-
<Left>{left}</Left>
47-
<Right>{right}</Right>
48-
</Box>
49-
);
50-
};
51-
export const Address = ({ value }: { value: string }) => (
52-
<SplitAddress value={value} color="neutral.c80" ff="Inter" fontSize={3} />
53-
);
54-
const Left = styled.div`
55-
overflow: hidden;
56-
max-width: calc(100% - 20px);
57-
white-space: nowrap;
58-
font-kerning: none;
59-
letter-spacing: 0px;
60-
`;
61-
const Right = styled.div`
62-
display: inline-block;
63-
flex-shrink: 1;
64-
direction: rtl;
65-
text-indent: 0.6ex;
66-
overflow: hidden;
67-
text-overflow: ellipsis;
68-
white-space: nowrap;
69-
font-kerning: none;
70-
min-width: 3ex;
71-
letter-spacing: 0px;
72-
`;
73-
export const Cell = styled(Box).attrs<{
74-
px?: number;
75-
}>(p => ({
76-
px: p.px === 0 ? p.px : p.px || 4,
77-
horizontal: true,
78-
alignItems: "center",
79-
}))`
80-
width: 150px;
81-
flex-grow: 1;
82-
flex-shrink: 1;
83-
display: block;
84-
`;
85-
type Props = {
9+
export type AddressCellProps = {
8610
operation: Operation;
8711
currency: Currency;
8812
};
@@ -94,7 +18,7 @@ const perOperationType = {
9418
REWARD_PAYOUT: showSender,
9519
_: showRecipient,
9620
};
97-
class AddressCell extends PureComponent<Props> {
21+
class AddressCell extends PureComponent<AddressCellProps> {
9822
render() {
9923
const { currency, operation } = this.props;
10024

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
import Box from "~/renderer/components/Box";
4+
5+
/**
6+
* Shared address display components.
7+
* Extracted to avoid circular dependency: operationDetails → AddressCell → families → operationDetails
8+
*/
9+
10+
export const splitAddress = (value: string): { left: string; right: string } => {
11+
let left, right;
12+
if (value.includes(".")) {
13+
const parts = value.split(".");
14+
left = parts[0] + ".";
15+
right = parts.slice(1).join(".");
16+
} else {
17+
const third = Math.round(value.length / 3);
18+
left = value.slice(0, third);
19+
right = value.slice(third, value.length);
20+
}
21+
return { left, right };
22+
};
23+
24+
const Left = styled.div`
25+
overflow: hidden;
26+
max-width: calc(100% - 20px);
27+
white-space: nowrap;
28+
font-kerning: none;
29+
letter-spacing: 0px;
30+
`;
31+
const Right = styled.div`
32+
display: inline-block;
33+
flex-shrink: 1;
34+
direction: rtl;
35+
text-indent: 0.6ex;
36+
overflow: hidden;
37+
text-overflow: ellipsis;
38+
white-space: nowrap;
39+
font-kerning: none;
40+
min-width: 3ex;
41+
letter-spacing: 0px;
42+
`;
43+
44+
export const SplitAddress = ({
45+
value,
46+
color,
47+
ff,
48+
fontSize,
49+
}: {
50+
value: string;
51+
color?: string;
52+
ff?: string;
53+
fontSize?: number;
54+
}) => {
55+
if (!value) {
56+
return <Box />;
57+
}
58+
const boxProps = {
59+
color,
60+
ff,
61+
fontSize,
62+
};
63+
64+
const { left, right } = splitAddress(value);
65+
66+
return (
67+
<Box horizontal {...boxProps}>
68+
<Left>{left}</Left>
69+
<Right>{right}</Right>
70+
</Box>
71+
);
72+
};
73+
74+
export const Address = ({ value }: { value: string }) => (
75+
<SplitAddress value={value} color="neutral.c80" ff="Inter" fontSize={3} />
76+
);
77+
78+
export const Cell = styled(Box).attrs<{
79+
px?: number;
80+
}>(p => ({
81+
px: p.px === 0 ? p.px : p.px || 4,
82+
horizontal: true,
83+
alignItems: "center",
84+
}))`
85+
width: 150px;
86+
flex-grow: 1;
87+
flex-shrink: 1;
88+
display: block;
89+
`;

apps/ledger-live-desktop/src/renderer/families/bitcoin/__tests__/operationDetails.test.tsx

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,27 @@ import { getCryptoCurrencyById } from "@ledgerhq/live-common/currencies/index";
66
import { Operation } from "@ledgerhq/types-live";
77
import operationDetails from "../operationDetails";
88

9-
const { OperationDetailsExtra } = operationDetails;
9+
const { OperationDetailsExtra, addressCell } = operationDetails;
1010

1111
const ZCASH_OPERATION_TYPES = [
12-
"IN",
13-
"OUT",
1412
"SHIELDED_TX_SAPLING_IN",
1513
"SHIELDED_TX_SAPLING_OUT",
1614
"SHIELDED_TX_ORCHARD_IN",
1715
"SHIELDED_TX_ORCHARD_OUT",
1816
] as const;
1917

20-
const createOperation = (type: string): Operation =>
18+
const createOperation = (
19+
type: string,
20+
options: { senders?: string[]; recipients?: string[] } = {},
21+
): Operation =>
2122
({
2223
id: `op-${type}-1`,
2324
hash: "0xhash",
2425
type,
2526
value: new BigNumber(1000),
2627
fee: new BigNumber(100),
27-
senders: ["sender1"],
28-
recipients: ["recipient1"],
28+
senders: options.senders ?? ["sender1"],
29+
recipients: options.recipients ?? ["recipient1"],
2930
blockHash: null,
3031
blockHeight: null,
3132
accountId: "account-1",
@@ -40,7 +41,7 @@ const zcashAccount = {
4041

4142
const bitcoinAccount = createFixtureAccount();
4243

43-
describe("Bitcoin operationDetails", () => {
44+
describe("Zcash operationDetails", () => {
4445
describe("OperationDetailsExtra", () => {
4546
it.each(ZCASH_OPERATION_TYPES)(
4647
"should render OperationDetailsExtra for Zcash account with %s operation type",
@@ -62,3 +63,89 @@ describe("Bitcoin operationDetails", () => {
6263
});
6364
});
6465
});
66+
67+
const zcashCurrency = getCryptoCurrencyById("zcash");
68+
69+
describe("addressCell", () => {
70+
describe("Zcash shielded operations", () => {
71+
it("should show the address when discreet mode is off (sapling)", () => {
72+
const address = "zs1abc123def456";
73+
const operation = createOperation("SHIELDED_TX_SAPLING_IN", {
74+
senders: [address],
75+
});
76+
77+
const AddressCell = addressCell["SHIELDED_TX_SAPLING_IN"];
78+
79+
const { container } = render(<AddressCell operation={operation} currency={zcashCurrency} />, {
80+
initialState: {
81+
settings: {
82+
discreetMode: false,
83+
},
84+
},
85+
});
86+
87+
expect(container).toHaveTextContent(address);
88+
});
89+
90+
it("should show asterisks instead of the address when discreet mode is on (sapling)", () => {
91+
const address = "zs1abc123def456";
92+
const operation = createOperation("SHIELDED_TX_SAPLING_OUT", {
93+
recipients: [address],
94+
});
95+
96+
const AddressCell = addressCell["SHIELDED_TX_SAPLING_OUT"];
97+
98+
const { container } = render(<AddressCell operation={operation} currency={zcashCurrency} />, {
99+
initialState: {
100+
settings: {
101+
discreetMode: true,
102+
},
103+
},
104+
});
105+
106+
const asterisks = "*".repeat(address.length);
107+
expect(screen.getByText(asterisks)).toBeInTheDocument();
108+
expect(container).not.toHaveTextContent(address);
109+
});
110+
111+
it("should show the address when discreet mode is off (orchard)", () => {
112+
const address = "zs1abc123def456";
113+
const operation = createOperation("SHIELDED_TX_ORCHARD_IN", {
114+
senders: [address],
115+
});
116+
117+
const AddressCell = addressCell["SHIELDED_TX_ORCHARD_IN"];
118+
119+
const { container } = render(<AddressCell operation={operation} currency={zcashCurrency} />, {
120+
initialState: {
121+
settings: {
122+
discreetMode: false,
123+
},
124+
},
125+
});
126+
127+
expect(container).toHaveTextContent(address);
128+
});
129+
130+
it("should show asterisks instead of the address when discreet mode is on (orchard)", () => {
131+
const address = "zs1abc123def456";
132+
const operation = createOperation("SHIELDED_TX_ORCHARD_OUT", {
133+
recipients: [address],
134+
});
135+
136+
const AddressCell = addressCell["SHIELDED_TX_ORCHARD_OUT"];
137+
138+
const { container } = render(<AddressCell operation={operation} currency={zcashCurrency} />, {
139+
initialState: {
140+
settings: {
141+
discreetMode: true,
142+
},
143+
},
144+
});
145+
146+
const asterisks = "*".repeat(address.length);
147+
expect(screen.getByText(asterisks)).toBeInTheDocument();
148+
expect(container).not.toHaveTextContent(address);
149+
});
150+
});
151+
});

0 commit comments

Comments
 (0)