Skip to content

Commit

Permalink
feat: added useSendCall and useSendCalls hooks to support call-ty…
Browse files Browse the repository at this point in the history
…pe transactions in `Transaction` component (#1130)
  • Loading branch information
0xAlec authored Aug 21, 2024
1 parent 746894d commit 07c5af6
Show file tree
Hide file tree
Showing 6 changed files with 429 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-actors-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/onchainkit": patch
---

**feat**: add `useSendCall` and `useSendCalls` hooks to support call-type transactions in `Transaction` component by @0xAlec #1130
188 changes: 188 additions & 0 deletions src/transaction/hooks/useSendCall.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useSendTransaction as useSendCallWagmi } from 'wagmi';
import { GENERIC_ERROR_MESSAGE } from '../constants';
import { isUserRejectedRequestError } from '../utils/isUserRejectedRequestError';
import { useSendCall } from './useSendCall';

vi.mock('wagmi', () => ({
useSendTransaction: vi.fn(),
}));

vi.mock('../utils/isUserRejectedRequestError', () => ({
isUserRejectedRequestError: vi.fn(),
}));

type UseSendCallConfig = {
mutation: {
onError: (error: Error) => void;
onSuccess: (hash: string) => void;
};
};

type MockUseSendCallReturn = {
status: 'idle' | 'error' | 'loading' | 'success';
sendCallAsync: ReturnType<typeof vi.fn>;
data: string | null;
};

describe('useSendCall', () => {
const mockSetLifeCycleStatus = vi.fn();

beforeEach(() => {
vi.resetAllMocks();
});

it('should return wagmi hook data when successful', () => {
const mockSendTransaction = vi.fn();
const mockData = 'mockTransactionHash';
(useSendCallWagmi as ReturnType<typeof vi.fn>).mockReturnValue({
status: 'idle',
sendTransactionAsync: mockSendTransaction,
data: mockData,
});
const { result } = renderHook(() =>
useSendCall({
setLifeCycleStatus: mockSetLifeCycleStatus,
transactionHashList: [],
}),
);
expect(result.current.status).toBe('idle');
expect(result.current.sendCallAsync).toBe(mockSendTransaction);
expect(result.current.data).toBe(mockData);
});

it('should handle generic error', () => {
const genericError = new Error(GENERIC_ERROR_MESSAGE);
let onErrorCallback: ((error: Error) => void) | undefined;
(useSendCallWagmi as ReturnType<typeof vi.fn>).mockImplementation(
({ mutation }: UseSendCallConfig) => {
onErrorCallback = mutation.onError;
return {
sendCallAsync: vi.fn(),
data: null,
status: 'error',
} as MockUseSendCallReturn;
},
);
renderHook(() =>
useSendCall({
setLifeCycleStatus: mockSetLifeCycleStatus,
transactionHashList: [],
}),
);
expect(onErrorCallback).toBeDefined();
onErrorCallback?.(genericError);
expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({
statusName: 'error',
statusData: {
code: 'TmUSCh01',
error: GENERIC_ERROR_MESSAGE,
message: GENERIC_ERROR_MESSAGE,
},
});
});

it('should handle user rejected error', () => {
const userRejectedError = new Error('Request denied.');
let onErrorCallback: ((error: Error) => void) | undefined;
(useSendCallWagmi as ReturnType<typeof vi.fn>).mockImplementation(
({ mutation }: UseSendCallConfig) => {
onErrorCallback = mutation.onError;
return {
sendCallAsync: vi.fn(),
data: null,
status: 'error',
} as MockUseSendCallReturn;
},
);
(isUserRejectedRequestError as vi.Mock).mockReturnValue(true);
renderHook(() =>
useSendCall({
setLifeCycleStatus: mockSetLifeCycleStatus,
transactionHashList: [],
}),
);
expect(onErrorCallback).toBeDefined();
onErrorCallback?.(userRejectedError);
expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({
statusName: 'error',
statusData: {
code: 'TmUSCh01',
error: 'Request denied.',
message: 'Request denied.',
},
});
});

it('should handle successful transaction', () => {
const transactionHash = '0x123456';
let onSuccessCallback: ((hash: string) => void) | undefined;
(useSendCallWagmi as ReturnType<typeof vi.fn>).mockImplementation(
({ mutation }: UseSendCallConfig) => {
onSuccessCallback = mutation.onSuccess;
return {
sendCallAsync: vi.fn(),
data: transactionHash,
status: 'success',
} as MockUseSendCallReturn;
},
);
renderHook(() =>
useSendCall({
setLifeCycleStatus: mockSetLifeCycleStatus,
transactionHashList: [],
}),
);
expect(onSuccessCallback).toBeDefined();
onSuccessCallback?.(transactionHash);
expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({
statusName: 'transactionLegacyExecuted',
statusData: {
transactionHashList: [transactionHash],
},
});
});

it('should handle multiple successful transactions', () => {
const transactionHash = '0x12345678';
let onSuccessCallback: ((hash: string) => void) | undefined;
(useSendCallWagmi as ReturnType<typeof vi.fn>).mockImplementation(
({ mutation }: UseSendCallConfig) => {
onSuccessCallback = mutation.onSuccess;
return {
sendCallAsync: vi.fn(),
data: transactionHash,
status: 'success',
} as MockUseSendCallReturn;
},
);
renderHook(() =>
useSendCall({
setLifeCycleStatus: mockSetLifeCycleStatus,
transactionHashList: [],
}),
);
expect(onSuccessCallback).toBeDefined();
onSuccessCallback?.(transactionHash);
expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({
statusName: 'transactionLegacyExecuted',
statusData: {
transactionHashList: [transactionHash],
},
});
renderHook(() =>
useSendCall({
setLifeCycleStatus: mockSetLifeCycleStatus,
transactionHashList: [transactionHash],
}),
);
onSuccessCallback?.(transactionHash);
expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({
statusName: 'transactionLegacyExecuted',
statusData: {
transactionHashList: [transactionHash, transactionHash],
},
});
});
});
46 changes: 46 additions & 0 deletions src/transaction/hooks/useSendCall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Address } from 'viem';
import { useSendTransaction as useSendCallWagmi } from 'wagmi';
import { GENERIC_ERROR_MESSAGE } from '../constants';
import type { UseSendCallParams } from '../types';
import { isUserRejectedRequestError } from '../utils/isUserRejectedRequestError';

/**
* Wagmi hook for single transactions with calldata.
* Supports both EOAs and Smart Wallets.
* Does not support transaction batching or paymasters.
*/
export function useSendCall({
setLifeCycleStatus,
transactionHashList,
}: UseSendCallParams) {
const {
status,
sendTransactionAsync: sendCallAsync,
data,
} = useSendCallWagmi({
mutation: {
onError: (e) => {
const errorMessage = isUserRejectedRequestError(e)
? 'Request denied.'
: GENERIC_ERROR_MESSAGE;
setLifeCycleStatus({
statusName: 'error',
statusData: {
code: 'TmUSCh01', // Transaction module UseSendCall hook 01 error
error: e.message,
message: errorMessage,
},
});
},
onSuccess: (hash: Address) => {
setLifeCycleStatus({
statusName: 'transactionLegacyExecuted',
statusData: {
transactionHashList: [...transactionHashList, hash],
},
});
},
},
});
return { status, sendCallAsync, data };
}
143 changes: 143 additions & 0 deletions src/transaction/hooks/useSendCalls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { renderHook } from '@testing-library/react';
import type { TransactionExecutionError } from 'viem';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useSendCalls as useSendCallsWagmi } from 'wagmi/experimental';
import { GENERIC_ERROR_MESSAGE } from '../constants';
import { isUserRejectedRequestError } from '../utils/isUserRejectedRequestError';
import { useSendCalls } from './useSendCalls';

vi.mock('wagmi/experimental', () => ({
useSendCalls: vi.fn(),
}));

vi.mock('../utils/isUserRejectedRequestError', () => ({
isUserRejectedRequestError: vi.fn(),
}));

type UseSendCallsConfig = {
mutation: {
onSettled: () => void;
onError: (error: TransactionExecutionError) => void;
onSuccess: (id: string) => void;
};
};

describe('useSendCalls', () => {
const mockSetLifeCycleStatus = vi.fn();
const mockSetTransactionId = vi.fn();

beforeEach(() => {
vi.resetAllMocks();
});

it('should return wagmi hook data when successful', () => {
const mockSendCallsAsync = vi.fn();
const mockData = 'mockTransactionId';
(useSendCallsWagmi as ReturnType<typeof vi.fn>).mockReturnValue({
status: 'idle',
sendCallsAsync: mockSendCallsAsync,
data: mockData,
});
const { result } = renderHook(() =>
useSendCalls({
setLifeCycleStatus: mockSetLifeCycleStatus,
setTransactionId: mockSetTransactionId,
}),
);
expect(result.current.status).toBe('idle');
expect(result.current.sendCallsAsync).toBe(mockSendCallsAsync);
expect(result.current.data).toBe(mockData);
});

it('should handle generic error', () => {
const genericError = new Error(GENERIC_ERROR_MESSAGE);
let onErrorCallback:
| ((error: TransactionExecutionError) => void)
| undefined;
(useSendCallsWagmi as ReturnType<typeof vi.fn>).mockImplementation(
({ mutation }: UseSendCallsConfig) => {
onErrorCallback = mutation.onError;
return {
sendCallsAsync: vi.fn(),
data: null,
status: 'error',
};
},
);
(isUserRejectedRequestError as vi.Mock).mockReturnValue(false);
renderHook(() =>
useSendCalls({
setLifeCycleStatus: mockSetLifeCycleStatus,
setTransactionId: mockSetTransactionId,
}),
);
expect(onErrorCallback).toBeDefined();
onErrorCallback?.(genericError as TransactionExecutionError);
expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({
statusName: 'error',
statusData: {
code: 'TmUSCSh01',
error: GENERIC_ERROR_MESSAGE,
message: GENERIC_ERROR_MESSAGE,
},
});
});

it('should handle user rejected error', () => {
const userRejectedError = new Error('Request denied.');
let onErrorCallback:
| ((error: TransactionExecutionError) => void)
| undefined;
(useSendCallsWagmi as ReturnType<typeof vi.fn>).mockImplementation(
({ mutation }: UseSendCallsConfig) => {
onErrorCallback = mutation.onError;
return {
sendCallsAsync: vi.fn(),
data: null,
status: 'error',
};
},
);
(isUserRejectedRequestError as vi.Mock).mockReturnValue(true);
renderHook(() =>
useSendCalls({
setLifeCycleStatus: mockSetLifeCycleStatus,
setTransactionId: mockSetTransactionId,
}),
);
expect(onErrorCallback).toBeDefined();
onErrorCallback?.(userRejectedError as TransactionExecutionError);
expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({
statusName: 'error',
statusData: {
code: 'TmUSCSh01',
error: 'Request denied.',
message: 'Request denied.',
},
});
});

it('should handle successful transaction', () => {
const transactionId = '0x123456';
let onSuccessCallback: ((id: string) => void) | undefined;
(useSendCallsWagmi as ReturnType<typeof vi.fn>).mockImplementation(
({ mutation }: UseSendCallsConfig) => {
onSuccessCallback = mutation.onSuccess;
return {
sendCallsAsync: vi.fn(),
data: transactionId,
status: 'success',
};
},
);
renderHook(() =>
useSendCalls({
setLifeCycleStatus: mockSetLifeCycleStatus,
setTransactionId: mockSetTransactionId,
}),
);
expect(onSuccessCallback).toBeDefined();
onSuccessCallback?.(transactionId);
expect(mockSetTransactionId).toHaveBeenCalledWith(transactionId);
});
});
Loading

0 comments on commit 07c5af6

Please sign in to comment.