Skip to content

Commit 31f0aa1

Browse files
committed
feat: new transfer fee UI
1 parent 74fdac0 commit 31f0aa1

File tree

4 files changed

+202
-6
lines changed

4 files changed

+202
-6
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { Stack } from "@namada/components";
2+
import { namadaRegistryChainAssetsMapAtom } from "atoms/integrations";
3+
import { tokenPricesFamily } from "atoms/prices/atoms";
4+
import BigNumber from "bignumber.js";
5+
import { TransactionFeeProps } from "hooks/useTransactionFee";
6+
import { useAtomValue } from "jotai";
7+
import { useMemo, useState } from "react";
8+
import { GoInfo } from "react-icons/go";
9+
import { IoIosArrowDown } from "react-icons/io";
10+
import { twMerge } from "tailwind-merge";
11+
import { Address, FrontendFeeEntry } from "types";
12+
import { calculateFrontendFeeAmount } from "utils/frontendFee";
13+
import { getDisplayGasFee } from "utils/gas";
14+
import { GasFeeModal } from "./GasFeeModal";
15+
import { IconTooltip } from "./IconTooltip";
16+
17+
type FrontendFeeInfo = {
18+
fee: FrontendFeeEntry;
19+
displayAmount?: BigNumber;
20+
token?: Address;
21+
};
22+
23+
export const TransferFeeButton = ({
24+
feeProps,
25+
inOrOutOfMASP,
26+
isShieldedTransfer = false,
27+
frontendFeeInfo,
28+
}: {
29+
feeProps: TransactionFeeProps;
30+
inOrOutOfMASP: boolean;
31+
isShieldedTransfer?: boolean;
32+
frontendFeeInfo?: FrontendFeeInfo;
33+
}): JSX.Element => {
34+
const [modalOpen, setModalOpen] = useState(false);
35+
const [feeDetailsOpen, setFeeDetailsOpen] = useState(false);
36+
37+
const chainAssetsMap = useAtomValue(namadaRegistryChainAssetsMapAtom);
38+
39+
const gasDollarMap =
40+
useAtomValue(
41+
tokenPricesFamily(
42+
feeProps.gasPriceTable?.map((item) => item.token.address) ?? []
43+
)
44+
).data ?? {};
45+
46+
const gasDisplayAmount = useMemo(() => {
47+
if (!chainAssetsMap.data) {
48+
return;
49+
}
50+
51+
return getDisplayGasFee(feeProps.gasConfig, chainAssetsMap.data);
52+
}, [feeProps, chainAssetsMap.data]);
53+
54+
const [frontendFeeAmount, frontendFeeFiatAmount, symbol] = useMemo((): [
55+
BigNumber?,
56+
BigNumber?,
57+
string?,
58+
] => {
59+
if (
60+
frontendFeeInfo &&
61+
frontendFeeInfo.token &&
62+
frontendFeeInfo.displayAmount
63+
) {
64+
const feeAmount = calculateFrontendFeeAmount(
65+
frontendFeeInfo.displayAmount,
66+
frontendFeeInfo.fee
67+
);
68+
const dollarPrice = gasDollarMap[frontendFeeInfo.token];
69+
const fiatFeeAmount = feeAmount.multipliedBy(dollarPrice);
70+
const symbol = chainAssetsMap?.data?.[frontendFeeInfo.token]?.symbol;
71+
72+
return [feeAmount, fiatFeeAmount, symbol];
73+
}
74+
return [];
75+
}, [gasDollarMap, frontendFeeInfo]);
76+
77+
const fiatAmount = useMemo(() => {
78+
if (!gasDisplayAmount || !gasDollarMap) {
79+
return;
80+
}
81+
const dollarPrice = gasDollarMap[feeProps.gasConfig.gasToken];
82+
let fiatAmount =
83+
gasDisplayAmount.totalDisplayAmount.multipliedBy(dollarPrice);
84+
85+
if (frontendFeeFiatAmount) {
86+
fiatAmount = fiatAmount.plus(frontendFeeFiatAmount);
87+
}
88+
return fiatAmount;
89+
}, [gasDisplayAmount, gasDollarMap, frontendFeeAmount]);
90+
91+
return (
92+
<Stack className="w-full text-sm text-neutral-300">
93+
<Stack direction="horizontal" className="justify-between items-center">
94+
<div
95+
className="cursor-pointer select-none underline "
96+
onClick={() => setFeeDetailsOpen((opened) => !opened)}
97+
>
98+
{feeDetailsOpen ? "Hide fee settings" : "Fee settings"}
99+
</div>
100+
<div>
101+
Total Fee {fiatAmount ? `$${fiatAmount.decimalPlaces(6)}` : ""}
102+
</div>
103+
</Stack>
104+
{feeDetailsOpen && (
105+
<Stack className="w-full">
106+
<Stack
107+
direction="horizontal"
108+
className="justify-between items-center"
109+
>
110+
<div>Gas fee:</div>
111+
<Stack direction="horizontal" gap={2} className="items-center">
112+
<div>
113+
{gasDisplayAmount ?
114+
gasDisplayAmount.totalDisplayAmount.toString()
115+
: ""}{" "}
116+
</div>
117+
<div className="flex items-center gap-2">
118+
<button
119+
type="button"
120+
className={twMerge(
121+
"flex items-center gap-1",
122+
"border rounded-sm px-2 py-1 text-xs",
123+
"transition-all cursor-pointer hover:text-yellow"
124+
)}
125+
onClick={() => setModalOpen(true)}
126+
>
127+
<span className="text- font-medium">
128+
{gasDisplayAmount?.asset.symbol || ""}
129+
</span>
130+
<IoIosArrowDown />
131+
</button>
132+
</div>
133+
</Stack>
134+
</Stack>
135+
{inOrOutOfMASP && frontendFeeInfo && (
136+
<Stack
137+
direction="horizontal"
138+
className="justify-between items-center"
139+
>
140+
<Stack direction="horizontal" gap={2} className="items-center">
141+
MASP fee
142+
<div className="flex relative items-center">
143+
<IconTooltip
144+
className="bg-transparent w-5 h-5"
145+
tooltipClassName="text-yellow text-center w-[340px]"
146+
icon={<GoInfo className="w-5 h-5 text-yellow" />}
147+
text={
148+
<div className="w-full">
149+
MASP fees are set by the Namadillo Host and may
150+
<br /> vary accross Namadillo instances
151+
</div>
152+
}
153+
/>
154+
</div>
155+
</Stack>
156+
157+
<div>
158+
{frontendFeeAmount && symbol ?
159+
`${frontendFeeAmount.toString()} ${symbol}`
160+
: "0"}
161+
</div>
162+
</Stack>
163+
)}
164+
</Stack>
165+
)}
166+
{modalOpen && (
167+
<GasFeeModal
168+
feeProps={feeProps}
169+
onClose={() => setModalOpen(false)}
170+
isShielded={isShieldedTransfer}
171+
chainAssetsMap={chainAssetsMap.data || {}}
172+
/>
173+
)}
174+
</Stack>
175+
);
176+
};

apps/namadillo/src/App/Transfer/TransferDestination.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { shortenAddress } from "@namada/utils";
55
import { ConnectProviderButton } from "App/Common/ConnectProviderButton";
66
import { TokenAmountCard } from "App/Common/TokenAmountCard";
77
import { TransactionFee } from "App/Common/TransactionFee";
8-
import { TransactionFeeButton } from "App/Common/TransactionFeeButton";
8+
import { TransferFeeButton } from "App/Common/TransferFeeButton";
99
import { routes } from "App/routes";
1010
import {
1111
isIbcAddress,
@@ -24,7 +24,7 @@ import { useAtomValue } from "jotai";
2424
import { useCallback, useEffect, useState } from "react";
2525
import { GoChevronDown } from "react-icons/go";
2626
import { useLocation } from "react-router-dom";
27-
import { Address } from "types";
27+
import { Address, FrontendFeeEntry } from "types";
2828
import namadaShieldedIcon from "./assets/namada-shielded.svg";
2929
import namadaTransparentIcon from "./assets/namada-transparent.svg";
3030
import semiTransparentEye from "./assets/semi-transparent-eye.svg";
@@ -53,6 +53,7 @@ type TransferDestinationProps = {
5353
onChangeMemo?: (address: string) => void;
5454
isShielding?: boolean;
5555
isUnshielding?: boolean;
56+
frontendFee?: FrontendFeeEntry;
5657
};
5758

5859
export const TransferDestination = ({
@@ -72,6 +73,7 @@ export const TransferDestination = ({
7273
onChangeMemo,
7374
isShielding = false,
7475
isUnshielding = false,
76+
frontendFee,
7577
}: TransferDestinationProps): JSX.Element => {
7678
const { data: accounts } = useAtomValue(allDefaultAccountsAtom);
7779
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -309,9 +311,17 @@ export const TransferDestination = ({
309311
<footer className="flex mt-10">
310312
{changeFeeEnabled ?
311313
feeProps && (
312-
<TransactionFeeButton
314+
<TransferFeeButton
313315
feeProps={feeProps}
314316
isShieldedTransfer={isShieldedTx}
317+
inOrOutOfMASP={isShielding || isUnshielding}
318+
frontendFeeInfo={
319+
frontendFee && {
320+
fee: frontendFee,
321+
displayAmount: amount,
322+
token: sourceAsset?.address,
323+
}
324+
}
315325
/>
316326
)
317327
: gasDisplayAmount &&

apps/namadillo/src/App/Transfer/TransferModule.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,15 @@ export const TransferModule = ({
133133
: undefined;
134134
}, [gasConfig]);
135135

136-
const availableAmountMinusFees = useMemo(() => {
136+
const [availableAmountMinusFees, frontendFeeEntry] = useMemo(() => {
137137
if (
138138
!availableAmount ||
139139
!availableAssets ||
140140
!displayGasFee ||
141141
!gasConfig ||
142142
!frontendFee
143143
)
144-
return;
144+
return [];
145145
let amountMinusFees = availableAmount;
146146

147147
if (gasConfig?.gasToken === selectedAsset?.asset.address) {
@@ -163,7 +163,7 @@ export const TransferModule = ({
163163
.decimalPlaces(6, BigNumber.ROUND_UP);
164164
}
165165

166-
return BigNumber.max(amountMinusFees, 0);
166+
return [BigNumber.max(amountMinusFees, 0), frontendSusFee] as const;
167167
}, [selectedAsset?.asset.address, availableAmount, displayGasFee]);
168168

169169
const validationResult = useMemo((): ValidationResult => {
@@ -264,6 +264,7 @@ export const TransferModule = ({
264264
isSubmitting={isSubmitting}
265265
isShielding={isShielding}
266266
isUnshielding={isUnshielding}
267+
frontendFee={frontendFeeEntry}
267268
/>
268269
{ibcTransfer && requiresIbcChannels && ibcChannels && (
269270
<IbcChannels

apps/namadillo/src/utils/frontendFee.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,12 @@ export const calculateAmountWithFrontendFee = (
5454
.multipliedBy(frontendFee.percentage.plus(1))
5555
.decimalPlaces(6, BigNumber.ROUND_DOWN);
5656
};
57+
58+
export const calculateFrontendFeeAmount = (
59+
displayAmount: BigNumber,
60+
frontendFee: FrontendFeeEntry
61+
): BigNumber => {
62+
return displayAmount
63+
.multipliedBy(frontendFee.percentage)
64+
.decimalPlaces(6, BigNumber.ROUND_DOWN);
65+
};

0 commit comments

Comments
 (0)