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

@@ -20,11 +20,11 @@ import {
DEFAULT_L2_MESSAGE_TREE_DEPTH,
DEFAULT_MAX_FEE_PER_GAS_CAP,
} from "./core/constants";
import { BaseError } from "./core/errors";
import { L1FeeEstimatorOptions, L2FeeEstimatorOptions, LineaSDKOptions, Network, SDKMode } from "./core/types";
import { NETWORKS } from "./core/constants";
import { isString } from "./core/utils";
import { Direction } from "./core/enums";
import { makeBaseError } from "./core/errors/utils";
export class LineaSDK {
private network: Network;
@@ -64,7 +64,7 @@ export class LineaSDK {
const { l1SignerPrivateKeyOrWallet, l2SignerPrivateKeyOrWallet } = options;
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);
@@ -108,7 +108,7 @@ export class LineaSDK {
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 {
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;
}
@@ -130,7 +130,7 @@ export class LineaSDK {
*/
public getL2Signer(): Signer {
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;
}
@@ -176,7 +176,7 @@ export class LineaSDK {
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 {
if (this.network === "custom") {
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;
} else {
const contractAddress = NETWORKS[this.network][`${contractType}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;
}
@@ -308,7 +308,7 @@ export class LineaSDK {
return privateKeyOrWallet instanceof Wallet ? privateKeyOrWallet : new Wallet(privateKeyOrWallet);
} catch (e) {
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;
}

View File

@@ -11,8 +11,8 @@ import { Cache } from "../../utils/Cache";
import { ILineaRollupClient } from "../../core/clients/ethereum";
import { IL2MessageServiceClient, IL2MessageServiceLogClient } from "../../core/clients/linea";
import { MessageSent, Network } from "../../core/types";
import { BaseError } from "../../core/errors";
import { FinalizationMessagingInfo, Proof } from "../../core/clients/ethereum/IMerkleTreeService";
import { makeBaseError } from "../../core/errors/utils";
export class L1ClaimingService {
private cache: Cache;
@@ -93,7 +93,7 @@ export class L1ClaimingService {
});
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) {

View File

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

View File

@@ -1,6 +1,5 @@
import { Block, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { SparseMerkleTreeFactory } from "../../utils/merkleTree/MerkleTreeFactory";
import { BaseError } from "../../core/errors";
import {
ILineaRollupLogClient,
FinalizationMessagingInfo,
@@ -16,6 +15,7 @@ import {
import { LineaRollup, LineaRollup__factory } from "../../contracts/typechain";
import { IProvider } from "../../core/clients/IProvider";
import { BrowserProvider, Provider } from "../providers";
import { makeBaseError } from "../../core/errors/utils";
export class MerkleTreeService implements IMerkleTreeService {
private readonly contract: LineaRollup;
@@ -54,7 +54,7 @@ export class MerkleTreeService implements IMerkleTreeService {
const [messageEvent] = await this.l2MessageServiceLogClient.getMessageSentEventsByMessageHash({ messageHash });
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({
@@ -62,7 +62,7 @@ export class MerkleTreeService implements IMerkleTreeService {
});
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);
@@ -78,7 +78,7 @@ export class MerkleTreeService implements IMerkleTreeService {
const tree = merkleTreeFactory.createAndAddLeaves(l2messages);
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));
@@ -93,7 +93,7 @@ export class MerkleTreeService implements IMerkleTreeService {
const receipt = await this.provider.getTransactionReceipt(transactionHash);
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;
@@ -114,11 +114,11 @@ export class MerkleTreeService implements IMerkleTreeService {
}
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) {
throw new BaseError(`No L2MessagingBlocksAnchored events found in this transaction.`);
throw makeBaseError(`No L2MessagingBlocksAnchored events found in this transaction.`);
}
return {
@@ -141,7 +141,7 @@ export class MerkleTreeService implements IMerkleTreeService {
const events = await this.l2MessageServiceLogClient.getMessageSentEventsByBlockRange(fromBlock, toBlock);
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);
@@ -161,7 +161,7 @@ export class MerkleTreeService implements IMerkleTreeService {
const messageHashIndex = messageHashes.indexOf(messageHash);
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;

View File

@@ -30,8 +30,7 @@ import {
import { LineaRollupClient } from "../LineaRollupClient";
import { ZERO_ADDRESS } from "../../../core/constants";
import { OnChainMessageStatus } from "../../../core/enums/message";
import { GasEstimationError } from "../../../core/errors/GasFeeErrors";
import { BaseError } from "../../../core/errors";
import { BaseError, makeBaseError } from "../../../core/errors";
import { EthersL2MessageServiceLogClient } from "../../linea/EthersL2MessageServiceLogClient";
import { EthersLineaRollupLogClient } from "../EthersLineaRollupLogClient";
import { DefaultGasProvider } from "../../gas/DefaultGasProvider";
@@ -231,7 +230,7 @@ describe("TestLineaRollupClient", () => {
jest.spyOn(gasFeeProvider, "getGasFees").mockRejectedValue(new Error("Gas fees estimation failed").message);
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 { FeeEstimationError } from "../../core/errors";
import { DefaultGasProviderConfig, FeeHistory, GasFees, IEthereumGasProvider } from "../../core/clients/IGasProvider";
import { IProvider } from "../../core/clients/IProvider";
import { BrowserProvider, LineaBrowserProvider, LineaProvider, Provider } from "../providers";
import { makeBaseError } from "../../core/errors/utils";
export class DefaultGasProvider implements IEthereumGasProvider<TransactionRequest> {
private gasFeesCache: GasFees;
@@ -62,7 +62,7 @@ export class DefaultGasProvider implements IEthereumGasProvider<TransactionReque
const maxPriorityFeePerGas = this.calculateMaxPriorityFee(feeHistory.reward);
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}!`,
);
}

View File

@@ -4,8 +4,8 @@ import { LineaGasProvider } from "./LineaGasProvider";
import { IProvider } from "../../core/clients/IProvider";
import { GasFees, GasProviderConfig, IGasProvider, LineaGasFees } from "../../core/clients/IGasProvider";
import { Direction } from "../../core/enums";
import { BaseError } from "../../core/errors";
import { BrowserProvider, LineaBrowserProvider, LineaProvider, Provider } from "../providers";
import { makeBaseError } from "../../core/errors/utils";
export class GasProvider implements IGasProvider<TransactionRequest> {
private defaultGasProvider: DefaultGasProvider;
@@ -50,7 +50,7 @@ export class GasProvider implements IGasProvider<TransactionRequest> {
if (this.config.direction === Direction.L1_TO_L2) {
if (this.config.enableLineaEstimateGas) {
if (!transactionRequest) {
throw new BaseError(
throw makeBaseError(
"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 { MockProxy, mock, mockClear } from "jest-mock-extended";
import { DefaultGasProvider } from "../DefaultGasProvider";
import { FeeEstimationError } from "../../../core/errors/GasFeeErrors";
import { Provider } from "../../providers/provider";
import { DEFAULT_GAS_ESTIMATION_PERCENTILE } from "../../../core/constants";
import { makeBaseError } from "../../../core/errors";
const MAX_FEE_PER_GAS = 100_000_000n;
@@ -48,7 +48,9 @@ describe("DefaultGasProvider", () => {
["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);
});

View File

@@ -9,7 +9,6 @@ import {
ErrorDescription,
} from "ethers";
import { L2MessageService, L2MessageService__factory } from "../../contracts/typechain";
import { GasEstimationError, BaseError } from "../../core/errors";
import { Message, SDKMode, MessageSent } from "../../core/types";
import { OnChainMessageStatus } from "../../core/enums";
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 { IMessageRetriever } from "../../core/clients/IMessageRetriever";
import { LineaBrowserProvider, LineaProvider } from "../providers";
import { makeBaseError } from "../../core/errors/utils";
export class L2MessageServiceClient
implements
@@ -111,7 +111,7 @@ export class L2MessageServiceClient
}
if (!signer) {
throw new BaseError("Please provide a signer.");
throw makeBaseError("Please provide a signer.");
}
return L2MessageService__factory.connect(contractAddress, signer);
@@ -141,7 +141,7 @@ export class L2MessageServiceClient
overrides: Overrides = {},
): Promise<LineaGasFees> {
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 {
@@ -155,9 +155,8 @@ export class L2MessageServiceClient
data: transactionData,
...overrides,
})) as LineaGasFees;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
throw new GasEstimationError(e, message);
} catch (e) {
throw makeBaseError(e, message);
}
}
@@ -173,7 +172,7 @@ export class L2MessageServiceClient
overrides: Overrides = {},
): Promise<ContractTransactionResponse> {
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;
@@ -205,17 +204,17 @@ export class L2MessageServiceClient
priceBumpPercent: number = 10,
): Promise<TransactionResponse> {
if (!Number.isInteger(priceBumpPercent)) {
throw new Error("'priceBumpPercent' must be an integer");
throw makeBaseError("'priceBumpPercent' must be an integer");
}
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);
if (!transaction) {
throw new BaseError(`Transaction with hash ${transactionHash} not found.`);
throw makeBaseError(`Transaction with hash ${transactionHash} not found.`);
}
let maxPriorityFeePerGas;

View File

@@ -19,8 +19,7 @@ import {
import { L2MessageServiceClient } from "../L2MessageServiceClient";
import { ZERO_ADDRESS } from "../../../core/constants";
import { OnChainMessageStatus } from "../../../core/enums/message";
import { GasEstimationError } from "../../../core/errors/GasFeeErrors";
import { BaseError } from "../../../core/errors";
import { BaseError, makeBaseError } from "../../../core/errors";
import { LineaProvider } from "../../providers";
import { GasProvider } from "../../gas";
@@ -107,7 +106,7 @@ describe("TestL2MessageServiceClient", () => {
jest.spyOn(gasFeeProvider, "getGasFees").mockRejectedValue(new Error("Gas fees estimation failed").message);
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 { BlockExtraData } from "../../core/clients/linea";
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
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();
if (!maxPriorityFeePerGas || !maxFeePerGas) {
throw new BaseError("Error getting fee data");
throw makeBaseError("Error getting fee data");
}
return { maxPriorityFeePerGas, maxFeePerGas };

View File

@@ -1,6 +1,6 @@
import { ethers } from "ethers";
import { BaseError } from "../../core/errors";
import { GasFees } from "../../core/clients/IGasProvider";
import { makeBaseError } from "../../core/errors/utils";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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();
if (!maxPriorityFeePerGas || !maxFeePerGas) {
throw new BaseError("Error getting fee data");
throw makeBaseError("Error getting fee data");
}
return { maxPriorityFeePerGas, maxFeePerGas };

View File

@@ -1,11 +1,35 @@
export class BaseError extends Error {
reason?: BaseError | Error | string;
export type InferErrorType<T> = T extends Error
? 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) {
super();
this.message = message || "An error occurred.";
Error.captureStackTrace(this, this.constructor);
constructor(error: T, metadata?: M) {
const message = BaseError.getMessage(error);
super(message);
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 { BaseError } from "../BaseError";
import { serialize } from "../../utils/serialize";
import { makeBaseError } from "../utils";
describe("BaseError", () => {
it("Should log error message when we only pass a short message", () => {
expect(serialize(new BaseError("An error message."))).toStrictEqual(
serialize({
name: "LineaSDKCoreError",
message: "An error message.",
}),
);
expect(makeBaseError("An error message.").message).toStrictEqual("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 { 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";
// Core errors
export { GasEstimationError, FeeEstimationError } from "./core/errors";
export { makeBaseError, isBaseError } from "./core/errors";
// Contracts types and factories (generated from typechain)
export { LineaRollup, LineaRollup__factory, L2MessageService, L2MessageService__factory } from "./contracts/typechain";

View File

@@ -1,5 +1,5 @@
import { ethers } from "ethers";
import { BaseError } from "../../core/errors";
import { makeBaseError } from "../../core/errors";
import { ZERO_HASH } from "../../core/constants";
import { Proof } from "../../core/clients/ethereum";
@@ -27,7 +27,7 @@ export class SparseMerkleTree {
*/
constructor(depth: number) {
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.emptyLeaves = this.generateEmptyLeaves(this.depth);
@@ -53,14 +53,14 @@ export class SparseMerkleTree {
*/
public getProof(key: number): Proof {
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 leaf = this.getLeaf(key);
if (leaf === this.emptyLeaves[0]) {
throw new BaseError(`Leaf does not exist`);
throw makeBaseError(`Leaf does not exist`);
}
return {
@@ -78,7 +78,7 @@ export class SparseMerkleTree {
*/
public getLeaf(key: number): string {
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);
return this.getLeafHelper(this.root, binaryKey, 0);