Skip to content

Commit

Permalink
feat: add promises to transactions and calls
Browse files Browse the repository at this point in the history
  • Loading branch information
alessey committed Oct 18, 2024
1 parent 64bd519 commit 9c3253b
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 51 deletions.
16 changes: 14 additions & 2 deletions playground/nextjs-app-router/components/demo/Transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,24 @@ import {
} from '@coinbase/onchainkit/transaction';
import { useCallback, useContext, useEffect } from 'react';
import { AppContext, TransactionTypes } from '../AppProvider';
import { Call } from '@/onchainkit/esm/transaction/types';
import { ContractFunctionParameters } from 'viem';

function TransactionDemo() {
const { chainId, transactionType } = useContext(AppContext);
const capabilities = useCapabilities();
const contracts = clickContracts;
const calls = clickCalls;
const promiseCalls = new Promise((resolve) => {
setTimeout(() => {
resolve(calls);
}, 4000);
}) as Promise<Call[]>;
const promiseContracts = new Promise((resolve) => {
setTimeout(() => {
resolve(contracts);
}, 4000);
}) as Promise<ContractFunctionParameters[]>;
useEffect(() => {
console.log('Playground.Transaction.chainId:', chainId);
}, [chainId]);
Expand All @@ -41,8 +53,8 @@ function TransactionDemo() {
<Transaction
chainId={chainId ?? 84532} // something breaks if we don't have default network?
{...(transactionType === TransactionTypes.Calls
? { calls }
: { contracts })}
? { calls: promiseCalls }
: { contracts: promiseContracts })}
capabilities={capabilities}
onStatus={handleOnStatus}
>
Expand Down
4 changes: 3 additions & 1 deletion src/transaction/components/TransactionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
32 changes: 28 additions & 4 deletions src/transaction/components/TransactionProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address[]>([]);
const transactions = calls || contracts;
const transactionType = calls
Expand Down Expand Up @@ -160,7 +163,6 @@ export function TransactionProvider({
capabilities,
sendCallAsync,
sendCallsAsync,
transactions,
transactionType,
walletCapabilities,
writeContractAsync,
Expand Down Expand Up @@ -233,8 +235,8 @@ export function TransactionProvider({
useEffect(() => {
if (
!transactions ||
transactionHashList.length !== transactions.length ||
transactions.length < 2
transactionHashList.length !== transactionCount ||
transactionCount < 2
) {
return;
}
Expand Down Expand Up @@ -278,13 +280,35 @@ 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',
},
});
}
}, []);

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.'
Expand Down
10 changes: 9 additions & 1 deletion src/transaction/hooks/useGetTransactionStatusLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.';
}
Expand All @@ -39,5 +47,5 @@ export function useGetTransactionStatusLabel() {
}

return { label, labelClassName };
}, [errorMessage, isInProgress, isPending, receipt]);
}, [errorMessage, isBuildingTransaction, isInProgress, isPending, receipt]);
}
18 changes: 16 additions & 2 deletions src/transaction/hooks/useGetTransactionToastLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Check failure on line 20 in src/transaction/hooks/useGetTransactionToastLabel.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

src/transaction/hooks/useGetTransactionToastLabel.test.tsx > useGetTransactionToastLabel > should return correct toast and actionElement when transaction is loading

TypeError: Cannot read properties of undefined (reading 'statusName') ❯ Module.useGetTransactionToastLabel src/transaction/hooks/useGetTransactionToastLabel.tsx:20:21 ❯ src/transaction/hooks/useGetTransactionToastLabel.test.tsx:40:41 ❯ TestComponent node_modules/@testing-library/react/dist/pure.js:309:27 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:15486:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20103:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21626:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27465:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26599:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26505:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26473:7

Check failure on line 20 in src/transaction/hooks/useGetTransactionToastLabel.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

src/transaction/hooks/useGetTransactionToastLabel.test.tsx > useGetTransactionToastLabel > should return status when transaction hash exists

TypeError: Cannot read properties of undefined (reading 'statusName') ❯ Module.useGetTransactionToastLabel src/transaction/hooks/useGetTransactionToastLabel.tsx:20:21 ❯ src/transaction/hooks/useGetTransactionToastLabel.test.tsx:50:41 ❯ TestComponent node_modules/@testing-library/react/dist/pure.js:309:27 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:15486:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20103:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21626:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27465:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26599:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26505:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26473:7

Check failure on line 20 in src/transaction/hooks/useGetTransactionToastLabel.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

src/transaction/hooks/useGetTransactionToastLabel.test.tsx > useGetTransactionToastLabel > should return status when transaction id exists

TypeError: Cannot read properties of undefined (reading 'statusName') ❯ Module.useGetTransactionToastLabel src/transaction/hooks/useGetTransactionToastLabel.tsx:20:21 ❯ src/transaction/hooks/useGetTransactionToastLabel.test.tsx:64:41 ❯ TestComponent node_modules/@testing-library/react/dist/pure.js:309:27 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:15486:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20103:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21626:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27465:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26599:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26505:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26473:7

Check failure on line 20 in src/transaction/hooks/useGetTransactionToastLabel.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

src/transaction/hooks/useGetTransactionToastLabel.test.tsx > useGetTransactionToastLabel > should return status when receipt exists

TypeError: Cannot read properties of undefined (reading 'statusName') ❯ Module.useGetTransactionToastLabel src/transaction/hooks/useGetTransactionToastLabel.tsx:20:21 ❯ src/transaction/hooks/useGetTransactionToastLabel.test.tsx:75:41 ❯ TestComponent node_modules/@testing-library/react/dist/pure.js:309:27 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:15486:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20103:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21626:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27465:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26599:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26505:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26473:7

Check failure on line 20 in src/transaction/hooks/useGetTransactionToastLabel.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

src/transaction/hooks/useGetTransactionToastLabel.test.tsx > useGetTransactionToastLabel > should return status when error occurs

TypeError: Cannot read properties of undefined (reading 'statusName') ❯ Module.useGetTransactionToastLabel src/transaction/hooks/useGetTransactionToastLabel.tsx:20:21 ❯ src/transaction/hooks/useGetTransactionToastLabel.test.tsx:87:41 ❯ TestComponent node_modules/@testing-library/react/dist/pure.js:309:27 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:15486:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20103:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21626:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27465:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26599:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26505:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26473:7

Check failure on line 20 in src/transaction/hooks/useGetTransactionToastLabel.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

src/transaction/hooks/useGetTransactionToastLabel.test.tsx > useGetTransactionToastLabel > should return status when no status available

TypeError: Cannot read properties of undefined (reading 'statusName') ❯ Module.useGetTransactionToastLabel src/transaction/hooks/useGetTransactionToastLabel.tsx:20:21 ❯ src/transaction/hooks/useGetTransactionToastLabel.test.tsx:97:41 ❯ TestComponent node_modules/@testing-library/react/dist/pure.js:309:27 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:15486:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20103:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21626:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27465:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26599:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26505:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26473:7

return useMemo(() => {
let label = '';
let labelClassName: string = color.foregroundMuted;

if (isBuildingTransaction) {
label = 'Building transaction';
}

if (isInProgress) {
label = 'Transaction in progress';
}
Expand Down
79 changes: 45 additions & 34 deletions src/transaction/hooks/useSendWalletTransactions.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<Call[]>
| Promise<ContractFunctionParameters[]>,
) => {
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,
],
);
};
23 changes: 17 additions & 6 deletions src/transaction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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;
Expand Down Expand Up @@ -87,7 +91,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<Call[]>
| Promise<ContractFunctionParameters[]>; // An array of transactions for the component.
transactionId?: string; // An optional string representing the ID of the transaction.
transactionHash?: string; // An optional string representing the hash of the transaction.
};
Expand Down Expand Up @@ -131,11 +139,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<Call[]>; // 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<ContractFunctionParameters[]>; // 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
Expand All @@ -145,12 +155,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<Call[]>; // 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<ContractFunctionParameters[]>; // 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
Expand Down Expand Up @@ -253,7 +265,6 @@ export type UseSendWalletTransactionsParams = {
// biome-ignore lint: cannot find module 'wagmi/experimental/query'
sendCallsAsync: any;
sendCallAsync: SendTransactionMutateAsync<Config, unknown> | (() => void);
transactions?: Call[] | ContractFunctionParameters[];
transactionType: string;
walletCapabilities: ViemWalletCapabilities;
// biome-ignore lint: cannot find module 'wagmi/experimental/query'
Expand Down
4 changes: 3 additions & 1 deletion src/transaction/utils/isSpinnerDisplayed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 9c3253b

Please sign in to comment.