-
Notifications
You must be signed in to change notification settings - Fork 158
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added
useSendCall
and useSendCalls
hooks to support call-ty…
…pe transactions in `Transaction` component (#1130)
- Loading branch information
Showing
6 changed files
with
429 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | ||
}, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
Oops, something went wrong.