-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Hooks to update Safe threshold + owners (#13)
* Use `useMutation` from `@tanstack/react-query` instead of from `wagmi/query` * Remove debug log line * refactor: Update `useSendTransaction` to handle SafeTransaction objects The useSendTransaction hook now handles both TransactionBase and SafeTransaction objects. It maps the transactions array and converts any SafeTransaction objects to the required format before sending them to the signerClient. * Add `isOwnerConnected` flag It defines whether the connected signer is an owner of the configured Safe * Add `useSignerClientMutation` hook for sending custom mutations via the SafeClient * Add UpdateThreshold hook for updating the threshold of the connected Safe * Add 'useAddOwner' hook for adding an owner to the connected Safe * Add `useRemoveOwner` hook for removing an owner from the connected Safe * Add `useSwapOwner` hook for swapping an owner of the connected Safe * Add `useUpdateOwners` hook for managing owners of the connected Safe It wraps the individual hooks: - `useAddOwner` - `useRemoveOwner` - `useSwapOwner` * refactor: Update usePendingTransactions to use usePublicClientQuery Refactor the usePendingTransactions hook to use the usePublicClientQuery hook instead of the usePublicClient hook. * Unit tests for `useSignerClientMutation` hook * refactor: Update `useUpdateThreshold` hook to use `useSignerClientMutation` * Unit tests for `useUpdateThreshold` hook * refactor: Update `useSendTransaction` hook to use `useSignerClientMutation` * refactor: Update `useConfirmTransaction` hook to use `useSignerClientMutation` * Add unit tests for `useUpdateOwners` hook Also add fixtures for mutation result objects * Refactor tests to use mutation result object fixtures that were added in previous commit * Add unit tests for `useRemoveOwner` hook * Add unit tests for `useSwapOwner` hook * Fix tests * Add unit tests for `useUpdateOwners` hook * refactor: Rename `getCustomMutationResult` to `createCustomMutationResult` for consistency * refactor: Add `createCustomQueryResult` for consistency with createCustomMutationResult * refactor: Call functions directly from SafeClient instance, instead of from `SafeClient.protocolKit` * refactor: Improve useAuthenticate hook Call `signerClient.isOwner` instead of checking owners array to compute `isOwnerConnected`. Throw error if not rendered in SafeProvider * Fix name of `useUpdateOwners` hook's param type * Improve JSDoc descriptions * Improve UseTransactionParams The config param can only be defined in combination with the safeTxHash param.
- Loading branch information
Showing
43 changed files
with
2,160 additions
and
488 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
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
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 |
---|---|---|
@@ -1,78 +1,156 @@ | ||
import { act } from 'react' | ||
import { waitFor } from '@testing-library/react' | ||
import { SafeClient } from '@safe-global/sdk-starter-kit' | ||
import { useAuthenticate } from '@/hooks/useAuthenticate.js' | ||
import { renderHookInMockedSafeProvider } from '@test/utils.js' | ||
import { signerPrivateKeys } from '@test/fixtures/index.js' | ||
import { useAuthenticate, UseConnectSignerReturnType } from '@/hooks/useAuthenticate.js' | ||
import * as useSignerAddress from '@/hooks/useSignerAddress.js' | ||
import { catchHookError, renderHookInMockedSafeProvider } from '@test/utils.js' | ||
import { safeInfo, signerPrivateKeys } from '@test/fixtures/index.js' | ||
import { SafeContextType } from '@/SafeContext.js' | ||
import { configExistingSafe } from '@test/config.js' | ||
|
||
describe('useAuthenticate', () => { | ||
const signerClientMock = { safeClient: 'signer' } as unknown as SafeClient | ||
const isOwnerMock = jest.fn() | ||
const signerClientMock = { isOwner: isOwnerMock } as unknown as SafeClient | ||
const setSignerMock = jest.fn(() => Promise.resolve()) | ||
const useSignerAddressSpy = jest.spyOn(useSignerAddress, 'useSignerAddress') | ||
|
||
const renderUseAuthenticate = async (context: Partial<SafeContextType> = {}) => { | ||
const renderUseAuthenticate = async ( | ||
context: Partial<SafeContextType> = {}, | ||
expected: Partial<UseConnectSignerReturnType> = {} | ||
) => { | ||
const renderOptions = { | ||
signerClient: signerClientMock, | ||
setSigner: setSignerMock, | ||
config: configExistingSafe, | ||
...context | ||
} | ||
|
||
const renderResult = renderHookInMockedSafeProvider(() => useAuthenticate(), renderOptions) | ||
const renderResult = renderHookInMockedSafeProvider(useAuthenticate, renderOptions) | ||
|
||
await waitFor(() => | ||
expect(renderResult.result.current).toEqual({ | ||
connect: expect.any(Function), | ||
disconnect: expect.any(Function), | ||
isSignerConnected: !!renderOptions.signerClient | ||
isSignerConnected: false, | ||
isOwnerConnected: false, | ||
...expected | ||
}) | ||
) | ||
|
||
return renderResult | ||
} | ||
|
||
beforeEach(() => { | ||
isOwnerMock.mockResolvedValue(true) | ||
useSignerAddressSpy.mockReturnValue(safeInfo.owners[1]) | ||
}) | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks() | ||
jest.resetAllMocks() | ||
}) | ||
|
||
describe('isOwnerConnected', () => { | ||
it('should be true if connected signer is not owner of the Safe', async () => { | ||
useSignerAddressSpy.mockReturnValue(safeInfo.owners[0]) | ||
|
||
const { | ||
result: { | ||
current: { isSignerConnected, isOwnerConnected } | ||
} | ||
} = await renderUseAuthenticate(undefined, { | ||
isSignerConnected: true, | ||
isOwnerConnected: true | ||
}) | ||
|
||
expect(isSignerConnected).toBeTruthy() | ||
expect(isOwnerConnected).toBeTruthy() | ||
}) | ||
|
||
it('should be false if connected signer is not owner of the Safe', async () => { | ||
useSignerAddressSpy.mockReturnValueOnce(safeInfo.owners[0]) | ||
isOwnerMock.mockResolvedValueOnce(false) | ||
|
||
const { | ||
result: { | ||
current: { isSignerConnected, isOwnerConnected } | ||
} | ||
} = await renderUseAuthenticate(undefined, { | ||
isSignerConnected: true | ||
}) | ||
|
||
expect(isSignerConnected).toBeTruthy() | ||
expect(isOwnerConnected).toBeFalsy() | ||
}) | ||
}) | ||
|
||
describe('connect', () => { | ||
it('should create a new signer client if being called with a valid private key', async () => { | ||
const { result } = await renderUseAuthenticate() | ||
const { result } = await renderUseAuthenticate(undefined, { | ||
isSignerConnected: true, | ||
isOwnerConnected: true | ||
}) | ||
|
||
await act(() => result.current.connect(signerPrivateKeys[1])) | ||
|
||
expect(isOwnerMock).toHaveBeenCalledTimes(1) | ||
expect(useSignerAddressSpy).toHaveBeenCalledTimes(2) | ||
|
||
expect(setSignerMock).toHaveBeenCalledTimes(1) | ||
expect(setSignerMock).toHaveBeenCalledWith(signerPrivateKeys[1]) | ||
}) | ||
|
||
it('should throw if being called with an empty private key string', async () => { | ||
useSignerAddressSpy.mockReturnValueOnce(undefined) | ||
|
||
const { result } = await renderUseAuthenticate() | ||
|
||
expect(() => result.current.connect('')).rejects.toThrow( | ||
'Failed to connect because signer is empty' | ||
) | ||
|
||
expect(isOwnerMock).toHaveBeenCalledTimes(0) | ||
expect(useSignerAddressSpy).toHaveBeenCalledTimes(1) | ||
|
||
expect(setSignerMock).toHaveBeenCalledTimes(0) | ||
}) | ||
}) | ||
|
||
describe('disconnect', () => { | ||
it('should set signer to `undefined` if connected', async () => { | ||
const { result } = await renderUseAuthenticate() | ||
const { result } = await renderUseAuthenticate(undefined, { | ||
isSignerConnected: true, | ||
isOwnerConnected: true | ||
}) | ||
|
||
await act(() => result.current.disconnect()) | ||
|
||
expect(isOwnerMock).toHaveBeenCalledTimes(1) | ||
expect(useSignerAddressSpy).toHaveBeenCalledTimes(2) | ||
|
||
expect(setSignerMock).toHaveBeenCalledTimes(1) | ||
expect(setSignerMock).toHaveBeenCalledWith(undefined) | ||
}) | ||
|
||
it('should throw if being called when signerClient is not defined', async () => { | ||
useSignerAddressSpy.mockReturnValueOnce(undefined) | ||
|
||
const { result } = await renderUseAuthenticate({ signerClient: undefined }) | ||
|
||
expect(() => result.current.disconnect()).rejects.toThrow( | ||
'Failed to disconnect because no signer is connected' | ||
) | ||
|
||
expect(isOwnerMock).toHaveBeenCalledTimes(0) | ||
expect(useSignerAddressSpy).toHaveBeenCalledTimes(1) | ||
|
||
expect(setSignerMock).toHaveBeenCalledTimes(0) | ||
}) | ||
}) | ||
|
||
it('should throw if not used within a `SafeProvider`', async () => { | ||
const error = catchHookError(() => useAuthenticate()) | ||
|
||
expect(error?.message).toEqual('`useAuthenticate` must be used within `SafeProvider`.') | ||
}) | ||
}) |
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
Oops, something went wrong.