diff --git a/apps/faucet/webpack.config.js b/apps/faucet/webpack.config.js index af72cdeca..e9e6debc8 100644 --- a/apps/faucet/webpack.config.js +++ b/apps/faucet/webpack.config.js @@ -10,8 +10,6 @@ require("dotenv").config({ path: resolve(__dirname, ".env") }); const { NODE_ENV } = process.env; -const ASSET_PATH = "/"; - const createStyledComponentsTransformer = require("typescript-plugin-styled-components").default; @@ -62,7 +60,7 @@ module.exports = { faucet: "./src", }, output: { - publicPath: ASSET_PATH, + publicPath: "/", path: resolve(__dirname, `./build/`), filename: "[name].bundle.js", }, diff --git a/apps/namadillo/src/App/AccountOverview/AccountOverview.tsx b/apps/namadillo/src/App/AccountOverview/AccountOverview.tsx index 1214e7f84..168c42c3f 100644 --- a/apps/namadillo/src/App/AccountOverview/AccountOverview.tsx +++ b/apps/namadillo/src/App/AccountOverview/AccountOverview.tsx @@ -63,7 +63,9 @@ export const AccountOverview = (): JSX.Element => { {showSidebar && (maspEnabled ? - + : )} diff --git a/apps/namadillo/src/App/Assets/ShieldedParty.svg b/apps/namadillo/src/App/Assets/ShieldedParty.svg new file mode 100644 index 000000000..cec1470c6 --- /dev/null +++ b/apps/namadillo/src/App/Assets/ShieldedParty.svg @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/namadillo/src/App/Common/SelectModal.tsx b/apps/namadillo/src/App/Common/SelectModal.tsx new file mode 100644 index 000000000..6e92dcc7f --- /dev/null +++ b/apps/namadillo/src/App/Common/SelectModal.tsx @@ -0,0 +1,40 @@ +import { Modal } from "@namada/components"; +import clsx from "clsx"; +import React from "react"; +import { IoClose } from "react-icons/io5"; +import { ModalTransition } from "./ModalTransition"; + +type SelectModalProps = { + children: React.ReactNode; + title: React.ReactNode; + onClose: () => void; +}; + +export const SelectModal = ({ + children, + title, + onClose, +}: SelectModalProps): JSX.Element => { + return ( + + +
+
+ {title} + + + +
+ {children} +
+
+
+ ); +}; diff --git a/apps/namadillo/src/App/Common/TabSelector.tsx b/apps/namadillo/src/App/Common/TabSelector.tsx new file mode 100644 index 000000000..29e0eea6b --- /dev/null +++ b/apps/namadillo/src/App/Common/TabSelector.tsx @@ -0,0 +1,42 @@ +import clsx from "clsx"; +import { twMerge } from "tailwind-merge"; +type TabSelectorItem = { + text: React.ReactNode; + id: string; + className?: string; +}; + +type TabSelectorProps = { + items: TabSelectorItem[]; + active: string; + onChange: (item: TabSelectorItem) => void; +}; + +export const TabSelector = ({ + items, + active, + onChange, +}: TabSelectorProps): JSX.Element => { + return ( + + ); +}; diff --git a/apps/namadillo/src/App/Governance/SubmitVote.tsx b/apps/namadillo/src/App/Governance/SubmitVote.tsx index f373c5f67..09e3dd8b7 100644 --- a/apps/namadillo/src/App/Governance/SubmitVote.tsx +++ b/apps/namadillo/src/App/Governance/SubmitVote.tsx @@ -57,6 +57,7 @@ export const WithProposalId: React.FC<{ proposalId: bigint }> = ({ data: voteTxData, isError, error: voteTxError, + isPending: isPerformingTx, } = useAtomValue(createVoteTxAtom); const dispatchNotification = useSetAtom(dispatchToastNotificationAtom); @@ -191,10 +192,12 @@ export const WithProposalId: React.FC<{ proposalId: bigint }> = ({ - Confirm + {isPerformingTx ? "Processing..." : "Confirm"} )} diff --git a/apps/namadillo/src/App/Sidebars/ShieldAllBanner.tsx b/apps/namadillo/src/App/Sidebars/ShieldAllBanner.tsx index c26d0e96b..3d228cbd8 100644 --- a/apps/namadillo/src/App/Sidebars/ShieldAllBanner.tsx +++ b/apps/namadillo/src/App/Sidebars/ShieldAllBanner.tsx @@ -1,8 +1,13 @@ import { ActionButton } from "@namada/components"; +import svgImg from "App/Assets/ShieldedParty.svg"; import TransferRoutes from "App/Transfer/routes"; +import { useState } from "react"; +import { Link } from "react-router-dom"; import { twMerge } from "tailwind-merge"; export const ShieldAllBanner = (): JSX.Element => { + const [isAnimating, setIsAnimating] = useState(false); + return (
{ "p-3" )} > -
img
- + // https://developer.mozilla.org/en-US/docs/Web/CSS/:target + // https://gist.github.com/LeaVerou/5198257 + src={`${svgImg}${isAnimating ? "#hover " : ""}`} + /> + setIsAnimating(true)} + onMouseLeave={() => setIsAnimating(false)} > - Shield All Assets - + + Shield All Assets + +
); }; diff --git a/apps/namadillo/src/App/Transfer/AvailableAmountFooter.tsx b/apps/namadillo/src/App/Transfer/AvailableAmountFooter.tsx new file mode 100644 index 000000000..e4a50c47a --- /dev/null +++ b/apps/namadillo/src/App/Transfer/AvailableAmountFooter.tsx @@ -0,0 +1,54 @@ +import { ActionButton, Currency } from "@namada/components"; +import { KnownCurrencies } from "@namada/utils"; +import BigNumber from "bignumber.js"; +import clsx from "clsx"; + +type AvailableAmountFooterProps = { + availableAmount?: BigNumber; + currency?: keyof typeof KnownCurrencies; + onClickMax?: () => void; +}; + +export const AvailableAmountFooter = ({ + availableAmount, + currency, + onClickMax, +}: AvailableAmountFooterProps): JSX.Element => { + if (!currency || availableAmount === undefined) { + return <>; + } + + return ( +
+ + Available: + + + + {onClickMax && ( + + Max + + )} + +
+ ); +}; diff --git a/apps/namadillo/src/App/Transfer/ChainCard.tsx b/apps/namadillo/src/App/Transfer/ChainCard.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/apps/namadillo/src/App/Transfer/ConnectProviderButton.tsx b/apps/namadillo/src/App/Transfer/ConnectProviderButton.tsx new file mode 100644 index 000000000..b5935dd22 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/ConnectProviderButton.tsx @@ -0,0 +1,21 @@ +import { ActionButton } from "@namada/components"; + +type ConnectProviderButtonProps = { + onClick?: () => void; +}; + +export const ConnectProviderButton = ({ + onClick, +}: ConnectProviderButtonProps): JSX.Element => { + return ( + + Connect Wallet + + ); +}; diff --git a/apps/namadillo/src/App/Transfer/CustomAddressForm.tsx b/apps/namadillo/src/App/Transfer/CustomAddressForm.tsx new file mode 100644 index 000000000..981386511 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/CustomAddressForm.tsx @@ -0,0 +1,35 @@ +import { Input, Stack } from "@namada/components"; + +type CustomAddressFormProps = { + onChangeAddress?: (address: string | undefined) => void; + customAddress?: string; + memo?: string; + onChangeMemo?: (address: string) => void; +}; + +export const CustomAddressForm = ({ + customAddress, + onChangeAddress, + memo, + onChangeMemo, +}: CustomAddressFormProps): JSX.Element => { + return ( + + {onChangeAddress && ( + onChangeAddress(e.target.value)} + /> + )} + {onChangeMemo && ( + onChangeMemo(e.target.value)} + placeholder="Required for centralized exchanges" + /> + )} + + ); +}; diff --git a/apps/namadillo/src/App/Transfer/EmptyResourceIcon.tsx b/apps/namadillo/src/App/Transfer/EmptyResourceIcon.tsx new file mode 100644 index 000000000..555021e8c --- /dev/null +++ b/apps/namadillo/src/App/Transfer/EmptyResourceIcon.tsx @@ -0,0 +1,20 @@ +import clsx from "clsx"; +import { twMerge } from "tailwind-merge"; +type EmptyResourceProps = { className?: string }; + +export const EmptyResourceIcon = ({ + className = "", +}: EmptyResourceProps): JSX.Element => { + return ( + + ); +}; diff --git a/apps/namadillo/src/App/Transfer/IBCFromNamadaModule.tsx b/apps/namadillo/src/App/Transfer/IBCFromNamadaModule.tsx new file mode 100644 index 000000000..088f5a72c --- /dev/null +++ b/apps/namadillo/src/App/Transfer/IBCFromNamadaModule.tsx @@ -0,0 +1,9 @@ +import { TransferModule } from "./TransferModule"; + +export const IBCFromNamadaModule = (): JSX.Element => { + return ( +
+ {}} /> +
+ ); +}; diff --git a/apps/namadillo/src/App/Transfer/IBCTransfers.tsx b/apps/namadillo/src/App/Transfer/IBCTransfers.tsx new file mode 100644 index 000000000..3a551ec52 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/IBCTransfers.tsx @@ -0,0 +1,12 @@ +import { Panel } from "@namada/components"; +import { IBCFromNamadaModule } from "./IBCFromNamadaModule"; + +export const IBCTransfers = (): JSX.Element => { + return ( +
+ + + +
+ ); +}; diff --git a/apps/namadillo/src/App/Transfer/SelectAssetsModal.tsx b/apps/namadillo/src/App/Transfer/SelectAssetsModal.tsx new file mode 100644 index 000000000..8faf84a6c --- /dev/null +++ b/apps/namadillo/src/App/Transfer/SelectAssetsModal.tsx @@ -0,0 +1,15 @@ +import { SelectModal } from "App/Common/SelectModal"; + +type SelectItemsModalProps = { + onClose: () => void; +}; + +export const SelectAssetsModal = ({ + onClose, +}: SelectItemsModalProps): JSX.Element => { + return ( + + <> + + ); +}; diff --git a/apps/namadillo/src/App/Transfer/SelectChainModal.tsx b/apps/namadillo/src/App/Transfer/SelectChainModal.tsx new file mode 100644 index 000000000..61fed101b --- /dev/null +++ b/apps/namadillo/src/App/Transfer/SelectChainModal.tsx @@ -0,0 +1,34 @@ +import { Stack } from "@namada/components"; +import { SelectModal } from "App/Common/SelectModal"; +import clsx from "clsx"; +import { Chain } from "types"; + +type SelectChainModalProps = { + onClose: () => void; + chains: Chain[]; +}; + +export const SelectChainModal = ({ + onClose, + chains, +}: SelectChainModalProps): JSX.Element => { + return ( + + + {chains.map((chain) => ( +
  • + +
  • + ))} +
    +
    + ); +}; diff --git a/apps/namadillo/src/App/Transfer/SelectProviderModal.tsx b/apps/namadillo/src/App/Transfer/SelectProviderModal.tsx new file mode 100644 index 000000000..88fcb4eb7 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/SelectProviderModal.tsx @@ -0,0 +1,43 @@ +import { SelectModal } from "App/Common/SelectModal"; +import { Provider } from "types"; + +type SelectProviderModalProps = { + onClose: () => void; + providers: Provider[]; + onConnect: (provider: Provider) => void; +}; + +export const SelectProviderModal = ({ + onClose, + onConnect, + providers, +}: SelectProviderModalProps): JSX.Element => { + return ( + +
      + {providers.map((provider: Provider, index) => ( +
    • + +
    • + ))} +
    +
    + ); +}; diff --git a/apps/namadillo/src/App/Transfer/SelectedAsset.tsx b/apps/namadillo/src/App/Transfer/SelectedAsset.tsx new file mode 100644 index 000000000..4f86e205b --- /dev/null +++ b/apps/namadillo/src/App/Transfer/SelectedAsset.tsx @@ -0,0 +1,51 @@ +import clsx from "clsx"; +import { GoChevronDown } from "react-icons/go"; +import { Asset, Chain } from "types"; +import { EmptyResourceIcon } from "./EmptyResourceIcon"; + +type SelectedAssetProps = { + chain?: Chain; + asset?: Asset; + onClick?: () => void; +}; + +export const SelectedAsset = ({ + chain, + asset, + onClick, +}: SelectedAssetProps): JSX.Element => { + const selectorClassList = clsx( + `flex items-center gap-2.5 text-lg text-white font-light cursor-pointer uppercase` + ); + + return ( + + ); +}; diff --git a/apps/namadillo/src/App/Transfer/SelectedChain.tsx b/apps/namadillo/src/App/Transfer/SelectedChain.tsx new file mode 100644 index 000000000..c2f923d0e --- /dev/null +++ b/apps/namadillo/src/App/Transfer/SelectedChain.tsx @@ -0,0 +1,53 @@ +import clsx from "clsx"; +import { GoChevronDown } from "react-icons/go"; +import { Chain, Provider } from "types"; +import { EmptyResourceIcon } from "./EmptyResourceIcon"; + +type SelectedChainProps = { + chain?: Chain; + provider?: Provider; + onClick?: () => void; +}; + +export const SelectedChain = ({ + chain, + provider, + onClick, +}: SelectedChainProps): JSX.Element => { + const selectorClassList = clsx( + `flex items-center gap-2.5 text-white font-light cursor-pointer` + ); + + return ( + + ); +}; diff --git a/apps/namadillo/src/App/Transfer/Transfer.tsx b/apps/namadillo/src/App/Transfer/Transfer.tsx index c1fcd2a81..26bc34d25 100644 --- a/apps/namadillo/src/App/Transfer/Transfer.tsx +++ b/apps/namadillo/src/App/Transfer/Transfer.tsx @@ -1,4 +1,5 @@ import { Route, Routes } from "react-router-dom"; +import { IBCTransfers } from "./IBCTransfers"; import { NamTransfer } from "./NamTransfer"; import { Shield } from "./Shield"; import { ShieldAll } from "./ShieldAll"; @@ -16,6 +17,10 @@ export const Transfer: React.FC = () => ( path={TransferRoutes.shieldAll().toString()} element={} /> + } + /> ); diff --git a/apps/namadillo/src/App/Transfer/TransferDestination.tsx b/apps/namadillo/src/App/Transfer/TransferDestination.tsx new file mode 100644 index 000000000..a9f454622 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/TransferDestination.tsx @@ -0,0 +1,103 @@ +import { NamCurrency } from "App/Common/NamCurrency"; +import { TabSelector } from "App/Common/TabSelector"; +import BigNumber from "bignumber.js"; +import clsx from "clsx"; +import { Chain, Provider } from "types"; +import namadaShieldedSvg from "./assets/namada-shielded.svg"; +import namadaTransparentSvg from "./assets/namada-transparent.svg"; +import { CustomAddressForm } from "./CustomAddressForm"; +import { SelectedChain } from "./SelectedChain"; + +type TransferDestinationProps = { + isShielded?: boolean; + onChangeShielded?: (isShielded: boolean) => void; + chain?: Chain; + provider?: Provider; + className?: string; + transactionFee?: BigNumber; + customAddressActive?: boolean; + onToggleCustomAddress?: (isActive: boolean) => void; + onChangeAddress?: (address: string | undefined) => void; + address?: string; + memo?: string; + onChangeMemo?: (address: string) => void; +}; + +const parseChainInfo = ( + chain?: Chain, + isShielded?: boolean +): Chain | undefined => { + if (chain?.name !== "Namada") { + return chain; + } + return { + ...chain, + name: isShielded ? "Namada Shielded" : "Namada Transparent", + iconUrl: isShielded ? namadaShieldedSvg : namadaTransparentSvg, + }; +}; + +export const TransferDestination = ({ + chain, + provider, + isShielded, + onChangeShielded, + transactionFee, + customAddressActive, + onToggleCustomAddress, + address, + onChangeAddress, + memo, + onChangeMemo, +}: TransferDestinationProps): JSX.Element => { + return ( +
    + {onChangeShielded && chain?.name === "Namada" && ( + onChangeShielded(!isShielded)} + /> + )} + + {onToggleCustomAddress && ( + onToggleCustomAddress(!customAddressActive)} + items={[ + { id: "my-address", text: "My Address", className: "text-white" }, + { id: "custom", text: "Custom Address", className: "text-white" }, + ]} + /> + )} + + + + {customAddressActive && ( + + )} + + {transactionFee && ( +
    + Transaction Fee + +
    + )} +
    + ); +}; diff --git a/apps/namadillo/src/App/Transfer/TransferModule.tsx b/apps/namadillo/src/App/Transfer/TransferModule.tsx new file mode 100644 index 000000000..04d198e97 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/TransferModule.tsx @@ -0,0 +1,85 @@ +import { Stack } from "@namada/components"; +import BigNumber from "bignumber.js"; +import { useState } from "react"; +import { Asset, Chain } from "types"; +import { SelectProviderModal } from "./SelectProviderModal"; +import { TransferDestination } from "./TransferDestination"; +import { TransferSource } from "./TransferSource"; + +type TransferModuleProps = { + isConnected: boolean; + sourceChain?: Chain; + onChangeSourceChain?: () => void; + destinationChain?: Chain; + onChangeDestinationChain?: (chain: Chain) => void; + selectedAsset?: Asset; + onChangeSelectedAsset?: (asset: Asset | undefined) => void; + amount?: BigNumber; + onChangeAmount?: (amount: BigNumber) => void; + isShielded?: boolean; + onChangeShielded?: (isShielded: boolean) => void; + enableCustomAddress?: boolean; + onSubmitTransfer: () => void; +}; + +export const TransferModule = ({ + isConnected, + selectedAsset, + sourceChain, + destinationChain, + isShielded, + onChangeShielded, + enableCustomAddress, +}: TransferModuleProps): JSX.Element => { + const [providerSelectorModalOpen, setProviderSelectorModalOpen] = + useState(false); + const [chainSelectorModalOpen, setChainSelectorModalOpen] = useState(false); + const [assetSelectorModalOpen, setAssetSelectorModalOpen] = useState(false); + const [customAddressActive, setCustomAddressActive] = useState(false); + const [memo, setMemo] = useState(""); + const [customAddress, setCustomAddress] = useState(""); + const [amount, setAmount] = useState(new BigNumber(0)); + + return ( + <> +
    + + setProviderSelectorModalOpen(true)} + openChainSelector={() => setChainSelectorModalOpen(true)} + openAssetSelector={() => setAssetSelectorModalOpen(true)} + amount={amount} + onChangeAmount={(e) => + setAmount(e.target.value || new BigNumber(0)) + } + /> + + {chainSelectorModalOpen &&
    } + {assetSelectorModalOpen &&
    } + +
    + {providerSelectorModalOpen && ( + setProviderSelectorModalOpen(false)} + onConnect={() => {}} + /> + )} + + ); +}; diff --git a/apps/namadillo/src/App/Transfer/TransferSource.tsx b/apps/namadillo/src/App/Transfer/TransferSource.tsx new file mode 100644 index 000000000..3e52ca813 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/TransferSource.tsx @@ -0,0 +1,64 @@ +import { AmountInput, ChangeAmountEvent } from "@namada/components"; +import BigNumber from "bignumber.js"; +import clsx from "clsx"; +import { Asset, Chain, Provider } from "types"; +import { AvailableAmountFooter } from "./AvailableAmountFooter"; +import { ConnectProviderButton } from "./ConnectProviderButton"; +import { SelectedAsset } from "./SelectedAsset"; +import { SelectedChain } from "./SelectedChain"; + +export type TransferSourceProps = { + isConnected: boolean; + provider?: Provider; + asset?: Asset; + chain?: Chain; + openChainSelector?: () => void; + openAssetSelector?: () => void; + openProviderSelector?: () => void; + amount?: BigNumber; + onChangeAmount?: ChangeAmountEvent; +}; + +export const TransferSource = ({ + chain, + asset, + provider, + openProviderSelector, + openChainSelector, + openAssetSelector, + amount, + onChangeAmount, +}: TransferSourceProps): JSX.Element => { + return ( +
    +
    + + {!provider && } +
    +
    +
    + + +
    +
    + +
    +
    + ); +}; diff --git a/apps/namadillo/src/App/Transfer/__mocks__/chains.ts b/apps/namadillo/src/App/Transfer/__mocks__/chains.ts new file mode 100644 index 000000000..da2ad5502 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/__mocks__/chains.ts @@ -0,0 +1,13 @@ +import { Chain } from "types"; + +export const namadaChainMock: Chain = { + chainId: "test", + name: "Namada", + iconUrl: "namada-icon", +}; + +export const randomChainMock: Chain = { + chainId: "test", + name: "TestChain", + iconUrl: "testchain-icon", +}; diff --git a/apps/namadillo/src/App/Transfer/__mocks__/providers.ts b/apps/namadillo/src/App/Transfer/__mocks__/providers.ts new file mode 100644 index 000000000..af2cd0157 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/__mocks__/providers.ts @@ -0,0 +1,12 @@ +import { Provider } from "types"; + +export const providerMock: Provider = { + name: "Keplr", + iconUrl: "test.svg", + connected: false, +}; + +export const providerConnectedMock: Provider = { + ...providerMock, + connected: true, +}; diff --git a/apps/namadillo/src/App/Transfer/__tests__/AvailableAmountFooter.test.tsx b/apps/namadillo/src/App/Transfer/__tests__/AvailableAmountFooter.test.tsx new file mode 100644 index 000000000..088ff37b3 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/__tests__/AvailableAmountFooter.test.tsx @@ -0,0 +1,52 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import BigNumber from "bignumber.js"; +import { AvailableAmountFooter } from "../AvailableAmountFooter"; + +describe("Component: AvailableAmountFooter", () => { + it("should render an empty tag when no available amount is provided", () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it("should render with correct information and behaviour enabled", () => { + const callback = jest.fn(); + render( + + ); + const amount = screen.getByText("1,234"); + const button = screen.getByRole("button"); + expect(amount.parentNode?.textContent).toContain("1,234.456 NAM"); + expect(button).toBeEnabled(); + fireEvent.click(button); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it("should not display MAX button when no callback was provided", () => { + render( + + ); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); + + it("should display disabled button when the amount is zero", () => { + const callback = jest.fn(); + render( + + ); + const button = screen.getByRole("button"); + expect(button).toBeDisabled(); + fireEvent.click(button); + expect(callback).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/namadillo/src/App/Transfer/__tests__/SelectedAsset.test.tsx b/apps/namadillo/src/App/Transfer/__tests__/SelectedAsset.test.tsx new file mode 100644 index 000000000..5ebe93cc1 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/__tests__/SelectedAsset.test.tsx @@ -0,0 +1,71 @@ +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { SelectedAsset } from "App/Transfer/SelectedAsset"; // Adjust the path accordingly +import { Asset, Chain } from "types"; // Adjust the path accordingly + +describe("SelectedAsset", () => { + const mockChain: Chain = { + chainId: "1", + name: "Ethereum", + iconUrl: "https://example.com/ethereum-icon.png", + }; + + const mockAsset: Partial = { + name: "Ethereum", + denomination: "ETH", + iconUrl: "https://example.com/eth-icon.png", + }; + + it("renders with no chain and disables the button", () => { + render(); + const button = screen.getByRole("button"); + expect(button).toBeDisabled(); + }); + + it("renders with no asset selected", () => { + const mockFn = jest.fn(); + render(); + + const button = screen.getByRole("button"); + expect(button).toBeEnabled(); + + const assetLabel = screen.getByText(/asset/i); + expect(assetLabel).toBeInTheDocument(); + + fireEvent.click(button); + expect(mockFn).toHaveBeenCalledTimes(1); + }); + + it("renders with asset selected", () => { + const handleClick = jest.fn(); + render( + + ); + + const button = screen.getByRole("button"); + expect(button).toBeEnabled(); + + const assetDenomination = screen.getByText(mockAsset.denomination!); + expect(assetDenomination).toBeInTheDocument(); + + const assetImage = screen.getByAltText(`${mockAsset.name} image`); + expect(assetImage).toHaveStyle( + `background-image: url(${mockAsset.iconUrl})` + ); + + fireEvent.click(button); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it("does not call onClick when the button is disabled", () => { + const handleClick = jest.fn(); + render(); + const button = screen.getByRole("button"); + fireEvent.click(button); + expect(handleClick).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/namadillo/src/App/Transfer/__tests__/SelectedChain.test.tsx b/apps/namadillo/src/App/Transfer/__tests__/SelectedChain.test.tsx new file mode 100644 index 000000000..9008be967 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/__tests__/SelectedChain.test.tsx @@ -0,0 +1,67 @@ +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { SelectedChain } from "App/Transfer/SelectedChain"; +import { Chain } from "types"; +import { providerConnectedMock } from "../__mocks__/providers"; + +describe("Component: SelectedChain", () => { + const mockChain: Chain = { + chainId: "chain-id", + name: "Ethereum", + iconUrl: "https://example.com/ethereum-icon.png", + }; + + it("renders disabled with no provider selected", () => { + render(); + const button = screen.getByRole("button"); + expect(button).toBeDisabled(); + }); + + it("renders empty when chain is passed, but provider is disconnected", () => { + render(); + const button = screen.getByRole("button"); + expect(button).toBeDisabled(); + expect(button.getAttribute("aria-description")).toMatch(/no chain/i); + }); + + it("renders correctly with no chain selected", () => { + render(); + const button = screen.getByRole("button"); + expect(button).toBeInTheDocument(); + expect(button).toBeEnabled(); + expect(button.getAttribute("aria-description")).toMatch(/no chain/i); + }); + + it("renders correctly with chain selected", () => { + render( + + ); + + const button = screen.getByRole("button"); + expect(button).toBeInTheDocument(); + expect(button.getAttribute("aria-description")).toContain(mockChain.name); + + const chainName = screen.getByText(mockChain.name); + expect(chainName).toBeInTheDocument(); + + const chainImage = screen.getByAltText(`${mockChain.name}`, { + exact: false, + }); + expect(chainImage).toBeInTheDocument(); + expect(chainImage).toHaveAttribute( + "style", + `background-image: url(${mockChain.iconUrl});` + ); + }); + + it("calls onClick when the component is clicked", () => { + const handleClick = jest.fn(); + render( + + ); + + const button = screen.getByRole("button"); + fireEvent.click(button); + expect(handleClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/namadillo/src/App/Transfer/__tests__/TransferDestination.test.tsx b/apps/namadillo/src/App/Transfer/__tests__/TransferDestination.test.tsx new file mode 100644 index 000000000..fee5a7878 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/__tests__/TransferDestination.test.tsx @@ -0,0 +1,100 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { + namadaChainMock, + randomChainMock, +} from "App/Transfer/__mocks__/chains"; +import { TransferDestination } from "App/Transfer/TransferDestination"; +import BigNumber from "bignumber.js"; +import { providerMock } from "../__mocks__/providers"; + +describe("TransferDestination", () => { + it("should render the component with the default props", () => { + render(); + expect(screen.getByText(/select chain/i)).toBeInTheDocument(); + }); + + it("should render the TabSelector for shielded/transparent when onChangeShielded is provided", () => { + render( + + ); + expect(screen.getByText("Shielded")).toBeInTheDocument(); + expect(screen.getByText("Transparent")).toBeInTheDocument(); + }); + + it("should render a yellow border when transfer is shielded", () => { + const { container } = render(); + expect(container.firstElementChild?.className).toContain("border-yellow"); + }); + + it("should render nothing related to shielding when provided chain is not Namada", () => { + render( + + ); + expect(screen.queryByText(/shielded/i)).not.toBeInTheDocument(); + }); + + it("should render correct chain name when shielded transfer is set", () => { + render( + + ); + expect(screen.getByText(/namada shielded/i)).toBeInTheDocument(); + }); + + it("should render correct chain name when transparent transfer is set", () => { + render( + + ); + expect(screen.getByText(/namada transparent/i)).toBeInTheDocument(); + }); + + it("should toggle between shielded and transparent", () => { + const onChangeShieldedMock = jest.fn(); + render( + + ); + const transparentButton = screen.getByText("Transparent"); + fireEvent.click(transparentButton); + expect(onChangeShieldedMock).toHaveBeenCalledWith(false); + }); + + it("should toggle between custom and my address when onToggleCustomAddress is provided", () => { + const onToggleCustomAddressMock = jest.fn(); + render( + + ); + const customAddressButton = screen.getByText("Custom Address"); + fireEvent.click(customAddressButton); + expect(onToggleCustomAddressMock).toHaveBeenCalledWith(true); + }); + + it("should display the transaction fee if provided", () => { + const fee = new BigNumber(0.01); + render(); + const transactionFee = screen.getByText("Transaction Fee"); + expect(transactionFee).toBeInTheDocument(); + expect(transactionFee.parentNode?.textContent).toContain("0.01"); + }); +}); diff --git a/apps/namadillo/src/App/Transfer/__tests__/TransferSource.test.tsx b/apps/namadillo/src/App/Transfer/__tests__/TransferSource.test.tsx new file mode 100644 index 000000000..b21c3c2c0 --- /dev/null +++ b/apps/namadillo/src/App/Transfer/__tests__/TransferSource.test.tsx @@ -0,0 +1,87 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { + TransferSource, + TransferSourceProps, +} from "App/Transfer/TransferSource"; +import BigNumber from "bignumber.js"; +import { namadaChainMock } from "../__mocks__/chains"; +import { providerConnectedMock } from "../__mocks__/providers"; + +describe("Component: TransferSource", () => { + it("should render the component with the default props", () => { + render( + + ); + expect(screen.getByText("Connect Wallet")).toBeInTheDocument(); + expect(screen.getByText(/select chain/i)).toBeInTheDocument(); + }); + + const setup = (props: Partial = {}): void => { + render( + + ); + }; + + const getEmptyChain = (): HTMLElement => { + return screen.getByText(/select chain/i); + }; + + const getEmptyAsset = (): HTMLElement => { + return screen.getByText(/asset/i); + }; + + it("should call onConnectProvider when Connect Wallet button is clicked", () => { + const onConnectProviderMock = jest.fn(); + setup({ openProviderSelector: onConnectProviderMock }); + fireEvent.click(screen.getByText("Connect Wallet")); + expect(onConnectProviderMock).toHaveBeenCalled(); + }); + + it("should call openChainSelector when the SelectedChain is clicked", () => { + const openChainSelectorMock = jest.fn(); + setup({ + openChainSelector: openChainSelectorMock, + provider: providerConnectedMock, + }); + const chain = getEmptyChain(); + fireEvent.click(chain); + expect(openChainSelectorMock).toHaveBeenCalled(); + }); + + it("should render controls disabled when chain is not defined", () => { + const openAssetSelectorMock = jest.fn(); + setup({ openAssetSelector: openAssetSelectorMock }); + const assetControl = getEmptyAsset(); + fireEvent.click(assetControl); + expect(openAssetSelectorMock).not.toHaveBeenCalled(); + const amountInput = screen.getByDisplayValue("0"); + expect(amountInput).toBeDisabled(); + }); + + it("should call openAssetSelector when the SelectedAsset is clicked", () => { + const openAssetSelectorMock = jest.fn(); + setup({ openAssetSelector: openAssetSelectorMock, chain: namadaChainMock }); + const assetControl = getEmptyAsset(); + fireEvent.click(assetControl); + expect(openAssetSelectorMock).toHaveBeenCalled(); + }); + + it("should render the amount input with the correct value", () => { + const amount = new BigNumber(100); + setup({ amount }); + const amountInput = screen.getByDisplayValue("100"); + expect(amountInput).toBeInTheDocument(); + }); + + it("should call onChangeAmount when the amount input is changed", () => { + const onChangeAmountMock = jest.fn(); + setup({ amount: new BigNumber(0), onChangeAmount: onChangeAmountMock }); + const amountInput = screen.getByDisplayValue("0"); + fireEvent.change(amountInput, { target: { value: new BigNumber("200") } }); + expect(onChangeAmountMock).toHaveBeenCalled(); + }); +}); diff --git a/apps/namadillo/src/App/Transfer/assets/namada-shielded.svg b/apps/namadillo/src/App/Transfer/assets/namada-shielded.svg new file mode 100644 index 000000000..8d1e160ab --- /dev/null +++ b/apps/namadillo/src/App/Transfer/assets/namada-shielded.svg @@ -0,0 +1 @@ + diff --git a/apps/namadillo/src/App/Transfer/assets/namada-transparent.svg b/apps/namadillo/src/App/Transfer/assets/namada-transparent.svg new file mode 100644 index 000000000..5218e65ad --- /dev/null +++ b/apps/namadillo/src/App/Transfer/assets/namada-transparent.svg @@ -0,0 +1 @@ + diff --git a/apps/namadillo/src/App/Transfer/routes.ts b/apps/namadillo/src/App/Transfer/routes.ts index b0fd05652..524459c38 100644 --- a/apps/namadillo/src/App/Transfer/routes.ts +++ b/apps/namadillo/src/App/Transfer/routes.ts @@ -10,7 +10,9 @@ export const namTransfer = (): RouteOutput => routeOutput("/nam"); export const shield = (): RouteOutput => routeOutput("/shield"); -export const shieldAll = (): RouteOutput => routeOutput("/shield-all"); +export const shieldAll = (): RouteOutput => routeOutput(`/shield-all`); + +export const ibcTransfer = (): RouteOutput => routeOutput(`/ibc`); export default { index, @@ -18,4 +20,5 @@ export default { namTransfer, shield, shieldAll, + ibcTransfer, }; diff --git a/apps/namadillo/src/atoms/accounts/atoms.ts b/apps/namadillo/src/atoms/accounts/atoms.ts index 40957ef08..e483d972b 100644 --- a/apps/namadillo/src/atoms/accounts/atoms.ts +++ b/apps/namadillo/src/atoms/accounts/atoms.ts @@ -7,6 +7,7 @@ import { namadaExtensionConnectedAtom } from "atoms/settings"; import { queryDependentFn } from "atoms/utils"; import BigNumber from "bignumber.js"; import { atomWithMutation, atomWithQuery } from "jotai-tanstack-query"; +import { chainConfigByName } from "registry"; import { fetchAccountBalance, fetchAccounts, @@ -50,6 +51,7 @@ export const accountBalanceAtom = atomWithQuery((get) => { const tokenAddress = get(nativeTokenAddressAtom); const enablePolling = get(shouldUpdateBalanceAtom); const api = get(indexerApiAtom); + const chainConfig = chainConfigByName("namada"); return { // TODO: subscribe to indexer events when it's done @@ -59,7 +61,10 @@ export const accountBalanceAtom = atomWithQuery((get) => { return await fetchAccountBalance( api, defaultAccount.data, - tokenAddress.data! + tokenAddress.data!, + // As this is a nam balance specific atom, we can safely assume that the + // first currency is the native token + chainConfig.currencies[0].coinDecimals ); }, [tokenAddress, defaultAccount]), }; diff --git a/apps/namadillo/src/atoms/accounts/services.ts b/apps/namadillo/src/atoms/accounts/services.ts index 453fb0ba9..44e546441 100644 --- a/apps/namadillo/src/atoms/accounts/services.ts +++ b/apps/namadillo/src/atoms/accounts/services.ts @@ -17,12 +17,13 @@ export const fetchDefaultAccount = async (): Promise => { export const fetchAccountBalance = async ( api: DefaultApi, account: Account | undefined, - tokenAddress: string + tokenAddress: string, + decimals: number ): Promise => { if (!account) return BigNumber(0); const balancesResponse = await api.apiV1AccountAddressGet(account.address); - const balances = balancesResponse.data + const balance = balancesResponse.data // TODO: add filter to the api call .filter(({ tokenAddress: ta }) => ta === tokenAddress) .map(({ tokenAddress, balance }) => { @@ -30,8 +31,10 @@ export const fetchAccountBalance = async ( token: tokenAddress, amount: balance, }; - }); + }) + .at(0); - if (balances.length === 0) return BigNumber(0); - return new BigNumber(balances[0].amount || 0); + return balance ? + BigNumber(balance.amount).shiftedBy(-decimals) + : BigNumber(0); }; diff --git a/apps/namadillo/src/atoms/staking/atoms.ts b/apps/namadillo/src/atoms/staking/atoms.ts index fef4c722e..8e9e5796b 100644 --- a/apps/namadillo/src/atoms/staking/atoms.ts +++ b/apps/namadillo/src/atoms/staking/atoms.ts @@ -96,21 +96,6 @@ export const createWithdrawTxAtomFamily = atomFamily((id: string) => { }); }); -export const claimRewardsAtom = atomWithMutation((get) => { - const chain = get(chainAtom); - return { - mutationKey: ["create-claim-tx"], - enabled: chain.isSuccess, - mutationFn: async ({ - params, - gasConfig, - account, - }: BuildTxAtomParams) => { - return createClaimTx(chain.data!, account, params, gasConfig); - }, - }; -}); - export const claimableRewardsAtom = atomWithQuery((get) => { const account = get(defaultAccountAtom); const chainParameters = get(chainParametersAtom); @@ -134,6 +119,21 @@ export const claimableRewardsAtom = atomWithQuery((get) => { }; }); +export const claimRewardsAtom = atomWithMutation((get) => { + const chain = get(chainAtom); + return { + mutationKey: ["create-claim-tx"], + enabled: chain.isSuccess, + mutationFn: async ({ + params, + gasConfig, + account, + }: BuildTxAtomParams) => { + return createClaimTx(chain.data!, account, params, gasConfig); + }, + }; +}); + export const claimAndStakeRewardsAtom = atomWithMutation((get) => { const chain = get(chainAtom); const claimableRewards = get(claimableRewardsAtom); diff --git a/apps/namadillo/src/atoms/staking/services.ts b/apps/namadillo/src/atoms/staking/services.ts index 7016e3f69..d8523b6cd 100644 --- a/apps/namadillo/src/atoms/staking/services.ts +++ b/apps/namadillo/src/atoms/staking/services.ts @@ -12,6 +12,7 @@ import { WithdrawProps, WrapperTxProps, } from "@namada/types"; +import { queryClient } from "App/Common/QueryProvider"; import { getSdkInstance } from "hooks"; import { TransactionPair, buildTxPair } from "lib/query"; import { Address, AddressBalance, ChainSettings, GasConfig } from "types"; @@ -159,3 +160,11 @@ export const createClaimAndStakeTx = async ( account.address ); }; + +export const clearClaimRewards = (accountAddress: string): void => { + const emptyClaimRewards = {}; + queryClient.setQueryData( + ["claim-rewards", accountAddress], + () => emptyClaimRewards + ); +}; diff --git a/apps/namadillo/src/hooks/useTransactionCallbacks.tsx b/apps/namadillo/src/hooks/useTransactionCallbacks.tsx index 7d736b189..be00e6a82 100644 --- a/apps/namadillo/src/hooks/useTransactionCallbacks.tsx +++ b/apps/namadillo/src/hooks/useTransactionCallbacks.tsx @@ -1,29 +1,36 @@ -import { accountBalanceAtom } from "atoms/accounts"; +import { accountBalanceAtom, defaultAccountAtom } from "atoms/accounts"; import { shouldUpdateBalanceAtom, shouldUpdateProposalAtom } from "atoms/etc"; -import { claimableRewardsAtom } from "atoms/staking"; +import { claimableRewardsAtom, clearClaimRewards } from "atoms/staking"; import { useAtomValue, useSetAtom } from "jotai"; import { useTransactionEventListener } from "utils"; export const useTransactionCallback = (): void => { const { refetch: refetchBalances } = useAtomValue(accountBalanceAtom); const { refetch: refetchRewards } = useAtomValue(claimableRewardsAtom); + const { data: account } = useAtomValue(defaultAccountAtom); const shouldUpdateBalance = useSetAtom(shouldUpdateBalanceAtom); const onBalanceUpdate = (): void => { // TODO: refactor this after event subscription is enabled on indexer shouldUpdateBalance(true); refetchBalances(); - refetchRewards(); const timePolling = 6 * 1000; setTimeout(() => shouldUpdateBalance(false), timePolling); + + if (account?.address) { + clearClaimRewards(account.address); + setTimeout(() => refetchRewards(), timePolling); + } }; useTransactionEventListener("Bond.Success", onBalanceUpdate); useTransactionEventListener("Unbond.Success", onBalanceUpdate); useTransactionEventListener("Withdraw.Success", onBalanceUpdate); useTransactionEventListener("Redelegate.Success", onBalanceUpdate); - useTransactionEventListener("ClaimRewards.Success", onBalanceUpdate); + useTransactionEventListener("ClaimRewards.Success", onBalanceUpdate, [ + account?.address, + ]); const shouldUpdateProposal = useSetAtom(shouldUpdateProposalAtom); diff --git a/apps/namadillo/src/registry/cosmoshub.json b/apps/namadillo/src/registry/cosmoshub.json new file mode 100644 index 000000000..f3e605021 --- /dev/null +++ b/apps/namadillo/src/registry/cosmoshub.json @@ -0,0 +1,22 @@ +{ + "chainName": "Cosmos Hub", + "currencies": [ + { + "coinDecimals": 6, + "coinDenom": "ATOM", + "coinMinimalDenom": "uatom" + } + ], + "feeCurrencies": [ + { + "coinDecimals": 6, + "coinDenom": "ATOM", + "coinMinimalDenom": "uatom" + } + ], + "stakeCurrency": { + "coinDecimals": 6, + "coinDenom": "ATOM", + "coinMinimalDenom": "uatom" + } +} diff --git a/apps/namadillo/src/registry/index.ts b/apps/namadillo/src/registry/index.ts new file mode 100644 index 000000000..86b83d5cf --- /dev/null +++ b/apps/namadillo/src/registry/index.ts @@ -0,0 +1,54 @@ +import cosmoshub from "./cosmoshub.json"; +import namada from "./namada.json"; + +type MinimalDenom = string; +type ConfigName = string; +type ChainName = "cosmoshub" | "namada"; +type ChainMinDenom = "uatom" | "namnam"; + +type Currency = { + coinDecimals: number; + coinDenom: string; + coinMinimalDenom: string; +}; + +export type ChainConfig = { + currencies: Currency[]; + feeCurrencies: Currency[]; + stakeCurrency: Currency; +}; + +const loadedConfigs: ChainConfig[] = []; +const minimalDenomMap: Map = new Map(); +const nameMap: Map = new Map(); + +export function chainConfigByMinDenom(minDenom: ChainMinDenom): ChainConfig { + const index = minimalDenomMap.get(minDenom); + if (typeof index === "undefined") { + throw new Error("Chain config not found"); + } + + return loadedConfigs[index]; +} + +export function chainConfigByName(name: ChainName): ChainConfig { + const index = nameMap.get(name); + if (typeof index === "undefined") { + throw new Error("Chain config not found"); + } + return loadedConfigs[index]; +} + +function loadConfigs(configs: [MinimalDenom, ConfigName, ChainConfig][]): void { + configs.forEach(([minimalDenom, name, config], index) => { + loadedConfigs.push(config); + + minimalDenomMap.set(minimalDenom, index); + nameMap.set(name, index); + }); +} + +loadConfigs([ + ["namnam", "namada", namada], + ["uatom", "cosmoshub", cosmoshub], +]); diff --git a/apps/namadillo/src/registry/namada.json b/apps/namadillo/src/registry/namada.json new file mode 100644 index 000000000..7ec080145 --- /dev/null +++ b/apps/namadillo/src/registry/namada.json @@ -0,0 +1,22 @@ +{ + "chainName": "Namada", + "currencies": [ + { + "coinDecimals": 6, + "coinDenom": "NAM", + "coinMinimalDenom": "namnam" + } + ], + "feeCurrencies": [ + { + "coinDecimals": 6, + "coinDenom": "NAM", + "coinMinimalDenom": "namnam" + } + ], + "stakeCurrency": { + "coinDecimals": 6, + "coinDenom": "NAM", + "coinMinimalDenom": "namnam" + } +} diff --git a/apps/namadillo/src/types.d.ts b/apps/namadillo/src/types.d.ts index bcc61d059..8f18d4a37 100644 --- a/apps/namadillo/src/types.d.ts +++ b/apps/namadillo/src/types.d.ts @@ -166,3 +166,24 @@ export type ToastNotification = { export type ToastNotificationEntryFilter = ( notification: ToastNotification ) => boolean; + +export type Provider = { + name: string; + iconUrl: string; + connected: boolean; +}; + +export type Chain = { + chainId: string; + name: string; + iconUrl: string; +}; + +export type Asset = { + chain: Chain; + name: string; + iconUrl: string; + denomination: string; + minimalDenomination: string; + decimals: number; +}; diff --git a/apps/namadillo/src/utils/index.ts b/apps/namadillo/src/utils/index.ts index b6d16d159..bc701aa18 100644 --- a/apps/namadillo/src/utils/index.ts +++ b/apps/namadillo/src/utils/index.ts @@ -36,14 +36,15 @@ export const proposalIdToString = (proposalId: bigint): string => export const useTransactionEventListener = ( event: T, - handler: (this: Window, ev: WindowEventMap[T]) => void + handler: (this: Window, ev: WindowEventMap[T]) => void, + deps: React.DependencyList = [] ): void => { useEffect(() => { window.addEventListener(event, handler); return () => { window.removeEventListener(event, handler); }; - }, []); + }, deps); }; const secondsToDateTime = (seconds: bigint): DateTime => diff --git a/apps/namadillo/vite.config.mjs b/apps/namadillo/vite.config.mjs index 618c482cf..a6d1ad14a 100644 --- a/apps/namadillo/vite.config.mjs +++ b/apps/namadillo/vite.config.mjs @@ -1,22 +1,11 @@ /* eslint-disable */ import react from "@vitejs/plugin-react"; -import { defineConfig, loadEnv } from "vite"; +import { defineConfig } from "vite"; import checker from "vite-plugin-checker"; import { nodePolyfills } from "vite-plugin-node-polyfills"; import tsconfigPaths from "vite-tsconfig-paths"; -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ""); - const filteredEnv = Object.keys(env).reduce((acc, current) => { - if (current.startsWith("NAMADA_INTERFACE_")) { - return { - ...acc, - [current]: env[current], - }; - } - return acc; - }, {}); - +export default defineConfig(() => { return { plugins: [ react(), @@ -30,9 +19,6 @@ export default defineConfig(({ mode }) => { overlay: { initialIsOpen: false }, }), ], - define: { - "process.env": filteredEnv, - }, optimizeDeps: { esbuildOptions: { // Node.js global to browser globalThis diff --git a/docker/faucet/nginx.conf b/docker/faucet/nginx.conf index 51acaff2f..a4476297d 100644 --- a/docker/faucet/nginx.conf +++ b/docker/faucet/nginx.conf @@ -6,6 +6,5 @@ server { index index.html index.htm; try_files $uri $uri/ $uri.html /index.html; } - gzip on; - gzip_types text/plain text/css application/javascript application/json application/vnd.ms-fontobject application/xml+rss application/atom+xml font/opentype font/ttf image/svg+xml; + gzip off; } diff --git a/docker/namadillo/nginx.conf b/docker/namadillo/nginx.conf index 51acaff2f..a4476297d 100644 --- a/docker/namadillo/nginx.conf +++ b/docker/namadillo/nginx.conf @@ -6,6 +6,5 @@ server { index index.html index.htm; try_files $uri $uri/ $uri.html /index.html; } - gzip on; - gzip_types text/plain text/css application/javascript application/json application/vnd.ms-fontobject application/xml+rss application/atom+xml font/opentype font/ttf image/svg+xml; + gzip off; } diff --git a/packages/components/src/AmountInput.tsx b/packages/components/src/AmountInput.tsx index 0f3b93d95..7d72d80ff 100644 --- a/packages/components/src/AmountInput.tsx +++ b/packages/components/src/AmountInput.tsx @@ -9,9 +9,11 @@ export type BigNumberElement = Omit & { value?: BigNumber; }; +export type ChangeAmountEvent = ChangeEventHandler; + type Props = Omit & { value?: BigNumber; - onChange?: ChangeEventHandler; + onChange?: ChangeAmountEvent; maxDecimalPlaces?: number; min?: string | number | BigNumber; max?: string | number | BigNumber; diff --git a/packages/components/src/Input.tsx b/packages/components/src/Input.tsx index c724efb12..71e51d69e 100644 --- a/packages/components/src/Input.tsx +++ b/packages/components/src/Input.tsx @@ -171,7 +171,11 @@ export const Input = ({ return ( ); diff --git a/packages/components/src/Modal.tsx b/packages/components/src/Modal.tsx index 9844298b9..23dc28954 100644 --- a/packages/components/src/Modal.tsx +++ b/packages/components/src/Modal.tsx @@ -45,6 +45,7 @@ export const Modal = ({ />