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 4337 hooks #24

Merged
merged 13 commits into from
Nov 11, 2024
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
Loading