Skip to content

Commit

Permalink
feat: Add 4337 hooks (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
yagopv authored Nov 11, 2024
1 parent 4beab17 commit 60b5812
Show file tree
Hide file tree
Showing 27 changed files with 452 additions and 84 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@safe-global/safe-react-hooks",
"version": "0.1.0",
"version": "0.2.0-alpha.0",
"description": "A collection of React Hooks that facilitates the interaction of React apps with Safe Smart Accounts",
"keywords": [
"Ethereum",
Expand Down Expand Up @@ -67,7 +67,7 @@
"typescript": "^5.3.3"
},
"dependencies": {
"@safe-global/sdk-starter-kit": "^1.0.1",
"@safe-global/sdk-starter-kit": "1.1.0-alpha.0",
"viem": "^2.18.6",
"wagmi": "^2.12.2"
},
Expand Down
4 changes: 2 additions & 2 deletions src/SafeContext.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createContext } from 'react'
import { Config } from 'wagmi'
import { SafeClient } from '@safe-global/sdk-starter-kit'
import type { SafeConfig } from '@/types/index.js'

import type { SafeClient, SafeConfig } from '@/types/index.js'

export type SafeContextType = {
isInitialized: boolean
Expand Down
4 changes: 2 additions & 2 deletions src/SafeProvider.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createElement, useCallback, useEffect, useMemo, useState } from 'react'
import { QueryClientProvider } from '@tanstack/react-query'
import { createConfig, WagmiProvider } from 'wagmi'
import { SafeClient } from '@safe-global/sdk-starter-kit'
import { InitializeSafeProviderError } from '@/errors/InitializeSafeProviderError.js'
import type { SafeConfig } from '@/types/index.js'
import { isSafeConfigWithSigner } from '@/types/guards.js'
import { createPublicClient, createSignerClient } from '@/createClient.js'
import { queryClient } from '@/queryClient.js'
import { SafeContext } from '@/SafeContext.js'

import type { SafeClient, SafeConfig } from '@/types/index.js'

export type SafeProviderProps = {
config: SafeConfig
}
Expand Down
8 changes: 6 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export enum QueryKey {
Threshold = 'threshold',
IsDeployed = 'isDeployed',
Owners = 'owners',
SafeInfo = 'safeInfo'
SafeInfo = 'safeInfo',
SafeOperations = 'safeOperations',
PendingSafeOperations = 'pendingSafeOperations'
}

export enum MutationKey {
Expand All @@ -15,5 +17,7 @@ export enum MutationKey {
UpdateThreshold = 'updateThreshold',
SwapOwner = 'swapOwner',
AddOwner = 'addOwner',
RemoveOwner = 'removeOwner'
RemoveOwner = 'removeOwner',
SendSafeOperation = 'sendSafeOperation',
ConfirmSafeOperation = 'confirmSafeOperation'
}
35 changes: 29 additions & 6 deletions src/createClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import type { SafeConfig, SafeConfigWithSigner } from '@/types/index.js'
import { createSafeClient } from '@safe-global/sdk-starter-kit'
import { createSafeClient, safeOperations } from '@safe-global/sdk-starter-kit'
import type {
SafeClient,
SafeConfig,
SafeConfigWithSigner,
SafeOperationOptions
} from '@/types/index.js'

const extendWithSafeOperations = async (
client: SafeClient,
operationOptions: SafeOperationOptions
) => {
const { bundlerUrl, ...paymasterOptions } = operationOptions
return await client.extend(safeOperations({ bundlerUrl }, paymasterOptions))
}

const getPublicClientConfig = ({ provider, safeAddress, safeOptions }: SafeConfig) => ({
signer: undefined,
Expand All @@ -12,16 +25,26 @@ const getPublicClientConfig = ({ provider, safeAddress, safeOptions }: SafeConfi
* @param config Config object for the Safe client
* @returns Safe client instance with public method capabilities
*/
export const createPublicClient = (config: SafeConfig) =>
createSafeClient(getPublicClientConfig(config))
export const createPublicClient = async (config: SafeConfig) => {
const publicClient = await createSafeClient(getPublicClientConfig(config))

return config.safeOperationOptions
? await extendWithSafeOperations(publicClient, config.safeOperationOptions)
: publicClient
}

/**
* Creates a SafeClient instance with signer capabilities.
* @param config Config object for the Safe client with mandatory `signer` property
* @returns Safe client instance with signer capabilities
*/
export const createSignerClient = ({ signer, ...config }: SafeConfigWithSigner) =>
createSafeClient({
export const createSignerClient = async ({ signer, ...config }: SafeConfigWithSigner) => {
const signerClient = await createSafeClient({
...getPublicClientConfig({ ...config, signer: undefined }),
signer
})

return config.safeOperationOptions
? await extendWithSafeOperations(signerClient, config.safeOperationOptions)
: signerClient
}
5 changes: 5 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ export * from './useTransaction.js'
export * from './useTransactions.js'
export * from './useUpdateThreshold.js'
export * from './useWaitForTransaction.js'
export * from './useSafeOperation.js'
export * from './usePendingSafeOperations.js'
export * from './useSendSafeOperation.js'
export * from './useConfirmSafeOperation.js'
export * from './useSafeOperations.js'
65 changes: 65 additions & 0 deletions src/hooks/useConfirmSafeOperation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { UseMutateAsyncFunction, UseMutateFunction, UseMutationResult } from '@tanstack/react-query'
import { ConfirmSafeOperationProps, SafeClientResult } from '@safe-global/sdk-starter-kit'
import { ConfigParam, SafeConfigWithSigner } from '@/types/index.js'
import { useSignerClientMutation } from '@/hooks/useSignerClientMutation.js'
import { MutationKey, QueryKey } from '@/constants.js'
import { invalidateQueries } from '@/queryClient.js'

export type ConfirmSafeOperationVariables = ConfirmSafeOperationProps

export type UseConfirmSafeOperationParams = ConfigParam<SafeConfigWithSigner>
export type UseConfirmSafeOperationReturnType = Omit<
UseMutationResult<SafeClientResult, Error, ConfirmSafeOperationVariables>,
'mutate' | 'mutateAsync'
> & {
confirmSafeOperation: UseMutateFunction<
SafeClientResult,
Error,
ConfirmSafeOperationVariables,
unknown
>
confirmSafeOperationAsync: UseMutateAsyncFunction<
SafeClientResult,
Error,
ConfirmSafeOperationVariables,
unknown
>
}

/**
* Hook to confirm pending Safe Operations.
* @param params Parameters to customize the hook behavior.
* @param params.config SafeConfig to use instead of the one provided by `SafeProvider`.
* @returns Object containing the mutation state and the confirmSafeOperation function.
*/
export function useConfirmSafeOperation(
params: UseConfirmSafeOperationParams = {}
): UseConfirmSafeOperationReturnType {
const { mutate, mutateAsync, ...result } = useSignerClientMutation<
SafeClientResult,
ConfirmSafeOperationVariables
>({
...params,
mutationKey: [MutationKey.ConfirmSafeOperation],
mutationSafeClientFn: async (signerClient, { safeOperationHash }) => {
if (!signerClient.confirmSafeOperation)
throw new Error(
'To use Safe Operations, you need to specify the safeOperationOptions in the SafeProvider configuration.'
)

const result = await signerClient.confirmSafeOperation({
safeOperationHash
})

if (result.safeOperations?.userOperationHash) {
invalidateQueries([QueryKey.SafeOperations, QueryKey.SafeInfo])
} else if (result.safeOperations?.safeOperationHash) {
invalidateQueries([QueryKey.PendingSafeOperations])
}

return result
}
})

return { ...result, confirmSafeOperation: mutate, confirmSafeOperationAsync: mutateAsync }
}
37 changes: 37 additions & 0 deletions src/hooks/usePendingSafeOperations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { type UseQueryResult } from '@tanstack/react-query'
import { ListOptions } from '@safe-global/api-kit'
import { usePublicClientQuery } from '@/hooks/usePublicClientQuery.js'
import type { ConfigParam, SafeConfig } from '@/types/index.js'
import { QueryKey } from '@/constants.js'
import { ListResponse, SafeOperationResponse } from '@safe-global/types-kit'

export type UsePendingSafeOperationsParams = ConfigParam<SafeConfig> & ListOptions
export type UsePendingSafeOperationsReturnType = UseQueryResult<ListResponse<SafeOperationResponse>>

/**
* Hook to get all pending Safe Operations for the connected Safe.
* @param params Parameters to customize the hook behavior.
* @param params.config SafeConfig to use instead of the one provided by `SafeProvider`.
* @returns Query result object containing the list of pending Safe Operations.
*/
export function usePendingSafeOperations(
params: UsePendingSafeOperationsParams = {}
): UsePendingSafeOperationsReturnType {
return usePublicClientQuery({
...params,
querySafeClientFn: async (safeClient) => {
if (!safeClient.getPendingSafeOperations)
throw new Error(
'To use Safe Operations, you need to specify the safeOperationOptions in the SafeProvider configuration.'
)

const pendingSafeOperations = await safeClient.getPendingSafeOperations({
limit: params.limit,
offset: params.offset
})

return pendingSafeOperations
},
queryKey: [QueryKey.PendingSafeOperations]
})
}
3 changes: 1 addition & 2 deletions src/hooks/usePublicClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useContext, useEffect, useState } from 'react'
import { type SafeClient } from '@safe-global/sdk-starter-kit'
import type { ConfigParam, SafeConfig } from '@/types/index.js'
import type { ConfigParam, SafeClient, SafeConfig } from '@/types/index.js'
import { SafeContext } from '@/SafeContext.js'
import { useCompareObject } from '@/hooks/helpers/useCompare.js'
import { createPublicClient } from '@/createClient.js'
Expand Down
3 changes: 1 addition & 2 deletions src/hooks/usePublicClientQuery.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useCallback } from 'react'
import { useQuery, type UseQueryResult } from '@tanstack/react-query'
import { SafeClient } from '@safe-global/sdk-starter-kit'
import { useConfig } from '@/hooks/useConfig.js'
import { usePublicClient } from '@/hooks/usePublicClient.js'
import type { ConfigParam, SafeConfig } from '@/types/index.js'
import type { ConfigParam, SafeConfig, SafeClient } from '@/types/index.js'

export type UsePublicClientQueryParams<T> = ConfigParam<SafeConfig> & {
querySafeClientFn: (safeClient: SafeClient) => Promise<T> | T
Expand Down
13 changes: 11 additions & 2 deletions src/hooks/useSafe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
useSafeInfo,
useSignerAddress,
useTransaction,
useTransactions
useTransactions,
usePendingSafeOperations,
useSafeOperation,
useSafeOperations
} from '@/hooks/index.js'
import { MissingSafeProviderError } from '@/errors/MissingSafeProviderError.js'
import { SafeContext } from '@/SafeContext.js'
Expand All @@ -22,6 +25,9 @@ export type UseSafeReturnType = UseConnectSignerReturnType & {
getTransactions: typeof useTransactions
getSafeInfo: typeof useSafeInfo
getSignerAddress: typeof useSignerAddress
getPendingSafeOperations: typeof usePendingSafeOperations
getSafeOperation: typeof useSafeOperation
getSafeOperations: typeof useSafeOperations
}

/**
Expand Down Expand Up @@ -49,6 +55,9 @@ export function useSafe(): UseSafeReturnType {
getTransaction: useTransaction,
getTransactions: useTransactions,
getSafeInfo: useSafeInfo,
getSignerAddress: useSignerAddress
getSignerAddress: useSignerAddress,
getPendingSafeOperations: usePendingSafeOperations,
getSafeOperation: useSafeOperation,
getSafeOperations: useSafeOperations
}
}
38 changes: 38 additions & 0 deletions src/hooks/useSafeOperation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useCallback, useMemo } from 'react'
import { Hash } from 'viem'
import { useQuery, type UseQueryResult } from '@tanstack/react-query'
import { useConfig } from '@/hooks/useConfig.js'
import { SafeMultisigTransactionResponse } from '@safe-global/types-kit'
import { usePublicClient } from '@/hooks/usePublicClient.js'
import type { ConfigParam, SafeConfig } from '@/types/index.js'

export type UseSafeOperationParams = ConfigParam<SafeConfig> & { safeOperationHash: Hash }
export type UseSafeOperationReturnType = UseQueryResult<SafeMultisigTransactionResponse>

/**
* Hook to get the status of a specific Safe Operation.
* @param params Parameters to customize the hook behavior.
* @param params.config SafeConfig to use instead of the one provided by `SafeProvider`.
* @param params.safeOperationHash Hash of Safe Operation to be fetched.
* @returns Query result object containing the transaction object.
*/
export function useSafeOperation(params: UseSafeOperationParams): UseSafeOperationReturnType {
const [config] = useConfig({ config: params.config })

const safeClient = usePublicClient({ config })

const getSafeOperation = useCallback(async () => {
if (!safeClient) {
throw new Error('SafeClient not initialized')
}

return safeClient.apiKit.getSafeOperation(params.safeOperationHash)
}, [safeClient])

const queryKey = useMemo(
() => ['getSafeOperation', params.safeOperationHash],
[params.safeOperationHash]
)

return useQuery({ queryKey, queryFn: getSafeOperation })
}
43 changes: 43 additions & 0 deletions src/hooks/useSafeOperations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useCallback } from 'react'
import { useQuery, type UseQueryResult } from '@tanstack/react-query'
import { ListOptions } from '@safe-global/api-kit'
import { useConfig } from '@/hooks/useConfig.js'
import { usePublicClient } from '@/hooks/usePublicClient.js'
import type { ConfigParam, SafeConfig } from '@/types/index.js'
import { QueryKey } from '@/constants.js'
import { useAddress } from '@/hooks/useSafeInfo/useAddress.js'
import { ListResponse, SafeOperationResponse } from '@safe-global/types-kit'

export type UseSafeOperationsParams = ConfigParam<SafeConfig> & ListOptions & { ordering?: string }
export type UseSafeOperationsReturnType = UseQueryResult<ListResponse<SafeOperationResponse>>

/**s
* Hook to get all Safe Operations for the connected Safe.
* @param params Parameters to customize the hook behavior.
* @param params.config SafeConfig to use instead of the one provided by `SafeProvider`.
* @returns Query result object containing the list of Safe Operations.
*/
export function useSafeOperations(
params: UseSafeOperationsParams = {}
): UseSafeOperationsReturnType {
const [config] = useConfig({ config: params.config })
const { data: address } = useAddress({ config })
const safeClient = usePublicClient({ config })

const getSafeOperations = useCallback(async () => {
if (!safeClient || !address) {
throw new Error('SafeClient not initialized')
}

const response = await safeClient.apiKit.getSafeOperationsByAddress({
safeAddress: address,
limit: params.limit,
offset: params.offset,
ordering: params.ordering
})

return response
}, [safeClient, address])

return useQuery({ queryKey: [QueryKey.SafeOperations, config], queryFn: getSafeOperations })
}
Loading

0 comments on commit 60b5812

Please sign in to comment.