From 66653574049d828705e037478149bf3b83064dba Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 21 Oct 2024 08:02:00 -0400 Subject: [PATCH] feat: add promises to transactions and calls (#1450) --- .changeset/mighty-kings-knock.md | 5 ++ .../components/AppProvider.tsx | 2 + .../components/demo/Transaction.tsx | 74 ++++++++++++++--- .../components/form/transaction-options.tsx | 6 ++ .../nextjs-app-router/onchainkit/package.json | 2 +- src/transaction/components/Transaction.tsx | 3 +- .../components/TransactionButton.tsx | 4 +- .../components/TransactionProvider.test.tsx | 26 +++++- .../components/TransactionProvider.tsx | 37 +++++++-- .../useGetTransactionStatusLabel.test.tsx | 8 ++ .../hooks/useGetTransactionStatusLabel.tsx | 10 ++- .../useGetTransactionToastLabel.test.tsx | 16 ++++ .../hooks/useGetTransactionToastLabel.tsx | 20 ++++- .../hooks/useSendWalletTransactions.test.tsx | 40 ++++++++-- .../hooks/useSendWalletTransactions.tsx | 79 +++++++++++-------- src/transaction/types.ts | 44 ++++------- .../utils/isSpinnerDisplayed.test.ts | 16 ++++ src/transaction/utils/isSpinnerDisplayed.ts | 4 +- 18 files changed, 301 insertions(+), 95 deletions(-) create mode 100644 .changeset/mighty-kings-knock.md diff --git a/.changeset/mighty-kings-knock.md b/.changeset/mighty-kings-knock.md new file mode 100644 index 0000000000..f85a5ae3ba --- /dev/null +++ b/.changeset/mighty-kings-knock.md @@ -0,0 +1,5 @@ +--- +'@coinbase/onchainkit': minor +--- + +-**feat**: Add handling for calls and contracts promises in Transactions component. By @alessey #1450 diff --git a/playground/nextjs-app-router/components/AppProvider.tsx b/playground/nextjs-app-router/components/AppProvider.tsx index 25ab324223..1d146924c5 100644 --- a/playground/nextjs-app-router/components/AppProvider.tsx +++ b/playground/nextjs-app-router/components/AppProvider.tsx @@ -21,6 +21,8 @@ export enum OnchainKitComponent { export enum TransactionTypes { Calls = 'calls', Contracts = 'contracts', + CallsPromise = 'callsPromise', + ContractsPromise = 'contractsPromise', } export type Paymaster = { diff --git a/playground/nextjs-app-router/components/demo/Transaction.tsx b/playground/nextjs-app-router/components/demo/Transaction.tsx index a0db9b844b..aabe18989c 100644 --- a/playground/nextjs-app-router/components/demo/Transaction.tsx +++ b/playground/nextjs-app-router/components/demo/Transaction.tsx @@ -1,5 +1,7 @@ import { useCapabilities } from '@/lib/hooks'; import { clickCalls, clickContracts } from '@/lib/transactions'; +import type { Call } from '@/onchainkit/esm/transaction/types'; +import type { LifecycleStatus } from '@/onchainkit/src/transaction'; import { Transaction, TransactionButton, @@ -12,37 +14,87 @@ import { TransactionToastIcon, TransactionToastLabel, } from '@coinbase/onchainkit/transaction'; -import { useCallback, useContext, useEffect } from 'react'; +import { useCallback, useContext, useEffect, useMemo } from 'react'; +import type { ContractFunctionParameters } from 'viem'; import { AppContext, TransactionTypes } from '../AppProvider'; function TransactionDemo() { const { chainId, transactionType } = useContext(AppContext); const capabilities = useCapabilities(); - const contracts = clickContracts; - const calls = clickCalls; + const contracts = clickContracts as ContractFunctionParameters[]; + const calls = clickCalls as Call[]; + const promiseCalls = new Promise((resolve) => { + setTimeout(() => { + resolve(calls); + }, 4000); + }) as Promise; + const promiseContracts = new Promise((resolve) => { + setTimeout(() => { + resolve(contracts); + }, 4000); + }) as Promise; useEffect(() => { console.log('Playground.Transaction.chainId:', chainId); }, [chainId]); - const handleOnStatus = useCallback((status) => { + const handleOnStatus = useCallback((status: LifecycleStatus) => { console.log('Playground.Transaction.onStatus:', status); }, []); useEffect(() => { console.log('Playground.Transaction.transactionType:', transactionType); - if (transactionType === TransactionTypes.Calls) { - console.log('Playground.Transaction.calls:', calls); - } else { - console.log('Playground.Transaction.contracts:', contracts); + switch (transactionType) { + case TransactionTypes.Calls: + console.log('Playground.Transaction.calls:', calls); + break; + case TransactionTypes.Contracts: + console.log('Playground.Transaction.contracts:', contracts); + break; + case TransactionTypes.CallsPromise: + console.log('Playground.Transaction.callsPromise'); + break; + case TransactionTypes.ContractsPromise: + console.log('Playground.Transaction.contractsPromise'); + break; } }, [transactionType, calls, contracts]); + const transactions = useMemo(() => { + if (transactionType === TransactionTypes.Calls) { + return { + calls, + contracts: undefined, + }; + } + + if (transactionType === TransactionTypes.Contracts) { + return { + calls: undefined, + contracts, + }; + } + + if (transactionType === TransactionTypes.CallsPromise) { + return { + calls: promiseCalls, + contracts: undefined, + }; + } + + if (transactionType === TransactionTypes.ContractsPromise) { + return { + contracts: promiseContracts, + calls: undefined, + }; + } + + return { calls: undefined, contracts: undefined }; + }, [calls, promiseCalls, contracts, promiseContracts, transactionType]); + return (
diff --git a/playground/nextjs-app-router/components/form/transaction-options.tsx b/playground/nextjs-app-router/components/form/transaction-options.tsx index 8db998fb0e..00e635f304 100644 --- a/playground/nextjs-app-router/components/form/transaction-options.tsx +++ b/playground/nextjs-app-router/components/form/transaction-options.tsx @@ -36,6 +36,12 @@ export function TransactionOptions() { Contracts + + Calls Promise + + + Contracts Promise +
diff --git a/playground/nextjs-app-router/onchainkit/package.json b/playground/nextjs-app-router/onchainkit/package.json index ff55aa8573..c97ff0ecb1 100644 --- a/playground/nextjs-app-router/onchainkit/package.json +++ b/playground/nextjs-app-router/onchainkit/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/onchainkit", - "version": "0.34.0", + "version": "0.34.1", "type": "module", "repository": "https://github.com/coinbase/onchainkit.git", "license": "MIT", diff --git a/src/transaction/components/Transaction.tsx b/src/transaction/components/Transaction.tsx index d319c97775..7fd89b7476 100644 --- a/src/transaction/components/Transaction.tsx +++ b/src/transaction/components/Transaction.tsx @@ -18,11 +18,12 @@ export function Transaction({ }: TransactionReact) { const isMounted = useIsMounted(); const componentTheme = useTheme(); + const { chain } = useOnchainKit(); + // prevents SSR hydration issue if (!isMounted) { return null; } - const { chain } = useOnchainKit(); // If chainId is not provided, // use the default chainId from the OnchainKit context diff --git a/src/transaction/components/TransactionButton.tsx b/src/transaction/components/TransactionButton.tsx index 27c151e8d6..528b3641a7 100644 --- a/src/transaction/components/TransactionButton.tsx +++ b/src/transaction/components/TransactionButton.tsx @@ -31,7 +31,9 @@ export function TransactionButton({ const { showCallsStatus } = useShowCallsStatus(); const isInProgress = - lifecycleStatus.statusName === 'transactionPending' || isLoading; + lifecycleStatus.statusName === 'buildingTransaction' || + lifecycleStatus.statusName === 'transactionPending' || + isLoading; const isMissingProps = !transactions || !address; const isWaitingForReceipt = !!transactionId || !!transactionHash; diff --git a/src/transaction/components/TransactionProvider.test.tsx b/src/transaction/components/TransactionProvider.test.tsx index 8e9c2d07b5..b96a7e0063 100644 --- a/src/transaction/components/TransactionProvider.test.tsx +++ b/src/transaction/components/TransactionProvider.test.tsx @@ -83,7 +83,8 @@ const TestComponent = () => { }); }; const handleStatusTransactionLegacyExecutedMultipleContracts = async () => { - context.setLifecycleStatus({ + await context.onSubmit(); + await context.setLifecycleStatus({ statusName: 'transactionLegacyExecuted', statusData: { transactionHashList: ['hash12345678', 'hash12345678'], @@ -256,6 +257,29 @@ describe('TransactionProvider', () => { }); }); + it('should emit onError when building transactions fails', async () => { + const sendWalletTransactionsMock = vi.fn(); + (useSendWalletTransactions as ReturnType).mockReturnValue( + sendWalletTransactionsMock, + ); + const onErrorMock = vi.fn(); + const contracts = Promise.reject(new Error('error')); + render( + + + , + ); + const button = screen.getByText('Submit'); + fireEvent.click(button); + await waitFor(() => { + expect(onErrorMock).toHaveBeenCalledWith({ + code: 'TmTPc04', + error: '{}', + message: 'Error building transactions', + }); + }); + }); + it('should emit onError when legacy transactions fail', async () => { const onErrorMock = vi.fn(); (waitForTransactionReceipt as ReturnType).mockRejectedValue( diff --git a/src/transaction/components/TransactionProvider.tsx b/src/transaction/components/TransactionProvider.tsx index 1c1a5c1ee1..60869d1884 100644 --- a/src/transaction/components/TransactionProvider.tsx +++ b/src/transaction/components/TransactionProvider.tsx @@ -71,6 +71,9 @@ export function TransactionProvider({ statusData: null, }); // Component lifecycle const [transactionId, setTransactionId] = useState(''); + const [transactionCount, setTransactionCount] = useState< + number | undefined + >(); const [transactionHashList, setTransactionHashList] = useState([]); const transactions = calls || contracts; const transactionType = calls @@ -160,7 +163,6 @@ export function TransactionProvider({ capabilities, sendCallAsync, sendCallsAsync, - transactions, transactionType, walletCapabilities, writeContractAsync, @@ -233,13 +235,13 @@ export function TransactionProvider({ useEffect(() => { if ( !transactions || - transactionHashList.length !== transactions.length || - transactions.length < 2 + transactionHashList.length !== transactionCount || + transactionCount < 2 ) { return; } getTransactionLegacyReceipts(); - }, [transactions, transactionHashList]); + }, [transactions, transactionCount, transactionHashList]); const getTransactionLegacyReceipts = useCallback(async () => { const receipts = []; @@ -278,13 +280,36 @@ export function TransactionProvider({ [account.chainId, switchChainAsync], ); + const buildTransaction = useCallback(async () => { + setLifecycleStatus({ + statusName: 'buildingTransaction', + statusData: null, + }); + try { + const resolvedTransactions = await Promise.resolve(transactions); + setTransactionCount(resolvedTransactions?.length); + return resolvedTransactions; + } catch (err) { + setLifecycleStatus({ + statusName: 'error', + statusData: { + code: 'TmTPc04', // Transaction module TransactionProvider component 04 error + error: JSON.stringify(err), + message: 'Error building transactions', + }, + }); + return undefined; + } + }, [transactions]); + const handleSubmit = useCallback(async () => { setErrorMessage(''); setIsToastVisible(true); try { // Switch chain before attempting transactions await switchChain(chainId); - await sendWalletTransactions(); + const resolvedTransactions = await buildTransaction(); + await sendWalletTransactions(resolvedTransactions); } catch (err) { const errorMessage = isUserRejectedRequestError(err) ? 'Request denied.' @@ -298,7 +323,7 @@ export function TransactionProvider({ }, }); } - }, [chainId, sendWalletTransactions, switchChain]); + }, [buildTransaction, chainId, sendWalletTransactions, switchChain]); const value = useValue({ chainId, diff --git a/src/transaction/hooks/useGetTransactionStatusLabel.test.tsx b/src/transaction/hooks/useGetTransactionStatusLabel.test.tsx index e61cf88136..bc20e6c470 100644 --- a/src/transaction/hooks/useGetTransactionStatusLabel.test.tsx +++ b/src/transaction/hooks/useGetTransactionStatusLabel.test.tsx @@ -40,6 +40,14 @@ describe('useGetTransactionStatusLabel', () => { expect(result.current.label).toBe('Confirm in wallet.'); }); + it('should return status when transaction is building', () => { + (useTransactionContext as Mock).mockReturnValue({ + lifecycleStatus: { statusName: 'buildingTransaction', statusData: null }, + }); + const { result } = renderHook(() => useGetTransactionStatusLabel()); + expect(result.current.label).toBe('Building transaction...'); + }); + it('should return status when transaction hash exists', () => { (useTransactionContext as Mock).mockReturnValue({ lifecycleStatus: { statusName: 'init', statusData: null }, diff --git a/src/transaction/hooks/useGetTransactionStatusLabel.tsx b/src/transaction/hooks/useGetTransactionStatusLabel.tsx index 0408bc4847..962443fd52 100644 --- a/src/transaction/hooks/useGetTransactionStatusLabel.tsx +++ b/src/transaction/hooks/useGetTransactionStatusLabel.tsx @@ -17,10 +17,18 @@ export function useGetTransactionStatusLabel() { // user started txn and needs to confirm in wallet const isPending = lifecycleStatus.statusName === 'transactionPending'; + // waiting for calls or contracts promise to resolve + const isBuildingTransaction = + lifecycleStatus.statusName === 'buildingTransaction'; + return useMemo(() => { let label = ''; let labelClassName: string = color.foregroundMuted; + if (isBuildingTransaction) { + label = 'Building transaction...'; + } + if (isPending) { label = 'Confirm in wallet.'; } @@ -39,5 +47,5 @@ export function useGetTransactionStatusLabel() { } return { label, labelClassName }; - }, [errorMessage, isInProgress, isPending, receipt]); + }, [errorMessage, isBuildingTransaction, isInProgress, isPending, receipt]); } diff --git a/src/transaction/hooks/useGetTransactionToastLabel.test.tsx b/src/transaction/hooks/useGetTransactionToastLabel.test.tsx index 98141a6b02..e9e04580d3 100644 --- a/src/transaction/hooks/useGetTransactionToastLabel.test.tsx +++ b/src/transaction/hooks/useGetTransactionToastLabel.test.tsx @@ -35,6 +35,7 @@ describe('useGetTransactionToastLabel', () => { it('should return correct toast and actionElement when transaction is loading', () => { (useTransactionContext as Mock).mockReturnValue({ isLoading: true, + lifecycleStatus: { statusName: 'init' }, }); const { result } = renderHook(() => useGetTransactionToastLabel()); @@ -45,6 +46,7 @@ describe('useGetTransactionToastLabel', () => { it('should return status when transaction hash exists', () => { (useTransactionContext as Mock).mockReturnValue({ transactionHash: '0x123', + lifecycleStatus: { statusName: 'init' }, }); const { result } = renderHook(() => useGetTransactionToastLabel()); @@ -55,6 +57,7 @@ describe('useGetTransactionToastLabel', () => { it('should return status when transaction id exists', () => { (useTransactionContext as Mock).mockReturnValue({ transactionId: 'ab123', + lifecycleStatus: { statusName: 'init' }, onSubmit: vi.fn(), }); @@ -66,10 +69,21 @@ describe('useGetTransactionToastLabel', () => { expect(result.current.label).toBe('Transaction in progress'); }); + it('should return status when building transaction', () => { + (useTransactionContext as Mock).mockReturnValue({ + lifecycleStatus: { statusName: 'buildingTransaction' }, + }); + + const { result } = renderHook(() => useGetTransactionToastLabel()); + + expect(result.current.label).toBe('Building transaction'); + }); + it('should return status when receipt exists', () => { (useTransactionContext as Mock).mockReturnValue({ receipt: 'receipt', transactionHash: '0x123', + lifecycleStatus: { statusName: 'init' }, }); const { result } = renderHook(() => useGetTransactionToastLabel()); @@ -81,6 +95,7 @@ describe('useGetTransactionToastLabel', () => { const onSubmitMock = vi.fn(); (useTransactionContext as Mock).mockReturnValue({ errorMessage: 'error', + lifecycleStatus: { statusName: 'init' }, onSubmit: onSubmitMock, }); @@ -92,6 +107,7 @@ describe('useGetTransactionToastLabel', () => { it('should return status when no status available', () => { (useTransactionContext as Mock).mockReturnValue({ errorMessage: '', + lifecycleStatus: { statusName: 'init' }, }); const { result } = renderHook(() => useGetTransactionToastLabel()); diff --git a/src/transaction/hooks/useGetTransactionToastLabel.tsx b/src/transaction/hooks/useGetTransactionToastLabel.tsx index 1d16c00326..598b2a23a7 100644 --- a/src/transaction/hooks/useGetTransactionToastLabel.tsx +++ b/src/transaction/hooks/useGetTransactionToastLabel.tsx @@ -3,16 +3,30 @@ import { color } from '../../styles/theme'; import { useTransactionContext } from '../components/TransactionProvider'; export function useGetTransactionToastLabel() { - const { errorMessage, isLoading, receipt, transactionHash, transactionId } = - useTransactionContext(); + const { + errorMessage, + isLoading, + lifecycleStatus, + receipt, + transactionHash, + transactionId, + } = useTransactionContext(); // user confirmed in wallet, txn in progress const isInProgress = isLoading || !!transactionId || !!transactionHash; + // waiting for calls or contracts promise to resolve + const isBuildingTransaction = + lifecycleStatus.statusName === 'buildingTransaction'; + return useMemo(() => { let label = ''; let labelClassName: string = color.foregroundMuted; + if (isBuildingTransaction) { + label = 'Building transaction'; + } + if (isInProgress) { label = 'Transaction in progress'; } @@ -27,5 +41,5 @@ export function useGetTransactionToastLabel() { } return { label, labelClassName }; - }, [errorMessage, isInProgress, receipt]); + }, [errorMessage, isBuildingTransaction, isInProgress, receipt]); } diff --git a/src/transaction/hooks/useSendWalletTransactions.test.tsx b/src/transaction/hooks/useSendWalletTransactions.test.tsx index 77db8c940b..8d6047094e 100644 --- a/src/transaction/hooks/useSendWalletTransactions.test.tsx +++ b/src/transaction/hooks/useSendWalletTransactions.test.tsx @@ -1,5 +1,5 @@ import { renderHook } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { TRANSACTION_TYPE_CALLS, TRANSACTION_TYPE_CONTRACTS, @@ -22,7 +22,6 @@ describe('useSendWalletTransactions', () => { const capabilities = {}; const { result } = renderHook(() => useSendWalletTransactions({ - transactions, transactionType: TRANSACTION_TYPE_CONTRACTS, capabilities, writeContractsAsync: vi.fn(), @@ -36,7 +35,7 @@ describe('useSendWalletTransactions', () => { }, }), ); - await result.current(); + await result.current(transactions); expect(sendBatchedTransactions).toHaveBeenCalledWith({ capabilities, sendCallsAsync: expect.any(Function), @@ -53,7 +52,6 @@ describe('useSendWalletTransactions', () => { ]; const { result } = renderHook(() => useSendWalletTransactions({ - transactions, transactionType: TRANSACTION_TYPE_CALLS, capabilities: undefined, writeContractsAsync: vi.fn(), @@ -67,7 +65,7 @@ describe('useSendWalletTransactions', () => { }, }), ); - await result.current(); + await result.current(transactions); expect(sendSingleTransactions).toHaveBeenCalledWith({ sendCallAsync: expect.any(Function), transactions, @@ -76,10 +74,37 @@ describe('useSendWalletTransactions', () => { }); }); + it('should handle a transactions promise', async () => { + const transactions = [{ to: '0x123', data: '0x456' }]; + const capabilities = {}; + const { result } = renderHook(() => + useSendWalletTransactions({ + transactionType: TRANSACTION_TYPE_CONTRACTS, + capabilities, + writeContractsAsync: vi.fn(), + writeContractAsync: vi.fn(), + sendCallsAsync: vi.fn(), + sendCallAsync: vi.fn(), + walletCapabilities: { + atomicBatch: { + supported: true, + }, + }, + }), + ); + await result.current(Promise.resolve(transactions)); + expect(sendBatchedTransactions).toHaveBeenCalledWith({ + capabilities, + sendCallsAsync: expect.any(Function), + transactions, + transactionType: TRANSACTION_TYPE_CONTRACTS, + writeContractsAsync: expect.any(Function), + }); + }); + it('should handle no transactions', async () => { const { result } = renderHook(() => useSendWalletTransactions({ - transactions: undefined, transactionType: TRANSACTION_TYPE_CONTRACTS, capabilities: undefined, writeContractsAsync: vi.fn(), @@ -101,7 +126,6 @@ describe('useSendWalletTransactions', () => { ]; const { result } = renderHook(() => useSendWalletTransactions({ - transactions, transactionType: TRANSACTION_TYPE_CALLS, capabilities: undefined, writeContractsAsync: vi.fn(), @@ -111,7 +135,7 @@ describe('useSendWalletTransactions', () => { walletCapabilities: {}, }), ); - await result.current(); + await result.current(transactions); expect(sendSingleTransactions).toHaveBeenCalledWith({ sendCallAsync: expect.any(Function), transactions, diff --git a/src/transaction/hooks/useSendWalletTransactions.tsx b/src/transaction/hooks/useSendWalletTransactions.tsx index dbbe0ebda7..b8bf2f6567 100644 --- a/src/transaction/hooks/useSendWalletTransactions.tsx +++ b/src/transaction/hooks/useSendWalletTransactions.tsx @@ -1,6 +1,7 @@ import { useCallback } from 'react'; +import type { ContractFunctionParameters } from 'viem'; import { Capabilities } from '../../constants'; -import type { UseSendWalletTransactionsParams } from '../types'; +import type { Call, UseSendWalletTransactionsParams } from '../types'; import { sendBatchedTransactions } from '../utils/sendBatchedTransactions'; import { sendSingleTransactions } from '../utils/sendSingleTransactions'; @@ -9,42 +10,52 @@ export const useSendWalletTransactions = ({ capabilities, sendCallAsync, sendCallsAsync, - transactions, transactionType, walletCapabilities, writeContractAsync, writeContractsAsync, }: UseSendWalletTransactionsParams) => { - return useCallback(async () => { - if (!transactions) { - return; - } - if (walletCapabilities[Capabilities.AtomicBatch]?.supported) { - // Batched transactions - await sendBatchedTransactions({ - capabilities, - sendCallsAsync, - transactions, - transactionType, - writeContractsAsync, - }); - } else { - // Non-batched transactions - await sendSingleTransactions({ - sendCallAsync, - transactions, - transactionType, - writeContractAsync, - }); - } - }, [ - writeContractsAsync, - writeContractAsync, - sendCallsAsync, - sendCallAsync, - capabilities, - transactions, - transactionType, - walletCapabilities, - ]); + return useCallback( + async ( + transactions?: + | Call[] + | ContractFunctionParameters[] + | Promise + | Promise, + ) => { + if (!transactions) { + return; + } + + const resolvedTransactions = await Promise.resolve(transactions); + + if (walletCapabilities[Capabilities.AtomicBatch]?.supported) { + // Batched transactions + await sendBatchedTransactions({ + capabilities, + sendCallsAsync, + transactions: resolvedTransactions, + transactionType, + writeContractsAsync, + }); + } else { + // Non-batched transactions + await sendSingleTransactions({ + sendCallAsync, + transactions: resolvedTransactions, + transactionType, + writeContractAsync, + }); + } + }, + [ + writeContractsAsync, + writeContractAsync, + sendCallsAsync, + sendCallAsync, + capabilities, + transactionType, + walletCapabilities, + ], + ); }; diff --git a/src/transaction/types.ts b/src/transaction/types.ts index 5fe608bebc..93cd0ad3ca 100644 --- a/src/transaction/types.ts +++ b/src/transaction/types.ts @@ -12,11 +12,6 @@ import type { SendTransactionMutateAsync, WriteContractMutateAsync, } from 'wagmi/query'; -// 🌲☀🌲 -import { - TRANSACTION_TYPE_CALLS, - TRANSACTION_TYPE_CONTRACTS, -} from './constants'; export type Call = { to: Hex; data?: Hex; value?: bigint }; @@ -39,6 +34,10 @@ export type LifecycleStatus = statusName: 'transactionIdle'; // initial status prior to the mutation function executing statusData: null; } + | { + statusName: 'buildingTransaction'; + statusData: null; + } | { statusName: 'transactionPending'; // if the mutation is currently executing statusData: null; @@ -87,7 +86,11 @@ export type TransactionContextType = { setIsToastVisible: (isVisible: boolean) => void; // A function to set the visibility of the transaction toast. setLifecycleStatus: (state: LifecycleStatus) => void; // A function to set the lifecycle status of the component setTransactionId: (id: string) => void; // A function to set the transaction ID. - transactions?: Call[] | ContractFunctionParameters[]; // An array of transactions for the component. + transactions?: + | Call[] + | ContractFunctionParameters[] + | Promise + | Promise; // An array of transactions for the component or a promise that resolves to an array of transactions. transactionId?: string; // An optional string representing the ID of the transaction. transactionHash?: string; // An optional string representing the hash of the transaction. }; @@ -131,11 +134,13 @@ export type TransactionError = { }; export type TransactionProviderReact = { - calls?: Call[]; // An array of calls for the transaction. Mutually exclusive with the `contracts` prop. + calls?: Call[] | Promise; // An array of calls for the transaction. Mutually exclusive with the `contracts` prop. capabilities?: WalletCapabilities; // Capabilities that a wallet supports (e.g. paymasters, session keys, etc). chainId: number; // The chainId for the transaction. children: ReactNode; // The child components to be rendered within the provider component. - contracts?: ContractFunctionParameters[]; // An array of contract function parameters provided to the child components. Mutually exclusive with the `calls` prop. + contracts?: + | ContractFunctionParameters[] + | Promise; // An array of contract function parameters provided to the child components. Mutually exclusive with the `calls` prop. onError?: (e: TransactionError) => void; // An optional callback function that handles errors within the provider. onStatus?: (lifecycleStatus: LifecycleStatus) => void; // An optional callback function that exposes the component lifecycle state onSuccess?: (response: TransactionResponse) => void; // An optional callback function that exposes the transaction receipts @@ -145,12 +150,14 @@ export type TransactionProviderReact = { * Note: exported as public Type */ export type TransactionReact = { - calls?: Call[]; // An array of calls to be made in the transaction. Mutually exclusive with the `contracts` prop. + calls?: Call[] | Promise; // An array of calls to be made in the transaction. Mutually exclusive with the `contracts` prop. capabilities?: WalletCapabilities; // Capabilities that a wallet supports (e.g. paymasters, session keys, etc). chainId?: number; // The chainId for the transaction. children: ReactNode; // The child components to be rendered within the transaction component. className?: string; // An optional CSS class name for styling the component. - contracts?: ContractFunctionParameters[]; // An array of contract function parameters for the transaction. Mutually exclusive with the `calls` prop. + contracts?: + | ContractFunctionParameters[] + | Promise; // An array of contract function parameters for the transaction. Mutually exclusive with the `calls` prop. onError?: (e: TransactionError) => void; // An optional callback function that handles transaction errors. onStatus?: (lifecycleStatus: LifecycleStatus) => void; // An optional callback function that exposes the component lifecycle state onSuccess?: (response: TransactionResponse) => void; // An optional callback function that exposes the transaction receipts @@ -253,7 +260,6 @@ export type UseSendWalletTransactionsParams = { // biome-ignore lint: cannot find module 'wagmi/experimental/query' sendCallsAsync: any; sendCallAsync: SendTransactionMutateAsync | (() => void); - transactions?: Call[] | ContractFunctionParameters[]; transactionType: string; walletCapabilities: ViemWalletCapabilities; // biome-ignore lint: cannot find module 'wagmi/experimental/query' @@ -261,22 +267,6 @@ export type UseSendWalletTransactionsParams = { writeContractAsync: WriteContractMutateAsync | (() => void); }; -export type UseTransactionTypeParams = { - calls?: Call[]; - contracts?: ContractFunctionParameters[]; - transactionStatuses: { - [TRANSACTION_TYPE_CALLS]: { - single: string; - batch: string; - }; - [TRANSACTION_TYPE_CONTRACTS]: { - single: string; - batch: string; - }; - }; - walletCapabilities: ViemWalletCapabilities; -}; - /** * Note: exported as public Type * diff --git a/src/transaction/utils/isSpinnerDisplayed.test.ts b/src/transaction/utils/isSpinnerDisplayed.test.ts index f09f0a3228..3395b7e12c 100644 --- a/src/transaction/utils/isSpinnerDisplayed.test.ts +++ b/src/transaction/utils/isSpinnerDisplayed.test.ts @@ -43,6 +43,22 @@ describe('isSpinnerDisplayed', () => { expect(result).toEqual(true); }); + it('should return true if status is building', () => { + transactionHash = ''; + errorMessage = ''; + lifecycleStatus = { statusName: 'buildingTransaction', statusData: null }; + transactionId = ''; + isLoading = false; + const result = isSpinnerDisplayed({ + errorMessage, + isLoading, + lifecycleStatus, + transactionHash, + transactionId, + }); + expect(result).toEqual(true); + }); + it('should return true if status is pending', () => { transactionHash = ''; errorMessage = ''; diff --git a/src/transaction/utils/isSpinnerDisplayed.ts b/src/transaction/utils/isSpinnerDisplayed.ts index 5dce5beac8..b10ab2b8f0 100644 --- a/src/transaction/utils/isSpinnerDisplayed.ts +++ b/src/transaction/utils/isSpinnerDisplayed.ts @@ -8,7 +8,9 @@ export function isSpinnerDisplayed({ transactionHash, transactionId, }: IsSpinnerDisplayedProps) { - const isPending = lifecycleStatus.statusName === 'transactionPending'; + const isPending = + lifecycleStatus.statusName === 'transactionPending' || + lifecycleStatus.statusName === 'buildingTransaction'; const isInProgress = transactionId || transactionHash; if (hasReceipt || errorMessage) {