Skip to content

Commit

Permalink
Merge pull request #821 from ensdomains/fix/insufficient-funds-button
Browse files Browse the repository at this point in the history
fix: insufficient funds btn
  • Loading branch information
sugh01 authored Oct 1, 2024
2 parents 6c4619d + 4ad4305 commit 28b0792
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 82 deletions.
3 changes: 2 additions & 1 deletion public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,8 @@
"title": "Confirm Details",
"message": "Double check these details before confirming in your wallet.",
"waitingForWallet": "Waiting for Wallet",
"openWallet": "Open Wallet"
"openWallet": "Open Wallet",
"insufficientFunds": "Insufficient funds"
},
"sent": {
"title": "Transaction Sent",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,27 @@ function useCreateSubnameRedirect(
}, [shouldTrigger, subdomain])
}

const getLowerError = ({
stage,
transactionError,
requestError,
}: {
stage: TransactionStage
transactionError: Error | null
requestError: Error | null
}) => {
if (stage === 'complete' || stage === 'sent') return null
const err = transactionError || requestError
if (!err) return null
if (!(err instanceof BaseError))
return {
message: 'message' in err ? err.message : 'transaction.error.unknown',
type: 'unknown',
} as const
const readableError = getReadableError(err)
return readableError || ({ message: err.shortMessage, type: 'unknown' } as const)
}

export const TransactionStageModal = ({
actionName,
currentStep,
Expand Down Expand Up @@ -355,7 +376,13 @@ export const TransactionStageModal = ({
refetchOnMount: 'always',
})

const { data: request, isLoading: requestLoading, error: requestError } = transactionRequestQuery
const {
data: request_,
isLoading: requestLoading,
error: requestError_,
} = transactionRequestQuery
const request = request_?.data
const requestError = request_?.error || requestError_
const isTransactionRequestCachedData = getIsCachedData(transactionRequestQuery)

useInvalidateOnBlock({
Expand Down Expand Up @@ -387,6 +414,35 @@ export const TransactionStageModal = ({
displayItems.find((i) => i.label === 'subname' && i.type === 'name')?.value,
)

const stepStatus = useMemo(() => {
if (stage === 'complete') {
return 'completed'
}
return 'inProgress'
}, [stage])

const initialErrorOptions = useQueryOptions({
params: { hash: transaction.hash, status: transactionStatus },
functionName: 'getTransactionError',
queryDependencyType: 'standard',
queryFn: getTransactionErrorQueryFn,
})

const preparedErrorOptions = queryOptions({
queryKey: initialErrorOptions.queryKey,
queryFn: initialErrorOptions.queryFn,
})

const { data: upperError } = useQuery({
...preparedErrorOptions,
enabled: !!transaction && !!transaction.hash && transactionStatus === 'failed',
})

const lowerError = useMemo(
() => getLowerError({ stage, transactionError, requestError }),
[stage, transactionError, requestError],
)

const FilledDisplayItems = useMemo(
() => <DisplayItems displayItems={[...(displayItems || [])]} />,
[displayItems],
Expand Down Expand Up @@ -467,6 +523,10 @@ export const TransactionStageModal = ({
</Button>
)
}

if (lowerError?.type === 'insufficientFunds')
return <Button disabled>{t('transaction.dialog.confirm.insufficientFunds')}</Button>

return (
<Button
disabled={
Expand Down Expand Up @@ -496,58 +556,23 @@ export const TransactionStageModal = ({
transactionLoading,
request,
isTransactionRequestCachedData,
lowerError,
])

const stepStatus = useMemo(() => {
if (stage === 'complete') {
return 'completed'
}
return 'inProgress'
}, [stage])

const initialErrorOptions = useQueryOptions({
params: { hash: transaction.hash, status: transactionStatus },
functionName: 'getTransactionError',
queryDependencyType: 'standard',
queryFn: getTransactionErrorQueryFn,
})

const preparedErrorOptions = queryOptions({
queryKey: initialErrorOptions.queryKey,
queryFn: initialErrorOptions.queryFn,
})

const { data: upperError } = useQuery({
...preparedErrorOptions,
enabled: !!transaction && !!transaction.hash && transactionStatus === 'failed',
})

const lowerError = useMemo(() => {
if (stage === 'complete' || stage === 'sent') return null
const err = transactionError || requestError
if (!err) return null
if (!(err instanceof BaseError)) {
if ('message' in err) return err.message
return t('transaction.error.unknown')
}
const readableError = getReadableError(err)
return readableError || err.shortMessage
}, [t, stage, transactionError, requestError])

return (
<>
<Dialog.Heading title={t(`transaction.dialog.${stage}.title`)} />
<Dialog.Content data-testid="transaction-modal-inner">
{MiddleContent}
{upperError && <Helper type="error">{t(upperError)}</Helper>}
{upperError && <Helper type="error">{t(upperError.message)}</Helper>}
{FilledDisplayItems}
{HelperContent}
{transaction.hash && (
<Outlink href={makeEtherscanLink(transaction.hash!, chainName)}>
{t('transaction.viewEtherscan')}
</Outlink>
)}
{lowerError && <Helper type="error">{lowerError}</Helper>}
{lowerError && <Helper type="error">{lowerError.message}</Helper>}
</Dialog.Content>
<Dialog.Footer
currentStep={currentStep}
Expand Down
115 changes: 76 additions & 39 deletions src/components/@molecules/TransactionDialogManager/stage/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const transactionSuccessHandler =
connectorClient: ConnectorClientWithEns
actionName: ManagedDialogProps['actionName']
txKey: string | null
request: PrepareTransactionRequestRequest<SupportedChain> | undefined
request: PrepareTransactionRequestRequest<SupportedChain> | null | undefined
addRecentTransaction: ReturnType<typeof useAddRecentTransaction>
dispatch: Dispatch<TransactionFlowAction>
isSafeApp: ReturnType<typeof useIsSafeApp>['data']
Expand Down Expand Up @@ -167,6 +167,61 @@ type CreateTransactionRequestQueryKey = CreateQueryKey<
'standard'
>

type CreateTransactionRequestUnsafeParameters = {
client: ClientWithEns
connectorClient: ConnectorClientWithEns
isSafeApp: CheckIsSafeAppReturnType | undefined
params: UniqueTransaction
chainId: SupportedChain['id']
}

const createTransactionRequestUnsafe = async ({
client,
connectorClient,
isSafeApp,
params,
chainId,
}: CreateTransactionRequestUnsafeParameters) => {
const transactionRequest = await createTransactionRequest({
name: params.name,
data: params.data,
connectorClient,
client,
})

const txWithZeroGas = {
...transactionRequest,
maxFeePerGas: 0n,
maxPriorityFeePerGas: 0n,
}

const { gasLimit, accessList } = await calculateGasLimit({
client,
connectorClient,
isSafeApp: !!isSafeApp,
txWithZeroGas,
transactionName: params.name,
})

const request = await prepareTransactionRequest(client, {
to: transactionRequest.to,
accessList,
account: connectorClient.account,
data: transactionRequest.data,
gas: gasLimit,
parameters: ['fees', 'nonce', 'type'],
...('value' in transactionRequest ? { value: transactionRequest.value } : {}),
})

return {
...request,
chain: request.chain!,
to: request.to!,
gas: request.gas!,
chainId,
}
}

export const createTransactionRequestQueryFn =
(config: ConfigWithEns) =>
({
Expand All @@ -185,43 +240,22 @@ export const createTransactionRequestQueryFn =
if (connectorClient.account.address !== address)
throw new Error('address does not match connector')

const transactionRequest = await createTransactionRequest({
name: params.name,
data: params.data,
connectorClient,
client,
})

const txWithZeroGas = {
...transactionRequest,
maxFeePerGas: 0n,
maxPriorityFeePerGas: 0n,
}

const { gasLimit, accessList } = await calculateGasLimit({
client,
connectorClient,
isSafeApp: !!isSafeApp,
txWithZeroGas,
transactionName: params.name,
})

const request = await prepareTransactionRequest(client, {
to: transactionRequest.to,
accessList,
account: connectorClient.account,
data: transactionRequest.data,
gas: gasLimit,
parameters: ['fees', 'nonce', 'type'],
...('value' in transactionRequest ? { value: transactionRequest.value } : {}),
})

return {
...request,
chain: request.chain!,
to: request.to!,
gas: request.gas!,
chainId,
try {
return {
data: await createTransactionRequestUnsafe({
client,
connectorClient,
isSafeApp,
params,
chainId,
}),
error: null,
}
} catch (e) {
return {
data: null,
error: e as Error,
}
}
}

Expand All @@ -242,7 +276,10 @@ export const getTransactionErrorQueryFn =
try {
await call(client, failedTransactionData as CallParameters<ConfigWithEns>)
// TODO: better errors for this
return 'transaction.dialog.error.gasLimit'
return {
message: 'transaction.dialog.error.gasLimit',
type: 'unknown',
}
} catch (err: unknown) {
return getReadableError(err)
}
Expand Down
41 changes: 38 additions & 3 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { BaseError, decodeErrorResult, RawContractError } from 'viem'
import {
BaseError,
decodeErrorResult,
EstimateGasExecutionError,
formatEther,
RawContractError,
} from 'viem'

import { ethRegistrarControllerErrors, nameWrapperErrors } from '@ensdomains/ensjs/contracts'

type ReadableErrorType = 'insufficientFunds' | 'contract' | 'unknown'
type ReadableError = {
message: string
type: ReadableErrorType
}

export const getViemRevertErrorData = (err: unknown) => {
if (!(err instanceof BaseError)) return undefined
const error = err.walk() as RawContractError
Expand All @@ -10,13 +22,36 @@ export const getViemRevertErrorData = (err: unknown) => {

export const allContractErrors = [...ethRegistrarControllerErrors, ...nameWrapperErrors]

export const getReadableError = (err: unknown) => {
const insufficientFundsRegex =
/insufficient funds for gas \* price \+ value: address (?<address>0x[a-fA-F0-9]{40}) have (?<availableBalance>\d*) want (?<requiredBalance>\d*)/

const getEstimateGasExecutionErrorMessage = (err: EstimateGasExecutionError) => {
const originError = err.walk()
const data = insufficientFundsRegex.exec(originError.message)
if (data?.groups) {
const { requiredBalance } = data.groups
return {
message: `Wallet balance too low. Minimum required balance: ${formatEther(
BigInt(requiredBalance),
)} ETH`,
type: 'insufficientFunds',
} as const
}

return null
}

export const getReadableError = (err: unknown): ReadableError | null => {
if (err instanceof EstimateGasExecutionError) return getEstimateGasExecutionErrorMessage(err)
const data = getViemRevertErrorData(err)
if (!data) return null
const decodedError = decodeErrorResult({
abi: allContractErrors,
data,
})
if (!decodedError) return null
return decodedError.errorName
return {
message: decodedError.errorName,
type: 'contract',
} as const
}

0 comments on commit 28b0792

Please sign in to comment.