diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fb8714..9008082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.7.0] - 2024-01-30 + +### Added Changes +- Added `isSending`, `isEstimating`, `containsSendingError`, `containsEstimatingError` to `useEtherspotTransactions` hook + ## [0.6.12] - 2024-01-26 ### Added Changes diff --git a/__tests__/hooks/useEtherspotTransactions.test.js b/__tests__/hooks/useEtherspotTransactions.test.js index 9ba6fa0..67cdc79 100644 --- a/__tests__/hooks/useEtherspotTransactions.test.js +++ b/__tests__/hooks/useEtherspotTransactions.test.js @@ -1,4 +1,4 @@ -import { renderHook, render } from '@testing-library/react'; +import { renderHook, render, act, waitFor } from '@testing-library/react'; import { ethers } from 'ethers'; // hooks @@ -203,11 +203,33 @@ describe('useEtherspotTransactions()', () => { const { result } = renderHook(() => useEtherspotTransactions(), { wrapper }); - const estimated = await result.current.estimate(); + expect(result.current.isEstimating).toBe(false); + + let estimatePromise; + act(() => { + estimatePromise = result.current.estimate(); + }); + + await waitFor(() => expect(result.current.isEstimating).toBe(true)); + await waitFor(() => expect(result.current.isEstimating).toBe(false)); + + const estimated = await estimatePromise; + expect(result.current.containsEstimatingError).toBe(false); expect(ethers.BigNumber.isBigNumber(estimated[0].estimatedBatches[0].cost)).toBe(true); expect(estimated[0].estimatedBatches[0].cost.toString()).toBe('350000'); - const sent = await result.current.send(); + expect(result.current.isSending).toBe(false); + + let sendPromise; + act(() => { + sendPromise = result.current.send(); + }); + + await waitFor(() => expect(result.current.isSending).toBe(true)); + await waitFor(() => expect(result.current.isSending).toBe(false)); + + const sent = await sendPromise; + expect(result.current.containsSendingError).toBe(false); expect(sent[0].sentBatches[0].userOpHash).toBe('0x7c'); }); @@ -246,7 +268,18 @@ describe('useEtherspotTransactions()', () => { const { result } = renderHook(() => useEtherspotTransactions(), { wrapper }); - const sent = await result.current.send(); + expect(result.current.isSending).toBe(false); + + let sendPromise; + act(() => { + sendPromise = result.current.send(); + }); + + await waitFor(() => expect(result.current.isSending).toBe(true)); + await waitFor(() => expect(result.current.isSending).toBe(false)); + + const sent = await sendPromise; + expect(result.current.containsSendingError).toBe(false); expect(sent[0].estimatedBatches[0].cost.toString()).toBe('350000'); expect(sent[0].sentBatches[0].userOpHash).toBe('0x7c'); }); @@ -308,13 +341,35 @@ describe('useEtherspotTransactions()', () => { const { result } = renderHook(() => useEtherspotTransactions(), { wrapper }); - const estimated = await result.current.estimate(); + expect(result.current.isEstimating).toBe(false); + + let estimatePromise; + act(() => { + estimatePromise = result.current.estimate(); + }); + + await waitFor(() => expect(result.current.isEstimating).toBe(true)); + await waitFor(() => expect(result.current.isEstimating).toBe(false)); + + const estimated = await estimatePromise; + expect(result.current.containsEstimatingError).toBe(false); expect(estimated[0].estimatedBatches[0].cost.toString()).toBe('350000'); expect(estimated[0].estimatedBatches[1].cost.toString()).toBe('200000'); expect(estimated[1].estimatedBatches.length).toBe(0); // has skip prop expect(estimated[2].estimatedBatches[0].cost.toString()).toBe('250000'); - const sent = await result.current.send(); + expect(result.current.isSending).toBe(false); + + let sendPromise; + act(() => { + sendPromise = result.current.send(); + }); + + await waitFor(() => expect(result.current.isSending).toBe(true)); + await waitFor(() => expect(result.current.isSending).toBe(false)); + + const sent = await sendPromise; + expect(result.current.containsSendingError).toBe(false); expect(sent[0].sentBatches[0].userOpHash).toBe('0x7c'); expect(sent[0].sentBatches[1].userOpHash).toBe('0x7d'); expect(sent[1].sentBatches.length).toBe(0); // has skip prop @@ -365,11 +420,33 @@ describe('useEtherspotTransactions()', () => { const { result } = renderHook(() => useEtherspotTransactions(), { wrapper }); - const estimated = await result.current.estimate(); + expect(result.current.isEstimating).toBe(false); + + let estimatePromise; + act(() => { + estimatePromise = result.current.estimate(); + }); + + await waitFor(() => expect(result.current.isEstimating).toBe(true)); + await waitFor(() => expect(result.current.isEstimating).toBe(false)); + + const estimated = await estimatePromise; + expect(result.current.containsEstimatingError).toBe(false); expect(estimated[0].estimatedBatches[0].cost.toString()).toBe('350000'); expect(estimated[1].estimatedBatches[0].cost.toString()).toBe('325000'); - const sent = await result.current.send(); + expect(result.current.isSending).toBe(false); + + let sendPromise; + act(() => { + sendPromise = result.current.send(); + }); + + await waitFor(() => expect(result.current.isSending).toBe(true)); + await waitFor(() => expect(result.current.isSending).toBe(false)); + + const sent = await sendPromise; + expect(result.current.containsSendingError).toBe(false); expect(sent[0].sentBatches[0].userOpHash).toBe('0x7c'); expect(sent[1].sentBatches[0].userOpHash).toBe('0x46'); }); @@ -434,13 +511,35 @@ describe('useEtherspotTransactions()', () => { const { result } = renderHook(() => useEtherspotTransactions(), { wrapper }); - const estimated = await result.current.estimate(); + expect(result.current.isEstimating).toBe(false); + + let estimatePromise; + act(() => { + estimatePromise = result.current.estimate(); + }); + + await waitFor(() => expect(result.current.isEstimating).toBe(true)); + await waitFor(() => expect(result.current.isEstimating).toBe(false)); + + const estimated = await estimatePromise; + expect(result.current.containsEstimatingError).toBe(false); expect(estimated[0].estimatedBatches[0].cost.toString()).toBe('350000'); expect(estimated[0].estimatedBatches[1].cost.toString()).toBe('200000'); expect(estimated[1].estimatedBatches[0].cost.toString()).toBe('325000'); expect(estimated[2].estimatedBatches[0].cost.toString()).toBe('325000'); - const sent = await result.current.send(); + expect(result.current.isSending).toBe(false); + + let sendPromise; + act(() => { + sendPromise = result.current.send(); + }); + + await waitFor(() => expect(result.current.isSending).toBe(true)); + await waitFor(() => expect(result.current.isSending).toBe(false)); + + const sent = await sendPromise; + expect(result.current.containsSendingError).toBe(false); expect(sent[0].sentBatches[0].userOpHash).toBe('0x7c'); expect(sent[0].sentBatches[1].userOpHash).toBe('0x7e'); expect(sent[1].sentBatches[0].userOpHash).toBe('0x46'); @@ -501,8 +600,18 @@ describe('useEtherspotTransactions()', () => { const { result } = renderHook(() => useEtherspotTransactions(), { wrapper }); - const estimated = await result.current.estimate(); + expect(result.current.isEstimating).toBe(false); + + let estimatePromise; + act(() => { + estimatePromise = result.current.estimate(); + }); + await waitFor(() => expect(result.current.isEstimating).toBe(true)); + await waitFor(() => expect(result.current.isEstimating).toBe(false)); + + const estimated = await estimatePromise; + expect(result.current.containsEstimatingError).toBe(false); expect(onEstimated1).toBeCalledTimes(1); expect(onEstimated2).toBeCalledTimes(1); expect(onEstimated1.mock.calls[0][0]).toStrictEqual(estimated[0].estimatedBatches); @@ -563,7 +672,17 @@ describe('useEtherspotTransactions()', () => { const { result } = renderHook(() => useEtherspotTransactions(), { wrapper }); - const sent = await result.current.send(); + expect(result.current.isSending).toBe(false); + + let sendPromise; + act(() => { + sendPromise = result.current.send(); + }); + + await waitFor(() => expect(result.current.isSending).toBe(true)); + await waitFor(() => expect(result.current.isSending).toBe(false)); + + const sent = await sendPromise; expect(onSent1).toBeCalledTimes(1); expect(onSent2).toBeCalledTimes(1); @@ -618,8 +737,18 @@ describe('useEtherspotTransactions()', () => { const { result } = renderHook(() => useEtherspotTransactions(), { wrapper }); - const estimated = await result.current.estimate(); + expect(result.current.isEstimating).toBe(false); + + let estimatePromise; + act(() => { + estimatePromise = result.current.estimate(); + }); + + await waitFor(() => expect(result.current.isEstimating).toBe(true)); + await waitFor(() => expect(result.current.isEstimating).toBe(false)); + const estimated = await estimatePromise; + expect(result.current.containsEstimatingError).toBe(true); expect(estimated[0].estimatedBatches[0].errorMessage).toBe('Transaction reverted: chain too high'); expect(estimated[1].estimatedBatches[0].errorMessage).toBe('Transaction reverted: invalid address'); expect(onEstimated1.mock.calls[0][0]).toStrictEqual(estimated[0].estimatedBatches); @@ -673,13 +802,34 @@ describe('useEtherspotTransactions()', () => { const { result } = renderHook(() => useEtherspotTransactions(), { wrapper }); - const estimated = await result.current.estimate(); + expect(result.current.isEstimating).toBe(false); + let estimatePromise; + act(() => { + estimatePromise = result.current.estimate(); + }); + + await waitFor(() => expect(result.current.isEstimating).toBe(true)); + await waitFor(() => expect(result.current.isEstimating).toBe(false)); + + const estimated = await estimatePromise; + expect(result.current.containsEstimatingError).toBe(false); expect(estimated[0].estimatedBatches[0].cost.toString()).toBe('350000'); expect(estimated[1].estimatedBatches[0].cost.toString()).toBe('250000'); - const sent = await result.current.send(); + expect(result.current.isSending).toBe(false); + + let sendPromise; + act(() => { + sendPromise = result.current.send(); + }); + await waitFor(() => expect(result.current.isSending).toBe(true)); + await waitFor(() => expect(result.current.isSending).toBe(false)); + + const sent = await sendPromise; + + expect(result.current.containsSendingError).toBe(true); expect(sent[0].sentBatches[0].errorMessage).toBe('Transaction reverted: chain too hot'); expect(sent[1].sentBatches[0].errorMessage).toBe('Transaction reverted: invalid signature'); expect(onSent1.mock.calls[0][0]).toStrictEqual(sent[0].sentBatches); @@ -756,12 +906,31 @@ describe('useEtherspotTransactions()', () => { const { result } = renderHook(() => useEtherspotTransactions(), { wrapper }); - const estimated1 = await result.current.estimate(['test-id-1']); + expect(result.current.isEstimating).toBe(false); + + let estimatePromise; + act(() => { + estimatePromise = result.current.estimate(['test-id-1']); + }); + + await waitFor(() => expect(result.current.isEstimating).toBe(true)); + await waitFor(() => expect(result.current.isEstimating).toBe(false)); + + const estimated1 = await estimatePromise; + expect(result.current.containsEstimatingError).toBe(true); expect(estimated1.length).toBe(1); expect(estimated1[0].estimatedBatches[0].errorMessage).toBe('Transaction reverted: chain too high'); expect(onEstimated1.mock.calls[0][0]).toStrictEqual(estimated1[0].estimatedBatches); - const estimated2 = await result.current.estimate(['test-id-2', 'test-id-3']); + act(() => { + estimatePromise = result.current.estimate(['test-id-2', 'test-id-3']); + }); + + await waitFor(() => expect(result.current.isEstimating).toBe(true)); + await waitFor(() => expect(result.current.isEstimating).toBe(false)); + + const estimated2 = await estimatePromise; + expect(result.current.containsEstimatingError).toBe(false); expect(estimated2.length).toBe(2); expect(estimated2[0].estimatedBatches[0].cost.toString()).toBe('325000'); expect(estimated2[1].estimatedBatches[0].cost.toString()).toBe('200000'); @@ -839,13 +1008,32 @@ describe('useEtherspotTransactions()', () => { const { result } = renderHook(() => useEtherspotTransactions(), { wrapper }); - const sent1 = await result.current.send(['test-id-1']); + expect(result.current.isSending).toBe(false); + + let sendPromise; + act(() => { + sendPromise = result.current.send(['test-id-1']); + }); + + await waitFor(() => expect(result.current.isSending).toBe(true)); + await waitFor(() => expect(result.current.isSending).toBe(false)); + + const sent1 = await sendPromise; expect(sent1.length).toBe(1); + expect(result.current.containsSendingError).toBe(true); expect(sent1[0].sentBatches[0].errorMessage).toBe('Transaction reverted: chain too hot'); expect(onSent1.mock.calls[0][0]).toStrictEqual(sent1[0].sentBatches); - const sent2 = await result.current.send(['test-id-2', 'test-id-3']); + act(() => { + sendPromise = result.current.send(['test-id-2', 'test-id-3']); + }); + + await waitFor(() => expect(result.current.isSending).toBe(true)); + await waitFor(() => expect(result.current.isSending).toBe(false)); + + const sent2 = await sendPromise; expect(sent2.length).toBe(2); + expect(result.current.containsSendingError).toBe(false); expect(sent2[0].sentBatches[0].userOpHash).toBe('0x46'); expect(sent2[1].sentBatches[0].userOpHash).toBe('0x47'); expect(onSent2.mock.calls[0][0]).toStrictEqual(sent2[0].sentBatches); diff --git a/package-lock.json b/package-lock.json index 0102df8..21a5da4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@etherspot/transaction-kit", - "version": "0.6.12", + "version": "0.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@etherspot/transaction-kit", - "version": "0.6.12", + "version": "0.7.0", "license": "MIT", "dependencies": { "@etherspot/eip1271-verification-util": "0.1.2", diff --git a/package.json b/package.json index 74c6d53..6f1753a 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "@etherspot/transaction-kit", "description": "React Etherspot Transaction Kit", - "version": "0.6.12", + "version": "0.7.0", "main": "dist/cjs/index.js", "scripts": { "rollup:build": "NODE_OPTIONS=--max-old-space-size=8192 rollup -c", "rollup:watch": "rimraf ./node_modules/react ./node_modules/react-dom && rollup -c -w", - "test": "jest __tests__ --silent", + "test": "jest __tests__ --silent --detectOpenHandles", "test:watch": "jest --watch" }, "repository": { diff --git a/src/contexts/EtherspotTransactionKitContext.tsx b/src/contexts/EtherspotTransactionKitContext.tsx index fbcd2fe..af1c941 100644 --- a/src/contexts/EtherspotTransactionKitContext.tsx +++ b/src/contexts/EtherspotTransactionKitContext.tsx @@ -10,6 +10,10 @@ export interface IEtherspotTransactionKitContext { batches: IBatches[]; estimate: (batchesIds?: string[]) => Promise; send: (batchesIds?: string[]) => Promise; + isEstimating: boolean; + isSending: boolean; + containsSendingError: boolean; + containsEstimatingError: boolean; }, setGroupedBatchesPerId: Dispatch>>; } diff --git a/src/providers/EtherspotTransactionKitContextProvider.tsx b/src/providers/EtherspotTransactionKitContextProvider.tsx index 3208eec..fa42cc3 100644 --- a/src/providers/EtherspotTransactionKitContextProvider.tsx +++ b/src/providers/EtherspotTransactionKitContextProvider.tsx @@ -20,15 +20,24 @@ interface EtherspotTransactionKitContextProviderProps { const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransactionKitContextProviderProps) => { const { provider, chainId, getSdk } = useEtherspot(); const [groupedBatchesPerId, setGroupedBatchesPerId] = useState>({}); + const [isEstimating, setIsEstimating] = useState(false); + const [isSending, setIsSending] = useState(false); + const [containsSendingError, setContainsSendingError] = useState(false); + const [containsEstimatingError, setContainsEstimatingError] = useState(false); const estimate = async ( batchesIds?: string[], forSending: boolean = false, ): Promise => { + if (!forSending) { + setIsEstimating(true); + setContainsSendingError(false); + } + const groupedBatchesToEstimate = Object.values(groupedBatchesPerId) .filter((groupedBatch) => (!batchesIds?.length|| batchesIds.some((batchesId) => batchesId === groupedBatch.id))); - return Promise.all(groupedBatchesToEstimate.map(async (groupedBatch): Promise => { + const result = await Promise.all(groupedBatchesToEstimate.map(async (groupedBatch): Promise => { const batches = (groupedBatch.batches ?? []) as IBatch[]; if (groupedBatch.skip) return { ...groupedBatch, estimatedBatches: [] }; @@ -71,9 +80,20 @@ const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransacti return { ...groupedBatch, estimatedBatches }; })); + + if (!forSending) { + const containsError = result.some((group) => group.estimatedBatches.some((batch) => !!batch.errorMessage)); + setContainsEstimatingError(containsError); + setIsEstimating(false); + } + + return result; } const send = async (batchesIds?: string[]): Promise => { + setIsSending(true); + setContainsSendingError(false); + const groupedBatchesToClean = Object.values(groupedBatchesPerId) .filter((groupedBatch) => (!batchesIds?.length|| batchesIds.some((batchesId) => batchesId === groupedBatch.id))); @@ -90,7 +110,7 @@ const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransacti const estimated = await estimate(batchesIds, true); - return Promise.all(estimated.map(async (estimatedBatches): Promise => { + const result = await Promise.all(estimated.map(async (estimatedBatches): Promise => { const sentBatches: SentBatch[] = []; // send in same order as estimated @@ -122,6 +142,16 @@ const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransacti return { ...estimatedBatches, sentBatches }; })); + + const containsError = result.some((group) => { + return group.estimatedBatches.some((batch) => !!batch.errorMessage) // estimate error during sending + || group.sentBatches.some((batch) => !!batch.errorMessage); + }); + + setContainsSendingError(containsError); + setIsSending(false); + + return result; } const contextData = useMemo(() => ({ @@ -129,9 +159,17 @@ const EtherspotTransactionKitContextProvider = ({ children }: EtherspotTransacti estimate, send, chainId, + isEstimating, + isSending, + containsEstimatingError, + containsSendingError, }), [ chainId, groupedBatchesPerId, + isEstimating, + isSending, + containsEstimatingError, + containsSendingError, ]); return ( diff --git a/src/types/EtherspotTransactionKit.ts b/src/types/EtherspotTransactionKit.ts index fa7be89..14ba4ed 100644 --- a/src/types/EtherspotTransactionKit.ts +++ b/src/types/EtherspotTransactionKit.ts @@ -30,14 +30,11 @@ export interface EstimatedBatch extends IBatch { userOp?: UserOp; } -export interface EtherspotPrimeSentBatch extends EstimatedBatch { +export interface SentBatch extends EstimatedBatch { + errorMessage?: string; userOpHash?: string; } -export type SentBatch = { - errorMessage?: string; -} & (EtherspotPrimeSentBatch); - export interface IBatches { id?: string; batches?: IBatch[];