Skip to content

Commit 1393e3a

Browse files
committed
feat: integrate solana token extensions into LLD
1 parent 5214b75 commit 1393e3a

11 files changed

+650
-6
lines changed

apps/ledger-live-desktop/src/renderer/components/Alert.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ type AlertType =
206206
| "update"
207207
| "twitter";
208208

209-
type Props = BoxProps & {
209+
type Props = Omit<BoxProps, "right" | "left"> & {
210210
type?: AlertType;
211211
children?: React.ReactNode;
212212
onLearnMore?: () => void;
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React from "react";
22
import { Trans } from "react-i18next";
33
import { SolanaAccount, SolanaTokenAccount } from "@ledgerhq/live-common/families/solana/types";
4-
import { isTokenAccountFrozen } from "@ledgerhq/live-common/families/solana/logic";
4+
import { isTokenAccountFrozen } from "@ledgerhq/live-common/families/solana/token";
55
import { SubAccount } from "@ledgerhq/types-live";
66

77
import Box from "~/renderer/components/Box";
88
import Alert from "~/renderer/components/Alert";
99
import AccountSubHeader from "../../components/AccountSubHeader/index";
10+
import TokenExtensionsInfoBox from "./Token2022/TokenExtensionsInfoBox";
1011

1112
type Account = SolanaAccount | SolanaTokenAccount | SubAccount;
1213

@@ -15,16 +16,25 @@ type Props = {
1516
};
1617

1718
export default function SolanaAccountSubHeader({ account }: Props) {
19+
const tokenExtensions =
20+
account.type === "TokenAccount" ? (account as SolanaTokenAccount)?.extensions : undefined;
1821
return (
1922
<>
23+
<AccountSubHeader family="Solana" team="Solana Labs"></AccountSubHeader>
2024
{isTokenAccountFrozen(account) && (
2125
<Box mb={10}>
2226
<Alert type="warning">
2327
<Trans i18nKey="solana.token.frozenStateWarning" />
2428
</Alert>
2529
</Box>
2630
)}
27-
<AccountSubHeader family="Solana" team="Solana Labs"></AccountSubHeader>
31+
{!!tokenExtensions && (
32+
<TokenExtensionsInfoBox
33+
mb={3}
34+
tokenAccount={account as SolanaTokenAccount}
35+
extensions={tokenExtensions}
36+
/>
37+
)}
2838
</>
2939
);
3040
}

apps/ledger-live-desktop/src/renderer/families/solana/MemoValueField.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Transaction,
99
SolanaAccount,
1010
} from "@ledgerhq/live-common/families/solana/types";
11+
import { SolanaRecipientMemoIsRequired } from "@ledgerhq/live-common/families/solana/errors";
1112

1213
type Props = {
1314
onChange: (t: Transaction) => void;
@@ -36,13 +37,19 @@ const MemoValueField = ({ onChange, account, transaction, status }: Props) => {
3637
},
3738
[onChange, transaction, bridge],
3839
);
40+
41+
const isRecipientMemoRequired = status?.errors?.memo instanceof SolanaRecipientMemoIsRequired;
3942
return transaction.model.kind === "transfer" || transaction.model.kind === "token.transfer" ? (
4043
<Input
4144
warning={status.warnings.memo}
4245
error={status.errors.memo}
4346
value={transaction.model.uiState.memo || ""}
4447
onChange={onMemoValueChange}
45-
placeholder={t("families.solana.memoPlaceholder")}
48+
placeholder={t(
49+
isRecipientMemoRequired
50+
? "families.solana.requiredMemoPlaceholder"
51+
: "families.solana.memoPlaceholder",
52+
)}
4653
/>
4754
) : null;
4855
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from "react";
2+
import {
3+
Transaction,
4+
TransactionStatus,
5+
SolanaTokenAccount,
6+
} from "@ledgerhq/live-common/families/solana/types";
7+
import { Account } from "@ledgerhq/types-live";
8+
import { findSubAccountById } from "@ledgerhq/live-common/account/index";
9+
import TokenTransferFeesWarning from "./Token2022/TokenTransferFeesWarning";
10+
11+
type Props = {
12+
account: Account;
13+
parentAccount: Account | null | undefined;
14+
updateTransaction: (updater: (t: Transaction) => Transaction) => void;
15+
onChange: (t: Transaction) => void;
16+
transaction: Transaction;
17+
status: TransactionStatus;
18+
bridgePending?: boolean;
19+
trackProperties?: Record<string, unknown>;
20+
};
21+
22+
const Root = ({ account, transaction, bridgePending, onChange }: Props) => {
23+
const tokenAcc = transaction.subAccountId
24+
? (findSubAccountById(account, transaction.subAccountId) as SolanaTokenAccount)
25+
: undefined;
26+
27+
return (
28+
<div>
29+
{!!tokenAcc && (
30+
<TokenTransferFeesWarning
31+
account={account}
32+
tokenAccount={tokenAcc}
33+
transaction={transaction}
34+
bridgePending={bridgePending}
35+
onChange={onChange}
36+
/>
37+
)}
38+
</div>
39+
);
40+
};
41+
42+
export default {
43+
component: Root,
44+
fields: [],
45+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React from "react";
2+
import { Trans } from "react-i18next";
3+
import BigNumber from "bignumber.js";
4+
import {
5+
Transaction,
6+
SolanaAccount,
7+
SolanaTokenAccount,
8+
TransferFeeCalculated,
9+
} from "@ledgerhq/live-common/families/solana/types";
10+
import { SubAccount } from "@ledgerhq/types-live";
11+
import { findSubAccountById, getMainAccount } from "@ledgerhq/live-common/account/index";
12+
import Text from "~/renderer/components/Text";
13+
import Box from "~/renderer/components/Box";
14+
import { useAccountUnit } from "~/renderer/hooks/useAccountUnit";
15+
import FormattedVal from "~/renderer/components/FormattedVal";
16+
import CounterValue from "~/renderer/components/CounterValue";
17+
18+
type Account = SolanaAccount | SolanaTokenAccount | SubAccount;
19+
20+
type Props = {
21+
account: Account;
22+
parentAccount: SolanaAccount | null | undefined;
23+
transaction: Transaction;
24+
};
25+
26+
const StepSummaryAdditionalRows = ({ account, parentAccount, transaction }: Props) => {
27+
const mainAccount = getMainAccount(account, parentAccount);
28+
const tokenAccount = transaction.subAccountId
29+
? (findSubAccountById(mainAccount, transaction.subAccountId) as SolanaTokenAccount)
30+
: undefined;
31+
32+
const transferFees =
33+
transaction.model.commandDescriptor?.command.kind === "token.transfer"
34+
? transaction.model.commandDescriptor.command.extensions?.transferFee
35+
: undefined;
36+
37+
return (
38+
<>
39+
{!!(transferFees && transferFees.feeBps > 0 && tokenAccount) && (
40+
<TransferFeeAdditionalRows
41+
transferFees={transferFees}
42+
tokenAccount={tokenAccount}
43+
transaction={transaction}
44+
/>
45+
)}
46+
</>
47+
);
48+
};
49+
50+
function TransferFeeAdditionalRows({
51+
transferFees,
52+
tokenAccount,
53+
}: {
54+
transferFees: TransferFeeCalculated;
55+
tokenAccount: SolanaTokenAccount;
56+
transaction: Transaction;
57+
}) {
58+
const unit = useAccountUnit(tokenAccount);
59+
return (
60+
<>
61+
<Box horizontal justifyContent="space-between" alignItems="center" mt={10} mb={20}>
62+
<Text ff="Inter|Medium" color="palette.text.shade40" fontSize={4}>
63+
<Trans i18nKey="solana.token.transferFees.transferFeesLabel" />
64+
</Text>
65+
<Box>
66+
<FormattedVal
67+
color="palette.text.shade80"
68+
disableRounding
69+
unit={unit}
70+
alwaysShowValue
71+
val={BigNumber(transferFees.transferFee)}
72+
fontSize={4}
73+
inline
74+
showCode
75+
/>
76+
<Box textAlign="right">
77+
<CounterValue
78+
color="palette.text.shade60"
79+
fontSize={3}
80+
currency={tokenAccount.token}
81+
value={BigNumber(transferFees.transferFee)}
82+
alwaysShowSign={false}
83+
alwaysShowValue
84+
/>
85+
</Box>
86+
</Box>
87+
</Box>
88+
</>
89+
);
90+
}
91+
92+
export default StepSummaryAdditionalRows;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import React from "react";
2+
import { Trans } from "react-i18next";
3+
import { SpaceProps } from "styled-system";
4+
5+
import {
6+
SolanaTokenAccount,
7+
SolanaTokenAccountExtensions,
8+
} from "@ledgerhq/live-common/families/solana/types";
9+
import { bpsToPercent } from "@ledgerhq/live-common/families/solana/token";
10+
import Box from "~/renderer/components/Box";
11+
import Alert from "~/renderer/components/Alert";
12+
import Text from "~/renderer/components/Text";
13+
import LabelInfoTooltip from "~/renderer/components/LabelInfoTooltip";
14+
import Button from "~/renderer/components/Button";
15+
import TokenExtensionsInfoDrawer from "./TokenExtensionsInfoDrawer";
16+
17+
type Props = SpaceProps & {
18+
tokenAccount: SolanaTokenAccount;
19+
extensions: SolanaTokenAccountExtensions;
20+
};
21+
22+
export default function TokenExtensionsInfoBox({ tokenAccount, extensions, ...boxProps }: Props) {
23+
const extensionsSize = Object.values(extensions);
24+
const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
25+
if (!extensionsSize.length) return null;
26+
27+
const onLearnMoreClick = () => {
28+
openDrawer();
29+
};
30+
31+
const openDrawer = () => {
32+
setIsDrawerOpen(true);
33+
};
34+
const closeDrawer = () => {
35+
setIsDrawerOpen(false);
36+
};
37+
38+
return (
39+
<Box {...boxProps}>
40+
<Alert
41+
type="hint"
42+
right={
43+
<Button small lighterPrimary onClick={onLearnMoreClick}>
44+
<Trans i18nKey="common.learnMore" />
45+
</Button>
46+
}
47+
>
48+
<Box flexDirection="column">
49+
{!!extensions.nonTransferable && (
50+
<Text>
51+
<Trans i18nKey="solana.token.nonTransferable.notice" />
52+
</Text>
53+
)}
54+
55+
{!!extensions.interestRate && (
56+
<LabelInfoTooltip text={<Trans i18nKey="solana.token.interestRate.tooltipHint" />}>
57+
<Text>
58+
<Trans
59+
i18nKey="solana.token.interestRate.notice"
60+
values={{ rate: bpsToPercent(extensions.interestRate.rateBps) }}
61+
/>
62+
</Text>
63+
</LabelInfoTooltip>
64+
)}
65+
66+
{extensions.permanentDelegate ? (
67+
<LabelInfoTooltip text={<Trans i18nKey="solana.token.permanentDelegate.tooltipHint" />}>
68+
<Text>
69+
{extensions.permanentDelegate.delegateAddress ? (
70+
<Trans i18nKey="solana.token.permanentDelegate.notice" />
71+
) : (
72+
<Trans i18nKey="solana.token.permanentDelegate.initializationNotice" />
73+
)}
74+
</Text>
75+
</LabelInfoTooltip>
76+
) : null}
77+
78+
{!!extensions.transferFee && (
79+
<LabelInfoTooltip text={<Trans i18nKey="solana.token.transferFees.tooltipHint" />}>
80+
<Text>
81+
<Trans
82+
i18nKey="solana.token.transferFees.notice"
83+
values={{ fee: bpsToPercent(extensions.transferFee.feeBps) }}
84+
/>
85+
</Text>
86+
</LabelInfoTooltip>
87+
)}
88+
89+
{extensions.transferHook ? (
90+
extensions.transferHook.programAddress ? (
91+
<LabelInfoTooltip
92+
text={
93+
<Trans
94+
i18nKey="solana.token.transferHook.tooltipHint"
95+
values={{ programAddress: extensions.transferHook.programAddress }}
96+
/>
97+
}
98+
>
99+
<Text>
100+
<Trans i18nKey="solana.token.transferHook.notice" />
101+
</Text>
102+
</LabelInfoTooltip>
103+
) : (
104+
<Text>
105+
<Trans i18nKey="solana.token.transferHook.initializationNotice" />
106+
</Text>
107+
)
108+
) : null}
109+
110+
{!!extensions.requiredMemoOnTransfer && (
111+
<LabelInfoTooltip
112+
text={<Trans i18nKey="solana.token.requiredMemoOnTransfer.tooltipHint" />}
113+
>
114+
<Text>
115+
<Trans i18nKey="solana.token.requiredMemoOnTransfer.notice" />
116+
</Text>
117+
</LabelInfoTooltip>
118+
)}
119+
</Box>
120+
<TokenExtensionsInfoDrawer
121+
extensions={extensions}
122+
tokenAccount={tokenAccount}
123+
isOpen={isDrawerOpen}
124+
closeDrawer={closeDrawer}
125+
/>
126+
</Alert>
127+
</Box>
128+
);
129+
}

0 commit comments

Comments
 (0)