Skip to content

Commit

Permalink
feat: implement useHandleOperation and refactor useWriteSmartContract…
Browse files Browse the repository at this point in the history
… for improved operation handling
  • Loading branch information
Ben-Rey committed Jan 15, 2025
1 parent 6806dc6 commit 45e872c
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 61 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 8 additions & 5 deletions src/lib/ConnectMassaWallets/store/accountStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface AccountStoreState {
unsubscribe: () => void;
};
network?: Network;

setAccounts: (wallet: Wallet, account?: Provider) => Promise<void>;
setCurrentWallet: (wallet?: Wallet, account?: Provider) => Promise<void>;
setWallets: (wallets: Wallet[]) => void;
setConnectedAccount: (account?: Provider) => void;
Expand Down Expand Up @@ -56,7 +56,7 @@ export const useAccountStore = create<AccountStoreState>((set, get) => ({
}
}

const accounts = await wallet.accounts();
get().setAccounts(wallet, account);

if (!get().accountObserver) {
setupAccountObserver(wallet, set, get);
Expand All @@ -68,16 +68,19 @@ export const useAccountStore = create<AccountStoreState>((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);
}

set({ isFetching: false });
},

setAccounts: async (wallet: Wallet, account?: Provider) => {
const accounts = await wallet.accounts();
set({ accounts });

Check failure on line 80 in src/lib/ConnectMassaWallets/store/accountStore.ts

View workflow job for this annotation

GitHub Actions / build

Type 'import("/home/runner/work/ui-kit/ui-kit/node_modules/@massalabs/wallet-provider/node_modules/@massalabs/massa-web3/dist/esm/provider/interface").Provider[]' is not assignable to type 'import("/home/runner/work/ui-kit/ui-kit/node_modules/@massalabs/massa-web3/dist/esm/provider/interface").Provider[]'.
set({ connectedAccount: account || accounts[0] });

Check failure on line 81 in src/lib/ConnectMassaWallets/store/accountStore.ts

View workflow job for this annotation

GitHub Actions / build

Type 'Provider | Provider' is not assignable to type 'Provider | undefined'.
},

setWallets: (wallets: Wallet[]) => {
set({ wallets });
if (!wallets.some((p) => p.name() === get().currentWallet?.name())) {
Expand Down
6 changes: 6 additions & 0 deletions src/lib/massa-react/hooks/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { OperationStatus } from '@massalabs/massa-web3';

export const ERROR_STATUSES = [
OperationStatus.Error,
OperationStatus.SpeculativeError,
];
6 changes: 6 additions & 0 deletions src/lib/massa-react/hooks/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type ToasterMessage = {
pending: string;
success: string;
error: string;
timeout?: string;
};
112 changes: 112 additions & 0 deletions src/lib/massa-react/hooks/useHandleOperation.tsx
Original file line number Diff line number Diff line change
@@ -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<Operation | undefined> {
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,
};
}
122 changes: 70 additions & 52 deletions src/lib/massa-react/hooks/useWriteSmartContract.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
const [state, setState] = useState({
isOpPending: false,
isPending: false,
isSuccess: false,
isError: false,
opId: undefined as string | undefined,
});

async function callSmartContract(
targetFunction: string,
Expand All @@ -25,18 +22,20 @@ export function useWriteSmartContract(account: Provider, isMainnet = false) {
messages: ToasterMessage,
coins = 0n,
fee?: bigint,
final = false,
): Promise<Operation | undefined> {
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({
Expand All @@ -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,
};
}
1 change: 1 addition & 0 deletions src/lib/massa-react/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './hooks/useWriteSmartContract';
export * from './hooks/useHandleOperation';
4 changes: 2 additions & 2 deletions src/lib/massa-react/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const op = new Operation(provider, operationId);
Expand Down

0 comments on commit 45e872c

Please sign in to comment.