diff --git a/package-lock.json b/package-lock.json index 3c95d84d..d308c3bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@headlessui/react": "^1.7.15", - "@massalabs/massa-web3": "^5.1.1-dev.20250110163508", + "@massalabs/massa-web3": "^5.1.1-dev", "@massalabs/wallet-provider": "^3.0.1-dev", "copy-to-clipboard": "^3.3.3", "currency.js": "^2.0.4", diff --git a/package.json b/package.json index 1b3162f6..123dc670 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@headlessui/react": "^1.7.15", - "@massalabs/massa-web3": "^5.1.1-dev.20250110163508", + "@massalabs/massa-web3": "^5.1.1-dev", "@massalabs/wallet-provider": "^3.0.1-dev", "copy-to-clipboard": "^3.3.3", "currency.js": "^2.0.4", diff --git a/src/lib/ConnectMassaWallets/store/accountStore.ts b/src/lib/ConnectMassaWallets/store/accountStore.ts index 2be163ee..29e28527 100644 --- a/src/lib/ConnectMassaWallets/store/accountStore.ts +++ b/src/lib/ConnectMassaWallets/store/accountStore.ts @@ -15,7 +15,7 @@ export interface AccountStoreState { unsubscribe: () => void; }; network?: Network; - + setAccounts: (wallet: Wallet, account?: Provider) => Promise; setCurrentWallet: (wallet?: Wallet, account?: Provider) => Promise; setWallets: (wallets: Wallet[]) => void; setConnectedAccount: (account?: Provider) => void; @@ -56,7 +56,7 @@ export const useAccountStore = create((set, get) => ({ } } - const accounts = await wallet.accounts(); + get().setAccounts(wallet, account); if (!get().accountObserver) { setupAccountObserver(wallet, set, get); @@ -68,9 +68,6 @@ export const useAccountStore = create((set, get) => ({ const network = await wallet.networkInfos(); get().setCurrentNetwork(network); - - set({ accounts }); - set({ connectedAccount: account || accounts[0] }); } catch (error) { console.log('Failed to set current wallet', error); } @@ -78,6 +75,12 @@ export const useAccountStore = create((set, get) => ({ set({ isFetching: false }); }, + setAccounts: async (wallet: Wallet, account?: Provider) => { + const accounts = await wallet.accounts(); + set({ accounts }); + set({ connectedAccount: account || accounts[0] }); + }, + setWallets: (wallets: Wallet[]) => { set({ wallets }); if (!wallets.some((p) => p.name() === get().currentWallet?.name())) { diff --git a/src/lib/massa-react/hooks/const.ts b/src/lib/massa-react/hooks/const.ts new file mode 100644 index 00000000..8469dd70 --- /dev/null +++ b/src/lib/massa-react/hooks/const.ts @@ -0,0 +1,6 @@ +import { OperationStatus } from '@massalabs/massa-web3'; + +export const ERROR_STATUSES = [ + OperationStatus.Error, + OperationStatus.SpeculativeError, +]; diff --git a/src/lib/massa-react/hooks/types.ts b/src/lib/massa-react/hooks/types.ts new file mode 100644 index 00000000..51f021ea --- /dev/null +++ b/src/lib/massa-react/hooks/types.ts @@ -0,0 +1,6 @@ +export type ToasterMessage = { + pending: string; + success: string; + error: string; + timeout?: string; +}; diff --git a/src/lib/massa-react/hooks/useHandleOperation.tsx b/src/lib/massa-react/hooks/useHandleOperation.tsx new file mode 100644 index 00000000..4131b7a0 --- /dev/null +++ b/src/lib/massa-react/hooks/useHandleOperation.tsx @@ -0,0 +1,112 @@ +import { useState } from 'react'; +import { CHAIN_ID, Operation, OperationStatus } from '@massalabs/massa-web3'; +import Intl from '../i18n'; +import { toast } from '../../../components'; +import { logSmartContractEvents, showToast } from '../utils'; +import { ToasterMessage } from './types'; +import { ERROR_STATUSES } from './const'; + +// TODO: Need to be refactored with the useWriteSmartContract.tsx +export function useHandleOperation() { + const [state, setState] = useState({ + isOpPending: false, + isPending: false, + isSuccess: false, + isError: false, + opId: undefined as string | undefined, + }); + + async function handleOperation( + operation: Operation, + messages: ToasterMessage, + final = false, + ): Promise { + const networkInfo = await operation.provider.networkInfos(); + const isMainnet = networkInfo.chainId === CHAIN_ID.Mainnet; + + if (state.isOpPending) { + throw new Error('Operation is already pending'); + } + + setState({ + ...state, + isOpPending: true, + isPending: true, + isSuccess: false, + isError: false, + opId: undefined, + }); + + try { + setState((prev) => ({ ...prev, opId: operation.id })); + + const loadingToastId = showToast( + 'loading', + messages.pending, + operation.id, + isMainnet, + ); + + const finalStatus = final + ? await operation.waitFinalExecution() + : await operation.waitSpeculativeExecution(); + + dismissLoadingToast(loadingToastId); + + if (finalStatus === OperationStatus.NotFound) { + handleOperationTimeout(messages.timeout, operation.id); + throw new Error('Operation not found'); + } else if (ERROR_STATUSES.includes(finalStatus)) { + logSmartContractEvents(operation.provider, operation.id); + throw new Error(`Operation failed with status: ${finalStatus}`); + } else { + handleOperationSuccess(messages.success, operation.id); + return operation; + } + } catch (error) { + handleOperationError(error, messages.error, state.opId); + } finally { + setState((prev) => ({ ...prev, isOpPending: false, isPending: false })); + } + } + + function dismissLoadingToast(toastId?: string): void { + if (toastId) { + toast.dismiss(toastId); + } else { + console.warn('Attempted to dismiss a toast with undefined ID.'); + } + } + + function handleOperationTimeout( + timeoutMessage?: string, + opId?: string, + ): void { + setState((prev) => ({ ...prev, isError: true })); + showToast('error', timeoutMessage || Intl.t('steps.failed-timeout'), opId); + } + + function handleOperationSuccess(successMessage: string, opId?: string): void { + setState((prev) => ({ ...prev, isSuccess: true })); + showToast('success', successMessage, opId); + } + + function handleOperationError( + error: unknown, + errorMessage: string, + opId?: string, + ): void { + console.error('Error during smart contract call:', error); + setState((prev) => ({ ...prev, isError: true })); + showToast('error', errorMessage, opId); + } + + return { + opId: state.opId, + isOpPending: state.isOpPending, + isPending: state.isPending, + isSuccess: state.isSuccess, + isError: state.isError, + handleOperation, + }; +} diff --git a/src/lib/massa-react/hooks/useWriteSmartContract.tsx b/src/lib/massa-react/hooks/useWriteSmartContract.tsx index d86911c7..5b8deab5 100644 --- a/src/lib/massa-react/hooks/useWriteSmartContract.tsx +++ b/src/lib/massa-react/hooks/useWriteSmartContract.tsx @@ -3,20 +3,17 @@ import { toast } from '../../../components'; import { logSmartContractEvents, showToast } from '../utils'; import Intl from '../i18n'; import { Operation, OperationStatus, Provider } from '@massalabs/massa-web3'; - -interface ToasterMessage { - pending: string; - success: string; - error: string; - timeout?: string; -} +import { ToasterMessage } from './types'; +import { ERROR_STATUSES } from './const'; export function useWriteSmartContract(account: Provider, isMainnet = false) { - const [isOpPending, setIsOpPending] = useState(false); - const [isPending, setIsPending] = useState(false); - const [isSuccess, setIsSuccess] = useState(false); - const [isError, setIsError] = useState(false); - const [opId, setOpId] = useState(); + const [state, setState] = useState({ + isOpPending: false, + isPending: false, + isSuccess: false, + isError: false, + opId: undefined as string | undefined, + }); async function callSmartContract( targetFunction: string, @@ -25,18 +22,20 @@ export function useWriteSmartContract(account: Provider, isMainnet = false) { messages: ToasterMessage, coins = 0n, fee?: bigint, + final = false, ): Promise { - if (isOpPending) { + if (state.isOpPending) { throw new Error('Operation is already pending'); } - setIsOpPending(false); - setIsSuccess(false); - setIsError(false); - setOpId(undefined); - setIsPending(true); - - let loadingToastId: string | undefined; + setState({ + ...state, + isOpPending: true, + isPending: true, + isSuccess: false, + isError: false, + opId: undefined, + }); try { const operation = await account.callSC({ @@ -47,56 +46,75 @@ export function useWriteSmartContract(account: Provider, isMainnet = false) { fee, }); - setOpId(operation.id); + setState((prev) => ({ ...prev, opId: operation.id })); - loadingToastId = showToast( + const loadingToastId = showToast( 'loading', messages.pending, operation.id, isMainnet, ); - const op = new Operation(account, operation.id); - const finalStatus = await op.waitSpeculativeExecution(); - - toast.dismiss(loadingToastId); + const finalStatus = final + ? await operation.waitFinalExecution() + : await operation.waitSpeculativeExecution(); - setIsPending(false); - setIsOpPending(false); + dismissLoadingToast(loadingToastId); if (finalStatus === OperationStatus.NotFound) { - setIsError(true); - showToast( - 'success', - messages.timeout || Intl.t('steps.failed-timeout'), - operation.id, - ); - } else if ( - ![OperationStatus.SpeculativeSuccess, OperationStatus.Success].includes( - finalStatus, - ) - ) { - logSmartContractEvents(account, operation.id); + handleOperationTimeout(messages.timeout, operation.id); + throw new Error('Operation not found'); + } else if (ERROR_STATUSES.includes(finalStatus)) { + logSmartContractEvents(operation.provider, operation.id); throw new Error(`Operation failed with status: ${finalStatus}`); } else { - setIsSuccess(true); - showToast('success', messages.success, operation.id); + handleOperationSuccess(messages.success, operation.id); + return operation; } - - return operation; } catch (error) { - console.error('Error during smart contract call:', error); - setIsError(true); - showToast('error', messages.error, opId); + handleOperationError(error, messages.error, state.opId); + } finally { + setState((prev) => ({ ...prev, isOpPending: false, isPending: false })); + } + } + + function dismissLoadingToast(toastId?: string): void { + if (toastId) { + toast.dismiss(toastId); + } else { + console.warn('Attempted to dismiss a toast with undefined ID.'); } } + function handleOperationTimeout( + timeoutMessage?: string, + opId?: string, + ): void { + setState((prev) => ({ ...prev, isError: true })); + showToast('error', timeoutMessage || Intl.t('steps.failed-timeout'), opId); + } + + function handleOperationSuccess(successMessage: string, opId?: string): void { + setState((prev) => ({ ...prev, isSuccess: true })); + showToast('success', successMessage, opId); + } + + function handleOperationError( + error: unknown, + errorMessage: string, + opId?: string, + ): void { + console.error('Error during smart contract call:', error); + setState((prev) => ({ ...prev, isError: true })); + showToast('error', errorMessage, opId); + } + return { - opId, - isOpPending, - isPending, - isSuccess, - isError, + opId: state.opId, + isOpPending: state.isOpPending, + isPending: state.isPending, + isSuccess: state.isSuccess, + isError: state.isError, callSmartContract, }; } diff --git a/src/lib/massa-react/index.ts b/src/lib/massa-react/index.ts index 15e3d9c8..3ae959ab 100644 --- a/src/lib/massa-react/index.ts +++ b/src/lib/massa-react/index.ts @@ -1 +1,2 @@ export * from './hooks/useWriteSmartContract'; +export * from './hooks/useHandleOperation'; diff --git a/src/lib/massa-react/utils.tsx b/src/lib/massa-react/utils.tsx index ec6fc5aa..7bb57ba8 100644 --- a/src/lib/massa-react/utils.tsx +++ b/src/lib/massa-react/utils.tsx @@ -6,12 +6,12 @@ import { import Intl from './i18n'; import { toast, ToastContent } from '../../components'; -import { Operation, Provider } from '@massalabs/massa-web3'; +import { Operation, Provider, PublicProvider } from '@massalabs/massa-web3'; import { Toast } from 'react-hot-toast'; import { OperationToast } from '../ConnectMassaWallets/components/OperationToast'; export async function logSmartContractEvents( - provider: Provider, + provider: PublicProvider, operationId: string, ): Promise { const op = new Operation(provider, operationId);