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 63af146
Show file tree
Hide file tree
Showing 17 changed files with 293 additions and 71 deletions.
2 changes: 2 additions & 0 deletions playground/nextjs-app-router/components/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export enum OnchainKitComponent {
export enum TransactionTypes {
Calls = 'calls',
Contracts = 'contracts',
CallsPromise = 'callsPromise',
ContractsPromise = 'contractsPromise',
}

export type Paymaster = {
Expand Down
74 changes: 63 additions & 11 deletions playground/nextjs-app-router/components/demo/Transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,89 @@ import {
TransactionToastIcon,
TransactionToastLabel,
} from '@coinbase/onchainkit/transaction';
import { useCallback, useContext, useEffect } from 'react';
import { useCallback, useContext, useEffect, useMemo } from 'react';
import { AppContext, TransactionTypes } from '../AppProvider';
import { Call } from '@/onchainkit/esm/transaction/types';
import { ContractFunctionParameters } from 'viem';
import { LifecycleStatus } from '@/onchainkit/src/transaction';

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<Call[]>;
const promiseContracts = new Promise((resolve) => {
setTimeout(() => {
resolve(contracts);
}, 4000);
}) as Promise<ContractFunctionParameters[]>;
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 };
}, []);

return (
<div className="mx-auto grid w-1/2 gap-8">
<Transaction
chainId={chainId ?? 84532} // something breaks if we don't have default network?
{...(transactionType === TransactionTypes.Calls
? { calls }
: { contracts })}
{...transactions}
capabilities={capabilities}
onStatus={handleOnStatus}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export function TransactionOptions() {
<SelectItem value={TransactionTypes.Contracts}>
Contracts
</SelectItem>
<SelectItem value={TransactionTypes.CallsPromise}>
Calls Promise
</SelectItem>
<SelectItem value={TransactionTypes.ContractsPromise}>
Contracts Promise
</SelectItem>
</SelectContent>
</Select>
</div>
Expand Down
2 changes: 1 addition & 1 deletion playground/nextjs-app-router/onchainkit/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/transaction/components/Transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
26 changes: 25 additions & 1 deletion src/transaction/components/TransactionProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ const TestComponent = () => {
});
};
const handleStatusTransactionLegacyExecutedMultipleContracts = async () => {
context.setLifecycleStatus({
await context.onSubmit();
await context.setLifecycleStatus({
statusName: 'transactionLegacyExecuted',
statusData: {
transactionHashList: ['hash12345678', 'hash12345678'],
Expand Down Expand Up @@ -256,6 +257,29 @@ describe('TransactionProvider', () => {
});
});

it('should emit onError when building transactions fails', async () => {
const sendWalletTransactionsMock = vi.fn();
(useSendWalletTransactions as ReturnType<typeof vi.fn>).mockReturnValue(
sendWalletTransactionsMock,
);
const onErrorMock = vi.fn();
const contracts = Promise.reject(new Error('error'));
render(
<TransactionProvider contracts={contracts} onError={onErrorMock}>
<TestComponent />
</TransactionProvider>,
);
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<typeof vi.fn>).mockRejectedValue(
Expand Down
33 changes: 29 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,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;
}
}, []);

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
8 changes: 8 additions & 0 deletions src/transaction/hooks/useGetTransactionStatusLabel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
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]);
}
16 changes: 16 additions & 0 deletions src/transaction/hooks/useGetTransactionToastLabel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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());
Expand All @@ -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(),
});

Expand All @@ -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());
Expand All @@ -81,6 +95,7 @@ describe('useGetTransactionToastLabel', () => {
const onSubmitMock = vi.fn();
(useTransactionContext as Mock).mockReturnValue({
errorMessage: 'error',
lifecycleStatus: { statusName: 'init' },
onSubmit: onSubmitMock,
});

Expand All @@ -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());
Expand Down
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';

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

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

if (isInProgress) {
label = 'Transaction in progress';
}
Expand Down
Loading

0 comments on commit 63af146

Please sign in to comment.