From 256ab2ed52304a0953ae4ace3ee3f74fc0a59a3d Mon Sep 17 00:00:00 2001 From: bry Date: Fri, 19 Jul 2024 16:12:57 -0500 Subject: [PATCH 1/7] WIP --- ios/Podfile.lock | 4 +- src/hooks/useSimulatedTransaction.ts | 341 ------------------ src/hooks/useSubmitTxn.ts | 22 +- src/solana/WalletSIgnBottomSheetSimulated.tsx | 277 ++++++++++++++ src/solana/WalletSignBottomSheet.tsx | 262 +++++++------- src/solana/WalletSignBottomSheetCompact.tsx | 44 +++ src/solana/walletSignBottomSheetTypes.tsx | 26 +- 7 files changed, 477 insertions(+), 499 deletions(-) delete mode 100644 src/hooks/useSimulatedTransaction.ts create mode 100644 src/solana/WalletSIgnBottomSheetSimulated.tsx create mode 100644 src/solana/WalletSignBottomSheetCompact.tsx diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fe8991b43..a20062ed9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -59,7 +59,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.71.5) - fmt (6.2.1) - glog (0.3.5) - - helium-react-native-sdk (3.0.4): + - helium-react-native-sdk (3.0.5): - React-Core - hermes-engine (0.71.5): - hermes-engine/Pre-built (= 0.71.5) @@ -858,7 +858,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: 627fd07f1b9d498c9fa572e76d7f1a6b1ee9a444 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - helium-react-native-sdk: bd924fa4ff053a6afb2b2febf03c3dfacd7dd671 + helium-react-native-sdk: 54928dcd95ea131437ac6e269cf5bb2d473bf57f hermes-engine: 0784cadad14b011580615c496f77e0ae112eed75 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 lottie-ios: e047b1d2e6239b787cc5e9755b988869cf190494 diff --git a/src/hooks/useSimulatedTransaction.ts b/src/hooks/useSimulatedTransaction.ts deleted file mode 100644 index 398f3dde9..000000000 --- a/src/hooks/useSimulatedTransaction.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { useSolOwnedAmount } from '@helium/helium-react-hooks' -import { toNumber, truthy } from '@helium/spl-utils' -import { AccountLayout, NATIVE_MINT, TOKEN_PROGRAM_ID } from '@solana/spl-token' -import { - AddressLookupTableAccount, - Connection, - ParsedAccountData, - PublicKey, - SimulatedTransactionAccountInfo, - SystemProgram, - VersionedTransaction, -} from '@solana/web3.js' -import { useAppStorage } from '@storage/AppStorageProvider' -import { useModal } from '@storage/ModalsProvider' -import { useBalance } from '@utils/Balance' -import { MIN_BALANCE_THRESHOLD } from '@utils/constants' -import { getCollectableByMint, isInsufficientBal } from '@utils/solanaUtils' -import BN from 'bn.js' -import { useMemo, useState } from 'react' -import { useAsync } from 'react-async-hook' -import { useSolana } from '../solana/SolanaProvider' -import * as logger from '../utils/logger' -import { useBN } from './useBN' - -type BalanceChange = { - nativeChange?: number - mint?: PublicKey - symbol?: string - type?: 'send' | 'receive' -} -type BalanceChanges = BalanceChange[] | null - -export type SimulatedTransactionResult = { - loading: boolean - solFee?: number - priorityFee?: number - estimateFeeErr?: Error | undefined - simulationError: boolean - insufficientFunds: boolean - balanceChanges?: BalanceChanges -} -export function useSimulatedTransaction( - serializedTx: Buffer | undefined, - wallet: PublicKey | undefined, -): SimulatedTransactionResult { - const { connection, anchorProvider } = useSolana() - const { tokenAccounts } = useBalance() - const { showModal } = useModal() - const solBalance = useBN(useSolOwnedAmount(wallet).amount) - const hasEnoughSol = useMemo(() => { - return (solBalance || new BN(0)).gt(new BN(MIN_BALANCE_THRESHOLD)) - }, [solBalance]) - const { autoGasManagementToken } = useAppStorage() - - const [simulationError, setSimulationError] = useState(false) - const [insufficientFunds, setInsufficientFunds] = useState(false) - - const transaction = useMemo(() => { - if (!serializedTx) return undefined - try { - const tx = VersionedTransaction.deserialize(serializedTx) - return tx - } catch (err) { - logger.error(err) - } - }, [serializedTx]) - - const { - result: { solFee, priorityFee } = { solFee: 5000, priorityFee: 0 }, - loading: loadingFee, - error: estimateFeeErr, - } = useAsync( - async ( - c: Connection | undefined, - t: VersionedTransaction | undefined, - ): Promise<{ solFee: number; priorityFee: number }> => { - const sFee = (t?.signatures.length || 1) * 5000 - let pFee = 0 - - if (!c || !t) { - return Promise.resolve({ solFee: sFee, priorityFee: pFee }) - } - - try { - const fee = - (await c?.getFeeForMessage(t.message, 'confirmed')).value || solFee - pFee = fee - sFee - return { - priorityFee: pFee, - solFee: sFee, - } - } catch (err) { - logger.error(err) - } - - return { solFee: sFee, priorityFee: pFee } - }, - [connection, transaction], - ) - - const { loading: loadingSim, result: simulatedTxnResult } = - useAsync(async () => { - if (!connection || !transaction || !wallet) return undefined - - setSimulationError(false) - setInsufficientFunds(false) - - try { - const addressLookupTableAccounts: Array = [] - const { addressTableLookups } = transaction.message - if (addressTableLookups.length > 0) { - // eslint-disable-next-line no-restricted-syntax - for (const addressTableLookup of addressTableLookups) { - // eslint-disable-next-line no-await-in-loop - const result = await connection?.getAddressLookupTable( - addressTableLookup.accountKey, - ) - if (result?.value) { - addressLookupTableAccounts.push(result?.value) - } - } - } - const accountKeys = transaction.message.getAccountKeys({ - addressLookupTableAccounts, - }) - - const simulationAccounts = [ - ...new Set( - accountKeys.staticAccountKeys.concat( - accountKeys.accountKeysFromLookups - ? // Only writable accounts will contribute to balance changes - accountKeys.accountKeysFromLookups.writable - : [], - ), - ), - ] - - const { blockhash } = await connection?.getLatestBlockhash() - transaction.message.recentBlockhash = blockhash - return { - simulationAccounts, - simulatedTxn: await connection?.simulateTransaction(transaction, { - accounts: { - encoding: 'base64', - addresses: - simulationAccounts?.map((account) => account.toBase58()) || [], - }, - }), - } - } catch (err) { - console.warn('err', err) - return undefined - } - }, [connection, transaction, anchorProvider, wallet]) - - const { loading: loadingBal, result: estimatedBalanceChanges } = - useAsync(async () => { - if (!simulatedTxnResult || !tokenAccounts || !connection || !wallet) { - return - } - try { - const { simulatedTxn: result, simulationAccounts } = simulatedTxnResult - if (result?.value.err) { - console.warn('failed to simulate', result?.value.err) - console.warn(result?.value.logs?.join('\n')) - if (!hasEnoughSol || isInsufficientBal(result?.value.err)) { - if (!hasEnoughSol && !autoGasManagementToken) { - showModal({ - type: 'InsufficientSolConversion', - onCancel: async () => { - setInsufficientFunds(true) - }, - onSuccess: async () => { - setInsufficientFunds(false) - setSimulationError(false) - }, - }) - } else { - setInsufficientFunds(true) - } - } - setSimulationError(true) - return undefined - } - - const accounts = result?.value.accounts - - if (!accounts) return undefined - - const balanceChanges = await Promise.all( - accounts.map( - async ( - account: SimulatedTransactionAccountInfo | null, - index: number, - ) => { - if (!account) return null - - // Token changes - const isToken = account.owner === TOKEN_PROGRAM_ID.toString() - const isNativeSol = - account.owner === SystemProgram.programId.toBase58() - - if (isToken || isNativeSol) { - try { - let accountNativeBalance: BN - let tokenMint: PublicKey - let existingNativeBalance: BN - - // Parse token accounts for change in balances - if (isToken) { - try { - const tokenAccount = AccountLayout.decode( - Buffer.from( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - account.data[0] as any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - account.data[1] as any, - ), - ) - - if (!new PublicKey(tokenAccount.owner).equals(wallet)) { - return null - } - accountNativeBalance = new BN( - tokenAccount.amount.toString(), - ) - // Standard token mint - tokenMint = new PublicKey(tokenAccount.mint) - } catch (error) { - // Decoding of token account failed, not a token account - return null - } - - // Find the existing token account - const existingTokenAccount = tokenAccounts.find((t) => - new PublicKey(t.mint).equals(tokenMint), - ) - - existingNativeBalance = existingTokenAccount - ? new BN(existingTokenAccount.balance) - : new BN(0) - } else { - // Not interested in SOL balance changes for accounts that - // are not the current address - if ( - simulationAccounts && - !simulationAccounts[index].equals(wallet) - ) { - return null - } - accountNativeBalance = new BN(account.lamports.toString()) - // Faux mint for native SOL - tokenMint = new PublicKey(NATIVE_MINT) - existingNativeBalance = new BN( - ( - await connection.getAccountInfo( - simulationAccounts[index], - ) - )?.lamports || 0, - ) - // Don't include fees here - // First account is feePayer, if we made it here feePayer is wallet - if (index === 0) { - accountNativeBalance = accountNativeBalance.add( - new BN(solFee || 5000), - ) - } - } - - const token = await connection.getParsedAccountInfo(tokenMint) - - const { decimals } = (token.value?.data as ParsedAccountData) - .parsed.info - - const tokenMetadata = await getCollectableByMint( - tokenMint, - connection, - ) - - const type = accountNativeBalance.lt(existingNativeBalance) - ? 'send' - : 'receive' - - // Filter out zero change - if (!accountNativeBalance.eq(existingNativeBalance)) { - let nativeChange: BN - if (type === 'send') { - nativeChange = - existingNativeBalance.sub(accountNativeBalance) - } else { - nativeChange = accountNativeBalance.sub( - existingNativeBalance, - ) - } - return { - nativeChange: Math.abs(toNumber(nativeChange, decimals)), - decimals, - mint: tokenMint, - symbol: tokenMint.equals(NATIVE_MINT) - ? 'SOL' - : tokenMetadata?.symbol, - type, - } as BalanceChange - } - } catch (err) { - // ignore, probably not a token account or some other - // failure, we don't want to fail displaying the popup - console.warn('failed to get balance changes', err) - return null - } - } - return null - }, - ), - ) - - return balanceChanges.filter(truthy) - } catch (err) { - console.warn('err', err) - return undefined - } - }, [ - simulatedTxnResult, - connection, - tokenAccounts, - hasEnoughSol, - anchorProvider, - wallet, - autoGasManagementToken, - ]) - - return { - loading: loadingBal || loadingFee || loadingSim, - simulationError, - insufficientFunds, - balanceChanges: estimatedBalanceChanges, - solFee: solFee || 5000, - priorityFee, - estimateFeeErr, - } -} diff --git a/src/hooks/useSubmitTxn.ts b/src/hooks/useSubmitTxn.ts index cc6c3bd62..589f7ed8d 100644 --- a/src/hooks/useSubmitTxn.ts +++ b/src/hooks/useSubmitTxn.ts @@ -75,13 +75,20 @@ export default () => { }), ) - const decision = await walletSignBottomSheetRef.show({ - type: WalletStandardMessageTypes.signTransaction, - url: '', - additionalMessage: t('transactions.signPaymentTxn'), - serializedTxs: txns.map((tx) => - Buffer.from(toVersionedTx(tx).serialize()), - ), + let decision + decision = await walletSignBottomSheetRef.show({ + header: 'Send Tokens', + message: t('transactions.signPaymentTxn'), + onSimulate: async () => { + decision = await walletSignBottomSheetRef.show({ + type: WalletStandardMessageTypes.signTransaction, + url: '', + additionalMessage: t('transactions.signPaymentTxn'), + serializedTxs: txns.map((tx) => + Buffer.from(toVersionedTx(tx).serialize()), + ), + }) + }, }) if (!decision) { @@ -490,6 +497,7 @@ export default () => { maker: { address: currentAccount.address, }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, }) diff --git a/src/solana/WalletSIgnBottomSheetSimulated.tsx b/src/solana/WalletSIgnBottomSheetSimulated.tsx new file mode 100644 index 000000000..ed58870e3 --- /dev/null +++ b/src/solana/WalletSIgnBottomSheetSimulated.tsx @@ -0,0 +1,277 @@ +import CancelIcon from '@assets/images/remixCancel.svg' +import Box from '@components/Box' +import SubmitButton from '@components/SubmitButton' +import Text from '@components/Text' +import { useSolOwnedAmount } from '@helium/helium-react-hooks' +import { useBN } from '@hooks/useBN' +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { useRentExempt } from '@hooks/useRentExempt' +import axios from 'axios' +import React, { ReactNode, useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useAsync } from 'react-async-hook' +import { DAO_KEY } from '@utils/constants' +import { entityCreatorKey } from '@helium/helium-entity-manager-sdk' +import { humanReadable } from '@helium/spl-utils' +import BN from 'bn.js' +import { useSolana } from './SolanaProvider' +import { + WalletSignOptsCommon, + WalletSignOptsSimulated, +} from './walletSignBottomSheetTypes' + +interface IWalletSignBottomSheetSimulatedProps + extends WalletSignOptsCommon, + WalletSignOptsSimulated { + children?: ReactNode +} +const WELL_KNOWN_CANOPY_URL = + 'https://shdw-drive.genesysgo.net/6tcnBSybPG7piEDShBcrVtYJDPSvGrDbVvXmXKpzBvWP/merkles.json' +let wellKnownCanopyCache: Record | undefined + +export const WalletSignBottomSheetSimulated = ({ + url, + type, + header, + warning, + serializedTxs, + suppressWarnings, + additionalMessage, + onCancelHandler, + onAcceptHandler, + children, +}: IWalletSignBottomSheetSimulatedProps) => { + const { t } = useTranslation() + const { connection, cluster } = useSolana() + const wallet = useCurrentWallet() + const [feesExpanded, setFeesExpanded] = useState(false) + const solBalance = useBN(useSolOwnedAmount(wallet).amount) + const { rentExempt } = useRentExempt() + + const [infoVisible, setInfoVisible] = useState(false) + const [writableInfoVisible, setWritableInfoVisible] = useState(false) + const [feesExpanded, setFeesExpanded] = useState(false) + const [currentPage, setCurrentPage] = useState(1) + + const { result: accountBlacklist } = useAsync(async () => { + if (!wellKnownCanopyCache) + wellKnownCanopyCache = await (await axios.get(WELL_KNOWN_CANOPY_URL)).data + + if (wellKnownCanopyCache) { + return new Set(Object.keys(wellKnownCanopyCache)) + } + + return new Set([]) + }, []) + + const { + result: simulationResults, + error, + loading, + } = useAsync(async () => { + if ( + connection && + wallet && + walletSignOpts.serializedTxs && + accountBlacklist + ) { + return sus({ + connection, + wallet, + serializedTransactions: walletSignOpts.serializedTxs, + checkCNfts: true, + cluster, + extraSearchAssetParams: { + creatorVerified: true, + creatorAddress: entityCreatorKey(DAO_KEY)[0].toBase58(), + }, + accountBlacklist, + }) + } + }, [cluster, connection, wallet, accountBlacklist, serializedTxs]) + + const [currentTxs, hasMore] = useMemo(() => { + if (!simulationResults) return [[], false] + + let scopedTxs + const itemsPerPage = 5 + const pages = Math.ceil((simulationResults.length || 0) / itemsPerPage) + const more = currentPage < pages + + if (simulationResults) { + const endIndex = currentPage * itemsPerPage + scopedTxs = simulationResults.slice(0, endIndex) + } + + return [scopedTxs, more] + }, [simulationResults, currentPage]) + + const handleLoadMore = useCallback(() => { + setCurrentPage((page) => page + 1) + }, [setCurrentPage]) + + const estimatedTotalLamports = useMemo( + () => + simulationResults?.reduce( + (a, b) => a + b.solFee + (b.priorityFee || 0), + 0, + ) || 0, + [simulationResults], + ) + + const estimatedTotalSol = useMemo( + () => (loading ? '...' : humanReadable(new BN(estimatedTotalLamports), 9)), + [estimatedTotalLamports, loading], + ) + + const estimatedTotalBaseFee = useMemo( + () => + humanReadable( + new BN(simulationResults?.reduce((a, b) => a + b.solFee, 0) || 0), + 9, + ), + [simulationResults], + ) + + const estimatedTotalPriorityFee = useMemo( + () => + humanReadable( + new BN( + simulationResults?.reduce((a, b) => a + (b.priorityFee || 0), 0) || 0, + ), + 9, + ), + [simulationResults], + ) + + const totalWarnings = useMemo( + () => + simulationResults?.reduce( + (a, b) => + a + + (b.warnings.filter((w) => w.severity === 'critical').length > 0 + ? 1 + : 0), + 0, + ), + [simulationResults], + ) + + const worstSeverity = useMemo(() => { + if (simulationResults) { + return simulationResults?.reduce((a, b) => { + if (a === 'critical') { + return 'critical' + } + if (b.warnings.some((w) => w.severity === 'critical')) { + return 'critical' + } + + return a + }, 'warning') + } + return 'critical' + }, [simulationResults]) + + const insufficientRentExempt = useMemo(() => { + if (solBalance) { + return new BN(solBalance.toString()) + .sub(new BN(estimatedTotalLamports)) + .lt(new BN(rentExempt || 0)) + } + }, [solBalance, estimatedTotalLamports, rentExempt]) + + const insufficientFunds = useMemo( + () => + new BN(estimatedTotalLamports).gt( + new BN(solBalance?.toString() || '0'), + ) || simulationResults?.some((r) => r.insufficientFunds), + [solBalance, estimatedTotalLamports, simulationResults], + ) + + const showWarnings = + totalWarnings && !suppressWarnings && worstSeverity === 'critical' + + return ( + + + {header} + + {children} + {showWarnings ? ( + + + + + + + ) : ( + + + + + + )} + + ) +} diff --git a/src/solana/WalletSignBottomSheet.tsx b/src/solana/WalletSignBottomSheet.tsx index 15f9f123a..2346fee4e 100644 --- a/src/solana/WalletSignBottomSheet.tsx +++ b/src/solana/WalletSignBottomSheet.tsx @@ -52,11 +52,13 @@ import { WalletSignOpts, WalletStandardMessageTypes, } from './walletSignBottomSheetTypes' - -let promiseResolve: (value: boolean | PromiseLike) => void +import { WalletSignBottomSheetCompact } from './WalletSignBottomSheetCompact' +import { WalletSignBottomSheetSimulated } from './WalletSIgnBottomSheetSimulated' const WELL_KNOWN_CANOPY_URL = 'https://shdw-drive.genesysgo.net/6tcnBSybPG7piEDShBcrVtYJDPSvGrDbVvXmXKpzBvWP/merkles.json' + +let promiseResolve: (value: boolean | PromiseLike) => void let wellKnownCanopyCache: Record | undefined const WalletSignBottomSheet = forwardRef( @@ -65,16 +67,21 @@ const WalletSignBottomSheet = forwardRef( ref: Ref, ) => { useImperativeHandle(ref, () => ({ show, hide })) - const { rentExempt } = useRentExempt() - const { backgroundStyle } = useOpacity('surfaceSecondary', 1) - const { secondaryText } = useColors() const { t } = useTranslation() + const { connection, cluster } = useSolana() const wallet = useCurrentWallet() + const { secondaryText } = useColors() + const { backgroundStyle } = useOpacity('surfaceSecondary', 1) + const animatedContentHeight = useSharedValue(0) const solBalance = useBN(useSolOwnedAmount(wallet).amount) + const { rentExempt } = useRentExempt() + const bottomSheetModalRef = useRef(null) const [isVisible, setIsVisible] = useState(false) const [infoVisible, setInfoVisible] = useState(false) const [writableInfoVisible, setWritableInfoVisible] = useState(false) + const [feesExpanded, setFeesExpanded] = useState(false) + const [currentPage, setCurrentPage] = useState(1) const [walletSignOpts, setWalletSignOpts] = useState({ type: WalletStandardMessageTypes.connect, url: '', @@ -83,7 +90,11 @@ const WalletSignBottomSheet = forwardRef( header: undefined, suppressWarnings: false, }) - const { connection, cluster } = useSolana() + + useEffect(() => { + bottomSheetModalRef.current?.present() + }, [bottomSheetModalRef]) + const { result: accountBlacklist } = useAsync(async () => { if (!wellKnownCanopyCache) wellKnownCanopyCache = await ( @@ -96,11 +107,7 @@ const WalletSignBottomSheet = forwardRef( return new Set([]) }, []) - const [feesExpanded, setFeesExpanded] = useState(false) - const Chevron = feesExpanded ? ChevronUp : ChevronDown - const itemsPerPage = 5 - const [currentPage, setCurrentPage] = useState(1) const { result: simulationResults, error, @@ -132,22 +139,21 @@ const WalletSignBottomSheet = forwardRef( wallet, accountBlacklist, ]) + const [currentTxs, hasMore] = useMemo(() => { + if (!simulationResults) return [[], false] + + let scopedTxs + const itemsPerPage = 5 + const pages = Math.ceil((simulationResults.length || 0) / itemsPerPage) + const more = currentPage < pages + if (simulationResults) { - const totalPages = Math.ceil( - (simulationResults.length || 0) / itemsPerPage, - ) - const more = currentPage < totalPages - let scopedTxs - - if (simulationResults) { - const endIndex = currentPage * itemsPerPage - scopedTxs = simulationResults.slice(0, endIndex) - } - - return [scopedTxs, more] + const endIndex = currentPage * itemsPerPage + scopedTxs = simulationResults.slice(0, endIndex) } - return [[], false] + + return [scopedTxs, more] }, [simulationResults, currentPage]) const handleLoadMore = useCallback(() => { @@ -162,11 +168,13 @@ const WalletSignBottomSheet = forwardRef( ) || 0, [simulationResults], ) + const estimatedTotalSol = useMemo( () => loading ? '...' : humanReadable(new BN(estimatedTotalLamports), 9), [estimatedTotalLamports, loading], ) + const estimatedTotalBaseFee = useMemo( () => humanReadable( @@ -175,6 +183,7 @@ const WalletSignBottomSheet = forwardRef( ), [simulationResults], ) + const estimatedTotalPriorityFee = useMemo( () => humanReadable( @@ -186,6 +195,7 @@ const WalletSignBottomSheet = forwardRef( ), [simulationResults], ) + const totalWarnings = useMemo( () => simulationResults?.reduce( @@ -198,6 +208,7 @@ const WalletSignBottomSheet = forwardRef( ), [simulationResults], ) + const worstSeverity = useMemo(() => { if (simulationResults) { return simulationResults?.reduce((a, b) => { @@ -230,41 +241,20 @@ const WalletSignBottomSheet = forwardRef( [solBalance, estimatedTotalLamports, simulationResults], ) - const animatedContentHeight = useSharedValue(0) - const hide = useCallback(() => { setIsVisible(false) bottomSheetModalRef.current?.close() }, []) - const show = useCallback( - ({ - type, - url, - warning, - additionalMessage, - serializedTxs, - header, - suppressWarnings, - }: WalletSignOpts) => { - bottomSheetModalRef.current?.expand() - setIsVisible(true) - setWalletSignOpts({ - type, - url, - warning, - additionalMessage, - serializedTxs, - header, - suppressWarnings, - }) - const p = new Promise((resolve) => { - promiseResolve = resolve - }) - return p - }, - [], - ) + const show = useCallback((opts: WalletSignOpts) => { + bottomSheetModalRef.current?.expand() + setIsVisible(true) + setWalletSignOpts(opts) + + return new Promise((resolve) => { + promiseResolve = resolve + }) + }, []) const renderBackdrop = useCallback( (props) => ( @@ -290,12 +280,6 @@ const WalletSignBottomSheet = forwardRef( } }, [onClose]) - const handleIndicatorStyle = useMemo(() => { - return { - backgroundColor: secondaryText, - } - }, [secondaryText]) - const onAcceptHandler = useCallback(() => { if (promiseResolve) { hide() @@ -310,16 +294,86 @@ const WalletSignBottomSheet = forwardRef( } }, [hide]) - useEffect(() => { - bottomSheetModalRef.current?.present() - }, [bottomSheetModalRef]) - + const Chevron = feesExpanded ? ChevronUp : ChevronDown + const isCompact = walletSignOpts instanceof WalletSignBottomSheetCompact + const { type, warning, additionalMessage, suppressWarnings } = + walletSignOpts const showWarnings = - totalWarnings && - !walletSignOpts.suppressWarnings && - worstSeverity === 'critical' + totalWarnings && !suppressWarnings && worstSeverity === 'critical' + + const renderButtons = () => ( + <> + {showWarnings ? ( + + + + + + + ) : ( + + + + + + )} + + ) - const { type, warning, additionalMessage } = walletSignOpts return ( @@ -330,7 +384,9 @@ const WalletSignBottomSheet = forwardRef( backdropComponent={renderBackdrop} onDismiss={handleModalDismiss} enableDismissOnClose - handleIndicatorStyle={handleIndicatorStyle} + handleIndicatorStyle={{ + backgroundColor: secondaryText, + }} // https://ethercreative.github.io/react-native-shadow-generator/ style={{ shadowColor: '#000', @@ -455,6 +511,7 @@ const WalletSignBottomSheet = forwardRef( text={t('browserScreen.suspiciousActivity', { num: totalWarnings, })} + // eslint-disable-next-line @typescript-eslint/no-explicit-any variant={worstSeverity as any} /> @@ -625,74 +682,7 @@ const WalletSignBottomSheet = forwardRef( )} - {showWarnings ? ( - - - - - - - ) : ( - - - - - - )} + {renderButtons()} diff --git a/src/solana/WalletSignBottomSheetCompact.tsx b/src/solana/WalletSignBottomSheetCompact.tsx new file mode 100644 index 000000000..6d59a13f7 --- /dev/null +++ b/src/solana/WalletSignBottomSheetCompact.tsx @@ -0,0 +1,44 @@ +import Box from '@components/Box' +import Text from '@components/Text' +import React, { ReactNode } from 'react' +import { TouchableOpacity } from 'react-native-gesture-handler' +import { + WalletSignOptsCommon, + WalletSignOptsCompact, +} from './walletSignBottomSheetTypes' + +interface IWalletSignBottomSheetCompactProps + extends WalletSignOptsCommon, + WalletSignOptsCompact { + children?: ReactNode +} + +export const WalletSignBottomSheetCompact = ({ + header, + message, + onSimulate, + onCancelHandler, + onAcceptHandler, + children, +}: IWalletSignBottomSheetCompactProps) => ( + + + {header} + + + {message} + + + + Simulate Transaction + + + {children} + +) diff --git a/src/solana/walletSignBottomSheetTypes.tsx b/src/solana/walletSignBottomSheetTypes.tsx index ad04b0578..5b012f146 100644 --- a/src/solana/walletSignBottomSheetTypes.tsx +++ b/src/solana/walletSignBottomSheetTypes.tsx @@ -7,13 +7,18 @@ export enum WalletStandardMessageTypes { signMessage = 'signMessage', } -export type BalanceChange = { - ticker: string - amount: number - type: 'send' | 'receive' +export type WalletSignOptsCommon = { + onCancelHandler: () => void + onAcceptHandler: () => void } -export type WalletSignOpts = { +export type WalletSignOptsCompact = { + header: string + message: string + onSimulate: () => Promise +} + +export type WalletSignOptsSimulated = { type: WalletStandardMessageTypes url: string serializedTxs: Buffer[] | undefined @@ -24,15 +29,10 @@ export type WalletSignOpts = { suppressWarnings?: boolean } +export type WalletSignOpts = WalletSignOptsCompact | WalletSignOptsSimulated + export type WalletSignBottomSheetRef = { - show: ({ - type, - url, - additionalMessage, - serializedTxs, - header, - suppressWarnings, - }: WalletSignOpts) => Promise + show: (opts: WalletSignOpts) => Promise hide: () => void } From 1acb6f97ff57a8933e96ec12b21bcd7bfc1d31a8 Mon Sep 17 00:00:00 2001 From: bry Date: Tue, 23 Jul 2024 17:15:29 -0500 Subject: [PATCH 2/7] WIP --- src/components/AutoGasBanner.tsx | 2 +- .../{useSubmitTxn.ts => useSubmitTxn.tsx} | 47 +- src/locales/en.ts | 4 + .../CollapsibleWritableAccountPreview.tsx | 1 + src/solana/CollectablePreview.tsx | 70 +++ src/solana/PaymentPreview.tsx | 60 ++ src/solana/WalletSIgnBottomSheetSimulated.tsx | 337 ++++++++-- src/solana/WalletSignBottomSheet.tsx | 577 +----------------- src/solana/WalletSignBottomSheetCompact.tsx | 178 ++++-- src/solana/walletSignBottomSheetTypes.tsx | 27 +- src/types/solana.ts | 6 + 11 files changed, 632 insertions(+), 677 deletions(-) rename src/hooks/{useSubmitTxn.ts => useSubmitTxn.tsx} (93%) create mode 100644 src/solana/CollectablePreview.tsx create mode 100644 src/solana/PaymentPreview.tsx diff --git a/src/components/AutoGasBanner.tsx b/src/components/AutoGasBanner.tsx index bca749324..d789b7342 100644 --- a/src/components/AutoGasBanner.tsx +++ b/src/components/AutoGasBanner.tsx @@ -120,7 +120,7 @@ const Banner = ({ onLayout, ...rest }: BannerProps) => { const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', - additionalMessage: t('transactions.autoGasConvert', { symbol }), + message: t('transactions.autoGasConvert', { symbol }), serializedTxs: [serializedTx], header: t('transactions.autoGasConvertHeader'), }) diff --git a/src/hooks/useSubmitTxn.ts b/src/hooks/useSubmitTxn.tsx similarity index 93% rename from src/hooks/useSubmitTxn.ts rename to src/hooks/useSubmitTxn.tsx index 589f7ed8d..cfb267834 100644 --- a/src/hooks/useSubmitTxn.ts +++ b/src/hooks/useSubmitTxn.tsx @@ -11,7 +11,9 @@ import { useAccountStorage } from '@storage/AccountStorageProvider' import i18n from '@utils/i18n' import * as solUtils from '@utils/solanaUtils' import BN from 'bn.js' -import { useCallback } from 'react' +import React, { useCallback } from 'react' +import { CollectablePreview } from '../solana/CollectablePreview' +import { PaymentPreivew } from '../solana/PaymentPreview' import { useSolana } from '../solana/SolanaProvider' import { useWalletSign } from '../solana/WalletSignProvider' import { WalletStandardMessageTypes } from '../solana/walletSignBottomSheetTypes' @@ -75,20 +77,17 @@ export default () => { }), ) - let decision - decision = await walletSignBottomSheetRef.show({ - header: 'Send Tokens', + const serializedTxs = txns.map((tx) => + Buffer.from(toVersionedTx(tx).serialize()), + ) + + const decision = await walletSignBottomSheetRef.show({ + type: WalletStandardMessageTypes.signTransaction, + url: '', + header: t('transactions.sendTokens'), message: t('transactions.signPaymentTxn'), - onSimulate: async () => { - decision = await walletSignBottomSheetRef.show({ - type: WalletStandardMessageTypes.signTransaction, - url: '', - additionalMessage: t('transactions.signPaymentTxn'), - serializedTxs: txns.map((tx) => - Buffer.from(toVersionedTx(tx).serialize()), - ), - }) - }, + serializedTxs, + renderer: () => , }) if (!decision) { @@ -148,10 +147,12 @@ export default () => { ).serialize() const decision = await walletSignBottomSheetRef.show({ - type: WalletStandardMessageTypes.signTransaction, url: '', - additionalMessage: t('transactions.signTransferCollectableTxn'), + type: WalletStandardMessageTypes.signTransaction, + header: t('transactions.transferCollectable'), + message: t('transactions.signTransferCollectableTxn'), serializedTxs: [Buffer.from(serializedTx)], + renderer: () => , }) if (!decision) { @@ -188,7 +189,7 @@ export default () => { const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', - additionalMessage: t('transactions.signSwapTxn'), + message: t('transactions.signSwapTxn'), serializedTxs: [Buffer.from(serializedTx)], suppressWarnings: true, }) @@ -241,7 +242,7 @@ export default () => { type: WalletStandardMessageTypes.signTransaction, url: '', warning: recipientExists ? '' : t('transactions.recipientNonExistent'), - additionalMessage: t('transactions.signSwapTxn'), + message: t('transactions.signSwapTxn'), serializedTxs: [Buffer.from(serializedTx)], }) @@ -286,7 +287,7 @@ export default () => { const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', - additionalMessage: t('transactions.signClaimRewardsTxn'), + message: t('transactions.signClaimRewardsTxn'), serializedTxs: serializedTxs.map(Buffer.from), }) @@ -376,7 +377,7 @@ export default () => { type: WalletStandardMessageTypes.signTransaction, url: '', warning: recipientExists ? '' : t('transactions.recipientNonExistent'), - additionalMessage: t('transactions.signMintDataCreditsTxn'), + message: t('transactions.signMintDataCreditsTxn'), serializedTxs: [Buffer.from(serializedTx)], }) @@ -429,7 +430,7 @@ export default () => { const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', - additionalMessage: t('transactions.signDelegateDCTxn'), + message: t('transactions.signDelegateDCTxn'), serializedTxs: [Buffer.from(serializedTx)], }) @@ -508,7 +509,7 @@ export default () => { const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', - additionalMessage: t('transactions.signAssertLocationTxn'), + message: t('transactions.signAssertLocationTxn'), serializedTxs, }) @@ -608,7 +609,7 @@ export default () => { warning: destinationExists ? '' : t('transactions.recipientNonExistent'), - additionalMessage: t('transactions.signPaymentTxn'), + message: t('transactions.signPaymentTxn'), serializedTxs: [Buffer.from(toVersionedTx(txn).serialize())], }) diff --git a/src/locales/en.ts b/src/locales/en.ts index 892977f4a..e203079b4 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1198,7 +1198,11 @@ export default { unstakeValidator: 'Unstake {{ticker}}', validator: 'Validator', delegated: 'Delegated', + sendTokens: 'Send Tokens', + transferCollectable: 'Transfer Collectable', signPaymentTxn: 'Sign this transaction to send your payment.', + simulateTxn: 'Simulate Transaction', + signTxn: 'Sign Transaction', signTransferCollectableTxn: 'Sign this transaction to transfer your collectable.', signSwapTxn: 'Sign this transaction to swap your tokens.', diff --git a/src/solana/CollapsibleWritableAccountPreview.tsx b/src/solana/CollapsibleWritableAccountPreview.tsx index ad2578e95..1e3c7d004 100644 --- a/src/solana/CollapsibleWritableAccountPreview.tsx +++ b/src/solana/CollapsibleWritableAccountPreview.tsx @@ -94,6 +94,7 @@ const NativeSolBalanceChange = ({ /> ) } + function accountExists(account: AccountInfo | null): boolean { return account ? account.lamports > 0 : false } diff --git a/src/solana/CollectablePreview.tsx b/src/solana/CollectablePreview.tsx new file mode 100644 index 000000000..1d3811978 --- /dev/null +++ b/src/solana/CollectablePreview.tsx @@ -0,0 +1,70 @@ +import Send from '@assets/images/send.svg' +import Box from '@components/Box' +import { Pill } from '@components/Pill' +import Text from '@components/Text' +import React, { useMemo } from 'react' +import ImageBox from '@components/ImageBox' +import { ellipsizeAddress } from '@utils/accountUtils' +import { Collectable, CompressedNFT, isCompressedNFT } from '../types/solana' + +interface ICollectablePreviewProps { + collectable: CompressedNFT | Collectable + payee: string +} + +export const CollectablePreview = ({ + collectable, + payee, +}: ICollectablePreviewProps) => { + const metadata = useMemo(() => { + if (isCompressedNFT(collectable)) { + return collectable.content.metadata + } + + return collectable.metadata + }, [collectable]) + + return ( + + + + {metadata && ( + + + + )} + {ellipsizeAddress(payee)} + + + + + + + ) +} diff --git a/src/solana/PaymentPreview.tsx b/src/solana/PaymentPreview.tsx new file mode 100644 index 000000000..f24468c55 --- /dev/null +++ b/src/solana/PaymentPreview.tsx @@ -0,0 +1,60 @@ +import Send from '@assets/images/send.svg' +import Box from '@components/Box' +import { Pill } from '@components/Pill' +import Text from '@components/Text' +import TokenIcon from '@components/TokenIcon' +import { useMint } from '@helium/helium-react-hooks' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { PublicKey } from '@solana/web3.js' +import { ellipsizeAddress } from '@utils/accountUtils' +import { humanReadable } from '@utils/solanaUtils' +import BN from 'bn.js' +import React from 'react' + +interface IPaymentPreviewProps { + payments: { + payee: string + balanceAmount: BN + max?: boolean + }[] + mint: PublicKey +} + +export const PaymentPreivew = ({ mint, payments }: IPaymentPreviewProps) => { + const decimals = useMint(mint)?.info?.decimals + const { symbol, json } = useMetaplexMetadata(mint) + + return ( + + {payments.map(({ payee, balanceAmount }, index) => ( + + + {json?.image ? : null} + {symbol} + {ellipsizeAddress(payee)} + + + + + + ))} + + ) +} diff --git a/src/solana/WalletSIgnBottomSheetSimulated.tsx b/src/solana/WalletSIgnBottomSheetSimulated.tsx index ed58870e3..ce0302c41 100644 --- a/src/solana/WalletSIgnBottomSheetSimulated.tsx +++ b/src/solana/WalletSIgnBottomSheetSimulated.tsx @@ -1,34 +1,46 @@ +import Checkmark from '@assets/images/checkmark.svg' +import IndentArrow from '@assets/images/indentArrow.svg' +import InfoIcon from '@assets/images/info.svg' import CancelIcon from '@assets/images/remixCancel.svg' +import ChevronDown from '@assets/images/remixChevronDown.svg' +import ChevronUp from '@assets/images/remixChevronUp.svg' import Box from '@components/Box' +import ButtonPressable from '@components/ButtonPressable' +import CircleLoader from '@components/CircleLoader' import SubmitButton from '@components/SubmitButton' import Text from '@components/Text' +import TouchableOpacityBox from '@components/TouchableOpacityBox' +import { WarningPill } from '@components/WarningPill' +import { entityCreatorKey } from '@helium/helium-entity-manager-sdk' import { useSolOwnedAmount } from '@helium/helium-react-hooks' +import { humanReadable } from '@helium/spl-utils' +import { sus } from '@helium/sus' import { useBN } from '@hooks/useBN' import { useCurrentWallet } from '@hooks/useCurrentWallet' import { useRentExempt } from '@hooks/useRentExempt' -import axios from 'axios' -import React, { ReactNode, useCallback, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useAsync } from 'react-async-hook' import { DAO_KEY } from '@utils/constants' -import { entityCreatorKey } from '@helium/helium-entity-manager-sdk' -import { humanReadable } from '@helium/spl-utils' +import axios from 'axios' import BN from 'bn.js' +import React, { useCallback, useMemo, useState } from 'react' +import { useAsync } from 'react-async-hook' +import { useTranslation } from 'react-i18next' +import { ScrollView } from 'react-native-gesture-handler' import { useSolana } from './SolanaProvider' +import WalletSignBottomSheetTransaction from './WalletSignBottomSheetTransaction' import { - WalletSignOptsCommon, WalletSignOptsSimulated, + WalletStandardMessageTypes, } from './walletSignBottomSheetTypes' -interface IWalletSignBottomSheetSimulatedProps - extends WalletSignOptsCommon, - WalletSignOptsSimulated { - children?: ReactNode -} const WELL_KNOWN_CANOPY_URL = 'https://shdw-drive.genesysgo.net/6tcnBSybPG7piEDShBcrVtYJDPSvGrDbVvXmXKpzBvWP/merkles.json' let wellKnownCanopyCache: Record | undefined +type IWalletSignBottomSheetSimulatedProps = WalletSignOptsSimulated & { + onCancel: () => void + onAccept: () => void +} + export const WalletSignBottomSheetSimulated = ({ url, type, @@ -36,15 +48,13 @@ export const WalletSignBottomSheetSimulated = ({ warning, serializedTxs, suppressWarnings, - additionalMessage, - onCancelHandler, - onAcceptHandler, - children, + message, + onAccept, + onCancel, }: IWalletSignBottomSheetSimulatedProps) => { const { t } = useTranslation() const { connection, cluster } = useSolana() const wallet = useCurrentWallet() - const [feesExpanded, setFeesExpanded] = useState(false) const solBalance = useBN(useSolOwnedAmount(wallet).amount) const { rentExempt } = useRentExempt() @@ -69,16 +79,11 @@ export const WalletSignBottomSheetSimulated = ({ error, loading, } = useAsync(async () => { - if ( - connection && - wallet && - walletSignOpts.serializedTxs && - accountBlacklist - ) { + if (connection && wallet && serializedTxs && accountBlacklist) { return sus({ connection, wallet, - serializedTransactions: walletSignOpts.serializedTxs, + serializedTransactions: serializedTxs, checkCNfts: true, cluster, extraSearchAssetParams: { @@ -189,21 +194,275 @@ export const WalletSignBottomSheetSimulated = ({ [solBalance, estimatedTotalLamports, simulationResults], ) + const Chevron = feesExpanded ? ChevronUp : ChevronDown const showWarnings = totalWarnings && !suppressWarnings && worstSeverity === 'critical' return ( - - - {header} - - {children} + + {header || url ? ( + + {header ? ( + + {header} + + ) : null} + {url ? ( + + {url || ''} + + ) : null} + + ) : null} + {type === WalletStandardMessageTypes.connect && ( + + + + + + {t('browserScreen.connectBullet1')} + + + + + + {t('browserScreen.connectBullet2')} + + + + + + {t('browserScreen.connectToWebsitesYouTrust')} + + + + )} + {(type === WalletStandardMessageTypes.signMessage || + type === WalletStandardMessageTypes.signAndSendTransaction || + type === WalletStandardMessageTypes.signTransaction) && ( + + {warning && ( + + + {warning} + + + )} + + + + + + {t('browserScreen.estimatedChanges')} + + setInfoVisible((prev) => !prev)} + > + + + + {infoVisible && ( + + {t('browserScreen.estimatedChangesDescription')} + + )} + + {!(insufficientFunds || insufficientRentExempt) && message && ( + + {message} + + )} + + {showWarnings ? ( + + + + ) : null} + + {(insufficientFunds || insufficientRentExempt) && ( + + + + )} + + + + + {t('browserScreen.writableAccounts')} + + setWritableInfoVisible((prev) => !prev)} + > + + + + + {t('browserScreen.transactions', { + num: simulationResults?.length || 1, + })} + + + {writableInfoVisible && ( + + {t('browserScreen.writableAccountsDescription')} + + )} + + + {loading && } + {error ? ( + + + + + {error.message || error.toString()} + + + + + ) : null} + {currentTxs && ( + <> + {currentTxs.map((tx, idx) => ( + + ))} + {hasMore && ( + + )} + + )} + + + {(type === WalletStandardMessageTypes.signAndSendTransaction || + type === WalletStandardMessageTypes.signTransaction) && ( + + setFeesExpanded(!feesExpanded)} + marginTop="s" + flexDirection="row" + justifyContent="space-between" + > + + {t('browserScreen.totalNetworkFee')} + + + + {`~${estimatedTotalSol} SOL`} + + + + + {feesExpanded ? ( + + + + + + {t('browserScreen.totalBaseFee')} + + + + + {`~${estimatedTotalBaseFee} SOL`} + + + + + + + {t('browserScreen.totalPriorityFee')} + + + + + {`~${estimatedTotalPriorityFee} SOL`} + + + + ) : null} + + )} + + + )} {showWarnings ? ( ) : ( @@ -251,7 +510,7 @@ export const WalletSignBottomSheetSimulated = ({ titleColorPressedOpacity={0.3} titleColor="white" title={t('browserScreen.cancel')} - onPress={onCancelHandler} + onPress={onCancel} /> )} diff --git a/src/solana/WalletSignBottomSheet.tsx b/src/solana/WalletSignBottomSheet.tsx index 2346fee4e..b651d55b5 100644 --- a/src/solana/WalletSignBottomSheet.tsx +++ b/src/solana/WalletSignBottomSheet.tsx @@ -1,33 +1,11 @@ -import Checkmark from '@assets/images/checkmark.svg' -import IndentArrow from '@assets/images/indentArrow.svg' -import InfoIcon from '@assets/images/info.svg' -import CancelIcon from '@assets/images/remixCancel.svg' -import ChevronDown from '@assets/images/remixChevronDown.svg' -import ChevronUp from '@assets/images/remixChevronUp.svg' import Box from '@components/Box' -import ButtonPressable from '@components/ButtonPressable' -import CircleLoader from '@components/CircleLoader' -import SubmitButton from '@components/SubmitButton' -import Text from '@components/Text' -import TouchableOpacityBox from '@components/TouchableOpacityBox' -import { WarningPill } from '@components/WarningPill' import { BottomSheetBackdrop, BottomSheetModal, BottomSheetModalProvider, BottomSheetScrollView, } from '@gorhom/bottom-sheet' -import { entityCreatorKey } from '@helium/helium-entity-manager-sdk' -import { useSolOwnedAmount } from '@helium/helium-react-hooks' -import { sus } from '@helium/sus' -import { useBN } from '@hooks/useBN' -import { useCurrentWallet } from '@hooks/useCurrentWallet' -import { useRentExempt } from '@hooks/useRentExempt' import { useColors, useOpacity } from '@theme/themeHooks' -import { DAO_KEY } from '@utils/constants' -import { humanReadable } from '@utils/solanaUtils' -import axios from 'axios' -import BN from 'bn.js' import React, { Ref, forwardRef, @@ -35,57 +13,36 @@ import React, { useCallback, useEffect, useImperativeHandle, - useMemo, useRef, useState, } from 'react' -import { useAsync } from 'react-async-hook' -import { useTranslation } from 'react-i18next' -import { TouchableOpacity } from 'react-native' -import { ScrollView } from 'react-native-gesture-handler' import { useSharedValue } from 'react-native-reanimated' -import { useSolana } from './SolanaProvider' -import WalletSignBottomSheetTransaction from './WalletSignBottomSheetTransaction' +import { WalletSignBottomSheetSimulated } from './WalletSIgnBottomSheetSimulated' +import { WalletSignBottomSheetCompact } from './WalletSignBottomSheetCompact' import { WalletSignBottomSheetProps, WalletSignBottomSheetRef, WalletSignOpts, WalletStandardMessageTypes, } from './walletSignBottomSheetTypes' -import { WalletSignBottomSheetCompact } from './WalletSignBottomSheetCompact' -import { WalletSignBottomSheetSimulated } from './WalletSIgnBottomSheetSimulated' - -const WELL_KNOWN_CANOPY_URL = - 'https://shdw-drive.genesysgo.net/6tcnBSybPG7piEDShBcrVtYJDPSvGrDbVvXmXKpzBvWP/merkles.json' let promiseResolve: (value: boolean | PromiseLike) => void -let wellKnownCanopyCache: Record | undefined - const WalletSignBottomSheet = forwardRef( ( { onClose, children }: WalletSignBottomSheetProps, ref: Ref, ) => { useImperativeHandle(ref, () => ({ show, hide })) - const { t } = useTranslation() - const { connection, cluster } = useSolana() - const wallet = useCurrentWallet() const { secondaryText } = useColors() const { backgroundStyle } = useOpacity('surfaceSecondary', 1) const animatedContentHeight = useSharedValue(0) - const solBalance = useBN(useSolOwnedAmount(wallet).amount) - const { rentExempt } = useRentExempt() const bottomSheetModalRef = useRef(null) - const [isVisible, setIsVisible] = useState(false) - const [infoVisible, setInfoVisible] = useState(false) - const [writableInfoVisible, setWritableInfoVisible] = useState(false) - const [feesExpanded, setFeesExpanded] = useState(false) - const [currentPage, setCurrentPage] = useState(1) + const [isCompact, setIsCompact] = useState(true) const [walletSignOpts, setWalletSignOpts] = useState({ type: WalletStandardMessageTypes.connect, - url: '', - additionalMessage: '', + url: undefined, + message: '', serializedTxs: undefined, header: undefined, suppressWarnings: false, @@ -95,160 +52,12 @@ const WalletSignBottomSheet = forwardRef( bottomSheetModalRef.current?.present() }, [bottomSheetModalRef]) - const { result: accountBlacklist } = useAsync(async () => { - if (!wellKnownCanopyCache) - wellKnownCanopyCache = await ( - await axios.get(WELL_KNOWN_CANOPY_URL) - ).data - - if (wellKnownCanopyCache) { - return new Set(Object.keys(wellKnownCanopyCache)) - } - - return new Set([]) - }, []) - - const { - result: simulationResults, - error, - loading, - } = useAsync(async () => { - if ( - connection && - wallet && - walletSignOpts.serializedTxs && - accountBlacklist - ) { - return sus({ - connection, - wallet, - serializedTransactions: walletSignOpts.serializedTxs, - checkCNfts: true, - cluster, - extraSearchAssetParams: { - creatorVerified: true, - creatorAddress: entityCreatorKey(DAO_KEY)[0].toBase58(), - }, - accountBlacklist, - }) - } - }, [ - walletSignOpts.serializedTxs, - cluster, - connection, - wallet, - accountBlacklist, - ]) - - const [currentTxs, hasMore] = useMemo(() => { - if (!simulationResults) return [[], false] - - let scopedTxs - const itemsPerPage = 5 - const pages = Math.ceil((simulationResults.length || 0) / itemsPerPage) - const more = currentPage < pages - - if (simulationResults) { - const endIndex = currentPage * itemsPerPage - scopedTxs = simulationResults.slice(0, endIndex) - } - - return [scopedTxs, more] - }, [simulationResults, currentPage]) - - const handleLoadMore = useCallback(() => { - setCurrentPage((page) => page + 1) - }, [setCurrentPage]) - - const estimatedTotalLamports = useMemo( - () => - simulationResults?.reduce( - (a, b) => a + b.solFee + (b.priorityFee || 0), - 0, - ) || 0, - [simulationResults], - ) - - const estimatedTotalSol = useMemo( - () => - loading ? '...' : humanReadable(new BN(estimatedTotalLamports), 9), - [estimatedTotalLamports, loading], - ) - - const estimatedTotalBaseFee = useMemo( - () => - humanReadable( - new BN(simulationResults?.reduce((a, b) => a + b.solFee, 0) || 0), - 9, - ), - [simulationResults], - ) - - const estimatedTotalPriorityFee = useMemo( - () => - humanReadable( - new BN( - simulationResults?.reduce((a, b) => a + (b.priorityFee || 0), 0) || - 0, - ), - 9, - ), - [simulationResults], - ) - - const totalWarnings = useMemo( - () => - simulationResults?.reduce( - (a, b) => - a + - (b.warnings.filter((w) => w.severity === 'critical').length > 0 - ? 1 - : 0), - 0, - ), - [simulationResults], - ) - - const worstSeverity = useMemo(() => { - if (simulationResults) { - return simulationResults?.reduce((a, b) => { - if (a === 'critical') { - return 'critical' - } - if (b.warnings.some((w) => w.severity === 'critical')) { - return 'critical' - } - - return a - }, 'warning') - } - return 'critical' - }, [simulationResults]) - - const insufficientRentExempt = useMemo(() => { - if (solBalance) { - return new BN(solBalance.toString()) - .sub(new BN(estimatedTotalLamports)) - .lt(new BN(rentExempt || 0)) - } - }, [solBalance, estimatedTotalLamports, rentExempt]) - - const insufficientFunds = useMemo( - () => - new BN(estimatedTotalLamports).gt( - new BN(solBalance?.toString() || '0'), - ) || simulationResults?.some((r) => r.insufficientFunds), - [solBalance, estimatedTotalLamports, simulationResults], - ) - const hide = useCallback(() => { - setIsVisible(false) bottomSheetModalRef.current?.close() }, []) const show = useCallback((opts: WalletSignOpts) => { bottomSheetModalRef.current?.expand() - setIsVisible(true) setWalletSignOpts(opts) return new Promise((resolve) => { @@ -274,7 +83,6 @@ const WalletSignBottomSheet = forwardRef( } // We need to re present the bottom sheet after it is dismissed so that it can be expanded again bottomSheetModalRef.current?.present() - setIsVisible(false) if (onClose) { onClose() } @@ -294,86 +102,6 @@ const WalletSignBottomSheet = forwardRef( } }, [hide]) - const Chevron = feesExpanded ? ChevronUp : ChevronDown - const isCompact = walletSignOpts instanceof WalletSignBottomSheetCompact - const { type, warning, additionalMessage, suppressWarnings } = - walletSignOpts - const showWarnings = - totalWarnings && !suppressWarnings && worstSeverity === 'critical' - - const renderButtons = () => ( - <> - {showWarnings ? ( - - - - - - - ) : ( - - - - - - )} - - ) - return ( @@ -402,288 +130,19 @@ const WalletSignBottomSheet = forwardRef( contentHeight={animatedContentHeight} > - - {walletSignOpts.header || walletSignOpts.url ? ( - - {walletSignOpts.header ? ( - - {walletSignOpts.header} - - ) : null} - {walletSignOpts.url ? ( - - {walletSignOpts.url || ''} - - ) : null} - - ) : null} - {type === WalletStandardMessageTypes.connect && ( - - - - - - {t('browserScreen.connectBullet1')} - - - - - - {t('browserScreen.connectBullet2')} - - - - - - {t('browserScreen.connectToWebsitesYouTrust')} - - - - )} - {(type === WalletStandardMessageTypes.signMessage || - type === WalletStandardMessageTypes.signAndSendTransaction || - type === WalletStandardMessageTypes.signTransaction) && ( - - {warning && ( - - - {warning} - - - )} - - - - - - {t('browserScreen.estimatedChanges')} - - setInfoVisible((prev) => !prev)} - > - - - - {infoVisible && ( - - {t('browserScreen.estimatedChangesDescription')} - - )} - - {!(insufficientFunds || insufficientRentExempt) && - additionalMessage && ( - - {additionalMessage} - - )} - - {showWarnings ? ( - - - - ) : null} - - {(insufficientFunds || insufficientRentExempt) && ( - - - - )} - - - - - {t('browserScreen.writableAccounts')} - - - setWritableInfoVisible((prev) => !prev) - } - > - - - - - {t('browserScreen.transactions', { - num: simulationResults?.length || 1, - })} - - - {writableInfoVisible && ( - - {t('browserScreen.writableAccountsDescription')} - - )} - - - {loading && } - {error ? ( - - - - - {error.message || error.toString()} - - - - - ) : null} - {isVisible && currentTxs && ( - <> - {currentTxs.map((tx, idx) => ( - - ))} - {hasMore && ( - - )} - - )} - - - {(type === - WalletStandardMessageTypes.signAndSendTransaction || - type === - WalletStandardMessageTypes.signTransaction) && ( - - setFeesExpanded(!feesExpanded)} - marginTop="s" - flexDirection="row" - justifyContent="space-between" - > - - {t('browserScreen.totalNetworkFee')} - - - - {`~${estimatedTotalSol} SOL`} - - - - - {feesExpanded ? ( - - - - - - {t('browserScreen.totalBaseFee')} - - - - - {`~${estimatedTotalBaseFee} SOL`} - - - - - - - {t('browserScreen.totalPriorityFee')} - - - - - {`~${estimatedTotalPriorityFee} SOL`} - - - - ) : null} - - )} - - - )} - {renderButtons()} - + {isCompact(walletSignOpts) ? ( + + ) : ( + + )} {children} diff --git a/src/solana/WalletSignBottomSheetCompact.tsx b/src/solana/WalletSignBottomSheetCompact.tsx index 6d59a13f7..62aca11fc 100644 --- a/src/solana/WalletSignBottomSheetCompact.tsx +++ b/src/solana/WalletSignBottomSheetCompact.tsx @@ -1,44 +1,152 @@ import Box from '@components/Box' +import ButtonPressable from '@components/ButtonPressable' import Text from '@components/Text' -import React, { ReactNode } from 'react' +import React, { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import { TouchableOpacity } from 'react-native-gesture-handler' -import { - WalletSignOptsCommon, - WalletSignOptsCompact, -} from './walletSignBottomSheetTypes' - -interface IWalletSignBottomSheetCompactProps - extends WalletSignOptsCommon, - WalletSignOptsCompact { - children?: ReactNode +import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { useBN } from '@hooks/useBN' +import { useSolOwnedAmount } from '@helium/helium-react-hooks' +import { useRentExempt } from '@hooks/useRentExempt' +import { LAMPORTS_PER_SOL } from '@solana/web3.js' +import BN from 'bn.js' +import { getBasePriorityFee } from '@utils/walletApiV2' +import { useAsync } from 'react-async-hook' +import { WalletSignOptsCompact } from './walletSignBottomSheetTypes' + +type IWalletSignBottomSheetCompactProps = WalletSignOptsCompact & { + onCancel: () => void + onAccept: () => void } export const WalletSignBottomSheetCompact = ({ header, message, + warning, + serializedTxs, + renderer, onSimulate, - onCancelHandler, - onAcceptHandler, - children, -}: IWalletSignBottomSheetCompactProps) => ( - - - {header} - - - {message} - - - - Simulate Transaction - - - {children} - -) + onCancel, + onAccept, +}: IWalletSignBottomSheetCompactProps) => { + const { t } = useTranslation() + const wallet = useCurrentWallet() + const solBalance = useBN(useSolOwnedAmount(wallet).amount) + const { rentExempt } = useRentExempt() + const [estimatedTotalSolByLamports, setEstimatedTotalSolByLamports] = + useState(0) + + useAsync(async () => { + let fees = 5000 / LAMPORTS_PER_SOL + if (serializedTxs) { + const basePriorityFee = await getBasePriorityFee() + const priorityFees = serializedTxs.length * basePriorityFee + fees = (serializedTxs.length * 5000 + priorityFees) / LAMPORTS_PER_SOL + } + + setEstimatedTotalSolByLamports(fees) + }, [serializedTxs, setEstimatedTotalSolByLamports]) + + const insufficientRentExempt = useMemo(() => { + if (solBalance) { + return new BN(solBalance.toString()) + .sub(new BN(estimatedTotalSolByLamports)) + .lt(new BN(rentExempt || 0)) + } + }, [solBalance, estimatedTotalSolByLamports, rentExempt]) + + const insufficientFunds = useMemo( + () => + new BN(estimatedTotalSolByLamports).gt( + new BN(solBalance?.toString() || '0'), + ), + [solBalance, estimatedTotalSolByLamports], + ) + + return ( + + {warning && ( + + + {warning} + + + )} + + {!(insufficientFunds || insufficientRentExempt) && ( + + {header || t('transactions.signTxn')} + + )} + + {!(insufficientFunds || insufficientRentExempt) && ( + + {message} + + )} + + {(insufficientFunds || insufficientRentExempt) && ( + + + {insufficientFunds + ? t('browserScreen.insufficientFunds') + : t('browserScreen.insufficientRentExempt', { + amount: rentExempt, + })} + + + )} + {renderer && renderer()} + + + {t('browserScreen.totalNetworkFee')} + + + {`~${estimatedTotalSolByLamports} SOL`} + + + + + + {t('transactions.simulateTxn')} + + + + + + + + + ) +} diff --git a/src/solana/walletSignBottomSheetTypes.tsx b/src/solana/walletSignBottomSheetTypes.tsx index 5b012f146..3ed6593bc 100644 --- a/src/solana/walletSignBottomSheetTypes.tsx +++ b/src/solana/walletSignBottomSheetTypes.tsx @@ -7,30 +7,17 @@ export enum WalletStandardMessageTypes { signMessage = 'signMessage', } -export type WalletSignOptsCommon = { - onCancelHandler: () => void - onAcceptHandler: () => void -} - -export type WalletSignOptsCompact = { - header: string - message: string - onSimulate: () => Promise -} - -export type WalletSignOptsSimulated = { +export type WalletSignOpts = { + url?: string type: WalletStandardMessageTypes - url: string - serializedTxs: Buffer[] | undefined - warning?: string - additionalMessage?: string - header?: string - // Allow supressing warnings for our own txs suppressWarnings?: boolean + header?: string + message?: string + warning?: string + serializedTxs?: Buffer[] + renderer?: () => ReactNode } -export type WalletSignOpts = WalletSignOptsCompact | WalletSignOptsSimulated - export type WalletSignBottomSheetRef = { show: (opts: WalletSignOpts) => Promise hide: () => void diff --git a/src/types/solana.ts b/src/types/solana.ts index c58f45ffe..9c5037bc8 100644 --- a/src/types/solana.ts +++ b/src/types/solana.ts @@ -87,6 +87,12 @@ export type CompressedNFT = { export type Collectable = any +export const isCompressedNFT = ( + collectable: CompressedNFT | Collectable, +): collectable is CompressedNFT => { + return (collectable as CompressedNFT).compression?.compressed !== undefined +} + type NativeTransfer = { fromUserAccount: string toUserAccount: string From f10d6aeb79f986590a12b28a2920dda68d4e727e Mon Sep 17 00:00:00 2001 From: bry Date: Wed, 24 Jul 2024 14:51:36 -0500 Subject: [PATCH 3/7] Feature almost finished, need to build out callout components --- src/features/governance/PositionCard.tsx | 92 +++++++++++++++---- src/hooks/useSubmitTxn.tsx | 39 ++++++++ src/locales/en.ts | 3 + src/solana/MessagePreview.tsx | 29 ++++++ src/solana/WalletSIgnBottomSheetSimulated.tsx | 8 +- src/solana/WalletSignBottomSheet.tsx | 12 ++- src/solana/WalletSignBottomSheetCompact.tsx | 11 +-- 7 files changed, 163 insertions(+), 31 deletions(-) create mode 100644 src/solana/MessagePreview.tsx diff --git a/src/features/governance/PositionCard.tsx b/src/features/governance/PositionCard.tsx index a1c3ebe2e..3c97b0e6f 100644 --- a/src/features/governance/PositionCard.tsx +++ b/src/features/governance/PositionCard.tsx @@ -53,6 +53,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react' import { useAsync } from 'react-async-hook' import { useTranslation } from 'react-i18next' import { FadeIn, FadeOut } from 'react-native-reanimated' +import { MessagePreview } from '../../solana/MessagePreview' import { useSolana } from '../../solana/SolanaProvider' import { useWalletSign } from '../../solana/WalletSignProvider' import { WalletStandardMessageTypes } from '../../solana/walletSignBottomSheetTypes' @@ -147,11 +148,19 @@ export const PositionCard = ({ const { anchorProvider } = useSolana() - const decideAndExecute = async ( - header: string, - instructions: TransactionInstruction[], - sigs: Keypair[] = [], - ) => { + const decideAndExecute = async ({ + header, + message, + instructions, + renderer, + sigs = [], + }: { + header: string + message: string + instructions: TransactionInstruction[] + sigs?: Keypair[] + renderer?: () => React.ReactNode + }) => { if (!anchorProvider || !walletSignBottomSheetRef) return const transactions = await batchInstructionsToTxsWithPriorityFee( @@ -173,6 +182,9 @@ export const PositionCard = ({ url: '', header, serializedTxs: txs.map((t) => Buffer.from(t.serialize())), + renderer: !renderer + ? () => + : renderer, }) if (decision) { @@ -348,7 +360,11 @@ export const PositionCard = ({ await closePosition({ position, onInstructions: async (ixs) => { - await decideAndExecute(t('gov.transactions.closePosition'), ixs) + await decideAndExecute({ + header: t('gov.transactions.closePosition'), + message: 'Are you sure you want to close your position?', + instructions: ixs, + }) if (!closingError) { refetchState() } @@ -360,12 +376,15 @@ export const PositionCard = ({ await flipPositionLockupKind({ position, onInstructions: async (ixs) => { - await decideAndExecute( - isConstant + await decideAndExecute({ + header: isConstant ? t('gov.transactions.unpauseLockup') : t('gov.transactions.pauseLockup'), - ixs, - ) + message: isConstant + ? 'Are you sure you want to upause this positions lockup?' + : 'Are you sure you want to pause this positions lockup?', + instructions: ixs, + }) if (!flippingError) { refetchState() @@ -379,7 +398,11 @@ export const PositionCard = ({ position, lockupPeriodsInDays: values.lockupPeriodInDays, onInstructions: async (ixs) => { - await decideAndExecute(t('gov.transactions.extendPosition'), ixs) + await decideAndExecute({ + header: t('gov.transactions.extendPosition'), + message: "Are you sure you want to extend this position's lockup?", + instructions: ixs, + }) if (!extendingError) { refetchState() } @@ -394,7 +417,13 @@ export const PositionCard = ({ lockupKind: values.lockupKind.value, lockupPeriodsInDays: values.lockupPeriodInDays, onInstructions: async (ixs, sigs) => { - await decideAndExecute(t('gov.transactions.splitPosition'), ixs, sigs) + await decideAndExecute({ + header: t('gov.transactions.splitPosition'), + message: "Are you sure you want to split this position's tokens?", + instructions: ixs, + sigs, + }) + if (!splitingError) { refetchState() } @@ -411,7 +440,12 @@ export const PositionCard = ({ amount, targetPosition, onInstructions: async (ixs) => { - await decideAndExecute(t('gov.transactions.transferPosition'), ixs) + await decideAndExecute({ + header: t('gov.transactions.transferPosition'), + message: "Are you sure you want to transfer this position's tokens?", + instructions: ixs, + }) + if (!transferingError) { refetchState() } @@ -424,7 +458,13 @@ export const PositionCard = ({ position, subDao, onInstructions: async (ixs) => { - await decideAndExecute(t('gov.transactions.delegatePosition'), ixs) + await decideAndExecute({ + header: t('gov.transactions.delegatePosition'), + message: `Are you sure you want to delegate this position's tokens to the + ${subDao.dntMetadata.name} subdao?`, + instructions: ixs, + }) + if (!delegatingError) { refetchState() } @@ -439,11 +479,20 @@ export const PositionCard = ({ const undelegate = ixs[ixs.length - 1] const claims = ixs.slice(0, ixs.length - 1) if (claims.length > 0) { - await decideAndExecute(t('gov.transactions.claimRewards'), claims) + await decideAndExecute({ + header: t('gov.transactions.claimRewards'), + message: 'Claim your rewards', + instructions: claims, + }) } - await decideAndExecute(t('gov.transactions.undelegatePosition'), [ - undelegate, - ]) + + await decideAndExecute({ + header: t('gov.transactions.undelegatePosition'), + message: + "Are you sure you want to undelegate this position's tokens?", + instructions: [undelegate], + }) + if (!undelegatingError) { refetchState() } @@ -456,7 +505,12 @@ export const PositionCard = ({ position, organization, onInstructions: async (ixs) => { - await decideAndExecute(t('gov.transactions.relinquishPosition'), ixs) + await decideAndExecute({ + header: t('gov.transactions.relinquishPosition'), + message: "Are you sure you want to relinquish this position's votes?", + instructions: ixs, + }) + if (!relinquishingError) { refetchState() } diff --git a/src/hooks/useSubmitTxn.tsx b/src/hooks/useSubmitTxn.tsx index cfb267834..301755a74 100644 --- a/src/hooks/useSubmitTxn.tsx +++ b/src/hooks/useSubmitTxn.tsx @@ -2,6 +2,7 @@ import { NetworkType } from '@helium/onboarding' import { useOnboarding } from '@helium/react-native-sdk' import { chunks, + DC_MINT, populateMissingDraftInfo, sendAndConfirmWithRetry, toVersionedTx, @@ -12,7 +13,9 @@ import i18n from '@utils/i18n' import * as solUtils from '@utils/solanaUtils' import BN from 'bn.js' import React, { useCallback } from 'react' +import { ellipsizeAddress } from '@utils/accountUtils' import { CollectablePreview } from '../solana/CollectablePreview' +import { MessagePreview } from '../solana/MessagePreview' import { PaymentPreivew } from '../solana/PaymentPreview' import { useSolana } from '../solana/SolanaProvider' import { useWalletSign } from '../solana/WalletSignProvider' @@ -189,6 +192,7 @@ export default () => { const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', + header: t('transactions.swapTokens'), message: t('transactions.signSwapTxn'), serializedTxs: [Buffer.from(serializedTx)], suppressWarnings: true, @@ -242,6 +246,7 @@ export default () => { type: WalletStandardMessageTypes.signTransaction, url: '', warning: recipientExists ? '' : t('transactions.recipientNonExistent'), + header: t('transactions.swapTokens'), message: t('transactions.signSwapTxn'), serializedTxs: [Buffer.from(serializedTx)], }) @@ -287,8 +292,12 @@ export default () => { const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', + header: t('transactions.claimRewards'), message: t('transactions.signClaimRewardsTxn'), serializedTxs: serializedTxs.map(Buffer.from), + renderer: () => ( + + ), }) if (!decision) { @@ -430,8 +439,22 @@ export default () => { const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', + header: t('transactions.delegateDC'), message: t('transactions.signDelegateDCTxn'), serializedTxs: [Buffer.from(serializedTx)], + renderer: () => ( + + ), }) if (!decision) { @@ -509,8 +532,16 @@ export default () => { const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', + header: t('collectablesScreen.hotspots.assertLocation'), message: t('transactions.signAssertLocationTxn'), serializedTxs, + renderer: () => ( + + ), }) if (!decision) { @@ -609,8 +640,16 @@ export default () => { warning: destinationExists ? '' : t('transactions.recipientNonExistent'), + header: t('transactions.updateRecipient'), message: t('transactions.signPaymentTxn'), serializedTxs: [Buffer.from(toVersionedTx(txn).serialize())], + renderer: () => ( + + ), }) if (!decision) { diff --git a/src/locales/en.ts b/src/locales/en.ts index e203079b4..fbf477569 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1137,6 +1137,7 @@ export default { description: 'Last updated {{date}}. Tap for info.', }, transactions: { + updateRecipient: 'Update Recipient', added: 'Hotspot Added to Blockchain', addToAddressBook: { message: 'Would you like to add this wallet to your address book?', @@ -1192,12 +1193,14 @@ export default { transfer: 'Hotspot Transfer', transferBuy: 'Transfer Hotspot (Buy)', transferSell: 'Transfer Hotspot (Sell)', + delegateDC: 'Delegate data credits', transferValidator: 'Transfer Stake', txnFee: 'Transaction Fee', txnFeePaidBy: 'Transaction Fee paid by {{feePayer}}', unstakeValidator: 'Unstake {{ticker}}', validator: 'Validator', delegated: 'Delegated', + claimRewards: 'Claim Rewards', sendTokens: 'Send Tokens', transferCollectable: 'Transfer Collectable', signPaymentTxn: 'Sign this transaction to send your payment.', diff --git a/src/solana/MessagePreview.tsx b/src/solana/MessagePreview.tsx new file mode 100644 index 000000000..601c58676 --- /dev/null +++ b/src/solana/MessagePreview.tsx @@ -0,0 +1,29 @@ +import Box from '@components/Box' +import Text from '@components/Text' +import React from 'react' + +interface IMransactionPreviewProps { + message?: string + warning?: string +} + +export const MessagePreview = ({ + warning, + message, +}: IMransactionPreviewProps) => ( + + {message && {message}} + {warning && ( + + {warning} + + )} + +) diff --git a/src/solana/WalletSIgnBottomSheetSimulated.tsx b/src/solana/WalletSIgnBottomSheetSimulated.tsx index ce0302c41..7037e0f29 100644 --- a/src/solana/WalletSIgnBottomSheetSimulated.tsx +++ b/src/solana/WalletSIgnBottomSheetSimulated.tsx @@ -28,7 +28,7 @@ import { ScrollView } from 'react-native-gesture-handler' import { useSolana } from './SolanaProvider' import WalletSignBottomSheetTransaction from './WalletSignBottomSheetTransaction' import { - WalletSignOptsSimulated, + WalletSignOpts, WalletStandardMessageTypes, } from './walletSignBottomSheetTypes' @@ -36,7 +36,7 @@ const WELL_KNOWN_CANOPY_URL = 'https://shdw-drive.genesysgo.net/6tcnBSybPG7piEDShBcrVtYJDPSvGrDbVvXmXKpzBvWP/merkles.json' let wellKnownCanopyCache: Record | undefined -type IWalletSignBottomSheetSimulatedProps = WalletSignOptsSimulated & { +type IWalletSignBottomSheetSimulatedProps = WalletSignOpts & { onCancel: () => void onAccept: () => void } @@ -201,9 +201,9 @@ export const WalletSignBottomSheetSimulated = ({ return ( {header || url ? ( - + {header ? ( - + {header} ) : null} diff --git a/src/solana/WalletSignBottomSheet.tsx b/src/solana/WalletSignBottomSheet.tsx index b651d55b5..15823a9ac 100644 --- a/src/solana/WalletSignBottomSheet.tsx +++ b/src/solana/WalletSignBottomSheet.tsx @@ -13,6 +13,7 @@ import React, { useCallback, useEffect, useImperativeHandle, + useMemo, useRef, useState, } from 'react' @@ -38,7 +39,7 @@ const WalletSignBottomSheet = forwardRef( const animatedContentHeight = useSharedValue(0) const bottomSheetModalRef = useRef(null) - const [isCompact, setIsCompact] = useState(true) + const [simulated, setSimulated] = useState(false) const [walletSignOpts, setWalletSignOpts] = useState({ type: WalletStandardMessageTypes.connect, url: undefined, @@ -48,12 +49,18 @@ const WalletSignBottomSheet = forwardRef( suppressWarnings: false, }) + const hasRenderer = useMemo( + () => walletSignOpts.renderer !== undefined, + [walletSignOpts], + ) + useEffect(() => { bottomSheetModalRef.current?.present() }, [bottomSheetModalRef]) const hide = useCallback(() => { bottomSheetModalRef.current?.close() + setSimulated(false) }, []) const show = useCallback((opts: WalletSignOpts) => { @@ -130,9 +137,10 @@ const WalletSignBottomSheet = forwardRef( contentHeight={animatedContentHeight} > - {isCompact(walletSignOpts) ? ( + {hasRenderer && !simulated ? ( setSimulated(true)} onAccept={onAcceptHandler} onCancel={onCancelHandler} /> diff --git a/src/solana/WalletSignBottomSheetCompact.tsx b/src/solana/WalletSignBottomSheetCompact.tsx index 62aca11fc..7e775c363 100644 --- a/src/solana/WalletSignBottomSheetCompact.tsx +++ b/src/solana/WalletSignBottomSheetCompact.tsx @@ -12,9 +12,10 @@ import { LAMPORTS_PER_SOL } from '@solana/web3.js' import BN from 'bn.js' import { getBasePriorityFee } from '@utils/walletApiV2' import { useAsync } from 'react-async-hook' -import { WalletSignOptsCompact } from './walletSignBottomSheetTypes' +import { WalletSignOpts } from './walletSignBottomSheetTypes' -type IWalletSignBottomSheetCompactProps = WalletSignOptsCompact & { +type IWalletSignBottomSheetCompactProps = WalletSignOpts & { + onSimulate: () => void onCancel: () => void onAccept: () => void } @@ -79,12 +80,10 @@ export const WalletSignBottomSheetCompact = ({ )} {!(insufficientFunds || insufficientRentExempt) && ( - - {header || t('transactions.signTxn')} - + {header || t('transactions.signTxn')} )} - {!(insufficientFunds || insufficientRentExempt) && ( + {!(insufficientFunds || insufficientRentExempt) && message && ( {message} From 0a5b2374970d9c43c21eec64ec9d0606ff720116 Mon Sep 17 00:00:00 2001 From: bry Date: Wed, 24 Jul 2024 17:29:25 -0500 Subject: [PATCH 4/7] Update all messages for bottom drawer --- src/features/governance/PositionCard.tsx | 42 ++++++++----- src/features/governance/ProposalScreen.tsx | 27 ++++++-- src/features/governance/VotingPowerScreen.tsx | 62 ++++++++++++------- src/features/swaps/SwapScreen.tsx | 2 +- src/hooks/useSubmitTxn.tsx | 2 +- 5 files changed, 90 insertions(+), 45 deletions(-) diff --git a/src/features/governance/PositionCard.tsx b/src/features/governance/PositionCard.tsx index 3c97b0e6f..589c708e5 100644 --- a/src/features/governance/PositionCard.tsx +++ b/src/features/governance/PositionCard.tsx @@ -43,6 +43,7 @@ import { useCreateOpacity } from '@theme/themeHooks' import { MAX_TRANSACTIONS_PER_SIGNATURE_BATCH } from '@utils/constants' import { daysToSecs, + getFormattedStringFromDays, getMinDurationFmt, getTimeLeftFromNowFmt, secsToDays, @@ -152,14 +153,12 @@ export const PositionCard = ({ header, message, instructions, - renderer, sigs = [], }: { header: string message: string instructions: TransactionInstruction[] sigs?: Keypair[] - renderer?: () => React.ReactNode }) => { if (!anchorProvider || !walletSignBottomSheetRef) return @@ -181,10 +180,8 @@ export const PositionCard = ({ type: WalletStandardMessageTypes.signTransaction, url: '', header, + renderer: () => , serializedTxs: txs.map((t) => Buffer.from(t.serialize())), - renderer: !renderer - ? () => - : renderer, }) if (decision) { @@ -380,9 +377,11 @@ export const PositionCard = ({ header: isConstant ? t('gov.transactions.unpauseLockup') : t('gov.transactions.pauseLockup'), - message: isConstant - ? 'Are you sure you want to upause this positions lockup?' - : 'Are you sure you want to pause this positions lockup?', + message: `Your current position of ${lockedTokens} ${symbol} is ${ + isConstant ? 'paused' : 'decaying' + }, please confirm whether you'd like to ${ + isConstant ? 'let it decay' : 'pause it' + } or not?`, instructions: ixs, }) @@ -400,7 +399,14 @@ export const PositionCard = ({ onInstructions: async (ixs) => { await decideAndExecute({ header: t('gov.transactions.extendPosition'), - message: "Are you sure you want to extend this position's lockup?", + message: `Are you sure you want to extend this position's lockup from ${ + isConstant + ? getMinDurationFmt( + position.lockup.startTs, + position.lockup.endTs, + ) + : getTimeLeftFromNowFmt(position.lockup.endTs) + } to ${getFormattedStringFromDays(values.lockupPeriodInDays)}?`, instructions: ixs, }) if (!extendingError) { @@ -419,7 +425,11 @@ export const PositionCard = ({ onInstructions: async (ixs, sigs) => { await decideAndExecute({ header: t('gov.transactions.splitPosition'), - message: "Are you sure you want to split this position's tokens?", + message: `Are you sure you want to split ${ + values.amount + } ${symbol} to a new position with a ${values.lockupKind.display.toLocaleLowerCase()} lockup of ${getFormattedStringFromDays( + values.lockupPeriodInDays, + )}?`, instructions: ixs, sigs, }) @@ -442,7 +452,11 @@ export const PositionCard = ({ onInstructions: async (ixs) => { await decideAndExecute({ header: t('gov.transactions.transferPosition'), - message: "Are you sure you want to transfer this position's tokens?", + message: `Are you sure you want to transfer ${amount} ${symbol} to the position with ${humanReadable( + targetPosition.amountDepositedNative, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + mintAcc!.decimals, + )} ${symbol}?`, instructions: ixs, }) @@ -460,8 +474,7 @@ export const PositionCard = ({ onInstructions: async (ixs) => { await decideAndExecute({ header: t('gov.transactions.delegatePosition'), - message: `Are you sure you want to delegate this position's tokens to the - ${subDao.dntMetadata.name} subdao?`, + message: `Are you sure you want to delegate ${lockedTokens} ${symbol} to the ${subDao.dntMetadata.name} subdao?`, instructions: ixs, }) @@ -488,8 +501,7 @@ export const PositionCard = ({ await decideAndExecute({ header: t('gov.transactions.undelegatePosition'), - message: - "Are you sure you want to undelegate this position's tokens?", + message: `Are you sure you want to undelegate ${lockedTokens} ${symbol}?`, instructions: [undelegate], }) diff --git a/src/features/governance/ProposalScreen.tsx b/src/features/governance/ProposalScreen.tsx index ef8cade9a..9f2ec342b 100644 --- a/src/features/governance/ProposalScreen.tsx +++ b/src/features/governance/ProposalScreen.tsx @@ -42,6 +42,7 @@ import { useTranslation } from 'react-i18next' import { ScrollView } from 'react-native' import Markdown from 'react-native-markdown-display' import { Edge } from 'react-native-safe-area-context' +import { MessagePreview } from '../../solana/MessagePreview' import { useSolana } from '../../solana/SolanaProvider' import { useWalletSign } from '../../solana/WalletSignProvider' import { WalletStandardMessageTypes } from '../../solana/walletSignBottomSheetTypes' @@ -183,10 +184,15 @@ export const ProposalScreen = () => { [proposal], ) - const decideAndExecute = async ( - header: string, - instructions: TransactionInstruction[], - ) => { + const decideAndExecute = async ({ + header, + message, + instructions, + }: { + header: string + message: string + instructions: TransactionInstruction[] + }) => { if (!anchorProvider || !walletSignBottomSheetRef) return const transactions = await batchInstructionsToTxsWithPriorityFee( @@ -207,6 +213,7 @@ export const ProposalScreen = () => { type: WalletStandardMessageTypes.signTransaction, url: '', header, + renderer: () => , serializedTxs: txs.map((transaction) => Buffer.from(transaction.serialize()), ), @@ -232,7 +239,11 @@ export const ProposalScreen = () => { await vote({ choice: choice.index, onInstructions: (ixs) => - decideAndExecute(t('gov.transactions.castVote'), ixs), + decideAndExecute({ + header: t('gov.transactions.castVote'), + message: `Are you sure you want to cast your vote for ${choice.name}?`, + instructions: ixs, + }), }) } } @@ -243,7 +254,11 @@ export const ProposalScreen = () => { relinquishVote({ choice: choice.index, onInstructions: async (instructions) => - decideAndExecute(t('gov.transactions.relinquishVote'), instructions), + decideAndExecute({ + header: t('gov.transactions.relinquishVote'), + message: `Are you sure you want to relinquish your vote for ${choice.name}?`, + instructions, + }), }) } } diff --git a/src/features/governance/VotingPowerScreen.tsx b/src/features/governance/VotingPowerScreen.tsx index 5f9fcad13..2cebf1c7a 100644 --- a/src/features/governance/VotingPowerScreen.tsx +++ b/src/features/governance/VotingPowerScreen.tsx @@ -6,16 +6,17 @@ import { DelayedFadeIn } from '@components/FadeInOut' import Text from '@components/Text' import { useOwnedAmount } from '@helium/helium-react-hooks' import { + HELIUM_COMMON_LUT, + HELIUM_COMMON_LUT_DEVNET, HNT_MINT, Status, batchInstructionsToTxsWithPriorityFee, bulkSendTransactions, + humanReadable, sendAndConfirmWithRetry, toBN, toNumber, toVersionedTx, - HELIUM_COMMON_LUT_DEVNET, - HELIUM_COMMON_LUT, } from '@helium/spl-utils' import { calcLockupMultiplier, @@ -23,18 +24,20 @@ import { useCreatePosition, } from '@helium/voter-stake-registry-hooks' import { useCurrentWallet } from '@hooks/useCurrentWallet' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' import { RouteProp, useRoute } from '@react-navigation/native' import { Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js' import { useGovernance } from '@storage/GovernanceProvider' import globalStyles from '@theme/globalStyles' import { MAX_TRANSACTIONS_PER_SIGNATURE_BATCH } from '@utils/constants' -import { daysToSecs } from '@utils/dateTools' +import { daysToSecs, getFormattedStringFromDays } from '@utils/dateTools' import { getBasePriorityFee } from '@utils/walletApiV2' import BN from 'bn.js' import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { ScrollView } from 'react-native' import { Edge } from 'react-native-safe-area-context' +import { MessagePreview } from '../../solana/MessagePreview' import { useSolana } from '../../solana/SolanaProvider' import { useWalletSign } from '../../solana/WalletSignProvider' import { WalletStandardMessageTypes } from '../../solana/walletSignBottomSheetTypes' @@ -62,6 +65,7 @@ export const VotingPowerScreen = () => { positions, setMint, } = useGovernance() + const { symbol } = useMetaplexMetadata(mint) const { amount: ownedAmount, decimals } = useOwnedAmount(wallet, mint) const { error: createPositionError, createPosition } = useCreatePosition() const { @@ -121,13 +125,21 @@ export const VotingPowerScreen = () => { const { anchorProvider } = useSolana() - const decideAndExecute = async ( - header: string, - instructions: TransactionInstruction[], - sigs: Keypair[] = [], - onProgress: (status: Status) => void = () => {}, + const decideAndExecute = async ({ + header, + message, + instructions, + sigs = [], + onProgress = () => {}, sequentially = false, - ) => { + }: { + header: string + message: string + instructions: TransactionInstruction[] + sigs?: Keypair[] + onProgress?: (status: Status) => void + sequentially?: boolean + }) => { if (!anchorProvider || !walletSignBottomSheetRef) return const transactions = await batchInstructionsToTxsWithPriorityFee( @@ -152,6 +164,7 @@ export const VotingPowerScreen = () => { Buffer.from(transaction.serialize()), ), suppressWarnings: sequentially, + renderer: () => , }) if (decision) { @@ -196,7 +209,7 @@ export const VotingPowerScreen = () => { const handleLockTokens = async (values: LockTokensModalFormValues) => { const { amount, lockupPeriodInDays, lockupKind, subDao } = values - if (decimals && walletSignBottomSheetRef) { + if (decimals && walletSignBottomSheetRef && symbol) { const amountToLock = toBN(amount, decimals) await createPosition({ @@ -206,13 +219,18 @@ export const VotingPowerScreen = () => { mint, subDao, onInstructions: (ixs, sigs) => - decideAndExecute( - t('gov.transactions.lockTokens'), - ixs, + decideAndExecute({ + header: t('gov.transactions.lockTokens'), + message: `Are you sure you want to lock ${humanReadable( + amountToLock, + decimals, + )} ${symbol} for ${getFormattedStringFromDays( + lockupPeriodInDays, + )}?`, + instructions: ixs, sigs, - undefined, - !!subDao, - ), + sequentially: !!subDao, + }), }) refetchState() @@ -224,12 +242,12 @@ export const VotingPowerScreen = () => { await claimAllPositionsRewards({ positions: positionsWithRewards, onInstructions: (ixs) => - decideAndExecute( - t('gov.transactions.claimRewards'), - ixs, - undefined, - setStatusOfClaim, - ), + decideAndExecute({ + header: t('gov.transactions.claimRewards'), + message: 'Approve this transaction to claim your rewards', + instructions: ixs, + onProgress: setStatusOfClaim, + }), }) if (!claimingAllRewardsError) { diff --git a/src/features/swaps/SwapScreen.tsx b/src/features/swaps/SwapScreen.tsx index b73701854..07da2cce4 100644 --- a/src/features/swaps/SwapScreen.tsx +++ b/src/features/swaps/SwapScreen.tsx @@ -815,7 +815,7 @@ const SwapScreen = () => { onPress={handleSwapTokens} TrailingComponent={ swapping ? ( - + ) : undefined } /> diff --git a/src/hooks/useSubmitTxn.tsx b/src/hooks/useSubmitTxn.tsx index 301755a74..bdec08d13 100644 --- a/src/hooks/useSubmitTxn.tsx +++ b/src/hooks/useSubmitTxn.tsx @@ -192,7 +192,7 @@ export default () => { const decision = await walletSignBottomSheetRef.show({ type: WalletStandardMessageTypes.signTransaction, url: '', - header: t('transactions.swapTokens'), + header: t('swapsScreen.swapTokens'), message: t('transactions.signSwapTxn'), serializedTxs: [Buffer.from(serializedTx)], suppressWarnings: true, From df3d153dd2d6a0600788592f0397b8c02eabc89b Mon Sep 17 00:00:00 2001 From: bry Date: Thu, 25 Jul 2024 12:09:29 -0500 Subject: [PATCH 5/7] Add swap preview compact version --- src/features/swaps/SwapScreen.tsx | 13 +++- src/hooks/useSubmitTxn.tsx | 28 ++++++++- src/solana/SwapPreview.tsx | 101 ++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 src/solana/SwapPreview.tsx diff --git a/src/features/swaps/SwapScreen.tsx b/src/features/swaps/SwapScreen.tsx index 07da2cce4..e86562824 100644 --- a/src/features/swaps/SwapScreen.tsx +++ b/src/features/swaps/SwapScreen.tsx @@ -591,7 +591,7 @@ const SwapScreen = () => { ) const handleSwapTokens = useCallback(async () => { - if (connection && currentAccount?.solanaAddress) { + if (connection && currentAccount?.solanaAddress && inputMint) { try { setSwapping(true) @@ -624,7 +624,15 @@ const SwapScreen = () => { if (!swapTxn) { throw new Error(t('errors.swap.tx')) } - await submitJupiterSwap(swapTxn) + + await submitJupiterSwap({ + inputMint, + inputAmount, + outputMint, + outputAmount, + minReceived, + swapTxn, + }) } setSwapping(false) @@ -646,6 +654,7 @@ const SwapScreen = () => { inputAmount, outputMint, outputAmount, + minReceived, navigation, submitTreasurySwap, submitMintDataCredits, diff --git a/src/hooks/useSubmitTxn.tsx b/src/hooks/useSubmitTxn.tsx index bdec08d13..3e1d3b40e 100644 --- a/src/hooks/useSubmitTxn.tsx +++ b/src/hooks/useSubmitTxn.tsx @@ -14,6 +14,7 @@ import * as solUtils from '@utils/solanaUtils' import BN from 'bn.js' import React, { useCallback } from 'react' import { ellipsizeAddress } from '@utils/accountUtils' +import { SwapPreview } from '../solana/SwapPreview' import { CollectablePreview } from '../solana/CollectablePreview' import { MessagePreview } from '../solana/MessagePreview' import { PaymentPreivew } from '../solana/PaymentPreview' @@ -182,7 +183,21 @@ export default () => { ) const submitJupiterSwap = useCallback( - async (swapTxn: VersionedTransaction) => { + async ({ + inputMint, + inputAmount, + outputMint, + outputAmount, + minReceived, + swapTxn, + }: { + inputMint: PublicKey + inputAmount: number + outputMint: PublicKey + outputAmount: number + minReceived: number + swapTxn: VersionedTransaction + }) => { if (!currentAccount || !anchorProvider || !walletSignBottomSheetRef) { throw new Error(t('errors.account')) } @@ -196,6 +211,17 @@ export default () => { message: t('transactions.signSwapTxn'), serializedTxs: [Buffer.from(serializedTx)], suppressWarnings: true, + renderer: () => ( + + ), }) if (!decision) { diff --git a/src/solana/SwapPreview.tsx b/src/solana/SwapPreview.tsx new file mode 100644 index 000000000..6dc589ee4 --- /dev/null +++ b/src/solana/SwapPreview.tsx @@ -0,0 +1,101 @@ +import Send from '@assets/images/send.svg' +import Receive from '@assets/images/receive.svg' +import Box from '@components/Box' +import CircleLoader from '@components/CircleLoader' +import { Pill } from '@components/Pill' +import Text from '@components/Text' +import TokenIcon from '@components/TokenIcon' +import { useMint } from '@helium/helium-react-hooks' +import { useMetaplexMetadata } from '@hooks/useMetaplexMetadata' +import { PublicKey } from '@solana/web3.js' +import React from 'react' + +interface ISwapPreviewProps { + inputMint: PublicKey + inputAmount: number + outputMint: PublicKey + outputAmount: number + minReceived: number +} + +export const SwapPreview = ({ + inputMint, + inputAmount, + outputMint, + outputAmount, + minReceived, +}: ISwapPreviewProps) => { + const outputDecimals = useMint(outputMint)?.info?.decimals + const { + loading: loadingInputMintMetadata, + symbol: inputMintSymbol, + json: inputMintJson, + } = useMetaplexMetadata(inputMint) + const { + loading: loadingOutputMintMetadata, + symbol: outputMintSymbol, + json: outputMintJson, + } = useMetaplexMetadata(outputMint) + + return ( + + {loadingInputMintMetadata || loadingOutputMintMetadata ? ( + + ) : ( + <> + + + {inputMintJson ? ( + + ) : null} + {inputMintSymbol} + + + + + + + + {outputMintJson ? ( + + ) : null} + {outputMintSymbol} + + + + + + + Min Recieved due to slippage: + + {`~${minReceived.toFixed(outputDecimals)}`} + + + + )} + + ) +} From 0dc8e2d5527030896482385fc3624be33e058657 Mon Sep 17 00:00:00 2001 From: bry Date: Thu, 25 Jul 2024 12:41:18 -0500 Subject: [PATCH 6/7] update text for position cards --- src/features/governance/PositionCard.tsx | 8 ++++---- src/locales/en.ts | 12 ++++++++---- src/solana/SwapPreview.tsx | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/features/governance/PositionCard.tsx b/src/features/governance/PositionCard.tsx index 589c708e5..505cb7477 100644 --- a/src/features/governance/PositionCard.tsx +++ b/src/features/governance/PositionCard.tsx @@ -568,7 +568,7 @@ export const PositionCard = ({ <> { setActionsOpen(false) if (hasActiveVotes) { @@ -585,7 +585,7 @@ export const PositionCard = ({ /> { setActionsOpen(false) if (hasActiveVotes) { @@ -614,8 +614,8 @@ export const PositionCard = ({ key="pause" title={ isConstant - ? t('gov.transactions.unpauseLockup') - : t('gov.transactions.pauseLockup') + ? t('gov.positions.unpause') + : t('gov.positions.pause') } onPress={async () => { setActionsOpen(false) diff --git a/src/locales/en.ts b/src/locales/en.ts index fbf477569..617fdc298 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1267,10 +1267,14 @@ export default { lockedAmount: 'Locked Amount {{amount}} {{symbol}}', constant: 'Constant', decaying: 'Decaying', - delegate: 'Delegate', - undelegate: 'Undelegate', - extend: 'Extend', - close: 'Close', + delegate: 'Delegate position', + undelegate: 'Undelegate position', + extend: 'Extend position', + close: 'Close position', + split: 'Split position', + transfer: 'Transfer between positions', + unpause: 'Unlock to start decaying', + pause: 'Pause to stop decaying', unableToClose: 'Unable to close', unableToSplit: 'Unable to split', unableToTransfer: 'Unable to transfer', diff --git a/src/solana/SwapPreview.tsx b/src/solana/SwapPreview.tsx index 6dc589ee4..ad20201d4 100644 --- a/src/solana/SwapPreview.tsx +++ b/src/solana/SwapPreview.tsx @@ -89,7 +89,7 @@ export const SwapPreview = ({ alignItems="center" justifyContent="space-between" > - Min Recieved due to slippage: + Min Received due to slippage: {`~${minReceived.toFixed(outputDecimals)}`} From ef7e38e719c1e2149423a9c6a2c69faf0c50218b Mon Sep 17 00:00:00 2001 From: bry Date: Thu, 25 Jul 2024 13:50:51 -0500 Subject: [PATCH 7/7] Move messages to en.ts --- src/features/governance/PositionCard.tsx | 62 ++++++++++++------- src/features/governance/ProposalScreen.tsx | 6 +- src/features/governance/VotingPowerScreen.tsx | 11 ++-- src/locales/en.ts | 16 +++++ 4 files changed, 63 insertions(+), 32 deletions(-) diff --git a/src/features/governance/PositionCard.tsx b/src/features/governance/PositionCard.tsx index 505cb7477..0d6bf5309 100644 --- a/src/features/governance/PositionCard.tsx +++ b/src/features/governance/PositionCard.tsx @@ -359,7 +359,7 @@ export const PositionCard = ({ onInstructions: async (ixs) => { await decideAndExecute({ header: t('gov.transactions.closePosition'), - message: 'Are you sure you want to close your position?', + message: t('gov.positions.closeMessage'), instructions: ixs, }) if (!closingError) { @@ -377,11 +377,12 @@ export const PositionCard = ({ header: isConstant ? t('gov.transactions.unpauseLockup') : t('gov.transactions.pauseLockup'), - message: `Your current position of ${lockedTokens} ${symbol} is ${ - isConstant ? 'paused' : 'decaying' - }, please confirm whether you'd like to ${ - isConstant ? 'let it decay' : 'pause it' - } or not?`, + message: t('gov.positions.flipLockupMessage', { + amount: lockedTokens, + symbol, + status: isConstant ? 'paused' : 'decaying', + action: isConstant ? 'let it decay' : 'pause it', + }), instructions: ixs, }) @@ -399,14 +400,15 @@ export const PositionCard = ({ onInstructions: async (ixs) => { await decideAndExecute({ header: t('gov.transactions.extendPosition'), - message: `Are you sure you want to extend this position's lockup from ${ - isConstant + message: t('gov.positions.extendMessage', { + existing: isConstant ? getMinDurationFmt( position.lockup.startTs, position.lockup.endTs, ) - : getTimeLeftFromNowFmt(position.lockup.endTs) - } to ${getFormattedStringFromDays(values.lockupPeriodInDays)}?`, + : getTimeLeftFromNowFmt(position.lockup.endTs), + new: getFormattedStringFromDays(values.lockupPeriodInDays), + }), instructions: ixs, }) if (!extendingError) { @@ -425,11 +427,12 @@ export const PositionCard = ({ onInstructions: async (ixs, sigs) => { await decideAndExecute({ header: t('gov.transactions.splitPosition'), - message: `Are you sure you want to split ${ - values.amount - } ${symbol} to a new position with a ${values.lockupKind.display.toLocaleLowerCase()} lockup of ${getFormattedStringFromDays( - values.lockupPeriodInDays, - )}?`, + message: t('gov.positions.splitMessage', { + amount: values.amount, + symbol, + lockupKind: values.lockupKind.display.toLocaleLowerCase(), + duration: getFormattedStringFromDays(values.lockupPeriodInDays), + }), instructions: ixs, sigs, }) @@ -452,11 +455,15 @@ export const PositionCard = ({ onInstructions: async (ixs) => { await decideAndExecute({ header: t('gov.transactions.transferPosition'), - message: `Are you sure you want to transfer ${amount} ${symbol} to the position with ${humanReadable( - targetPosition.amountDepositedNative, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - mintAcc!.decimals, - )} ${symbol}?`, + message: t('gov.positions.transferMessage', { + amount, + symbol, + targetAmount: humanReadable( + targetPosition.amountDepositedNative, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + mintAcc!.decimals, + ), + }), instructions: ixs, }) @@ -474,7 +481,11 @@ export const PositionCard = ({ onInstructions: async (ixs) => { await decideAndExecute({ header: t('gov.transactions.delegatePosition'), - message: `Are you sure you want to delegate ${lockedTokens} ${symbol} to the ${subDao.dntMetadata.name} subdao?`, + message: t('gov.positions.delegateMessage', { + amount: lockedTokens, + symbol, + subDao: subDao.dntMetadata.name, + }), instructions: ixs, }) @@ -494,14 +505,17 @@ export const PositionCard = ({ if (claims.length > 0) { await decideAndExecute({ header: t('gov.transactions.claimRewards'), - message: 'Claim your rewards', + message: t('gov.transactions.claimRewards'), instructions: claims, }) } await decideAndExecute({ header: t('gov.transactions.undelegatePosition'), - message: `Are you sure you want to undelegate ${lockedTokens} ${symbol}?`, + message: t('gov.positions.undelegateMessage', { + amount: lockedTokens, + symbol, + }), instructions: [undelegate], }) @@ -519,7 +533,7 @@ export const PositionCard = ({ onInstructions: async (ixs) => { await decideAndExecute({ header: t('gov.transactions.relinquishPosition'), - message: "Are you sure you want to relinquish this position's votes?", + message: t('gov.positions.relinquishVotesMessage'), instructions: ixs, }) diff --git a/src/features/governance/ProposalScreen.tsx b/src/features/governance/ProposalScreen.tsx index 9f2ec342b..18be2901b 100644 --- a/src/features/governance/ProposalScreen.tsx +++ b/src/features/governance/ProposalScreen.tsx @@ -241,7 +241,7 @@ export const ProposalScreen = () => { onInstructions: (ixs) => decideAndExecute({ header: t('gov.transactions.castVote'), - message: `Are you sure you want to cast your vote for ${choice.name}?`, + message: t('gov.proposals.castVoteFor', { choice: choice.name }), instructions: ixs, }), }) @@ -256,7 +256,9 @@ export const ProposalScreen = () => { onInstructions: async (instructions) => decideAndExecute({ header: t('gov.transactions.relinquishVote'), - message: `Are you sure you want to relinquish your vote for ${choice.name}?`, + message: t('gov.proposals.relinquishVoteFor', { + choice: choice.name, + }), instructions, }), }) diff --git a/src/features/governance/VotingPowerScreen.tsx b/src/features/governance/VotingPowerScreen.tsx index 2cebf1c7a..d06f9b019 100644 --- a/src/features/governance/VotingPowerScreen.tsx +++ b/src/features/governance/VotingPowerScreen.tsx @@ -221,12 +221,11 @@ export const VotingPowerScreen = () => { onInstructions: (ixs, sigs) => decideAndExecute({ header: t('gov.transactions.lockTokens'), - message: `Are you sure you want to lock ${humanReadable( - amountToLock, - decimals, - )} ${symbol} for ${getFormattedStringFromDays( - lockupPeriodInDays, - )}?`, + message: t('gov.votingPower.lockYourTokens', { + amount: humanReadable(amountToLock, decimals), + symbol, + duration: getFormattedStringFromDays(lockupPeriodInDays), + }), instructions: ixs, sigs, sequentially: !!subDao, diff --git a/src/locales/en.ts b/src/locales/en.ts index 617fdc298..c816bbc3e 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1261,6 +1261,7 @@ export default { locked: '{{symbol}} Locked', youHave: 'You have {{amount}} more {{symbol}} available to lock.', increase: 'Increase your voting power by locking tokens.', + lockYourTokens: 'Lock {{amount}} {{symbol}} for {{duration}}?', }, positions: { relinquish: 'Relinquish Votes', @@ -1317,6 +1318,19 @@ export default { selectTransfer: 'Select position to transfer too.', selectSubDao: 'Select a existing SubNetwork to delegate to.', fetchingSubDaos: 'Fetching SubDaos...', + closeMessage: 'Close this position?', + flipLockupMesage: + "Your current position of {{amount}} {{symbol}} is {{status}}, please confirm whether you'd like to {{action}} or not?", + extendMessage: + 'Extend this positions lockup from {{existing}} to {{new}}?', + splitMessage: + 'Split {{amount}} {{symbol}} into a new position with {{lockupKind}} lockup of {{duration}}?', + transferMessage: + 'Transfer {{amount}} {{symbol}} to the position with {{targetAmount}} {{symbol}}?', + delegateMessage: + 'delegate {{amount}} {{symbol}} to the {{subdao}} subdao?', + undelegateMessage: 'Undelegate {{amount}} {{symbol}}?', + relinquishVotesMessage: 'Relinquish this positions votes?', }, proposals: { overviewTitle: 'Proposal Overview', @@ -1330,6 +1344,8 @@ export default { failed: 'Failed', cancelled: 'Cancelled', votes: 'Votes', + castVoteFor: 'Cast your vote for {{choice}}?', + relinquishVoteFor: 'Relinquish your vote for {{choice}}?', toVote: 'To vote, click on any option. To remove your vote, click the option again. Vote for up to {{maxChoicesPerVoter}} of {{choicesLength}} options.', },