fix: improve postman logging and error parsing (#622)

* fix: improve postman logging and error parsing

* fix: remove unnecessary casting
This commit is contained in:
Victorien Gauch
2025-01-29 17:12:01 +01:00
committed by GitHub
parent c819e319cc
commit 317ef4f06e
30 changed files with 548 additions and 339 deletions

View File

@@ -15,6 +15,8 @@ import {
L2ClaimMessageTransactionSizeProcessorConfig, L2ClaimMessageTransactionSizeProcessorConfig,
} from "../../core/services/processors/IL2ClaimMessageTransactionSizeProcessor"; } from "../../core/services/processors/IL2ClaimMessageTransactionSizeProcessor";
import { IL2ClaimTransactionSizeCalculator } from "../../core/services/processors/IL2ClaimTransactionSizeCalculator"; import { IL2ClaimTransactionSizeCalculator } from "../../core/services/processors/IL2ClaimTransactionSizeCalculator";
import { ErrorParser } from "../../utils/ErrorParser";
import { Message } from "../../core/entities/Message";
export class L2ClaimMessageTransactionSizeProcessor implements IL2ClaimMessageTransactionSizeProcessor { export class L2ClaimMessageTransactionSizeProcessor implements IL2ClaimMessageTransactionSizeProcessor {
/** /**
@@ -48,6 +50,8 @@ export class L2ClaimMessageTransactionSizeProcessor implements IL2ClaimMessageTr
* @returns {Promise<void>} A promise that resolves when the processing is complete. * @returns {Promise<void>} A promise that resolves when the processing is complete.
*/ */
public async process(): Promise<void> { public async process(): Promise<void> {
let message: Message | null = null;
try { try {
const messages = await this.databaseService.getNFirstMessagesByStatus( const messages = await this.databaseService.getNFirstMessagesByStatus(
MessageStatus.ANCHORED, MessageStatus.ANCHORED,
@@ -57,10 +61,11 @@ export class L2ClaimMessageTransactionSizeProcessor implements IL2ClaimMessageTr
); );
if (messages.length === 0) { if (messages.length === 0) {
this.logger.info("No anchored messages found to compute transaction size.");
return; return;
} }
const message = messages[0]; message = messages[0];
const { gasLimit, maxPriorityFeePerGas, maxFeePerGas } = const { gasLimit, maxPriorityFeePerGas, maxFeePerGas } =
await this.l2MessageServiceClient.estimateClaimGasFees(message); await this.l2MessageServiceClient.estimateClaimGasFees(message);
@@ -86,7 +91,32 @@ export class L2ClaimMessageTransactionSizeProcessor implements IL2ClaimMessageTr
gasLimit, gasLimit,
); );
} catch (e) { } catch (e) {
this.logger.error(e); await this.handleProcessingError(e, message);
} }
} }
/**
* Handles error that occur during the processing.
*
* @param {unknown} e - The error that occurred.
* @param {Message | null} message - The message object being processed when the error occurred.
* @returns {Promise<void>} A promise that resolves when the error has been handled.
*/
private async handleProcessingError(e: unknown, message: Message | null): Promise<void> {
const parsedError = ErrorParser.parseErrorWithMitigation(e);
if (parsedError?.mitigation && !parsedError.mitigation.shouldRetry && message) {
message.edit({ status: MessageStatus.NON_EXECUTABLE });
await this.databaseService.updateMessage(message);
this.logger.warnOrError("Error occurred while processing message transaction size.", {
...parsedError,
messageHash: message.messageHash,
});
return;
}
this.logger.warnOrError("Error occurred while processing message transaction size.", {
parsedError,
});
}
} }

View File

@@ -18,6 +18,7 @@ import { MessageStatus } from "../../core/enums";
import { ILogger } from "../../core/utils/logging/ILogger"; import { ILogger } from "../../core/utils/logging/ILogger";
import { IMessageServiceContract } from "../../core/services/contracts/IMessageServiceContract"; import { IMessageServiceContract } from "../../core/services/contracts/IMessageServiceContract";
import { IMessageDBService } from "../../core/persistence/IMessageDBService"; import { IMessageDBService } from "../../core/persistence/IMessageDBService";
import { ErrorParser } from "../../utils/ErrorParser";
export class MessageAnchoringProcessor implements IMessageAnchoringProcessor { export class MessageAnchoringProcessor implements IMessageAnchoringProcessor {
private readonly maxFetchMessagesFromDb: number; private readonly maxFetchMessagesFromDb: number;
@@ -93,7 +94,12 @@ export class MessageAnchoringProcessor implements IMessageAnchoringProcessor {
await this.databaseService.saveMessages(messages); await this.databaseService.saveMessages(messages);
} catch (e) { } catch (e) {
this.logger.error(e); const error = ErrorParser.parseErrorWithMitigation(e);
this.logger.error("An error occurred while processing messages.", {
errorCode: error?.errorCode,
errorMessage: error?.errorMessage,
...(error?.data ? { data: error.data } : {}),
});
} }
} }
} }

View File

@@ -20,6 +20,7 @@ import {
MessageClaimingPersisterConfig, MessageClaimingPersisterConfig,
} from "../../core/services/processors/IMessageClaimingPersister"; } from "../../core/services/processors/IMessageClaimingPersister";
import { IMessageDBService } from "../../core/persistence/IMessageDBService"; import { IMessageDBService } from "../../core/persistence/IMessageDBService";
import { ErrorParser } from "../../utils/ErrorParser";
export class MessageClaimingPersister implements IMessageClaimingPersister { export class MessageClaimingPersister implements IMessageClaimingPersister {
private messageBeingRetry: { message: Message | null; retries: number }; private messageBeingRetry: { message: Message | null; retries: number };
@@ -79,6 +80,7 @@ export class MessageClaimingPersister implements IMessageClaimingPersister {
try { try {
firstPendingMessage = await this.databaseService.getFirstPendingMessage(this.config.direction); firstPendingMessage = await this.databaseService.getFirstPendingMessage(this.config.direction);
if (!firstPendingMessage?.claimTxHash) { if (!firstPendingMessage?.claimTxHash) {
this.logger.info("No pending message status to update.");
return; return;
} }
@@ -111,7 +113,13 @@ export class MessageClaimingPersister implements IMessageClaimingPersister {
await this.updateReceiptStatus(firstPendingMessage, receipt); await this.updateReceiptStatus(firstPendingMessage, receipt);
} catch (e) { } catch (e) {
this.logger.error(e); const error = ErrorParser.parseErrorWithMitigation(e);
this.logger.error("Error processing message.", {
...(firstPendingMessage ? { messageHash: firstPendingMessage.messageHash } : {}),
...(error?.errorCode ? { errorCode: error.errorCode } : {}),
...(error?.errorMessage ? { errorMessage: error.errorMessage } : {}),
...(error?.data ? { data: error.data } : {}),
});
} }
} }

View File

@@ -3,7 +3,6 @@ import {
Overrides, Overrides,
TransactionResponse, TransactionResponse,
ContractTransactionResponse, ContractTransactionResponse,
EthersError,
TransactionReceipt, TransactionReceipt,
Signer, Signer,
ErrorDescription, ErrorDescription,
@@ -74,6 +73,7 @@ export class MessageClaimingProcessor implements IMessageClaimingProcessor {
); );
if (!nextMessageToClaim) { if (!nextMessageToClaim) {
this.logger.info("No message to claim found");
return; return;
} }
@@ -265,7 +265,7 @@ export class MessageClaimingProcessor implements IMessageClaimingProcessor {
* @returns {Promise<void>} A promise that resolves when the error has been handled. * @returns {Promise<void>} A promise that resolves when the error has been handled.
*/ */
private async handleProcessingError(e: unknown, message: Message | null): Promise<void> { private async handleProcessingError(e: unknown, message: Message | null): Promise<void> {
const parsedError = ErrorParser.parseErrorWithMitigation(e as EthersError); const parsedError = ErrorParser.parseErrorWithMitigation(e);
if (parsedError?.mitigation && !parsedError.mitigation.shouldRetry && message) { if (parsedError?.mitigation && !parsedError.mitigation.shouldRetry && message) {
message.edit({ status: MessageStatus.NON_EXECUTABLE }); message.edit({ status: MessageStatus.NON_EXECUTABLE });
@@ -274,6 +274,7 @@ export class MessageClaimingProcessor implements IMessageClaimingProcessor {
this.logger.warnOrError(e, { this.logger.warnOrError(e, {
parsedError, parsedError,
...(message ? { messageHash: message.messageHash } : {}),
}); });
} }
} }

View File

@@ -3,12 +3,13 @@ import { mock } from "jest-mock-extended";
import { import {
ContractTransactionResponse, ContractTransactionResponse,
ErrorDescription, ErrorDescription,
makeError,
Overrides, Overrides,
Signer, Signer,
TransactionReceipt, TransactionReceipt,
TransactionResponse, TransactionResponse,
} from "ethers"; } from "ethers";
import { Direction } from "@consensys/linea-sdk"; import { Direction, makeBaseError } from "@consensys/linea-sdk";
import { TestLogger } from "../../../utils/testing/helpers"; import { TestLogger } from "../../../utils/testing/helpers";
import { MessageStatus } from "../../../core/enums"; import { MessageStatus } from "../../../core/enums";
import { testL1NetworkConfig, testMessage, DEFAULT_MAX_FEE_PER_GAS } from "../../../utils/testing/constants"; import { testL1NetworkConfig, testMessage, DEFAULT_MAX_FEE_PER_GAS } from "../../../utils/testing/constants";
@@ -67,7 +68,7 @@ describe("L2ClaimMessageTransactionSizeProcessor", () => {
it("Should log as error when calculateTransactionSize failed", async () => { it("Should log as error when calculateTransactionSize failed", async () => {
const testGasLimit = 50_000n; const testGasLimit = 50_000n;
const loggerErrorSpy = jest.spyOn(logger, "error"); const loggerErrorSpy = jest.spyOn(logger, "warnOrError");
jest.spyOn(databaseService, "getNFirstMessagesByStatus").mockResolvedValue([testMessage]); jest.spyOn(databaseService, "getNFirstMessagesByStatus").mockResolvedValue([testMessage]);
jest.spyOn(l2ContractClientMock, "estimateClaimGasFees").mockResolvedValue({ jest.spyOn(l2ContractClientMock, "estimateClaimGasFees").mockResolvedValue({
gasLimit: testGasLimit, gasLimit: testGasLimit,
@@ -81,7 +82,53 @@ describe("L2ClaimMessageTransactionSizeProcessor", () => {
await transactionSizeProcessor.process(); await transactionSizeProcessor.process();
expect(loggerErrorSpy).toHaveBeenCalledTimes(1); expect(loggerErrorSpy).toHaveBeenCalledTimes(1);
expect(loggerErrorSpy).toHaveBeenCalledWith(new Error("calculation failed.")); expect(loggerErrorSpy).toHaveBeenCalledWith("Error occurred while processing message transaction size.", {
errorCode: "UNKNOWN_ERROR",
errorMessage: "calculation failed.",
messageHash: testMessage.messageHash,
mitigation: { shouldRetry: false },
});
});
it("Should log as error when estimateClaimGasFees failed", async () => {
const loggerErrorSpy = jest.spyOn(logger, "warnOrError");
jest.spyOn(databaseService, "getNFirstMessagesByStatus").mockResolvedValue([testMessage]);
jest.spyOn(l2ContractClientMock, "estimateClaimGasFees").mockRejectedValue(
makeBaseError(
makeError("could not coalesce error", "UNKNOWN_ERROR", {
error: {
code: -32000,
data: "0x5461344300000000000000000000000034be5b8c30ee4fde069dc878989686abe9884470",
message: "Execution reverted",
},
payload: {
id: 1,
jsonrpc: "2.0",
method: "linea_estimateGas",
params: [
{
data: "0x491e09360000000000000000000000004420ce157f2c39edaae6cc107a42c8e527d6e02800000000000000000000000034be5b8c30ee4fde069dc878989686abe988447000000000000000000000000000000000000000000000000000006182ba2f0b400000000000000000000000000000000000000000000000000001c6bf52634000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000052b130000000000000000000000000000000000000000000000000000000000000000",
from: "0x46eA7a855DA88FBC09cc59de93468E6bFbf0d81b",
to: "0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec",
value: "0",
},
],
},
}),
testMessage,
),
);
await transactionSizeProcessor.process();
expect(loggerErrorSpy).toHaveBeenCalledTimes(1);
expect(loggerErrorSpy).toHaveBeenCalledWith("Error occurred while processing message transaction size.", {
data: "0x5461344300000000000000000000000034be5b8c30ee4fde069dc878989686abe9884470",
errorCode: "UNKNOWN_ERROR",
errorMessage: "Execution reverted",
messageHash: testMessage.messageHash,
mitigation: { shouldRetry: false },
});
}); });
it("Should log as info and call updateMessage if the transaction size calculation succeed", async () => { it("Should log as info and call updateMessage if the transaction size calculation succeed", async () => {

View File

@@ -129,7 +129,10 @@ describe("TestMessageAnchoringProcessor", () => {
await anchoringProcessor.process(); await anchoringProcessor.process();
expect(loggerErrorSpy).toHaveBeenCalledTimes(1); expect(loggerErrorSpy).toHaveBeenCalledTimes(1);
expect(loggerErrorSpy).toHaveBeenCalledWith(error); expect(loggerErrorSpy).toHaveBeenCalledWith("An error occurred while processing messages.", {
errorCode: "UNKNOWN_ERROR",
errorMessage: error.message,
});
expect(messageRepositoryMockSaveSpy).toHaveBeenCalledTimes(0); expect(messageRepositoryMockSaveSpy).toHaveBeenCalledTimes(0);
}); });
}); });

View File

@@ -82,7 +82,11 @@ describe("TestMessageClaimingPersister ", () => {
await messageClaimingPersister.process(); await messageClaimingPersister.process();
expect(loggerErrorSpy).toHaveBeenCalledTimes(1); expect(loggerErrorSpy).toHaveBeenCalledTimes(1);
expect(loggerErrorSpy).toHaveBeenCalledWith(getTxReceiptError); expect(loggerErrorSpy).toHaveBeenCalledWith("Error processing message.", {
messageHash: testPendingMessage.messageHash,
errorCode: "UNKNOWN_ERROR",
errorMessage: getTxReceiptError.message,
});
}); });
it("Should log as info and update message as claimed success if successful", async () => { it("Should log as info and update message as claimed success if successful", async () => {

View File

@@ -370,6 +370,7 @@ describe("TestMessageClaimingProcessor", () => {
expect(loggerWarnOrErrorSpy).toHaveBeenCalledTimes(1); expect(loggerWarnOrErrorSpy).toHaveBeenCalledTimes(1);
expect(loggerWarnOrErrorSpy).toHaveBeenCalledWith(actionRejectedError, { expect(loggerWarnOrErrorSpy).toHaveBeenCalledWith(actionRejectedError, {
parsedError: ErrorParser.parseErrorWithMitigation(actionRejectedError as EthersError), parsedError: ErrorParser.parseErrorWithMitigation(actionRejectedError as EthersError),
messageHash: expectedLoggingMessage.messageHash,
}); });
}); });
}); });

View File

@@ -1,5 +1,5 @@
import { EthersError, ErrorCode } from "ethers"; import { EthersError, ErrorCode, isError } from "ethers";
import { GasEstimationError } from "@consensys/linea-sdk"; import { isBaseError } from "@consensys/linea-sdk";
import { DatabaseAccessError } from "../core/errors/DatabaseErrors"; import { DatabaseAccessError } from "../core/errors/DatabaseErrors";
import { MessageProps } from "../core/entities/Message"; import { MessageProps } from "../core/entities/Message";
@@ -12,8 +12,8 @@ export type Mitigation = {
export type ParsedErrorResult = { export type ParsedErrorResult = {
errorCode: ErrorCode; errorCode: ErrorCode;
context?: string; errorMessage?: string;
reason?: string; data?: string;
mitigation: Mitigation; mitigation: Mitigation;
}; };
@@ -26,82 +26,140 @@ export class ErrorParser {
* @param {EthersError} error - The error encountered during Ethereum operations. * @param {EthersError} error - The error encountered during Ethereum operations.
* @returns {ParsedErrorResult | null} An object containing the parsed error result and mitigation strategies, or `null` if the error is `undefined`. * @returns {ParsedErrorResult | null} An object containing the parsed error result and mitigation strategies, or `null` if the error is `undefined`.
*/ */
public static parseErrorWithMitigation(error: EthersError): ParsedErrorResult | null { public static parseErrorWithMitigation(error: unknown): ParsedErrorResult | null {
if (!error) { if (!error) {
return null; return null;
} }
const parsedErrResult: ParsedErrorResult = { if (!isBaseError(error)) {
errorCode: "UNKNOWN_ERROR", if (error instanceof DatabaseAccessError) {
mitigation: { shouldRetry: false }, return {
}; errorCode: "UNKNOWN_ERROR",
errorMessage: (error as DatabaseAccessError<MessageProps>).message,
switch (error.code) { mitigation: { shouldRetry: true },
case "NETWORK_ERROR":
case "SERVER_ERROR":
case "TIMEOUT":
case "INSUFFICIENT_FUNDS":
case "REPLACEMENT_UNDERPRICED":
case "NONCE_EXPIRED":
parsedErrResult.context = error.shortMessage;
parsedErrResult.mitigation = {
shouldRetry: true,
}; };
break; }
case "CALL_EXCEPTION":
if (
error.shortMessage.includes("execution reverted") ||
error.info?.error?.code === 4001 || //The user rejected the request (EIP-1193)
error.info?.error?.code === -32603 //Internal JSON-RPC error (EIP-1474)
) {
parsedErrResult.context = error.info?.error?.message ?? error.shortMessage;
break;
}
if ( return {
error.info?.error?.code === -32000 && //Missing or invalid parameters (EIP-1474) errorCode: "UNKNOWN_ERROR",
(error.info?.error?.message.includes("gas required exceeds allowance (0)") || errorMessage: error instanceof Error ? error.message : String(error),
error.info?.error?.message.includes("max priority fee per gas higher than max fee per gas") || mitigation: { shouldRetry: false },
error.info?.error?.message.includes("max fee per gas less than block base fee")) };
) { }
parsedErrResult.context = error.info?.error?.message;
parsedErrResult.mitigation = { if (!this.isEthersError(error.error)) {
return {
errorCode: "UNKNOWN_ERROR",
errorMessage: error instanceof Error ? error.message : String(error),
mitigation: { shouldRetry: false },
};
}
return this.parseEthersError(error.error);
}
private static isEthersError(error: unknown): error is EthersError {
return (error as EthersError).shortMessage !== undefined || (error as EthersError).code !== undefined;
}
public static parseEthersError(error: EthersError): ParsedErrorResult {
if (
isError(error, "NETWORK_ERROR") ||
isError(error, "SERVER_ERROR") ||
isError(error, "TIMEOUT") ||
isError(error, "INSUFFICIENT_FUNDS") ||
isError(error, "REPLACEMENT_UNDERPRICED") ||
isError(error, "NONCE_EXPIRED")
) {
return {
errorCode: error.code,
errorMessage: error.message,
mitigation: {
shouldRetry: true,
},
};
}
if (isError(error, "CALL_EXCEPTION")) {
if (
error.shortMessage.includes("execution reverted") ||
error.info?.error?.code === 4001 || //The user rejected the request (EIP-1193)
error.info?.error?.code === -32603 //Internal JSON-RPC error (EIP-1474)
) {
return {
errorCode: error.code,
errorMessage: error.info?.error?.message ?? error.shortMessage,
mitigation: {
shouldRetry: false,
},
};
}
if (
error.info?.error?.code === -32000 && //Missing or invalid parameters (EIP-1474)
(error.info?.error?.message.includes("gas required exceeds allowance (0)") ||
error.info?.error?.message.includes("max priority fee per gas higher than max fee per gas") ||
error.info?.error?.message.includes("max fee per gas less than block base fee"))
) {
return {
errorCode: error.code,
errorMessage: error.info?.error?.message ?? error.shortMessage,
mitigation: {
shouldRetry: true, shouldRetry: true,
}; },
break;
}
parsedErrResult.context = error.shortMessage;
parsedErrResult.mitigation = {
shouldRetry: true,
}; };
break; }
case "ACTION_REJECTED":
case "UNKNOWN_ERROR":
parsedErrResult.context = error.shortMessage;
break;
default:
if (error instanceof GasEstimationError) {
parsedErrResult.context = (error as GasEstimationError<MessageProps>).message;
break;
}
if (error instanceof DatabaseAccessError) { return {
parsedErrResult.context = (error as DatabaseAccessError<MessageProps>).message; errorCode: error.code,
} else { errorMessage: error.shortMessage,
parsedErrResult.context = error.message; mitigation: {
}
parsedErrResult.context = error.shortMessage ?? error.message;
parsedErrResult.mitigation = {
shouldRetry: true, shouldRetry: true,
},
};
}
if (isError(error, "ACTION_REJECTED")) {
return {
errorCode: error.code,
errorMessage: error.info?.error?.message ?? error.shortMessage,
mitigation: {
shouldRetry: false,
},
};
}
if (isError(error, "UNKNOWN_ERROR")) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (error.error?.code === -32000 && error.error?.message?.toLowerCase().includes("execution reverted")) {
return {
errorCode: error.code,
errorMessage: error.error?.message ?? error.shortMessage,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
data: error.error?.data,
mitigation: {
shouldRetry: false,
},
}; };
break; }
return {
errorCode: error.code,
errorMessage: error.shortMessage,
mitigation: {
shouldRetry: false,
},
};
} }
return { return {
...parsedErrResult, errorCode: error.code,
errorCode: error.code || parsedErrResult.errorCode, errorMessage: error.shortMessage ?? error.message,
mitigation: {
shouldRetry: true,
},
}; };
} }
} }

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from "@jest/globals"; import { describe, it, expect } from "@jest/globals";
import { ErrorCode, EthersError } from "ethers"; import { ErrorCode, makeError } from "ethers";
import { GasEstimationError } from "@consensys/linea-sdk"; import { makeBaseError } from "@consensys/linea-sdk";
import { ErrorParser } from "../ErrorParser"; import { ErrorParser } from "../ErrorParser";
import { DatabaseAccessError } from "../../core/errors"; import { DatabaseAccessError } from "../../core/errors";
import { DatabaseErrorType, DatabaseRepoName } from "../../core/enums"; import { DatabaseErrorType, DatabaseRepoName } from "../../core/enums";
@@ -9,17 +9,36 @@ import { generateMessage } from "../testing/helpers";
describe("ErrorParser", () => { describe("ErrorParser", () => {
describe("parseErrorWithMitigation", () => { describe("parseErrorWithMitigation", () => {
it("should return null when error is null", () => { it("should return null when error is null", () => {
expect(ErrorParser.parseErrorWithMitigation(null as unknown as EthersError)).toStrictEqual(null); expect(ErrorParser.parseErrorWithMitigation(null)).toStrictEqual(null);
}); });
it("should return NETWORK_ERROR and shouldRetry = true when error code = NETWORK_ERROR", () => { it("should return UNKNOWN_ERROR and shouldRetry = false when error is not instance of BaseError", () => {
const errorMessage = { expect(ErrorParser.parseErrorWithMitigation(new Error("any reason"))).toStrictEqual({
code: "NETWORK_ERROR", errorMessage: "any reason",
shortMessage: "any reason", errorCode: "UNKNOWN_ERROR",
}; mitigation: {
shouldRetry: false,
},
});
});
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ it("should return UNKNOWN_ERROR and shouldRetry = false when error is instance of BaseError but the underlying error is not an EthersError", () => {
context: "any reason", expect(ErrorParser.parseErrorWithMitigation(makeBaseError(new Error("any reason")))).toStrictEqual({
errorMessage: "any reason",
errorCode: "UNKNOWN_ERROR",
mitigation: {
shouldRetry: false,
},
});
});
});
describe("parseEthersError", () => {
it("should return NETWORK_ERROR and shouldRetry = true when error code = NETWORK_ERROR", () => {
const error = makeError("any reason", "NETWORK_ERROR");
expect(ErrorParser.parseEthersError(error)).toStrictEqual({
errorMessage: "any reason (code=NETWORK_ERROR, version=6.13.4)",
errorCode: "NETWORK_ERROR", errorCode: "NETWORK_ERROR",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -28,13 +47,10 @@ describe("ErrorParser", () => {
}); });
it("should return SERVER_ERROR and shouldRetry = true when error code = SERVER_ERROR", () => { it("should return SERVER_ERROR and shouldRetry = true when error code = SERVER_ERROR", () => {
const errorMessage = { const error = makeError("any reason", "SERVER_ERROR");
code: "SERVER_ERROR",
shortMessage: "any reason",
};
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "any reason", errorMessage: "any reason (code=SERVER_ERROR, version=6.13.4)",
errorCode: "SERVER_ERROR", errorCode: "SERVER_ERROR",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -43,13 +59,10 @@ describe("ErrorParser", () => {
}); });
it("should return TIMEOUT and shouldRetry = true when error code = TIMEOUT", () => { it("should return TIMEOUT and shouldRetry = true when error code = TIMEOUT", () => {
const errorMessage = { const error = makeError("any reason", "TIMEOUT");
code: "TIMEOUT",
shortMessage: "any reason",
};
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "any reason", errorMessage: "any reason (code=TIMEOUT, version=6.13.4)",
errorCode: "TIMEOUT", errorCode: "TIMEOUT",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -58,13 +71,10 @@ describe("ErrorParser", () => {
}); });
it("should return INSUFFICIENT_FUNDS and shouldRetry = true when error code = INSUFFICIENT_FUNDS", () => { it("should return INSUFFICIENT_FUNDS and shouldRetry = true when error code = INSUFFICIENT_FUNDS", () => {
const errorMessage = { const error = makeError("any reason", "INSUFFICIENT_FUNDS");
code: "INSUFFICIENT_FUNDS",
shortMessage: "any reason",
};
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "any reason", errorMessage: "any reason (code=INSUFFICIENT_FUNDS, version=6.13.4)",
errorCode: "INSUFFICIENT_FUNDS", errorCode: "INSUFFICIENT_FUNDS",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -72,14 +82,11 @@ describe("ErrorParser", () => {
}); });
}); });
it("should return REPLACEMENT_UNDERPRICED and shouldRetry = true when error code = NETWOREPLACEMENT_UNDERPRICEDK_ERROR", () => { it("should return REPLACEMENT_UNDERPRICED and shouldRetry = true when error code = REPLACEMENT_UNDERPRICED", () => {
const errorMessage = { const error = makeError("any reason", "REPLACEMENT_UNDERPRICED");
code: "REPLACEMENT_UNDERPRICED",
shortMessage: "any reason",
};
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "any reason", errorMessage: "any reason (code=REPLACEMENT_UNDERPRICED, version=6.13.4)",
errorCode: "REPLACEMENT_UNDERPRICED", errorCode: "REPLACEMENT_UNDERPRICED",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -88,13 +95,10 @@ describe("ErrorParser", () => {
}); });
it("should return NONCE_EXPIRED and shouldRetry = true when error code = NONCE_EXPIRED", () => { it("should return NONCE_EXPIRED and shouldRetry = true when error code = NONCE_EXPIRED", () => {
const errorMessage = { const error = makeError("any reason", "NONCE_EXPIRED");
code: "NONCE_EXPIRED",
shortMessage: "any reason",
};
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "any reason", errorMessage: "any reason (code=NONCE_EXPIRED, version=6.13.4)",
errorCode: "NONCE_EXPIRED", errorCode: "NONCE_EXPIRED",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -103,13 +107,10 @@ describe("ErrorParser", () => {
}); });
it("should return CALL_EXCEPTION and shouldRetry = true when error code = CALL_EXCEPTION", () => { it("should return CALL_EXCEPTION and shouldRetry = true when error code = CALL_EXCEPTION", () => {
const errorMessage = { const error = makeError("any reason", "CALL_EXCEPTION");
shortMessage: "any reason",
code: "CALL_EXCEPTION",
};
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "any reason", errorMessage: "any reason",
errorCode: "CALL_EXCEPTION", errorCode: "CALL_EXCEPTION",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -118,18 +119,26 @@ describe("ErrorParser", () => {
}); });
it("should return CALL_EXCEPTION and shouldRetry = false when error code = CALL_EXCEPTION with short message as execution reverted", () => { it("should return CALL_EXCEPTION and shouldRetry = false when error code = CALL_EXCEPTION with short message as execution reverted", () => {
const errorMessage = { const error = makeError("execution reverted", "CALL_EXCEPTION", {
shortMessage: "execution reverted",
code: "CALL_EXCEPTION",
info: { info: {
error: { error: {
message: "execution reverted for some reason", message: "execution reverted for some reason",
}, },
}, },
}; reason: "execution reverted",
action: "call",
data: "0x0123456789abcdef",
transaction: {
to: null,
from: undefined,
data: "",
},
invocation: null,
revert: null,
});
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "execution reverted for some reason", errorMessage: "execution reverted for some reason",
errorCode: "CALL_EXCEPTION", errorCode: "CALL_EXCEPTION",
mitigation: { mitigation: {
shouldRetry: false, shouldRetry: false,
@@ -138,19 +147,27 @@ describe("ErrorParser", () => {
}); });
it("should return CALL_EXCEPTION and shouldRetry = false when error code = CALL_EXCEPTION with inner error code as 4001", () => { it("should return CALL_EXCEPTION and shouldRetry = false when error code = CALL_EXCEPTION with inner error code as 4001", () => {
const errorMessage = { const error = makeError("any reason", "CALL_EXCEPTION", {
shortMessage: "any reason",
code: "CALL_EXCEPTION",
info: { info: {
error: { error: {
message: "execution reverted for some reason", message: "execution reverted for some reason",
code: 4001, code: 4001,
}, },
}, },
}; action: "call",
data: null,
reason: null,
transaction: {
to: null,
from: undefined,
data: "",
},
invocation: null,
revert: null,
});
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "execution reverted for some reason", errorMessage: "execution reverted for some reason",
errorCode: "CALL_EXCEPTION", errorCode: "CALL_EXCEPTION",
mitigation: { mitigation: {
shouldRetry: false, shouldRetry: false,
@@ -159,19 +176,27 @@ describe("ErrorParser", () => {
}); });
it("should return CALL_EXCEPTION and shouldRetry = false when error code = CALL_EXCEPTION with inner error code as -32603", () => { it("should return CALL_EXCEPTION and shouldRetry = false when error code = CALL_EXCEPTION with inner error code as -32603", () => {
const errorMessage = { const error = makeError("any reason", "CALL_EXCEPTION", {
shortMessage: "any reason",
code: "CALL_EXCEPTION",
info: { info: {
error: { error: {
message: "execution reverted for some reason", message: "execution reverted for some reason",
code: -32603, code: -32603,
}, },
}, },
}; action: "call",
data: null,
reason: null,
transaction: {
to: null,
from: undefined,
data: "",
},
invocation: null,
revert: null,
});
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "execution reverted for some reason", errorMessage: "execution reverted for some reason",
errorCode: "CALL_EXCEPTION", errorCode: "CALL_EXCEPTION",
mitigation: { mitigation: {
shouldRetry: false, shouldRetry: false,
@@ -180,19 +205,27 @@ describe("ErrorParser", () => {
}); });
it("should return CALL_EXCEPTION and shouldRetry = true when error code = CALL_EXCEPTION and inner error code as -32000 and message as gas required exceeds allowance (0)", () => { it("should return CALL_EXCEPTION and shouldRetry = true when error code = CALL_EXCEPTION and inner error code as -32000 and message as gas required exceeds allowance (0)", () => {
const errorMessage = { const error = makeError("any reason", "CALL_EXCEPTION", {
shortMessage: "any reason",
code: "CALL_EXCEPTION",
info: { info: {
error: { error: {
message: "gas required exceeds allowance (0)", message: "gas required exceeds allowance (0)",
code: -32000, code: -32000,
}, },
}, },
}; action: "call",
data: null,
reason: null,
transaction: {
to: null,
from: undefined,
data: "",
},
invocation: null,
revert: null,
});
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "gas required exceeds allowance (0)", errorMessage: "gas required exceeds allowance (0)",
errorCode: "CALL_EXCEPTION", errorCode: "CALL_EXCEPTION",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -201,19 +234,27 @@ describe("ErrorParser", () => {
}); });
it("should return CALL_EXCEPTION and shouldRetry = true when error code = CALL_EXCEPTION and inner error code as -32000 and message as max priority fee per gas higher", () => { it("should return CALL_EXCEPTION and shouldRetry = true when error code = CALL_EXCEPTION and inner error code as -32000 and message as max priority fee per gas higher", () => {
const errorMessage = { const error = makeError("any reason", "CALL_EXCEPTION", {
shortMessage: "any reason",
code: "CALL_EXCEPTION",
info: { info: {
error: { error: {
message: "max priority fee per gas higher than max fee per gas", message: "max priority fee per gas higher than max fee per gas",
code: -32000, code: -32000,
}, },
}, },
}; action: "call",
data: null,
reason: null,
transaction: {
to: null,
from: undefined,
data: "",
},
invocation: null,
revert: null,
});
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "max priority fee per gas higher than max fee per gas", errorMessage: "max priority fee per gas higher than max fee per gas",
errorCode: "CALL_EXCEPTION", errorCode: "CALL_EXCEPTION",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -222,19 +263,27 @@ describe("ErrorParser", () => {
}); });
it("should return CALL_EXCEPTION and shouldRetry = true when error code = CALL_EXCEPTION and inner error code as -32000 and message as max fee per gas less than block base fee", () => { it("should return CALL_EXCEPTION and shouldRetry = true when error code = CALL_EXCEPTION and inner error code as -32000 and message as max fee per gas less than block base fee", () => {
const errorMessage = { const error = makeError("any reason", "CALL_EXCEPTION", {
shortMessage: "any reason",
code: "CALL_EXCEPTION",
info: { info: {
error: { error: {
message: "max fee per gas less than block base fee", message: "max fee per gas less than block base fee",
code: -32000, code: -32000,
}, },
}, },
}; action: "call",
data: null,
reason: null,
transaction: {
to: null,
from: undefined,
data: "",
},
invocation: null,
revert: null,
});
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "max fee per gas less than block base fee", errorMessage: "max fee per gas less than block base fee",
errorCode: "CALL_EXCEPTION", errorCode: "CALL_EXCEPTION",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -243,19 +292,27 @@ describe("ErrorParser", () => {
}); });
it("should return CALL_EXCEPTION and shouldRetry = true when error code = CALL_EXCEPTION with other inner error", () => { it("should return CALL_EXCEPTION and shouldRetry = true when error code = CALL_EXCEPTION with other inner error", () => {
const errorMessage = { const error = makeError("any reason", "CALL_EXCEPTION", {
shortMessage: "any reason",
code: "CALL_EXCEPTION",
info: { info: {
error: { error: {
message: "invalid method parameters", message: "invalid method parameters",
code: -32602, code: -32602,
}, },
}, },
}; action: "call",
data: null,
reason: null,
transaction: {
to: null,
from: undefined,
data: "",
},
invocation: null,
revert: null,
});
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "any reason", errorMessage: "any reason",
errorCode: "CALL_EXCEPTION", errorCode: "CALL_EXCEPTION",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -264,13 +321,10 @@ describe("ErrorParser", () => {
}); });
it("should return ACTION_REJECTED and shouldRetry = false when error code = ACTION_REJECTED", () => { it("should return ACTION_REJECTED and shouldRetry = false when error code = ACTION_REJECTED", () => {
const errorMessage = { const error = makeError("any reason", "ACTION_REJECTED");
shortMessage: "any reason",
code: "ACTION_REJECTED",
};
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "any reason", errorMessage: "any reason",
errorCode: "ACTION_REJECTED", errorCode: "ACTION_REJECTED",
mitigation: { mitigation: {
shouldRetry: false, shouldRetry: false,
@@ -279,13 +333,10 @@ describe("ErrorParser", () => {
}); });
it("should return UNKNOWN_ERROR and shouldRetry = false when error code = UNKNOWN_ERROR", () => { it("should return UNKNOWN_ERROR and shouldRetry = false when error code = UNKNOWN_ERROR", () => {
const errorMessage = { const error = makeError("any reason", "UNKNOWN_ERROR");
shortMessage: "any reason",
code: "UNKNOWN_ERROR",
};
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "any reason", errorMessage: "any reason",
errorCode: "UNKNOWN_ERROR", errorCode: "UNKNOWN_ERROR",
mitigation: { mitigation: {
shouldRetry: false, shouldRetry: false,
@@ -294,10 +345,10 @@ describe("ErrorParser", () => {
}); });
it("should return UNKNOWN_ERROR and shouldRetry = false when error = GasEstimationError", () => { it("should return UNKNOWN_ERROR and shouldRetry = false when error = GasEstimationError", () => {
const gasEstimationError = new GasEstimationError("Gas estimation failed", generateMessage()); const gasEstimationError = makeBaseError(makeError("Gas estimation failed", "UNKNOWN_ERROR"), generateMessage());
expect(ErrorParser.parseErrorWithMitigation(gasEstimationError as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseErrorWithMitigation(gasEstimationError)).toStrictEqual({
context: "Gas estimation failed", errorMessage: "Gas estimation failed",
errorCode: "UNKNOWN_ERROR", errorCode: "UNKNOWN_ERROR",
mitigation: { mitigation: {
shouldRetry: false, shouldRetry: false,
@@ -305,6 +356,25 @@ describe("ErrorParser", () => {
}); });
}); });
it("should return UNKNOWN_ERROR and shouldRetry = false when error is execution reverted", () => {
const error = makeError("Gas estimation failed", "UNKNOWN_ERROR", {
error: {
code: -32000,
message: "execution reverted",
data: "0x0123456789abcdef",
},
});
expect(ErrorParser.parseEthersError(error)).toStrictEqual({
errorMessage: "execution reverted",
errorCode: "UNKNOWN_ERROR",
data: "0x0123456789abcdef",
mitigation: {
shouldRetry: false,
},
});
});
it("should return UNKNOWN_ERROR and shouldRetry = false when error = DatabaseAccessError", () => { it("should return UNKNOWN_ERROR and shouldRetry = false when error = DatabaseAccessError", () => {
const databaseAccessError = new DatabaseAccessError( const databaseAccessError = new DatabaseAccessError(
DatabaseRepoName.MessageRepository, DatabaseRepoName.MessageRepository,
@@ -313,8 +383,8 @@ describe("ErrorParser", () => {
generateMessage(), generateMessage(),
); );
expect(ErrorParser.parseErrorWithMitigation(databaseAccessError as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseErrorWithMitigation(databaseAccessError)).toStrictEqual({
context: "MessageRepository: insert - Database access failed", errorMessage: "MessageRepository: insert - Database access failed",
errorCode: "UNKNOWN_ERROR", errorCode: "UNKNOWN_ERROR",
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,
@@ -339,13 +409,10 @@ describe("ErrorParser", () => {
"OFFCHAIN_FAULT", "OFFCHAIN_FAULT",
]; ];
otherErrorCodes.forEach((errorCode: ErrorCode) => { otherErrorCodes.forEach((errorCode: ErrorCode) => {
const errorMessage = { const error = makeError("any reason", errorCode);
shortMessage: "any reason",
code: errorCode,
};
expect(ErrorParser.parseErrorWithMitigation(errorMessage as unknown as EthersError)).toStrictEqual({ expect(ErrorParser.parseEthersError(error)).toStrictEqual({
context: "any reason", errorMessage: "any reason",
errorCode: errorCode, errorCode: errorCode,
mitigation: { mitigation: {
shouldRetry: true, shouldRetry: true,

View File

@@ -20,11 +20,11 @@ import {
DEFAULT_L2_MESSAGE_TREE_DEPTH, DEFAULT_L2_MESSAGE_TREE_DEPTH,
DEFAULT_MAX_FEE_PER_GAS_CAP, DEFAULT_MAX_FEE_PER_GAS_CAP,
} from "./core/constants"; } from "./core/constants";
import { BaseError } from "./core/errors";
import { L1FeeEstimatorOptions, L2FeeEstimatorOptions, LineaSDKOptions, Network, SDKMode } from "./core/types"; import { L1FeeEstimatorOptions, L2FeeEstimatorOptions, LineaSDKOptions, Network, SDKMode } from "./core/types";
import { NETWORKS } from "./core/constants"; import { NETWORKS } from "./core/constants";
import { isString } from "./core/utils"; import { isString } from "./core/utils";
import { Direction } from "./core/enums"; import { Direction } from "./core/enums";
import { makeBaseError } from "./core/errors/utils";
export class LineaSDK { export class LineaSDK {
private network: Network; private network: Network;
@@ -64,7 +64,7 @@ export class LineaSDK {
const { l1SignerPrivateKeyOrWallet, l2SignerPrivateKeyOrWallet } = options; const { l1SignerPrivateKeyOrWallet, l2SignerPrivateKeyOrWallet } = options;
if (!l1SignerPrivateKeyOrWallet || !l2SignerPrivateKeyOrWallet) { if (!l1SignerPrivateKeyOrWallet || !l2SignerPrivateKeyOrWallet) {
throw new BaseError("You need to provide both L1 and L2 signer private keys or wallets."); throw makeBaseError("You need to provide both L1 and L2 signer private keys or wallets.");
} }
this.l1Signer = this.getWallet(l1SignerPrivateKeyOrWallet).connect(this.l1Provider); this.l1Signer = this.getWallet(l1SignerPrivateKeyOrWallet).connect(this.l1Provider);
@@ -108,7 +108,7 @@ export class LineaSDK {
return new BrowserProvider(l1RpcUrlOrProvider); return new BrowserProvider(l1RpcUrlOrProvider);
} }
throw new BaseError("Invalid argument: l1RpcUrlOrProvider must be a string or Eip1193Provider"); throw makeBaseError("Invalid argument: l1RpcUrlOrProvider must be a string or Eip1193Provider");
} }
/** /**
@@ -118,7 +118,7 @@ export class LineaSDK {
*/ */
public getL1Signer(): Signer { public getL1Signer(): Signer {
if (!this.l1Signer) { if (!this.l1Signer) {
throw new BaseError("L1 signer is not available in read-only mode."); throw makeBaseError("L1 signer is not available in read-only mode.");
} }
return this.l1Signer; return this.l1Signer;
} }
@@ -130,7 +130,7 @@ export class LineaSDK {
*/ */
public getL2Signer(): Signer { public getL2Signer(): Signer {
if (!this.l2Signer) { if (!this.l2Signer) {
throw new BaseError("L2 signer is not available in read-only mode."); throw makeBaseError("L2 signer is not available in read-only mode.");
} }
return this.l2Signer; return this.l2Signer;
} }
@@ -176,7 +176,7 @@ export class LineaSDK {
return new LineaBrowserProvider(l2RpcUrlOrProvider); return new LineaBrowserProvider(l2RpcUrlOrProvider);
} }
throw new Error("Invalid argument: l2RpcUrlOrProvider must be a string or Eip1193Provider"); throw makeBaseError("Invalid argument: l2RpcUrlOrProvider must be a string or Eip1193Provider");
} }
/** /**
@@ -284,13 +284,13 @@ export class LineaSDK {
private getContractAddress(contractType: "l1" | "l2", localContractAddress?: string): string { private getContractAddress(contractType: "l1" | "l2", localContractAddress?: string): string {
if (this.network === "custom") { if (this.network === "custom") {
if (!localContractAddress) { if (!localContractAddress) {
throw new BaseError(`You need to provide a ${contractType.toUpperCase()} contract address.`); throw makeBaseError(`You need to provide a ${contractType.toUpperCase()} contract address.`);
} }
return localContractAddress; return localContractAddress;
} else { } else {
const contractAddress = NETWORKS[this.network][`${contractType}ContractAddress`]; const contractAddress = NETWORKS[this.network][`${contractType}ContractAddress`];
if (!contractAddress) { if (!contractAddress) {
throw new BaseError(`Contract address for ${contractType.toUpperCase()} not found in network ${this.network}.`); throw makeBaseError(`Contract address for ${contractType.toUpperCase()} not found in network ${this.network}.`);
} }
return contractAddress; return contractAddress;
} }
@@ -308,7 +308,7 @@ export class LineaSDK {
return privateKeyOrWallet instanceof Wallet ? privateKeyOrWallet : new Wallet(privateKeyOrWallet); return privateKeyOrWallet instanceof Wallet ? privateKeyOrWallet : new Wallet(privateKeyOrWallet);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message.includes("invalid private key")) { if (e instanceof Error && e.message.includes("invalid private key")) {
throw new BaseError("Something went wrong when trying to generate Wallet. Please check your private key."); throw makeBaseError("Something went wrong when trying to generate Wallet. Please check your private key.");
} }
throw e; throw e;
} }

View File

@@ -11,8 +11,8 @@ import { Cache } from "../../utils/Cache";
import { ILineaRollupClient } from "../../core/clients/ethereum"; import { ILineaRollupClient } from "../../core/clients/ethereum";
import { IL2MessageServiceClient, IL2MessageServiceLogClient } from "../../core/clients/linea"; import { IL2MessageServiceClient, IL2MessageServiceLogClient } from "../../core/clients/linea";
import { MessageSent, Network } from "../../core/types"; import { MessageSent, Network } from "../../core/types";
import { BaseError } from "../../core/errors";
import { FinalizationMessagingInfo, Proof } from "../../core/clients/ethereum/IMerkleTreeService"; import { FinalizationMessagingInfo, Proof } from "../../core/clients/ethereum/IMerkleTreeService";
import { makeBaseError } from "../../core/errors/utils";
export class L1ClaimingService { export class L1ClaimingService {
private cache: Cache; private cache: Cache;
@@ -93,7 +93,7 @@ export class L1ClaimingService {
}); });
if (!messageSentEvent) { if (!messageSentEvent) {
throw new BaseError(`Message hash does not exist on L2. Message hash: ${messageHash}`); throw makeBaseError(`Message hash does not exist on L2. Message hash: ${messageHash}`);
} }
if (migrationBlock > messageSentEvent.blockNumber) { if (migrationBlock > messageSentEvent.blockNumber) {

View File

@@ -9,7 +9,6 @@ import {
ErrorDescription, ErrorDescription,
} from "ethers"; } from "ethers";
import { LineaRollup, LineaRollup__factory } from "../../contracts/typechain"; import { LineaRollup, LineaRollup__factory } from "../../contracts/typechain";
import { BaseError, GasEstimationError } from "../../core/errors";
import { Message, SDKMode, MessageSent } from "../../core/types"; import { Message, SDKMode, MessageSent } from "../../core/types";
import { OnChainMessageStatus } from "../../core/enums"; import { OnChainMessageStatus } from "../../core/enums";
import { import {
@@ -31,6 +30,7 @@ import { GasFees, IEthereumGasProvider } from "../../core/clients/IGasProvider";
import { IMessageRetriever } from "../../core/clients/IMessageRetriever"; import { IMessageRetriever } from "../../core/clients/IMessageRetriever";
import { IProvider } from "../../core/clients/IProvider"; import { IProvider } from "../../core/clients/IProvider";
import { BrowserProvider, Provider } from "../providers"; import { BrowserProvider, Provider } from "../providers";
import { makeBaseError } from "../../core/errors/utils";
export class LineaRollupClient export class LineaRollupClient
implements implements
@@ -163,7 +163,7 @@ export class LineaRollupClient
} }
if (!signer) { if (!signer) {
throw new BaseError("Please provide a signer."); throw makeBaseError("Please provide a signer.");
} }
return LineaRollup__factory.connect(contractAddress, signer); return LineaRollup__factory.connect(contractAddress, signer);
@@ -216,7 +216,7 @@ export class LineaRollupClient
const [messageEvent] = await this.l2MessageServiceLogClient.getMessageSentEventsByMessageHash({ messageHash }); const [messageEvent] = await this.l2MessageServiceLogClient.getMessageSentEventsByMessageHash({ messageHash });
if (!messageEvent) { if (!messageEvent) {
throw new BaseError(`Message hash does not exist on L2. Message hash: ${messageHash}`); throw makeBaseError(`Message hash does not exist on L2. Message hash: ${messageHash}`);
} }
const [[l2MessagingBlockAnchoredEvent], isMessageClaimed] = await Promise.all([ const [[l2MessagingBlockAnchoredEvent], isMessageClaimed] = await Promise.all([
@@ -248,7 +248,7 @@ export class LineaRollupClient
overrides: Overrides = {}, overrides: Overrides = {},
): Promise<bigint> { ): Promise<bigint> {
if (this.mode === "read-only") { if (this.mode === "read-only") {
throw new BaseError("'EstimateClaimGas' function not callable using readOnly mode."); throw makeBaseError("'EstimateClaimGas' function not callable using readOnly mode.");
} }
const { messageSender, destination, fee, value, calldata, messageNonce, feeRecipient } = message; const { messageSender, destination, fee, value, calldata, messageNonce, feeRecipient } = message;
@@ -267,9 +267,8 @@ export class LineaRollupClient
...overrides, ...overrides,
}, },
); );
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) {
} catch (e: any) { throw makeBaseError(e, message);
throw new GasEstimationError(e, message);
} }
} }
@@ -284,7 +283,7 @@ export class LineaRollupClient
overrides: Overrides = {}, overrides: Overrides = {},
): Promise<ContractTransactionResponse> { ): Promise<ContractTransactionResponse> {
if (this.mode === "read-only") { if (this.mode === "read-only") {
throw new BaseError("'claim' function not callable using readOnly mode."); throw makeBaseError("'claim' function not callable using readOnly mode.");
} }
const { messageSender, destination, fee, value, calldata, messageNonce, feeRecipient } = message; const { messageSender, destination, fee, value, calldata, messageNonce, feeRecipient } = message;
@@ -316,7 +315,7 @@ export class LineaRollupClient
overrides: Overrides = {}, overrides: Overrides = {},
): Promise<bigint> { ): Promise<bigint> {
if (this.mode === "read-only") { if (this.mode === "read-only") {
throw new BaseError("'EstimateClaimGasFees' function not callable using readOnly mode."); throw makeBaseError("'EstimateClaimGasFees' function not callable using readOnly mode.");
} }
const { messageSender, destination, fee, value, calldata, messageNonce, feeRecipient } = message; const { messageSender, destination, fee, value, calldata, messageNonce, feeRecipient } = message;
@@ -344,9 +343,8 @@ export class LineaRollupClient
...overrides, ...overrides,
}, },
); );
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) {
} catch (e: any) { throw makeBaseError(e, message);
throw new GasEstimationError(e, message);
} }
} }
@@ -361,7 +359,7 @@ export class LineaRollupClient
overrides: Overrides = {}, overrides: Overrides = {},
): Promise<ContractTransactionResponse> { ): Promise<ContractTransactionResponse> {
if (this.mode === "read-only") { if (this.mode === "read-only") {
throw new BaseError("'claim' function not callable using readOnly mode."); throw makeBaseError("'claim' function not callable using readOnly mode.");
} }
const { messageSender, destination, fee, value, calldata, messageNonce, feeRecipient } = message; const { messageSender, destination, fee, value, calldata, messageNonce, feeRecipient } = message;
@@ -401,17 +399,17 @@ export class LineaRollupClient
priceBumpPercent: number = 10, priceBumpPercent: number = 10,
): Promise<TransactionResponse> { ): Promise<TransactionResponse> {
if (!Number.isInteger(priceBumpPercent)) { if (!Number.isInteger(priceBumpPercent)) {
throw new Error("'priceBumpPercent' must be an integer"); throw makeBaseError("'priceBumpPercent' must be an integer");
} }
if (this.mode === "read-only") { if (this.mode === "read-only") {
throw new BaseError("'retryTransactionWithHigherFee' function not callable using readOnly mode."); throw makeBaseError("'retryTransactionWithHigherFee' function not callable using readOnly mode.");
} }
const transaction = await this.provider.getTransaction(transactionHash); const transaction = await this.provider.getTransaction(transactionHash);
if (!transaction) { if (!transaction) {
throw new BaseError(`Transaction with hash ${transactionHash} not found.`); throw makeBaseError(`Transaction with hash ${transactionHash} not found.`);
} }
let maxPriorityFeePerGas; let maxPriorityFeePerGas;

View File

@@ -1,6 +1,5 @@
import { Block, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers"; import { Block, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { SparseMerkleTreeFactory } from "../../utils/merkleTree/MerkleTreeFactory"; import { SparseMerkleTreeFactory } from "../../utils/merkleTree/MerkleTreeFactory";
import { BaseError } from "../../core/errors";
import { import {
ILineaRollupLogClient, ILineaRollupLogClient,
FinalizationMessagingInfo, FinalizationMessagingInfo,
@@ -16,6 +15,7 @@ import {
import { LineaRollup, LineaRollup__factory } from "../../contracts/typechain"; import { LineaRollup, LineaRollup__factory } from "../../contracts/typechain";
import { IProvider } from "../../core/clients/IProvider"; import { IProvider } from "../../core/clients/IProvider";
import { BrowserProvider, Provider } from "../providers"; import { BrowserProvider, Provider } from "../providers";
import { makeBaseError } from "../../core/errors/utils";
export class MerkleTreeService implements IMerkleTreeService { export class MerkleTreeService implements IMerkleTreeService {
private readonly contract: LineaRollup; private readonly contract: LineaRollup;
@@ -54,7 +54,7 @@ export class MerkleTreeService implements IMerkleTreeService {
const [messageEvent] = await this.l2MessageServiceLogClient.getMessageSentEventsByMessageHash({ messageHash }); const [messageEvent] = await this.l2MessageServiceLogClient.getMessageSentEventsByMessageHash({ messageHash });
if (!messageEvent) { if (!messageEvent) {
throw new BaseError(`Message hash does not exist on L2. Message hash: ${messageHash}`); throw makeBaseError(`Message hash does not exist on L2. Message hash: ${messageHash}`);
} }
const [l2MessagingBlockAnchoredEvent] = await this.lineaRollupLogClient.getL2MessagingBlockAnchoredEvents({ const [l2MessagingBlockAnchoredEvent] = await this.lineaRollupLogClient.getL2MessagingBlockAnchoredEvents({
@@ -62,7 +62,7 @@ export class MerkleTreeService implements IMerkleTreeService {
}); });
if (!l2MessagingBlockAnchoredEvent) { if (!l2MessagingBlockAnchoredEvent) {
throw new BaseError(`L2 block number ${messageEvent.blockNumber} has not been finalized on L1.`); throw makeBaseError(`L2 block number ${messageEvent.blockNumber} has not been finalized on L1.`);
} }
const finalizationInfo = await this.getFinalizationMessagingInfo(l2MessagingBlockAnchoredEvent.transactionHash); const finalizationInfo = await this.getFinalizationMessagingInfo(l2MessagingBlockAnchoredEvent.transactionHash);
@@ -78,7 +78,7 @@ export class MerkleTreeService implements IMerkleTreeService {
const tree = merkleTreeFactory.createAndAddLeaves(l2messages); const tree = merkleTreeFactory.createAndAddLeaves(l2messages);
if (!finalizationInfo.l2MerkleRoots.includes(tree.getRoot())) { if (!finalizationInfo.l2MerkleRoots.includes(tree.getRoot())) {
throw new BaseError("Merkle tree build failed."); throw makeBaseError("Merkle tree build failed.");
} }
return tree.getProof(l2messages.indexOf(messageHash)); return tree.getProof(l2messages.indexOf(messageHash));
@@ -93,7 +93,7 @@ export class MerkleTreeService implements IMerkleTreeService {
const receipt = await this.provider.getTransactionReceipt(transactionHash); const receipt = await this.provider.getTransactionReceipt(transactionHash);
if (!receipt || receipt.logs.length === 0) { if (!receipt || receipt.logs.length === 0) {
throw new BaseError(`Transaction does not exist or no logs found in this transaction: ${transactionHash}.`); throw makeBaseError(`Transaction does not exist or no logs found in this transaction: ${transactionHash}.`);
} }
let treeDepth = 0; let treeDepth = 0;
@@ -114,11 +114,11 @@ export class MerkleTreeService implements IMerkleTreeService {
} }
if (l2MerkleRoots.length === 0) { if (l2MerkleRoots.length === 0) {
throw new BaseError(`No L2MerkleRootAdded events found in this transaction.`); throw makeBaseError(`No L2MerkleRootAdded events found in this transaction.`);
} }
if (blocksNumber.length === 0) { if (blocksNumber.length === 0) {
throw new BaseError(`No L2MessagingBlocksAnchored events found in this transaction.`); throw makeBaseError(`No L2MessagingBlocksAnchored events found in this transaction.`);
} }
return { return {
@@ -141,7 +141,7 @@ export class MerkleTreeService implements IMerkleTreeService {
const events = await this.l2MessageServiceLogClient.getMessageSentEventsByBlockRange(fromBlock, toBlock); const events = await this.l2MessageServiceLogClient.getMessageSentEventsByBlockRange(fromBlock, toBlock);
if (events.length === 0) { if (events.length === 0) {
throw new BaseError(`No MessageSent events found in this block range on L2.`); throw makeBaseError(`No MessageSent events found in this block range on L2.`);
} }
return events.map((event) => event.messageHash); return events.map((event) => event.messageHash);
@@ -161,7 +161,7 @@ export class MerkleTreeService implements IMerkleTreeService {
const messageHashIndex = messageHashes.indexOf(messageHash); const messageHashIndex = messageHashes.indexOf(messageHash);
if (messageHashIndex === -1) { if (messageHashIndex === -1) {
throw new BaseError("Message hash not found in messages."); throw makeBaseError("Message hash not found in messages.");
} }
const start = Math.floor(messageHashIndex / numberOfMessagesInTrees) * numberOfMessagesInTrees; const start = Math.floor(messageHashIndex / numberOfMessagesInTrees) * numberOfMessagesInTrees;

View File

@@ -30,8 +30,7 @@ import {
import { LineaRollupClient } from "../LineaRollupClient"; import { LineaRollupClient } from "../LineaRollupClient";
import { ZERO_ADDRESS } from "../../../core/constants"; import { ZERO_ADDRESS } from "../../../core/constants";
import { OnChainMessageStatus } from "../../../core/enums/message"; import { OnChainMessageStatus } from "../../../core/enums/message";
import { GasEstimationError } from "../../../core/errors/GasFeeErrors"; import { BaseError, makeBaseError } from "../../../core/errors";
import { BaseError } from "../../../core/errors";
import { EthersL2MessageServiceLogClient } from "../../linea/EthersL2MessageServiceLogClient"; import { EthersL2MessageServiceLogClient } from "../../linea/EthersL2MessageServiceLogClient";
import { EthersLineaRollupLogClient } from "../EthersLineaRollupLogClient"; import { EthersLineaRollupLogClient } from "../EthersLineaRollupLogClient";
import { DefaultGasProvider } from "../../gas/DefaultGasProvider"; import { DefaultGasProvider } from "../../gas/DefaultGasProvider";
@@ -231,7 +230,7 @@ describe("TestLineaRollupClient", () => {
jest.spyOn(gasFeeProvider, "getGasFees").mockRejectedValue(new Error("Gas fees estimation failed").message); jest.spyOn(gasFeeProvider, "getGasFees").mockRejectedValue(new Error("Gas fees estimation failed").message);
await expect(lineaRollupClient.estimateClaimWithoutProofGas(message)).rejects.toThrow( await expect(lineaRollupClient.estimateClaimWithoutProofGas(message)).rejects.toThrow(
new GasEstimationError("Gas fees estimation failed", message), makeBaseError("Gas fees estimation failed", message),
); );
}); });

View File

@@ -1,8 +1,8 @@
import { Block, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers"; import { Block, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { FeeEstimationError } from "../../core/errors";
import { DefaultGasProviderConfig, FeeHistory, GasFees, IEthereumGasProvider } from "../../core/clients/IGasProvider"; import { DefaultGasProviderConfig, FeeHistory, GasFees, IEthereumGasProvider } from "../../core/clients/IGasProvider";
import { IProvider } from "../../core/clients/IProvider"; import { IProvider } from "../../core/clients/IProvider";
import { BrowserProvider, LineaBrowserProvider, LineaProvider, Provider } from "../providers"; import { BrowserProvider, LineaBrowserProvider, LineaProvider, Provider } from "../providers";
import { makeBaseError } from "../../core/errors/utils";
export class DefaultGasProvider implements IEthereumGasProvider<TransactionRequest> { export class DefaultGasProvider implements IEthereumGasProvider<TransactionRequest> {
private gasFeesCache: GasFees; private gasFeesCache: GasFees;
@@ -62,7 +62,7 @@ export class DefaultGasProvider implements IEthereumGasProvider<TransactionReque
const maxPriorityFeePerGas = this.calculateMaxPriorityFee(feeHistory.reward); const maxPriorityFeePerGas = this.calculateMaxPriorityFee(feeHistory.reward);
if (maxPriorityFeePerGas > this.config.maxFeePerGasCap) { if (maxPriorityFeePerGas > this.config.maxFeePerGasCap) {
throw new FeeEstimationError( throw makeBaseError(
`Estimated miner tip of ${maxPriorityFeePerGas} exceeds configured max fee per gas of ${this.config.maxFeePerGasCap}!`, `Estimated miner tip of ${maxPriorityFeePerGas} exceeds configured max fee per gas of ${this.config.maxFeePerGasCap}!`,
); );
} }

View File

@@ -4,8 +4,8 @@ import { LineaGasProvider } from "./LineaGasProvider";
import { IProvider } from "../../core/clients/IProvider"; import { IProvider } from "../../core/clients/IProvider";
import { GasFees, GasProviderConfig, IGasProvider, LineaGasFees } from "../../core/clients/IGasProvider"; import { GasFees, GasProviderConfig, IGasProvider, LineaGasFees } from "../../core/clients/IGasProvider";
import { Direction } from "../../core/enums"; import { Direction } from "../../core/enums";
import { BaseError } from "../../core/errors";
import { BrowserProvider, LineaBrowserProvider, LineaProvider, Provider } from "../providers"; import { BrowserProvider, LineaBrowserProvider, LineaProvider, Provider } from "../providers";
import { makeBaseError } from "../../core/errors/utils";
export class GasProvider implements IGasProvider<TransactionRequest> { export class GasProvider implements IGasProvider<TransactionRequest> {
private defaultGasProvider: DefaultGasProvider; private defaultGasProvider: DefaultGasProvider;
@@ -50,7 +50,7 @@ export class GasProvider implements IGasProvider<TransactionRequest> {
if (this.config.direction === Direction.L1_TO_L2) { if (this.config.direction === Direction.L1_TO_L2) {
if (this.config.enableLineaEstimateGas) { if (this.config.enableLineaEstimateGas) {
if (!transactionRequest) { if (!transactionRequest) {
throw new BaseError( throw makeBaseError(
"You need to provide transaction request as param to call the getGasFees function on Linea.", "You need to provide transaction request as param to call the getGasFees function on Linea.",
); );
} }

View File

@@ -1,9 +1,9 @@
import { describe, afterEach, jest, it, expect, beforeEach } from "@jest/globals"; import { describe, afterEach, jest, it, expect, beforeEach } from "@jest/globals";
import { MockProxy, mock, mockClear } from "jest-mock-extended"; import { MockProxy, mock, mockClear } from "jest-mock-extended";
import { DefaultGasProvider } from "../DefaultGasProvider"; import { DefaultGasProvider } from "../DefaultGasProvider";
import { FeeEstimationError } from "../../../core/errors/GasFeeErrors";
import { Provider } from "../../providers/provider"; import { Provider } from "../../providers/provider";
import { DEFAULT_GAS_ESTIMATION_PERCENTILE } from "../../../core/constants"; import { DEFAULT_GAS_ESTIMATION_PERCENTILE } from "../../../core/constants";
import { makeBaseError } from "../../../core/errors";
const MAX_FEE_PER_GAS = 100_000_000n; const MAX_FEE_PER_GAS = 100_000_000n;
@@ -48,7 +48,9 @@ describe("DefaultGasProvider", () => {
["0x3b9aca00", "0x59682f00"], ["0x3b9aca00", "0x59682f00"],
], ],
}); });
await expect(eip1559GasProvider.getGasFees()).rejects.toThrow(FeeEstimationError); await expect(eip1559GasProvider.getGasFees()).rejects.toThrow(
makeBaseError(`Estimated miner tip of ${1271935510} exceeds configured max fee per gas of ${MAX_FEE_PER_GAS}!`),
);
expect(sendSpy).toHaveBeenCalledTimes(1); expect(sendSpy).toHaveBeenCalledTimes(1);
}); });

View File

@@ -9,7 +9,6 @@ import {
ErrorDescription, ErrorDescription,
} from "ethers"; } from "ethers";
import { L2MessageService, L2MessageService__factory } from "../../contracts/typechain"; import { L2MessageService, L2MessageService__factory } from "../../contracts/typechain";
import { GasEstimationError, BaseError } from "../../core/errors";
import { Message, SDKMode, MessageSent } from "../../core/types"; import { Message, SDKMode, MessageSent } from "../../core/types";
import { OnChainMessageStatus } from "../../core/enums"; import { OnChainMessageStatus } from "../../core/enums";
import { IL2MessageServiceClient, ILineaProvider } from "../../core/clients/linea"; import { IL2MessageServiceClient, ILineaProvider } from "../../core/clients/linea";
@@ -18,6 +17,7 @@ import { formatMessageStatus, isString } from "../../core/utils";
import { IGasProvider, LineaGasFees } from "../../core/clients/IGasProvider"; import { IGasProvider, LineaGasFees } from "../../core/clients/IGasProvider";
import { IMessageRetriever } from "../../core/clients/IMessageRetriever"; import { IMessageRetriever } from "../../core/clients/IMessageRetriever";
import { LineaBrowserProvider, LineaProvider } from "../providers"; import { LineaBrowserProvider, LineaProvider } from "../providers";
import { makeBaseError } from "../../core/errors/utils";
export class L2MessageServiceClient export class L2MessageServiceClient
implements implements
@@ -111,7 +111,7 @@ export class L2MessageServiceClient
} }
if (!signer) { if (!signer) {
throw new BaseError("Please provide a signer."); throw makeBaseError("Please provide a signer.");
} }
return L2MessageService__factory.connect(contractAddress, signer); return L2MessageService__factory.connect(contractAddress, signer);
@@ -141,7 +141,7 @@ export class L2MessageServiceClient
overrides: Overrides = {}, overrides: Overrides = {},
): Promise<LineaGasFees> { ): Promise<LineaGasFees> {
if (this.mode === "read-only") { if (this.mode === "read-only") {
throw new BaseError("'EstimateClaimGasFees' function not callable using readOnly mode."); throw makeBaseError("'EstimateClaimGasFees' function not callable using readOnly mode.");
} }
try { try {
@@ -155,9 +155,8 @@ export class L2MessageServiceClient
data: transactionData, data: transactionData,
...overrides, ...overrides,
})) as LineaGasFees; })) as LineaGasFees;
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) {
} catch (e: any) { throw makeBaseError(e, message);
throw new GasEstimationError(e, message);
} }
} }
@@ -173,7 +172,7 @@ export class L2MessageServiceClient
overrides: Overrides = {}, overrides: Overrides = {},
): Promise<ContractTransactionResponse> { ): Promise<ContractTransactionResponse> {
if (this.mode === "read-only") { if (this.mode === "read-only") {
throw new BaseError("'claim' function not callable using readOnly mode."); throw makeBaseError("'claim' function not callable using readOnly mode.");
} }
const { messageSender, destination, fee, value, calldata, messageNonce, feeRecipient } = message; const { messageSender, destination, fee, value, calldata, messageNonce, feeRecipient } = message;
@@ -205,17 +204,17 @@ export class L2MessageServiceClient
priceBumpPercent: number = 10, priceBumpPercent: number = 10,
): Promise<TransactionResponse> { ): Promise<TransactionResponse> {
if (!Number.isInteger(priceBumpPercent)) { if (!Number.isInteger(priceBumpPercent)) {
throw new Error("'priceBumpPercent' must be an integer"); throw makeBaseError("'priceBumpPercent' must be an integer");
} }
if (this.mode === "read-only") { if (this.mode === "read-only") {
throw new BaseError("'retryTransactionWithHigherFee' function not callable using readOnly mode."); throw makeBaseError("'retryTransactionWithHigherFee' function not callable using readOnly mode.");
} }
const transaction = await this.provider.getTransaction(transactionHash); const transaction = await this.provider.getTransaction(transactionHash);
if (!transaction) { if (!transaction) {
throw new BaseError(`Transaction with hash ${transactionHash} not found.`); throw makeBaseError(`Transaction with hash ${transactionHash} not found.`);
} }
let maxPriorityFeePerGas; let maxPriorityFeePerGas;

View File

@@ -19,8 +19,7 @@ import {
import { L2MessageServiceClient } from "../L2MessageServiceClient"; import { L2MessageServiceClient } from "../L2MessageServiceClient";
import { ZERO_ADDRESS } from "../../../core/constants"; import { ZERO_ADDRESS } from "../../../core/constants";
import { OnChainMessageStatus } from "../../../core/enums/message"; import { OnChainMessageStatus } from "../../../core/enums/message";
import { GasEstimationError } from "../../../core/errors/GasFeeErrors"; import { BaseError, makeBaseError } from "../../../core/errors";
import { BaseError } from "../../../core/errors";
import { LineaProvider } from "../../providers"; import { LineaProvider } from "../../providers";
import { GasProvider } from "../../gas"; import { GasProvider } from "../../gas";
@@ -107,7 +106,7 @@ describe("TestL2MessageServiceClient", () => {
jest.spyOn(gasFeeProvider, "getGasFees").mockRejectedValue(new Error("Gas fees estimation failed").message); jest.spyOn(gasFeeProvider, "getGasFees").mockRejectedValue(new Error("Gas fees estimation failed").message);
await expect(l2MessageServiceClient.estimateClaimGasFees(message)).rejects.toThrow( await expect(l2MessageServiceClient.estimateClaimGasFees(message)).rejects.toThrow(
new GasEstimationError("Gas fees estimation failed", message), makeBaseError("Gas fees estimation failed", message),
); );
}); });

View File

@@ -1,7 +1,7 @@
import { BlockTag, dataSlice, ethers, toNumber } from "ethers"; import { BlockTag, dataSlice, ethers, toNumber } from "ethers";
import { BlockExtraData } from "../../core/clients/linea"; import { BlockExtraData } from "../../core/clients/linea";
import { GasFees } from "../../core/clients/IGasProvider"; import { GasFees } from "../../core/clients/IGasProvider";
import { BaseError } from "../../core/errors"; import { makeBaseError } from "../../core/errors/utils";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = new (...args: any[]) => T; type Constructor<T = object> = new (...args: any[]) => T;
@@ -21,7 +21,7 @@ function LineaProviderMixIn<TBase extends Constructor<ethers.Provider>>(Base: TB
const { maxPriorityFeePerGas, maxFeePerGas } = await this.getFeeData(); const { maxPriorityFeePerGas, maxFeePerGas } = await this.getFeeData();
if (!maxPriorityFeePerGas || !maxFeePerGas) { if (!maxPriorityFeePerGas || !maxFeePerGas) {
throw new BaseError("Error getting fee data"); throw makeBaseError("Error getting fee data");
} }
return { maxPriorityFeePerGas, maxFeePerGas }; return { maxPriorityFeePerGas, maxFeePerGas };

View File

@@ -1,6 +1,6 @@
import { ethers } from "ethers"; import { ethers } from "ethers";
import { BaseError } from "../../core/errors";
import { GasFees } from "../../core/clients/IGasProvider"; import { GasFees } from "../../core/clients/IGasProvider";
import { makeBaseError } from "../../core/errors/utils";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = new (...args: any[]) => T; type Constructor<T = object> = new (...args: any[]) => T;
@@ -22,7 +22,7 @@ export function ProviderMixIn<TBase extends Constructor<ethers.Provider>>(Base:
const { maxPriorityFeePerGas, maxFeePerGas } = await this.getFeeData(); const { maxPriorityFeePerGas, maxFeePerGas } = await this.getFeeData();
if (!maxPriorityFeePerGas || !maxFeePerGas) { if (!maxPriorityFeePerGas || !maxFeePerGas) {
throw new BaseError("Error getting fee data"); throw makeBaseError("Error getting fee data");
} }
return { maxPriorityFeePerGas, maxFeePerGas }; return { maxPriorityFeePerGas, maxFeePerGas };

View File

@@ -1,11 +1,35 @@
export class BaseError extends Error { export type InferErrorType<T> = T extends Error
reason?: BaseError | Error | string; ? Error
: T extends string
? string
: T extends number
? number
: T extends { message: string }
? { message: string }
: unknown;
override name = "LineaSDKCoreError"; export class BaseError<T = unknown, M = unknown> extends Error {
stack: string = "";
metadata?: M;
error: InferErrorType<T>;
constructor(message?: string) { constructor(error: T, metadata?: M) {
super(); const message = BaseError.getMessage(error);
this.message = message || "An error occurred."; super(message);
Error.captureStackTrace(this, this.constructor); this.stack = error instanceof Error && error.stack ? error.stack : message;
this.metadata = metadata;
this.error = error as InferErrorType<T>;
Object.setPrototypeOf(this, BaseError.prototype);
}
private static getMessage(error: unknown): string {
if (typeof error === "string") return error;
if (typeof error === "number") return `Error Code: ${error}`;
if (error instanceof Error) return error.message;
if (typeof error === "object" && error !== null && "message" in error) {
return String(error.message);
}
return "Unknown error";
} }
} }

View File

@@ -1,16 +0,0 @@
import { Message } from "../types";
import { BaseError } from "./BaseError";
export class FeeEstimationError extends BaseError {
override name = FeeEstimationError.name;
}
export class GasEstimationError<T extends Message> extends BaseError {
override name = GasEstimationError.name;
public rejectedMessage?: T;
constructor(message: string, rejectedMessage?: T) {
super(message);
this.rejectedMessage = rejectedMessage;
}
}

View File

@@ -1,14 +1,8 @@
import { describe, it } from "@jest/globals"; import { describe, it } from "@jest/globals";
import { BaseError } from "../BaseError"; import { makeBaseError } from "../utils";
import { serialize } from "../../utils/serialize";
describe("BaseError", () => { describe("BaseError", () => {
it("Should log error message when we only pass a short message", () => { it("Should log error message when we only pass a short message", () => {
expect(serialize(new BaseError("An error message."))).toStrictEqual( expect(makeBaseError("An error message.").message).toStrictEqual("An error message.");
serialize({
name: "LineaSDKCoreError",
message: "An error message.",
}),
);
}); });
}); });

View File

@@ -1,50 +0,0 @@
import { describe, it } from "@jest/globals";
import { FeeEstimationError, GasEstimationError } from "../GasFeeErrors";
import { serialize } from "../../utils/serialize";
import { Message } from "../../types/message";
import { ZERO_ADDRESS, ZERO_HASH } from "../../constants";
describe("BaseError", () => {
describe("FeeEstimationError", () => {
it("Should log error message", () => {
expect(serialize(new FeeEstimationError("An error message."))).toStrictEqual(
serialize({
name: "FeeEstimationError",
message: "An error message.",
}),
);
});
});
describe("GasEstimationError", () => {
it("Should log error message", () => {
const rejectedMessage: Message = {
messageHash: ZERO_HASH,
messageSender: ZERO_ADDRESS,
destination: ZERO_ADDRESS,
fee: 0n,
value: 0n,
messageNonce: 0n,
calldata: "0x",
};
const estimationError = new Error("estimation error");
expect(serialize(new GasEstimationError(estimationError.message, rejectedMessage))).toStrictEqual(
serialize({
name: "GasEstimationError",
message: "estimation error",
rejectedMessage: {
messageHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
messageSender: "0x0000000000000000000000000000000000000000",
destination: "0x0000000000000000000000000000000000000000",
fee: 0n,
value: 0n,
messageNonce: 0n,
calldata: "0x",
},
}),
);
});
});
});

View File

@@ -1,2 +1,2 @@
export { BaseError } from "./BaseError"; export { BaseError } from "./BaseError";
export { GasEstimationError, FeeEstimationError } from "./GasFeeErrors"; export { makeBaseError, isBaseError } from "./utils";

View File

@@ -0,0 +1,35 @@
import { isNativeError } from "util/types";
import { BaseError, InferErrorType } from "./BaseError";
/**
* Converts an `unknown` value that was thrown into a `BaseError` object.
*
* @param value - An `unknown` value.
*
* @returns A `BaseError` object.
*/
export const makeBaseError = <E, M>(value: E extends BaseError<E, M> ? never : E, metadata?: M): BaseError<E, M> => {
if (isNativeError(value)) {
return new BaseError(value, metadata);
} else {
try {
return new BaseError(
new Error(`${typeof value === "object" ? JSON.stringify(value) : String(value)}`),
metadata,
) as BaseError<E, M>;
} catch {
return new BaseError(new Error(`Unexpected value thrown: non-stringifiable object`), metadata) as BaseError<E, M>;
}
}
};
/**
* Type guard to check if an `unknown` value is a `BaseError` object.
*
* @param value - The value to check.
*
* @returns `true` if the value is a `BaseError` object, otherwise `false`.
*/
export const isBaseError = <E, M>(value: unknown): value is BaseError<InferErrorType<E>, M> => {
return value instanceof BaseError && typeof value.stack === "string" && "metadata" in value && "error" in value;
};

View File

@@ -25,7 +25,7 @@ export { Provider, BrowserProvider, LineaProvider, LineaBrowserProvider } from "
export { DefaultGasProvider, GasProvider, LineaGasProvider } from "./clients/gas"; export { DefaultGasProvider, GasProvider, LineaGasProvider } from "./clients/gas";
// Core errors // Core errors
export { GasEstimationError, FeeEstimationError } from "./core/errors"; export { makeBaseError, isBaseError } from "./core/errors";
// Contracts types and factories (generated from typechain) // Contracts types and factories (generated from typechain)
export { LineaRollup, LineaRollup__factory, L2MessageService, L2MessageService__factory } from "./contracts/typechain"; export { LineaRollup, LineaRollup__factory, L2MessageService, L2MessageService__factory } from "./contracts/typechain";

View File

@@ -1,5 +1,5 @@
import { ethers } from "ethers"; import { ethers } from "ethers";
import { BaseError } from "../../core/errors"; import { makeBaseError } from "../../core/errors";
import { ZERO_HASH } from "../../core/constants"; import { ZERO_HASH } from "../../core/constants";
import { Proof } from "../../core/clients/ethereum"; import { Proof } from "../../core/clients/ethereum";
@@ -27,7 +27,7 @@ export class SparseMerkleTree {
*/ */
constructor(depth: number) { constructor(depth: number) {
if (depth <= 1) { if (depth <= 1) {
throw new BaseError("Merkle tree depth must be greater than 1"); throw makeBaseError("Merkle tree depth must be greater than 1");
} }
this.depth = depth; this.depth = depth;
this.emptyLeaves = this.generateEmptyLeaves(this.depth); this.emptyLeaves = this.generateEmptyLeaves(this.depth);
@@ -53,14 +53,14 @@ export class SparseMerkleTree {
*/ */
public getProof(key: number): Proof { public getProof(key: number): Proof {
if (key < 0 || key >= Math.pow(2, this.depth)) { if (key < 0 || key >= Math.pow(2, this.depth)) {
throw new BaseError(`Leaf index is out of range`); throw makeBaseError(`Leaf index is out of range`);
} }
const binaryKey = this.keyToBinary(key); const binaryKey = this.keyToBinary(key);
const leaf = this.getLeaf(key); const leaf = this.getLeaf(key);
if (leaf === this.emptyLeaves[0]) { if (leaf === this.emptyLeaves[0]) {
throw new BaseError(`Leaf does not exist`); throw makeBaseError(`Leaf does not exist`);
} }
return { return {
@@ -78,7 +78,7 @@ export class SparseMerkleTree {
*/ */
public getLeaf(key: number): string { public getLeaf(key: number): string {
if (key < 0 || key >= Math.pow(2, this.depth)) { if (key < 0 || key >= Math.pow(2, this.depth)) {
throw new BaseError("Leaf index is out of range"); throw makeBaseError("Leaf index is out of range");
} }
const binaryKey = this.keyToBinary(key); const binaryKey = this.keyToBinary(key);
return this.getLeafHelper(this.root, binaryKey, 0); return this.getLeafHelper(this.root, binaryKey, 0);