From 5a2a7c7b90d0a3ea0c4b725d453ba773333f9445 Mon Sep 17 00:00:00 2001 From: felimadu Date: Tue, 15 Jul 2025 01:45:04 -0400 Subject: [PATCH 1/3] using useGetOperationsToExecuteTransaction for app transactions --- src/shared/core/useIsOrbyEnabled.ts | 15 ++ .../SendTransaction/NetworkFee/NetworkFee.tsx | 157 +++++++++++++++++- .../pages/SendTransaction/SendTransaction.tsx | 101 ++++++++++- .../TransactionAdvancedView.tsx | 16 ++ .../TransactionConfiguration.tsx | 32 ++++ 5 files changed, 317 insertions(+), 4 deletions(-) create mode 100644 src/shared/core/useIsOrbyEnabled.ts diff --git a/src/shared/core/useIsOrbyEnabled.ts b/src/shared/core/useIsOrbyEnabled.ts new file mode 100644 index 0000000000..d518290a00 --- /dev/null +++ b/src/shared/core/useIsOrbyEnabled.ts @@ -0,0 +1,15 @@ +import { useIsOrbySupportedChain } from '@orb-labs/orby-react'; +import { useMemo } from 'react'; +import _ from 'lodash'; +import { useIsOneClickTransactionsAndGasAbstractionEnabled } from './useIsOneClickTransactionsAndGasAbstractionEnabled'; + +export function useIsOrbyEnabled(chainId?: bigint | undefined) { + const oneClickTransactionsAndGasAbstractionEnabled = + useIsOneClickTransactionsAndGasAbstractionEnabled(); + + const { isSupported } = useIsOrbySupportedChain(chainId); + + return useMemo(() => { + return oneClickTransactionsAndGasAbstractionEnabled && isSupported; + }, [oneClickTransactionsAndGasAbstractionEnabled, isSupported]); +} diff --git a/src/ui/pages/SendTransaction/NetworkFee/NetworkFee.tsx b/src/ui/pages/SendTransaction/NetworkFee/NetworkFee.tsx index 6dcb7427e3..281bdb4078 100644 --- a/src/ui/pages/SendTransaction/NetworkFee/NetworkFee.tsx +++ b/src/ui/pages/SendTransaction/NetworkFee/NetworkFee.tsx @@ -15,9 +15,26 @@ import type { IncomingTransactionWithFrom } from 'src/modules/ethereum/types/Inc import { useNetworks } from 'src/modules/networks/useNetworks'; import { useCurrency } from 'src/modules/currency/useCurrency'; import { formatCurrencyValueExtra } from 'src/shared/units/formatCurrencyValue'; +import type { StandardizedBalance } from '@orb-labs/orby-core'; +import { TokenIcon } from 'src/ui/ui-kit/TokenIcon'; +import { BottomSheetDialog } from 'src/ui/ui-kit/ModalDialogs/BottomSheetDialog'; +import { DialogTitle } from 'src/ui/ui-kit/ModalDialogs/DialogTitle'; +import { SurfaceList } from 'src/ui/ui-kit/SurfaceList'; +import { VStack } from 'src/ui/ui-kit/VStack'; +import { createPortal } from 'react-dom'; +import { getRootDomNode } from 'src/ui/shared/getRootDomNode'; +import DownIcon from 'jsx:src/ui/assets/chevron-down.svg'; +import { useIsOrbyEnabled } from 'src/shared/core/useIsOrbyEnabled'; import type { TransactionFee } from '../TransactionConfiguration/useTransactionFee'; -import { NetworkFeeDialog } from './NetworkFeeDialog'; import { NETWORK_SPEED_TO_TITLE } from './constants'; +import { NetworkFeeDialog } from './NetworkFeeDialog'; + +export interface GasTokenInput { + name: string; + standardizedTokenId?: string; + isDefault: boolean; + url?: string; +} function getFeeTypeTitle( type: Exclude | undefined @@ -32,6 +49,128 @@ function getFeeTypeTitle( return labels[type]; } +export function GasTokenSelector({ + selectedGasToken, + setSelectedGasToken, + fungibleTokens, +}: { + selectedGasToken?: GasTokenInput; + setSelectedGasToken?: (gasToken?: GasTokenInput) => void; + fungibleTokens?: StandardizedBalance[] | undefined; +}) { + const dialogRef = useRef(null); + const rootNode = getRootDomNode(); + + const handleSelectGasToken = (token: StandardizedBalance) => { + const gasToken: GasTokenInput = { + name: token.total.currency.symbol, + standardizedTokenId: token.standardizedTokenId, + isDefault: false, + url: token.total.currency.logoUrl, + }; + setSelectedGasToken?.(gasToken); + dialogRef.current?.close(); + }; + + const handleSelectDefault = () => { + setSelectedGasToken?.({ + name: 'Native Token', + standardizedTokenId: undefined, + isDefault: true, + }); + dialogRef.current?.close(); + }; + + const items = [ + { + key: 'default', + component: ( + + + No Gas Abstraction + + ), + onClick: handleSelectDefault, + isInteractive: true, + }, + ...(fungibleTokens + ? fungibleTokens.map((token, index) => ({ + key: token.standardizedTokenId || `token-${index}`, + component: ( + + + {token.total.currency.symbol} + + ), + onClick: () => handleSelectGasToken(token), + isInteractive: true, + })) + : []), + ]; + + return ( + <> + {createPortal( + + Select Gas Token} + alignTitle="start" + /> + + + + , + rootNode + )} + + { + dialogRef.current?.showModal(); + }} + > + + + + {selectedGasToken?.name || 'Select Token'} + + + + + + ); +} + export function NetworkFee({ transaction, transactionFee, @@ -43,6 +182,9 @@ export function NetworkFee({ label, renderDisplayValue, displayValueEnd, + selectedGasToken, + setSelectedGasToken, + fungibleTokens, }: { transaction: IncomingTransactionWithFrom; transactionFee: TransactionFee; @@ -57,9 +199,15 @@ export function NetworkFee({ displayValue: string; }) => React.ReactNode; displayValueEnd?: React.ReactNode; + selectedGasToken?: GasTokenInput; + setSelectedGasToken?: (gasToken?: GasTokenInput) => void; + fungibleTokens?: StandardizedBalance[] | undefined; }) { const { networks } = useNetworks(); const { currency } = useCurrency(); + const isOrbyEnabled = useIsOrbyEnabled( + transaction.chainId ? BigInt(transaction.chainId) : undefined + ); const dialogRef = useRef(null); const { time, feeEstimation, feeEstimationQuery, costs, costsQuery } = transactionFee; @@ -138,6 +286,13 @@ export function NetworkFee({ ) : ( Network Fee )} + {selectedGasToken && isOrbyEnabled ? ( + + ) : null} {isLoading ? ( ) : displayValue ? ( diff --git a/src/ui/pages/SendTransaction/SendTransaction.tsx b/src/ui/pages/SendTransaction/SendTransaction.tsx index e49fa4b72e..b4cebb529f 100644 --- a/src/ui/pages/SendTransaction/SendTransaction.tsx +++ b/src/ui/pages/SendTransaction/SendTransaction.tsx @@ -99,6 +99,10 @@ import ScrollIcon from 'jsx:src/ui/assets/scroll.svg'; import ArrowDownIcon from 'jsx:src/ui/assets/caret-down-filled.svg'; import { SiteFaviconImg } from 'src/ui/components/SiteFaviconImg'; import { NetworkId } from 'src/modules/networks/NetworkId'; +import { useGetOperationsToExecuteTransaction } from '@orb-labs/orby-react'; +import { CreateOperationsStatus } from '@orb-labs/orby-core'; +import _ from 'lodash'; +import { useIsOrbyEnabled } from 'src/shared/core/useIsOrbyEnabled'; import type { PopoverToastHandle } from '../Settings/PopoverToast'; import { PopoverToast } from '../Settings/PopoverToast'; import { TransactionConfiguration } from './TransactionConfiguration'; @@ -110,6 +114,7 @@ import { TransactionAdvancedView } from './TransactionAdvancedView'; import { TransactionWarnings } from './TransactionWarnings'; import { txErrorToMessage } from './shared/transactionErrorToMessage'; import { AddressActionNetworkFee } from './TransactionConfiguration/TransactionConfiguration'; +import type { GasTokenInput } from './NetworkFee/NetworkFee'; async function configureTransactionToSign( transaction: T, @@ -226,7 +231,7 @@ function usePreparedTx(transaction: IncomingTransaction, origin: string) { queryFn: async () => withChainId ? resolveGasAndFee(withChainId, { source }) : null, enabled: Boolean(withChainId), - useErrorBoundary: true, + // useErrorBoundary: true, suspense: false, }); return { @@ -303,6 +308,8 @@ function TransactionDefaultView({ paymasterWaiting, onOpenAdvancedView, onFeeValueCommonReady, + selectedGasToken, + setSelectedGasToken, }: { networks: Networks; chain: Chain; @@ -324,6 +331,8 @@ function TransactionDefaultView({ paymasterWaiting: boolean; onOpenAdvancedView: () => void; onFeeValueCommonReady: (value: string) => void; + selectedGasToken?: GasTokenInput; + setSelectedGasToken?: (gasToken?: GasTokenInput) => void; }) { const { singleAddress } = useAddressParams(); const [params] = useSearchParams(); @@ -497,6 +506,8 @@ function TransactionDefaultView({ : null } listViewTransitions={true} + selectedGasToken={selectedGasToken} + setSelectedGasToken={setSelectedGasToken} /> ) : null} @@ -553,6 +564,9 @@ function SendTransactionContent({ }); const [allowanceQuantityBase, setAllowanceQuantityBase] = useState(''); + const [operationSetError, setOperationSetError] = useState( + null + ); const configureTransactionToBeSigned = useEvent( async ( @@ -576,6 +590,72 @@ function SendTransactionContent({ const network = networks.getByNetworkId(chain) || null; const source = preferences?.testnetMode?.on ? 'testnet' : 'mainnet'; + const isOrbyEnabled = useIsOrbyEnabled(BigInt(populatedTransaction?.chainId)); + const [selectedGasToken, setSelectedGasToken] = useState({ + name: 'Native Token', + standardizedTokenId: undefined, + isDefault: true, + }); + + const gasToken = useMemo(() => { + return selectedGasToken?.standardizedTokenId + ? { standardizedTokenId: selectedGasToken.standardizedTokenId } + : undefined; + }, [selectedGasToken]); + + const { operationSet, isLoading: OperationSetLoading } = + useGetOperationsToExecuteTransaction( + isOrbyEnabled ? (wallet?.address as string) : undefined, + isOrbyEnabled && populatedTransaction?.chainId + ? BigInt(populatedTransaction?.chainId as number) + : undefined, + populatedTransaction?.to as string, + populatedTransaction?.data as string, + populatedTransaction?.value + ? BigInt(populatedTransaction?.value.toString()) + : undefined, + gasToken + ); + + const selectGasToken = useCallback( + (gasToken?: GasTokenInput) => { + if (gasToken) { + setSelectedGasToken(gasToken); + } + }, + [setSelectedGasToken] + ); + + // Check for operation set errors and update error state + React.useEffect(() => { + if (operationSet?.status && isOrbyEnabled) { + let errorMessage: string | null = null; + + if (operationSet.status === CreateOperationsStatus.INSUFFICIENT_FUNDS) { + errorMessage = 'Insufficient funds'; + } else if ( + operationSet.status === CreateOperationsStatus.NO_EXECUTION_PATH + ) { + errorMessage = 'No execution path'; + } else if ( + operationSet.status === + CreateOperationsStatus.INSUFFICIENT_FUNDS_FOR_GAS + ) { + errorMessage = 'Insufficient funds for gas. Choose another token'; + } else if (operationSet.status === CreateOperationsStatus.INTERNAL) { + errorMessage = 'Internal error'; + } else if ( + operationSet.status === CreateOperationsStatus.INVALID_ARGUMENT + ) { + errorMessage = 'Invalid argument'; + } + + setOperationSetError(errorMessage); + } else { + setOperationSetError(null); + } + }, [operationSet?.status, isOrbyEnabled]); + const paymasterPossible = USE_PAYMASTER_FEATURE && Boolean(network?.supports_sponsored_transactions); @@ -790,6 +870,8 @@ function SendTransactionContent({ paymasterWaiting={paymasterWaiting} onOpenAdvancedView={openAdvancedView} onFeeValueCommonReady={handleFeeValueCommonReady} + selectedGasToken={selectedGasToken} + setSelectedGasToken={selectGasToken} /> ) : null} toastRef.current?.showToast()} + operationSet={operationSet} /> )} @@ -832,6 +915,11 @@ function SendTransactionContent({ {txErrorToMessage(sendTransactionMutation.error)} ) : null} + {operationSetError ? ( + + {operationSetError} + + ) : null} {view === View.customAllowance ? ( ) : ( @@ -859,8 +947,14 @@ function SendTransactionContent({ wallet={wallet} ref={sendTxBtnRef} onClick={() => sendTransaction()} - isLoading={sendTransactionMutation.isLoading} - disabled={sendTransactionMutation.isLoading} + isLoading={ + sendTransactionMutation.isLoading || OperationSetLoading + } + disabled={ + sendTransactionMutation.isLoading || + OperationSetLoading || + !!operationSetError + } buttonKind={ interpretationHasCriticalWarning ? 'danger' : 'primary' } @@ -1077,6 +1171,7 @@ function SolDefaultView({ transaction={{ solana: rawTransaction }} addressAction={addressAction} onCopyData={() => toastRef.current?.showToast()} + operationSet={undefined} /> )} diff --git a/src/ui/pages/SendTransaction/TransactionAdvancedView/TransactionAdvancedView.tsx b/src/ui/pages/SendTransaction/TransactionAdvancedView/TransactionAdvancedView.tsx index c0d9e7b739..c717c02ecf 100644 --- a/src/ui/pages/SendTransaction/TransactionAdvancedView/TransactionAdvancedView.tsx +++ b/src/ui/pages/SendTransaction/TransactionAdvancedView/TransactionAdvancedView.tsx @@ -28,6 +28,8 @@ import { DialogButtonValue } from 'src/ui/ui-kit/ModalDialogs/DialogTitle'; import { Spacer } from 'src/ui/ui-kit/Spacer'; import { PageBottom } from 'src/ui/components/PageBottom'; import type { MultichainTransaction } from 'src/shared/types/MultichainTransaction'; +import type { OperationSet } from '@orb-labs/orby-core'; +import { TokenStateSection } from 'src/ui/components/TokenStateSection'; function maybeHexValue(value?: BigNumberish | null): string | null { return value ? valueToHex(value) : null; @@ -205,6 +207,7 @@ export function TransactionAdvancedView({ interpretation, addressAction, onCopyData, + operationSet, }: { networks: Networks; chain: Chain; @@ -212,6 +215,7 @@ export function TransactionAdvancedView({ interpretation?: InterpretResponse | null; addressAction: AnyAddressAction; onCopyData?: () => void; + operationSet?: OperationSet; }) { const transactionFormatted = useMemo(() => { if (transaction.evm) { @@ -240,6 +244,18 @@ export function TransactionAdvancedView({ chain={chain} networks={networks} /> + {operationSet && ( + <> + + + + )} {transaction.evm ? ( void; + fungibleTokens: StandardizedBalance[]; }) { const { data: chainGasPrices = null } = useGasPrices(chain, { suspense: true, @@ -234,6 +244,9 @@ function NetworkFeeLine({ chainGasPrices={chainGasPrices} networkFeeConfiguration={configuration.networkFee} label={Network Fee} + selectedGasToken={selectedGasToken} + setSelectedGasToken={setSelectedGasToken} + fungibleTokens={fungibleTokens} displayValueEnd={ paymasterPossible && !paymasterEligible ? ( <> @@ -278,6 +291,8 @@ export function TransactionConfiguration({ keepPreviousData = false, gasback: gasbackData, listViewTransitions = false, + selectedGasToken, + setSelectedGasToken, }: { transaction: IncomingTransaction; from: string; @@ -292,6 +307,8 @@ export function TransactionConfiguration({ gasback: GasbackData | null; /** Hacky, experimental and only needed on SendTransaction View because list is stuck to the bottom */ listViewTransitions?: boolean; + selectedGasToken?: GasTokenInput; + setSelectedGasToken?: (gasToken?: GasTokenInput) => void; }) { const { preferences } = usePreferences(); const transactionWithFrom = useMemo( @@ -302,6 +319,11 @@ export function TransactionConfiguration({ const [gasback, setGasback] = useState(gasbackValueOriginal); const hasGasbackOnFirstRenderRef = useRef(gasback); + const isOrbyEnabled = useIsOrbyEnabled( + incomingTransaction.chainId + ? BigInt(incomingTransaction.chainId) + : undefined + ); const { data: loyaltyEnabled } = useRemoteConfigValue( 'extension_loyalty_enabled' ); @@ -329,6 +351,13 @@ export function TransactionConfiguration({ // viewTransitionNames need to be unique when this component is used more than once on the same view const id = usePlainId(); + const { fungibleTokens } = useGetFungibleTokenPortfolio( + undefined, + transactionWithFrom.chainId && isOrbyEnabled + ? BigInt(transactionWithFrom.chainId) + : undefined + ); + return ( {paymasterWaiting ? ( @@ -370,6 +399,9 @@ export function TransactionConfiguration({ onFeeValueCommonReady={onFeeValueCommonReady} paymasterPossible={paymasterPossible} paymasterEligible={paymasterEligible} + selectedGasToken={selectedGasToken} + setSelectedGasToken={setSelectedGasToken} + fungibleTokens={fungibleTokens} /> )} From 4c407c957d3e9ec91a7435ece85a2f8469d961d3 Mon Sep 17 00:00:00 2001 From: felimadu Date: Tue, 15 Jul 2025 02:11:43 -0400 Subject: [PATCH 2/3] adding token selection code --- .../TokenStateSection/TokenStateSection.tsx | 89 +++++++++++++++++++ src/ui/components/TokenStateSection/index.ts | 1 + 2 files changed, 90 insertions(+) create mode 100644 src/ui/components/TokenStateSection/TokenStateSection.tsx create mode 100644 src/ui/components/TokenStateSection/index.ts diff --git a/src/ui/components/TokenStateSection/TokenStateSection.tsx b/src/ui/components/TokenStateSection/TokenStateSection.tsx new file mode 100644 index 0000000000..5fc27198f6 --- /dev/null +++ b/src/ui/components/TokenStateSection/TokenStateSection.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Surface } from 'src/ui/ui-kit/Surface'; +import { UIText } from 'src/ui/ui-kit/UIText'; +import { VStack } from 'src/ui/ui-kit/VStack'; +import { HStack } from 'src/ui/ui-kit/HStack'; +import type { State } from '@orb-labs/orby-core'; +import { TokenIcon } from 'src/ui/ui-kit/TokenIcon'; +import { Media } from 'src/ui/ui-kit/Media'; +import { formatTokenValue } from 'src/shared/units/formatTokenValue'; +import { AssetQuantity } from 'src/ui/components/AssetQuantity'; +import { BigNumber } from 'bignumber.js'; + +export function TokenStateSection({ + title, + tokens, +}: { + title: string; + tokens: State | undefined; +}) { + const fungibleTokens = tokens?.getFungibleTokens(); + if (!fungibleTokens) { + return null; + } + + if (fungibleTokens.length === 0) { + return ( + + + + {title} + + + No tokens moving + + + + ); + } + + return ( + + + + {title} + + {fungibleTokens.map((tokenAmount, index) => { + // Convert raw amount to decimal based on token decimals + const rawAmount = BigNumber(tokenAmount.toRawAmount().toString()); + const decimals = tokenAmount.token.currency().decimals; + const decimalAmount = rawAmount.dividedBy( + BigNumber(10).pow(decimals) + ); + + return ( + + } + text={ + + + + {tokenAmount.token.currency().symbol} + + + } + detailText={ + + {formatTokenValue( + decimalAmount, + tokenAmount.token.currency().symbol + )} + + } + /> + ); + })} + + + ); +} diff --git a/src/ui/components/TokenStateSection/index.ts b/src/ui/components/TokenStateSection/index.ts new file mode 100644 index 0000000000..a5930eb528 --- /dev/null +++ b/src/ui/components/TokenStateSection/index.ts @@ -0,0 +1 @@ +export { TokenStateSection } from './TokenStateSection'; From 70db7377bef7b60e52fd1525cd90a455fc64300e Mon Sep 17 00:00:00 2001 From: felimadu Date: Tue, 22 Jul 2025 18:11:01 -0400 Subject: [PATCH 3/3] refactoring --- .../pages/SendTransaction/SendTransaction.tsx | 67 +++---------------- src/ui/shared/hooks/useOperationSetError.ts | 47 +++++++++++++ ...rationsToSignTransactionOrSignTypedData.ts | 37 ++++++++++ 3 files changed, 94 insertions(+), 57 deletions(-) create mode 100644 src/ui/shared/hooks/useOperationSetError.ts create mode 100644 src/ui/shared/hooks/useOrbyGetOperationsToSignTransactionOrSignTypedData.ts diff --git a/src/ui/pages/SendTransaction/SendTransaction.tsx b/src/ui/pages/SendTransaction/SendTransaction.tsx index b4cebb529f..a01fa600a2 100644 --- a/src/ui/pages/SendTransaction/SendTransaction.tsx +++ b/src/ui/pages/SendTransaction/SendTransaction.tsx @@ -99,10 +99,8 @@ import ScrollIcon from 'jsx:src/ui/assets/scroll.svg'; import ArrowDownIcon from 'jsx:src/ui/assets/caret-down-filled.svg'; import { SiteFaviconImg } from 'src/ui/components/SiteFaviconImg'; import { NetworkId } from 'src/modules/networks/NetworkId'; -import { useGetOperationsToExecuteTransaction } from '@orb-labs/orby-react'; -import { CreateOperationsStatus } from '@orb-labs/orby-core'; -import _ from 'lodash'; import { useIsOrbyEnabled } from 'src/shared/core/useIsOrbyEnabled'; +import { useOrbyGetOperationsToSignTransactionOrSignTypedData } from 'src/ui/shared/hooks/useOrbyGetOperationsToSignTransactionOrSignTypedData'; import type { PopoverToastHandle } from '../Settings/PopoverToast'; import { PopoverToast } from '../Settings/PopoverToast'; import { TransactionConfiguration } from './TransactionConfiguration'; @@ -564,9 +562,6 @@ function SendTransactionContent({ }); const [allowanceQuantityBase, setAllowanceQuantityBase] = useState(''); - const [operationSetError, setOperationSetError] = useState( - null - ); const configureTransactionToBeSigned = useEvent( async ( @@ -597,26 +592,6 @@ function SendTransactionContent({ isDefault: true, }); - const gasToken = useMemo(() => { - return selectedGasToken?.standardizedTokenId - ? { standardizedTokenId: selectedGasToken.standardizedTokenId } - : undefined; - }, [selectedGasToken]); - - const { operationSet, isLoading: OperationSetLoading } = - useGetOperationsToExecuteTransaction( - isOrbyEnabled ? (wallet?.address as string) : undefined, - isOrbyEnabled && populatedTransaction?.chainId - ? BigInt(populatedTransaction?.chainId as number) - : undefined, - populatedTransaction?.to as string, - populatedTransaction?.data as string, - populatedTransaction?.value - ? BigInt(populatedTransaction?.value.toString()) - : undefined, - gasToken - ); - const selectGasToken = useCallback( (gasToken?: GasTokenInput) => { if (gasToken) { @@ -626,35 +601,13 @@ function SendTransactionContent({ [setSelectedGasToken] ); - // Check for operation set errors and update error state - React.useEffect(() => { - if (operationSet?.status && isOrbyEnabled) { - let errorMessage: string | null = null; - - if (operationSet.status === CreateOperationsStatus.INSUFFICIENT_FUNDS) { - errorMessage = 'Insufficient funds'; - } else if ( - operationSet.status === CreateOperationsStatus.NO_EXECUTION_PATH - ) { - errorMessage = 'No execution path'; - } else if ( - operationSet.status === - CreateOperationsStatus.INSUFFICIENT_FUNDS_FOR_GAS - ) { - errorMessage = 'Insufficient funds for gas. Choose another token'; - } else if (operationSet.status === CreateOperationsStatus.INTERNAL) { - errorMessage = 'Internal error'; - } else if ( - operationSet.status === CreateOperationsStatus.INVALID_ARGUMENT - ) { - errorMessage = 'Invalid argument'; - } - - setOperationSetError(errorMessage); - } else { - setOperationSetError(null); - } - }, [operationSet?.status, isOrbyEnabled]); + const { operationSet, operationSetError, operationSetLoading } = + useOrbyGetOperationsToSignTransactionOrSignTypedData( + populatedTransaction, + wallet, + isOrbyEnabled, + selectedGasToken + ); const paymasterPossible = USE_PAYMASTER_FEATURE && Boolean(network?.supports_sponsored_transactions); @@ -948,11 +901,11 @@ function SendTransactionContent({ ref={sendTxBtnRef} onClick={() => sendTransaction()} isLoading={ - sendTransactionMutation.isLoading || OperationSetLoading + sendTransactionMutation.isLoading || operationSetLoading } disabled={ sendTransactionMutation.isLoading || - OperationSetLoading || + operationSetLoading || !!operationSetError } buttonKind={ diff --git a/src/ui/shared/hooks/useOperationSetError.ts b/src/ui/shared/hooks/useOperationSetError.ts new file mode 100644 index 0000000000..e5e4ea5048 --- /dev/null +++ b/src/ui/shared/hooks/useOperationSetError.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import { CreateOperationsStatus } from '@orb-labs/orby-core'; +import type { OperationSet } from '@orb-labs/orby-core'; + +/** + * Custom hook that monitors operation set status and returns appropriate error messages + * @param operationSet - The operation set to monitor + * @param isOrbyEnabled - Whether Orby is enabled for the current chain + * @returns The error message string or null if no error + */ +export function useOperationSetError( + operationSet: OperationSet | undefined, + isOrbyEnabled: boolean | undefined +): string | null { + const [errorMessage, setErrorMessage] = useState(null); + + useEffect(() => { + if (operationSet?.status && isOrbyEnabled) { + let newErrorMessage: string | null = null; + + if (operationSet.status === CreateOperationsStatus.INSUFFICIENT_FUNDS) { + newErrorMessage = 'Insufficient funds'; + } else if ( + operationSet.status === CreateOperationsStatus.NO_EXECUTION_PATH + ) { + newErrorMessage = 'No execution path'; + } else if ( + operationSet.status === + CreateOperationsStatus.INSUFFICIENT_FUNDS_FOR_GAS + ) { + newErrorMessage = 'Insufficient funds for gas. Choose another token'; + } else if (operationSet.status === CreateOperationsStatus.INTERNAL) { + newErrorMessage = 'Internal error'; + } else if ( + operationSet.status === CreateOperationsStatus.INVALID_ARGUMENT + ) { + newErrorMessage = 'Invalid argument'; + } + + setErrorMessage(newErrorMessage); + } else { + setErrorMessage(null); + } + }, [operationSet?.status, isOrbyEnabled]); + + return errorMessage; +} diff --git a/src/ui/shared/hooks/useOrbyGetOperationsToSignTransactionOrSignTypedData.ts b/src/ui/shared/hooks/useOrbyGetOperationsToSignTransactionOrSignTypedData.ts new file mode 100644 index 0000000000..bedb22f422 --- /dev/null +++ b/src/ui/shared/hooks/useOrbyGetOperationsToSignTransactionOrSignTypedData.ts @@ -0,0 +1,37 @@ +import { useMemo } from 'react'; +import type { IncomingTransactionWithChainId } from 'src/modules/ethereum/types/IncomingTransaction'; +import { useGetOperationsToSignTransactionOrSignTypedData } from '@orb-labs/orby-react'; +import type { ExternallyOwnedAccount } from 'src/shared/types/ExternallyOwnedAccount'; +import type { GasTokenInput } from 'src/ui/pages/SendTransaction/NetworkFee/NetworkFee'; +import { useOperationSetError } from './useOperationSetError'; + +export function useOrbyGetOperationsToSignTransactionOrSignTypedData( + populatedTransaction: IncomingTransactionWithChainId | undefined, + wallet: ExternallyOwnedAccount, + isOrbyEnabled: boolean | undefined, + selectedGasToken: GasTokenInput | undefined +) { + const gasToken = useMemo(() => { + return selectedGasToken?.standardizedTokenId + ? { standardizedTokenId: selectedGasToken.standardizedTokenId } + : undefined; + }, [selectedGasToken]); + + const { operationSet, isLoading: operationSetLoading } = + useGetOperationsToSignTransactionOrSignTypedData( + populatedTransaction?.data as string, + populatedTransaction?.to as string, + populatedTransaction?.value + ? BigInt(populatedTransaction?.value.toString()) + : undefined, + isOrbyEnabled ? (wallet?.address as string) : undefined, + isOrbyEnabled && populatedTransaction?.chainId + ? BigInt(populatedTransaction?.chainId as number) + : undefined, + gasToken + ); + + const operationSetError = useOperationSetError(operationSet, isOrbyEnabled); + + return { operationSet, operationSetError, operationSetLoading }; +}