Skip to content

Commit 8cacded

Browse files
feat: add gas selector (#1585)
* feat: add gas selector * feat: adding disabled state when gas is not present * fix: fixing unit test --------- Co-authored-by: Pedro Rezende <xpedrorezende@gmail.com>
1 parent 9879db0 commit 8cacded

31 files changed

+444
-369
lines changed

apps/namadillo/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"jotai": "^2.6.3",
2929
"jotai-tanstack-query": "^0.8.5",
3030
"lodash.debounce": "^4.0.8",
31+
"lodash.isequal": "^4.5.0",
3132
"namada-chain-registry": "https://github.com/anoma/namada-chain-registry",
3233
"react": "^18.3.1",
3334
"react-dom": "^18.3.1",
@@ -91,6 +92,7 @@
9192
"@types/invariant": "^2.2.37",
9293
"@types/jest": "^29.5.12",
9394
"@types/lodash.debounce": "^4.0.9",
95+
"@types/lodash.isequal": "^4",
9496
"@types/node": "^22.5.4",
9597
"@types/react": "^18.3.5",
9698
"@types/react-dom": "^18.3.0",

apps/namadillo/src/App/Common/GasFeeModal.tsx

Lines changed: 185 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { Modal, SkeletonLoading } from "@namada/components";
2-
import { chainAssetsMapAtom, nativeTokenAddressAtom } from "atoms/chain";
31
import {
4-
gasPriceForAllTokensAtom,
5-
storageGasTokenAtom,
6-
} from "atoms/fees/atoms";
2+
ActionButton,
3+
AmountInput,
4+
Modal,
5+
StyledSelectBox,
6+
} from "@namada/components";
7+
import { chainAssetsMapAtom, nativeTokenAddressAtom } from "atoms/chain";
8+
import { GasPriceTable, GasPriceTableItem } from "atoms/fees/atoms";
79
import { tokenPricesFamily } from "atoms/prices/atoms";
810
import BigNumber from "bignumber.js";
9-
import { useAtomValue, useSetAtom } from "jotai";
11+
import { TransactionFeeProps } from "hooks/useTransactionFee";
12+
import { useAtomValue } from "jotai";
1013
import { IoClose } from "react-icons/io5";
1114
import { twMerge } from "tailwind-merge";
1215
import { GasConfig } from "types";
@@ -15,22 +18,87 @@ import { getDisplayGasFee } from "utils/gas";
1518
import { FiatCurrency } from "./FiatCurrency";
1619
import { TokenCurrency } from "./TokenCurrency";
1720

18-
export const GasFeeModal = ({
21+
const useSortByNativeToken = () => {
22+
const nativeToken = useAtomValue(nativeTokenAddressAtom).data;
23+
return (a: GasPriceTableItem, b: GasPriceTableItem) =>
24+
a.token === nativeToken ? -1
25+
: b.token === nativeToken ? 1
26+
: 0;
27+
};
28+
29+
const useBuildGasOption = ({
1930
gasConfig,
20-
onClose,
31+
gasPriceTable,
2132
}: {
2233
gasConfig: GasConfig;
23-
onClose: () => void;
24-
}): JSX.Element => {
25-
const setStorageGasToken = useSetAtom(storageGasTokenAtom);
26-
const gasPriceForAllTokens = useAtomValue(gasPriceForAllTokensAtom);
34+
gasPriceTable: GasPriceTable | undefined;
35+
}) => {
2736
const chainAssetsMap = useAtomValue(chainAssetsMapAtom);
28-
const nativeTokenAddress = useAtomValue(nativeTokenAddressAtom).data;
37+
const gasDollarMap =
38+
useAtomValue(
39+
tokenPricesFamily(gasPriceTable?.map((item) => item.token) ?? [])
40+
).data ?? {};
2941

30-
const data = gasPriceForAllTokens.data ?? [];
42+
return (
43+
override: Partial<GasConfig>
44+
): {
45+
option: GasConfig;
46+
selected: boolean;
47+
disabled: boolean;
48+
symbol: string;
49+
displayAmount: BigNumber;
50+
dollar?: BigNumber;
51+
} => {
52+
const option: GasConfig = {
53+
...gasConfig,
54+
...override,
55+
};
56+
57+
const displayAmount = getDisplayGasFee(option);
58+
const price = gasDollarMap[option.gasToken];
59+
const dollar = price ? price.multipliedBy(displayAmount) : undefined;
60+
61+
const selected =
62+
!gasConfig.gasLimit.isEqualTo(0) &&
63+
option.gasLimit.isEqualTo(gasConfig.gasLimit) &&
64+
option.gasPrice.isEqualTo(gasConfig.gasPrice) &&
65+
option.gasToken === gasConfig.gasToken;
66+
67+
const disabled =
68+
gasConfig.gasLimit.isEqualTo(0) || gasConfig.gasPrice.isEqualTo(0);
3169

32-
const tokenAddresses = data.map((item) => item.token);
33-
const gasDollarMap = useAtomValue(tokenPricesFamily(tokenAddresses));
70+
const asset =
71+
chainAssetsMap[option.gasToken] ?? unknownAsset(option.gasToken);
72+
const symbol = asset.symbol;
73+
74+
return {
75+
option,
76+
selected,
77+
disabled,
78+
symbol,
79+
displayAmount,
80+
dollar,
81+
};
82+
};
83+
};
84+
85+
export const GasFeeModal = ({
86+
feeProps,
87+
onClose,
88+
}: {
89+
feeProps: TransactionFeeProps;
90+
onClose: () => void;
91+
}): JSX.Element => {
92+
const {
93+
gasConfig,
94+
gasEstimate,
95+
gasPriceTable,
96+
onChangeGasLimit,
97+
onChangeGasToken,
98+
} = feeProps;
99+
100+
const sortByNativeToken = useSortByNativeToken();
101+
const buildGasOption = useBuildGasOption({ gasConfig, gasPriceTable });
34102

35103
return (
36104
<Modal onClose={onClose}>
@@ -49,61 +117,114 @@ export const GasFeeModal = ({
49117
>
50118
<IoClose />
51119
</i>
52-
<div className="text-center">
53-
<h2 className="font-medium">Select Gas Token</h2>
54-
<div className="text-sm mt-1">
55-
Gas fees deducted from your Namada accounts
56-
</div>
120+
121+
<h2 className="text-xl font-medium">Fee Options</h2>
122+
<div className="text-sm">
123+
Gas fees deducted from your Namada accounts
57124
</div>
58-
<div className="flex flex-col mt-4 max-h-[60vh] overflow-auto">
59-
{!data.length ?
60-
<SkeletonLoading height="100px" width="100%" />
61-
: data
62-
.sort((a, b) =>
63-
a.token === nativeTokenAddress ? -1
64-
: b.token === nativeTokenAddress ? 1
65-
: 0
66-
)
67-
.map(({ token, minDenomAmount }) => {
68-
const asset = chainAssetsMap[token] ?? unknownAsset(token);
69-
const symbol = asset.symbol;
70-
const fee = getDisplayGasFee({
71-
gasLimit: gasConfig.gasLimit,
72-
gasPrice: BigNumber(minDenomAmount),
73-
gasToken: token,
74-
asset,
75-
});
76-
const price = gasDollarMap.data?.[token];
77-
const dollar = price ? fee.multipliedBy(price) : undefined;
78-
79-
const selected = token === gasConfig.gasToken;
80-
81-
return (
82-
<button
83-
key={token}
84-
className={twMerge(
85-
"flex justify-between items-center",
86-
"bg-rblack rounded-sm px-5 min-h-[58px]",
87-
"hover:text-yellow hover:border-yellow transition-colors duration-300",
88-
selected ? "border border-white" : "m-px"
89-
)}
90-
type="button"
91-
onClick={() => {
92-
setStorageGasToken(token);
93-
onClose();
94-
}}
95-
>
96-
<div>{symbol}</div>
125+
126+
<div className="text-sm mt-8 mb-1">Fee</div>
127+
<div className="grid grid-cols-3 rounded-sm overflow-hidden">
128+
{[
129+
{ label: "Low", amount: gasEstimate?.min ?? 0 },
130+
{ label: "Average", amount: gasEstimate?.avg ?? 0 },
131+
{ label: "High", amount: gasEstimate?.max ?? 0 },
132+
].map((item) => {
133+
const { symbol, displayAmount, dollar, selected, disabled } =
134+
buildGasOption({
135+
gasLimit: BigNumber(item.amount),
136+
});
137+
138+
return (
139+
<button
140+
key={item.label}
141+
type="button"
142+
disabled={disabled}
143+
className={twMerge(
144+
"flex flex-col justify-center items-center flex-1 py-5 leading-4",
145+
"transition-colors duration-150 ease-out-quad",
146+
selected ?
147+
"cursor-auto bg-yellow text-black"
148+
: "cursor-pointer bg-neutral-800 hover:bg-neutral-700"
149+
)}
150+
onClick={() => onChangeGasLimit(BigNumber(item.amount))}
151+
>
152+
<div className="font-semibold">{item.label}</div>
153+
{dollar && (
154+
<FiatCurrency
155+
amount={dollar}
156+
className="text-xs text-neutral-500 font-medium"
157+
/>
158+
)}
159+
<TokenCurrency
160+
amount={displayAmount}
161+
symbol={symbol}
162+
className="font-semibold mt-1"
163+
/>
164+
</button>
165+
);
166+
})}
167+
</div>
168+
169+
<div className="text-sm mt-4 mb-1">Fee Token</div>
170+
<StyledSelectBox
171+
id="fee-token-select"
172+
value={gasConfig.gasToken}
173+
containerProps={{
174+
className: twMerge(
175+
"text-sm w-full flex-1 border border-white rounded-sm",
176+
"px-4 py-[9px]"
177+
),
178+
}}
179+
arrowContainerProps={{ className: "right-4" }}
180+
listContainerProps={{ className: "w-full mt-2 border border-white" }}
181+
listItemProps={{ className: "border-0 px-2 -mx-2 rounded-sm" }}
182+
onChange={(e) => onChangeGasToken(e.target.value)}
183+
options={
184+
gasPriceTable?.sort(sortByNativeToken).map((item) => {
185+
const { symbol, displayAmount, dollar } = buildGasOption({
186+
gasPrice: item.gasPrice,
187+
gasToken: item.token,
188+
});
189+
return {
190+
id: item.token,
191+
value: (
192+
<div className="flex items-center justify-between w-full min-h-[42px] mr-5">
193+
<div className="text-base">{symbol}</div>
97194
<div className="text-right">
98195
{dollar && <FiatCurrency amount={dollar} />}
99196
<div className="text-xs">
100-
<TokenCurrency amount={fee} symbol={symbol} />
197+
<TokenCurrency amount={displayAmount} symbol={symbol} />
101198
</div>
102199
</div>
103-
</button>
104-
);
105-
})
200+
</div>
201+
),
202+
ariaLabel: symbol,
203+
};
204+
}) ?? []
106205
}
206+
/>
207+
208+
<div className="mt-4">
209+
<AmountInput
210+
label="Gas Amount"
211+
value={gasConfig.gasLimit}
212+
onChange={(e) => e.target.value && onChangeGasLimit(e.target.value)}
213+
/>
214+
</div>
215+
216+
<div className="mt-8">
217+
<ActionButton
218+
size="sm"
219+
className="max-w-[200px] mx-auto"
220+
backgroundColor="gray"
221+
backgroundHoverColor="yellow"
222+
textColor="white"
223+
textHoverColor="black"
224+
onClick={onClose}
225+
>
226+
Close
227+
</ActionButton>
107228
</div>
108229
</div>
109230
</Modal>
Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { chainAssetsMapAtom } from "atoms/chain";
2+
import { useAtomValue } from "jotai";
13
import { GasConfig } from "types";
24
import { unknownAsset } from "utils/assets";
35
import { getDisplayGasFee } from "utils/gas";
@@ -8,14 +10,21 @@ export const TransactionFee = ({
810
}: {
911
gasConfig: GasConfig;
1012
}): JSX.Element => {
11-
const asset = gasConfig.asset ?? unknownAsset(gasConfig.gasToken);
12-
const symbol = asset.symbol;
13-
const fee = getDisplayGasFee(gasConfig);
13+
const chainAssetsMap = useAtomValue(chainAssetsMapAtom);
14+
15+
const { gasToken } = gasConfig;
16+
const asset = chainAssetsMap[gasToken] ?? unknownAsset(gasToken);
17+
18+
const amount = getDisplayGasFee(gasConfig);
1419

1520
return (
1621
<div className="text-sm">
1722
Transaction fee:{" "}
18-
<TokenCurrency symbol={symbol} amount={fee} className="font-medium " />
23+
<TokenCurrency
24+
symbol={asset.symbol}
25+
amount={amount}
26+
className="font-medium"
27+
/>
1928
</div>
2029
);
2130
};

apps/namadillo/src/App/Common/TransactionFeeButton.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
import { TransactionFeeProps } from "hooks/useTransactionFee";
12
import { useState } from "react";
2-
import { GasConfig } from "types";
33
import { GasFeeModal } from "./GasFeeModal";
44
import { TransactionFee } from "./TransactionFee";
55

66
export const TransactionFeeButton = ({
7-
gasConfig,
7+
feeProps,
88
}: {
9-
gasConfig: GasConfig;
9+
feeProps: TransactionFeeProps;
1010
}): JSX.Element => {
1111
const [modalOpen, setModalOpen] = useState(false);
1212

@@ -17,13 +17,10 @@ export const TransactionFeeButton = ({
1717
className="underline hover:text-yellow transition-all cursor-pointer"
1818
onClick={() => setModalOpen(true)}
1919
>
20-
<TransactionFee gasConfig={gasConfig} />
20+
<TransactionFee gasConfig={feeProps.gasConfig} />
2121
</button>
2222
{modalOpen && (
23-
<GasFeeModal
24-
gasConfig={gasConfig}
25-
onClose={() => setModalOpen(false)}
26-
/>
23+
<GasFeeModal feeProps={feeProps} onClose={() => setModalOpen(false)} />
2724
)}
2825
</>
2926
);

apps/namadillo/src/App/Governance/AllProposalsTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export const AllProposalsTable: React.FC<ExtensionConnectedProps> = (props) => {
260260

261261
const TableSelectOption: React.FC<{
262262
children?: React.ReactNode;
263-
}> = ({ children }) => <span className="col-span-full w-fit">{children}</span>;
263+
}> = ({ children }) => <span className="whitespace-nowrap">{children}</span>;
264264

265265
type TableSelectProps<T extends string> = Omit<
266266
React.ComponentProps<typeof StyledSelectBox<T>>,

0 commit comments

Comments
 (0)