diff --git a/.gitignore b/.gitignore index 73c1456ea2..83b24ad25c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,8 @@ yarn-error.log* !.yarn/sdks !.yarn/versions +.yalc +yalc.lock + # Local Netlify folder .netlify diff --git a/apps/extension/package.json b/apps/extension/package.json index f654bd4a6c..b194652cd5 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -35,7 +35,7 @@ "@dao-xyz/borsh": "^5.1.5", "@ledgerhq/hw-transport": "^6.31.4", "@ledgerhq/hw-transport-webusb": "^6.29.4", - "@namada/sdk": "0.22.0", + "@namada/sdk": "0.23.0-beta.1", "@zondax/ledger-namada": "^2.0.0", "bignumber.js": "^9.1.1", "buffer": "^6.0.3", @@ -54,7 +54,7 @@ }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.20.11", - "@namada/sdk-node": "^0.22.0", + "@namada/sdk-node": "0.23.0-beta.1", "@svgr/webpack": "^6.3.1", "@types/chrome": "^0.0.237", "@types/firefox-webext-browser": "^94.0.1", diff --git a/apps/extension/src/Approvals/Commitment.tsx b/apps/extension/src/Approvals/Commitment.tsx index 1a33345357..4c97e74841 100644 --- a/apps/extension/src/Approvals/Commitment.tsx +++ b/apps/extension/src/Approvals/Commitment.tsx @@ -1,4 +1,4 @@ -import { TxType } from "@namada/sdk"; +import { IbcTransferProps, TxType } from "@namada/sdk"; import { BondProps, ClaimRewardsProps, @@ -12,6 +12,9 @@ import { } from "@namada/types"; import { shortenAddress } from "@namada/utils"; import { NamCurrency } from "App/Common/NamCurrency"; +import * as J from "fp-ts/Json"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/Option"; import { ReactNode } from "react"; import { FaVoteYea } from "react-icons/fa"; import { FaRegEye, FaWallet } from "react-icons/fa6"; @@ -19,6 +22,7 @@ import { GoStack } from "react-icons/go"; import { PiDotsNineBold } from "react-icons/pi"; import { isShieldedPool, parseTransferType, ShieldedPoolLabel } from "utils"; import { TransactionCard } from "./TransactionCard"; +import { OsmosisSwapMemo } from "./types"; type CommitmentProps = { commitment: CommitmentDetailProps; @@ -37,6 +41,7 @@ const IconMap: Record = { [TxType.VoteProposal]: , [TxType.Batch]: , [TxType.ClaimRewards]: , + [TxType.OsmosisSwap]: , }; const TitleMap: Record = { @@ -51,6 +56,7 @@ const TitleMap: Record = { [TxType.VoteProposal]: "Vote", [TxType.Batch]: "Batch", [TxType.ClaimRewards]: "Claim Rewards", + [TxType.OsmosisSwap]: "Shielded Swap", }; const formatAddress = (address: string): string => @@ -147,6 +153,27 @@ export const Commitment = ({ wrapperFeePayer ); title = `${type} ${title}`; + } else if (commitment.txType === TxType.IBCTransfer) { + const ibcTx = commitment as CommitmentDetailProps; + + // It's fine not to handle errors here as memo can be optional and not JSON at all + const maybeMemo = pipe( + O.fromNullable(ibcTx.memo), + O.map((memo) => J.parse(memo)), + O.map(O.fromEither), + O.flatten + ); + + const maybeOsmosisSwapMemo = pipe( + maybeMemo, + O.map(OsmosisSwapMemo.decode), + O.map(O.fromEither), + O.flatten + ); + + if (O.isSome(maybeOsmosisSwapMemo)) { + title = TitleMap[TxType.OsmosisSwap]; + } } return ( diff --git a/apps/extension/src/Approvals/types.ts b/apps/extension/src/Approvals/types.ts index dc56de68fe..96c80409fd 100644 --- a/apps/extension/src/Approvals/types.ts +++ b/apps/extension/src/Approvals/types.ts @@ -1,3 +1,4 @@ +import * as t from "io-ts"; import { Message } from "router"; export enum TopLevelRoute { @@ -40,3 +41,63 @@ export enum Status { Pending, Failed, } + +const NamadaOsmosisSwap = t.type({ + overflow_receiver: t.string, + shielded_amount: t.string, + shielding_data: t.string, +}); + +const FinalMemoNamada = t.type({ + osmosis_swap: NamadaOsmosisSwap, +}); + +const FinalMemo = t.type({ + namada: FinalMemoNamada, +}); + +const OnFailedDelivery = t.type({ + local_recovery_addr: t.string, +}); + +const RouteItem = t.type({ + pool_id: t.string, + token_out_denom: t.string, +}); + +const Slippage = t.type({ + min_output_amount: t.string, +}); + +const OsmosisSwapMsg = t.type({ + final_memo: FinalMemo, + on_failed_delivery: OnFailedDelivery, + output_denom: t.string, + receiver: t.string, + route: t.array(RouteItem), + slippage: Slippage, +}); + +const Msg = t.type({ + osmosis_swap: OsmosisSwapMsg, +}); + +const Wasm = t.type({ + contract: t.string, + msg: Msg, +}); + +export const OsmosisSwapMemo = t.type({ + wasm: Wasm, +}); + +export type NamadaOsmosisSwap = t.TypeOf; +export type FinalMemoNamada = t.TypeOf; +export type FinalMemo = t.TypeOf; +export type OnFailedDelivery = t.TypeOf; +export type RouteItem = t.TypeOf; +export type Slippage = t.TypeOf; +export type OsmosisSwapMsg = t.TypeOf; +export type Msg = t.TypeOf; +export type Wasm = t.TypeOf; +export type OsmosisSwapMemo = t.TypeOf; diff --git a/apps/namadillo/package.json b/apps/namadillo/package.json index 8293bcb772..10d6097f70 100644 --- a/apps/namadillo/package.json +++ b/apps/namadillo/package.json @@ -12,7 +12,7 @@ "@keplr-wallet/types": "^0.12.136", "@namada/chain-registry": "^1.5.2", "@namada/indexer-client": "4.0.5", - "@namada/sdk-multicore": "0.22.0", + "@namada/sdk-multicore": "0.23.0-beta.1", "@tailwindcss/container-queries": "^0.1.1", "@tanstack/query-core": "^5.40.0", "@tanstack/react-query": "^5.40.0", @@ -79,7 +79,7 @@ }, "devDependencies": { "@eslint/js": "^9.9.1", - "@namada/sdk-node": "^0.22.0", + "@namada/sdk-node": "0.23.0-beta.1", "@namada/vite-esbuild-plugin": "^1.0.1", "@playwright/test": "^1.24.1", "@svgr/webpack": "^6.5.1", diff --git a/apps/namadillo/src/App/AppRoutes.tsx b/apps/namadillo/src/App/AppRoutes.tsx index 1a9ede7c1c..dd43e02cd3 100644 --- a/apps/namadillo/src/App/AppRoutes.tsx +++ b/apps/namadillo/src/App/AppRoutes.tsx @@ -36,6 +36,7 @@ import { StakingOverview } from "./Staking/StakingOverview"; import { StakingRewards } from "./Staking/StakingRewards"; import { StakingWithdrawModal } from "./Staking/StakingWithdrawModal"; import { Unstake } from "./Staking/Unstake"; +import { SwapModule } from "./Swap/SwapModule"; import { SwitchAccountPanel } from "./SwitchAccount/SwitchAccountPanel"; import { TransactionDetails } from "./Transactions/TransactionDetails"; import { TransactionHistory } from "./Transactions/TransactionHistory"; @@ -99,6 +100,9 @@ export const MainRoutes = (): JSX.Element => { } /> + {/* Swapping */} + } /> + {/* Transaction History */} {(features.namTransfersEnabled || features.ibcTransfersEnabled) && ( diff --git a/apps/namadillo/src/App/Transfer/AvailableAmountFooter.tsx b/apps/namadillo/src/App/Common/AvailableAmountFooter.tsx similarity index 100% rename from apps/namadillo/src/App/Transfer/AvailableAmountFooter.tsx rename to apps/namadillo/src/App/Common/AvailableAmountFooter.tsx diff --git a/apps/namadillo/src/App/Transfer/ConnectProviderButton.tsx b/apps/namadillo/src/App/Common/ConnectProviderButton.tsx similarity index 89% rename from apps/namadillo/src/App/Transfer/ConnectProviderButton.tsx rename to apps/namadillo/src/App/Common/ConnectProviderButton.tsx index 0b1a2c31d9..e8ae0ef9b8 100644 --- a/apps/namadillo/src/App/Transfer/ConnectProviderButton.tsx +++ b/apps/namadillo/src/App/Common/ConnectProviderButton.tsx @@ -3,11 +3,13 @@ import { ActionButton } from "@namada/components"; type ConnectProviderButtonProps = { onClick?: () => void; disabled?: boolean; + text?: string; }; export const ConnectProviderButton = ({ onClick, disabled, + text, }: ConnectProviderButtonProps): JSX.Element => { return ( - Select Address + {text || "Select Address"} ); }; diff --git a/apps/namadillo/src/App/Transfer/CurrentStatus.tsx b/apps/namadillo/src/App/Common/CurrentStatus.tsx similarity index 100% rename from apps/namadillo/src/App/Transfer/CurrentStatus.tsx rename to apps/namadillo/src/App/Common/CurrentStatus.tsx diff --git a/apps/namadillo/src/App/Common/LedgerDeviceTooltip.tsx b/apps/namadillo/src/App/Common/LedgerDeviceTooltip.tsx new file mode 100644 index 0000000000..42f54d5e71 --- /dev/null +++ b/apps/namadillo/src/App/Common/LedgerDeviceTooltip.tsx @@ -0,0 +1,32 @@ +import { routes } from "App/routes"; +import { BsQuestionCircleFill } from "react-icons/bs"; +import { Link, useNavigate } from "react-router-dom"; +import { IconTooltip } from "./IconTooltip"; + +export const LedgerDeviceTooltip = (): JSX.Element => { + const navigate = useNavigate(); + return ( + } + text={ + + If your device is connected and the app is open, please go to{" "} + { + e.preventDefault(); + navigate(routes.settingsLedger, { + state: { backgroundLocation: location }, + }); + }} + to={routes.settingsLedger} + className="text-yellow" + > + Settings + {" "} + and pair your device with Namadillo. + + } + /> + ); +}; diff --git a/apps/namadillo/src/App/Common/SelectAssetModal.tsx b/apps/namadillo/src/App/Common/SelectAssetModal.tsx new file mode 100644 index 0000000000..bb9cb9c705 --- /dev/null +++ b/apps/namadillo/src/App/Common/SelectAssetModal.tsx @@ -0,0 +1,100 @@ +import { Stack } from "@namada/components"; +import { Search } from "App/Common/Search"; +import { SelectModal } from "App/Common/SelectModal"; +import { TokenCard } from "App/Common/TokenCard"; +import { ConnectedWalletInfo } from "App/Transfer/ConnectedWalletInfo"; +import { nativeTokenAddressAtom } from "atoms/chain/atoms"; +import { applicationFeaturesAtom } from "atoms/settings/atoms"; +import BigNumber from "bignumber.js"; +import clsx from "clsx"; +import { useAtomValue } from "jotai"; +import { useMemo, useState } from "react"; +import { twMerge } from "tailwind-merge"; +import { Address, Asset, NamadaAsset } from "types"; + +type DisplayAmount = BigNumber; +type FiatAmount = BigNumber; +type SelectWalletModalProps = { + onClose: () => void; + onSelect: (address: Address) => void; + assets: Asset[]; + walletAddress: string; + ibcTransfer?: "deposit" | "withdraw"; + balances?: Record; +}; + +export const SelectAssetModal = ({ + onClose, + onSelect, + assets, + walletAddress, + ibcTransfer, + balances, +}: SelectWalletModalProps): JSX.Element => { + const { namTransfersEnabled } = useAtomValue(applicationFeaturesAtom); + const nativeTokenAddress = useAtomValue(nativeTokenAddressAtom).data; + + const [filter, setFilter] = useState(""); + + const filteredAssets = useMemo(() => { + return assets.filter( + (asset) => + asset.name.toLowerCase().indexOf(filter.toLowerCase()) >= 0 || + asset.symbol.toLowerCase().indexOf(filter.toLowerCase()) >= 0 + ); + }, [assets, filter]); + + return ( + + +
+ +
+ + {filteredAssets.map((asset) => { + // Fpr IbcTransfer(Deposits), we consider base denom as a token address. + const tokenAddress = + ibcTransfer === "deposit" ? + asset.base + : (asset as NamadaAsset).address; + + const disabled = + !namTransfersEnabled && asset.address === nativeTokenAddress; + return ( +
  • + +
  • + ); + })} + {filteredAssets.length === 0 && ( +

    There are no available assets

    + )} +
    +
    + ); +}; diff --git a/apps/namadillo/src/App/Transfer/SelectWalletModal.tsx b/apps/namadillo/src/App/Common/SelectWalletModal.tsx similarity index 100% rename from apps/namadillo/src/App/Transfer/SelectWalletModal.tsx rename to apps/namadillo/src/App/Common/SelectWalletModal.tsx diff --git a/apps/namadillo/src/App/Transfer/SelectedAsset.tsx b/apps/namadillo/src/App/Common/SelectedAsset.tsx similarity index 97% rename from apps/namadillo/src/App/Transfer/SelectedAsset.tsx rename to apps/namadillo/src/App/Common/SelectedAsset.tsx index 7271a0ec99..e0729cd86a 100644 --- a/apps/namadillo/src/App/Transfer/SelectedAsset.tsx +++ b/apps/namadillo/src/App/Common/SelectedAsset.tsx @@ -1,9 +1,9 @@ import { SkeletonLoading, Tooltip } from "@namada/components"; +import { EmptyResourceIcon } from "App/Transfer/EmptyResourceIcon"; import clsx from "clsx"; import { getAssetImageUrl } from "integrations/utils"; import { GoChevronDown } from "react-icons/go"; import { Asset } from "types"; -import { EmptyResourceIcon } from "./EmptyResourceIcon"; type SelectedAssetProps = { asset?: Asset; diff --git a/apps/namadillo/src/App/Common/SidebarMenuItem.tsx b/apps/namadillo/src/App/Common/SidebarMenuItem.tsx index fedcef554f..0e004c5a9a 100644 --- a/apps/namadillo/src/App/Common/SidebarMenuItem.tsx +++ b/apps/namadillo/src/App/Common/SidebarMenuItem.tsx @@ -8,7 +8,7 @@ type Props = { export const SidebarMenuItem = ({ url, children }: Props): JSX.Element => { const className = clsx( - "flex items-center gap-5 text-lg text-white", + "flex items-center gap-4 text-lg text-white", "transition-colors duration-300 ease-out-quad hover:text-cyan", { "!text-neutral-500 pointer-events-none select-none": !url, diff --git a/apps/namadillo/src/App/Transfer/TokenAmountCard.tsx b/apps/namadillo/src/App/Common/TokenAmountCard.tsx similarity index 100% rename from apps/namadillo/src/App/Transfer/TokenAmountCard.tsx rename to apps/namadillo/src/App/Common/TokenAmountCard.tsx diff --git a/apps/namadillo/src/App/Common/TokenCard.tsx b/apps/namadillo/src/App/Common/TokenCard.tsx index 8be72cf227..a41d049ed9 100644 --- a/apps/namadillo/src/App/Common/TokenCard.tsx +++ b/apps/namadillo/src/App/Common/TokenCard.tsx @@ -1,24 +1,42 @@ +import { Stack } from "@namada/components"; import { InactiveChannelWarning } from "App/Common/InactiveChannelWarning"; import { AssetImage } from "App/Transfer/AssetImage"; +import BigNumber from "bignumber.js"; import { ReactNode } from "react"; import { Address, Asset } from "types"; +import { FiatCurrency } from "./FiatCurrency"; export const TokenCard = ({ address, asset, + balance, disabled, }: { address: Address; asset: Asset; + balance?: [BigNumber, BigNumber?]; disabled?: ReactNode; }): JSX.Element => { + const [amount, fiatAmount] = balance || []; + return (
    -
    - {asset.symbol} +
    + +
    {asset.symbol}
    + + {amount &&
    {amount.toFixed()}
    } + {fiatAmount && ( + + )} +
    +
    {disabled && (
    disabled until phase 5
    diff --git a/apps/namadillo/src/App/Common/TransactionFee.tsx b/apps/namadillo/src/App/Common/TransactionFee.tsx index 58983e3417..9ebfa58017 100644 --- a/apps/namadillo/src/App/Common/TransactionFee.tsx +++ b/apps/namadillo/src/App/Common/TransactionFee.tsx @@ -1,19 +1,28 @@ import BigNumber from "bignumber.js"; +import clsx from "clsx"; import { TokenCurrency } from "./TokenCurrency"; type TransactionFeeProps = { displayAmount: BigNumber; symbol: string; + compact?: boolean; }; export const TransactionFee = ({ displayAmount, symbol, + compact = false, }: TransactionFeeProps): JSX.Element => { return (
    - - Transaction Fee + + {compact ? "Fee:" : "Transaction Fee"} { const [modalOpen, setModalOpen] = useState(false); @@ -34,16 +36,18 @@ export const TransactionFeeButton = ({ <>
    -
    Fees:
    + {!compact &&
    Fees:
    }