Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add useSendCall and useSendCalls hooks to support call-type transactions in Transaction component #1130

Merged
merged 7 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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