Skip to content

Commit

Permalink
feat: add gas selector (#1585)
Browse files Browse the repository at this point in the history
* feat: add gas selector

* feat: adding disabled state when gas is not present

* fix: fixing unit test

---------

Co-authored-by: Pedro Rezende <[email protected]>
  • Loading branch information
euharrison and pedrorezende authored Jan 27, 2025
1 parent 9879db0 commit 8cacded
Show file tree
Hide file tree
Showing 31 changed files with 444 additions and 369 deletions.
2 changes: 2 additions & 0 deletions apps/namadillo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"jotai": "^2.6.3",
"jotai-tanstack-query": "^0.8.5",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"namada-chain-registry": "https://github.com/anoma/namada-chain-registry",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down Expand Up @@ -91,6 +92,7 @@
"@types/invariant": "^2.2.37",
"@types/jest": "^29.5.12",
"@types/lodash.debounce": "^4.0.9",
"@types/lodash.isequal": "^4",
"@types/node": "^22.5.4",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
Expand Down
249 changes: 185 additions & 64 deletions apps/namadillo/src/App/Common/GasFeeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Modal, SkeletonLoading } from "@namada/components";
import { chainAssetsMapAtom, nativeTokenAddressAtom } from "atoms/chain";
import {
gasPriceForAllTokensAtom,
storageGasTokenAtom,
} from "atoms/fees/atoms";
ActionButton,
AmountInput,
Modal,
StyledSelectBox,
} from "@namada/components";
import { chainAssetsMapAtom, nativeTokenAddressAtom } from "atoms/chain";
import { GasPriceTable, GasPriceTableItem } from "atoms/fees/atoms";
import { tokenPricesFamily } from "atoms/prices/atoms";
import BigNumber from "bignumber.js";
import { useAtomValue, useSetAtom } from "jotai";
import { TransactionFeeProps } from "hooks/useTransactionFee";
import { useAtomValue } from "jotai";
import { IoClose } from "react-icons/io5";
import { twMerge } from "tailwind-merge";
import { GasConfig } from "types";
Expand All @@ -15,22 +18,87 @@ import { getDisplayGasFee } from "utils/gas";
import { FiatCurrency } from "./FiatCurrency";
import { TokenCurrency } from "./TokenCurrency";

export const GasFeeModal = ({
const useSortByNativeToken = () => {
const nativeToken = useAtomValue(nativeTokenAddressAtom).data;
return (a: GasPriceTableItem, b: GasPriceTableItem) =>
a.token === nativeToken ? -1
: b.token === nativeToken ? 1
: 0;
};

const useBuildGasOption = ({
gasConfig,
onClose,
gasPriceTable,
}: {
gasConfig: GasConfig;
onClose: () => void;
}): JSX.Element => {
const setStorageGasToken = useSetAtom(storageGasTokenAtom);
const gasPriceForAllTokens = useAtomValue(gasPriceForAllTokensAtom);
gasPriceTable: GasPriceTable | undefined;
}) => {
const chainAssetsMap = useAtomValue(chainAssetsMapAtom);
const nativeTokenAddress = useAtomValue(nativeTokenAddressAtom).data;
const gasDollarMap =
useAtomValue(
tokenPricesFamily(gasPriceTable?.map((item) => item.token) ?? [])
).data ?? {};

const data = gasPriceForAllTokens.data ?? [];
return (
override: Partial<GasConfig>
): {
option: GasConfig;
selected: boolean;
disabled: boolean;
symbol: string;
displayAmount: BigNumber;
dollar?: BigNumber;
} => {
const option: GasConfig = {
...gasConfig,
...override,
};

const displayAmount = getDisplayGasFee(option);
const price = gasDollarMap[option.gasToken];
const dollar = price ? price.multipliedBy(displayAmount) : undefined;

const selected =
!gasConfig.gasLimit.isEqualTo(0) &&
option.gasLimit.isEqualTo(gasConfig.gasLimit) &&
option.gasPrice.isEqualTo(gasConfig.gasPrice) &&
option.gasToken === gasConfig.gasToken;

const disabled =
gasConfig.gasLimit.isEqualTo(0) || gasConfig.gasPrice.isEqualTo(0);

const tokenAddresses = data.map((item) => item.token);
const gasDollarMap = useAtomValue(tokenPricesFamily(tokenAddresses));
const asset =
chainAssetsMap[option.gasToken] ?? unknownAsset(option.gasToken);
const symbol = asset.symbol;

return {
option,
selected,
disabled,
symbol,
displayAmount,
dollar,
};
};
};

export const GasFeeModal = ({
feeProps,
onClose,
}: {
feeProps: TransactionFeeProps;
onClose: () => void;
}): JSX.Element => {
const {
gasConfig,
gasEstimate,
gasPriceTable,
onChangeGasLimit,
onChangeGasToken,
} = feeProps;

const sortByNativeToken = useSortByNativeToken();
const buildGasOption = useBuildGasOption({ gasConfig, gasPriceTable });

return (
<Modal onClose={onClose}>
Expand All @@ -49,61 +117,114 @@ export const GasFeeModal = ({
>
<IoClose />
</i>
<div className="text-center">
<h2 className="font-medium">Select Gas Token</h2>
<div className="text-sm mt-1">
Gas fees deducted from your Namada accounts
</div>

<h2 className="text-xl font-medium">Fee Options</h2>
<div className="text-sm">
Gas fees deducted from your Namada accounts
</div>
<div className="flex flex-col mt-4 max-h-[60vh] overflow-auto">
{!data.length ?
<SkeletonLoading height="100px" width="100%" />
: data
.sort((a, b) =>
a.token === nativeTokenAddress ? -1
: b.token === nativeTokenAddress ? 1
: 0
)
.map(({ token, minDenomAmount }) => {
const asset = chainAssetsMap[token] ?? unknownAsset(token);
const symbol = asset.symbol;
const fee = getDisplayGasFee({
gasLimit: gasConfig.gasLimit,
gasPrice: BigNumber(minDenomAmount),
gasToken: token,
asset,
});
const price = gasDollarMap.data?.[token];
const dollar = price ? fee.multipliedBy(price) : undefined;

const selected = token === gasConfig.gasToken;

return (
<button
key={token}
className={twMerge(
"flex justify-between items-center",
"bg-rblack rounded-sm px-5 min-h-[58px]",
"hover:text-yellow hover:border-yellow transition-colors duration-300",
selected ? "border border-white" : "m-px"
)}
type="button"
onClick={() => {
setStorageGasToken(token);
onClose();
}}
>
<div>{symbol}</div>

<div className="text-sm mt-8 mb-1">Fee</div>
<div className="grid grid-cols-3 rounded-sm overflow-hidden">
{[
{ label: "Low", amount: gasEstimate?.min ?? 0 },
{ label: "Average", amount: gasEstimate?.avg ?? 0 },
{ label: "High", amount: gasEstimate?.max ?? 0 },
].map((item) => {
const { symbol, displayAmount, dollar, selected, disabled } =
buildGasOption({
gasLimit: BigNumber(item.amount),
});

return (
<button
key={item.label}
type="button"
disabled={disabled}
className={twMerge(
"flex flex-col justify-center items-center flex-1 py-5 leading-4",
"transition-colors duration-150 ease-out-quad",
selected ?
"cursor-auto bg-yellow text-black"
: "cursor-pointer bg-neutral-800 hover:bg-neutral-700"
)}
onClick={() => onChangeGasLimit(BigNumber(item.amount))}
>
<div className="font-semibold">{item.label}</div>
{dollar && (
<FiatCurrency
amount={dollar}
className="text-xs text-neutral-500 font-medium"
/>
)}
<TokenCurrency
amount={displayAmount}
symbol={symbol}
className="font-semibold mt-1"
/>
</button>
);
})}
</div>

<div className="text-sm mt-4 mb-1">Fee Token</div>
<StyledSelectBox
id="fee-token-select"
value={gasConfig.gasToken}
containerProps={{
className: twMerge(
"text-sm w-full flex-1 border border-white rounded-sm",
"px-4 py-[9px]"
),
}}
arrowContainerProps={{ className: "right-4" }}
listContainerProps={{ className: "w-full mt-2 border border-white" }}
listItemProps={{ className: "border-0 px-2 -mx-2 rounded-sm" }}
onChange={(e) => onChangeGasToken(e.target.value)}
options={
gasPriceTable?.sort(sortByNativeToken).map((item) => {
const { symbol, displayAmount, dollar } = buildGasOption({
gasPrice: item.gasPrice,
gasToken: item.token,
});
return {
id: item.token,
value: (
<div className="flex items-center justify-between w-full min-h-[42px] mr-5">
<div className="text-base">{symbol}</div>
<div className="text-right">
{dollar && <FiatCurrency amount={dollar} />}
<div className="text-xs">
<TokenCurrency amount={fee} symbol={symbol} />
<TokenCurrency amount={displayAmount} symbol={symbol} />
</div>
</div>
</button>
);
})
</div>
),
ariaLabel: symbol,
};
}) ?? []
}
/>

<div className="mt-4">
<AmountInput
label="Gas Amount"
value={gasConfig.gasLimit}
onChange={(e) => e.target.value && onChangeGasLimit(e.target.value)}
/>
</div>

<div className="mt-8">
<ActionButton
size="sm"
className="max-w-[200px] mx-auto"
backgroundColor="gray"
backgroundHoverColor="yellow"
textColor="white"
textHoverColor="black"
onClick={onClose}
>
Close
</ActionButton>
</div>
</div>
</Modal>
Expand Down
17 changes: 13 additions & 4 deletions apps/namadillo/src/App/Common/TransactionFee.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { chainAssetsMapAtom } from "atoms/chain";
import { useAtomValue } from "jotai";
import { GasConfig } from "types";
import { unknownAsset } from "utils/assets";
import { getDisplayGasFee } from "utils/gas";
Expand All @@ -8,14 +10,21 @@ export const TransactionFee = ({
}: {
gasConfig: GasConfig;
}): JSX.Element => {
const asset = gasConfig.asset ?? unknownAsset(gasConfig.gasToken);
const symbol = asset.symbol;
const fee = getDisplayGasFee(gasConfig);
const chainAssetsMap = useAtomValue(chainAssetsMapAtom);

const { gasToken } = gasConfig;
const asset = chainAssetsMap[gasToken] ?? unknownAsset(gasToken);

const amount = getDisplayGasFee(gasConfig);

return (
<div className="text-sm">
Transaction fee:{" "}
<TokenCurrency symbol={symbol} amount={fee} className="font-medium " />
<TokenCurrency
symbol={asset.symbol}
amount={amount}
className="font-medium"
/>
</div>
);
};
13 changes: 5 additions & 8 deletions apps/namadillo/src/App/Common/TransactionFeeButton.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { TransactionFeeProps } from "hooks/useTransactionFee";
import { useState } from "react";
import { GasConfig } from "types";
import { GasFeeModal } from "./GasFeeModal";
import { TransactionFee } from "./TransactionFee";

export const TransactionFeeButton = ({
gasConfig,
feeProps,
}: {
gasConfig: GasConfig;
feeProps: TransactionFeeProps;
}): JSX.Element => {
const [modalOpen, setModalOpen] = useState(false);

Expand All @@ -17,13 +17,10 @@ export const TransactionFeeButton = ({
className="underline hover:text-yellow transition-all cursor-pointer"
onClick={() => setModalOpen(true)}
>
<TransactionFee gasConfig={gasConfig} />
<TransactionFee gasConfig={feeProps.gasConfig} />
</button>
{modalOpen && (
<GasFeeModal
gasConfig={gasConfig}
onClose={() => setModalOpen(false)}
/>
<GasFeeModal feeProps={feeProps} onClose={() => setModalOpen(false)} />
)}
</>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/namadillo/src/App/Governance/AllProposalsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export const AllProposalsTable: React.FC<ExtensionConnectedProps> = (props) => {

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

type TableSelectProps<T extends string> = Omit<
React.ComponentProps<typeof StyledSelectBox<T>>,
Expand Down
Loading

0 comments on commit 8cacded

Please sign in to comment.