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: Hooks to update Safe threshold + owners #13

Merged
merged 29 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
103023e
Use `useMutation` from `@tanstack/react-query` instead of from `wagmi…
tmjssz Sep 27, 2024
efdf31b
Remove debug log line
tmjssz Sep 27, 2024
c6f3410
refactor: Update `useSendTransaction` to handle SafeTransaction objects
tmjssz Sep 27, 2024
aaf8028
Add `isOwnerConnected` flag
tmjssz Sep 27, 2024
e990b78
Add `useSignerClientMutation` hook for sending custom mutations via t…
tmjssz Sep 27, 2024
e59668a
Add UpdateThreshold hook for updating the threshold of the connected …
tmjssz Sep 27, 2024
596e64a
Add 'useAddOwner' hook for adding an owner to the connected Safe
tmjssz Sep 27, 2024
2ca3247
Add `useRemoveOwner` hook for removing an owner from the connected Safe
tmjssz Sep 27, 2024
a755088
Add `useSwapOwner` hook for swapping an owner of the connected Safe
tmjssz Sep 27, 2024
ae4af85
Add `useUpdateOwners` hook for managing owners of the connected Safe
tmjssz Sep 27, 2024
85f161d
refactor: Update usePendingTransactions to use usePublicClientQuery
tmjssz Sep 27, 2024
3164ccd
Unit tests for `useSignerClientMutation` hook
tmjssz Sep 27, 2024
7dc99c9
refactor: Update `useUpdateThreshold` hook to use `useSignerClientMut…
tmjssz Sep 27, 2024
0cd5e9f
Unit tests for `useUpdateThreshold` hook
tmjssz Sep 27, 2024
2087fc4
refactor: Update `useSendTransaction` hook to use `useSignerClientMut…
tmjssz Sep 27, 2024
c66588c
refactor: Update `useConfirmTransaction` hook to use `useSignerClient…
tmjssz Sep 30, 2024
19d24b1
Add unit tests for `useUpdateOwners` hook
tmjssz Oct 1, 2024
31fc54c
Refactor tests to use mutation result object fixtures that were added…
tmjssz Oct 1, 2024
9f2559a
Add unit tests for `useRemoveOwner` hook
tmjssz Oct 1, 2024
0aad173
Add unit tests for `useSwapOwner` hook
tmjssz Oct 1, 2024
e2da97a
Fix tests
tmjssz Oct 1, 2024
0e3ccf5
Add unit tests for `useUpdateOwners` hook
tmjssz Oct 1, 2024
899658d
refactor: Rename `getCustomMutationResult` to `createCustomMutationRe…
tmjssz Oct 2, 2024
37207ab
refactor: Add `createCustomQueryResult` for consistency with createCu…
tmjssz Oct 2, 2024
05c3a3e
refactor: Call functions directly from SafeClient instance, instead o…
tmjssz Oct 2, 2024
09b3ed6
refactor: Improve useAuthenticate hook
tmjssz Oct 2, 2024
0395fb2
Fix name of `useUpdateOwners` hook's param type
tmjssz Oct 7, 2024
94873dc
Improve JSDoc descriptions
tmjssz Oct 7, 2024
2d931f4
Improve UseTransactionParams
tmjssz Oct 8, 2024
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
6 changes: 5 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@ export enum QueryKey {

export enum MutationKey {
SendTransaction = 'sendTransaction',
ConfirmTransaction = 'confirmTransaction'
ConfirmTransaction = 'confirmTransaction',
UpdateThreshold = 'updateThreshold',
SwapOwner = 'swapOwner',
AddOwner = 'addOwner',
RemoveOwner = 'removeOwner'
}
3 changes: 3 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ export * from './usePendingTransactions.js'
export * from './useSafe.js'
export * from './usePublicClient.js'
export * from './useSafeInfo/index.js'
export * from './useUpdateOwners/index.js'
export * from './useSendTransaction.js'
export * from './useSignerAddress.js'
export * from './useSignerClient.js'
export * from './useTransaction.js'
export * from './useTransactions.js'
export * from './useUpdateThreshold.js'
export * from './useWaitForTransaction.js'
96 changes: 87 additions & 9 deletions src/hooks/useAuthenticate.test.ts
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`.')
})
})
28 changes: 24 additions & 4 deletions src/hooks/useAuthenticate.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import { useCallback, useContext } from 'react'
import { useCallback, useContext, useEffect, useState } from 'react'
import { SafeContext } from '@/SafeContext.js'
import { useSignerAddress } from '@/hooks/useSignerAddress.js'
import { AuthenticationError } from '@/errors/AuthenticationError.js'
import { ConfigParam, SafeConfig } from '@/types/index.js'
import { MissingSafeProviderError } from '@/errors/MissingSafeProviderError.js'

export type UseAuthenticateParams = ConfigParam<SafeConfig>

export type UseConnectSignerReturnType = {
connect: (signer: string) => Promise<void>
disconnect: () => Promise<void>
isSignerConnected: boolean
isOwnerConnected: boolean
}

/**
* Hook to authenticate a signer.
* @returns Functions to connect and disconnect a signer.
*/
export function useAuthenticate(): UseConnectSignerReturnType {
const { signerClient, setSigner } = useContext(SafeContext)
const { signerClient, setSigner, config } = useContext(SafeContext) || {}

if (!config) {
throw new MissingSafeProviderError('`useAuthenticate` must be used within `SafeProvider`.')
}

const signerAddress = useSignerAddress()

const [isOwnerConnected, setIsOwnerConnected] = useState(false)

const connect = useCallback(
async (signer: string) => {
Expand All @@ -32,7 +46,13 @@ export function useAuthenticate(): UseConnectSignerReturnType {
return setSigner(undefined)
}, [setSigner])

const isSignerConnected = !!signerClient
const isSignerConnected = !!signerAddress

useEffect(() => {
if (signerClient && signerAddress) {
signerClient.isOwner(signerAddress).then(setIsOwnerConnected)
}
}, [signerClient, signerAddress])

return { connect, disconnect, isSignerConnected }
return { connect, disconnect, isSignerConnected, isOwnerConnected }
}
Loading