@0xsplits/splits-sdk-react
Version:
React wrapper for the 0xSplits SDK
1,051 lines (942 loc) • 27.9 kB
text/typescript
import { useContext, useEffect, useState } from 'react'
import { Address, getAddress, zeroAddress } from 'viem'
import { mainnet } from 'viem/chains'
import {
AccountNotFoundError,
FormattedContractEarnings,
FormattedSplitEarnings,
FormattedUserEarnings,
FormattedUserEarningsByContract,
LiquidSplit,
Split,
Swapper,
VestingModule,
WaterfallModule,
} from '@0xsplits/splits-sdk'
import {
getSplitMainAddress,
getSplitV1StartBlock,
getSplitV2FactoriesStartBlock,
getSplitV2FactoryAddress,
splitV1CreatedEvent,
SplitV1CreatedLogType,
splitV1UpdatedEvent,
SplitV1UpdatedLogType,
splitV2CreatedEvent,
SplitV2CreatedLogType,
splitV2UpdatedEvent,
SplitV2UpdatedLogType,
} from '@0xsplits/splits-sdk/constants'
import {
getLargestValidBlockRange,
getSplitCreateAndUpdateLogs,
LOGS_SEARCH_BATCH_SIZE,
searchLogs,
} from '@0xsplits/splits-sdk/utils'
import { SplitV2Type } from '@0xsplits/splits-sdk/types'
import {
DataLoadStatus,
RequestError,
SplitProviderSearchCacheData,
} from '../types'
import { getSplitsClient } from '../utils'
import { SplitsContext } from '../context'
import { V1MainnetNotSupportedError } from './errors'
export const useSplitMetadataViaProvider = (
chainId: number,
splitAddress: string,
options?: {
cacheData?: {
blockRange?: bigint
controller?: Address
blocks?: {
createBlock?: bigint
updateBlock?: bigint
latestScannedBlock: bigint
}
}
},
): {
isLoading: boolean
data?: Split
status?: DataLoadStatus
error?: RequestError
currentBlockRange?: {
from: bigint
to: bigint
}
cacheData?: SplitProviderSearchCacheData
} => {
const context = useContext(SplitsContext)
const splitsV1Client = getSplitsClient(context).splitV1
const splitsV2Client = getSplitsClient(context).splitV2
const [splitMetadata, setSplitMetadata] = useState<Split | undefined>()
const [isLoading, setIsLoading] = useState(!!splitAddress)
const [status, setStatus] = useState<DataLoadStatus | undefined>(
splitAddress ? 'loading' : undefined,
)
const [error, setError] = useState<RequestError>()
const [currentBlockRange, setCurrentBlockRange] = useState<
{ from: bigint; to: bigint } | undefined
>()
const [cacheData, setCacheData] = useState<
SplitProviderSearchCacheData | undefined
>()
const cachedBlockRange = options?.cacheData?.blockRange
const cachedCreateBlock = options?.cacheData?.blocks?.createBlock
const cachedUpdateBlock = options?.cacheData?.blocks?.updateBlock
const cachedLatestScannedBlock =
options?.cacheData?.blocks?.latestScannedBlock
const cachedController = options?.cacheData?.controller
useEffect(() => {
let isActive = true
const fetchMetadata = async () => {
try {
let split: Split
let createLog, updateLog
const formattedSplitAddress = getAddress(splitAddress)
const publicClient = splitsV2Client._getPublicClient(chainId)
const [splitV1Exists, splitV2Exists] = await Promise.all([
splitsV1Client._doesSplitExist({
splitAddress: formattedSplitAddress,
chainId,
}),
splitsV2Client._doesSplitExist({
splitAddress: formattedSplitAddress,
chainId,
}),
])
if (splitV1Exists && splitV2Exists)
throw new Error('Found v1 and v2 split')
if (!splitV1Exists && !splitV2Exists)
throw new AccountNotFoundError(
'split',
formattedSplitAddress,
chainId,
)
if (splitV1Exists && chainId === mainnet.id) {
throw new V1MainnetNotSupportedError()
}
const addresses = splitV1Exists
? [getSplitMainAddress(chainId)]
: [
formattedSplitAddress,
getSplitV2FactoryAddress(chainId, SplitV2Type.Pull),
getSplitV2FactoryAddress(chainId, SplitV2Type.Push),
]
const splitCreatedEvent = splitV1Exists
? splitV1CreatedEvent
: splitV2CreatedEvent
const splitUpdatedEvent = splitV1Exists
? splitV1UpdatedEvent
: splitV2UpdatedEvent
const version = splitV2Exists
? await splitsV2Client.getSplitVersion({
splitAddress: formattedSplitAddress,
chainId,
})
: undefined
let blockRange =
cachedBlockRange ??
(await getLargestValidBlockRange({ publicClient }))
const lastBlockNumber = await publicClient.getBlockNumber()
const shouldSearch =
!cachedCreateBlock ||
(cachedController && cachedController !== zeroAddress)
if (shouldSearch) {
let currentBlockNumber = lastBlockNumber
const splitV2StartBlock =
cachedLatestScannedBlock ?? getSplitV2FactoriesStartBlock(chainId)
const splitV1StartBlock =
cachedLatestScannedBlock ?? getSplitV1StartBlock(chainId)
// eslint-disable-next-line no-loops/no-loops
while (
currentBlockNumber > splitV1StartBlock &&
currentBlockNumber > splitV2StartBlock
) {
const rangeStart =
currentBlockNumber - blockRange * BigInt(LOGS_SEARCH_BATCH_SIZE)
const startBlock = rangeStart >= BigInt(0) ? rangeStart : BigInt(0)
setCurrentBlockRange({ from: startBlock, to: currentBlockNumber })
const {
blockRange: searchBlockRange,
createLog: searchCreateLog,
updateLog: searchUpdateLog,
} = await searchLogs({
formattedSplitAddress,
publicClient,
addresses,
splitCreatedEvent,
splitUpdatedEvent,
endBlock: currentBlockNumber,
startBlock,
defaultBlockRange: blockRange,
splitV2Version: version,
})
blockRange = searchBlockRange
createLog = searchCreateLog
updateLog = searchUpdateLog
if (createLog) break
if (updateLog && cachedCreateBlock) break
if (
(updateLog && version === 'splitV2o1') ||
(updateLog && version === 'splitV2o2')
)
break
currentBlockNumber = startBlock - BigInt(1)
}
}
if (!createLog) {
if (cachedCreateBlock) {
const { createLog: cachedCreateLog, updateLog: cachedUpdateLog } =
await getSplitCreateAndUpdateLogs({
splitAddress: formattedSplitAddress,
publicClient,
currentEndBlockNumber: cachedLatestScannedBlock,
startBlockNumber: cachedLatestScannedBlock!,
defaultBlockRange: blockRange,
cachedBlocks: {
createBlock: cachedCreateBlock,
updateBlock: cachedUpdateBlock,
latestScannedBlock: cachedLatestScannedBlock!,
},
splitCreatedEvent,
splitUpdatedEvent,
addresses,
})
createLog = cachedCreateLog
// Only use cached update log if we did not find a more recent one
updateLog = updateLog ? updateLog : cachedUpdateLog
}
if (!createLog && (splitV1Exists || version === 'splitV2'))
throw new AccountNotFoundError(
'split',
formattedSplitAddress,
chainId,
)
}
if (splitV1Exists) {
split = await splitsV1Client._getSplitFromLogs({
splitAddress: formattedSplitAddress,
chainId,
createLog: createLog as SplitV1CreatedLogType,
updateLog: updateLog as SplitV1UpdatedLogType,
})
} else {
split = await splitsV2Client._getSplitFromLogs({
splitAddress: formattedSplitAddress,
chainId,
createLog: createLog as SplitV2CreatedLogType,
updateLog: updateLog as SplitV2UpdatedLogType,
})
}
if (!isActive) return
setSplitMetadata(split)
setCacheData({
blockRange,
controller: split.controller?.address ?? zeroAddress,
blocks: {
createBlock: createLog?.blockNumber,
updateBlock: updateLog?.blockNumber,
latestScannedBlock: lastBlockNumber,
},
})
setStatus('success')
} catch (e) {
if (isActive) {
setStatus('error')
setError(e)
}
} finally {
if (isActive) setIsLoading(false)
}
}
setError(undefined)
if (splitAddress) {
setIsLoading(true)
setStatus('loading')
fetchMetadata()
} else {
setStatus(undefined)
setIsLoading(false)
setSplitMetadata(undefined)
}
return () => {
isActive = false
}
}, [splitsV1Client, splitsV2Client, chainId, splitAddress])
return {
isLoading,
data: splitMetadata,
currentBlockRange,
cacheData,
error,
status,
}
}
export const useSplitMetadata = (
chainId: number,
splitAddress: string,
options?: {
requireDataClient?: boolean
},
): {
isLoading: boolean
data: Split | undefined
status?: DataLoadStatus
error?: RequestError
} => {
const context = useContext(SplitsContext)
const dataClient = getSplitsClient(context).dataClient
const splitsV1Client = getSplitsClient(context).splitV1
const splitsV2Client = getSplitsClient(context).splitV2
const [splitMetadata, setSplitMetadata] = useState<Split | undefined>()
const [isLoading, setIsLoading] = useState(!!splitAddress)
const [status, setStatus] = useState<DataLoadStatus | undefined>(
splitAddress ? 'loading' : undefined,
)
const [error, setError] = useState<RequestError>()
const requireDataClient = options?.requireDataClient ?? true
useEffect(() => {
let isActive = true
const fetchMetadata = async () => {
if (requireDataClient && !dataClient)
throw new Error('Missing api key for data client')
try {
let split: Split
if (dataClient)
split = await dataClient.getSplitMetadata({
chainId,
splitAddress,
})
else {
const [splitV1Result, splitV2Result] = await Promise.allSettled([
splitsV1Client.getSplitMetadataViaProvider({
chainId,
splitAddress,
}),
splitsV2Client.getSplitMetadataViaProvider({
chainId,
splitAddress,
}),
])
if (splitV1Result.status === 'fulfilled')
split = splitV1Result.value.split
else if (splitV2Result.status === 'fulfilled')
split = splitV2Result.value.split
else throw new AccountNotFoundError('split', splitAddress, chainId)
}
if (!isActive) return
setSplitMetadata(split)
setStatus('success')
} catch (e) {
if (isActive) {
setStatus('error')
setError(e)
}
} finally {
if (isActive) setIsLoading(false)
}
}
setError(undefined)
if (splitAddress) {
setIsLoading(true)
setStatus('loading')
fetchMetadata()
} else {
setStatus(undefined)
setIsLoading(false)
setSplitMetadata(undefined)
}
return () => {
isActive = false
}
}, [
requireDataClient,
dataClient,
splitsV1Client,
splitsV2Client,
chainId,
splitAddress,
])
return {
isLoading,
data: splitMetadata,
error,
status,
}
}
export const useSplitEarnings = (
chainId: number,
splitAddress: string,
options?: {
includeActiveBalances?: boolean
erc20TokenList?: string[]
requireDataClient?: boolean
},
): {
isLoading: boolean
data: FormattedSplitEarnings | undefined
refetch: () => void
status?: DataLoadStatus
error?: RequestError
} => {
const context = useContext(SplitsContext)
const dataClient = getSplitsClient(context).dataClient
const splitsV1Client = getSplitsClient(context).splitV1
const splitsV2Client = getSplitsClient(context).splitV2
const requireDataClient = options?.requireDataClient ?? true
const erc20TokenList = options?.erc20TokenList
const includeActiveBalances = options?.includeActiveBalances ?? false
const [splitEarnings, setSplitEarnings] = useState<
FormattedSplitEarnings | undefined
>()
const [isLoading, setIsLoading] = useState(!!splitAddress)
const [status, setStatus] = useState<DataLoadStatus | undefined>(
splitAddress ? 'loading' : undefined,
)
const [error, setError] = useState<RequestError>()
const [manualTrigger, setManualTrigger] = useState(false)
const refetch = () => setManualTrigger((prev) => !prev)
const stringErc20List =
erc20TokenList !== undefined ? JSON.stringify(erc20TokenList) : undefined
useEffect(() => {
let isActive = true
const fetchEarnings = async () => {
if (requireDataClient && !dataClient)
throw new Error('Missing api key for data client')
setIsLoading(true)
setStatus('loading')
try {
let earnings: FormattedSplitEarnings
if (dataClient)
earnings = await dataClient.getSplitEarnings({
chainId,
splitAddress,
includeActiveBalances,
erc20TokenList:
stringErc20List !== undefined
? JSON.parse(stringErc20List)
: undefined,
})
else {
const [splitV1Result, splitV2Result] = await Promise.allSettled([
splitsV1Client.getSplitActiveBalances({
chainId,
splitAddress,
erc20TokenList:
stringErc20List !== undefined
? JSON.parse(stringErc20List)
: undefined,
}),
splitsV2Client.getSplitActiveBalances({
chainId,
splitAddress,
erc20TokenList:
stringErc20List !== undefined
? JSON.parse(stringErc20List)
: undefined,
}),
])
earnings = {
distributed: {},
activeBalances: {},
}
if (splitV1Result.status === 'fulfilled')
earnings.activeBalances = splitV1Result.value.activeBalances
else if (splitV2Result.status === 'fulfilled')
earnings.activeBalances = splitV2Result.value.activeBalances
else throw new AccountNotFoundError('split', splitAddress, chainId)
}
if (!isActive) return
setSplitEarnings(earnings)
setStatus('success')
} catch (e) {
if (isActive) {
setStatus('error')
setError(e)
}
} finally {
if (isActive) setIsLoading(false)
}
}
setError(undefined)
if (splitAddress) {
fetchEarnings()
} else {
setStatus(undefined)
setIsLoading(false)
setSplitEarnings(undefined)
}
return () => {
isActive = false
}
}, [
requireDataClient,
dataClient,
splitsV1Client,
splitsV2Client,
chainId,
splitAddress,
includeActiveBalances,
stringErc20List,
manualTrigger,
])
return {
isLoading,
data: splitEarnings,
refetch,
status,
error,
}
}
export const useContractEarnings = (
chainId: number,
contractAddress: string,
options?: {
includeActiveBalances?: boolean
erc20TokenList?: string[]
},
): {
isLoading: boolean
data: FormattedContractEarnings | undefined
status?: DataLoadStatus
error?: RequestError
} => {
const context = useContext(SplitsContext)
const splitsClient = getSplitsClient(context).dataClient
const [contractEarnings, setContractEarnings] = useState<
FormattedContractEarnings | undefined
>()
const [isLoading, setIsLoading] = useState(!!contractAddress)
const [status, setStatus] = useState<DataLoadStatus | undefined>(
contractAddress ? 'loading' : undefined,
)
const [error, setError] = useState<RequestError>()
const erc20TokenList = options?.erc20TokenList
const includeActiveBalances = options?.includeActiveBalances ?? false
const stringErc20List =
erc20TokenList !== undefined ? JSON.stringify(erc20TokenList) : undefined
useEffect(() => {
let isActive = true
const fetchEarnings = async () => {
if (!splitsClient) throw new Error('Missing api key for data client')
try {
const earnings = await splitsClient.getContractEarnings({
chainId,
contractAddress,
includeActiveBalances,
erc20TokenList:
stringErc20List !== undefined
? JSON.parse(stringErc20List)
: undefined,
})
if (!isActive) return
setContractEarnings(earnings)
setStatus('success')
} catch (e) {
if (isActive) {
setStatus('error')
setError(e)
}
} finally {
if (isActive) setIsLoading(false)
}
}
setError(undefined)
if (contractAddress) {
setIsLoading(true)
setStatus('loading')
fetchEarnings()
} else {
setStatus(undefined)
setIsLoading(false)
setContractEarnings(undefined)
}
return () => {
isActive = false
}
}, [
splitsClient,
chainId,
contractAddress,
includeActiveBalances,
stringErc20List,
])
return {
isLoading,
data: contractEarnings,
status,
error,
}
}
export const useLiquidSplitMetadata = (
chainId: number,
liquidSplitAddress: string,
): {
isLoading: boolean
data: LiquidSplit | undefined
status?: DataLoadStatus
error?: RequestError
} => {
const context = useContext(SplitsContext)
const splitsClient = getSplitsClient(context).dataClient
const [liquidSplitMetadata, setLiquidSplitMetadata] = useState<
LiquidSplit | undefined
>()
const [isLoading, setIsLoading] = useState(!!liquidSplitAddress)
const [status, setStatus] = useState<DataLoadStatus | undefined>(
liquidSplitAddress ? 'loading' : undefined,
)
const [error, setError] = useState<RequestError>()
useEffect(() => {
let isActive = true
const fetchMetadata = async () => {
if (!splitsClient) throw new Error('Missing api key for data client')
try {
const liquidSplit = await splitsClient.getLiquidSplitMetadata({
chainId,
liquidSplitAddress,
})
if (!isActive) return
setLiquidSplitMetadata(liquidSplit)
setStatus('success')
} catch (e) {
if (isActive) {
setStatus('error')
setError(e)
}
} finally {
if (isActive) setIsLoading(false)
}
}
setError(undefined)
if (liquidSplitAddress) {
setStatus('loading')
setIsLoading(true)
fetchMetadata()
} else {
setStatus(undefined)
setIsLoading(false)
setLiquidSplitMetadata(undefined)
}
return () => {
isActive = false
}
}, [splitsClient, chainId, liquidSplitAddress])
return {
isLoading,
data: liquidSplitMetadata,
status,
error,
}
}
export const useSwapperMetadata = (
chainId: number,
swapperAddress: string,
): {
isLoading: boolean
data: Swapper | undefined
status?: DataLoadStatus
error?: RequestError
} => {
const context = useContext(SplitsContext)
const splitsClient = getSplitsClient(context).dataClient
const [swapperMetadata, setSwapperMetadata] = useState<Swapper | undefined>()
const [isLoading, setIsLoading] = useState(!!swapperAddress)
const [status, setStatus] = useState<DataLoadStatus | undefined>(
swapperAddress ? 'loading' : undefined,
)
const [error, setError] = useState<RequestError>()
useEffect(() => {
let isActive = true
const fetchMetadata = async () => {
if (!splitsClient) throw new Error('Missing api key for data client')
try {
const swapper = await splitsClient.getSwapperMetadata({
chainId,
swapperAddress,
})
if (!isActive) return
setSwapperMetadata(swapper)
setStatus('success')
} catch (e) {
if (isActive) {
setStatus('error')
setError(e)
}
} finally {
if (isActive) setIsLoading(false)
}
}
setError(undefined)
if (swapperAddress) {
setIsLoading(true)
setStatus('loading')
fetchMetadata()
} else {
setStatus(undefined)
setIsLoading(false)
setSwapperMetadata(undefined)
}
return () => {
isActive = false
}
}, [splitsClient, chainId, swapperAddress])
return {
isLoading,
data: swapperMetadata,
status,
error,
}
}
export const useUserEarnings = (
chainId: number,
userAddress: string,
): {
isLoading: boolean
data?: FormattedUserEarnings
status?: DataLoadStatus
error?: RequestError
} => {
const context = useContext(SplitsContext)
const splitsClient = getSplitsClient(context).dataClient
const [userEarnings, setUserEarnings] = useState<
FormattedUserEarnings | undefined
>()
const [isLoading, setIsLoading] = useState(!!userAddress)
const [status, setStatus] = useState<DataLoadStatus | undefined>(
userAddress ? 'loading' : undefined,
)
const [error, setError] = useState<RequestError>()
useEffect(() => {
let isActive = true
const fetchEarnings = async () => {
if (!splitsClient) throw new Error('Missing api key for data client')
try {
const earnings = await splitsClient.getUserEarnings({
chainId,
userAddress,
})
if (!isActive) return
setUserEarnings(earnings)
setStatus('success')
} catch (e) {
if (isActive) {
setStatus('error')
setError(e)
}
} finally {
if (isActive) setIsLoading(false)
}
}
setError(undefined)
if (userAddress) {
setIsLoading(true)
setStatus('loading')
fetchEarnings()
} else {
setStatus(undefined)
setIsLoading(false)
setUserEarnings(undefined)
}
return () => {
isActive = false
}
}, [splitsClient, chainId, userAddress])
return {
isLoading,
data: userEarnings,
status,
error,
}
}
export const useUserEarningsByContract = (
chainId: number,
userAddress: string,
options?: {
contractAddresses?: string[]
},
): {
isLoading: boolean
data?: FormattedUserEarningsByContract
status?: DataLoadStatus
error?: RequestError
} => {
const context = useContext(SplitsContext)
const splitsClient = getSplitsClient(context).dataClient
const contractAddresses = options?.contractAddresses
const contractAddressesString = JSON.stringify(contractAddresses)
const [userEarningsByContract, setUserEarningsByContract] = useState<
FormattedUserEarningsByContract | undefined
>()
const [isLoading, setIsLoading] = useState(!!userAddress)
const [status, setStatus] = useState<DataLoadStatus | undefined>(
userAddress ? 'loading' : undefined,
)
const [error, setError] = useState<RequestError>()
useEffect(() => {
let isActive = true
const fetchEarnings = async () => {
if (!splitsClient) throw new Error('Missing api key for data client')
try {
const earnings = await splitsClient.getUserEarningsByContract({
chainId,
userAddress,
contractAddresses,
})
if (!isActive) return
setUserEarningsByContract(earnings)
setStatus('success')
} catch (e) {
if (isActive) {
setStatus('error')
setError(e)
}
} finally {
if (isActive) setIsLoading(false)
}
}
setError(undefined)
if (userAddress) {
setIsLoading(true)
setStatus('loading')
fetchEarnings()
} else {
setStatus(undefined)
setIsLoading(false)
setUserEarningsByContract(undefined)
}
return () => {
isActive = false
}
}, [splitsClient, chainId, userAddress, contractAddressesString])
return {
isLoading,
data: userEarningsByContract,
status,
error,
}
}
export const useVestingMetadata = (
chainId: number,
vestingModuleAddress: string,
): {
isLoading: boolean
data: VestingModule | undefined
status?: DataLoadStatus
error?: RequestError
} => {
const context = useContext(SplitsContext)
const splitsClient = getSplitsClient(context).dataClient
const [vestingMetadata, setVestingMetadata] = useState<
VestingModule | undefined
>()
const [isLoading, setIsLoading] = useState(!!vestingModuleAddress)
const [status, setStatus] = useState<DataLoadStatus | undefined>(
vestingModuleAddress ? 'loading' : undefined,
)
const [error, setError] = useState<RequestError>()
useEffect(() => {
let isActive = true
const fetchMetadata = async () => {
if (!splitsClient) throw new Error('Missing api key for data client')
try {
const vesting = await splitsClient.getVestingMetadata({
chainId,
vestingModuleAddress,
})
if (!isActive) return
setVestingMetadata(vesting)
setStatus('success')
} catch (e) {
if (isActive) {
setStatus('error')
setError(e)
}
} finally {
if (isActive) setIsLoading(false)
}
}
setError(undefined)
if (vestingModuleAddress) {
setIsLoading(true)
setStatus('loading')
fetchMetadata()
} else {
setStatus(undefined)
setIsLoading(false)
setVestingMetadata(undefined)
}
return () => {
isActive = false
}
}, [splitsClient, chainId, vestingModuleAddress])
return {
isLoading,
data: vestingMetadata,
status,
error,
}
}
export const useWaterfallMetadata = (
chainId: number,
waterfallModuleAddress: string,
): {
isLoading: boolean
data: WaterfallModule | undefined
status?: DataLoadStatus
error?: RequestError
} => {
const context = useContext(SplitsContext)
const splitsClient = getSplitsClient(context).dataClient
const [waterfallMetadata, setWaterfallMetadata] = useState<
WaterfallModule | undefined
>()
const [isLoading, setIsLoading] = useState(!!waterfallModuleAddress)
const [status, setStatus] = useState<DataLoadStatus | undefined>(
waterfallModuleAddress ? 'loading' : undefined,
)
const [error, setError] = useState<RequestError>()
useEffect(() => {
let isActive = true
const fetchMetadata = async () => {
if (!splitsClient) throw new Error('Missing api key for data client')
try {
const waterfall = await splitsClient.getWaterfallMetadata({
chainId,
waterfallModuleAddress,
})
if (!isActive) return
setWaterfallMetadata(waterfall)
setStatus('success')
} catch (e) {
if (isActive) {
setStatus('error')
setError(e)
}
} finally {
if (isActive) setIsLoading(false)
}
}
setError(undefined)
if (waterfallModuleAddress) {
setStatus('loading')
setIsLoading(true)
fetchMetadata()
} else {
setStatus(undefined)
setIsLoading(false)
setWaterfallMetadata(undefined)
}
return () => {
isActive = false
}
}, [splitsClient, chainId, waterfallModuleAddress])
return {
isLoading,
data: waterfallMetadata,
status,
error,
}
}