Skip to content

Commit

Permalink
display better error messages for insufficient funds
Browse files Browse the repository at this point in the history
  • Loading branch information
zoeyTM committed May 14, 2024
1 parent a90481b commit a8a3adc
Show file tree
Hide file tree
Showing 7 changed files with 530 additions and 62 deletions.
13 changes: 13 additions & 0 deletions packages/core/src/internal/errors-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,19 @@ export const ERRORS = {
number: 407,
message: "The calculated max fee per gas exceeds the configured limit.",
},
INSUFFICIENT_FUNDS_FOR_TRANSFER: {
number: 408,
message:
"Account %sender% has insufficient funds to transfer %amount% wei",
},
INSUFFICIENT_FUNDS_FOR_DEPLOY: {
number: 409,
message: "Account %sender% has insufficient funds to deploy the contract",
},
GAS_ESTIMATION_FAILED: {
number: 410,
message: "Gas estimation failed: %error%",
},
},
RECONCILIATION: {
INVALID_EXECUTION_STATUS: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import {
CallStrategyGenerator,
DeploymentStrategyGenerator,
ExecutionStrategy,
OnchainInteractionResponseType,
SIMULATION_SUCCESS_SIGNAL_TYPE,
} from "../../types/execution-strategy";
import {
CallExecutionStateCompleteMessage,
Expand All @@ -23,6 +21,7 @@ import {
TransactionSendMessage,
} from "../../types/messages";
import { NetworkInteractionType } from "../../types/network-interaction";
import { decodeSimulationResult } from "../helpers/decode-simulation-result";
import { createExecutionStateCompleteMessageForExecutionsWithOnchainInteractions } from "../helpers/messages-helpers";
import {
TRANSACTION_SENT_TYPE,
Expand Down Expand Up @@ -87,27 +86,8 @@ export async function sendTransaction(
jsonRpcClient,
exState.from,
lastNetworkInteraction,
async (_sender: string) => nonceManager.getNextNonce(_sender),
async (simulationResult) => {
const response = await strategyGenerator.next({
type: OnchainInteractionResponseType.SIMULATION_RESULT,
result: simulationResult,
});

assertIgnitionInvariant(
response.value.type === SIMULATION_SUCCESS_SIGNAL_TYPE ||
response.value.type ===
ExecutionResultType.STRATEGY_SIMULATION_ERROR ||
response.value.type === ExecutionResultType.SIMULATION_ERROR,
`Invalid response received from strategy after a simulation was run before sending a transaction for ExecutionState ${exState.id}`
);

if (response.value.type === SIMULATION_SUCCESS_SIGNAL_TYPE) {
return undefined;
}

return response.value;
}
nonceManager,
decodeSimulationResult(strategyGenerator, exState)
);

// If the transaction failed during simulation, we need to revert the nonce allocation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { assertIgnitionInvariant } from "../../../utils/assertions";
import {
ExecutionResultType,
SimulationErrorExecutionResult,
StrategySimulationErrorExecutionResult,
} from "../../types/execution-result";
import {
CallExecutionState,
DeploymentExecutionState,
SendDataExecutionState,
} from "../../types/execution-state";
import {
CallStrategyGenerator,
DeploymentStrategyGenerator,
OnchainInteractionResponseType,
SIMULATION_SUCCESS_SIGNAL_TYPE,
} from "../../types/execution-strategy";
import { RawStaticCallResult } from "../../types/jsonrpc";

export function decodeSimulationResult(
strategyGenerator: DeploymentStrategyGenerator | CallStrategyGenerator,
exState:
| DeploymentExecutionState
| CallExecutionState
| SendDataExecutionState
) {
return async (
simulationResult: RawStaticCallResult
): Promise<
| SimulationErrorExecutionResult
| StrategySimulationErrorExecutionResult
| undefined
> => {
const response = await strategyGenerator.next({
type: OnchainInteractionResponseType.SIMULATION_RESULT,
result: simulationResult,
});

assertIgnitionInvariant(
response.value.type === SIMULATION_SUCCESS_SIGNAL_TYPE ||
response.value.type === ExecutionResultType.STRATEGY_SIMULATION_ERROR ||
response.value.type === ExecutionResultType.SIMULATION_ERROR,
`Invalid response received from strategy after a simulation was run before sending a transaction for ExecutionState ${exState.id}`
);

if (response.value.type === SIMULATION_SUCCESS_SIGNAL_TYPE) {
return undefined;
}

return response.value;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
* @file
*/

import { IgnitionError } from "../../../../errors";
import { ERRORS } from "../../../errors-list";
import { assertIgnitionInvariant } from "../../../utils/assertions";
import { JsonRpcClient, TransactionParams } from "../../jsonrpc-client";
import { NonceManager } from "../../nonce-management/json-rpc-nonce-manager";
import {
SimulationErrorExecutionResult,
StrategySimulationErrorExecutionResult,
Expand Down Expand Up @@ -96,7 +99,7 @@ export async function sendTransactionForOnchainInteraction(
client: JsonRpcClient,
sender: string,
onchainInteraction: OnchainInteraction,
getNonce: (sender: string) => Promise<number>,
nonceManager: NonceManager,
decodeSimulationResult: (
simulationResult: RawStaticCallResult
) => Promise<
Expand All @@ -113,7 +116,8 @@ export async function sendTransactionForOnchainInteraction(
nonce: number;
}
> {
const nonce = onchainInteraction.nonce ?? (await getNonce(sender));
const nonce =
onchainInteraction.nonce ?? (await nonceManager.getNextNonce(sender));
const fees = await getNextTransactionFees(client, onchainInteraction);

// TODO: Should we check the balance here? Before or after estimating gas?
Expand All @@ -140,10 +144,6 @@ export async function sendTransactionForOnchainInteraction(

// If the gas estimation failed, we simulate the transaction to get information
// about why it failed.
//
// TODO: We are catching every error (e.g. network errors) here, which may be
// too broad and make the assertion below fail. We could try to catch only
// estimation errors.
const failedEstimateGasSimulationResult = await client.call(
paramsWithoutFees,
"pending"
Expand All @@ -153,12 +153,35 @@ export async function sendTransactionForOnchainInteraction(
failedEstimateGasSimulationResult
);

if (decoded !== undefined) {
return decoded;
}

// this is just for type inference
assertIgnitionInvariant(
decoded !== undefined,
"Expected failed simulation after having failed to estimate gas"
error instanceof Error,
"Unexpected error type while resolving failed gas estimation"
);

return decoded;
// If the user has tried to transfer funds (i.e. m.send(...)) and they have insufficient funds
if (/insufficient funds for transfer/.test(error.message)) {
throw new IgnitionError(
ERRORS.EXECUTION.INSUFFICIENT_FUNDS_FOR_TRANSFER,
{ sender, amount: estimateGasPrams.value.toString() }
);
}
// if the user has insufficient funds to deploy the contract they're trying to deploy
else if (/contract creation code storage out of gas/.test(error.message)) {
throw new IgnitionError(ERRORS.EXECUTION.INSUFFICIENT_FUNDS_FOR_DEPLOY, {
sender,
});
}
// catch-all error for all other errors
else {
throw new IgnitionError(ERRORS.EXECUTION.GAS_ESTIMATION_FAILED, {
error: error.message,
});
}
}

const transactionParams: TransactionParams = {
Expand Down
Loading

0 comments on commit a8a3adc

Please sign in to comment.