- Consolidated duplicate UndoManagers to single instance - Fixed connection promise to only resolve on 'connected' status - Fixed WebSocketProvider import (WebsocketProvider) - Added proper doc.destroy() cleanup - Renamed isPresenceInitialized property to avoid conflict Co-Authored-By: Paperclip <noreply@paperclip.ing>
420 lines
14 KiB
TypeScript
420 lines
14 KiB
TypeScript
import type { Address } from 'abitype'
|
|
import {
|
|
type ReadContractErrorType,
|
|
readContract,
|
|
} from '../../actions/public/readContract.js'
|
|
import type { Client } from '../../clients/createClient.js'
|
|
import type { Transport } from '../../clients/transports/createTransport.js'
|
|
import { ContractFunctionRevertedError } from '../../errors/contract.js'
|
|
import type { ErrorType } from '../../errors/utils.js'
|
|
import type { Account } from '../../types/account.js'
|
|
import type {
|
|
Chain,
|
|
DeriveChain,
|
|
GetChainParameter,
|
|
} from '../../types/chain.js'
|
|
import type { Hash } from '../../types/misc.js'
|
|
import type { TransactionReceipt } from '../../types/transaction.js'
|
|
import type { OneOf } from '../../types/utils.js'
|
|
import { anchorStateRegistryAbi, portal2Abi, portalAbi } from '../abis.js'
|
|
import {
|
|
ReceiptContainsNoWithdrawalsError,
|
|
type ReceiptContainsNoWithdrawalsErrorType,
|
|
} from '../errors/withdrawal.js'
|
|
import type { TargetChain } from '../types/chain.js'
|
|
import type { GetContractAddressParameter } from '../types/contract.js'
|
|
import {
|
|
type GetWithdrawalsErrorType,
|
|
getWithdrawals,
|
|
} from '../utils/getWithdrawals.js'
|
|
import {
|
|
type GetGameErrorType,
|
|
type GetGameParameters,
|
|
getGame,
|
|
} from './getGame.js'
|
|
import {
|
|
type GetL2OutputErrorType,
|
|
type GetL2OutputParameters,
|
|
getL2Output,
|
|
} from './getL2Output.js'
|
|
import {
|
|
type GetPortalVersionParameters,
|
|
getPortalVersion,
|
|
} from './getPortalVersion.js'
|
|
import {
|
|
type GetTimeToFinalizeErrorType,
|
|
type GetTimeToFinalizeParameters,
|
|
getTimeToFinalize,
|
|
} from './getTimeToFinalize.js'
|
|
|
|
export type GetWithdrawalStatusParameters<
|
|
chain extends Chain | undefined = Chain | undefined,
|
|
chainOverride extends Chain | undefined = Chain | undefined,
|
|
_derivedChain extends Chain | undefined = DeriveChain<chain, chainOverride>,
|
|
> = GetChainParameter<chain, chainOverride> &
|
|
OneOf<
|
|
| GetContractAddressParameter<_derivedChain, 'l2OutputOracle' | 'portal'>
|
|
| GetContractAddressParameter<
|
|
_derivedChain,
|
|
'disputeGameFactory' | 'portal'
|
|
>
|
|
> & {
|
|
/**
|
|
* Limit of games to extract to check withdrawal status.
|
|
* @default 100
|
|
*/
|
|
gameLimit?: number
|
|
} & OneOf<
|
|
| {
|
|
/**
|
|
* The relative index of the withdrawal in the transaction receipt logs.
|
|
* @default 0
|
|
*/
|
|
logIndex?: number
|
|
/**
|
|
* The transaction receipt of the withdrawal.
|
|
*/
|
|
receipt: TransactionReceipt
|
|
}
|
|
| {
|
|
/**
|
|
* The L2 block number of the withdrawal.
|
|
*/
|
|
l2BlockNumber: bigint
|
|
/**
|
|
* The sender of the withdrawal.
|
|
*/
|
|
sender: Address
|
|
/**
|
|
* The hash of the withdrawal.
|
|
*/
|
|
withdrawalHash: Hash
|
|
}
|
|
>
|
|
export type GetWithdrawalStatusReturnType =
|
|
| 'waiting-to-prove'
|
|
| 'ready-to-prove'
|
|
| 'waiting-to-finalize'
|
|
| 'ready-to-finalize'
|
|
| 'finalized'
|
|
|
|
export type GetWithdrawalStatusErrorType =
|
|
| GetL2OutputErrorType
|
|
| GetTimeToFinalizeErrorType
|
|
| GetWithdrawalsErrorType
|
|
| ReadContractErrorType
|
|
| ReceiptContainsNoWithdrawalsErrorType
|
|
| ErrorType
|
|
|
|
/**
|
|
* Returns the current status of a withdrawal. Used for the [Withdrawal](/op-stack/guides/withdrawals) flow.
|
|
*
|
|
* - Docs: https://viem.sh/op-stack/actions/getWithdrawalStatus
|
|
*
|
|
* @param client - Client to use
|
|
* @param parameters - {@link GetWithdrawalStatusParameters}
|
|
* @returns Status of the withdrawal. {@link GetWithdrawalStatusReturnType}
|
|
*
|
|
* @example
|
|
* import { createPublicClient, http } from 'viem'
|
|
* import { getBlockNumber } from 'viem/actions'
|
|
* import { mainnet, optimism } from 'viem/chains'
|
|
* import { getWithdrawalStatus } from 'viem/op-stack'
|
|
*
|
|
* const publicClientL1 = createPublicClient({
|
|
* chain: mainnet,
|
|
* transport: http(),
|
|
* })
|
|
* const publicClientL2 = createPublicClient({
|
|
* chain: optimism,
|
|
* transport: http(),
|
|
* })
|
|
*
|
|
* const receipt = await publicClientL2.getTransactionReceipt({ hash: '0x...' })
|
|
* const status = await getWithdrawalStatus(publicClientL1, {
|
|
* receipt,
|
|
* targetChain: optimism
|
|
* })
|
|
*/
|
|
export async function getWithdrawalStatus<
|
|
chain extends Chain | undefined,
|
|
account extends Account | undefined,
|
|
chainOverride extends Chain | undefined = undefined,
|
|
>(
|
|
client: Client<Transport, chain, account>,
|
|
parameters: GetWithdrawalStatusParameters<chain, chainOverride>,
|
|
): Promise<GetWithdrawalStatusReturnType> {
|
|
const {
|
|
chain = client.chain,
|
|
gameLimit = 100,
|
|
receipt,
|
|
targetChain: targetChain_,
|
|
logIndex = 0,
|
|
} = parameters
|
|
|
|
const targetChain = targetChain_ as unknown as TargetChain
|
|
|
|
const portalAddress = (() => {
|
|
if (parameters.portalAddress) return parameters.portalAddress
|
|
if (chain) return targetChain.contracts.portal[chain.id].address
|
|
return Object.values(targetChain.contracts.portal)[0].address
|
|
})()
|
|
|
|
const l2BlockNumber = receipt?.blockNumber ?? parameters.l2BlockNumber
|
|
|
|
const withdrawal = (() => {
|
|
if (receipt) {
|
|
const withdrawal = getWithdrawals({ logs: receipt.logs })[logIndex]
|
|
if (!withdrawal)
|
|
throw new ReceiptContainsNoWithdrawalsError({
|
|
hash: receipt.transactionHash,
|
|
})
|
|
return withdrawal
|
|
}
|
|
return {
|
|
sender: parameters.sender,
|
|
withdrawalHash: parameters.withdrawalHash,
|
|
}
|
|
})()
|
|
|
|
const portalVersion = await getPortalVersion(
|
|
client,
|
|
parameters as GetPortalVersionParameters,
|
|
)
|
|
|
|
// Legacy (Portal < v3)
|
|
if (portalVersion.major < 3) {
|
|
const [outputResult, proveResult, finalizedResult, timeToFinalizeResult] =
|
|
await Promise.allSettled([
|
|
getL2Output(client, {
|
|
...parameters,
|
|
l2BlockNumber,
|
|
} as GetL2OutputParameters),
|
|
readContract(client, {
|
|
abi: portalAbi,
|
|
address: portalAddress,
|
|
functionName: 'provenWithdrawals',
|
|
args: [withdrawal.withdrawalHash],
|
|
}),
|
|
readContract(client, {
|
|
abi: portalAbi,
|
|
address: portalAddress,
|
|
functionName: 'finalizedWithdrawals',
|
|
args: [withdrawal.withdrawalHash],
|
|
}),
|
|
getTimeToFinalize(client, {
|
|
...parameters,
|
|
withdrawalHash: withdrawal.withdrawalHash,
|
|
} as GetTimeToFinalizeParameters),
|
|
])
|
|
|
|
// If the L2 Output is not processed yet (ie. the actions throws), this means
|
|
// that the withdrawal is not ready to prove.
|
|
if (outputResult.status === 'rejected') {
|
|
const error = outputResult.reason as GetL2OutputErrorType
|
|
if (
|
|
error.cause instanceof ContractFunctionRevertedError &&
|
|
error.cause.data?.args?.[0] ===
|
|
'L2OutputOracle: cannot get output for a block that has not been proposed'
|
|
)
|
|
return 'waiting-to-prove'
|
|
throw error
|
|
}
|
|
if (proveResult.status === 'rejected') throw proveResult.reason
|
|
if (finalizedResult.status === 'rejected') throw finalizedResult.reason
|
|
if (timeToFinalizeResult.status === 'rejected')
|
|
throw timeToFinalizeResult.reason
|
|
|
|
const [_, proveTimestamp] = proveResult.value
|
|
if (!proveTimestamp) return 'ready-to-prove'
|
|
|
|
const finalized = finalizedResult.value
|
|
if (finalized) return 'finalized'
|
|
|
|
const { seconds } = timeToFinalizeResult.value
|
|
return seconds > 0 ? 'waiting-to-finalize' : 'ready-to-finalize'
|
|
}
|
|
|
|
const numProofSubmitters = await readContract(client, {
|
|
abi: portal2Abi,
|
|
address: portalAddress,
|
|
functionName: 'numProofSubmitters',
|
|
args: [withdrawal.withdrawalHash],
|
|
}).catch(() => 1n)
|
|
|
|
const proofSubmitter = await readContract(client, {
|
|
abi: portal2Abi,
|
|
address: portalAddress,
|
|
functionName: 'proofSubmitters',
|
|
args: [withdrawal.withdrawalHash, numProofSubmitters - 1n],
|
|
}).catch(() => withdrawal.sender)
|
|
|
|
const [
|
|
disputeGameResult,
|
|
provenWithdrawalsResult,
|
|
checkWithdrawalResult,
|
|
finalizedResult,
|
|
] = await Promise.allSettled([
|
|
getGame(client, {
|
|
...parameters,
|
|
l2BlockNumber,
|
|
limit: gameLimit,
|
|
} as GetGameParameters),
|
|
readContract(client, {
|
|
abi: portal2Abi,
|
|
address: portalAddress,
|
|
functionName: 'provenWithdrawals',
|
|
args: [withdrawal.withdrawalHash, proofSubmitter],
|
|
}),
|
|
readContract(client, {
|
|
abi: portal2Abi,
|
|
address: portalAddress,
|
|
functionName: 'checkWithdrawal',
|
|
args: [withdrawal.withdrawalHash, proofSubmitter],
|
|
}),
|
|
readContract(client, {
|
|
abi: portal2Abi,
|
|
address: portalAddress,
|
|
functionName: 'finalizedWithdrawals',
|
|
args: [withdrawal.withdrawalHash],
|
|
}),
|
|
])
|
|
|
|
if (finalizedResult.status === 'fulfilled' && finalizedResult.value)
|
|
return 'finalized'
|
|
|
|
if (provenWithdrawalsResult.status === 'rejected')
|
|
throw provenWithdrawalsResult.reason
|
|
|
|
if (disputeGameResult.status === 'rejected') {
|
|
const error = disputeGameResult.reason as GetGameErrorType
|
|
if (error.name === 'GameNotFoundError') return 'waiting-to-prove'
|
|
throw disputeGameResult.reason
|
|
}
|
|
|
|
if (checkWithdrawalResult.status === 'rejected') {
|
|
const error = checkWithdrawalResult.reason as ReadContractErrorType
|
|
if (error.cause instanceof ContractFunctionRevertedError) {
|
|
// All potential error causes listed here, can either be the error string or the error name
|
|
// if custom error types are returned.
|
|
const errorCauses = {
|
|
'ready-to-prove': [
|
|
'OptimismPortal: invalid game type',
|
|
'OptimismPortal: withdrawal has not been proven yet',
|
|
'OptimismPortal: withdrawal has not been proven by proof submitter address yet',
|
|
'OptimismPortal: dispute game created before respected game type was updated',
|
|
'InvalidGameType',
|
|
'LegacyGame',
|
|
'Unproven',
|
|
// After U16
|
|
'OptimismPortal_Unproven',
|
|
'OptimismPortal_InvalidProofTimestamp',
|
|
],
|
|
'waiting-to-finalize': [
|
|
'OptimismPortal: proven withdrawal has not matured yet',
|
|
'OptimismPortal: output proposal has not been finalized yet',
|
|
'OptimismPortal: output proposal in air-gap',
|
|
],
|
|
}
|
|
|
|
// Pick out the error message and/or error name
|
|
// Return the status based on the error
|
|
const errors = [
|
|
error.cause.data?.errorName,
|
|
error.cause.data?.args?.[0] as string,
|
|
]
|
|
|
|
// After U16 we get a generic error message (OptimismPortal_InvalidRootClaim) because the
|
|
// OptimismPortal will call AnchorStateRegistry.isGameClaimValid which simply returns
|
|
// true/false. If we get this generic error, we need to figure out why the function returned
|
|
// false and return a proper status accordingly. We can also check these conditions when we
|
|
// get ProofNotOldEnough so users can be notified when their pending proof becomes invalid
|
|
// before it can be finalized.
|
|
if (
|
|
errors.includes('OptimismPortal_InvalidRootClaim') ||
|
|
errors.includes('OptimismPortal_ProofNotOldEnough')
|
|
) {
|
|
// Get the dispute game address from the proven withdrawal.
|
|
const disputeGameAddress = provenWithdrawalsResult.value[0] as Address
|
|
|
|
// Get the AnchorStateRegistry address from the portal.
|
|
const anchorStateRegistry = await readContract(client, {
|
|
abi: portal2Abi,
|
|
address: portalAddress,
|
|
functionName: 'anchorStateRegistry',
|
|
})
|
|
|
|
// Check if the game is proper, respected, and finalized.
|
|
const [
|
|
isGameProperResult,
|
|
isGameRespectedResult,
|
|
isGameFinalizedResult,
|
|
] = await Promise.allSettled([
|
|
readContract(client, {
|
|
abi: anchorStateRegistryAbi,
|
|
address: anchorStateRegistry,
|
|
functionName: 'isGameProper',
|
|
args: [disputeGameAddress],
|
|
}),
|
|
readContract(client, {
|
|
abi: anchorStateRegistryAbi,
|
|
address: anchorStateRegistry,
|
|
functionName: 'isGameRespected',
|
|
args: [disputeGameAddress],
|
|
}),
|
|
readContract(client, {
|
|
abi: anchorStateRegistryAbi,
|
|
address: anchorStateRegistry,
|
|
functionName: 'isGameFinalized',
|
|
args: [disputeGameAddress],
|
|
}),
|
|
])
|
|
|
|
// If any of the calls failed, throw the error.
|
|
if (isGameProperResult.status === 'rejected')
|
|
throw isGameProperResult.reason
|
|
if (isGameRespectedResult.status === 'rejected')
|
|
throw isGameRespectedResult.reason
|
|
if (isGameFinalizedResult.status === 'rejected')
|
|
throw isGameFinalizedResult.reason
|
|
|
|
// If the game isn't proper, the user needs to re-prove.
|
|
if (!isGameProperResult.value) {
|
|
return 'ready-to-prove'
|
|
}
|
|
|
|
// If the game isn't respected, the user needs to re-prove.
|
|
if (!isGameRespectedResult.value) {
|
|
return 'ready-to-prove'
|
|
}
|
|
|
|
// If the game isn't finalized, the user needs to wait to finalize.
|
|
if (!isGameFinalizedResult.value) {
|
|
return 'waiting-to-finalize'
|
|
}
|
|
|
|
// If the actual error was ProofNotOldEnough, then at this point the game is probably
|
|
// completely fine but the proof hasn't passed the waiting period. Otherwise, the only
|
|
// reason we'd be here is if the game resolved in favor of the challenger, which means the
|
|
// user needs to re-prove the withdrawal.
|
|
if (errors.includes('OptimismPortal_ProofNotOldEnough')) {
|
|
return 'waiting-to-finalize'
|
|
}
|
|
return 'ready-to-prove'
|
|
}
|
|
if (errorCauses['ready-to-prove'].some((cause) => errors.includes(cause)))
|
|
return 'ready-to-prove'
|
|
if (
|
|
errorCauses['waiting-to-finalize'].some((cause) =>
|
|
errors.includes(cause),
|
|
)
|
|
)
|
|
return 'waiting-to-finalize'
|
|
}
|
|
throw checkWithdrawalResult.reason
|
|
}
|
|
if (finalizedResult.status === 'rejected') throw finalizedResult.reason
|
|
|
|
return 'ready-to-finalize'
|
|
}
|