diff --git a/apps/namadillo/src/App/Masp/MaspShield.tsx b/apps/namadillo/src/App/Masp/MaspShield.tsx index b567bcad68..316f61cf32 100644 --- a/apps/namadillo/src/App/Masp/MaspShield.tsx +++ b/apps/namadillo/src/App/Masp/MaspShield.tsx @@ -10,13 +10,14 @@ import { import { allDefaultAccountsAtom } from "atoms/accounts"; import { namadaTransparentAssetsAtom } from "atoms/balance/atoms"; import { chainParametersAtom } from "atoms/chain/atoms"; +import { ledgerStatusDataAtom } from "atoms/ledger"; import { rpcUrlAtom } from "atoms/settings"; import BigNumber from "bignumber.js"; import { useTransactionActions } from "hooks/useTransactionActions"; import { useTransfer } from "hooks/useTransfer"; import { wallets } from "integrations"; import invariant from "invariant"; -import { useAtomValue } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { createTransferDataFromNamada } from "lib/transactions"; import { useState } from "react"; import { generatePath, useNavigate, useSearchParams } from "react-router-dom"; @@ -32,13 +33,17 @@ export const MaspShield: React.FC = () => { const rpcUrl = useAtomValue(rpcUrlAtom); const chainParameters = useAtomValue(chainParametersAtom); const defaultAccounts = useAtomValue(allDefaultAccountsAtom); - + const [ledgerStatus, setLedgerStatusStop] = useAtom(ledgerStatusDataAtom); const { data: availableAssets, isLoading: isLoadingAssets } = useAtomValue( namadaTransparentAssetsAtom ); const { storeTransaction } = useTransactionActions(); + const ledgerAccountInfo = ledgerStatus && { + deviceConnected: ledgerStatus.connected, + errorMessage: ledgerStatus.errorMessage, + }; const chainId = chainParameters.data?.chainId; const sourceAddress = defaultAccounts.data?.find( (account) => account.type !== AccountType.ShieldedKeys @@ -120,6 +125,9 @@ export const MaspShield: React.FC = () => { } }; + // We stop the ledger status check when the transfer is in progress + setLedgerStatusStop(isPerformingTransfer); + return (
@@ -143,6 +151,7 @@ export const MaspShield: React.FC = () => { onChangeSelectedAsset, amount: displayAmount, onChangeAmount: setDisplayAmount, + ledgerAccountInfo, }} destination={{ chain: namadaChain as Chain, diff --git a/apps/namadillo/src/App/Masp/MaspUnshield.tsx b/apps/namadillo/src/App/Masp/MaspUnshield.tsx index 72d3d11c46..3b60d916f8 100644 --- a/apps/namadillo/src/App/Masp/MaspUnshield.tsx +++ b/apps/namadillo/src/App/Masp/MaspUnshield.tsx @@ -10,13 +10,14 @@ import { import { allDefaultAccountsAtom } from "atoms/accounts"; import { namadaShieldedAssetsAtom } from "atoms/balance/atoms"; import { chainParametersAtom } from "atoms/chain/atoms"; +import { ledgerStatusDataAtom } from "atoms/ledger/atoms"; import { rpcUrlAtom } from "atoms/settings"; import BigNumber from "bignumber.js"; import { useTransactionActions } from "hooks/useTransactionActions"; import { useTransfer } from "hooks/useTransfer"; import { wallets } from "integrations"; import invariant from "invariant"; -import { useAtomValue } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { createTransferDataFromNamada } from "lib/transactions"; import { useState } from "react"; import { generatePath, useNavigate, useSearchParams } from "react-router-dom"; @@ -32,13 +33,17 @@ export const MaspUnshield: React.FC = () => { const rpcUrl = useAtomValue(rpcUrlAtom); const chainParameters = useAtomValue(chainParametersAtom); const defaultAccounts = useAtomValue(allDefaultAccountsAtom); - + const [ledgerStatus, setLedgerStatusStop] = useAtom(ledgerStatusDataAtom); const { data: availableAssets, isLoading: isLoadingAssets } = useAtomValue( namadaShieldedAssetsAtom ); const { storeTransaction } = useTransactionActions(); + const ledgerAccountInfo = ledgerStatus && { + deviceConnected: ledgerStatus.connected, + errorMessage: ledgerStatus.errorMessage, + }; const chainId = chainParameters.data?.chainId; const account = defaultAccounts.data?.find( (account) => account.type === AccountType.ShieldedKeys @@ -120,6 +125,8 @@ export const MaspUnshield: React.FC = () => { setGeneralErrorMessage(err + ""); } }; + // We stop the ledger status check when the transfer is in progress + setLedgerStatusStop(isPerformingTransfer); return ( @@ -145,6 +152,7 @@ export const MaspUnshield: React.FC = () => { onChangeSelectedAsset, amount: displayAmount, onChangeAmount: setDisplayAmount, + ledgerAccountInfo, }} destination={{ chain: namadaChain as Chain, diff --git a/apps/namadillo/src/App/NamadaTransfer/NamadaTransfer.tsx b/apps/namadillo/src/App/NamadaTransfer/NamadaTransfer.tsx index ddabc87297..72d529e523 100644 --- a/apps/namadillo/src/App/NamadaTransfer/NamadaTransfer.tsx +++ b/apps/namadillo/src/App/NamadaTransfer/NamadaTransfer.tsx @@ -13,13 +13,14 @@ import { namadaTransparentAssetsAtom, } from "atoms/balance/atoms"; import { chainParametersAtom } from "atoms/chain/atoms"; +import { ledgerStatusDataAtom } from "atoms/ledger"; import { applicationFeaturesAtom, rpcUrlAtom } from "atoms/settings"; import BigNumber from "bignumber.js"; import { useTransactionActions } from "hooks/useTransactionActions"; import { useTransfer } from "hooks/useTransfer"; import { wallets } from "integrations"; import invariant from "invariant"; -import { useAtomValue } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { createTransferDataFromNamada } from "lib/transactions"; import { useMemo, useState } from "react"; import { generatePath, useNavigate, useSearchParams } from "react-router-dom"; @@ -41,6 +42,7 @@ export const NamadaTransfer: React.FC = () => { const features = useAtomValue(applicationFeaturesAtom); const chainParameters = useAtomValue(chainParametersAtom); const defaultAccounts = useAtomValue(allDefaultAccountsAtom); + const [ledgerStatus, setLedgerStatusStop] = useAtom(ledgerStatusDataAtom); const { data: availableAssetsData, isLoading: isLoadingAssets } = useAtomValue( @@ -49,6 +51,10 @@ export const NamadaTransfer: React.FC = () => { const { storeTransaction } = useTransactionActions(); + const ledgerAccountInfo = ledgerStatus && { + deviceConnected: ledgerStatus.connected, + errorMessage: ledgerStatus.errorMessage, + }; const availableAssets = useMemo(() => { if (features.namTransfersEnabled) { return availableAssetsData; @@ -153,6 +159,9 @@ export const NamadaTransfer: React.FC = () => { } }; + // We stop the ledger status check when the transfer is in progress + setLedgerStatusStop(isPerformingTransfer); + return (
@@ -181,12 +190,14 @@ export const NamadaTransfer: React.FC = () => { onChangeShielded: setShielded, amount: displayAmount, onChangeAmount: setDisplayAmount, + ledgerAccountInfo, }} destination={{ chain: namadaChain as Chain, enableCustomAddress: true, customAddress, onChangeCustomAddress: setCustomAddress, + isShielded: isTargetShielded, }} gasConfig={gasConfig} isSubmitting={isPerformingTransfer} diff --git a/apps/namadillo/src/App/Transfer/TransferModule.tsx b/apps/namadillo/src/App/Transfer/TransferModule.tsx index 0481ac730c..1d057ad3d9 100644 --- a/apps/namadillo/src/App/Transfer/TransferModule.tsx +++ b/apps/namadillo/src/App/Transfer/TransferModule.tsx @@ -8,6 +8,7 @@ import { Address, AddressWithAssetAndAmountMap, GasConfig, + LedgerAccountInfo, WalletProvider, } from "types"; import { getDisplayGasFee } from "utils/gas"; @@ -31,6 +32,8 @@ type TransferModuleConfig = { onChangeChain?: (chain: Chain) => void; isShielded?: boolean; onChangeShielded?: (isShielded: boolean) => void; + // Additional information if selected account is a ledger + ledgerAccountInfo?: LedgerAccountInfo; }; export type TransferSourceProps = TransferModuleConfig & { @@ -87,6 +90,7 @@ type ValidationResult = | "NoDestinationChain" | "NoTransactionFee" | "NotEnoughBalance" + | "NoLedgerConnected" | "Ok"; export const TransferModule = ({ @@ -155,6 +159,12 @@ export const TransferModule = ({ return "NotEnoughBalance"; } else if (!destination.wallet && !destination.customAddress) { return "NoDestinationWallet"; + } else if ( + (source.isShielded || destination.isShielded) && + source.ledgerAccountInfo && + !source.ledgerAccountInfo.deviceConnected + ) { + return "NoLedgerConnected"; } else { return "Ok"; } @@ -236,6 +246,10 @@ export const TransferModule = ({ return "Select Asset"; } + if (validationResult === "NoLedgerConnected") { + return "Connect your ledger and open the Namada App"; + } + // TODO: this should be updated for nfts if (validationResult === "NoAmount") { return "Define an amount to transfer"; diff --git a/apps/namadillo/src/atoms/accounts/atoms.ts b/apps/namadillo/src/atoms/accounts/atoms.ts index c4c0af8816..6e359ceeac 100644 --- a/apps/namadillo/src/atoms/accounts/atoms.ts +++ b/apps/namadillo/src/atoms/accounts/atoms.ts @@ -10,6 +10,7 @@ import { namadaExtensionConnectedAtom } from "atoms/settings"; import { queryDependentFn } from "atoms/utils"; import BigNumber from "bignumber.js"; import { NamadaKeychain } from "hooks/useNamadaKeychain"; +import { atom } from "jotai"; import { atomWithMutation, atomWithQuery } from "jotai-tanstack-query"; import { fetchAccountBalance, @@ -69,6 +70,13 @@ export const allDefaultAccountsAtom = atomWithQuery((get) => { }; }); +export const isLedgerAccountAtom = atom((get) => { + const defaultAccounts = get(allDefaultAccountsAtom); + return Boolean( + defaultAccounts.data?.find((account) => account.type === AccountType.Ledger) + ); +}); + export const updateDefaultAccountAtom = atomWithMutation(() => { const namadaPromise = new NamadaKeychain().get(); return { diff --git a/apps/namadillo/src/atoms/ledger/atoms.ts b/apps/namadillo/src/atoms/ledger/atoms.ts new file mode 100644 index 0000000000..c82f91f6e9 --- /dev/null +++ b/apps/namadillo/src/atoms/ledger/atoms.ts @@ -0,0 +1,75 @@ +import { ledgerUSBList } from "@namada/sdk/web"; +import { LedgerError } from "@zondax/ledger-namada"; +import { isLedgerAccountAtom } from "atoms/accounts"; +import { atom } from "jotai"; + +import { atomWithQuery } from "jotai-tanstack-query"; +import { getSdkInstance } from "utils/sdk"; + +export type LedgerStatus = { + connected: boolean; + errorMessage: string; +}; + +const ledgerStatusStopAtom = atom(false); + +export const ledgerStatusAtom = atomWithQuery(() => { + return { + refetchInterval: 1000, + queryKey: ["ledger-status"], + queryFn: async () => { + const devices = await ledgerUSBList(); + + if (devices.length > 0) { + try { + // Disable console.warn to prevent the warning from showing up in the UI in the loop + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (console as any).warnOld = console.warn; + console.warn = () => {}; + + const sdk = await getSdkInstance(); + const ledger = await sdk.initLedger(); + const { + version: { returnCode, errorMessage }, + } = await ledger.status(); + + const connected = returnCode === LedgerError.NoErrors; + + await ledger.closeTransport(); + + return { + connected, + errorMessage, + }; + } catch (e) { + return { + connected: false, + errorMessage: `${e}`, + }; + } finally { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + console.warn = (console as any).warnOld; + } + } + + return { + connected: false, + errorMessage: "Ledger device not detected", + }; + }, + }; +}); + +export const ledgerStatusDataAtom = atom( + (get) => { + const isLedgerAccount = get(isLedgerAccountAtom); + const ledgetStatusStop = get(ledgerStatusStopAtom); + + if (isLedgerAccount && !ledgetStatusStop) { + return get(ledgerStatusAtom).data; + } + }, + (_, set, stop: boolean) => { + set(ledgerStatusStopAtom, stop); + } +); diff --git a/apps/namadillo/src/atoms/ledger/index.ts b/apps/namadillo/src/atoms/ledger/index.ts new file mode 100644 index 0000000000..4e0d46d9aa --- /dev/null +++ b/apps/namadillo/src/atoms/ledger/index.ts @@ -0,0 +1 @@ +export * from "./atoms"; diff --git a/apps/namadillo/src/types.ts b/apps/namadillo/src/types.ts index 91746830a2..746714ffe8 100644 --- a/apps/namadillo/src/types.ts +++ b/apps/namadillo/src/types.ts @@ -376,3 +376,8 @@ export type LocalnetToml = { chain_1_channel: string; chain_2_channel: string; }; + +export type LedgerAccountInfo = { + deviceConnected: boolean; + errorMessage: string; +}; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 29db7341ee..627c840b48 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,5 +1,5 @@ // Make Ledger available for direct-import as it is not dependent on Sdk initialization -export { Ledger, initLedgerUSBTransport } from "./ledger"; +export { Ledger, initLedgerUSBTransport, ledgerUSBList } from "./ledger"; export type { LedgerAddressAndPublicKey, LedgerProofGenerationKey, diff --git a/packages/sdk/src/ledger.ts b/packages/sdk/src/ledger.ts index 804b3652dd..e2ee314b70 100644 --- a/packages/sdk/src/ledger.ts +++ b/packages/sdk/src/ledger.ts @@ -55,6 +55,14 @@ export const initLedgerUSBTransport = async (): Promise => { return await TransportUSB.create(); }; +/** + * Returns a list of ledger devices + * @async + * @returns List of USB devices + */ +export const ledgerUSBList = async (): Promise => { + return await TransportUSB.list(); +}; export const DEFAULT_LEDGER_BIP44_PATH = makeBip44Path(coinType, { account: 0, change: 0,