diff --git a/apps/namadillo/src/App/Masp/MaspShield.tsx b/apps/namadillo/src/App/Masp/MaspShield.tsx index b567bcad68..7bf092802f 100644 --- a/apps/namadillo/src/App/Masp/MaspShield.tsx +++ b/apps/namadillo/src/App/Masp/MaspShield.tsx @@ -10,6 +10,7 @@ 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"; @@ -32,13 +33,17 @@ export const MaspShield: React.FC = () => { const rpcUrl = useAtomValue(rpcUrlAtom); const chainParameters = useAtomValue(chainParametersAtom); const defaultAccounts = useAtomValue(allDefaultAccountsAtom); - + const ledgerStatus = useAtomValue(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 @@ -143,6 +148,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..05df21191b 100644 --- a/apps/namadillo/src/App/Masp/MaspUnshield.tsx +++ b/apps/namadillo/src/App/Masp/MaspUnshield.tsx @@ -10,6 +10,7 @@ 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"; @@ -32,13 +33,17 @@ export const MaspUnshield: React.FC = () => { const rpcUrl = useAtomValue(rpcUrlAtom); const chainParameters = useAtomValue(chainParametersAtom); const defaultAccounts = useAtomValue(allDefaultAccountsAtom); - + const ledgerStatus = useAtomValue(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 @@ -145,6 +150,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 a7e3a973aa..c4dc35a1e8 100644 --- a/apps/namadillo/src/App/NamadaTransfer/NamadaTransfer.tsx +++ b/apps/namadillo/src/App/NamadaTransfer/NamadaTransfer.tsx @@ -10,6 +10,7 @@ import { import { allDefaultAccountsAtom } from "atoms/accounts"; 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"; @@ -38,12 +39,17 @@ export const NamadaTransfer: React.FC = () => { const features = useAtomValue(applicationFeaturesAtom); const chainParameters = useAtomValue(chainParametersAtom); const defaultAccounts = useAtomValue(allDefaultAccountsAtom); + const ledgerStatus = useAtomValue(ledgerStatusDataAtom); const { data: availableAssetsData, isLoading: isLoadingAssets } = useAtomValue(namadaTransparentAssetsAtom); const { storeTransaction } = useTransactionActions(); + const ledgerAccountInfo = ledgerStatus && { + deviceConnected: ledgerStatus.connected, + errorMessage: ledgerStatus.errorMessage, + }; const availableAssets = useMemo(() => { if (features.namTransfersEnabled) { return availableAssetsData; @@ -62,7 +68,7 @@ export const NamadaTransfer: React.FC = () => { const account = defaultAccounts.data?.find((account) => shielded ? account.type === AccountType.ShieldedKeys - : account.type !== AccountType.ShieldedKeys + : account.type !== AccountType.ShieldedKeys ); const sourceAddress = account?.address; const selectedAssetAddress = searchParams.get(params.asset) || undefined; @@ -172,12 +178,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 c3171ed550..8d4cdefd02 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 & { @@ -85,6 +88,7 @@ type ValidationResult = | "NoDestinationChain" | "NoTransactionFee" | "NotEnoughBalance" + | "NoLedgerConnected" | "Ok"; export const TransferModule = ({ @@ -153,6 +157,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"; } @@ -234,6 +244,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..6c62d49d71 --- /dev/null +++ b/apps/namadillo/src/atoms/ledger/atoms.ts @@ -0,0 +1,67 @@ +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; +}; + +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); + + if (isLedgerAccount) { + return get(ledgerStatusAtom).data; + } +}); 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 49deac9efb..13d1a26478 100644 --- a/apps/namadillo/src/types.ts +++ b/apps/namadillo/src/types.ts @@ -379,3 +379,8 @@ export type TempIndexerHealthType = { version: string; commit: string; }; + +export type LedgerAccountInfo = { + deviceConnected: boolean; + errorMessage: string; +}; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index cd7c35ece4..6033187b0b 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -3,6 +3,8 @@ export { Ledger, initLedgerHIDTransport, initLedgerUSBTransport, + ledgerHIDList, + ledgerUSBList, } from "./ledger"; export type { LedgerAddressAndPublicKey, diff --git a/packages/sdk/src/ledger.ts b/packages/sdk/src/ledger.ts index 32d515d313..ac6fd1dd96 100644 --- a/packages/sdk/src/ledger.ts +++ b/packages/sdk/src/ledger.ts @@ -63,6 +63,24 @@ export const initLedgerHIDTransport = async (): Promise => { return await TransportHID.create(); }; +/** + * Returns a list of ledger devices + * @async + * @returns List of USB devices + */ +export const ledgerUSBList = async (): Promise => { + return await TransportUSB.list(); +}; + +/** + * Returns a list of ledger devices + * @async + * @returns List of HID devices + */ +export const ledgerHIDList = async (): Promise => { + return await TransportHID.list(); +}; + export const DEFAULT_LEDGER_BIP44_PATH = makeBip44Path(coinType, { account: 0, change: 0,