diff --git a/apps/extension/package.json b/apps/extension/package.json index 9eca4d1f33..0f6ec8f390 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.23.0", + "@namada/sdk": "0.24.0-beta.2", "@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.23.0", + "@namada/sdk-node": "0.24.0-beta.1", "@svgr/webpack": "^6.3.1", "@types/chrome": "^0.0.237", "@types/firefox-webext-browser": "^94.0.1", diff --git a/apps/namadillo/package.json b/apps/namadillo/package.json index 978efe681e..d79889fd5c 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.23.0", + "@namada/sdk-multicore": "0.24.0-beta.2", "@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.23.0", + "@namada/sdk-node": "0.24.0-beta.2", "@namada/vite-esbuild-plugin": "^1.0.1", "@playwright/test": "^1.24.1", "@svgr/webpack": "^6.5.1", diff --git a/apps/namadillo/public/config.toml b/apps/namadillo/public/config.toml index 9e4e902e0a..d3fb6945bf 100644 --- a/apps/namadillo/public/config.toml +++ b/apps/namadillo/public/config.toml @@ -4,3 +4,9 @@ #masp_indexer_url = "" #localnet_enabled = false #fathom_site_id = "" + +[frontend_fee."*"] +transparent_target = "tnam1qrxsru5rdu4he400xny6p779fcw7xuftsgjnmzup" +shielded_target = "znam1494fmm9qd4frr7jrydnxlcyt57nhngzutxnzgh6y70mkvh23d9z0jk2aasxh4p8dn2kczlgpans" +percentage = 0.01 + diff --git a/apps/namadillo/src/App/Common/TransferFee.tsx b/apps/namadillo/src/App/Common/TransferFee.tsx new file mode 100644 index 0000000000..a532b81146 --- /dev/null +++ b/apps/namadillo/src/App/Common/TransferFee.tsx @@ -0,0 +1,189 @@ +import { Stack } from "@namada/components"; +import { namadaRegistryChainAssetsMapAtom } from "atoms/integrations"; +import { tokenPricesFamily } from "atoms/prices/atoms"; +import BigNumber from "bignumber.js"; +import { TransactionFeeProps } from "hooks/useTransactionFee"; +import { useAtomValue } from "jotai"; +import { useMemo, useState } from "react"; +import { GoInfo } from "react-icons/go"; +import { IoIosArrowDown } from "react-icons/io"; +import { twMerge } from "tailwind-merge"; +import { Address, FrontendFeeEntry } from "types"; +import { calculateFrontendFeeAmount } from "utils/frontendFee"; +import { getDisplayGasFee } from "utils/gas"; +import { GasFeeModal } from "./GasFeeModal"; +import { IconTooltip } from "./IconTooltip"; + +type FrontendFeeInfo = { + fee: FrontendFeeEntry; + displayAmount?: BigNumber; + token?: Address; +}; +export const TransferFee = ({ + feeProps, + inOrOutOfMASP, + isShieldedTransfer = false, + frontendFeeInfo, + showButton = true, +}: { + feeProps: TransactionFeeProps; + inOrOutOfMASP: boolean; + isShieldedTransfer?: boolean; + frontendFeeInfo?: FrontendFeeInfo; + showButton?: boolean; +}): JSX.Element => { + const [modalOpen, setModalOpen] = useState(false); + const [feeDetailsOpen, setFeeDetailsOpen] = useState(false); + + const chainAssetsMap = useAtomValue(namadaRegistryChainAssetsMapAtom); + + const chainAssetsMapData = chainAssetsMap.data; + + const gasDisplayAmount = useMemo(() => { + if (!chainAssetsMapData) { + return; + } + + return getDisplayGasFee(feeProps.gasConfig, chainAssetsMapData); + }, [feeProps, chainAssetsMapData]); + + const gasToken = gasDisplayAmount?.asset.address; + const frontendFeeToken = frontendFeeInfo?.token; + const tokenAddresses = [gasToken, frontendFeeToken].filter( + (address) => typeof address !== "undefined" + ); + + const gasDollarMap = + useAtomValue(tokenPricesFamily(tokenAddresses)).data ?? {}; + + const [frontendFeeAmount, frontendFeeFiatAmount, symbol] = useMemo((): [ + BigNumber?, + BigNumber?, + string?, + ] => { + if ( + frontendFeeInfo && + frontendFeeInfo.token && + frontendFeeInfo.displayAmount + ) { + const feeAmount = calculateFrontendFeeAmount( + frontendFeeInfo.displayAmount, + frontendFeeInfo.fee + ); + const dollarPrice = gasDollarMap[frontendFeeInfo.token]; + const fiatFeeAmount = feeAmount.multipliedBy(dollarPrice); + const symbol = chainAssetsMapData?.[frontendFeeInfo.token]?.symbol; + + return [feeAmount, fiatFeeAmount, symbol]; + } + return []; + }, [gasDollarMap, frontendFeeInfo]); + + const fiatAmount = useMemo(() => { + if ( + !gasDisplayAmount || + !gasDollarMap || + !gasToken || + !gasDollarMap[gasToken] + ) { + return; + } + const dollarPrice = gasDollarMap[gasToken]; + let fiatAmount = + gasDisplayAmount.totalDisplayAmount.multipliedBy(dollarPrice); + + if (inOrOutOfMASP && frontendFeeFiatAmount) { + fiatAmount = fiatAmount.plus(frontendFeeFiatAmount); + } + return fiatAmount; + }, [gasDisplayAmount, gasDollarMap, inOrOutOfMASP, gasToken]); + + return ( + + +
setFeeDetailsOpen((opened) => !opened)} + > + {feeDetailsOpen ? "Hide fee settings" : "Fee settings"} +
+
+ Total Fee {fiatAmount ? `$${fiatAmount.decimalPlaces(6)}` : ""} +
+
+ {feeDetailsOpen && ( + + +
Gas fee:
+ +
+ {gasDisplayAmount ? + gasDisplayAmount.totalDisplayAmount.toString() + : ""}{" "} +
+ {!showButton && gasDisplayAmount?.asset.symbol} + {showButton && ( +
+ +
+ )} +
+
+ {inOrOutOfMASP && frontendFeeInfo && ( + + + MASP fee +
+ } + text={ +
+ MASP fees are set by the Namadillo Host and may +
vary accross Namadillo instances +
+ } + /> +
+
+ +
+ {frontendFeeAmount && symbol ? + `${frontendFeeAmount.toString()} ${symbol}` + : "0"} +
+
+ )} +
+ )} + {modalOpen && ( + setModalOpen(false)} + isShielded={isShieldedTransfer} + chainAssetsMap={chainAssetsMap.data || {}} + /> + )} +
+ ); +}; diff --git a/apps/namadillo/src/App/Ibc/IbcTransfer.tsx b/apps/namadillo/src/App/Ibc/IbcTransfer.tsx index 71c3e60137..ee869b3c23 100644 --- a/apps/namadillo/src/App/Ibc/IbcTransfer.tsx +++ b/apps/namadillo/src/App/Ibc/IbcTransfer.tsx @@ -68,21 +68,28 @@ export const IbcTransfer = ({ ); const { trackEvent } = useFathomTracker(); const { storeTransaction } = useTransactionActions(); + const shielded = isShieldedAddress(destinationAddress ?? ""); + + const availableDisplayAmount = mapUndefined((baseDenom) => { + return userAssets ? userAssets[baseDenom]?.amount : undefined; + }, selectedAssetWithAmount?.asset?.address); + + const amountForGasCalc = + amount && availableDisplayAmount ? + BigNumber.minimum(amount, availableDisplayAmount) + : undefined; - const { transferToNamada, gasConfig } = useIbcTransaction({ + const { transferToNamada, transactionFeeProps } = useIbcTransaction({ registry, sourceAddress, sourceChannel, destinationChannel, - shielded: isShieldedAddress(destinationAddress ?? ""), + shielded, selectedAsset: selectedAssetWithAmount?.asset, + amount: amountForGasCalc, }); // DERIVED VALUES - const shielded = isShieldedAddress(destinationAddress ?? ""); - const availableDisplayAmount = mapUndefined((baseDenom) => { - return userAssets ? userAssets[baseDenom]?.amount : undefined; - }, selectedAssetWithAmount?.asset?.address); const namadaAddress = useMemo(() => { return ( defaultAccounts.data?.find( @@ -124,11 +131,16 @@ export const IbcTransfer = ({ }: OnSubmitTransferParams): Promise => { try { invariant(registry?.chain, "Error: Chain not selected"); + invariant(selectedAssetWithAmount?.asset, "Error: Asset not selected"); + invariant(displayAmount, "Error: Amount not specified"); + invariant(destinationAddress, "Error: Destination address not specified"); + setGeneralErrorMessage(""); setCurrentStatus("Submitting..."); + const result = await transferToNamada.mutateAsync({ - destinationAddress: destinationAddress ?? "", - displayAmount: new BigNumber(displayAmount ?? "0"), + destinationAddress, + displayAmount: BigNumber(displayAmount), memo, onUpdateStatus: setCurrentStatus, }); @@ -161,7 +173,7 @@ export const IbcTransfer = ({ isShieldedAddress: shielded, onChangeAddress: setDestinationAddress, }} - gasConfig={gasConfig.data} + feeProps={transactionFeeProps} isSubmitting={ transferToNamada.isPending || /* isSuccess means that the transaction has been broadcasted, but doesn't take diff --git a/apps/namadillo/src/App/Swap/hooks/usePerformOsmosisSwapTx.tsx b/apps/namadillo/src/App/Swap/hooks/usePerformOsmosisSwapTx.tsx index fe69d73e4d..02d8db7cb1 100644 --- a/apps/namadillo/src/App/Swap/hooks/usePerformOsmosisSwapTx.tsx +++ b/apps/namadillo/src/App/Swap/hooks/usePerformOsmosisSwapTx.tsx @@ -181,6 +181,7 @@ export function usePerformOsmosisSwapTx(): UsePerformOsmosisSwapResult { account: transparentAccount, params: [params], gasConfig: feeProps.gasConfig, + frontendFee: feeProps.frontendFee, }); setStatus(SwapStatus.awaitingSignature()); diff --git a/apps/namadillo/src/App/Transfer/TransferDestination.tsx b/apps/namadillo/src/App/Transfer/TransferDestination.tsx index 9ee9f363d9..5c25974900 100644 --- a/apps/namadillo/src/App/Transfer/TransferDestination.tsx +++ b/apps/namadillo/src/App/Transfer/TransferDestination.tsx @@ -4,8 +4,7 @@ import { AccountType } from "@namada/types"; import { shortenAddress } from "@namada/utils"; import { ConnectProviderButton } from "App/Common/ConnectProviderButton"; import { TokenAmountCard } from "App/Common/TokenAmountCard"; -import { TransactionFee } from "App/Common/TransactionFee"; -import { TransactionFeeButton } from "App/Common/TransactionFeeButton"; +import { TransferFee } from "App/Common/TransferFee"; import { routes } from "App/routes"; import { isIbcAddress, @@ -24,7 +23,7 @@ import { useAtomValue } from "jotai"; import { useCallback, useEffect, useState } from "react"; import { GoChevronDown } from "react-icons/go"; import { useLocation } from "react-router-dom"; -import { Address } from "types"; +import { Address, FrontendFeeEntry } from "types"; import namadaShieldedIcon from "./assets/namada-shielded.svg"; import namadaTransparentIcon from "./assets/namada-transparent.svg"; import semiTransparentEye from "./assets/semi-transparent-eye.svg"; @@ -40,8 +39,6 @@ type TransferDestinationProps = { isShieldedTx?: boolean; isSubmitting?: boolean; walletAddress?: string; - gasDisplayAmount?: BigNumber; - gasAsset?: Asset; feeProps?: TransactionFeeProps; destinationAsset?: Asset; amount?: BigNumber; @@ -53,14 +50,13 @@ type TransferDestinationProps = { onChangeMemo?: (address: string) => void; isShielding?: boolean; isUnshielding?: boolean; + frontendFee?: FrontendFeeEntry; }; export const TransferDestination = ({ isShieldedAddress, isShieldedTx = false, isSubmitting, - gasDisplayAmount, - gasAsset, feeProps, destinationAsset, amount, @@ -72,6 +68,7 @@ export const TransferDestination = ({ onChangeMemo, isShielding = false, isUnshielding = false, + frontendFee, }: TransferDestinationProps): JSX.Element => { const { data: accounts } = useAtomValue(allDefaultAccountsAtom); const [isModalOpen, setIsModalOpen] = useState(false); @@ -307,21 +304,21 @@ export const TransferDestination = ({ {!isSubmitting && ( )} diff --git a/apps/namadillo/src/App/Transfer/TransferModule.tsx b/apps/namadillo/src/App/Transfer/TransferModule.tsx index f4d7d99cdd..5b1abe224a 100644 --- a/apps/namadillo/src/App/Transfer/TransferModule.tsx +++ b/apps/namadillo/src/App/Transfer/TransferModule.tsx @@ -25,8 +25,12 @@ import { useNavigate, useSearchParams, } from "react-router-dom"; -import { AssetWithAmountAndChain } from "types"; +import { AssetWithAmountAndChain, NamadaAsset } from "types"; import { filterAvailableAssetsWithBalance } from "utils/assets"; +import { + calculateAmountWithoutFrontendFee, + getFrontendFeeEntry, +} from "utils/frontendFee"; import { getDisplayGasFee } from "utils/gas"; import { isIbcAddress, isShieldedAddress } from "./common"; import { IbcChannels } from "./IbcChannels"; @@ -50,7 +54,6 @@ export const TransferModule = ({ errorMessage, currentStatus, currentStatusExplanation, - gasConfig: gasConfigProp, onSubmitTransfer, completedAt, onComplete, @@ -121,7 +124,8 @@ export const TransferModule = ({ }); }; - const gasConfig = gasConfigProp ?? feeProps?.gasConfig; + const gasConfig = feeProps?.gasConfig; + const frontendFee = feeProps?.frontendFee; const displayGasFee = useMemo(() => { return gasConfig ? @@ -129,22 +133,39 @@ export const TransferModule = ({ : undefined; }, [gasConfig]); - const availableAmountMinusFees = useMemo(() => { - if (!availableAmount || !availableAssets) return; - + const [availableAmountMinusFees, frontendFeeEntry] = useMemo(() => { if ( - !displayGasFee?.totalDisplayAmount || - // Don't subtract if the gas token is different than the selected asset: - gasConfig?.gasToken !== selectedAsset?.asset.address - ) { - return availableAmount; + !availableAmount || + !availableAssets || + !displayGasFee || + !gasConfig || + !frontendFee + ) + return []; + let amountMinusFees = availableAmount; + + if (gasConfig?.gasToken === selectedAsset?.asset.address) { + amountMinusFees = availableAmount + .minus(displayGasFee.totalDisplayAmount) + .decimalPlaces(6); } - const amountMinusFees = availableAmount - .minus(displayGasFee.totalDisplayAmount) - .decimalPlaces(6); + const frontendSusFee = getFrontendFeeEntry( + frontendFee, + (selectedAsset.asset as NamadaAsset).address + ); + + const shouldApplyFrontendFee = + (isSourceShielded && destinationAddress && !isTargetShielded) || + (isTargetShielded && sourceAddress && !isSourceShielded); + if (frontendSusFee && shouldApplyFrontendFee) { + amountMinusFees = calculateAmountWithoutFrontendFee( + amountMinusFees, + frontendSusFee + ); + } - return BigNumber.max(amountMinusFees, 0); + return [BigNumber.max(amountMinusFees, 0), frontendSusFee] as const; }, [selectedAsset?.asset.address, availableAmount, displayGasFee]); const validationResult = useMemo((): ValidationResult => { @@ -238,13 +259,12 @@ export const TransferModule = ({ memo={destination.memo} onChangeMemo={destination.onChangeMemo} feeProps={feeProps} - gasDisplayAmount={displayGasFee?.totalDisplayAmount} - gasAsset={displayGasFee?.asset} destinationAsset={selectedAsset?.asset} amount={source.amount} isSubmitting={isSubmitting} isShielding={isShielding} isUnshielding={isUnshielding} + frontendFee={frontendFeeEntry} /> {ibcTransfer && requiresIbcChannels && ibcChannels && ( Promise; diff --git a/apps/namadillo/src/atoms/fees/atoms.ts b/apps/namadillo/src/atoms/fees/atoms.ts index 2dddc4cb6e..6f236cf45b 100644 --- a/apps/namadillo/src/atoms/fees/atoms.ts +++ b/apps/namadillo/src/atoms/fees/atoms.ts @@ -5,9 +5,17 @@ import { import { defaultAccountAtom } from "atoms/accounts"; import { indexerApiAtom } from "atoms/api"; import { namadaRegistryChainAssetsMapAtom } from "atoms/integrations"; +import { defaultServerConfigAtom } from "atoms/settings"; import { queryDependentFn } from "atoms/utils"; import BigNumber from "bignumber.js"; +import * as E from "fp-ts/Either"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/Option"; +import * as R from "fp-ts/Record"; import invariant from "invariant"; +import * as t from "io-ts"; +import { PathReporter } from "io-ts/PathReporter"; +import { atom } from "jotai"; import { atomWithQuery } from "jotai-tanstack-query"; import { atomFamily } from "jotai/utils"; import { isPublicKeyRevealed } from "lib/query"; @@ -98,3 +106,41 @@ export const isPublicKeyRevealedAtom = atomWithQuery((get) => { }, [defaultAccount]), }; }); + +const FrontendFeeSchema = t.union([ + t.undefined, + t.record( + t.string, + t.type({ + transparent_target: t.string, + shielded_target: t.string, + percentage: t.number, + }) + ), +]); + +export const frontendFeeAtom = atom((get) => { + const maybeFrontendFee = get(defaultServerConfigAtom).data?.frontend_fee; + const eitherFrontendFee = FrontendFeeSchema.decode(maybeFrontendFee); + if (E.isLeft(eitherFrontendFee)) { + console.warn( + "Invalid frontend fee schema: ", + PathReporter.report(eitherFrontendFee).join("\n") + ); + return {}; + } + // TODO: validate if targets are valid addresses + + return pipe( + O.fromNullable(eitherFrontendFee.right), + O.fold( + () => ({}), + (fees) => fees + ), + R.map((fee) => ({ + transparentTarget: fee.transparent_target, + shieldedTarget: fee.shielded_target, + percentage: BigNumber(fee.percentage), + })) + ); +}); diff --git a/apps/namadillo/src/atoms/integrations/services.ts b/apps/namadillo/src/atoms/integrations/services.ts index bc235bd896..cac16a3ba8 100644 --- a/apps/namadillo/src/atoms/integrations/services.ts +++ b/apps/namadillo/src/atoms/integrations/services.ts @@ -16,6 +16,7 @@ import { } from "@namada/indexer-client"; import { getIndexerApi } from "atoms/api"; import { chainParametersAtom } from "atoms/chain"; +import { frontendFeeAtom } from "atoms/fees"; import { rpcUrlAtom } from "atoms/settings"; import { queryForAck, queryForIbcTimeout } from "atoms/transactions"; import BigNumber from "bignumber.js"; @@ -35,6 +36,7 @@ import { TransferStep, TransferTransactionData, } from "types"; +import { frontendSusMsgFromConfig } from "utils/frontendFee"; import { isError404 } from "utils/http"; import { getKeplrWallet } from "utils/ibc"; import { getSdkInstance } from "utils/sdk"; @@ -66,6 +68,7 @@ export type IbcTransferParams = TransparentParams | ShieldedParams; export const getShieldedArgs = async ( target: string, token: string, + namadaToken: string, amount: BigNumber, destinationChannelId: string ): Promise<{ receiver: string; memo: string }> => { @@ -73,6 +76,7 @@ export const getShieldedArgs = async ( const store = getDefaultStore(); const rpcUrl = store.get(rpcUrlAtom); const chain = store.get(chainParametersAtom); + const frontendFee = store.get(frontendFeeAtom); if (!chain.isSuccess) throw "Chain not loaded"; @@ -83,6 +87,13 @@ export const getShieldedArgs = async ( payload: { rpcUrl, token: sdk.nativeToken, maspIndexerUrl: "" }, }); + const frontendSusFee = frontendSusMsgFromConfig( + frontendFee, + namadaToken, + // For IBC shielding, the fee is always deposited into shielded pool + "shielded" + ); + const msg: GenerateIbcShieldingMemo = { type: "generate-ibc-shielding-memo", payload: { @@ -91,6 +102,7 @@ export const getShieldedArgs = async ( amount, destinationChannelId, chainId: chain.data.chainId, + frontendSusFee, }, }; diff --git a/apps/namadillo/src/atoms/prices/atoms.ts b/apps/namadillo/src/atoms/prices/atoms.ts index 5d422194d5..35de624403 100644 --- a/apps/namadillo/src/atoms/prices/atoms.ts +++ b/apps/namadillo/src/atoms/prices/atoms.ts @@ -1,5 +1,6 @@ import { namadaRegistryChainAssetsMapAtom } from "atoms/integrations"; import { queryDependentFn } from "atoms/utils"; +import BigNumber from "bignumber.js"; import invariant from "invariant"; import { atomWithQuery } from "jotai-tanstack-query"; import { atomFamily } from "jotai/utils"; @@ -16,7 +17,26 @@ export const tokenPricesFamily = atomFamily( queryKey: ["token-prices", addresses, chainAssetsMap.data], ...queryDependentFn(async () => { invariant(chainAssetsMap.data, "No chain assets"); - return fetchTokenPrices(addresses, chainAssetsMap.data); + // TODO: for some reason, the first fetch often returns all zeros, so we loop until we get a non-zero result + const checkAllZero = ( + prices: Record + ): boolean => { + return Object.values(prices).every((price) => price.isZero()); + }; + const fetch = fetchTokenPrices.bind( + null, + addresses, + chainAssetsMap.data + ); + + let result = await fetch(); + let allZero = checkAllZero(result); + while (allZero) { + await new Promise((resolve) => setTimeout(resolve, 10)); + result = await fetch(); + allZero = checkAllZero(result); + } + return result; }, [chainAssetsMap]), }; }), diff --git a/apps/namadillo/src/atoms/transfer/atoms.ts b/apps/namadillo/src/atoms/transfer/atoms.ts index ba5162b5a8..72fe46d966 100644 --- a/apps/namadillo/src/atoms/transfer/atoms.ts +++ b/apps/namadillo/src/atoms/transfer/atoms.ts @@ -103,6 +103,7 @@ export const createShieldingTransferAtom = atomWithMutation((get) => { gasConfig, account, memo, + frontendFee, }: BuildTxAtomParams) => createShieldingTransferTx( chain.data!, @@ -110,6 +111,7 @@ export const createShieldingTransferAtom = atomWithMutation((get) => { params, gasConfig, rpcUrl, + frontendFee, memo ), }; @@ -128,6 +130,7 @@ export const createUnshieldingTransferAtom = atomWithMutation((get) => { gasConfig, account, signer, + frontendFee, memo, }: BuildTxAtomParams) => { invariant( @@ -150,6 +153,7 @@ export const createUnshieldingTransferAtom = atomWithMutation((get) => { gasConfig, rpcUrl, signer, + frontendFee, memo ); }, @@ -221,6 +225,7 @@ export const createIbcTxAtom = atomWithMutation((get) => { account, signer, memo, + frontendFee, }: BuildTxAtomParams) => { invariant( signer, @@ -236,6 +241,7 @@ export const createIbcTxAtom = atomWithMutation((get) => { gasConfig, rpcUrl, signer?.publicKey, + frontendFee, memo ); }, diff --git a/apps/namadillo/src/atoms/transfer/services.ts b/apps/namadillo/src/atoms/transfer/services.ts index 9e7ba4e91f..75dad780e7 100644 --- a/apps/namadillo/src/atoms/transfer/services.ts +++ b/apps/namadillo/src/atoms/transfer/services.ts @@ -17,7 +17,8 @@ import BigNumber from "bignumber.js"; import * as Comlink from "comlink"; import { NamadaKeychain } from "hooks/useNamadaKeychain"; import { buildTx, EncodedTxData, isPublicKeyRevealed } from "lib/query"; -import { Address, ChainSettings, GasConfig } from "types"; +import { Address, ChainSettings, FrontendFee, GasConfig } from "types"; +import { frontendSusMsgFromConfig } from "utils/frontendFee"; import { getSdkInstance } from "utils/sdk"; import { IbcTransfer, @@ -168,6 +169,7 @@ export const createShieldingTransferTx = async ( props: ShieldingTransferProps[], gasConfig: GasConfig, rpcUrl: string, + frontendFee: FrontendFee, memo?: string ): Promise | undefined> => { const source = props[0]?.data[0]?.source; @@ -189,10 +191,16 @@ export const createShieldingTransferTx = async ( nativeToken: chain.nativeTokenAddress, buildTxFn: async (workerLink) => { const publicKeyRevealed = await isPublicKeyRevealed(account.address); + const frontendSusFee = frontendSusMsgFromConfig( + frontendFee, + token, + "transparent" + ); const msgValue: ShieldingTransferProps = { target: destination, data: [{ source, token, amount }], bparams, + frontendSusFee, }; const msg: Shield = { type: "shield", @@ -220,6 +228,7 @@ export const createUnshieldingTransferTx = async ( gasConfig: GasConfig, rpcUrl: string, disposableSigner: GenDisposableSignerResponse, + frontendFee: FrontendFee, memo?: string ): Promise | undefined> => { const { publicKey: signerPublicKey } = disposableSigner; @@ -243,10 +252,16 @@ export const createUnshieldingTransferTx = async ( rpcUrl, nativeToken: chain.nativeTokenAddress, buildTxFn: async (workerLink) => { + const frontendSusFee = frontendSusMsgFromConfig( + frontendFee, + token, + "transparent" + ); const msgValue: UnshieldingTransferProps = { source, data: [{ target: destination, token, amount }], bparams, + frontendSusFee, }; const msg: Unshield = { @@ -274,6 +289,7 @@ export const createIbcTx = async ( gasConfig: GasConfig, rpcUrl: string, signerPublicKey: string, + frontendFee: FrontendFee, memo?: string ): Promise> => { let bparams: BparamsMsgValue[] | undefined; @@ -288,10 +304,17 @@ export const createIbcTx = async ( rpcUrl, nativeToken: chain.nativeTokenAddress, buildTxFn: async (workerLink) => { + const firstProps = props[0]; + const isUnshielding = firstProps.gasSpendingKey === firstProps.source; + const frontendSusFee = + isUnshielding ? + frontendSusMsgFromConfig(frontendFee, firstProps.token, "transparent") + : undefined; const msgValue: IbcTransferProps = { - ...props[0], - gasSpendingKey: props[0].gasSpendingKey, + ...firstProps, + gasSpendingKey: firstProps.gasSpendingKey, bparams, + frontendSusFee, }; // We only check if we need to reveal the public key if the gas spending key is not provided diff --git a/apps/namadillo/src/hooks/useIbcTransaction.tsx b/apps/namadillo/src/hooks/useIbcTransaction.tsx index 8efbeb6ede..a6a4afc4bf 100644 --- a/apps/namadillo/src/hooks/useIbcTransaction.tsx +++ b/apps/namadillo/src/hooks/useIbcTransaction.tsx @@ -3,10 +3,10 @@ import { useMutation, UseMutationResult, useQuery, - UseQueryResult, } from "@tanstack/react-query"; import { TokenCurrency } from "App/Common/TokenCurrency"; import { chainParametersAtom } from "atoms/chain"; +import { frontendFeeAtom } from "atoms/fees"; import { broadcastIbcTransactionAtom, createStargateClient, @@ -20,6 +20,8 @@ import { dispatchToastNotificationAtom, } from "atoms/notifications"; import BigNumber from "bignumber.js"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/Option"; import invariant from "invariant"; import { useAtomValue, useSetAtom } from "jotai"; import { @@ -31,15 +33,20 @@ import { Address, Asset, ChainRegistryEntry, - GasConfig, IbcTransferStage, + NamadaAsset, TransferStep, TransferTransactionData, } from "types"; import { toBaseAmount } from "utils"; import { sanitizeAddress } from "utils/address"; +import { + calculateAmountWithFrontendFee, + getFrontendFeeEntry, +} from "utils/frontendFee"; import { getKeplrWallet, sanitizeChannel } from "utils/ibc"; import { useSimulateIbcTransferFee } from "./useSimulateIbcTransferFee"; +import { TransactionFeeProps } from "./useTransactionFee"; type useIbcTransactionProps = { sourceAddress?: string; @@ -48,10 +55,11 @@ type useIbcTransactionProps = { shielded?: boolean; destinationChannel?: Address; selectedAsset?: Asset; + amount?: BigNumber; }; type useIbcTransactionOutput = { - gasConfig: UseQueryResult; + transactionFeeProps?: TransactionFeeProps; transferToNamada: UseMutationResult< TransferTransactionData, Error, @@ -73,10 +81,12 @@ export const useIbcTransaction = ({ sourceChannel, shielded, destinationChannel, + amount, }: useIbcTransactionProps): useIbcTransactionOutput => { const broadcastIbcTx = useAtomValue(broadcastIbcTransactionAtom); const dispatchNotification = useSetAtom(dispatchToastNotificationAtom); const chainParameters = useAtomValue(chainParametersAtom); + const frontendFee = useAtomValue(frontendFeeAtom); const [txHash, setTxHash] = useState(); const [rpcUrl, setRpcUrl] = useState(); const [stargateClient, setStargateClient] = useState< @@ -102,6 +112,16 @@ export const useIbcTransaction = ({ }, }); + const baseAmount = + selectedAsset && amount && toBaseAmount(selectedAsset, amount); + const frontendFeeEntry = + selectedAsset && + getFrontendFeeEntry(frontendFee, (selectedAsset as NamadaAsset).address); + const amountWithFrontendFee = + baseAmount && + frontendFeeEntry && + calculateAmountWithFrontendFee(baseAmount, frontendFeeEntry); + const gasConfigQuery = useSimulateIbcTransferFee({ stargateClient, registry, @@ -109,6 +129,7 @@ export const useIbcTransaction = ({ isShieldedTransfer: shielded, sourceAddress, channel: sourceChannel, + amount: amountWithFrontendFee || baseAmount, }); const dispatchPendingTxNotification = (tx: TransferTransactionData): void => { @@ -189,6 +210,11 @@ export const useIbcTransaction = ({ gasConfigQuery.error?.message ); + const frontendFeeEntry = getFrontendFeeEntry( + frontendFee, + (selectedAsset as NamadaAsset).address + ); + const baseAmount = toBaseAmount(selectedAsset, displayAmount); const sourceChainAssets = @@ -208,11 +234,13 @@ export const useIbcTransaction = ({ const token = asset.traces?.find((trace) => trace.type === "ibc")?.chain.path || asset.base; + invariant(selectedAsset.address, "Asset address is required"); return shielded ? await getShieldedArgs( destinationAddress, token, + selectedAsset.address, baseAmount, destinationChannel! ) @@ -221,11 +249,25 @@ export const useIbcTransaction = ({ const chainId = registry.chain.chain_id; const denomination = asset.base; + const amount = pipe( + frontendFeeEntry, + O.fromNullable, + O.filter(() => !!shielded), + O.fold( + () => baseAmount, + (fee) => + toBaseAmount( + selectedAsset, + calculateAmountWithFrontendFee(BigNumber(displayAmount), fee) + ) + ) + ); + const transferMsg = createIbcTransferMessage( sanitizeChannel(sourceChannel!), sanitizeAddress(sourceAddress), sanitizeAddress(maspCompatibleReceiver), - baseAmount, + amount, denomination, maspCompatibleMemo ); @@ -250,7 +292,8 @@ export const useIbcTransaction = ({ chainId, destinationChainId || "", getIbcTransferStage(!!shielded), - !!shielded + !!shielded, + baseAmount ); dispatchPendingTxNotification(tx); setTxHash(tx.hash); @@ -268,8 +311,19 @@ export const useIbcTransaction = ({ mutationFn: transferToNamada, }); + const transactionFeeProps: TransactionFeeProps | undefined = + gasConfigQuery.data && { + gasConfig: gasConfigQuery.data, + isLoading: gasConfigQuery.isLoading, + onChangeGasLimit: () => {}, + onChangeGasToken: () => {}, + frontendFee, + gasEstimate: undefined, + gasPriceTable: undefined, + }; + return { transferToNamada: transferToNamadaQuery, - gasConfig: gasConfigQuery, + transactionFeeProps, }; }; diff --git a/apps/namadillo/src/hooks/useSimulateIbcTransferFee.ts b/apps/namadillo/src/hooks/useSimulateIbcTransferFee.ts index 219785d5ec..507560b3d6 100644 --- a/apps/namadillo/src/hooks/useSimulateIbcTransferFee.ts +++ b/apps/namadillo/src/hooks/useSimulateIbcTransferFee.ts @@ -17,6 +17,7 @@ type useSimulateIbcTransferFeeProps = { sourceAddress?: string; selectedAsset?: Asset; channel?: string; + amount?: BigNumber; }; export const useSimulateIbcTransferFee = ({ @@ -26,6 +27,7 @@ export const useSimulateIbcTransferFee = ({ isShieldedTransfer, sourceAddress, channel, + amount, }: useSimulateIbcTransferFeeProps): UseQueryResult => { return useQuery({ queryKey: [ @@ -33,6 +35,7 @@ export const useSimulateIbcTransferFee = ({ registry?.chain?.chain_id, selectedAsset?.base, isShieldedTransfer, + amount?.toString(), ], retry: false, queryFn: async () => { @@ -59,7 +62,7 @@ export const useSimulateIbcTransferFee = ({ // a valid address with funds sanitizeAddress(sourceAddress!), sanitizeAddress(sourceAddress!), - new BigNumber(1), + amount || BigNumber(1), getToken(), isShieldedTransfer ? "0".repeat(MASP_MEMO_LENGTH) : "" ); diff --git a/apps/namadillo/src/hooks/useTransaction.tsx b/apps/namadillo/src/hooks/useTransaction.tsx index ca9409c950..5916d6e0cf 100644 --- a/apps/namadillo/src/hooks/useTransaction.tsx +++ b/apps/namadillo/src/hooks/useTransaction.tsx @@ -147,6 +147,7 @@ export const useTransaction = ({ params, gasConfig: feeProps.gasConfig, account, + frontendFee: feeProps.frontendFee, ...txAdditionalParams, }; const encodedTxData = await performBuildTx(variables); diff --git a/apps/namadillo/src/hooks/useTransactionFee/useTransactionFee.ts b/apps/namadillo/src/hooks/useTransactionFee/useTransactionFee.ts index c2814193f7..ffc5e1ead0 100644 --- a/apps/namadillo/src/hooks/useTransactionFee/useTransactionFee.ts +++ b/apps/namadillo/src/hooks/useTransactionFee/useTransactionFee.ts @@ -3,6 +3,7 @@ import { transparentBalanceAtom } from "atoms/accounts"; import { shieldedBalanceAtom } from "atoms/balance"; import { nativeTokenAddressAtom } from "atoms/chain"; import { + frontendFeeAtom, gasEstimateFamily, GasPriceTable, gasPriceTableAtom, @@ -13,7 +14,7 @@ import BigNumber from "bignumber.js"; import invariant from "invariant"; import { useAtomValue } from "jotai"; import { useMemo, useState } from "react"; -import { GasConfig } from "types"; +import { FrontendFee, GasConfig } from "types"; import { TxKind } from "types/txKind"; import { findCheapestToken } from "./internal"; @@ -24,6 +25,7 @@ export type TransactionFeeProps = { gasPriceTable: GasPriceTable | undefined; onChangeGasLimit: (value: BigNumber) => void; onChangeGasToken: (value: string) => void; + frontendFee: FrontendFee; }; export const useTransactionFee = ( @@ -35,6 +37,7 @@ export const useTransactionFee = ( const userTransparentBalances = useAtomValue(transparentBalanceAtom); const userShieldedBalances = useAtomValue(shieldedBalanceAtom); const isPublicKeyRevealed = useAtomValue(isPublicKeyRevealedAtom); + const frontendFee = useAtomValue(frontendFeeAtom); const { data: nativeToken, isLoading: isLoadingNativeToken } = useAtomValue( nativeTokenAddressAtom @@ -145,6 +148,7 @@ export const useTransactionFee = ( return { gasConfig, + frontendFee, isLoading, gasEstimate, gasPriceTable, diff --git a/apps/namadillo/src/lib/transactions.ts b/apps/namadillo/src/lib/transactions.ts index 0f41403d36..c2202827f4 100644 --- a/apps/namadillo/src/lib/transactions.ts +++ b/apps/namadillo/src/lib/transactions.ts @@ -139,18 +139,24 @@ export const createTransferDataFromIbc = ( sourceChainId: string, destinationChainId: string, details: IbcTransferStage, - isShieldedTx: boolean + isShieldedTx: boolean, + // It's needed to calculate real amount if frontend fee is used + declaratedBaseAmount: BigNumber ): TransferTransactionData => { const transferAttributes = getIbcTransferAttributes(tx); const packetAttributes = getEventAttribute(tx, "send_packet"); const feeAttributes = getEventAttribute(tx, "fee_pay"); const tipAttributes = getEventAttribute(tx, "tip_pay"); const baseDenom = asset.base; - - const transferAmount = toDisplayAmount( - asset, - getAmountAttributeValue(transferAttributes, "amount", baseDenom) + const attrAmount = getAmountAttributeValue( + transferAttributes, + "amount", + baseDenom ); + const baseAmount = + attrAmount.eq(declaratedBaseAmount) ? attrAmount : declaratedBaseAmount; + + const transferAmount = toDisplayAmount(asset, baseAmount); const tipPaid = toDisplayAmount( asset, diff --git a/apps/namadillo/src/types.ts b/apps/namadillo/src/types.ts index b02739eee2..151c3e8587 100644 --- a/apps/namadillo/src/types.ts +++ b/apps/namadillo/src/types.ts @@ -26,6 +26,8 @@ type Unique = { export type PublicKey = string; export type Address = string; +export type TokenAddress = Address; +export type PaymentAddress = string; export type BaseDenom = string; @@ -46,6 +48,20 @@ export type GasConfig = { gasToken: GasToken; }; +export type FrontendFee = Record< + TokenAddress, + { + transparentTarget: Address; + shieldedTarget: PaymentAddress; + percentage: BigNumber; + } +>; +export type FrontendFeeEntry = { + transparentTarget: Address; + shieldedTarget: PaymentAddress; + percentage: BigNumber; +}; + export type GasConfigToDisplay = { totalDisplayAmount: BigNumber; asset: Asset; @@ -71,6 +87,14 @@ export type SettingsTomlOptions = { rpc_url?: string; localnet_enabled?: boolean; fathom_site_id?: string; + frontend_fee?: Record< + string, + { + transparent_target: string; + shielded_target: string; + percentage: number; + } + >; }; export type ChainParameters = { @@ -184,6 +208,7 @@ export type BuildTxAtomParams = { gasConfig: GasConfig; memo?: string; signer?: Signer; + frontendFee: FrontendFee; }; export type SortOptions = "asc" | "desc" | undefined; diff --git a/apps/namadillo/src/utils/frontendFee.ts b/apps/namadillo/src/utils/frontendFee.ts new file mode 100644 index 0000000000..b5ffa2b055 --- /dev/null +++ b/apps/namadillo/src/utils/frontendFee.ts @@ -0,0 +1,65 @@ +import { FrontendSusFeeProps } from "@namada/sdk-multicore"; +import { assertNever } from "@namada/utils"; +import BigNumber from "bignumber.js"; +import { FrontendFee, FrontendFeeEntry } from "types"; + +export const getFrontendFeeEntry = ( + frontendFee: FrontendFee, + address: string +): FrontendFeeEntry | undefined => { + return frontendFee[address] || frontendFee["*"]; +}; + +export const frontendSusMsgFromConfig = ( + frontendFee: FrontendFee, + token: string, + whichTarget: "shielded" | "transparent" +): FrontendSusFeeProps | undefined => { + const entry = getFrontendFeeEntry(frontendFee, token); + if (!entry) return; + + const { percentage, shieldedTarget, transparentTarget } = entry; + + const target = + whichTarget === "shielded" ? shieldedTarget + : whichTarget === "transparent" ? transparentTarget + : assertNever(whichTarget); + + const frontendSusFee = { + percentage: percentage, + target, + }; + + return frontendSusFee; +}; + +export const calculateAmountWithoutFrontendFee = ( + displayAmount: BigNumber, + frontendFee: FrontendFeeEntry +): BigNumber => { + return ( + displayAmount + .div(frontendFee.percentage.plus(1)) + // We have to round DOWN here as sdk rounds UP when calculating the fee. + // Otherwise we might end up with remaining dust. + .decimalPlaces(6, BigNumber.ROUND_DOWN) + ); +}; + +export const calculateAmountWithFrontendFee = ( + displayAmount: BigNumber, + frontendFee: FrontendFeeEntry +): BigNumber => { + return displayAmount + .multipliedBy(frontendFee.percentage.plus(1)) + .decimalPlaces(6, BigNumber.ROUND_UP); +}; + +export const calculateFrontendFeeAmount = ( + displayAmount: BigNumber, + frontendFee: FrontendFeeEntry +): BigNumber => { + return displayAmount + .multipliedBy(frontendFee.percentage) + .decimalPlaces(6, BigNumber.ROUND_UP); +}; diff --git a/apps/namadillo/src/utils/gas.ts b/apps/namadillo/src/utils/gas.ts index 270be10512..e780baf928 100644 --- a/apps/namadillo/src/utils/gas.ts +++ b/apps/namadillo/src/utils/gas.ts @@ -1,6 +1,5 @@ import { isTransparentAddress } from "App/Transfer/common"; import BigNumber from "bignumber.js"; -import namadaAssets from "chain-registry/mainnet/namada/assets"; import { Address, Asset, GasConfig, GasConfigToDisplay } from "types"; import { isNamadaAsset, toDisplayAmount } from "utils"; import { unknownAsset } from "./assets"; @@ -28,7 +27,7 @@ export const getDisplayGasFee = ( // However, if the gasConfig contains a token used by Keplr, it could be the asset // denomination unit, like "uosmo" asset = - namadaAssets.assets.find((a) => + Object.values(chainAssetsMap).find((a) => a.denom_units.some((d) => d.aliases?.some((alias) => alias === gasToken) ) diff --git a/apps/namadillo/src/workers/MaspTxMessages.ts b/apps/namadillo/src/workers/MaspTxMessages.ts index 5860139d3a..47c0afb758 100644 --- a/apps/namadillo/src/workers/MaspTxMessages.ts +++ b/apps/namadillo/src/workers/MaspTxMessages.ts @@ -1,4 +1,5 @@ import { + FrontendSusFeeProps, IbcTransferProps, SdkWasmOptions, ShieldedTransferProps, @@ -93,6 +94,7 @@ type GenerateIbcShieldingMemoPayload = { amount: BigNumber; destinationChannelId: string; chainId: string; + frontendSusFee?: FrontendSusFeeProps; }; export type GenerateIbcShieldingMemo = WebWorkerMessage< "generate-ibc-shielding-memo", diff --git a/apps/namadillo/src/workers/MaspTxWorker.ts b/apps/namadillo/src/workers/MaspTxWorker.ts index 2e7375dcb7..ae6d223b1e 100644 --- a/apps/namadillo/src/workers/MaspTxWorker.ts +++ b/apps/namadillo/src/workers/MaspTxWorker.ts @@ -1,4 +1,5 @@ import { + GenerateIbcShieldingMemoProps, IbcTransferProps, Sdk, ShieldedTransferProps, @@ -260,14 +261,26 @@ async function generateIbcShieldingMemo( sdk: Sdk, payload: GenerateIbcShieldingMemo["payload"] ): Promise { - const { target, token, amount, destinationChannelId, chainId } = payload; + const { + target, + token, + amount, + destinationChannelId, + chainId, + frontendSusFee, + } = payload; await sdk.masp.loadMaspParams("", chainId); - const memo = await sdk.tx.generateIbcShieldingMemo( + const generateIbcShieldingMemoProps: GenerateIbcShieldingMemoProps = { target, token, amount, - destinationChannelId + channelId: destinationChannelId, + frontendSusFee, + }; + + const memo = await sdk.tx.generateIbcShieldingMemo( + generateIbcShieldingMemoProps ); return memo; diff --git a/yarn.lock b/yarn.lock index d643dcfe3c..091acadd2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3585,8 +3585,8 @@ __metadata: "@dao-xyz/borsh": "npm:^5.1.5" "@ledgerhq/hw-transport": "npm:^6.31.4" "@ledgerhq/hw-transport-webusb": "npm:^6.29.4" - "@namada/sdk": "npm:0.23.0" - "@namada/sdk-node": "npm:0.23.0" + "@namada/sdk": "npm:0.24.0-beta.2" + "@namada/sdk-node": "npm:0.24.0-beta.1" "@svgr/webpack": "npm:^6.3.1" "@types/chrome": "npm:^0.0.237" "@types/firefox-webext-browser": "npm:^94.0.1" @@ -3772,8 +3772,8 @@ __metadata: "@keplr-wallet/types": "npm:^0.12.136" "@namada/chain-registry": "npm:^1.5.2" "@namada/indexer-client": "npm:4.0.5" - "@namada/sdk-multicore": "npm:0.23.0" - "@namada/sdk-node": "npm:0.23.0" + "@namada/sdk-multicore": "npm:0.24.0-beta.2" + "@namada/sdk-node": "npm:0.24.0-beta.2" "@namada/vite-esbuild-plugin": "npm:^1.0.1" "@playwright/test": "npm:^1.24.1" "@svgr/webpack": "npm:^6.5.1" @@ -3858,9 +3858,9 @@ __metadata: languageName: unknown linkType: soft -"@namada/sdk-multicore@npm:0.23.0": - version: 0.23.0 - resolution: "@namada/sdk-multicore@npm:0.23.0" +"@namada/sdk-multicore@npm:0.24.0-beta.2": + version: 0.24.0-beta.2 + resolution: "@namada/sdk-multicore@npm:0.24.0-beta.2" dependencies: "@cosmjs/encoding": "npm:^0.29.0" "@dao-xyz/borsh": "npm:^5.1.5" @@ -3871,13 +3871,13 @@ __metadata: buffer: "npm:^6.0.3" semver: "npm:^7.7.2" slip44: "npm:^3.0.18" - checksum: 10c0/435a2a08f4abbffbc62ad1f164f8ca5d9700eec53a4930508f73dff09ddcfc06081f6bad1fe6811d18d5ad9695ed2237a559b395eed907327c9a7f955a454723 + checksum: 10c0/0892ab4f9bc5c715bd016262fac06051c175d16b0b60f6cb71de22f4ec1661f8623cf2d1b882df1ee496c28ab76fd5531b58549318c2f485393cf65bd1ffb157 languageName: node linkType: hard -"@namada/sdk-node@npm:0.23.0": - version: 0.23.0 - resolution: "@namada/sdk-node@npm:0.23.0" +"@namada/sdk-node@npm:0.24.0-beta.1": + version: 0.24.0-beta.1 + resolution: "@namada/sdk-node@npm:0.24.0-beta.1" dependencies: "@cosmjs/encoding": "npm:^0.29.0" "@dao-xyz/borsh": "npm:^5.1.5" @@ -3888,13 +3888,30 @@ __metadata: buffer: "npm:^6.0.3" semver: "npm:^7.7.2" slip44: "npm:^3.0.18" - checksum: 10c0/2b4d1458cd7b3de2420f20019d45a80e81d57008cfe139b79f94eaca031e2afd0cfdebbd29bfa8d2e42403047ee9e4668d47c00de8040694aa557774ffe4b244 + checksum: 10c0/c1e1a9acf31af3e8e4cc53944fe44155d33d6ed83651c1699b32e732e98dc483212beaa86634679a1c7fbb538ec3d4c452a52bc9e07fe23f841ae930bf659cac languageName: node linkType: hard -"@namada/sdk@npm:0.23.0": - version: 0.23.0 - resolution: "@namada/sdk@npm:0.23.0" +"@namada/sdk-node@npm:0.24.0-beta.2": + version: 0.24.0-beta.2 + resolution: "@namada/sdk-node@npm:0.24.0-beta.2" + dependencies: + "@cosmjs/encoding": "npm:^0.29.0" + "@dao-xyz/borsh": "npm:^5.1.5" + "@ledgerhq/hw-transport": "npm:^6.31.4" + "@ledgerhq/hw-transport-webusb": "npm:^6.29.4" + "@zondax/ledger-namada": "npm:^2.0.0" + bignumber.js: "npm:^9.1.1" + buffer: "npm:^6.0.3" + semver: "npm:^7.7.2" + slip44: "npm:^3.0.18" + checksum: 10c0/929f164939308efa9260c0291e0260854dfcea8397b76024c219f9258a117367d1eff88464cf73460ffec4847506400a861530671e6ecae92c7777567438f69e + languageName: node + linkType: hard + +"@namada/sdk@npm:0.24.0-beta.2": + version: 0.24.0-beta.2 + resolution: "@namada/sdk@npm:0.24.0-beta.2" dependencies: "@cosmjs/encoding": "npm:^0.29.0" "@dao-xyz/borsh": "npm:^5.1.5" @@ -3905,7 +3922,7 @@ __metadata: buffer: "npm:^6.0.3" semver: "npm:^7.6.3" slip44: "npm:^3.0.18" - checksum: 10c0/e90983c4f1d3544fd58f8c85dd24419693e9ad2f62e7630680d5b93a7239a1dd82ac41de8ee8ebcf8122c2a808638c549da1db5721d18adaa50be1a3da4fce3c + checksum: 10c0/d447cb247770e3cca5cb54435f5cf6a4978252ded7e4ab933a28ceb30d0e0edf058c56ad3a25b58c3700298ed8b5dbfd8f0fdb6021b1427ef93a17cd6e8ff3fc languageName: node linkType: hard