Feat/272 split sdk and postman (#381)

* feat: split postman and sdk

* fix: update postman client and sendMessage script

* fix: clean the sdk

* fix: update sdk dependencies

* fix: remove .env.sample file

* fix: remove testing helpers from the build

* fix: update gas provider in linea sdk and update postman client

* fix: update postman dependencies

* fix: update postman dockerfile and fix tests imports and types

* fix: remove unused code in the sdk + move typechain folder

* fix: remove unused code + fix imports in postman

* fix: pnpm lock file issue

* fix: import issue

* fix: case sensitive file issue

* fix: update sdk fees options and update exports

* fix: remove postman unused code and adjust imports and tests

* fix: update contracts abis + clean error parsing

* fix: update postman based on new SDk changes

* add readme + remove unused interface in postman

* fix: rename Base.ts file to BaseError.ts

* fix: rename Base.ts file to BaseError.ts in postman

* chore: update readme for the postman

* fix: rename maxFeePerGas to maxFeePerGasCap

* fix: update DefaultGasProvider fees check

* fix: default gas provider test issue

* fix: update main ci filter

* fix: issue in default gas provider
This commit is contained in:
Victorien Gauch
2024-12-09 12:12:45 +01:00
committed by GitHub
parent 4e93fa3a10
commit f58c12a455
217 changed files with 7157 additions and 7230 deletions

View File

@@ -1,41 +0,0 @@
L1_RPC_URL=http://localhost:8445
L1_CONTRACT_ADDRESS=0x378041D4A8C8392cF26d5D86eB1E84A880b0E0F2
L1_SIGNER_PRIVATE_KEY=
L1_LISTENER_INTERVAL=4000
L1_LISTENER_INITIAL_FROM_BLOCK=0
L1_LISTENER_BLOCK_CONFIRMATION=4
L1_MAX_BLOCKS_TO_FETCH_LOGS=1000
L1_MAX_GAS_FEE_ENFORCED=false
L2_RPC_URL=http://localhost:8545
L2_CONTRACT_ADDRESS=0x5767aB2Ed64666bFE27e52D4675EDd60Ec7D6EDF
L2_SIGNER_PRIVATE_KEY=
L2_LISTENER_INTERVAL=4000
L2_LISTENER_INITIAL_FROM_BLOCK=0
L2_LISTENER_BLOCK_CONFIRMATION=0
L2_MAX_BLOCKS_TO_FETCH_LOGS=1000
L2_MESSAGE_TREE_DEPTH=5
L2_MAX_GAS_FEE_ENFORCED=false
MESSAGE_SUBMISSION_TIMEOUT=300000
MAX_FETCH_MESSAGES_FROM_DB=1000
MAX_NONCE_DIFF=10000
MAX_FEE_PER_GAS=100000000000
GAS_ESTIMATION_PERCENTILE=50
PROFIT_MARGIN=1.0
MAX_NUMBER_OF_RETRIES=100
RETRY_DELAY_IN_SECONDS=30
MAX_CLAIM_GAS_LIMIT=100000
MAX_TX_RETRIES=20
L1_L2_EOA_ENABLED=true
L1_L2_CALLDATA_ENABLED=false
L1_L2_AUTO_CLAIM_ENABLED=true
L2_L1_EOA_ENABLED=true
L2_L1_CALLDATA_ENABLED=false
L2_L1_AUTO_CLAIM_ENABLED=true
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postman_db
DB_CLEANER_ENABLED=true
DB_CLEANING_INTERVAL=10000
DB_DAYS_BEFORE_NOW_TO_DELETE=1

View File

@@ -1,42 +0,0 @@
FROM node:lts-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 ca-certificates bash curl make g++ \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
FROM base AS builder
WORKDIR /usr/src/app
ARG NATIVE_LIBS_RELEASE_TAG
ENV NATIVE_LIBS_RELEASE_TAG=${NATIVE_LIBS_RELEASE_TAG}
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json ./
COPY ./sdk/package.json ./sdk/package.json
COPY ./ts-libs/linea-native-libs/package.json ./ts-libs/linea-native-libs/package.json
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
COPY ./sdk ./sdk
COPY ts-libs/linea-native-libs ./ts-libs/linea-native-libs
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm run build \
&& pnpm deploy --filter=./sdk --prod ./prod/sdk
FROM node:lts-slim AS production
ENV NODE_ENV=production
WORKDIR /usr/src/app
USER node
COPY --from=builder /usr/src/app/prod/sdk ./sdk
CMD [ "node", "./sdk/dist/scripts/runPostman.js" ]

View File

@@ -1,42 +1,233 @@
# Linea SDK
# Linea Bridge SDK
## Description
The Linea SDK package a TypeScript library for seamless bridging operations between Ethereum (L1) and Linea (L2) networks. It provides functionality for interacting with contracts and retrieving message status and information.
The Linea SDK package is a comprehensive solution consisting of two distinct parts that facilitate message delivery between Ethereum and Linea networks. It provides functionality for interacting with contracts and retrieving; message status and information.
## Features
## Part 1: SDK
The Linea SDK is the first component of the package. It focuses on interacting with smart contracts on both Ethereum and Linea networks and provides custom functions to obtain message information. Notable features of the Linea SDK include:
- Feature 1: Getting contract instances and addresses
- Feature 2: Getting message information by message hash
- Feature 3: Getting messages by transaction hash
- Feature 4: Getting a message status by message hash
- Feature 5: Claiming messages
## Part 2: Postman
The Postman component is the second part of the package and enables the delivery of messages between Ethereum and Linea networks. It offers the following key features:
- Feature 1: Listening for message sent events on Ethereum
- Feature 2: Listening for message hash anchoring events to check if a message is ready to be claimed
- Feature 3: Automatic claiming of messages with a configurable retry mechanism
- Feature 4: Checking receipt status for each transaction
All messages are stored in a configurable Postgres DB.
- 🌉 **Bridge Operations**
- Claim bridged assets on L1 and L2
- Get message proof to claim on L1
- Track message status across chains
- 🔍 **Message Tracking**
- Query message status
- Monitor bridge events
-**Gas Management**
- Automatic gas estimation
- Custom fee strategies
## Installation
To install this package, execute the following command:
```bash
npm install @consensys/linea-sdk
# or
yarn add @consensys/linea-sdk
# or
pnpm add @consensys/linea-sdk
```
`npm install @consensys/linea-sdk`
## Quick Start
## Usage
```typescript
import { LineaSDK } from '@consensys/linea-sdk';
This package exposes two main classes for usage:
- The `PostmanServiceClient` class is used to run a Postman service for delivering messages.
- The `LineaSDK` class is used to interact with smart contracts on Ethereum and Linea (both Sepolia and Mainnet).
// Initialize SDK
const sdk = new LineaSDK({
network: 'linea-mainnet', // or 'linea-sepolia' or 'custom'
mode: 'read-write',
l1RpcUrlOrProvider: 'YOUR_L1_RPC_URL',
l2RpcUrlOrProvider: 'YOUR_L2_RPC_URL',
l1SignerPrivateKeyOrWallet: 'YOUR_L1_PRIVATE_KEY',
l2SignerPrivateKeyOrWallet: 'YOUR_L2_PRIVATE_KEY'
});
// Get L1 and L2 contract instances
const l1Contract = sdk.getL1Contract();
const l2Contract = sdk.getL2Contract();
```
## SDK Configuration
### LineaSDKOptions
The SDK can be initialized in two modes: `read-only` or `read-write`. The configuration options differ based on the mode:
#### Read-Only Mode
```typescript
interface ReadOnlyModeOptions {
network: "linea-mainnet" | "linea-sepolia" | "custom";
mode: "read-only";
l1RpcUrlOrProvider: string | Eip1193Provider;
l2RpcUrlOrProvider: string | Eip1193Provider;
l2MessageTreeDepth?: number;
l1FeeEstimatorOptions?: {
maxFeePerGasCap?: bigint;
gasFeeEstimationPercentile?: number;
enforceMaxGasFee?: boolean;
};
l2FeeEstimatorOptions?: {
maxFeePerGasCap?: bigint;
gasFeeEstimationPercentile?: number;
enforceMaxGasFee?: boolean;
};
}
```
#### Read-Write Mode
```typescript
interface WriteModeOptions extends ReadOnlyModeOptions {
mode: "read-write";
l1SignerPrivateKeyOrWallet: string | Wallet;
l2SignerPrivateKeyOrWallet: string | Wallet;
}
```
#### Field Explanations
- `network`: `"linea-mainnet"` | `"linea-sepolia"` | `"custom"`
- Description: Specifies the blockchain network to connect to.
- Possible Values:
- `"linea-mainnet"`: Connects to the Linea Mainnet contracts.
- `"linea-sepolia"`: Connects to the Linea Sepolia testnet contracts.
- `"custom"`: Allows for a custom contracts addresses, requiring custom RPC URLs for L1 and L2.
- `mode`: `"read-only"` | `"read-write"`
- Description: Determines the operation mode of the client.
- Possible Values:
- `"read-only"`: The client operates without the ability to send transactions; it can only read data from the blockchain.
- `"read-write"`: The client can read data and also send transactions, requiring signing credentials.
- `l1RpcUrlOrProvider`: `string | Eip1193Provider`
- Description: The RPC URL or provider for connecting to Layer 1 (L1) of the blockchain.
- Options:
- `string`: A URL pointing to the L1 RPC endpoint.
- `Eip1193Provider`: An EIP-1193 compliant provider instance.
- `l2RpcUrlOrProvider`: `string | Eip1193Provider`
- Description: The RPC URL or provider for connecting to Layer 2 (L2) of the blockchain.
- Options:
- `string`: A URL pointing to the L2 RPC endpoint.
- `Eip1193Provider`: An EIP-1193 compliant provider instance.
- `l2MessageTreeDepth?`: `number` (Optional)
- Description: Specifies the depth of the L2 message tree used in cryptographic operations or data structures.
- Default: If not provided, a default value of `5` is used which is the value used in Mainnet and Sepolia.
- `l1FeeEstimatorOptions?`: (Optional)
- Description: Configuration options for estimating transaction fees on Layer 1.
- Fields:
- `maxFeePerGasCap?`: `bigint` (Optional)
- Description: The maximum gas price (in wei) you're willing to pay per unit of gas on L1.
- Default: If not provided, a default value of `100000000000n` is used.
- `gasFeeEstimationPercentile?`: `number` (Optional)
- Description: The percentile of recent gas prices to use for fee estimation (used in `eth_feeHistory`).
- Default: If not provided, a default value of `20` is used.
- `enforceMaxGasFee?`: `boolean` (Optional)
- Description: If true, ensures the gas fee does not exceed maxFeePerGasCap.
- Default: `false`
- `l2FeeEstimatorOptions?`: (Optional)
- Description: Configuration options for estimating transaction fees on Layer 2.
- Fields:
- `maxFeePerGasCap?`: `bigint` (Optional)
- Description: The maximum gas price (in wei) you're willing to pay per unit of gas on L2.
- Default: If not provided, a default value of `100000000000n` is used.
- `gasFeeEstimationPercentile?`: `number` (Optional)
- Description: The percentile of recent gas prices to use for fee estimation (used in `eth_feeHistory`).
- Default: If not provided, a default value of `20` is used.
- `enforceMaxGasFee?`: `boolean` (Optional)
- Description: If true, ensures the gas fee does not exceed maxFeePerGasCap.
- Default: `false`
- `l1SignerPrivateKeyOrWallet`: `string | Wallet` <strong>(Required in "read-write" mode)</strong>
- Description: Credentials used to sign transactions on Layer 1.
- Options:
- `string`: A hexadecimal string representing the private key.
- `Wallet`: A wallet instance (e.g., from ethers.js) containing the private key and signing functionality.
- `l2SignerPrivateKeyOrWallet`: `string | Wallet` <strong>(Required in "read-write" mode)</strong>
- Description: Credentials used to sign transactions on Layer 2.
- Options:
- `string`: A hexadecimal string representing the private key.
- `Wallet`: A wallet instance for signing L2 transactions.
#### Additional Notes
- <strong>Common Fields</strong>: The fields `network`, `mode`, `l1RpcUrlOrProvider`, `l2RpcUrlOrProvider`, `l2MessageTreeDepth`, `l1FeeEstimatorOptions`, and `l2FeeEstimatorOptions` are common to both `read-only` and `read-write` modes.
- <strong>Mode-Specific Fields</strong>:
- In `read-only` mode:
- Only the common fields are required.
- The client can interact with the blockchain to read data but cannot send transactions.
- In `read-write` mode:
- All common fields are required.
- Additional Required Fields: l1SignerPrivateKeyOrWallet and l2SignerPrivateKeyOrWallet are necessary to enable transaction signing and sending capabilities.
- <strong>Fee Estimator Options</strong>:
- `maxFeePerGasCap`: Sets an upper limit on the gas price you're willing to pay.
- `gasFeeEstimationPercentile`: Helps in choosing a gas price based on recent network activity (used in `eth_feeHistory`).
- `enforceMaxGasFee`: Ensures that the gas fee does not exceed the maxFeePerGasCap value, providing cost control.
#### Usage Summary
- To <strong>read data only</strong> from the blockchain:
- Set `mode` to `read-only`.
- Provide the necessary network and RPC configurations.
- To <strong>read and write</strong> data (send transactions):
- Set `mode` to `read-write`.
- Provide all the common fields plus the signing credentials (`l1SignerPrivateKeyOrWallet` and `l2SignerPrivateKeyOrWallet`).
## Main Components
### L1 Contract (LineaRollupClient)
The L1 contract handles operations on the Ethereum side.
#### Message Operations
```typescript
// Get message by hash
const message = await l1Contract.getMessageByMessageHash(messageHash);
// Get messages by transaction hash
const messages = await l1Contract.getMessagesByTransactionHash(txHash);
// Get message status
const status = await l1Contract.getMessageStatus(messageHash);
// Get transaction receipt by message hash
const receipt = await l1Contract.getTransactionReceiptByMessageHash(messageHash);
```
#### Claiming Operations
```typescript
// Estimate claim gas
const gas = await l1Contract.estimateClaimGas(message);
// Claim message
const tx = await l1Contract.claim(message);
```
### L2 Contract (L2MessageServiceClient)
The L2 contract handles operations on the Linea side.
#### Message Operations
```typescript
// Get message by hash
const message = await l2Contract.getMessageByMessageHash(messageHash);
// Get messages by transaction hash
const messages = await l2Contract.getMessagesByTransactionHash(txHash);
// Get message status
const status = await l2Contract.getMessageStatus(messageHash);
// Get transaction receipt by message hash
const receipt = await l2Contract.getTransactionReceiptByMessageHash(messageHash);
```
#### Claiming and Transaction Operations
```typescript
// Estimate claim gas and fees
const gasFees = await l2Contract.estimateClaimGasFees(message)
// Claim message on L2
const tx = await l2Contract.claim(message);
```
## License
This package is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for more information.
This package is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.

View File

@@ -7,19 +7,6 @@ module.exports = {
collectCoverage: true,
collectCoverageFrom: ["src/**/*.ts"],
coverageReporters: ["html", "lcov", "text"],
testPathIgnorePatterns: [
"src/clients/blockchain/typechain",
"src/application/postman/persistence/migrations/",
"src/application/postman/persistence/repositories/",
"src/index.ts",
"src/utils/WinstonLogger.ts",
],
coveragePathIgnorePatterns: [
"src/clients/blockchain/typechain",
"src/application/postman/persistence/migrations/",
"src/application/postman/persistence/repositories/",
"src/index.ts",
"src/utils/WinstonLogger.ts",
"src/utils/testing/",
],
testPathIgnorePatterns: ["src/contracts/typechain", "src/index.ts"],
coveragePathIgnorePatterns: ["src/contracts/typechain", "src/index.ts", "src/utils/testing/"],
};

View File

@@ -4,46 +4,34 @@
"author": "Consensys Software Inc.",
"license": "Apache-2.0",
"description": "",
"main": "dist/lib/index.js",
"types": "dist/lib/index.d.ts",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"lint:ts": "npx eslint '**/*.ts'",
"lint:ts:fix": "npx eslint --fix '**/*.ts'",
"prettier": "prettier -c '**/*.ts'",
"prettier:fix": "prettier -w '**/*.ts'",
"lint:fix": "pnpm run lint:ts:fix && pnpm run prettier:fix",
"clean": "rimraf dist src/typechain node_modules coverage tsconfig.build.tsbuildinfo",
"build:pre": "pnpm run typechain",
"build": "pnpm run build:pre && tsc -p tsconfig.build.json",
"build:runSdk": "tsc ./scripts/runSdk.ts",
"typechain": "typechain --target ethers-v6 --out-dir ./src/clients/blockchain/typechain './src/clients/blockchain/abis/*.json'",
"test": "npx jest --bail --detectOpenHandles --forceExit",
"test:run": "ts-node ./scripts/runSdk.ts",
"lint:fix": "pnpm run lint:ts:fix && pnpm run prettier:fix"
"typechain": "typechain --target ethers-v6 --out-dir ./src/contracts/typechain './src/contracts/abis/*.abi'",
"test": "npx jest --bail --detectOpenHandles --forceExit"
},
"dependencies": {
"@consensys/linea-native-libs": "workspace:*",
"better-sqlite3": "9.6.0",
"class-validator": "0.14.1",
"dotenv": "16.4.5",
"ethers": "6.12.0",
"lru-cache": "10.2.2",
"pg": "8.11.3",
"typeorm": "0.3.20",
"typeorm-naming-strategies": "4.1.0",
"winston": "3.13.0"
"ethers": "6.13.4",
"lru-cache": "11.0.2"
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@typechain/ethers-v6": "0.5.1",
"@types/jest": "29.5.12",
"@types/yargs": "17.0.32",
"@types/jest": "29.5.14",
"jest": "29.7.0",
"jest-mock-extended": "3.0.5",
"ts-jest": "29.1.2",
"typechain": "8.3.2",
"yargs": "17.7.2"
"ts-jest": "29.2.5",
"typechain": "8.3.2"
},
"files": [
"dist/**/*"
"dist"
]
}

View File

@@ -1,32 +0,0 @@
const HEXADECIMAL_REGEX = new RegExp("^0[xX][0-9a-fA-F]+$");
const ADDRESS_HEX_STR_SIZE = 42;
const PRIVKEY_HEX_STR_SIZE = 66;
function sanitizeHexBytes(paramName: string, value: string, expectedSize: number) {
if (!value.startsWith("0x")) {
value = "0x" + value;
}
if (!HEXADECIMAL_REGEX.test(value)) {
throw new Error(`${paramName}: '${value}' is not a valid Hexadecimal notation!`);
}
if (value.length !== expectedSize) {
throw new Error(`${paramName} has size ${value.length} expected ${expectedSize}`);
}
return value;
}
function sanitizeAddress(argName: string) {
return (input: string) => {
return sanitizeHexBytes(argName, input, ADDRESS_HEX_STR_SIZE);
};
}
function sanitizePrivKey(argName: string) {
return (input: string) => {
return sanitizeHexBytes(argName, input, PRIVKEY_HEX_STR_SIZE);
};
}
export { sanitizeHexBytes, sanitizeAddress, sanitizePrivKey };

View File

@@ -1,156 +0,0 @@
import { BaseContract, ContractFactory, JsonRpcProvider, Wallet, ethers } from "ethers";
import { config } from "dotenv";
import fs from "fs";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import {
ZkEvmV2__factory,
TransparentUpgradeableProxy__factory,
ProxyAdmin__factory,
PlonkVerifier__factory,
L2MessageService__factory,
} from "../src/clients/blockchain/typechain/factories";
import { ProxyAdmin } from "../src/clients/blockchain/typechain/ProxyAdmin";
import { sanitizePrivKey } from "./cli";
config();
const argv = yargs(hideBin(process.argv))
.option("l1-rpc-url", {
describe: "L1 rpc url",
type: "string",
demandOption: true,
})
.option("l2-rpc-url", {
describe: "L2 rpc url",
type: "string",
demandOption: true,
})
.option("l1-deployer-priv-key", {
describe: "L1 deployer private key",
type: "string",
demandOption: true,
coerce: sanitizePrivKey("priv-key"),
})
.option("l2-deployer-priv-key", {
describe: "L2 deployer private key",
type: "string",
demandOption: true,
coerce: sanitizePrivKey("priv-key"),
})
.parseSync();
const getInitializerData = (contractInterface: ethers.Interface, args: unknown[]) => {
const initializer = "initialize";
const fragment = contractInterface.getFunction(initializer);
return contractInterface.encodeFunctionData(fragment!, args);
};
const deployUpgradableContract = async <T extends ContractFactory>(
contractFactory: T,
deployer: Wallet,
admin: ProxyAdmin,
initializerData = "0x",
): Promise<BaseContract> => {
const instance = await contractFactory.connect(deployer).deploy();
await instance.waitForDeployment();
const proxy = await new TransparentUpgradeableProxy__factory()
.connect(deployer)
.deploy(await instance.getAddress(), await admin.getAddress(), initializerData);
await proxy.waitForDeployment();
return instance.attach(await proxy.getAddress());
};
const deployLineaRollup = async (deployer: Wallet): Promise<{ zkevmV2ContractAddress: string }> => {
const proxyFactory = new ProxyAdmin__factory(deployer);
const proxyAdmin = await proxyFactory.connect(deployer).deploy();
await proxyAdmin.waitForDeployment();
console.log(`ProxyAdmin contract deployed at address: ${await proxyAdmin.getAddress()}`);
const plonkVerifierFactory = new PlonkVerifier__factory(deployer);
const plonkVerifier = await plonkVerifierFactory.deploy();
await plonkVerifier.waitForDeployment();
console.log(`PlonkVerifier contract deployed at address: ${await plonkVerifier.getAddress()}`);
const zkevmV2Contract = await deployUpgradableContract(
new ZkEvmV2__factory(deployer),
deployer,
proxyAdmin,
getInitializerData(ZkEvmV2__factory.createInterface(), [
ethers.ZeroHash,
0,
await plonkVerifier.getAddress(),
deployer.address,
[deployer.address],
86400,
ethers.parseEther("5"),
]),
);
const zkEvmV2Address = await zkevmV2Contract.getAddress();
console.log(`ZkEvmV2 contract deployed at address: ${zkEvmV2Address}`);
return { zkevmV2ContractAddress: zkEvmV2Address };
};
const deployL2MessageService = async (deployer: Wallet): Promise<string> => {
const proxyFactory = new ProxyAdmin__factory(deployer);
const proxyAdmin = await proxyFactory.connect(deployer).deploy();
await proxyAdmin.waitForDeployment();
console.log(`L2 ProxyAdmin contract deployed at address: ${await proxyAdmin.getAddress()}`);
const l2MessageService = await deployUpgradableContract(
new L2MessageService__factory(deployer),
deployer,
proxyAdmin,
getInitializerData(L2MessageService__factory.createInterface(), [
deployer.address,
deployer.address,
86400,
ethers.parseEther("5"),
]),
);
const l2MessageServiceAddress = await l2MessageService.getAddress();
console.log(`L2MessageService contract deployed at address: ${l2MessageServiceAddress}`);
return l2MessageServiceAddress;
};
const main = async (args: typeof argv) => {
const l1Provider = new JsonRpcProvider(args.l1RpcUrl);
const l2Provider = new JsonRpcProvider(args.l2RpcUrl);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const l1Deployer = new Wallet(args.l1DeployerPrivKey, l1Provider);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const l2Deployer = new Wallet(args.l2DeployerPrivKey, l2Provider);
const { zkevmV2ContractAddress } = await deployLineaRollup(l1Deployer);
const l2MessageServiceAddress = await deployL2MessageService(l2Deployer);
const tx = await l2Deployer.sendTransaction({
to: l2MessageServiceAddress,
value: ethers.parseEther("1000"),
data: "0x",
});
await tx.wait();
const tx2 = await l1Deployer.sendTransaction({
to: zkevmV2ContractAddress,
value: ethers.parseEther("1000"),
data: "0x",
});
await tx2.wait();
fs.writeFileSync(
"./scripts/contractAddresses.json",
JSON.stringify({ zkevmV2ContractAddress, l2MessageServiceAddress }, null, 2),
);
};
main(argv)
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -1,18 +0,0 @@
import { ethers } from "ethers";
export async function encodeSendMessage(
sender: string,
receiver: string,
fee: bigint,
amount: bigint,
messageNonce: bigint,
calldata: string,
) {
const abiCoder = new ethers.AbiCoder();
const data = abiCoder.encode(
["address", "address", "uint256", "uint256", "uint256", "bytes"],
[sender, receiver, fee, amount, messageNonce, calldata],
);
return ethers.keccak256(data);
}

View File

@@ -1,130 +0,0 @@
import * as dotenv from "dotenv";
import { transports } from "winston";
import { PostmanServiceClient } from "../src/application/postman/app/PostmanServiceClient";
dotenv.config();
async function main() {
const client = new PostmanServiceClient({
l1Options: {
rpcUrl: process.env.L1_RPC_URL ?? "",
messageServiceContractAddress: process.env.L1_CONTRACT_ADDRESS ?? "",
isEOAEnabled: process.env.L1_L2_EOA_ENABLED === "true",
isCalldataEnabled: process.env.L1_L2_CALLDATA_ENABLED === "true",
listener: {
pollingInterval: process.env.L1_LISTENER_INTERVAL ? parseInt(process.env.L1_LISTENER_INTERVAL) : undefined,
maxFetchMessagesFromDb: process.env.MAX_FETCH_MESSAGES_FROM_DB
? parseInt(process.env.MAX_FETCH_MESSAGES_FROM_DB)
: undefined,
maxBlocksToFetchLogs: process.env.L1_MAX_BLOCKS_TO_FETCH_LOGS
? parseInt(process.env.L1_MAX_BLOCKS_TO_FETCH_LOGS)
: undefined,
...(parseInt(process.env.L1_LISTENER_INITIAL_FROM_BLOCK ?? "") >= 0
? { initialFromBlock: parseInt(process.env.L1_LISTENER_INITIAL_FROM_BLOCK ?? "") }
: {}),
...(parseInt(process.env.L1_LISTENER_BLOCK_CONFIRMATION ?? "") >= 0
? { blockConfirmation: parseInt(process.env.L1_LISTENER_BLOCK_CONFIRMATION ?? "") }
: {}),
},
claiming: {
signerPrivateKey: process.env.L1_SIGNER_PRIVATE_KEY ?? "",
messageSubmissionTimeout: process.env.MESSAGE_SUBMISSION_TIMEOUT
? parseInt(process.env.MESSAGE_SUBMISSION_TIMEOUT)
: undefined,
maxNonceDiff: process.env.MAX_NONCE_DIFF ? parseInt(process.env.MAX_NONCE_DIFF) : undefined,
maxFeePerGas: process.env.MAX_FEE_PER_GAS ? BigInt(process.env.MAX_FEE_PER_GAS) : undefined,
gasEstimationPercentile: process.env.GAS_ESTIMATION_PERCENTILE
? parseInt(process.env.GAS_ESTIMATION_PERCENTILE)
: undefined,
profitMargin: process.env.PROFIT_MARGIN ? parseFloat(process.env.PROFIT_MARGIN) : undefined,
maxNumberOfRetries: process.env.MAX_NUMBER_OF_RETRIES ? parseInt(process.env.MAX_NUMBER_OF_RETRIES) : undefined,
retryDelayInSeconds: process.env.RETRY_DELAY_IN_SECONDS
? parseInt(process.env.RETRY_DELAY_IN_SECONDS)
: undefined,
maxClaimGasLimit: process.env.MAX_CLAIM_GAS_LIMIT ? BigInt(process.env.MAX_CLAIM_GAS_LIMIT) : undefined,
maxTxRetries: process.env.MAX_TX_RETRIES ? parseInt(process.env.MAX_TX_RETRIES) : undefined,
isMaxGasFeeEnforced: process.env.L1_MAX_GAS_FEE_ENFORCED === "true",
},
},
l2Options: {
rpcUrl: process.env.L2_RPC_URL ?? "",
messageServiceContractAddress: process.env.L2_CONTRACT_ADDRESS ?? "",
isEOAEnabled: process.env.L2_L1_EOA_ENABLED === "true",
isCalldataEnabled: process.env.L2_L1_CALLDATA_ENABLED === "true",
listener: {
pollingInterval: process.env.L2_LISTENER_INTERVAL ? parseInt(process.env.L2_LISTENER_INTERVAL) : undefined,
maxFetchMessagesFromDb: process.env.MAX_FETCH_MESSAGES_FROM_DB
? parseInt(process.env.MAX_FETCH_MESSAGES_FROM_DB)
: undefined,
maxBlocksToFetchLogs: process.env.L2_MAX_BLOCKS_TO_FETCH_LOGS
? parseInt(process.env.L2_MAX_BLOCKS_TO_FETCH_LOGS)
: undefined,
...(parseInt(process.env.L2_LISTENER_INITIAL_FROM_BLOCK ?? "") >= 0
? { initialFromBlock: parseInt(process.env.L2_LISTENER_INITIAL_FROM_BLOCK ?? "") }
: {}),
...(parseInt(process.env.L2_LISTENER_BLOCK_CONFIRMATION ?? "") >= 0
? { blockConfirmation: parseInt(process.env.L2_LISTENER_BLOCK_CONFIRMATION ?? "") }
: {}),
},
claiming: {
signerPrivateKey: process.env.L2_SIGNER_PRIVATE_KEY ?? "",
messageSubmissionTimeout: process.env.MESSAGE_SUBMISSION_TIMEOUT
? parseInt(process.env.MESSAGE_SUBMISSION_TIMEOUT)
: undefined,
maxNonceDiff: process.env.MAX_NONCE_DIFF ? parseInt(process.env.MAX_NONCE_DIFF) : undefined,
maxFeePerGas: process.env.MAX_FEE_PER_GAS ? BigInt(process.env.MAX_FEE_PER_GAS) : undefined,
gasEstimationPercentile: process.env.GAS_ESTIMATION_PERCENTILE
? parseInt(process.env.GAS_ESTIMATION_PERCENTILE)
: undefined,
profitMargin: process.env.PROFIT_MARGIN ? parseFloat(process.env.PROFIT_MARGIN) : undefined,
maxNumberOfRetries: process.env.MAX_NUMBER_OF_RETRIES ? parseInt(process.env.MAX_NUMBER_OF_RETRIES) : undefined,
retryDelayInSeconds: process.env.RETRY_DELAY_IN_SECONDS
? parseInt(process.env.RETRY_DELAY_IN_SECONDS)
: undefined,
maxClaimGasLimit: process.env.MAX_CLAIM_GAS_LIMIT ? BigInt(process.env.MAX_CLAIM_GAS_LIMIT) : undefined,
maxTxRetries: process.env.MAX_TX_RETRIES ? parseInt(process.env.MAX_TX_RETRIES) : undefined,
isMaxGasFeeEnforced: process.env.L2_MAX_GAS_FEE_ENFORCED === "true",
},
l2MessageTreeDepth: process.env.L2_MESSAGE_TREE_DEPTH ? parseInt(process.env.L2_MESSAGE_TREE_DEPTH) : undefined,
enableLineaEstimateGas: process.env.ENABLE_LINEA_ESTIMATE_GAS === "true",
},
l1L2AutoClaimEnabled: process.env.L1_L2_AUTO_CLAIM_ENABLED === "true",
l2L1AutoClaimEnabled: process.env.L2_L1_AUTO_CLAIM_ENABLED === "true",
loggerOptions: {
level: "info",
transports: [new transports.Console()],
},
databaseOptions: {
type: "postgres",
host: process.env.POSTGRES_HOST ?? "127.0.0.1",
port: parseInt(process.env.POSTGRES_PORT ?? "5432"),
username: process.env.POSTGRES_USER ?? "postgres",
password: process.env.POSTGRES_PASSWORD ?? "postgres",
database: process.env.POSTGRES_DB ?? "postman_db",
},
databaseCleanerOptions: {
enabled: process.env.DB_CLEANER_ENABLED === "true",
cleaningInterval: process.env.DB_CLEANING_INTERVAL ? parseInt(process.env.DB_CLEANING_INTERVAL) : undefined,
daysBeforeNowToDelete: process.env.DB_DAYS_BEFORE_NOW_TO_DELETE
? parseInt(process.env.DB_DAYS_BEFORE_NOW_TO_DELETE)
: undefined,
},
});
await client.connectDatabase();
client.startAllServices();
}
main()
.then()
.catch((error) => {
console.error("", error);
process.exit(1);
});
process.on("SIGINT", () => {
process.exit(0);
});
process.on("SIGTERM", () => {
process.exit(0);
});

View File

@@ -1,42 +0,0 @@
import * as dotenv from "dotenv";
import { LineaSDK } from "../src";
import { ZERO_ADDRESS } from "../src/core/constants";
import { Direction, MessageStatus } from "../src/core/enums/MessageEnums";
dotenv.config();
async function main() {
const sdk = new LineaSDK({
l1RpcUrlOrProvider: process.env.L1_RPC_URL ?? "",
l2RpcUrlOrProvider: process.env.L2_RPC_URL ?? "",
network: "linea-sepolia",
mode: "read-only",
});
const l2Contract = sdk.getL2Contract();
const data = l2Contract.encodeClaimMessageTransactionData({
messageSender: "0x5eEeA0e70FFE4F5419477056023c4b0acA016562",
destination: "0x5eEeA0e70FFE4F5419477056023c4b0acA016562",
fee: 0n,
value: 100000000000000000n,
feeRecipient: ZERO_ADDRESS,
calldata: "0x",
messageNonce: 3105n,
messageHash: "",
contractAddress: "",
sentBlockNumber: 0,
direction: Direction.L1_TO_L2,
status: MessageStatus.SENT,
claimNumberOfRetry: 0,
});
console.log(data);
}
main()
.then(() => {
process.exit(0);
})
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -1,197 +0,0 @@
import { BytesLike, ContractTransactionReceipt, Overrides, Wallet, JsonRpcProvider } from "ethers";
import { config } from "dotenv";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { SendMessageArgs } from "./types";
import { sanitizeAddress, sanitizePrivKey } from "./cli";
import {
L2MessageService,
L2MessageService__factory,
ZkEvmV2__factory,
ZkEvmV2,
LineaRollup,
LineaRollup__factory,
} from "../src/clients/blockchain/typechain";
import { encodeSendMessage } from "./helpers";
config();
const argv = yargs(hideBin(process.argv))
.option("l1-rpc-url", {
describe: "Rpc url",
type: "string",
demandOption: true,
})
.option("l2-rpc-url", {
describe: "Rpc url",
type: "string",
demandOption: true,
})
.option("l1-priv-key", {
describe: "Signer private key on L1",
type: "string",
demandOption: true,
coerce: sanitizePrivKey("priv-key"),
})
.option("l2-priv-key", {
describe: "Signer private key on L2 from account with L1_L2_MESSAGE_SETTER_ROLE",
type: "string",
demandOption: false,
coerce: sanitizePrivKey("priv-key"),
})
.option("l1-contract-address", {
describe: "Smart contract address",
type: "string",
demandOption: true,
coerce: sanitizeAddress("smc-address"),
})
.option("l2-contract-address", {
describe: "Smart contract address",
type: "string",
demandOption: true,
coerce: sanitizeAddress("smc-address"),
})
.option("to", {
describe: "Destination address",
type: "string",
demandOption: true,
coerce: sanitizeAddress("to"),
})
.option("fee", {
describe: "Fee passed to send message function",
type: "number",
demandOption: true,
})
.option("value", {
describe: "Value passed to send message function",
type: "number",
demandOption: true,
})
.option("calldata", {
describe: "Encoded message calldata",
type: "string",
demandOption: true,
})
.option("number-of-message", {
describe: "Number of messages to send",
type: "number",
demandOption: true,
})
.option("auto-anchoring", {
describe: "Auto anchoring",
type: "boolean",
demandOption: false,
default: false,
})
.parseSync();
const sendMessage = async (
contract: ZkEvmV2,
args: SendMessageArgs,
overrides: Overrides = {},
): Promise<ContractTransactionReceipt | null> => {
const tx = await contract.sendMessage(args.to, args.fee, args.calldata, overrides);
return await tx.wait();
};
const sendMessages = async (
contract: ZkEvmV2,
signer: Wallet,
numberOfMessages: number,
args: SendMessageArgs,
overrides?: Overrides,
) => {
let nonce = await signer.getNonce();
const sendMessagePromises: Promise<ContractTransactionReceipt | null>[] = [];
for (let i = 0; i < numberOfMessages; i++) {
sendMessagePromises.push(
sendMessage(contract, args, {
...overrides,
nonce,
}),
);
nonce++;
}
await Promise.all(sendMessagePromises);
};
const getMessageCounter = async (contractAddress: string, signer: Wallet) => {
const lineaRollup = ZkEvmV2__factory.connect(contractAddress, signer) as ZkEvmV2;
return lineaRollup.nextMessageNumber();
};
const anchorMessageHashesOnL2 = async (
lineaRollup: LineaRollup,
l2MessageService: L2MessageService,
messageHashes: BytesLike[],
startingMessageNumber: bigint,
) => {
const finalMessageNumber = startingMessageNumber + BigInt(messageHashes.length - 1);
const rollingHashes = await lineaRollup.rollingHashes(finalMessageNumber);
const tx = await l2MessageService.anchorL1L2MessageHashes(
messageHashes,
startingMessageNumber,
finalMessageNumber,
rollingHashes,
);
await tx.wait();
};
const main = async (args: typeof argv) => {
if (args.autoAnchoring && !args.l2PrivKey) {
console.error(
`private key from an L2 account with L1_L2_MESSAGE_SETTER_ROLE must be given if auto-anchoring is true`,
);
return;
}
const l1Provider = new JsonRpcProvider(args.l1RpcUrl);
const l2Provider = new JsonRpcProvider(args.l2RpcUrl);
const l1Signer = new Wallet(args.l1PrivKey, l1Provider);
const functionArgs: SendMessageArgs & Overrides = {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
to: args.to,
fee: BigInt(args.fee.toString()),
calldata: args.calldata,
};
const zkEvmV2 = ZkEvmV2__factory.connect(args.l1ContractAddress, l1Signer) as ZkEvmV2;
await sendMessages(zkEvmV2, l1Signer, args.numberOfMessage, functionArgs, { value: BigInt(args.value.toString()) });
// Anchor messages hash on L2
const nextMessageCounter = await getMessageCounter(args.l1ContractAddress, l1Signer);
const startCounter = nextMessageCounter - BigInt(args.numberOfMessage);
const messageHashesToAnchor: string[] = [];
for (let i = startCounter; i < nextMessageCounter; i++) {
const messageHash = await encodeSendMessage(
l1Signer.address,
args.to,
BigInt(args.fee.toString()),
BigInt(args.value.toString()) - BigInt(args.fee.toString()),
BigInt(i),
args.calldata,
);
console.log(messageHash);
messageHashesToAnchor.push(messageHash);
}
if (!args.autoAnchoring) return;
const l2Signer = new Wallet(args.l2PrivKey!, l2Provider);
const lineaRollup = LineaRollup__factory.connect(args.l1ContractAddress, l1Signer) as LineaRollup;
const l2MessageService = L2MessageService__factory.connect(args.l2ContractAddress, l2Signer) as L2MessageService;
const startingMessageNumber = startCounter;
await anchorMessageHashesOnL2(lineaRollup, l2MessageService, messageHashesToAnchor, startingMessageNumber);
};
main(argv)
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -1,109 +0,0 @@
import { ContractTransactionReceipt, Overrides, JsonRpcProvider, Wallet } from "ethers";
import { config } from "dotenv";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { SendMessageArgs } from "./types";
import { sanitizeAddress, sanitizePrivKey } from "./cli";
import { L2MessageService, L2MessageService__factory } from "../src/clients/blockchain/typechain";
config();
const argv = yargs(hideBin(process.argv))
.option("rpc-url", {
describe: "Rpc url",
type: "string",
demandOption: true,
})
.option("priv-key", {
describe: "Signer private key",
type: "string",
demandOption: true,
coerce: sanitizePrivKey("priv-key"),
})
.option("contract-address", {
describe: "Smart contract address",
type: "string",
demandOption: true,
coerce: sanitizeAddress("smc-address"),
})
.option("to", {
describe: "Destination address",
type: "string",
demandOption: true,
coerce: sanitizeAddress("to"),
})
.option("fee", {
describe: "Fee passed to send message function",
type: "string",
})
.option("value", {
describe: "Value passed to send message function",
type: "string",
})
.option("calldata", {
describe: "Encoded message calldata",
type: "string",
demandOption: true,
})
.option("number-of-message", {
describe: "Number of messages to send",
type: "number",
demandOption: true,
})
.parseSync();
const sendMessage = async (
contract: L2MessageService,
args: SendMessageArgs,
overrides: Overrides = {},
): Promise<ContractTransactionReceipt | null> => {
const tx = await contract.sendMessage(args.to, args.fee, args.calldata, overrides);
return await tx.wait();
};
const sendMessages = async (
contract: L2MessageService,
signer: Wallet,
numberOfMessages: number,
args: SendMessageArgs,
overrides?: Overrides,
) => {
let nonce = await signer.getNonce();
const sendMessagePromises: Promise<ContractTransactionReceipt | null>[] = [];
for (let i = 0; i < numberOfMessages; i++) {
sendMessagePromises.push(
sendMessage(contract, args, {
...overrides,
nonce,
}),
);
nonce++;
}
await Promise.all(sendMessagePromises);
};
const main = async (args: typeof argv) => {
const provider = new JsonRpcProvider(args.rpcUrl);
const signer = new Wallet(args.privKey, provider);
const functionArgs: SendMessageArgs & Overrides = {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
to: args.to,
fee: BigInt(args.fee!),
calldata: args.calldata,
value: args.value,
};
const l2MessageService = L2MessageService__factory.connect(args.contractAddress, signer) as L2MessageService;
await sendMessages(l2MessageService, signer, args.numberOfMessage, functionArgs, { value: args.value });
};
main(argv)
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -1,7 +0,0 @@
import { BytesLike } from "ethers";
export type SendMessageArgs = {
to: string;
fee: bigint;
calldata: BytesLike;
};

View File

@@ -1,13 +1,13 @@
import { describe, it, expect } from "@jest/globals";
import { JsonRpcProvider, Wallet } from "ethers";
import ProviderService from "../ProviderService";
import { LineaSDK } from "../LineaSDK";
import { L1ClaimingService } from "../claiming/L1ClaimingService";
import { TEST_L1_SIGNER_PRIVATE_KEY, TEST_L2_SIGNER_PRIVATE_KEY, TEST_RPC_URL } from "../../utils/testing/constants";
import { NETWORKS } from "../networks";
import { EthersL2MessageServiceLogClient } from "../../clients/blockchain/linea/EthersL2MessageServiceLogClient";
import { serialize } from "../../core/utils/serialize";
import { generateL2MessageServiceClient, generateLineaRollupClient } from "../../utils/testing/helpers";
import { LineaSDK } from "./LineaSDK";
import { L1ClaimingService } from "./clients/ethereum";
import { LineaProvider, Provider } from "./clients/providers";
import { EthersL2MessageServiceLogClient } from "./clients/linea";
import { NETWORKS } from "./core/constants";
import { serialize } from "./core/utils";
import { TEST_L1_SIGNER_PRIVATE_KEY, TEST_L2_SIGNER_PRIVATE_KEY, TEST_RPC_URL } from "./utils/testing/constants/common";
import { generateL2MessageServiceClient, generateLineaRollupClient } from "./utils/testing/helpers";
describe("LineaSDK", () => {
describe("getL1Contract", () => {
@@ -23,8 +23,8 @@ describe("LineaSDK", () => {
expect(serialize(lineaRollupClient)).toStrictEqual(
serialize(
generateLineaRollupClient(
new JsonRpcProvider(TEST_RPC_URL),
new JsonRpcProvider(TEST_RPC_URL),
new Provider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
NETWORKS["linea-sepolia"].l1ContractAddress,
NETWORKS["linea-sepolia"].l2ContractAddress,
"read-only",
@@ -47,12 +47,12 @@ describe("LineaSDK", () => {
expect(serialize(lineaRollupClient)).toStrictEqual(
serialize(
generateLineaRollupClient(
new JsonRpcProvider(TEST_RPC_URL),
new JsonRpcProvider(TEST_RPC_URL),
new Provider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
NETWORKS["linea-sepolia"].l1ContractAddress,
NETWORKS["linea-sepolia"].l2ContractAddress,
"read-write",
new ProviderService(TEST_RPC_URL).getSigner(TEST_L1_SIGNER_PRIVATE_KEY),
new Wallet(TEST_L1_SIGNER_PRIVATE_KEY).connect(new JsonRpcProvider(TEST_RPC_URL)),
).lineaRollupClient,
),
);
@@ -64,7 +64,7 @@ describe("LineaSDK", () => {
network: "linea-sepolia",
l1SignerPrivateKeyOrWallet: TEST_L1_SIGNER_PRIVATE_KEY,
l2SignerPrivateKeyOrWallet: TEST_L2_SIGNER_PRIVATE_KEY,
l1RpcUrlOrProvider: new JsonRpcProvider(TEST_RPC_URL),
l1RpcUrlOrProvider: TEST_RPC_URL,
l2RpcUrlOrProvider: TEST_RPC_URL,
});
@@ -72,12 +72,12 @@ describe("LineaSDK", () => {
expect(serialize(lineaRollupClient)).toStrictEqual(
serialize(
generateLineaRollupClient(
new JsonRpcProvider(TEST_RPC_URL),
new JsonRpcProvider(TEST_RPC_URL),
new Provider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
NETWORKS["linea-sepolia"].l1ContractAddress,
NETWORKS["linea-sepolia"].l2ContractAddress,
"read-write",
new ProviderService(TEST_RPC_URL).getSigner(TEST_L1_SIGNER_PRIVATE_KEY),
new Wallet(TEST_L1_SIGNER_PRIVATE_KEY).connect(new Provider(TEST_RPC_URL)),
).lineaRollupClient,
),
);
@@ -98,8 +98,8 @@ describe("LineaSDK", () => {
expect(serialize(lineaRollupClient)).toStrictEqual(
serialize(
generateLineaRollupClient(
new JsonRpcProvider(TEST_RPC_URL),
new JsonRpcProvider(TEST_RPC_URL),
new Provider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
NETWORKS["linea-sepolia"].l1ContractAddress,
NETWORKS["linea-sepolia"].l2ContractAddress,
"read-write",
@@ -119,7 +119,7 @@ describe("LineaSDK", () => {
l2RpcUrlOrProvider: TEST_RPC_URL,
});
expect(() => sdk.getL1Contract()).toThrow("You need to provide both L1 and L2 contract addresses.");
expect(() => sdk.getL1Contract()).toThrow("You need to provide a L1 contract address.");
});
it("should return LineaRollupClient with custom contract address when the network option is set to 'custom'", () => {
@@ -140,12 +140,12 @@ describe("LineaSDK", () => {
expect(serialize(l1MessageService)).toStrictEqual(
serialize(
generateLineaRollupClient(
new JsonRpcProvider(TEST_RPC_URL),
new JsonRpcProvider(TEST_RPC_URL),
new Provider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
localL1ContractAddress,
localL2ContractAddress,
"read-write",
new ProviderService(TEST_RPC_URL).getSigner(TEST_L1_SIGNER_PRIVATE_KEY),
new Wallet(TEST_L1_SIGNER_PRIVATE_KEY).connect(new Provider(TEST_RPC_URL)),
).lineaRollupClient,
),
);
@@ -165,7 +165,7 @@ describe("LineaSDK", () => {
expect(serialize(l2MessageService)).toStrictEqual(
serialize(
generateL2MessageServiceClient(
new JsonRpcProvider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
NETWORKS["linea-sepolia"].l2ContractAddress,
"read-only",
).l2MessageServiceClient,
@@ -187,10 +187,10 @@ describe("LineaSDK", () => {
expect(serialize(l2MessageService)).toStrictEqual(
serialize(
generateL2MessageServiceClient(
new JsonRpcProvider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
NETWORKS["linea-sepolia"].l2ContractAddress,
"read-write",
new ProviderService(TEST_RPC_URL).getSigner(TEST_L2_SIGNER_PRIVATE_KEY),
new Wallet(TEST_L2_SIGNER_PRIVATE_KEY).connect(new LineaProvider(TEST_RPC_URL)),
).l2MessageServiceClient,
),
);
@@ -203,17 +203,17 @@ describe("LineaSDK", () => {
l1SignerPrivateKeyOrWallet: TEST_L1_SIGNER_PRIVATE_KEY,
l2SignerPrivateKeyOrWallet: TEST_L2_SIGNER_PRIVATE_KEY,
l1RpcUrlOrProvider: TEST_RPC_URL,
l2RpcUrlOrProvider: new JsonRpcProvider(TEST_RPC_URL),
l2RpcUrlOrProvider: TEST_RPC_URL,
});
const l2MessageService = sdk.getL2Contract();
expect(serialize(l2MessageService)).toStrictEqual(
serialize(
generateL2MessageServiceClient(
new JsonRpcProvider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
NETWORKS["linea-sepolia"].l2ContractAddress,
"read-write",
new ProviderService(TEST_RPC_URL).getSigner(TEST_L2_SIGNER_PRIVATE_KEY),
new Wallet(TEST_L2_SIGNER_PRIVATE_KEY).connect(new LineaProvider(TEST_RPC_URL)),
).l2MessageServiceClient,
),
);
@@ -234,7 +234,7 @@ describe("LineaSDK", () => {
expect(serialize(l2MessageService)).toStrictEqual(
serialize(
generateL2MessageServiceClient(
new JsonRpcProvider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
NETWORKS["linea-sepolia"].l2ContractAddress,
"read-write",
wallet,
@@ -272,10 +272,10 @@ describe("LineaSDK", () => {
expect(serialize(l2MessageService)).toStrictEqual(
serialize(
generateL2MessageServiceClient(
new JsonRpcProvider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
localContractAddress,
"read-write",
new ProviderService(TEST_RPC_URL).getSigner(TEST_L2_SIGNER_PRIVATE_KEY),
new Wallet(TEST_L2_SIGNER_PRIVATE_KEY).connect(new LineaProvider(TEST_RPC_URL)),
).l2MessageServiceClient,
),
);
@@ -292,23 +292,24 @@ describe("LineaSDK", () => {
});
const l1ClaimingService = sdk.getL1ClaimingService();
expect(serialize(l1ClaimingService)).toStrictEqual(
serialize(
new L1ClaimingService(
generateLineaRollupClient(
new JsonRpcProvider(TEST_RPC_URL),
new JsonRpcProvider(TEST_RPC_URL),
new Provider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
NETWORKS["linea-sepolia"].l1ContractAddress,
NETWORKS["linea-sepolia"].l2ContractAddress,
"read-only",
).lineaRollupClient,
generateL2MessageServiceClient(
new JsonRpcProvider(TEST_RPC_URL),
new LineaProvider(TEST_RPC_URL),
NETWORKS["linea-sepolia"].l2ContractAddress,
"read-only",
).l2MessageServiceClient,
new EthersL2MessageServiceLogClient(
new ProviderService(TEST_RPC_URL).provider,
new LineaProvider(TEST_RPC_URL).provider,
NETWORKS["linea-sepolia"].l2ContractAddress,
),
"linea-sepolia",

327
sdk/src/LineaSDK.ts Normal file
View File

@@ -0,0 +1,327 @@
import { Eip1193Provider, Signer, Wallet } from "ethers";
import {
LineaRollupClient,
EthersLineaRollupLogClient,
L1ClaimingService,
LineaRollupMessageRetriever,
MerkleTreeService,
} from "./clients/ethereum";
import {
L2MessageServiceClient,
EthersL2MessageServiceLogClient,
L2MessageServiceMessageRetriever,
} from "./clients/linea";
import { DefaultGasProvider, GasProvider } from "./clients/gas";
import { Provider, LineaProvider, BrowserProvider, LineaBrowserProvider } from "./clients/providers";
import {
DEFAULT_ENABLE_LINEA_ESTIMATE_GAS,
DEFAULT_ENFORCE_MAX_GAS_FEE,
DEFAULT_GAS_ESTIMATION_PERCENTILE,
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";
export class LineaSDK {
private network: Network;
private l1Signer?: Signer;
private l2Signer?: Signer;
private l1Provider: Provider | BrowserProvider;
private l2Provider: LineaProvider | LineaBrowserProvider;
private l1FeeEstimatorOptions: Required<L1FeeEstimatorOptions>;
private l2FeeEstimatorOptions: Required<L2FeeEstimatorOptions>;
private mode: SDKMode;
private l2MessageTreeDepth: number;
/**
* Initializes a new instance of the LineaSDK with the specified options.
*
* @param {LineaSDKOptions} options - Configuration options for the SDK, including network details, operational mode, and optional settings for L2 message tree depth and fee estimation.
*/
constructor(options: LineaSDKOptions) {
const {
network,
mode,
l2MessageTreeDepth = DEFAULT_L2_MESSAGE_TREE_DEPTH,
l1RpcUrlOrProvider,
l2RpcUrlOrProvider,
l1FeeEstimatorOptions = {},
l2FeeEstimatorOptions = {},
} = options;
this.network = network;
this.mode = mode;
this.l2MessageTreeDepth = l2MessageTreeDepth;
this.l1Provider = this.getL1Provider(l1RpcUrlOrProvider);
this.l2Provider = this.getL2Provider(l2RpcUrlOrProvider);
if (mode === "read-write") {
const { l1SignerPrivateKeyOrWallet, l2SignerPrivateKeyOrWallet } = options;
if (!l1SignerPrivateKeyOrWallet || !l2SignerPrivateKeyOrWallet) {
throw new BaseError("You need to provide both L1 and L2 signer private keys or wallets.");
}
this.l1Signer = this.getWallet(l1SignerPrivateKeyOrWallet).connect(this.l1Provider);
this.l2Signer = this.getWallet(l2SignerPrivateKeyOrWallet).connect(this.l2Provider);
}
const { maxFeePerGasCap, gasFeeEstimationPercentile, enforceMaxGasFee } = l1FeeEstimatorOptions;
const {
maxFeePerGasCap: l2MaxFeePerGasCap,
gasFeeEstimationPercentile: l2GasFeeEstimationPercentile,
enableLineaEstimateGas: l2EnableLineaEstimateGas,
enforceMaxGasFee: l2EnforceMaxGasFee,
} = l2FeeEstimatorOptions;
this.l1FeeEstimatorOptions = {
maxFeePerGasCap: maxFeePerGasCap || DEFAULT_MAX_FEE_PER_GAS_CAP,
gasFeeEstimationPercentile: gasFeeEstimationPercentile || DEFAULT_GAS_ESTIMATION_PERCENTILE,
enforceMaxGasFee: enforceMaxGasFee || DEFAULT_ENFORCE_MAX_GAS_FEE,
};
this.l2FeeEstimatorOptions = {
maxFeePerGasCap: l2MaxFeePerGasCap || DEFAULT_MAX_FEE_PER_GAS_CAP,
gasFeeEstimationPercentile: l2GasFeeEstimationPercentile || DEFAULT_GAS_ESTIMATION_PERCENTILE,
enforceMaxGasFee: l2EnforceMaxGasFee || DEFAULT_ENFORCE_MAX_GAS_FEE,
enableLineaEstimateGas: l2EnableLineaEstimateGas || DEFAULT_ENABLE_LINEA_ESTIMATE_GAS,
};
}
/**
* Gets the L1 provider instance based on the provided RPC URL or provider.
* @param {string | Eip1193Provider} l1RpcUrlOrProvider - The L1 RPC URL or EIP-1193 provider
* @returns {Provider | BrowserProvider} The configured L1 provider
* @throws {BaseError} If the provided argument is invalid
*/
public getL1Provider(l1RpcUrlOrProvider: string | Eip1193Provider): Provider | BrowserProvider {
if (isString(l1RpcUrlOrProvider)) {
return new Provider(l1RpcUrlOrProvider);
}
if (this.isEip1193Provider(l1RpcUrlOrProvider)) {
return new BrowserProvider(l1RpcUrlOrProvider);
}
throw new BaseError("Invalid argument: l1RpcUrlOrProvider must be a string or Eip1193Provider");
}
/**
* Gets the L1 signer instance.
* @returns {Signer} The L1 signer
* @throws {BaseError} If the signer is not available in read-only mode
*/
public getL1Signer(): Signer {
if (!this.l1Signer) {
throw new BaseError("L1 signer is not available in read-only mode.");
}
return this.l1Signer;
}
/**
* Gets the L2 signer instance.
* @returns {Signer} The L2 signer
* @throws {BaseError} If the signer is not available in read-only mode
*/
public getL2Signer(): Signer {
if (!this.l2Signer) {
throw new BaseError("L2 signer is not available in read-only mode.");
}
return this.l2Signer;
}
/**
* Gets the L1 gas provider configured with the SDK's fee estimation options.
* @returns {DefaultGasProvider} The configured L1 gas provider
*/
public getL1GasProvider(): DefaultGasProvider {
return new DefaultGasProvider(this.l1Provider, {
maxFeePerGasCap: this.l1FeeEstimatorOptions.maxFeePerGasCap,
gasEstimationPercentile: this.l1FeeEstimatorOptions.gasFeeEstimationPercentile,
enforceMaxGasFee: this.l1FeeEstimatorOptions.enforceMaxGasFee,
});
}
/**
* Gets the L2 gas provider configured with the SDK's fee estimation options.
* @returns {GasProvider} The configured L2 gas provider
*/
public getL2GasProvider(): GasProvider {
return new GasProvider(this.l2Provider, {
maxFeePerGasCap: this.l2FeeEstimatorOptions.maxFeePerGasCap,
enforceMaxGasFee: this.l2FeeEstimatorOptions.enforceMaxGasFee,
gasEstimationPercentile: this.l2FeeEstimatorOptions.gasFeeEstimationPercentile,
direction: Direction.L1_TO_L2,
enableLineaEstimateGas: this.l2FeeEstimatorOptions.enableLineaEstimateGas,
});
}
/**
* Gets the L2 provider instance based on the provided RPC URL or provider.
* @param {string | Eip1193Provider} l2RpcUrlOrProvider - The L2 RPC URL or EIP-1193 provider
* @returns {LineaProvider | LineaBrowserProvider} The configured L2 provider
* @throws {Error} If the provided argument is invalid
*/
public getL2Provider(l2RpcUrlOrProvider: string | Eip1193Provider): LineaProvider | LineaBrowserProvider {
if (isString(l2RpcUrlOrProvider)) {
return new LineaProvider(l2RpcUrlOrProvider);
}
if (this.isEip1193Provider(l2RpcUrlOrProvider)) {
return new LineaBrowserProvider(l2RpcUrlOrProvider);
}
throw new Error("Invalid argument: l2RpcUrlOrProvider must be a string or Eip1193Provider");
}
/**
* Creates an instance of the `EthersLineaRollupLogClient` for interacting with L1 contract event logs.
*
* @param {string} [localL1ContractAddress] - Optional custom L1 contract address. Required if the network is set to 'custom'.
* @returns {EthersLineaRollupLogClient} An instance of the L1 message service log client.
*/
public getL1ContractEventLogClient(localL1ContractAddress?: string): EthersLineaRollupLogClient {
const l1ContractAddress = this.getContractAddress("l1", localL1ContractAddress);
return new EthersLineaRollupLogClient(this.l1Provider, l1ContractAddress);
}
/**
* Creates an instance of the `EthersL2MessageServiceLogClient` for interacting with L2 contract event logs.
*
* @param {string} [localL2ContractAddress] - Optional custom L2 contract address. Required if the network is set to 'custom'.
* @returns {EthersL2MessageServiceLogClient} An instance of the L2 message service log client.
*/
public getL2ContractEventLogClient(localL2ContractAddress?: string): EthersL2MessageServiceLogClient {
const l2ContractAddress = this.getContractAddress("l2", localL2ContractAddress);
return new EthersL2MessageServiceLogClient(this.l2Provider, l2ContractAddress);
}
/**
* Retrieves an instance of the `LineaRollupClient` for interacting with the L1 contract.
*
* @param {string} [localL1ContractAddress] - Optional custom L1 contract address. Required if the network is set to 'custom'.
* @param {string} [localL2ContractAddress] - Optional custom L2 contract address. Required if the network is set to 'custom'.
* @returns {LineaRollupClient} An instance of the `LineaRollupClient` configured for the specified L1 contract.
*/
public getL1Contract(localL1ContractAddress?: string, localL2ContractAddress?: string): LineaRollupClient {
const l1ContractAddress = this.getContractAddress("l1", localL1ContractAddress);
const l2ContractAddress = this.getContractAddress("l2", localL2ContractAddress);
const lineaRollupLogClient = new EthersLineaRollupLogClient(this.l1Provider, l1ContractAddress);
const l2MessageServiceLogClient = this.getL2ContractEventLogClient(l2ContractAddress);
return new LineaRollupClient(
this.l1Provider,
l1ContractAddress,
lineaRollupLogClient,
l2MessageServiceLogClient,
this.getL1GasProvider(),
new LineaRollupMessageRetriever(this.l1Provider, lineaRollupLogClient, l1ContractAddress),
new MerkleTreeService(
this.l1Provider,
l1ContractAddress,
lineaRollupLogClient,
l2MessageServiceLogClient,
this.l2MessageTreeDepth,
),
this.mode,
this.l1Signer,
);
}
/**
* Retrieves an instance of the `L2MessageServiceClient` for interacting with the L2 contract.
*
* @param {string} [localContractAddress] - Optional custom L2 contract address. Required if the network is set to 'custom'.
* @returns {L2MessageServiceClient} An instance of the `L2MessageServiceClient` configured for the specified L2 contract.
*/
public getL2Contract(localContractAddress?: string): L2MessageServiceClient {
const l2ContractAddress = this.getContractAddress("l2", localContractAddress);
const l2MessageServiceContract = new L2MessageServiceClient(
this.l2Provider,
l2ContractAddress,
new L2MessageServiceMessageRetriever(
this.l2Provider,
this.getL2ContractEventLogClient(l2ContractAddress),
l2ContractAddress,
),
this.getL2GasProvider(),
this.mode,
this.l2Signer,
);
return l2MessageServiceContract;
}
/**
* Creates an instance of the `L1ClaimingService` for managing message claiming on L1.
*
* @param {string} [localL1ContractAddress] - Optional custom L1 contract address. Required if the network is set to 'custom'.
* @param {string} [localL2ContractAddress] - Optional custom L2 contract address. Required if the network is set to 'custom'.
* @returns {L1ClaimingService} An instance of the `L1ClaimingService` configured for the specified contract addresses.
*/
public getL1ClaimingService(localL1ContractAddress?: string, localL2ContractAddress?: string): L1ClaimingService {
return new L1ClaimingService(
this.getL1Contract(localL1ContractAddress, localL2ContractAddress),
this.getL2Contract(localL2ContractAddress),
this.getL2ContractEventLogClient(localL2ContractAddress),
this.network,
);
}
/**
* Retrieves the contract address for the specified contract type.
* @param contractType The type of contract to retrieve the address for.
* @param localContractAddress Optional custom contract address.
* @returns The contract address for the specified contract type.
*/
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.`);
}
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}.`);
}
return contractAddress;
}
}
/**
* Creates a wallet instance from a private key or existing wallet.
* @param {string | Wallet} privateKeyOrWallet - The private key or wallet instance
* @returns {Signer} The configured signer
* @throws {BaseError} If the private key is invalid
* @private
*/
private getWallet(privateKeyOrWallet: string | Wallet): Signer {
try {
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 e;
}
}
/**
* Type guard to check if an object implements the EIP-1193 provider interface.
* @param {any} obj - The object to check
* @returns {boolean} True if the object is an EIP-1193 provider
* @private
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private isEip1193Provider(obj: any): obj is Eip1193Provider {
return obj && typeof obj.request === "function";
}
}

View File

@@ -1,481 +0,0 @@
import { Wallet, JsonRpcProvider } from "ethers";
import { DataSource } from "typeorm";
import { ILogger } from "../../../core/utils/logging/ILogger";
import { DatabaseCleaner } from "../../../services/persistence/DatabaseCleaner";
import { TypeOrmMessageRepository } from "../persistence/repositories/TypeOrmMessageRepository";
import { LineaRollupClient } from "../../../clients/blockchain/ethereum/LineaRollupClient";
import { L2MessageServiceClient } from "../../../clients/blockchain/linea/L2MessageServiceClient";
import { EthersLineaRollupLogClient } from "../../../clients/blockchain/ethereum/EthersLineaRollupLogClient";
import { ChainQuerier } from "../../../clients/blockchain/ChainQuerier";
import { WinstonLogger } from "../../../utils/WinstonLogger";
import { EthersL2MessageServiceLogClient } from "../../../clients/blockchain/linea/EthersL2MessageServiceLogClient";
import { MessageSentEventPoller } from "../../../services/pollers/MessageSentEventPoller";
import { IPoller } from "../../../core/services/pollers/IPoller";
import { MessageAnchoringPoller } from "../../../services/pollers/MessageAnchoringPoller";
import { MessageAnchoringProcessor } from "../../../services/processors/MessageAnchoringProcessor";
import { PostmanOptions } from "./config/config";
import { DB } from "../persistence/dataSource";
import { Direction } from "../../../core/enums/MessageEnums";
import { MessageClaimingProcessor } from "../../../services/processors/MessageClaimingProcessor";
import { MessageClaimingPoller } from "../../../services/pollers/MessageClaimingPoller";
import { MessageClaimingPersister } from "../../../services/processors/MessageClaimingPersister";
import { MessagePersistingPoller } from "../../../services/pollers/MessagePersistingPoller";
import { MessageSentEventProcessor } from "../../../services/processors/MessageSentEventProcessor";
import { DatabaseCleaningPoller } from "../../../services/pollers/DatabaseCleaningPoller";
import { BaseError } from "../../../core/errors/Base";
import { LineaRollupMessageRetriever } from "../../../clients/blockchain/ethereum/LineaRollupMessageRetriever";
import { L2MessageServiceMessageRetriever } from "../../../clients/blockchain/linea/L2MessageServiceMessageRetriever";
import { MerkleTreeService } from "../../../clients/blockchain/ethereum/MerkleTreeService";
import { LineaMessageDBService } from "../../../services/persistence/LineaMessageDBService";
import { L2ChainQuerier } from "../../../clients/blockchain/linea/L2ChainQuerier";
import { EthereumMessageDBService } from "../../../services/persistence/EthereumMessageDBService";
import { L2ClaimMessageTransactionSizePoller } from "../../../services/pollers/L2ClaimMessageTransactionSizePoller";
import { L2ClaimMessageTransactionSizeProcessor } from "../../../services/processors/L2ClaimMessageTransactionSizeProcessor";
import { L2ClaimTransactionSizeCalculator } from "../../../services/L2ClaimTransactionSizeCalculator";
import { GasProvider } from "../../../clients/blockchain/gas/GasProvider";
import { LineaTransactionValidationService } from "../../../services/LineaTransactionValidationService";
import { EthereumTransactionValidationService } from "../../../services/EthereumTransactionValidationService";
import { getConfig } from "./config/utils";
export class PostmanServiceClient {
// L1 -> L2 flow
private l1MessageSentEventPoller: IPoller;
private l2MessageAnchoringPoller: IPoller;
private l2MessageClaimingPoller: IPoller;
private l2MessagePersistingPoller: IPoller;
private l2ClaimMessageTransactionSizePoller: IPoller;
// L2 -> L1 flow
private l2MessageSentEventPoller: IPoller;
private l1MessageAnchoringPoller: IPoller;
private l1MessageClaimingPoller: IPoller;
private l1MessagePersistingPoller: IPoller;
// Database Cleaner
private databaseCleaningPoller: IPoller;
private logger: ILogger;
private db: DataSource;
private l1L2AutoClaimEnabled: boolean;
private l2L1AutoClaimEnabled: boolean;
/**
* Initializes a new instance of the PostmanServiceClient.
*
* @param {PostmanOptions} options - Configuration options for the Postman service, including network details, database options, and logging configurations.
*/
constructor(options: PostmanOptions) {
const config = getConfig(options);
this.logger = new WinstonLogger(PostmanServiceClient.name, config.loggerOptions);
this.l1L2AutoClaimEnabled = config.l1L2AutoClaimEnabled;
this.l2L1AutoClaimEnabled = config.l2L1AutoClaimEnabled;
const l1Provider = new JsonRpcProvider(config.l1Config.rpcUrl);
const l2Provider = new JsonRpcProvider(config.l2Config.rpcUrl);
const l1Signer = this.getSigner(config.l1Config.claiming.signerPrivateKey, l1Provider);
const l2Signer = this.getSigner(config.l2Config.claiming.signerPrivateKey, l2Provider);
const l1Querier = new ChainQuerier(l1Provider, l1Signer);
const l2Querier = new L2ChainQuerier(l2Provider, l2Signer);
const lineaRollupLogClient = new EthersLineaRollupLogClient(
l1Provider,
config.l1Config.messageServiceContractAddress,
);
const l2MessageServiceLogClient = new EthersL2MessageServiceLogClient(
l2Provider,
config.l2Config.messageServiceContractAddress,
);
const l1GasProvider = new GasProvider(l1Querier, {
maxFeePerGas: config.l1Config.claiming.maxFeePerGas,
gasEstimationPercentile: config.l1Config.claiming.gasEstimationPercentile,
enforceMaxGasFee: config.l1Config.claiming.isMaxGasFeeEnforced,
enableLineaEstimateGas: false,
direction: Direction.L2_TO_L1,
});
const l2GasProvider = new GasProvider(l2Querier, {
maxFeePerGas: config.l2Config.claiming.maxFeePerGas,
gasEstimationPercentile: config.l2Config.claiming.gasEstimationPercentile,
enforceMaxGasFee: config.l2Config.claiming.isMaxGasFeeEnforced,
enableLineaEstimateGas: config.l2Config.enableLineaEstimateGas,
direction: Direction.L1_TO_L2,
});
const lineaRollupMessageRetriever = new LineaRollupMessageRetriever(
l1Querier,
lineaRollupLogClient,
config.l1Config.messageServiceContractAddress,
);
const l1MerkleTreeService = new MerkleTreeService(
l1Querier,
config.l1Config.messageServiceContractAddress,
lineaRollupLogClient,
l2MessageServiceLogClient,
config.l2Config.l2MessageTreeDepth,
);
const l2MessageServiceMessageRetriever = new L2MessageServiceMessageRetriever(
l2Querier,
l2MessageServiceLogClient,
config.l2Config.messageServiceContractAddress,
);
const l1MessageServiceContract = new LineaRollupClient(
l1Querier,
config.l1Config.messageServiceContractAddress,
lineaRollupLogClient,
l2MessageServiceLogClient,
l1GasProvider,
lineaRollupMessageRetriever,
l1MerkleTreeService,
"read-write",
l1Signer,
);
const l2MessageServiceContract = new L2MessageServiceClient(
l2Querier,
config.l2Config.messageServiceContractAddress,
l2MessageServiceMessageRetriever,
l2GasProvider,
"read-write",
l2Signer,
);
this.db = DB.create(config.databaseOptions);
const messageRepository = new TypeOrmMessageRepository(this.db);
const lineaMessageDBService = new LineaMessageDBService(l2Querier, messageRepository);
const ethereumMessageDBService = new EthereumMessageDBService(l1GasProvider, messageRepository);
// L1 -> L2 flow
const l1MessageSentEventProcessor = new MessageSentEventProcessor(
lineaMessageDBService,
lineaRollupLogClient,
l1Querier,
{
direction: Direction.L1_TO_L2,
maxBlocksToFetchLogs: config.l1Config.listener.maxBlocksToFetchLogs,
blockConfirmation: config.l1Config.listener.blockConfirmation,
isEOAEnabled: config.l1Config.isEOAEnabled,
isCalldataEnabled: config.l1Config.isCalldataEnabled,
},
new WinstonLogger(`L1${MessageSentEventProcessor.name}`, config.loggerOptions),
);
this.l1MessageSentEventPoller = new MessageSentEventPoller(
l1MessageSentEventProcessor,
l1Querier,
lineaMessageDBService,
{
direction: Direction.L1_TO_L2,
pollingInterval: config.l1Config.listener.pollingInterval,
initialFromBlock: config.l1Config.listener.initialFromBlock,
originContractAddress: config.l1Config.messageServiceContractAddress,
},
new WinstonLogger(`L1${MessageSentEventPoller.name}`, config.loggerOptions),
);
const l2MessageAnchoringProcessor = new MessageAnchoringProcessor(
l2MessageServiceContract,
l2Querier,
lineaMessageDBService,
{
maxFetchMessagesFromDb: config.l1Config.listener.maxFetchMessagesFromDb,
originContractAddress: config.l1Config.messageServiceContractAddress,
},
new WinstonLogger(`L2${MessageAnchoringProcessor.name}`, config.loggerOptions),
);
this.l2MessageAnchoringPoller = new MessageAnchoringPoller(
l2MessageAnchoringProcessor,
{
direction: Direction.L1_TO_L2,
pollingInterval: config.l2Config.listener.pollingInterval,
},
new WinstonLogger(`L2${MessageAnchoringPoller.name}`, config.loggerOptions),
);
const l2TransactionValidationService = new LineaTransactionValidationService(
{
profitMargin: config.l2Config.claiming.profitMargin,
maxClaimGasLimit: BigInt(config.l2Config.claiming.maxClaimGasLimit),
},
l2Querier,
l2MessageServiceContract,
);
const l2MessageClaimingProcessor = new MessageClaimingProcessor(
l2MessageServiceContract,
l2Querier,
lineaMessageDBService,
l2TransactionValidationService,
{
direction: Direction.L1_TO_L2,
originContractAddress: config.l1Config.messageServiceContractAddress,
maxNonceDiff: config.l2Config.claiming.maxNonceDiff,
feeRecipientAddress: config.l2Config.claiming.feeRecipientAddress,
profitMargin: config.l2Config.claiming.profitMargin,
maxNumberOfRetries: config.l2Config.claiming.maxNumberOfRetries,
retryDelayInSeconds: config.l2Config.claiming.retryDelayInSeconds,
maxClaimGasLimit: BigInt(config.l2Config.claiming.maxClaimGasLimit),
},
new WinstonLogger(`L2${MessageClaimingProcessor.name}`, config.loggerOptions),
);
this.l2MessageClaimingPoller = new MessageClaimingPoller(
l2MessageClaimingProcessor,
{
direction: Direction.L1_TO_L2,
pollingInterval: config.l2Config.listener.pollingInterval,
},
new WinstonLogger(`L2${MessageClaimingPoller.name}`, config.loggerOptions),
);
const l2MessageClaimingPersister = new MessageClaimingPersister(
lineaMessageDBService,
l2MessageServiceContract,
l2Querier,
{
direction: Direction.L1_TO_L2,
messageSubmissionTimeout: config.l2Config.claiming.messageSubmissionTimeout,
maxTxRetries: config.l2Config.claiming.maxTxRetries,
},
new WinstonLogger(`L2${MessageClaimingPersister.name}`, config.loggerOptions),
);
this.l2MessagePersistingPoller = new MessagePersistingPoller(
l2MessageClaimingPersister,
{
direction: Direction.L1_TO_L2,
pollingInterval: config.l2Config.listener.pollingInterval,
},
new WinstonLogger(`L2${MessagePersistingPoller.name}`, config.loggerOptions),
);
const transactionSizeCalculator = new L2ClaimTransactionSizeCalculator(l2MessageServiceContract);
const transactionSizeCompressor = new L2ClaimMessageTransactionSizeProcessor(
lineaMessageDBService,
l2MessageServiceContract,
transactionSizeCalculator,
{
direction: Direction.L1_TO_L2,
originContractAddress: config.l1Config.messageServiceContractAddress,
},
new WinstonLogger(`${L2ClaimMessageTransactionSizeProcessor.name}`, config.loggerOptions),
);
this.l2ClaimMessageTransactionSizePoller = new L2ClaimMessageTransactionSizePoller(
transactionSizeCompressor,
{
pollingInterval: config.l2Config.listener.pollingInterval,
},
new WinstonLogger(`${L2ClaimMessageTransactionSizePoller.name}`, config.loggerOptions),
);
// L2 -> L1 flow
const l2MessageSentEventProcessor = new MessageSentEventProcessor(
ethereumMessageDBService,
l2MessageServiceLogClient,
l2Querier,
{
direction: Direction.L2_TO_L1,
maxBlocksToFetchLogs: config.l2Config.listener.maxBlocksToFetchLogs,
blockConfirmation: config.l2Config.listener.blockConfirmation,
isEOAEnabled: config.l2Config.isEOAEnabled,
isCalldataEnabled: config.l2Config.isCalldataEnabled,
},
new WinstonLogger(`L2${MessageSentEventProcessor.name}`, config.loggerOptions),
);
this.l2MessageSentEventPoller = new MessageSentEventPoller(
l2MessageSentEventProcessor,
l2Querier,
ethereumMessageDBService,
{
direction: Direction.L2_TO_L1,
pollingInterval: config.l2Config.listener.pollingInterval,
initialFromBlock: config.l2Config.listener.initialFromBlock,
originContractAddress: config.l2Config.messageServiceContractAddress,
},
new WinstonLogger(`L2${MessageSentEventPoller.name}`, config.loggerOptions),
);
const l1MessageAnchoringProcessor = new MessageAnchoringProcessor(
l1MessageServiceContract,
l1Querier,
ethereumMessageDBService,
{
maxFetchMessagesFromDb: config.l1Config.listener.maxFetchMessagesFromDb,
originContractAddress: config.l2Config.messageServiceContractAddress,
},
new WinstonLogger(`L1${MessageAnchoringProcessor.name}`, config.loggerOptions),
);
this.l1MessageAnchoringPoller = new MessageAnchoringPoller(
l1MessageAnchoringProcessor,
{
direction: Direction.L2_TO_L1,
pollingInterval: config.l1Config.listener.pollingInterval,
},
new WinstonLogger(`L1${MessageAnchoringPoller.name}`, config.loggerOptions),
);
const l1TransactionValidationService = new EthereumTransactionValidationService(
l1MessageServiceContract,
l1GasProvider,
{
profitMargin: config.l1Config.claiming.profitMargin,
maxClaimGasLimit: BigInt(config.l1Config.claiming.maxClaimGasLimit),
},
);
const l1MessageClaimingProcessor = new MessageClaimingProcessor(
l1MessageServiceContract,
l1Querier,
ethereumMessageDBService,
l1TransactionValidationService,
{
direction: Direction.L2_TO_L1,
maxNonceDiff: config.l1Config.claiming.maxNonceDiff,
feeRecipientAddress: config.l1Config.claiming.feeRecipientAddress,
profitMargin: config.l1Config.claiming.profitMargin,
maxNumberOfRetries: config.l1Config.claiming.maxNumberOfRetries,
retryDelayInSeconds: config.l1Config.claiming.retryDelayInSeconds,
maxClaimGasLimit: BigInt(config.l1Config.claiming.maxClaimGasLimit),
originContractAddress: config.l2Config.messageServiceContractAddress,
},
new WinstonLogger(`L1${MessageClaimingProcessor.name}`, config.loggerOptions),
);
this.l1MessageClaimingPoller = new MessageClaimingPoller(
l1MessageClaimingProcessor,
{
direction: Direction.L2_TO_L1,
pollingInterval: config.l1Config.listener.pollingInterval,
},
new WinstonLogger(`L1${MessageClaimingPoller.name}`, config.loggerOptions),
);
const l1MessageClaimingPersister = new MessageClaimingPersister(
ethereumMessageDBService,
l1MessageServiceContract,
l1Querier,
{
direction: Direction.L2_TO_L1,
messageSubmissionTimeout: config.l1Config.claiming.messageSubmissionTimeout,
maxTxRetries: config.l1Config.claiming.maxTxRetries,
},
new WinstonLogger(`L1${MessageClaimingPersister.name}`, config.loggerOptions),
);
this.l1MessagePersistingPoller = new MessagePersistingPoller(
l1MessageClaimingPersister,
{
direction: Direction.L2_TO_L1,
pollingInterval: config.l1Config.listener.pollingInterval,
},
new WinstonLogger(`L1${MessagePersistingPoller.name}`, config.loggerOptions),
);
// Database Cleaner
const databaseCleaner = new DatabaseCleaner(
ethereumMessageDBService,
new WinstonLogger(`${DatabaseCleaner.name}`, config.loggerOptions),
);
this.databaseCleaningPoller = new DatabaseCleaningPoller(
databaseCleaner,
new WinstonLogger(`${DatabaseCleaningPoller.name}`, config.loggerOptions),
{
enabled: config.databaseCleanerConfig.enabled,
daysBeforeNowToDelete: config.databaseCleanerConfig.daysBeforeNowToDelete,
cleaningInterval: config.databaseCleanerConfig.cleaningInterval,
},
);
}
/**
* Creates a Wallet instance as a signer using the provided private key and JSON RPC provider.
*
* @param {string} privateKey - The private key to use for the signer.
* @param {JsonRpcProvider} provider - The JSON RPC provider associated with the network.
* @returns {Wallet} A Wallet instance configured with the provided private key and provider.
*/
private getSigner(privateKey: string, provider: JsonRpcProvider): Wallet {
try {
return new Wallet(privateKey, provider);
} catch (e) {
throw new BaseError(
"Something went wrong when trying to generate Wallet. Please check your private key and the provider url.",
);
}
}
/**
* Initializes the database connection using the configuration provided.
*/
public async connectDatabase() {
await this.db.initialize();
}
/**
* Starts all configured services and pollers. This includes message event pollers for both L1 to L2 and L2 to L1 flows, message anchoring, claiming, persisting pollers, and the database cleaning poller.
*/
public startAllServices(): void {
if (this.l1L2AutoClaimEnabled) {
// L1 -> L2 flow
this.l1MessageSentEventPoller.start();
this.l2MessageAnchoringPoller.start();
this.l2MessageClaimingPoller.start();
this.l2MessagePersistingPoller.start();
this.l2ClaimMessageTransactionSizePoller.start();
}
if (this.l2L1AutoClaimEnabled) {
// L2 -> L1 flow
this.l2MessageSentEventPoller.start();
this.l1MessageAnchoringPoller.start();
this.l1MessageClaimingPoller.start();
this.l1MessagePersistingPoller.start();
}
// Database Cleaner
this.databaseCleaningPoller.start();
this.logger.info("All listeners and message deliverers have been started.");
}
/**
* Stops all running services and pollers to gracefully shut down the Postman service.
*/
public stopAllServices(): void {
if (this.l1L2AutoClaimEnabled) {
// L1 -> L2 flow
this.l1MessageSentEventPoller.stop();
this.l2MessageAnchoringPoller.stop();
this.l2MessageClaimingPoller.stop();
this.l2MessagePersistingPoller.stop();
this.l2ClaimMessageTransactionSizePoller.stop();
}
if (this.l2L1AutoClaimEnabled) {
// L2 -> L1 flow
this.l2MessageSentEventPoller.stop();
this.l1MessageAnchoringPoller.stop();
this.l1MessageClaimingPoller.stop();
this.l1MessagePersistingPoller.stop();
}
// Database Cleaner
this.databaseCleaningPoller.stop();
this.logger.info("All listeners and message deliverers have been stopped.");
}
}

View File

@@ -1,197 +0,0 @@
import { describe, it } from "@jest/globals";
import { mockClear } from "jest-mock-extended";
import { DataSource } from "typeorm";
import { SnakeNamingStrategy } from "typeorm-naming-strategies";
import { PostmanServiceClient } from "../PostmanServiceClient";
import {
TEST_CONTRACT_ADDRESS_1,
TEST_CONTRACT_ADDRESS_2,
TEST_L1_SIGNER_PRIVATE_KEY,
TEST_L2_SIGNER_PRIVATE_KEY,
TEST_RPC_URL,
} from "../../../../utils/testing/constants";
import { WinstonLogger } from "../../../../utils/WinstonLogger";
import { PostmanOptions } from "../config/config";
import { MessageEntity } from "../../persistence/entities/Message.entity";
import { InitialDatabaseSetup1685985945638 } from "../../persistence/migrations/1685985945638-InitialDatabaseSetup";
import { AddNewColumns1687890326970 } from "../../persistence/migrations/1687890326970-AddNewColumns";
import { UpdateStatusColumn1687890694496 } from "../../persistence/migrations/1687890694496-UpdateStatusColumn";
import { RemoveUniqueConstraint1689084924789 } from "../../persistence/migrations/1689084924789-RemoveUniqueConstraint";
import { AddNewIndexes1701265652528 } from "../../persistence/migrations/1701265652528-AddNewIndexes";
import { MessageSentEventPoller } from "../../../../services/pollers/MessageSentEventPoller";
import { MessageAnchoringPoller } from "../../../../services/pollers/MessageAnchoringPoller";
import { MessageClaimingPoller } from "../../../../services/pollers/MessageClaimingPoller";
import { MessagePersistingPoller } from "../../../../services/pollers/MessagePersistingPoller";
import { DatabaseCleaningPoller } from "../../../../services/pollers/DatabaseCleaningPoller";
import { TypeOrmMessageRepository } from "../../persistence/repositories/TypeOrmMessageRepository";
import { L2ClaimMessageTransactionSizePoller } from "../../../../services/pollers/L2ClaimMessageTransactionSizePoller";
import { DEFAULT_MAX_CLAIM_GAS_LIMIT } from "../../../../core/constants";
jest.mock("ethers", () => {
const allAutoMocked = jest.createMockFromModule("ethers");
const actual = jest.requireActual("ethers");
return {
__esModules: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
...allAutoMocked,
Wallet: actual.Wallet,
};
});
const postmanServiceClientOptions: PostmanOptions = {
l1Options: {
rpcUrl: TEST_RPC_URL,
messageServiceContractAddress: TEST_CONTRACT_ADDRESS_1,
isEOAEnabled: true,
isCalldataEnabled: false,
listener: {
pollingInterval: 4000,
maxFetchMessagesFromDb: 1000,
maxBlocksToFetchLogs: 1000,
},
claiming: {
signerPrivateKey: TEST_L1_SIGNER_PRIVATE_KEY,
messageSubmissionTimeout: 300000,
maxNonceDiff: 10000,
maxFeePerGas: 100000000000n,
gasEstimationPercentile: 50,
profitMargin: 1.0,
maxNumberOfRetries: 100,
retryDelayInSeconds: 30,
},
},
l2Options: {
rpcUrl: TEST_RPC_URL,
messageServiceContractAddress: TEST_CONTRACT_ADDRESS_2,
isEOAEnabled: true,
isCalldataEnabled: false,
listener: {
pollingInterval: 4000,
maxFetchMessagesFromDb: 1000,
maxBlocksToFetchLogs: 1000,
},
claiming: {
signerPrivateKey: TEST_L2_SIGNER_PRIVATE_KEY,
messageSubmissionTimeout: 300000,
maxNonceDiff: 10000,
maxFeePerGas: 100000000000n,
gasEstimationPercentile: 50,
profitMargin: 1.0,
maxNumberOfRetries: 100,
retryDelayInSeconds: 30,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
},
},
l1L2AutoClaimEnabled: true,
l2L1AutoClaimEnabled: true,
loggerOptions: {
silent: true,
},
databaseOptions: {
type: "postgres",
host: "127.0.0.1",
port: 5432,
username: "postgres",
password: "postgres",
database: "db_name",
},
};
describe("PostmanServiceClient", () => {
let postmanServiceClient: PostmanServiceClient;
let loggerSpy: unknown;
beforeEach(() => {
postmanServiceClient = new PostmanServiceClient(postmanServiceClientOptions);
loggerSpy = jest.spyOn(WinstonLogger.prototype, "info");
});
afterEach(() => {
mockClear(loggerSpy);
});
describe("constructor", () => {
it("should throw an error when at least one private key is invalid", () => {
const postmanServiceClientOptionsWithInvalidPrivateKey: PostmanOptions = {
...postmanServiceClientOptions,
l1Options: {
...postmanServiceClientOptions.l1Options,
claiming: {
...postmanServiceClientOptions.l1Options.claiming,
signerPrivateKey: "",
},
},
};
expect(() => new PostmanServiceClient(postmanServiceClientOptionsWithInvalidPrivateKey)).toThrow(
new Error(
"Something went wrong when trying to generate Wallet. Please check your private key and the provider url.",
),
);
});
});
describe("connectDatabase", () => {
it("should initialize the db", async () => {
const initializeSpy = jest.spyOn(DataSource.prototype, "initialize").mockResolvedValue(
new DataSource({
type: "postgres",
host: "127.0.0.1",
port: 5432,
username: "postgres",
password: "postgres",
database: "db_name",
entities: [MessageEntity],
namingStrategy: new SnakeNamingStrategy(),
migrations: [
InitialDatabaseSetup1685985945638,
AddNewColumns1687890326970,
UpdateStatusColumn1687890694496,
RemoveUniqueConstraint1689084924789,
AddNewIndexes1701265652528,
],
migrationsTableName: "migrations",
logging: ["error"],
migrationsRun: true,
}),
);
await postmanServiceClient.connectDatabase();
expect(initializeSpy).toHaveBeenCalledTimes(1);
});
});
describe("startAllServices", () => {
it("should start all postman services", () => {
jest.spyOn(MessageSentEventPoller.prototype, "start").mockImplementationOnce(jest.fn());
jest.spyOn(MessageAnchoringPoller.prototype, "start").mockImplementationOnce(jest.fn());
jest.spyOn(MessageClaimingPoller.prototype, "start").mockImplementationOnce(jest.fn());
jest.spyOn(L2ClaimMessageTransactionSizePoller.prototype, "start").mockImplementationOnce(jest.fn());
jest.spyOn(MessagePersistingPoller.prototype, "start").mockImplementationOnce(jest.fn());
jest.spyOn(DatabaseCleaningPoller.prototype, "start").mockImplementationOnce(jest.fn());
jest.spyOn(TypeOrmMessageRepository.prototype, "getLatestMessageSent").mockImplementationOnce(jest.fn());
postmanServiceClient.startAllServices();
expect(loggerSpy).toHaveBeenCalledTimes(5);
expect(loggerSpy).toHaveBeenCalledWith("All listeners and message deliverers have been started.");
postmanServiceClient.stopAllServices();
});
it("should stop all postman services", () => {
jest.spyOn(MessageSentEventPoller.prototype, "stop").mockImplementationOnce(jest.fn());
jest.spyOn(MessageAnchoringPoller.prototype, "stop").mockImplementationOnce(jest.fn());
jest.spyOn(MessageClaimingPoller.prototype, "stop").mockImplementationOnce(jest.fn());
jest.spyOn(L2ClaimMessageTransactionSizePoller.prototype, "stop").mockImplementationOnce(jest.fn());
jest.spyOn(MessagePersistingPoller.prototype, "stop").mockImplementationOnce(jest.fn());
jest.spyOn(DatabaseCleaningPoller.prototype, "stop").mockImplementationOnce(jest.fn());
postmanServiceClient.stopAllServices();
expect(loggerSpy).toHaveBeenCalledTimes(9);
expect(loggerSpy).toHaveBeenCalledWith("All listeners and message deliverers have been stopped.");
});
});
});

View File

@@ -1,238 +0,0 @@
import { describe } from "@jest/globals";
import { getConfig } from "../utils";
import {
TEST_ADDRESS_1,
TEST_ADDRESS_2,
TEST_CONTRACT_ADDRESS_1,
TEST_CONTRACT_ADDRESS_2,
TEST_L1_SIGNER_PRIVATE_KEY,
TEST_L2_SIGNER_PRIVATE_KEY,
TEST_RPC_URL,
} from "../../../../../utils/testing/constants";
import {
DEFAULT_CALLDATA_ENABLED,
DEFAULT_DB_CLEANER_ENABLED,
DEFAULT_DB_CLEANING_INTERVAL,
DEFAULT_DB_DAYS_BEFORE_NOW_TO_DELETE,
DEFAULT_ENFORCE_MAX_GAS_FEE,
DEFAULT_EOA_ENABLED,
DEFAULT_GAS_ESTIMATION_PERCENTILE,
DEFAULT_INITIAL_FROM_BLOCK,
DEFAULT_L2_MESSAGE_TREE_DEPTH,
DEFAULT_LISTENER_BLOCK_CONFIRMATIONS,
DEFAULT_LISTENER_INTERVAL,
DEFAULT_MAX_BLOCKS_TO_FETCH_LOGS,
DEFAULT_MAX_CLAIM_GAS_LIMIT,
DEFAULT_MAX_FEE_PER_GAS,
DEFAULT_MAX_FETCH_MESSAGES_FROM_DB,
DEFAULT_MAX_NONCE_DIFF,
DEFAULT_MAX_NUMBER_OF_RETRIES,
DEFAULT_MAX_TX_RETRIES,
DEFAULT_MESSAGE_SUBMISSION_TIMEOUT,
DEFAULT_PROFIT_MARGIN,
DEFAULT_RETRY_DELAY_IN_SECONDS,
} from "../../../../../core/constants";
describe("Config utils", () => {
describe("getConfig", () => {
it("should return the default config when no optional parameters are passed.", () => {
const config = getConfig({
l1Options: {
rpcUrl: TEST_RPC_URL,
messageServiceContractAddress: TEST_CONTRACT_ADDRESS_1,
listener: {},
claiming: {
signerPrivateKey: TEST_L1_SIGNER_PRIVATE_KEY,
},
},
l2Options: {
rpcUrl: TEST_RPC_URL,
messageServiceContractAddress: TEST_CONTRACT_ADDRESS_2,
listener: {},
claiming: {
signerPrivateKey: TEST_L2_SIGNER_PRIVATE_KEY,
},
},
l1L2AutoClaimEnabled: false,
l2L1AutoClaimEnabled: false,
databaseOptions: {
type: "postgres",
},
});
expect(config).toStrictEqual({
databaseCleanerConfig: {
cleaningInterval: DEFAULT_DB_CLEANING_INTERVAL,
daysBeforeNowToDelete: DEFAULT_DB_DAYS_BEFORE_NOW_TO_DELETE,
enabled: DEFAULT_DB_CLEANER_ENABLED,
},
databaseOptions: {
type: "postgres",
},
l1Config: {
claiming: {
feeRecipientAddress: undefined,
gasEstimationPercentile: DEFAULT_GAS_ESTIMATION_PERCENTILE,
isMaxGasFeeEnforced: DEFAULT_ENFORCE_MAX_GAS_FEE,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxNonceDiff: DEFAULT_MAX_NONCE_DIFF,
maxNumberOfRetries: DEFAULT_MAX_NUMBER_OF_RETRIES,
maxTxRetries: DEFAULT_MAX_TX_RETRIES,
messageSubmissionTimeout: DEFAULT_MESSAGE_SUBMISSION_TIMEOUT,
profitMargin: DEFAULT_PROFIT_MARGIN,
retryDelayInSeconds: DEFAULT_RETRY_DELAY_IN_SECONDS,
signerPrivateKey: TEST_L1_SIGNER_PRIVATE_KEY,
},
isCalldataEnabled: DEFAULT_CALLDATA_ENABLED,
isEOAEnabled: DEFAULT_EOA_ENABLED,
listener: {
blockConfirmation: DEFAULT_LISTENER_BLOCK_CONFIRMATIONS,
initialFromBlock: DEFAULT_INITIAL_FROM_BLOCK,
maxBlocksToFetchLogs: DEFAULT_MAX_BLOCKS_TO_FETCH_LOGS,
maxFetchMessagesFromDb: DEFAULT_MAX_FETCH_MESSAGES_FROM_DB,
pollingInterval: DEFAULT_LISTENER_INTERVAL,
},
messageServiceContractAddress: TEST_CONTRACT_ADDRESS_1,
rpcUrl: TEST_RPC_URL,
},
l1L2AutoClaimEnabled: false,
l2Config: {
claiming: {
feeRecipientAddress: undefined,
gasEstimationPercentile: DEFAULT_GAS_ESTIMATION_PERCENTILE,
isMaxGasFeeEnforced: DEFAULT_ENFORCE_MAX_GAS_FEE,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxNonceDiff: DEFAULT_MAX_NONCE_DIFF,
maxNumberOfRetries: DEFAULT_MAX_NUMBER_OF_RETRIES,
maxTxRetries: DEFAULT_MAX_TX_RETRIES,
messageSubmissionTimeout: DEFAULT_MESSAGE_SUBMISSION_TIMEOUT,
profitMargin: DEFAULT_PROFIT_MARGIN,
retryDelayInSeconds: DEFAULT_RETRY_DELAY_IN_SECONDS,
signerPrivateKey: TEST_L2_SIGNER_PRIVATE_KEY,
},
enableLineaEstimateGas: false,
isCalldataEnabled: DEFAULT_CALLDATA_ENABLED,
isEOAEnabled: DEFAULT_EOA_ENABLED,
l2MessageTreeDepth: DEFAULT_L2_MESSAGE_TREE_DEPTH,
listener: {
blockConfirmation: DEFAULT_LISTENER_BLOCK_CONFIRMATIONS,
initialFromBlock: DEFAULT_INITIAL_FROM_BLOCK,
maxBlocksToFetchLogs: DEFAULT_MAX_BLOCKS_TO_FETCH_LOGS,
maxFetchMessagesFromDb: DEFAULT_MAX_FETCH_MESSAGES_FROM_DB,
pollingInterval: DEFAULT_LISTENER_INTERVAL,
},
messageServiceContractAddress: TEST_CONTRACT_ADDRESS_2,
rpcUrl: TEST_RPC_URL,
},
l2L1AutoClaimEnabled: false,
loggerOptions: undefined,
});
});
it("should return the config when some optional parameters are passed.", () => {
const config = getConfig({
l1Options: {
rpcUrl: TEST_RPC_URL,
messageServiceContractAddress: TEST_CONTRACT_ADDRESS_1,
listener: {
pollingInterval: DEFAULT_LISTENER_INTERVAL + 1000,
},
claiming: {
signerPrivateKey: TEST_L1_SIGNER_PRIVATE_KEY,
feeRecipientAddress: TEST_ADDRESS_1,
},
},
l2Options: {
rpcUrl: TEST_RPC_URL,
messageServiceContractAddress: TEST_CONTRACT_ADDRESS_2,
enableLineaEstimateGas: true,
listener: {
pollingInterval: DEFAULT_LISTENER_INTERVAL + 1000,
},
claiming: {
signerPrivateKey: TEST_L2_SIGNER_PRIVATE_KEY,
feeRecipientAddress: TEST_ADDRESS_2,
},
},
l1L2AutoClaimEnabled: true,
l2L1AutoClaimEnabled: true,
databaseOptions: {
type: "postgres",
},
databaseCleanerOptions: {
enabled: true,
},
});
expect(config).toStrictEqual({
databaseCleanerConfig: {
cleaningInterval: DEFAULT_DB_CLEANING_INTERVAL,
daysBeforeNowToDelete: DEFAULT_DB_DAYS_BEFORE_NOW_TO_DELETE,
enabled: true,
},
databaseOptions: {
type: "postgres",
},
l1Config: {
claiming: {
feeRecipientAddress: TEST_ADDRESS_1,
gasEstimationPercentile: DEFAULT_GAS_ESTIMATION_PERCENTILE,
isMaxGasFeeEnforced: DEFAULT_ENFORCE_MAX_GAS_FEE,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxNonceDiff: DEFAULT_MAX_NONCE_DIFF,
maxNumberOfRetries: DEFAULT_MAX_NUMBER_OF_RETRIES,
maxTxRetries: DEFAULT_MAX_TX_RETRIES,
messageSubmissionTimeout: DEFAULT_MESSAGE_SUBMISSION_TIMEOUT,
profitMargin: DEFAULT_PROFIT_MARGIN,
retryDelayInSeconds: DEFAULT_RETRY_DELAY_IN_SECONDS,
signerPrivateKey: TEST_L1_SIGNER_PRIVATE_KEY,
},
isCalldataEnabled: DEFAULT_CALLDATA_ENABLED,
isEOAEnabled: DEFAULT_EOA_ENABLED,
listener: {
blockConfirmation: DEFAULT_LISTENER_BLOCK_CONFIRMATIONS,
initialFromBlock: DEFAULT_INITIAL_FROM_BLOCK,
maxBlocksToFetchLogs: DEFAULT_MAX_BLOCKS_TO_FETCH_LOGS,
maxFetchMessagesFromDb: DEFAULT_MAX_FETCH_MESSAGES_FROM_DB,
pollingInterval: DEFAULT_LISTENER_INTERVAL + 1000,
},
messageServiceContractAddress: TEST_CONTRACT_ADDRESS_1,
rpcUrl: TEST_RPC_URL,
},
l1L2AutoClaimEnabled: true,
l2Config: {
claiming: {
feeRecipientAddress: TEST_ADDRESS_2,
gasEstimationPercentile: DEFAULT_GAS_ESTIMATION_PERCENTILE,
isMaxGasFeeEnforced: DEFAULT_ENFORCE_MAX_GAS_FEE,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxNonceDiff: DEFAULT_MAX_NONCE_DIFF,
maxNumberOfRetries: DEFAULT_MAX_NUMBER_OF_RETRIES,
maxTxRetries: DEFAULT_MAX_TX_RETRIES,
messageSubmissionTimeout: DEFAULT_MESSAGE_SUBMISSION_TIMEOUT,
profitMargin: DEFAULT_PROFIT_MARGIN,
retryDelayInSeconds: DEFAULT_RETRY_DELAY_IN_SECONDS,
signerPrivateKey: TEST_L2_SIGNER_PRIVATE_KEY,
},
enableLineaEstimateGas: true,
isCalldataEnabled: DEFAULT_CALLDATA_ENABLED,
isEOAEnabled: DEFAULT_EOA_ENABLED,
l2MessageTreeDepth: DEFAULT_L2_MESSAGE_TREE_DEPTH,
listener: {
blockConfirmation: DEFAULT_LISTENER_BLOCK_CONFIRMATIONS,
initialFromBlock: DEFAULT_INITIAL_FROM_BLOCK,
maxBlocksToFetchLogs: DEFAULT_MAX_BLOCKS_TO_FETCH_LOGS,
maxFetchMessagesFromDb: DEFAULT_MAX_FETCH_MESSAGES_FROM_DB,
pollingInterval: DEFAULT_LISTENER_INTERVAL + 1000,
},
messageServiceContractAddress: TEST_CONTRACT_ADDRESS_2,
rpcUrl: TEST_RPC_URL,
},
l2L1AutoClaimEnabled: true,
loggerOptions: undefined,
});
});
});
});

View File

@@ -1,104 +0,0 @@
import { LoggerOptions } from "winston";
import { DBOptions, DBCleanerOptions, DBCleanerConfig } from "../../persistence/config/types";
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};
/**
* Configuration for the Postman service, including network configurations, database options, and logging.
*/
export type PostmanOptions = {
l1Options: L1NetworkOptions;
l2Options: L2NetworkOptions;
l1L2AutoClaimEnabled: boolean;
l2L1AutoClaimEnabled: boolean;
databaseOptions: DBOptions;
databaseCleanerOptions?: DBCleanerOptions;
loggerOptions?: LoggerOptions;
};
/**
* Configuration for the Postman service, including network configurations, database options, and logging.
*/
export type PostmanConfig = {
l1Config: L1NetworkConfig;
l2Config: L2NetworkConfig;
l1L2AutoClaimEnabled: boolean;
l2L1AutoClaimEnabled: boolean;
databaseOptions: DBOptions;
databaseCleanerConfig: DBCleanerConfig;
loggerOptions?: LoggerOptions;
};
/**
* Base configuration for a network, including claiming, listener settings, and contract details.
*/
type NetworkOptions = {
claiming: ClaimingOptions;
listener: ListenerOptions;
rpcUrl: string;
messageServiceContractAddress: string;
isEOAEnabled?: boolean;
isCalldataEnabled?: boolean;
};
type NetworkConfig = Omit<DeepRequired<NetworkOptions>, "claiming" | "listener"> & {
claiming: ClaimingConfig;
listener: ListenerConfig;
};
/**
* Configuration specific to the L1 network, extending the base NetworkConfig.
*/
export type L1NetworkOptions = NetworkOptions;
export type L1NetworkConfig = NetworkConfig;
/**
* Configuration specific to the L2 network, extending the base NetworkConfig with additional options.
*/
export type L2NetworkOptions = NetworkOptions & {
l2MessageTreeDepth?: number;
enableLineaEstimateGas?: boolean;
};
export type L2NetworkConfig = NetworkConfig & {
l2MessageTreeDepth: number;
enableLineaEstimateGas: boolean;
};
/**
* Configuration for claiming operations, including signer details, fee settings, and retry policies.
*/
export type ClaimingOptions = {
signerPrivateKey: string;
messageSubmissionTimeout?: number;
feeRecipientAddress?: string;
maxNonceDiff?: number;
maxFeePerGas?: bigint;
gasEstimationPercentile?: number;
isMaxGasFeeEnforced?: boolean;
profitMargin?: number;
maxNumberOfRetries?: number;
retryDelayInSeconds?: number;
maxClaimGasLimit?: bigint;
maxTxRetries?: number;
};
export type ClaimingConfig = Omit<Required<ClaimingOptions>, "feeRecipientAddress"> & {
feeRecipientAddress?: string;
};
/**
* Configuration for the event listener, including polling settings and block fetching limits.
*/
export type ListenerOptions = {
pollingInterval?: number;
initialFromBlock?: number;
blockConfirmation?: number;
maxFetchMessagesFromDb?: number;
maxBlocksToFetchLogs?: number;
};
export type ListenerConfig = Required<ListenerOptions>;

View File

@@ -1,106 +0,0 @@
import {
DEFAULT_CALLDATA_ENABLED,
DEFAULT_EOA_ENABLED,
DEFAULT_GAS_ESTIMATION_PERCENTILE,
DEFAULT_INITIAL_FROM_BLOCK,
DEFAULT_L2_MESSAGE_TREE_DEPTH,
DEFAULT_LISTENER_BLOCK_CONFIRMATIONS,
DEFAULT_LISTENER_INTERVAL,
DEFAULT_MAX_BLOCKS_TO_FETCH_LOGS,
DEFAULT_MAX_CLAIM_GAS_LIMIT,
DEFAULT_MAX_FEE_PER_GAS,
DEFAULT_MAX_FETCH_MESSAGES_FROM_DB,
DEFAULT_MAX_NONCE_DIFF,
DEFAULT_MAX_NUMBER_OF_RETRIES,
DEFAULT_MAX_TX_RETRIES,
DEFAULT_MESSAGE_SUBMISSION_TIMEOUT,
DEFAULT_PROFIT_MARGIN,
DEFAULT_RETRY_DELAY_IN_SECONDS,
} from "../../../../core/constants";
import { PostmanConfig, PostmanOptions } from "./config";
/**
* @notice Generates the configuration for the Postman service based on provided options.
* @dev This function merges the provided options with default values where necessary.
* @param postmanOptions The options provided to configure the Postman service.
* @return postmanConfig The complete configuration for the Postman service.
*/
export function getConfig(postmanOptions: PostmanOptions): PostmanConfig {
const {
l1Options,
l2Options,
l1L2AutoClaimEnabled,
l2L1AutoClaimEnabled,
databaseOptions,
databaseCleanerOptions,
loggerOptions,
} = postmanOptions;
return {
l1Config: {
rpcUrl: l1Options.rpcUrl,
messageServiceContractAddress: l1Options.messageServiceContractAddress,
isEOAEnabled: l1Options.isEOAEnabled ?? DEFAULT_EOA_ENABLED,
isCalldataEnabled: l1Options.isCalldataEnabled ?? DEFAULT_CALLDATA_ENABLED,
listener: {
pollingInterval: l1Options.listener.pollingInterval ?? DEFAULT_LISTENER_INTERVAL,
maxFetchMessagesFromDb: l1Options.listener.maxFetchMessagesFromDb ?? DEFAULT_MAX_FETCH_MESSAGES_FROM_DB,
maxBlocksToFetchLogs: l1Options.listener.maxBlocksToFetchLogs ?? DEFAULT_MAX_BLOCKS_TO_FETCH_LOGS,
initialFromBlock: l1Options.listener.initialFromBlock ?? DEFAULT_INITIAL_FROM_BLOCK,
blockConfirmation: l1Options.listener.blockConfirmation ?? DEFAULT_LISTENER_BLOCK_CONFIRMATIONS,
},
claiming: {
signerPrivateKey: l1Options.claiming.signerPrivateKey,
messageSubmissionTimeout: l1Options.claiming.messageSubmissionTimeout ?? DEFAULT_MESSAGE_SUBMISSION_TIMEOUT,
feeRecipientAddress: l1Options.claiming.feeRecipientAddress,
maxNonceDiff: l1Options.claiming.maxNonceDiff ?? DEFAULT_MAX_NONCE_DIFF,
maxFeePerGas: l1Options.claiming.maxFeePerGas ?? DEFAULT_MAX_FEE_PER_GAS,
gasEstimationPercentile: l1Options.claiming.gasEstimationPercentile ?? DEFAULT_GAS_ESTIMATION_PERCENTILE,
isMaxGasFeeEnforced: l1Options.claiming.isMaxGasFeeEnforced ?? false,
profitMargin: l1Options.claiming.profitMargin ?? DEFAULT_PROFIT_MARGIN,
maxNumberOfRetries: l1Options.claiming.maxNumberOfRetries ?? DEFAULT_MAX_NUMBER_OF_RETRIES,
retryDelayInSeconds: l1Options.claiming.retryDelayInSeconds ?? DEFAULT_RETRY_DELAY_IN_SECONDS,
maxClaimGasLimit: l1Options.claiming.maxClaimGasLimit ?? DEFAULT_MAX_CLAIM_GAS_LIMIT,
maxTxRetries: l1Options.claiming.maxTxRetries ?? DEFAULT_MAX_TX_RETRIES,
},
},
l2Config: {
rpcUrl: l2Options.rpcUrl,
messageServiceContractAddress: l2Options.messageServiceContractAddress,
isEOAEnabled: l2Options.isEOAEnabled ?? DEFAULT_EOA_ENABLED,
isCalldataEnabled: l2Options.isCalldataEnabled ?? DEFAULT_CALLDATA_ENABLED,
l2MessageTreeDepth: l2Options.l2MessageTreeDepth ?? DEFAULT_L2_MESSAGE_TREE_DEPTH,
enableLineaEstimateGas: l2Options.enableLineaEstimateGas ?? false,
listener: {
pollingInterval: l2Options.listener.pollingInterval ?? DEFAULT_LISTENER_INTERVAL,
maxFetchMessagesFromDb: l2Options.listener.maxFetchMessagesFromDb ?? DEFAULT_MAX_FETCH_MESSAGES_FROM_DB,
maxBlocksToFetchLogs: l2Options.listener.maxBlocksToFetchLogs ?? DEFAULT_MAX_BLOCKS_TO_FETCH_LOGS,
initialFromBlock: l2Options.listener.initialFromBlock ?? DEFAULT_INITIAL_FROM_BLOCK,
blockConfirmation: l2Options.listener.blockConfirmation ?? DEFAULT_LISTENER_BLOCK_CONFIRMATIONS,
},
claiming: {
signerPrivateKey: l2Options.claiming.signerPrivateKey,
messageSubmissionTimeout: l2Options.claiming.messageSubmissionTimeout ?? DEFAULT_MESSAGE_SUBMISSION_TIMEOUT,
feeRecipientAddress: l2Options.claiming.feeRecipientAddress,
maxNonceDiff: l2Options.claiming.maxNonceDiff ?? DEFAULT_MAX_NONCE_DIFF,
maxFeePerGas: l2Options.claiming.maxFeePerGas ?? DEFAULT_MAX_FEE_PER_GAS,
gasEstimationPercentile: l2Options.claiming.gasEstimationPercentile ?? DEFAULT_GAS_ESTIMATION_PERCENTILE,
isMaxGasFeeEnforced: l2Options.claiming.isMaxGasFeeEnforced ?? false,
profitMargin: l2Options.claiming.profitMargin ?? DEFAULT_PROFIT_MARGIN,
maxNumberOfRetries: l2Options.claiming.maxNumberOfRetries ?? DEFAULT_MAX_NUMBER_OF_RETRIES,
retryDelayInSeconds: l2Options.claiming.retryDelayInSeconds ?? DEFAULT_RETRY_DELAY_IN_SECONDS,
maxClaimGasLimit: l2Options.claiming.maxClaimGasLimit ?? DEFAULT_MAX_CLAIM_GAS_LIMIT,
maxTxRetries: l2Options.claiming.maxTxRetries ?? DEFAULT_MAX_TX_RETRIES,
},
},
l1L2AutoClaimEnabled,
l2L1AutoClaimEnabled,
databaseOptions,
databaseCleanerConfig: {
enabled: databaseCleanerOptions?.enabled ?? false,
cleaningInterval: databaseCleanerOptions?.cleaningInterval ?? 43200000,
daysBeforeNowToDelete: databaseCleanerOptions?.daysBeforeNowToDelete ?? 14,
},
loggerOptions,
};
}

View File

@@ -1,12 +0,0 @@
import { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
import { PostgresConnectionOptions } from "typeorm/driver/postgres/PostgresConnectionOptions";
export type DBOptions = PostgresConnectionOptions | BetterSqlite3ConnectionOptions;
export type DBCleanerOptions = {
enabled: boolean;
cleaningInterval?: number;
daysBeforeNowToDelete?: number;
};
export type DBCleanerConfig = Required<DBCleanerOptions>;

View File

@@ -1,33 +0,0 @@
import { DataSource } from "typeorm";
import { SnakeNamingStrategy } from "typeorm-naming-strategies";
import { InitialDatabaseSetup1685985945638 } from "./migrations/1685985945638-InitialDatabaseSetup";
import { AddNewColumns1687890326970 } from "./migrations/1687890326970-AddNewColumns";
import { UpdateStatusColumn1687890694496 } from "./migrations/1687890694496-UpdateStatusColumn";
import { RemoveUniqueConstraint1689084924789 } from "./migrations/1689084924789-RemoveUniqueConstraint";
import { AddNewIndexes1701265652528 } from "./migrations/1701265652528-AddNewIndexes";
import { AddUniqueConstraint1709901138056 } from "./migrations/1709901138056-AddUniqueConstraint";
import { DBOptions } from "./config/types";
import { MessageEntity } from "./entities/Message.entity";
import { AddCompressedTxSizeColumn1718026260629 } from "./migrations/1718026260629-AddCompressedTxSizeColumn";
export class DB {
public static create(config: DBOptions): DataSource {
return new DataSource({
...config,
entities: [MessageEntity],
namingStrategy: new SnakeNamingStrategy(),
migrations: [
InitialDatabaseSetup1685985945638,
AddNewColumns1687890326970,
UpdateStatusColumn1687890694496,
RemoveUniqueConstraint1689084924789,
AddNewIndexes1701265652528,
AddUniqueConstraint1709901138056,
AddCompressedTxSizeColumn1718026260629,
],
migrationsTableName: "migrations",
logging: ["error"],
migrationsRun: true,
});
}
}

View File

@@ -1,95 +0,0 @@
import { IsDate, IsDecimal, IsEnum, IsNumber, IsString } from "class-validator";
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm";
import { Direction, MessageStatus } from "../../../../core/enums/MessageEnums";
@Entity({ name: "message" })
export class MessageEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
@IsString()
messageSender: string;
@Column()
@IsString()
destination: string;
@Column()
@IsString()
fee: string;
@Column()
@IsString()
value: string;
@Column()
@IsNumber()
messageNonce: number;
@Column()
@IsString()
calldata: string;
@Column()
@IsString()
messageHash: string;
@Column()
@IsString()
messageContractAddress: string;
@Column()
@IsNumber()
sentBlockNumber: number;
@Column()
@IsEnum(Direction)
direction: Direction;
@Column()
@IsEnum(MessageStatus)
status: MessageStatus;
@Column({ nullable: true })
@IsDate()
claimTxCreationDate?: Date;
@Column({ nullable: true })
claimTxGasLimit?: number;
@Column({ nullable: true, type: "bigint" })
claimTxMaxFeePerGas?: bigint;
@Column({ nullable: true, type: "bigint" })
claimTxMaxPriorityFeePerGas?: bigint;
@Column({ nullable: true })
claimTxNonce?: number;
@Column({ nullable: true })
@IsString()
claimTxHash?: string;
@Column()
@IsNumber()
claimNumberOfRetry: number;
@Column({ nullable: true })
@IsDate()
claimLastRetriedAt?: Date;
@Column({ nullable: true })
@IsDecimal()
claimGasEstimationThreshold?: number;
@Column({ nullable: true })
@IsNumber()
compressedTransactionSize?: number;
@CreateDateColumn()
public createdAt: Date;
@UpdateDateColumn()
public updatedAt: Date;
}

View File

@@ -1,79 +0,0 @@
import { describe, it, expect } from "@jest/globals";
import { generateMessage, generateMessageEntity } from "../../../../../utils/testing/helpers";
import { mapMessageEntityToMessage, mapMessageToMessageEntity } from "../messageMappers";
import {
TEST_ADDRESS_1,
TEST_CONTRACT_ADDRESS_1,
TEST_CONTRACT_ADDRESS_2,
TEST_MESSAGE_HASH,
} from "../../../../../utils/testing/constants";
import { Direction, MessageStatus } from "../../../../../core/enums/MessageEnums";
import { Message } from "../../../../../core/entities/Message";
describe("Message Mappers", () => {
describe("mapMessageToMessageEntity", () => {
it("should map a message to a message entity", () => {
const message = generateMessage();
expect(mapMessageToMessageEntity(message)).toStrictEqual({
calldata: "0x",
claimGasEstimationThreshold: undefined,
claimLastRetriedAt: undefined,
claimNumberOfRetry: 0,
claimTxCreationDate: undefined,
claimTxGasLimit: undefined,
claimTxHash: undefined,
claimTxMaxFeePerGas: undefined,
claimTxMaxPriorityFeePerGas: undefined,
claimTxNonce: undefined,
compressedTransactionSize: undefined,
contractAddress: TEST_CONTRACT_ADDRESS_2,
createdAt: new Date("2023-08-04"),
destination: TEST_CONTRACT_ADDRESS_1,
direction: Direction.L1_TO_L2,
fee: "10",
id: 1,
messageContractAddress: TEST_CONTRACT_ADDRESS_2,
messageHash: TEST_MESSAGE_HASH,
messageNonce: 1,
messageSender: TEST_ADDRESS_1,
sentBlockNumber: 100_000,
status: MessageStatus.SENT,
updatedAt: new Date("2023-08-04"),
value: "2",
});
});
});
describe("mapMessageToMessageEntity", () => {
it("should map a message entity to a message", () => {
const messageEntity = generateMessageEntity();
expect(mapMessageEntityToMessage(messageEntity)).toStrictEqual(
new Message({
calldata: "0x",
claimGasEstimationThreshold: undefined,
claimLastRetriedAt: undefined,
claimNumberOfRetry: 0,
claimTxCreationDate: undefined,
claimTxGasLimit: undefined,
claimTxHash: undefined,
claimTxMaxFeePerGas: undefined,
claimTxMaxPriorityFeePerGas: undefined,
claimTxNonce: undefined,
contractAddress: TEST_CONTRACT_ADDRESS_2,
createdAt: new Date("2023-08-04"),
destination: TEST_CONTRACT_ADDRESS_1,
direction: Direction.L1_TO_L2,
fee: 10n,
id: 1,
messageHash: TEST_MESSAGE_HASH,
messageNonce: 1n,
messageSender: TEST_ADDRESS_1,
sentBlockNumber: 100_000,
status: MessageStatus.SENT,
updatedAt: new Date("2023-08-04"),
value: 2n,
}),
);
});
});
});

View File

@@ -1,44 +0,0 @@
import { Message } from "../../../../core/entities/Message";
import { MessageEntity } from "../entities/Message.entity";
export const mapMessageToMessageEntity = (message: Message): MessageEntity => {
return {
id: message?.id as number,
...message,
fee: message.fee.toString(),
value: message.value.toString(),
messageNonce: parseInt(message.messageNonce.toString()),
messageContractAddress: message.contractAddress,
createdAt: message.createdAt ?? new Date(),
updatedAt: message.updatedAt ?? new Date(),
};
};
export const mapMessageEntityToMessage = (entity: MessageEntity): Message => {
return new Message({
id: entity.id,
messageSender: entity.messageSender,
destination: entity.destination,
fee: BigInt(entity.fee),
value: BigInt(entity.value),
messageNonce: BigInt(entity.messageNonce),
calldata: entity.calldata,
messageHash: entity.messageHash,
contractAddress: entity.messageContractAddress,
sentBlockNumber: entity.sentBlockNumber,
direction: entity.direction,
status: entity.status,
claimTxCreationDate: entity.claimTxCreationDate,
claimTxGasLimit: entity.claimTxGasLimit,
claimTxMaxFeePerGas: entity.claimTxMaxFeePerGas,
claimTxMaxPriorityFeePerGas: entity.claimTxMaxPriorityFeePerGas,
claimTxNonce: entity.claimTxNonce,
claimTxHash: entity.claimTxHash,
claimNumberOfRetry: entity.claimNumberOfRetry,
claimLastRetriedAt: entity.claimLastRetriedAt,
claimGasEstimationThreshold: entity.claimGasEstimationThreshold,
compressedTransactionSize: entity.compressedTransactionSize,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
});
};

View File

@@ -1,137 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from "typeorm";
export class InitialDatabaseSetup1685985945638 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: "message",
columns: [
{
name: "id",
type: "integer",
isPrimary: true,
isGenerated: true,
generationStrategy: "increment",
},
{
name: "message_sender",
type: "varchar",
},
{
name: "destination",
type: "varchar",
},
{
name: "fee",
type: "varchar",
},
{
name: "value",
type: "varchar",
},
{
name: "message_nonce",
type: "integer",
},
{
name: "calldata",
type: "varchar",
},
{
name: "message_hash",
type: "varchar",
isUnique: true,
},
{
name: "message_contract_address",
type: "varchar",
},
{
name: "sent_block_number",
type: "integer",
},
{
name: "direction",
type: "enum",
enum: ["L1_TO_L2", "L2_TO_L1"],
enumName: "directionEnum",
},
{
name: "status",
type: "enum",
enum: ["SENT", "ANCHORED", "PENDING", "CLAIMED_SUCCESS", "CLAIMED_REVERTED", "NON_EXECUTABLE"],
enumName: "statusEnum",
},
{
name: "claim_tx_creation_date",
type: "timestamp with time zone",
isNullable: true,
},
{
name: "claim_tx_gas_limit",
type: "integer",
isNullable: true,
},
{
name: "claim_tx_max_fee_per_gas",
type: "bigint",
isNullable: true,
},
{
name: "claim_tx_max_priority_fee_per_gas",
type: "bigint",
isNullable: true,
},
{
name: "claim_tx_nonce",
type: "integer",
isNullable: true,
},
{
name: "claim_tx_hash",
type: "varchar",
isNullable: true,
},
{
name: "created_at",
type: "timestamp with time zone",
default: "now()",
},
{
name: "updated_at",
type: "timestamp with time zone",
default: "now()",
},
],
}),
true,
);
const messageHashIndex = new TableIndex({
columnNames: ["message_hash"],
name: "message_hash_index",
});
const transactionHashIndex = new TableIndex({
columnNames: ["claim_tx_hash"],
name: "claim_tx__hash_index",
});
await queryRunner.createIndex("message", messageHashIndex);
await queryRunner.createIndex("message", transactionHashIndex);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable("message");
await queryRunner.dropIndices("message", [
new TableIndex({
columnNames: ["message_hash"],
name: "message_hash_index",
}),
new TableIndex({
columnNames: ["claim_tx_hash"],
name: "claim_tx__hash_index",
}),
]);
}
}

View File

@@ -1,30 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
export class AddNewColumns1687890326970 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumns("message", [
new TableColumn({
name: "claim_number_of_retry",
type: "integer",
isNullable: false,
default: 0,
}),
new TableColumn({
name: "claim_last_retried_at",
type: "timestamp with time zone",
isNullable: true,
}),
new TableColumn({
name: "claim_gas_estimation_threshold",
type: "numeric",
isNullable: true,
}),
]);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("message", "claim_number_of_retry");
await queryRunner.dropColumn("message", "claim_last_retried_at");
await queryRunner.dropColumn("message", "claim_gas_estimation_threshold");
}
}

View File

@@ -1,15 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class UpdateStatusColumn1687890694496 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TYPE "statusEnum" ADD VALUE 'ZERO_FEE'`);
await queryRunner.query(`ALTER TYPE "statusEnum" ADD VALUE 'FEE_UNDERPRICED'`);
await queryRunner.query(`ALTER TYPE "statusEnum" ADD VALUE 'EXCLUDED'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TYPE "statusEnum" DROP VALUE 'ZERO_FEE'`);
await queryRunner.query(`ALTER TYPE "statusEnum" DROP VALUE 'FEE_UNDERPRICED'`);
await queryRunner.query(`ALTER TYPE "statusEnum" DROP VALUE 'EXCLUDED'`);
}
}

View File

@@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner, TableUnique } from "typeorm";
export class RemoveUniqueConstraint1689084924789 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropUniqueConstraint("message", "UQ_4ae806cf878a218ad891a030ab5");
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createUniqueConstraint("message", new TableUnique({ columnNames: ["message_hash"] }));
}
}

View File

@@ -1,53 +0,0 @@
import { MigrationInterface, QueryRunner, TableIndex } from "typeorm";
export class AddNewIndexes1701265652528 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createIndices("message", [
new TableIndex({
columnNames: ["direction"],
name: "direction_index",
}),
new TableIndex({
columnNames: ["claim_tx_nonce"],
name: "claim_tx_nonce_index",
}),
new TableIndex({
columnNames: ["created_at"],
name: "created_at_index",
}),
new TableIndex({
columnNames: ["message_contract_address"],
name: "message_contract_address_index",
}),
new TableIndex({
columnNames: ["status"],
name: "status_index",
}),
]);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropIndices("message", [
new TableIndex({
columnNames: ["direction"],
name: "direction_index",
}),
new TableIndex({
columnNames: ["claim_tx_nonce"],
name: "claim_tx_nonce_index",
}),
new TableIndex({
columnNames: ["created_at"],
name: "created_at_index",
}),
new TableIndex({
columnNames: ["message_contract_address"],
name: "message_contract_address_index",
}),
new TableIndex({
columnNames: ["status"],
name: "status_index",
}),
]);
}
}

View File

@@ -1,17 +0,0 @@
import { MigrationInterface, QueryRunner, TableUnique } from "typeorm";
export class AddUniqueConstraint1709901138056 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const messageUniqueConstraint = new TableUnique({
columnNames: ["message_hash", "direction"],
});
await queryRunner.createUniqueConstraint("message", messageUniqueConstraint);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const messageUniqueConstraint = new TableUnique({
columnNames: ["message_hash", "direction"],
});
await queryRunner.dropUniqueConstraint("message", messageUniqueConstraint);
}
}

View File

@@ -1,20 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
export class AddCompressedTxSizeColumn1718026260629 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
"message",
new TableColumn({
name: "compressed_transaction_size",
type: "numeric",
isNullable: true,
}),
);
await queryRunner.query(`ALTER TYPE "statusEnum" ADD VALUE 'TRANSACTION_SIZE_COMPUTED'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("message", "compressed_transaction_size");
await queryRunner.query(`ALTER TYPE "statusEnum" DROP VALUE 'TRANSACTION_SIZE_COMPUTED'`);
}
}

View File

@@ -1,320 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Brackets, DataSource, Repository } from "typeorm";
import { Message } from "../../../../core/entities/Message";
import { mapMessageEntityToMessage, mapMessageToMessageEntity } from "../mappers/messageMappers";
import { Direction, MessageStatus } from "../../../../core/enums/MessageEnums";
import { DatabaseErrorType, DatabaseRepoName } from "../../../../core/enums/DatabaseEnums";
import { DatabaseAccessError } from "../../../../core/errors/DatabaseErrors";
import { MessageEntity } from "../entities/Message.entity";
import { subtractSeconds } from "../../../../core/utils/shared";
import { ContractTransactionResponse } from "ethers";
import { IMessageRepository } from "../../../../core/persistence/IMessageRepository";
export class TypeOrmMessageRepository<TransactionResponse extends ContractTransactionResponse>
extends Repository<MessageEntity>
implements IMessageRepository<TransactionResponse>
{
constructor(readonly dataSource: DataSource) {
super(MessageEntity, dataSource.createEntityManager());
}
async findByMessageHash(message: Message, direction: Direction): Promise<Message | null> {
try {
const messageInDb = await this.findOneBy({
messageHash: message.messageHash,
direction,
});
if (!messageInDb) {
return null;
}
return mapMessageEntityToMessage(messageInDb);
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Read, err, message);
}
}
async insertMessage(message: Message): Promise<void> {
try {
const messageInDb = await this.findOneBy({
messageHash: message.messageHash,
direction: message.direction,
});
if (!messageInDb) {
await this.manager.save(MessageEntity, mapMessageToMessageEntity(message));
}
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Insert, err, message);
}
}
async updateMessage(message: Message): Promise<void> {
try {
const messageInDb = await this.findOneBy({
messageHash: message.messageHash,
direction: message.direction,
});
if (messageInDb) {
await this.manager.save(MessageEntity, mapMessageToMessageEntity(message));
}
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Update, err, message);
}
}
async updateMessageByTransactionHash(transactionHash: string, direction: Direction, message: Message): Promise<void> {
try {
const messageInDb = await this.findOneBy({
claimTxHash: transactionHash,
direction,
});
if (messageInDb) {
await this.manager.save(MessageEntity, mapMessageToMessageEntity(message));
}
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Update, err);
}
}
async saveMessages(messages: Message[]): Promise<void> {
try {
await this.manager.save(MessageEntity, messages.map(mapMessageToMessageEntity));
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Insert, err);
}
}
async deleteMessages(msBeforeNowToDelete: number): Promise<number> {
try {
const d = new Date();
d.setTime(d.getTime() - msBeforeNowToDelete);
const formattedDateStr = d.toISOString().replace("T", " ").replace("Z", "");
const deleteResult = await this.createQueryBuilder("message")
.delete()
.where("message.status IN(:...statuses)", {
statuses: [
MessageStatus.CLAIMED_SUCCESS,
MessageStatus.CLAIMED_REVERTED,
MessageStatus.EXCLUDED,
MessageStatus.ZERO_FEE,
],
})
.andWhere("message.updated_at < :updated_before", { updated_before: formattedDateStr })
.execute();
return deleteResult.affected ?? 0;
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Delete, err);
}
}
async getFirstMessageToClaimOnL1(
direction: Direction,
contractAddress: string,
currentGasPrice: bigint,
gasEstimationMargin: number,
maxRetry: number,
retryDelay: number,
): Promise<Message | null> {
try {
const message = await this.createQueryBuilder("message")
.where("message.direction = :direction", { direction })
.andWhere("message.messageContractAddress = :contractAddress", { contractAddress })
.andWhere("message.status IN(:...statuses)", {
statuses: [MessageStatus.ANCHORED, MessageStatus.FEE_UNDERPRICED],
})
.andWhere("message.claimNumberOfRetry < :maxRetry", { maxRetry })
.andWhere(
new Brackets((qb) => {
qb.where("message.claimLastRetriedAt IS NULL").orWhere("message.claimLastRetriedAt < :lastRetriedDate", {
lastRetriedDate: subtractSeconds(new Date(), retryDelay).toISOString(),
});
}),
)
.andWhere(
new Brackets((qb) => {
qb.where("message.claimGasEstimationThreshold > :threshold", {
threshold: parseFloat(currentGasPrice.toString()) * gasEstimationMargin,
}).orWhere("message.claimGasEstimationThreshold IS NULL");
}),
)
.orderBy("CAST(message.status as CHAR)", "ASC")
.addOrderBy("message.claimGasEstimationThreshold", "DESC")
.addOrderBy("message.sentBlockNumber", "ASC")
.getOne();
return message ? mapMessageEntityToMessage(message) : null;
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Read, err);
}
}
async getFirstMessageToClaimOnL2(
direction: Direction,
contractAddress: string,
messageStatuses: MessageStatus[],
maxRetry: number,
retryDelay: number,
feeEstimationOptions: {
minimumMargin: number;
extraDataVariableCost: number;
extraDataFixedCost: number;
},
): Promise<Message | null> {
try {
const message = await this.createQueryBuilder("message")
.where("message.direction = :direction", { direction })
.andWhere("message.messageContractAddress = :contractAddress", { contractAddress })
.andWhere("message.status IN(:...statuses)", {
statuses: messageStatuses,
})
.andWhere("message.claimNumberOfRetry < :maxRetry", { maxRetry })
.andWhere(
new Brackets((qb) => {
qb.where("message.claimLastRetriedAt IS NULL").orWhere("message.claimLastRetriedAt < :lastRetriedDate", {
lastRetriedDate: subtractSeconds(new Date(), retryDelay).toISOString(),
});
}),
)
.andWhere(
"CAST(message.fee AS numeric) > :minimumMargin * ((:extraDataVariableCost * message.compressedTransactionSize) / message.claimTxGasLimit + :extraDataFixedCost) * message.claimTxGasLimit",
{
minimumMargin: feeEstimationOptions.minimumMargin,
extraDataVariableCost: feeEstimationOptions.extraDataVariableCost,
extraDataFixedCost: feeEstimationOptions.extraDataFixedCost,
},
)
.orderBy("CAST(message.status as CHAR)", "ASC")
.addOrderBy("CAST(message.fee AS numeric)", "DESC")
.addOrderBy("message.sentBlockNumber", "ASC")
.getOne();
return message ? mapMessageEntityToMessage(message) : null;
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Read, err);
}
}
async getLatestMessageSent(direction: Direction, contractAddress: string): Promise<Message | null> {
try {
const pendingMessages = await this.find({
where: {
direction,
messageContractAddress: contractAddress,
},
take: 1,
order: {
createdAt: "DESC",
},
});
if (pendingMessages.length === 0) return null;
return mapMessageEntityToMessage(pendingMessages[0]);
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Read, err);
}
}
async getNFirstMessagesByStatus(
status: MessageStatus,
direction: Direction,
limit: number,
contractAddress: string,
): Promise<Message[]> {
try {
const messages = await this.find({
where: {
direction,
status,
messageContractAddress: contractAddress,
},
take: limit,
order: {
sentBlockNumber: "ASC",
},
});
return messages.map(mapMessageEntityToMessage);
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Read, err);
}
}
async getMessageSent(direction: Direction, contractAddress: string): Promise<Message | null> {
try {
const message = await this.findOne({
where: {
direction,
status: MessageStatus.SENT,
messageContractAddress: contractAddress,
},
});
return message ? mapMessageEntityToMessage(message) : null;
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Read, err);
}
}
async getLastClaimTxNonce(direction: Direction): Promise<number | null> {
try {
const message = await this.createQueryBuilder("message")
.select("MAX(message.claimTxNonce)", "lastTxNonce")
.where("message.direction = :direction", { direction })
.getRawOne();
if (!message.lastTxNonce) {
return null;
}
return message.lastTxNonce;
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Read, err);
}
}
async getFirstPendingMessage(direction: Direction): Promise<Message | null> {
try {
const message = await this.createQueryBuilder("message")
.where("message.direction = :direction", { direction })
.andWhere("message.status = :status", { status: MessageStatus.PENDING })
.orderBy("message.claimTxNonce", "ASC")
.getOne();
return message ? mapMessageEntityToMessage(message) : null;
} catch (err: any) {
throw new DatabaseAccessError(DatabaseRepoName.MessageRepository, DatabaseErrorType.Read, err);
}
}
async updateMessageWithClaimTxAtomic(
message: Message,
nonce: number,
claimTxResponsePromise: Promise<ContractTransactionResponse>,
): Promise<void> {
await this.manager.transaction(async (entityManager) => {
await entityManager.update(
MessageEntity,
{ messageHash: message.messageHash, direction: message.direction },
{
claimTxCreationDate: new Date(),
claimTxNonce: nonce,
status: MessageStatus.PENDING,
...(message.status === MessageStatus.FEE_UNDERPRICED
? { claimNumberOfRetry: message.claimNumberOfRetry + 1, claimLastRetriedAt: new Date() }
: {}),
},
);
const tx = await claimTxResponsePromise;
await entityManager.update(
MessageEntity,
{ messageHash: message.messageHash, direction: message.direction },
{
claimTxGasLimit: parseInt(tx.gasLimit.toString()),
claimTxMaxFeePerGas: tx.maxFeePerGas ?? undefined,
claimTxMaxPriorityFeePerGas: tx.maxPriorityFeePerGas ?? undefined,
claimTxHash: tx.hash,
},
);
});
}
}

View File

@@ -1,152 +0,0 @@
import {
Block,
BlockTag,
JsonRpcProvider,
Signer,
TransactionReceipt,
TransactionRequest,
TransactionResponse,
} from "ethers";
import { BaseError } from "../../core/errors/Base";
import { IChainQuerier } from "../../core/clients/blockchain/IChainQuerier";
import { GasFees } from "../../core/clients/blockchain/IGasProvider";
export class ChainQuerier
implements IChainQuerier<TransactionReceipt, Block, TransactionRequest, TransactionResponse, JsonRpcProvider>
{
/**
* Creates an instance of ChainQuerier.
*
* @param {JsonRpcProvider} provider - The JSON RPC provider for interacting with the Ethereum network.
* @param {Signer} [signer] - An optional Ethers.js signer object for signing transactions.
*/
constructor(
protected readonly provider: JsonRpcProvider,
protected readonly signer?: Signer,
) {}
/**
* Retrieves the current nonce for a given account address.
*
* @param {string} [accountAddress] - The Ethereum account address to fetch the nonce for. Optional if a signer is provided.
* @returns {Promise<number>} A promise that resolves to the current nonce of the account.
* @throws {BaseError} If no account address is provided and no signer is available.
*/
public async getCurrentNonce(accountAddress?: string): Promise<number> {
if (accountAddress) {
return this.provider.getTransactionCount(accountAddress);
}
if (!this.signer) {
throw new BaseError("Please provide a signer.");
}
return this.provider.getTransactionCount(await this.signer.getAddress());
}
/**
* Retrieves the current block number of the blockchain.
*
* @returns {Promise<number>} A promise that resolves to the current block number.
*/
public async getCurrentBlockNumber(): Promise<number> {
return this.provider.getBlockNumber();
}
/**
* Retrieves the transaction receipt for a given transaction hash.
*
* @param {string} transactionHash - The hash of the transaction to fetch the receipt for.
* @returns {Promise<TransactionReceipt | null>} A promise that resolves to the transaction receipt, or null if the transaction is not found.
*/
public async getTransactionReceipt(transactionHash: string): Promise<TransactionReceipt | null> {
return this.provider.getTransactionReceipt(transactionHash);
}
/**
* Retrieves a block by its number or tag.
*
* @param {BlockTag} blockNumber - The block number or tag to fetch.
* @returns {Promise<Block | null>} A promise that resolves to the block, or null if the block is not found.
*/
public async getBlock(blockNumber: BlockTag): Promise<Block | null> {
return this.provider.getBlock(blockNumber);
}
/**
* Sends a custom JSON-RPC request.
*
* @param {string} methodName - The name of the JSON-RPC method to call.
* @param {any[]} params - The parameters to pass to the JSON-RPC method.
* @returns {Promise<any>} A promise that resolves to the result of the JSON-RPC call.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public async sendRequest(methodName: string, params: any[]): Promise<any> {
return this.provider.send(methodName, params);
}
/**
* Estimates the gas required for a transaction.
*
* @param {TransactionRequest} transactionRequest - The transaction request to estimate gas for.
* @returns {Promise<bigint>} A promise that resolves to the estimated gas.
*/
public async estimateGas(transactionRequest: TransactionRequest): Promise<bigint> {
return this.provider.estimateGas(transactionRequest);
}
/**
* Gets the JSON RPC provider.
*
* @returns {JsonRpcProvider} The JSON RPC provider.
*/
public getProvider(): JsonRpcProvider {
return this.provider;
}
/**
* Retrieves a transaction by its hash.
*
* @param {string} transactionHash - The hash of the transaction to fetch.
* @returns {Promise<TransactionResponse | null>} A promise that resolves to the transaction, or null if the transaction is not found.
*/
public async getTransaction(transactionHash: string): Promise<TransactionResponse | null> {
return this.provider.getTransaction(transactionHash);
}
/**
* Sends a signed transaction.
*
* @param {string} signedTx - The signed transaction to broadcast.
* @returns {Promise<TransactionResponse>} A promise that resolves to the transaction response.
*/
public async broadcastTransaction(signedTx: string): Promise<TransactionResponse> {
return this.provider.broadcastTransaction(signedTx);
}
/**
* Executes a call on the Ethereum network.
*
* @param {TransactionRequest} transactionRequest - The transaction request to execute.
* @returns {Promise<string>} A promise that resolves to the result of the call.
*/
public ethCall(transactionRequest: TransactionRequest): Promise<string> {
return this.provider.call(transactionRequest);
}
/**
* Retrieves the current gas fees.
*
* @returns {Promise<GasFees>} A promise that resolves to an object containing the current gas fees.
* @throws {BaseError} If there is an error getting the fee data.
*/
public async getFees(): Promise<GasFees> {
const { maxPriorityFeePerGas, maxFeePerGas } = await this.provider.getFeeData();
if (!maxPriorityFeePerGas || !maxFeePerGas) {
throw new BaseError("Error getting fee data");
}
return { maxPriorityFeePerGas, maxFeePerGas };
}
}

View File

@@ -1,56 +0,0 @@
import { JsonRpcProvider, Wallet } from "ethers";
import { describe, afterEach, it, expect, beforeEach } from "@jest/globals";
import { MockProxy, mock, mockClear } from "jest-mock-extended";
import { ChainQuerier } from "../ChainQuerier";
import { TEST_ADDRESS_1, TEST_L1_SIGNER_PRIVATE_KEY, TEST_TRANSACTION_HASH } from "../../../utils/testing/constants";
import { generateTransactionReceipt } from "../../../utils/testing/helpers";
describe("ChainQuerier", () => {
let providerMock: MockProxy<JsonRpcProvider>;
let chainQuerier: ChainQuerier;
beforeEach(() => {
providerMock = mock<JsonRpcProvider>();
chainQuerier = new ChainQuerier(providerMock, new Wallet(TEST_L1_SIGNER_PRIVATE_KEY, providerMock));
});
afterEach(() => {
mockClear(providerMock);
});
describe("getCurrentNonce", () => {
it("should throw an error when accountAddress param is undefined and no signer has been passed to the ChainQuerier class", async () => {
const chainQuerier = new ChainQuerier(providerMock);
await expect(chainQuerier.getCurrentNonce()).rejects.toThrow("Please provide a signer.");
});
it("should return the nonce of the accountAddress passed in parameter", async () => {
const getTransactionCountSpy = jest.spyOn(providerMock, "getTransactionCount").mockResolvedValueOnce(10);
expect(await chainQuerier.getCurrentNonce(TEST_ADDRESS_1)).toEqual(10);
expect(getTransactionCountSpy).toHaveBeenCalledTimes(1);
expect(getTransactionCountSpy).toHaveBeenCalledWith(TEST_ADDRESS_1);
});
it("should return the nonce of the signer address", async () => {
const getTransactionCountSpy = jest.spyOn(providerMock, "getTransactionCount").mockResolvedValueOnce(10);
expect(await chainQuerier.getCurrentNonce()).toEqual(10);
expect(getTransactionCountSpy).toHaveBeenCalledTimes(1);
expect(getTransactionCountSpy).toHaveBeenCalledWith("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf");
});
});
describe("getCurrentBlockNumber", () => {
it("should return the current block number", async () => {
const mockBlockNumber = 10_000;
jest.spyOn(providerMock, "getBlockNumber").mockResolvedValueOnce(mockBlockNumber);
expect(await chainQuerier.getCurrentBlockNumber()).toEqual(mockBlockNumber);
});
});
describe("getTransactionReceipt", () => {
it("should return the transaction receipt", async () => {
const mockTransactionReceipt = generateTransactionReceipt();
jest.spyOn(providerMock, "getTransactionReceipt").mockResolvedValueOnce(mockTransactionReceipt);
expect(await chainQuerier.getTransactionReceipt(TEST_TRANSACTION_HASH)).toEqual(mockTransactionReceipt);
});
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,158 +0,0 @@
{
"contractName": "ProxyAdmin",
"abi": [
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"inputs": [
{
"internalType": "contract ITransparentUpgradeableProxy",
"name": "proxy",
"type": "address"
},
{
"internalType": "address",
"name": "newAdmin",
"type": "address"
}
],
"name": "changeProxyAdmin",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract ITransparentUpgradeableProxy",
"name": "proxy",
"type": "address"
}
],
"name": "getProxyAdmin",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract ITransparentUpgradeableProxy",
"name": "proxy",
"type": "address"
}
],
"name": "getProxyImplementation",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract ITransparentUpgradeableProxy",
"name": "proxy",
"type": "address"
},
{
"internalType": "address",
"name": "implementation",
"type": "address"
}
],
"name": "upgrade",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract ITransparentUpgradeableProxy",
"name": "proxy",
"type": "address"
},
{
"internalType": "address",
"name": "implementation",
"type": "address"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "upgradeAndCall",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}
],
"bytecode": "0x608060405234801561001057600080fd5b5061001a3361001f565b61006f565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b61069a8061007e6000396000f3fe60806040526004361061007b5760003560e01c80639623609d1161004e5780639623609d1461011157806399a88ec414610124578063f2fde38b14610144578063f3b7dead1461016457600080fd5b8063204e1c7a14610080578063715018a6146100bc5780637eff275e146100d35780638da5cb5b146100f3575b600080fd5b34801561008c57600080fd5b506100a061009b366004610499565b610184565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c857600080fd5b506100d1610215565b005b3480156100df57600080fd5b506100d16100ee3660046104bd565b610229565b3480156100ff57600080fd5b506000546001600160a01b03166100a0565b6100d161011f36600461050c565b610291565b34801561013057600080fd5b506100d161013f3660046104bd565b610300565b34801561015057600080fd5b506100d161015f366004610499565b610336565b34801561017057600080fd5b506100a061017f366004610499565b6103b4565b6000806000836001600160a01b03166040516101aa90635c60da1b60e01b815260040190565b600060405180830381855afa9150503d80600081146101e5576040519150601f19603f3d011682016040523d82523d6000602084013e6101ea565b606091505b5091509150816101f957600080fd5b8080602001905181019061020d91906105e2565b949350505050565b61021d6103da565b6102276000610434565b565b6102316103da565b6040516308f2839760e41b81526001600160a01b038281166004830152831690638f283970906024015b600060405180830381600087803b15801561027557600080fd5b505af1158015610289573d6000803e3d6000fd5b505050505050565b6102996103da565b60405163278f794360e11b81526001600160a01b03841690634f1ef2869034906102c990869086906004016105ff565b6000604051808303818588803b1580156102e257600080fd5b505af11580156102f6573d6000803e3d6000fd5b5050505050505050565b6103086103da565b604051631b2ce7f360e11b81526001600160a01b038281166004830152831690633659cfe69060240161025b565b61033e6103da565b6001600160a01b0381166103a85760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b60648201526084015b60405180910390fd5b6103b181610434565b50565b6000806000836001600160a01b03166040516101aa906303e1469160e61b815260040190565b6000546001600160a01b031633146102275760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015260640161039f565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6001600160a01b03811681146103b157600080fd5b6000602082840312156104ab57600080fd5b81356104b681610484565b9392505050565b600080604083850312156104d057600080fd5b82356104db81610484565b915060208301356104eb81610484565b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b60008060006060848603121561052157600080fd5b833561052c81610484565b9250602084013561053c81610484565b9150604084013567ffffffffffffffff8082111561055957600080fd5b818601915086601f83011261056d57600080fd5b81358181111561057f5761057f6104f6565b604051601f8201601f19908116603f011681019083821181831017156105a7576105a76104f6565b816040528281528960208487010111156105c057600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b6000602082840312156105f457600080fd5b81516104b681610484565b60018060a01b038316815260006020604081840152835180604085015260005b8181101561063b5785810183015185820160600152820161061f565b8181111561064d576000606083870101525b50601f01601f19169290920160600194935050505056fea26469706673582212207ad53e1008cce369999f6b5f2f77109510b404ff1de9b47b639981fd68e6239264736f6c63430008090033",
"deployedBytecode": "0x60806040526004361061007b5760003560e01c80639623609d1161004e5780639623609d1461011157806399a88ec414610124578063f2fde38b14610144578063f3b7dead1461016457600080fd5b8063204e1c7a14610080578063715018a6146100bc5780637eff275e146100d35780638da5cb5b146100f3575b600080fd5b34801561008c57600080fd5b506100a061009b366004610499565b610184565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c857600080fd5b506100d1610215565b005b3480156100df57600080fd5b506100d16100ee3660046104bd565b610229565b3480156100ff57600080fd5b506000546001600160a01b03166100a0565b6100d161011f36600461050c565b610291565b34801561013057600080fd5b506100d161013f3660046104bd565b610300565b34801561015057600080fd5b506100d161015f366004610499565b610336565b34801561017057600080fd5b506100a061017f366004610499565b6103b4565b6000806000836001600160a01b03166040516101aa90635c60da1b60e01b815260040190565b600060405180830381855afa9150503d80600081146101e5576040519150601f19603f3d011682016040523d82523d6000602084013e6101ea565b606091505b5091509150816101f957600080fd5b8080602001905181019061020d91906105e2565b949350505050565b61021d6103da565b6102276000610434565b565b6102316103da565b6040516308f2839760e41b81526001600160a01b038281166004830152831690638f283970906024015b600060405180830381600087803b15801561027557600080fd5b505af1158015610289573d6000803e3d6000fd5b505050505050565b6102996103da565b60405163278f794360e11b81526001600160a01b03841690634f1ef2869034906102c990869086906004016105ff565b6000604051808303818588803b1580156102e257600080fd5b505af11580156102f6573d6000803e3d6000fd5b5050505050505050565b6103086103da565b604051631b2ce7f360e11b81526001600160a01b038281166004830152831690633659cfe69060240161025b565b61033e6103da565b6001600160a01b0381166103a85760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201526564647265737360d01b60648201526084015b60405180910390fd5b6103b181610434565b50565b6000806000836001600160a01b03166040516101aa906303e1469160e61b815260040190565b6000546001600160a01b031633146102275760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015260640161039f565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6001600160a01b03811681146103b157600080fd5b6000602082840312156104ab57600080fd5b81356104b681610484565b9392505050565b600080604083850312156104d057600080fd5b82356104db81610484565b915060208301356104eb81610484565b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b60008060006060848603121561052157600080fd5b833561052c81610484565b9250602084013561053c81610484565b9150604084013567ffffffffffffffff8082111561055957600080fd5b818601915086601f83011261056d57600080fd5b81358181111561057f5761057f6104f6565b604051601f8201601f19908116603f011681019083821181831017156105a7576105a76104f6565b816040528281528960208487010111156105c057600080fd5b8260208601602083013760006020848301015280955050505050509250925092565b6000602082840312156105f457600080fd5b81516104b681610484565b60018060a01b038316815260006020604081840152835180604085015260005b8181101561063b5785810183015185820160600152820161061f565b8181111561064d576000606083870101525b50601f01601f19169290920160600194935050505056fea26469706673582212207ad53e1008cce369999f6b5f2f77109510b404ff1de9b47b639981fd68e6239264736f6c63430008090033",
"linkReferences": {},
"deployedLinkReferences": {}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,66 +0,0 @@
import { BlockTag, JsonRpcProvider, Signer, dataSlice, toNumber } from "ethers";
import { ChainQuerier } from "../ChainQuerier";
import { BlockExtraData } from "../../../core/clients/blockchain/linea/IL2ChainQuerier";
export class L2ChainQuerier extends ChainQuerier {
private blockExtraDataCache: BlockExtraData;
private cacheIsValidForBlockNumber: bigint;
/**
* Creates an instance of `L2ChainQuerier`.
*
* @param {JsonRpcProvider} provider - The JSON RPC provider for interacting with the Ethereum network.
* @param {Signer} [signer] - An optional Ethers.js signer object for signing transactions.
*/
constructor(provider: JsonRpcProvider, signer?: Signer) {
super(provider, signer);
}
/**
* Fetches and format extra data from a block.
*
* @param {BlockTag} blockNumber - The block number or tag to fetch extra data for.
* @returns {Promise<BlockExtraData | null>} A promise that resolves to an object containing the formatted block's extra data, or null if the block is not found.
*/
public async getBlockExtraData(blockNumber: BlockTag): Promise<BlockExtraData | null> {
if (typeof blockNumber === "number" && this.isCacheValid(blockNumber)) {
return this.blockExtraDataCache;
}
const block = await this.getBlock(blockNumber);
if (!block) {
return null;
}
const version = dataSlice(block.extraData, 0, 1);
const fixedCost = dataSlice(block.extraData, 1, 5);
const variableCost = dataSlice(block.extraData, 5, 9);
const ethGasPrice = dataSlice(block.extraData, 9, 13);
// original values are in Kwei and here we convert them back to wei
const extraData = {
version: toNumber(version),
fixedCost: toNumber(fixedCost) * 1000,
variableCost: toNumber(variableCost) * 1000,
ethGasPrice: toNumber(ethGasPrice) * 1000,
};
if (typeof blockNumber === "number") {
this.cacheIsValidForBlockNumber = BigInt(blockNumber);
this.blockExtraDataCache = extraData;
}
return extraData;
}
/**
* Checks if the cached block extra data is still valid based on the current block number.
*
* @private
* @param {number} currentBlockNumber - The current block number.
* @returns {boolean} True if the cache is valid, false otherwise.
*/
private isCacheValid(currentBlockNumber: number): boolean {
return this.cacheIsValidForBlockNumber >= BigInt(currentBlockNumber);
}
}

View File

@@ -1,52 +0,0 @@
import { Block, JsonRpcProvider, Wallet } from "ethers";
import { describe, afterEach, it, beforeEach } from "@jest/globals";
import { MockProxy, mock, mockClear } from "jest-mock-extended";
import { L2ChainQuerier } from "../L2ChainQuerier";
import { TEST_L1_SIGNER_PRIVATE_KEY } from "../../../../utils/testing/constants";
describe("L2ChainQuerier", () => {
let providerMock: MockProxy<JsonRpcProvider>;
let chainQuerier: L2ChainQuerier;
beforeEach(() => {
providerMock = mock<JsonRpcProvider>();
chainQuerier = new L2ChainQuerier(providerMock, new Wallet(TEST_L1_SIGNER_PRIVATE_KEY, providerMock));
});
afterEach(() => {
mockClear(providerMock);
});
describe("getBlockExtraData", () => {
it("should return block extraData", async () => {
const blockMocked: Block = {
baseFeePerGas: 7n,
difficulty: 2n,
extraData:
"0x0100989680015eb3c80000ea600000000000000000000000000000000000000024997ceb570c667b9c369d351b384ce97dcfe0dda90696fc3b007b8d7160672548a6716cc33ffe0e4004c555a0c7edd9ddc2545a630f2276a2964dcf856e6ab501",
gasLimit: 61000000n,
gasUsed: 138144n,
hash: "0xc53d0f6b65feabf0422bb897d3f5de2c32d57612f88eb4a366d8076a040a715a",
miner: "0x0000000000000000000000000000000000000000",
nonce: "0x0000000000000000",
number: 1635519,
parentHash: "0xecc7bd0b6d533b13fc65529e5da174062d93f8f426c32929914a375f00a19cc3",
receiptsRoot: "0x0cfd91942f25f029d50078225749dbd8601dd922713e783e4210568ed45b6cd3",
stateRoot: "0xad1346e81f574b511c917cd76c5b70f1d6852b871569bedd9ef02160746e3ffa",
timestamp: 1718030601,
transactions: [],
provider: providerMock,
parentBeaconBlockRoot: null,
blobGasUsed: null,
excessBlobGas: null,
} as unknown as Block;
jest.spyOn(providerMock, "getBlock").mockResolvedValue(blockMocked);
expect(await chainQuerier.getBlockExtraData("latest")).toStrictEqual({
version: 1,
fixedCost: 10000000000,
variableCost: 22983624000,
ethGasPrice: 60000000,
});
});
});
});

View File

@@ -1,15 +1,19 @@
import { JsonRpcProvider } from "ethers";
import {
MessageSentEventFilters,
ILineaRollupLogClient,
L2MessagingBlockAnchoredFilters,
MessageClaimedFilters,
} from "../../../core/clients/blockchain/ethereum/ILineaRollupLogClient";
import { L2MessagingBlockAnchored, MessageClaimed, MessageSent } from "../../../core/types/Events";
import { LineaRollup, LineaRollup__factory } from "../typechain";
import { TypedContractEvent, TypedDeferredTopicFilter, TypedEventLog } from "../typechain/common";
import { L2MessagingBlockAnchoredEvent, MessageClaimedEvent, MessageSentEvent } from "../typechain/LineaRollup";
import { isUndefined } from "../../../core/utils/shared";
} from "../../core/clients/ethereum";
import { L2MessagingBlockAnchored, MessageClaimed, MessageSent } from "../../core/types";
import { LineaRollup, LineaRollup__factory } from "../../contracts/typechain";
import { TypedContractEvent, TypedDeferredTopicFilter, TypedEventLog } from "../../contracts/typechain/common";
import {
L2MessagingBlockAnchoredEvent,
MessageClaimedEvent,
MessageSentEvent,
} from "../../contracts/typechain/LineaRollup";
import { isUndefined } from "../../core/utils";
import { BrowserProvider, Provider } from "../providers";
export class EthersLineaRollupLogClient implements ILineaRollupLogClient {
private lineaRollup: LineaRollup;
@@ -17,10 +21,10 @@ export class EthersLineaRollupLogClient implements ILineaRollupLogClient {
/**
* Initializes a new instance of the `EthersLineaRollupLogClient`.
*
* @param {JsonRpcProvider} provider - The JSON RPC provider for interacting with the Ethereum network.
* @param {Provider | BrowserProvider} provider - The JSON RPC provider for interacting with the Ethereum network.
* @param {string} contractAddress - The address of the Linea Rollup contract.
*/
constructor(provider: JsonRpcProvider, contractAddress: string) {
constructor(provider: Provider | BrowserProvider, contractAddress: string) {
this.lineaRollup = LineaRollup__factory.connect(contractAddress, provider);
}

View File

@@ -1,13 +1,18 @@
import { Overrides, ContractTransactionResponse, Signer, TransactionReceipt, TransactionResponse } from "ethers";
import { OnChainMessageStatus } from "../../core/enums/MessageEnums";
import { Cache } from "../Cache";
import { ILineaRollupClient } from "../../core/clients/blockchain/ethereum/ILineaRollupClient";
import { IL2MessageServiceClient } from "../../core/clients/blockchain/linea/IL2MessageServiceClient";
import { IL2MessageServiceLogClient } from "../../core/clients/blockchain/linea/IL2MessageServiceLogClient";
import { MessageSent } from "../../core/types/Events";
import { BaseError } from "../../core/errors/Base";
import { Network } from "../config";
import { FinalizationMessagingInfo, Proof } from "../../core/clients/blockchain/ethereum/IMerkleTreeService";
import {
Overrides,
ContractTransactionResponse,
Signer,
TransactionReceipt,
TransactionResponse,
ErrorDescription,
} from "ethers";
import { OnChainMessageStatus } from "../../core/enums/message";
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";
export class L1ClaimingService {
private cache: Cache;
@@ -25,14 +30,16 @@ export class L1ClaimingService {
Overrides,
TransactionReceipt,
TransactionResponse,
ContractTransactionResponse
ContractTransactionResponse,
ErrorDescription
>,
public readonly l2ContractClient: IL2MessageServiceClient<
Overrides,
TransactionReceipt,
TransactionResponse,
ContractTransactionResponse,
Signer
Signer,
ErrorDescription
>,
public readonly l2EventLogClient: IL2MessageServiceLogClient,
private readonly network: Network,

View File

@@ -1,48 +1,53 @@
import {
Overrides,
ContractTransactionResponse,
JsonRpcProvider,
TransactionRequest,
TransactionResponse,
Signer,
TransactionReceipt,
Block,
ErrorDescription,
} from "ethers";
import { LineaRollup, LineaRollup__factory } from "../typechain";
import { GasEstimationError } from "../../../core/errors/GasFeeErrors";
import { Message, MessageProps } from "../../../core/entities/Message";
import { OnChainMessageStatus } from "../../../core/enums/MessageEnums";
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 {
MESSAGE_UNKNOWN_STATUS,
MESSAGE_CLAIMED_STATUS,
ZERO_ADDRESS,
DEFAULT_RATE_LIMIT_MARGIN,
} from "../../../core/constants";
import { ILineaRollupClient } from "../../../core/clients/blockchain/ethereum/ILineaRollupClient";
import { ILineaRollupLogClient } from "../../../core/clients/blockchain/ethereum/ILineaRollupLogClient";
import { IL2MessageServiceLogClient } from "../../../core/clients/blockchain/linea/IL2MessageServiceLogClient";
import { BaseError } from "../../../core/errors/Base";
import { SDKMode } from "../../../sdk/config";
import { formatMessageStatus } from "../../../core/utils/message";
import { GasFees, IEthereumGasProvider } from "../../../core/clients/blockchain/IGasProvider";
import { IMessageRetriever } from "../../../core/clients/blockchain/IMessageRetriever";
} from "../../core/constants";
import {
ILineaRollupClient,
ILineaRollupLogClient,
FinalizationMessagingInfo,
IMerkleTreeService,
Proof,
} from "../../../core/clients/blockchain/ethereum/IMerkleTreeService";
import { MessageSent } from "../../../core/types/Events";
import { IChainQuerier } from "../../../core/clients/blockchain/IChainQuerier";
} from "../../core/clients/ethereum";
import { IL2MessageServiceLogClient } from "../../core/clients/linea";
import { formatMessageStatus, isString } from "../../core/utils";
import { GasFees, IEthereumGasProvider } from "../../core/clients/IGasProvider";
import { IMessageRetriever } from "../../core/clients/IMessageRetriever";
import { IProvider } from "../../core/clients/IProvider";
import { BrowserProvider, Provider } from "../providers";
export class LineaRollupClient
implements ILineaRollupClient<Overrides, TransactionReceipt, TransactionResponse, ContractTransactionResponse>
implements
ILineaRollupClient<
Overrides,
TransactionReceipt,
TransactionResponse,
ContractTransactionResponse,
ErrorDescription
>
{
private readonly contract: LineaRollup;
/**
* @notice Initializes a new instance of the `LineaRollupClient`.
* @dev This constructor sets up the Linea Rollup Client with the necessary dependencies and configurations.
* @param {IChainQuerier} chainQuerier The chain querier for interacting with the blockchain.
* @param {ethers.Provider} provider The provider for interacting with the blockchain.
* @param {string} contractAddress The address of the Linea Rollup contract.
* @param {ILineaRollupLogClient} lineaRollupLogClient An instance of a class implementing the `ILineaRollupLogClient` interface for fetching events from the blockchain.
* @param {IL2MessageServiceLogClient} l2MessageServiceLogClient An instance of a class implementing the `IL2MessageServiceLogClient` interface for fetching events from the blockchain.
@@ -53,12 +58,12 @@ export class LineaRollupClient
* @param {Signer} signer An optional Ethers.js signer object for signing transactions.
*/
constructor(
private readonly chainQuerier: IChainQuerier<
private readonly provider: IProvider<
TransactionReceipt,
Block,
TransactionRequest,
TransactionResponse,
JsonRpcProvider
Provider | BrowserProvider
>,
private readonly contractAddress: string,
private readonly lineaRollupLogClient: ILineaRollupLogClient,
@@ -154,7 +159,7 @@ export class LineaRollupClient
*/
private getContract(contractAddress: string, signer?: Signer): LineaRollup {
if (this.mode === "read-only") {
return LineaRollup__factory.connect(contractAddress, this.chainQuerier.getProvider());
return LineaRollup__factory.connect(contractAddress, this.provider);
}
if (!signer) {
@@ -234,12 +239,12 @@ export class LineaRollupClient
/**
* Estimates the gas required for the claimMessage transaction.
* @param {MessageProps & { feeRecipient?: string }} message - The message information.
* @param {Message & { feeRecipient?: string }} message - The message information.
* @param {Overrides} [overrides={}] - Ethers payable overrides. Defaults to `{}` if not specified.
* @returns {Promise<bigint>} The estimated transaction gas.
*/
public async estimateClaimWithoutProofGas(
message: MessageProps & { feeRecipient?: string },
message: Message & { feeRecipient?: string },
overrides: Overrides = {},
): Promise<bigint> {
if (this.mode === "read-only") {
@@ -270,12 +275,12 @@ export class LineaRollupClient
/**
* Claims the message on L1 without merkle tree (for message sent before the migration).
* @param {MessageProps & { feeRecipient?: string }} message - The message information.
* @param {Message & { feeRecipient?: string }} message - The message information.
* @param {Overrides} [overrides={}] - Ethers payable overrides. Defaults to `{}` if not specified.
* @returns {Promise<ContractTransactionResponse>} The transaction response.
*/
public async claimWithoutProof(
message: MessageProps & { feeRecipient?: string },
message: Message & { feeRecipient?: string },
overrides: Overrides = {},
): Promise<ContractTransactionResponse> {
if (this.mode === "read-only") {
@@ -403,7 +408,7 @@ export class LineaRollupClient
throw new BaseError("'retryTransactionWithHigherFee' function not callable using readOnly mode.");
}
const transaction = await this.chainQuerier.getTransaction(transactionHash);
const transaction = await this.provider.getTransaction(transactionHash);
if (!transaction) {
throw new BaseError(`Transaction with hash ${transactionHash} not found.`);
@@ -440,7 +445,7 @@ export class LineaRollupClient
maxFeePerGas,
};
const signedTransaction = await this.signer!.signTransaction(updatedTransaction);
return await this.chainQuerier.broadcastTransaction(signedTransaction);
return await this.provider.broadcastTransaction(signedTransaction);
}
/**
@@ -460,14 +465,15 @@ export class LineaRollupClient
}
/**
* Checks if an error is of type 'RateLimitExceeded'.
* Parses the error from the transaction.
* @param {string} transactionHash - The transaction hash.
* @returns {Promise<boolean>} True if the error type is 'RateLimitExceeded', false otherwise.
* @returns {Promise<ErrorDescription | string>} The error description or the error bytes.
*/
public async isRateLimitExceededError(transactionHash: string): Promise<boolean> {
public async parseTransactionError(transactionHash: string): Promise<ErrorDescription | string> {
let errorEncodedData = "0x";
try {
const tx = await this.chainQuerier.getTransaction(transactionHash);
const errorEncodedData = await this.chainQuerier.ethCall({
const tx = await this.provider.getTransaction(transactionHash);
errorEncodedData = await this.provider.call({
to: tx?.to,
from: tx?.from,
nonce: tx?.nonce,
@@ -481,9 +487,28 @@ export class LineaRollupClient
});
const error = this.contract.interface.parseError(errorEncodedData);
return error?.name === "RateLimitExceeded";
if (!error) {
return errorEncodedData;
}
return error;
} catch (e) {
return false;
return errorEncodedData;
}
}
/**
* Checks if an error is of type 'RateLimitExceeded'.
* @param {string} transactionHash - The transaction hash.
* @returns {Promise<boolean>} True if the error type is 'RateLimitExceeded', false otherwise.
*/
public async isRateLimitExceededError(transactionHash: string): Promise<boolean> {
const parsedError = await this.parseTransactionError(transactionHash);
if (isString(parsedError)) {
return false;
}
return parsedError.name === "RateLimitExceeded";
}
}

View File

@@ -1,11 +1,12 @@
import { ILineaRollupLogClient } from "../../../core/clients/blockchain/ethereum/ILineaRollupLogClient";
import { Block, JsonRpcProvider, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { MessageSent } from "../../../core/types/Events";
import { MESSAGE_SENT_EVENT_SIGNATURE } from "../../../core/constants";
import { isNull } from "../../../core/utils/shared";
import { LineaRollup, LineaRollup__factory } from "../typechain";
import { IMessageRetriever } from "../../../core/clients/blockchain/IMessageRetriever";
import { IChainQuerier } from "sdk/src/core/clients/blockchain/IChainQuerier";
import { Block, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { ILineaRollupLogClient } from "../../core/clients/ethereum";
import { MessageSent } from "../../core/types";
import { MESSAGE_SENT_EVENT_SIGNATURE } from "../../core/constants";
import { isNull } from "../../core/utils";
import { LineaRollup, LineaRollup__factory } from "../../contracts/typechain";
import { IMessageRetriever } from "../../core/clients/IMessageRetriever";
import { IProvider } from "../../core/clients/IProvider";
import { BrowserProvider, Provider } from "../providers";
export class LineaRollupMessageRetriever implements IMessageRetriever<TransactionReceipt> {
private readonly contract: LineaRollup;
@@ -13,22 +14,22 @@ export class LineaRollupMessageRetriever implements IMessageRetriever<Transactio
/**
* Initializes a new instance of the `LineaRollupMessageRetriever`.
*
* @param {IChainQuerier} chainQuerier - The chain querier for interacting with the blockchain.
* @param {IProvider} provider - The provider for interacting with the blockchain.
* @param {ILineaRollupLogClient} lineaRollupLogClient - An instance of a class implementing the `ILineaRollupLogClient` interface for fetching events from the blockchain.
* @param {string} contractAddress - The address of the Linea Rollup contract.
*/
constructor(
private readonly chainQuerier: IChainQuerier<
private readonly provider: IProvider<
TransactionReceipt,
Block,
TransactionRequest,
TransactionResponse,
JsonRpcProvider
Provider | BrowserProvider
>,
private readonly lineaRollupLogClient: ILineaRollupLogClient,
private readonly contractAddress: string,
) {
this.contract = LineaRollup__factory.connect(contractAddress, this.chainQuerier.getProvider());
this.contract = LineaRollup__factory.connect(contractAddress, this.provider);
}
/**
@@ -51,7 +52,7 @@ export class LineaRollupMessageRetriever implements IMessageRetriever<Transactio
* @returns {Promise<MessageSent[] | null>} An array of message information or null if not found.
*/
public async getMessagesByTransactionHash(transactionHash: string): Promise<MessageSent[] | null> {
const receipt = await this.chainQuerier.getTransactionReceipt(transactionHash);
const receipt = await this.provider.getTransactionReceipt(transactionHash);
if (!receipt) {
return null;
}
@@ -82,7 +83,7 @@ export class LineaRollupMessageRetriever implements IMessageRetriever<Transactio
return null;
}
const receipt = await this.chainQuerier.getTransactionReceipt(event.transactionHash);
const receipt = await this.provider.getTransactionReceipt(event.transactionHash);
if (!receipt) {
return null;
}

View File

@@ -1,20 +1,21 @@
import { Block, JsonRpcProvider, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { SparseMerkleTreeFactory } from "../../../services/merkleTree/MerkleTreeFactory";
import { BaseError } from "../../../core/errors/Base";
import { ILineaRollupLogClient } from "../../../core/clients/blockchain/ethereum/ILineaRollupLogClient";
import { IL2MessageServiceLogClient } from "../../../core/clients/blockchain/linea/IL2MessageServiceLogClient";
import { Block, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { SparseMerkleTreeFactory } from "../../utils/merkleTree/MerkleTreeFactory";
import { BaseError } from "../../core/errors";
import {
ILineaRollupLogClient,
FinalizationMessagingInfo,
IMerkleTreeService,
Proof,
} from "../../core/clients/ethereum";
import { IL2MessageServiceLogClient } from "../../core/clients/linea";
import {
L2_MERKLE_TREE_ADDED_EVENT_SIGNATURE,
L2_MESSAGING_BLOCK_ANCHORED_EVENT_SIGNATURE,
ZERO_HASH,
} from "../../../core/constants";
import { LineaRollup, LineaRollup__factory } from "../typechain";
import {
FinalizationMessagingInfo,
IMerkleTreeService,
Proof,
} from "../../../core/clients/blockchain/ethereum/IMerkleTreeService";
import { IChainQuerier } from "../../../core/clients/blockchain/IChainQuerier";
} from "../../core/constants";
import { LineaRollup, LineaRollup__factory } from "../../contracts/typechain";
import { IProvider } from "../../core/clients/IProvider";
import { BrowserProvider, Provider } from "../providers";
export class MerkleTreeService implements IMerkleTreeService {
private readonly contract: LineaRollup;
@@ -22,26 +23,26 @@ export class MerkleTreeService implements IMerkleTreeService {
/**
* Initializes a new instance of the `MerkleTreeService`.
*
* @param {IChainQuerier} chainQuerier - The chain querier for interacting with the blockchain.
* @param {IProvider} provider - The provider for interacting with the blockchain.
* @param {string} contractAddress - The address of the Linea Rollup contract.
* @param {ILineaRollupLogClient} lineaRollupLogClient - An instance of a class implementing the `ILineaRollupLogClient` interface for fetching events from ethereum.
* @param {IL2MessageServiceLogClient} l2MessageServiceLogClient - An instance of a class implementing the `IL2MessageServiceLogClient` interface for fetching events from linea.
* @param {number} l2MessageTreeDepth - The depth of the L2 message tree.
*/
constructor(
private readonly chainQuerier: IChainQuerier<
private readonly provider: IProvider<
TransactionReceipt,
Block,
TransactionRequest,
TransactionResponse,
JsonRpcProvider
Provider | BrowserProvider
>,
private readonly contractAddress: string,
private readonly lineaRollupLogClient: ILineaRollupLogClient,
private readonly l2MessageServiceLogClient: IL2MessageServiceLogClient,
private readonly l2MessageTreeDepth: number,
) {
this.contract = LineaRollup__factory.connect(contractAddress, this.chainQuerier.getProvider());
this.contract = LineaRollup__factory.connect(contractAddress, this.provider);
}
/**
@@ -89,7 +90,7 @@ export class MerkleTreeService implements IMerkleTreeService {
* @returns {Promise<FinalizationMessagingInfo>} The finalization messaging info: l2MessagingBlocksRange, l2MerkleRoots, treeDepth.
*/
public async getFinalizationMessagingInfo(transactionHash: string): Promise<FinalizationMessagingInfo> {
const receipt = await this.chainQuerier.getTransactionReceipt(transactionHash);
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}.`);

View File

@@ -1,26 +1,26 @@
import { describe, afterEach, it, expect, beforeEach } from "@jest/globals";
import { MockProxy, mock, mockClear } from "jest-mock-extended";
import { JsonRpcProvider } from "ethers";
import { EthersLineaRollupLogClient } from "../EthersLineaRollupLogClient";
import { TEST_CONTRACT_ADDRESS_1 } from "../../../utils/testing/constants/common";
import {
testL1NetworkConfig,
testL2MessagingBlockAnchoredEvent,
testL2MessagingBlockAnchoredEventLog,
testMessageClaimedEvent,
testMessageClaimedEventLog,
testMessageSentEvent,
testMessageSentEventLog,
} from "../../../../utils/testing/constants";
import { LineaRollup, LineaRollup__factory } from "../../typechain";
import { mockProperty } from "../../../../utils/testing/helpers";
} from "../../../utils/testing/constants/events";
import { LineaRollup, LineaRollup__factory } from "../../../contracts/typechain";
import { mockProperty } from "../../../utils/testing/helpers";
import { Provider } from "../../providers";
describe("TestEthersLineaRollupLogClient", () => {
let providerMock: MockProxy<JsonRpcProvider>;
let providerMock: MockProxy<Provider>;
let lineaRollupMock: MockProxy<LineaRollup>;
let lineaRollupLogClient: EthersLineaRollupLogClient;
beforeEach(() => {
providerMock = mock<JsonRpcProvider>();
providerMock = mock<Provider>();
lineaRollupMock = mock<LineaRollup>();
mockProperty(lineaRollupMock, "filters", {
...lineaRollupMock.filters,
@@ -31,10 +31,7 @@ describe("TestEthersLineaRollupLogClient", () => {
} as any);
jest.spyOn(LineaRollup__factory, "connect").mockReturnValue(lineaRollupMock);
lineaRollupLogClient = new EthersLineaRollupLogClient(
providerMock,
testL1NetworkConfig.messageServiceContractAddress,
);
lineaRollupLogClient = new EthersLineaRollupLogClient(providerMock, TEST_CONTRACT_ADDRESS_1);
});
afterEach(() => {

View File

@@ -1,9 +1,9 @@
import { describe, it } from "@jest/globals";
import { ContractTransactionResponse, ethers, JsonRpcProvider } from "ethers";
import { ContractTransactionResponse, ethers } from "ethers";
import { MockProxy, mock, mockClear } from "jest-mock-extended";
import { L1ClaimingService } from "../L1ClaimingService";
import { LineaRollupClient } from "../../../clients/blockchain/ethereum/LineaRollupClient";
import { L2MessageServiceClient } from "../../../clients/blockchain/linea/L2MessageServiceClient";
import { LineaRollupClient } from "../LineaRollupClient";
import { L2MessageServiceClient } from "../../linea";
import {
generateHexString,
generateL2MerkleTreeAddedLog,
@@ -20,17 +20,20 @@ import {
TEST_MESSAGE_HASH,
TEST_MESSAGE_HASH_2,
TEST_TRANSACTION_HASH,
} from "../../../utils/testing/constants/common";
import {
testL2MessagingBlockAnchoredEvent,
testMessageSentEvent,
testServiceVersionMigratedEvent,
} from "../../../utils/testing/constants";
import { EthersL2MessageServiceLogClient } from "../../../clients/blockchain/linea/EthersL2MessageServiceLogClient";
import { EthersLineaRollupLogClient } from "../../../clients/blockchain/ethereum/EthersLineaRollupLogClient";
} from "../../../utils/testing/constants/events";
import { EthersL2MessageServiceLogClient } from "../../linea/EthersL2MessageServiceLogClient";
import { EthersLineaRollupLogClient } from "../EthersLineaRollupLogClient";
import { ZERO_ADDRESS } from "../../../core/constants";
import { LineaProvider, Provider } from "../../providers";
describe("L1ClaimingService", () => {
let l1Provider: MockProxy<JsonRpcProvider>;
let l2Provider: MockProxy<JsonRpcProvider>;
let l1Provider: MockProxy<Provider>;
let l2Provider: MockProxy<LineaProvider>;
let l1ClaimingService: L1ClaimingService;
let lineaRollupClient: LineaRollupClient;
@@ -40,8 +43,8 @@ describe("L1ClaimingService", () => {
let l1LogClient: EthersLineaRollupLogClient;
beforeEach(() => {
l1Provider = mock<JsonRpcProvider>();
l2Provider = mock<JsonRpcProvider>();
l1Provider = mock<Provider>();
l2Provider = mock<LineaProvider>();
const clients = generateLineaRollupClient(
l1Provider,

View File

@@ -1,19 +1,22 @@
import { describe, afterEach, it, expect, beforeEach } from "@jest/globals";
import { MockProxy, mock, mockClear, mockDeep } from "jest-mock-extended";
import { ContractTransactionResponse, JsonRpcProvider, Wallet } from "ethers";
import { ContractTransactionResponse, Wallet } from "ethers";
import {
testMessageSentEvent,
TEST_MESSAGE_HASH,
TEST_CONTRACT_ADDRESS_1,
TEST_TRANSACTION_HASH,
TEST_ADDRESS_2,
testMessageClaimedEvent,
testL2MessagingBlockAnchoredEvent,
TEST_MERKLE_ROOT,
TEST_CONTRACT_ADDRESS_2,
TEST_ADDRESS_1,
} from "../../../../utils/testing/constants";
import { LineaRollup, LineaRollup__factory } from "../../typechain";
DEFAULT_MAX_FEE_PER_GAS,
} from "../../../utils/testing/constants/common";
import {
testMessageSentEvent,
testMessageClaimedEvent,
testL2MessagingBlockAnchoredEvent,
} from "../../../utils/testing/constants/events";
import { LineaRollup, LineaRollup__factory } from "../../../contracts/typechain";
import {
generateL2MerkleTreeAddedLog,
generateL2MessagingBlockAnchoredLog,
@@ -23,19 +26,20 @@ import {
generateTransactionReceiptWithLogs,
generateTransactionResponse,
mockProperty,
} from "../../../../utils/testing/helpers";
} from "../../../utils/testing/helpers";
import { LineaRollupClient } from "../LineaRollupClient";
import { DEFAULT_MAX_FEE_PER_GAS, ZERO_ADDRESS } from "../../../../core/constants";
import { OnChainMessageStatus } from "../../../../core/enums/MessageEnums";
import { GasEstimationError } from "../../../../core/errors/GasFeeErrors";
import { BaseError } from "../../../../core/errors/Base";
import { ZERO_ADDRESS } from "../../../core/constants";
import { OnChainMessageStatus } from "../../../core/enums/message";
import { GasEstimationError } from "../../../core/errors/GasFeeErrors";
import { BaseError } from "../../../core/errors";
import { EthersL2MessageServiceLogClient } from "../../linea/EthersL2MessageServiceLogClient";
import { EthersLineaRollupLogClient } from "../EthersLineaRollupLogClient";
import { DefaultGasProvider } from "../../gas/DefaultGasProvider";
import { LineaProvider, Provider } from "../../providers";
describe("TestLineaRollupClient", () => {
let providerMock: MockProxy<JsonRpcProvider>;
let l2ProviderMock: MockProxy<JsonRpcProvider>;
let providerMock: MockProxy<Provider>;
let l2ProviderMock: MockProxy<LineaProvider>;
let walletMock: MockProxy<Wallet>;
let lineaRollupMock: MockProxy<LineaRollup>;
@@ -45,8 +49,8 @@ describe("TestLineaRollupClient", () => {
let gasFeeProvider: DefaultGasProvider;
beforeEach(() => {
providerMock = mock<JsonRpcProvider>();
l2ProviderMock = mock<JsonRpcProvider>();
providerMock = mock<Provider>();
l2ProviderMock = mock<LineaProvider>();
walletMock = mock<Wallet>();
lineaRollupMock = mockDeep<LineaRollup>();
jest.spyOn(LineaRollup__factory, "connect").mockReturnValue(lineaRollupMock);
@@ -595,7 +599,7 @@ describe("TestLineaRollupClient", () => {
"read-write",
walletMock,
{
maxFeePerGas: 500000000n,
maxFeePerGasCap: 500000000n,
enforceMaxGasFee: true,
},
).lineaRollupClient;
@@ -633,7 +637,7 @@ describe("TestLineaRollupClient", () => {
"read-write",
walletMock,
{
maxFeePerGas: 500000000n,
maxFeePerGasCap: 500000000n,
enforceMaxGasFee: true,
},
).lineaRollupClient;
@@ -818,7 +822,7 @@ describe("TestLineaRollupClient", () => {
"read-write",
walletMock,
{
maxFeePerGas: 500000000n,
maxFeePerGasCap: 500000000n,
},
).lineaRollupClient;
@@ -856,7 +860,7 @@ describe("TestLineaRollupClient", () => {
"read-write",
walletMock,
{
maxFeePerGas: 500000000n,
maxFeePerGasCap: 500000000n,
enforceMaxGasFee: true,
},
).lineaRollupClient;

View File

@@ -1,28 +1,29 @@
import { describe, beforeEach } from "@jest/globals";
import { JsonRpcProvider, Wallet } from "ethers";
import { Wallet } from "ethers";
import { MockProxy, mock } from "jest-mock-extended";
import {
TEST_CONTRACT_ADDRESS_1,
TEST_CONTRACT_ADDRESS_2,
TEST_MESSAGE_HASH,
TEST_TRANSACTION_HASH,
testMessageSentEvent,
} from "../../../../utils/testing/constants";
import { generateLineaRollupClient, generateTransactionReceipt } from "../../../../utils/testing/helpers";
} from "../../../utils/testing/constants/common";
import { testMessageSentEvent } from "../../../utils/testing/constants/events";
import { generateLineaRollupClient, generateTransactionReceipt } from "../../../utils/testing/helpers";
import { EthersLineaRollupLogClient } from "../EthersLineaRollupLogClient";
import { LineaRollupMessageRetriever } from "../LineaRollupMessageRetriever";
import { LineaProvider, Provider } from "../../providers";
describe("LineaRollupMessageRetriever", () => {
let providerMock: MockProxy<JsonRpcProvider>;
let l2ProviderMock: MockProxy<JsonRpcProvider>;
let providerMock: MockProxy<Provider>;
let l2ProviderMock: MockProxy<LineaProvider>;
let walletMock: MockProxy<Wallet>;
let messageRetriever: LineaRollupMessageRetriever;
let lineaRollupLogClient: EthersLineaRollupLogClient;
beforeEach(() => {
providerMock = mock<JsonRpcProvider>();
l2ProviderMock = mock<JsonRpcProvider>();
providerMock = mock<Provider>();
l2ProviderMock = mock<LineaProvider>();
walletMock = mock<Wallet>();
const clients = generateLineaRollupClient(

View File

@@ -1,5 +1,5 @@
import { describe, beforeEach } from "@jest/globals";
import { JsonRpcProvider, Wallet } from "ethers";
import { Wallet } from "ethers";
import { MockProxy, mock } from "jest-mock-extended";
import {
TEST_CONTRACT_ADDRESS_1,
@@ -7,23 +7,23 @@ import {
TEST_MERKLE_ROOT_2,
TEST_MESSAGE_HASH,
TEST_MESSAGE_HASH_2,
testL2MessagingBlockAnchoredEvent,
testMessageSentEvent,
} from "../../../../utils/testing/constants";
} from "../../../utils/testing/constants/common";
import { testL2MessagingBlockAnchoredEvent, testMessageSentEvent } from "../../../utils/testing/constants/events";
import {
generateL2MerkleTreeAddedLog,
generateL2MessagingBlockAnchoredLog,
generateLineaRollupClient,
generateTransactionReceiptWithLogs,
} from "../../../../utils/testing/helpers";
import { LineaRollup, LineaRollup__factory } from "../../typechain";
} from "../../../utils/testing/helpers";
import { LineaRollup, LineaRollup__factory } from "../../../contracts/typechain";
import { EthersL2MessageServiceLogClient } from "../../linea/EthersL2MessageServiceLogClient";
import { EthersLineaRollupLogClient } from "../EthersLineaRollupLogClient";
import { MerkleTreeService } from "../MerkleTreeService";
import { LineaProvider, Provider } from "../../providers";
describe("MerkleTreeService", () => {
let providerMock: MockProxy<JsonRpcProvider>;
let l2ProviderMock: MockProxy<JsonRpcProvider>;
let providerMock: MockProxy<Provider>;
let l2ProviderMock: MockProxy<LineaProvider>;
let walletMock: MockProxy<Wallet>;
let lineaRollupMock: MockProxy<LineaRollup>;
@@ -32,8 +32,8 @@ describe("MerkleTreeService", () => {
let l2MessageServiceLogClient: EthersL2MessageServiceLogClient;
beforeEach(() => {
providerMock = mock<JsonRpcProvider>();
l2ProviderMock = mock<JsonRpcProvider>();
providerMock = mock<Provider>();
l2ProviderMock = mock<LineaProvider>();
walletMock = mock<Wallet>();
lineaRollupMock = mock<LineaRollup>();

View File

@@ -0,0 +1,5 @@
export { EthersLineaRollupLogClient } from "./EthersLineaRollupLogClient";
export { LineaRollupClient } from "./LineaRollupClient";
export { LineaRollupMessageRetriever } from "./LineaRollupMessageRetriever";
export { MerkleTreeService } from "./MerkleTreeService";
export { L1ClaimingService } from "./L1ClaimingService";

View File

@@ -1,12 +1,8 @@
import { Block, JsonRpcProvider, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { FeeEstimationError } from "../../../core/errors/GasFeeErrors";
import {
DefaultGasProviderConfig,
FeeHistory,
GasFees,
IEthereumGasProvider,
} from "../../../core/clients/blockchain/IGasProvider";
import { IChainQuerier } from "../../../core/clients/blockchain/IChainQuerier";
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";
export class DefaultGasProvider implements IEthereumGasProvider<TransactionRequest> {
private gasFeesCache: GasFees;
@@ -15,21 +11,24 @@ export class DefaultGasProvider implements IEthereumGasProvider<TransactionReque
/**
* Creates an instance of DefaultGasProvider.
*
* @param {IChainQuerier} chainQuerier - The chain querier for interacting with the blockchain.
* @param {IProvider} provider - The provider for interacting with the blockchain.
* @param {DefaultGasProviderConfig} config - The configuration for the gas provider.
*/
constructor(
protected readonly chainQuerier: IChainQuerier<
protected readonly provider: IProvider<
TransactionReceipt,
Block,
TransactionRequest,
TransactionResponse,
JsonRpcProvider
Provider | LineaProvider | LineaBrowserProvider | BrowserProvider
>,
private readonly config: DefaultGasProviderConfig,
) {
this.cacheIsValidForBlockNumber = 0n;
this.gasFeesCache = { maxFeePerGas: this.config.maxFeePerGas, maxPriorityFeePerGas: this.config.maxFeePerGas };
this.gasFeesCache = {
maxFeePerGas: this.config.maxFeePerGasCap,
maxPriorityFeePerGas: this.config.maxFeePerGasCap,
};
}
/**
@@ -49,12 +48,12 @@ export class DefaultGasProvider implements IEthereumGasProvider<TransactionReque
public async getGasFees(): Promise<GasFees> {
if (this.config.enforceMaxGasFee) {
return {
maxPriorityFeePerGas: this.config.maxFeePerGas,
maxFeePerGas: this.config.maxFeePerGas,
maxPriorityFeePerGas: this.config.maxFeePerGasCap,
maxFeePerGas: this.config.maxFeePerGasCap,
};
}
const currentBlockNumber = await this.chainQuerier.getCurrentBlockNumber();
const currentBlockNumber = await this.provider.getBlockNumber();
if (this.isCacheValid(currentBlockNumber)) {
return this.gasFeesCache;
}
@@ -62,9 +61,9 @@ export class DefaultGasProvider implements IEthereumGasProvider<TransactionReque
const feeHistory = await this.fetchFeeHistory();
const maxPriorityFeePerGas = this.calculateMaxPriorityFee(feeHistory.reward);
if (maxPriorityFeePerGas > this.config.maxFeePerGas) {
if (maxPriorityFeePerGas > this.config.maxFeePerGasCap) {
throw new FeeEstimationError(
`Estimated miner tip of ${maxPriorityFeePerGas} exceeds configured max fee per gas of ${this.config.maxFeePerGas}!`,
`Estimated miner tip of ${maxPriorityFeePerGas} exceeds configured max fee per gas of ${this.config.maxFeePerGasCap}!`,
);
}
@@ -79,7 +78,7 @@ export class DefaultGasProvider implements IEthereumGasProvider<TransactionReque
* @returns {Promise<FeeHistory>} A promise that resolves to the fee history.
*/
private async fetchFeeHistory(): Promise<FeeHistory> {
return this.chainQuerier.sendRequest("eth_feeHistory", ["0x4", "latest", [this.config.gasEstimationPercentile]]);
return this.provider.send("eth_feeHistory", ["0x4", "latest", [this.config.gasEstimationPercentile]]);
}
/**
@@ -120,12 +119,12 @@ export class DefaultGasProvider implements IEthereumGasProvider<TransactionReque
if (maxFeePerGas > 0n && maxPriorityFeePerGas > 0n) {
this.gasFeesCache = {
maxPriorityFeePerGas,
maxFeePerGas: maxFeePerGas > this.config.maxFeePerGas ? this.config.maxFeePerGas : maxFeePerGas,
maxFeePerGas: maxFeePerGas > this.config.maxFeePerGasCap ? this.config.maxFeePerGasCap : maxFeePerGas,
};
} else {
this.gasFeesCache = {
maxPriorityFeePerGas: this.config.maxFeePerGas,
maxFeePerGas: this.config.maxFeePerGas,
maxPriorityFeePerGas: this.config.maxFeePerGasCap,
maxFeePerGas: this.config.maxFeePerGasCap,
};
}
}
@@ -136,6 +135,6 @@ export class DefaultGasProvider implements IEthereumGasProvider<TransactionReque
* @returns {bigint} The maximum fee per gas.
*/
public getMaxFeePerGas(): bigint {
return this.config.maxFeePerGas;
return this.config.maxFeePerGasCap;
}
}

View File

@@ -1,10 +1,11 @@
import { Block, JsonRpcProvider, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { Block, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { DefaultGasProvider } from "./DefaultGasProvider";
import { LineaGasProvider } from "./LineaGasProvider";
import { IChainQuerier } from "../../../core/clients/blockchain/IChainQuerier";
import { GasFees, GasProviderConfig, IGasProvider, LineaGasFees } from "../../../core/clients/blockchain/IGasProvider";
import { Direction } from "../../../core/enums/MessageEnums";
import { BaseError } from "../../../core/errors/Base";
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";
export class GasProvider implements IGasProvider<TransactionRequest> {
private defaultGasProvider: DefaultGasProvider;
@@ -13,26 +14,26 @@ export class GasProvider implements IGasProvider<TransactionRequest> {
/**
* Creates an instance of `GasProvider`.
*
* @param {IChainQuerier} chainQuerier - The chain querier for interacting with the blockchain.
* @param {IProvider} provider - The provider for interacting with the blockchain.
* @param {GasProviderConfig} config - The configuration for the gas provider.
*/
constructor(
private chainQuerier: IChainQuerier<
private provider: IProvider<
TransactionReceipt,
Block,
TransactionRequest,
TransactionResponse,
JsonRpcProvider
Provider | LineaProvider | LineaBrowserProvider | BrowserProvider
>,
private readonly config: GasProviderConfig,
) {
this.defaultGasProvider = new DefaultGasProvider(this.chainQuerier, {
maxFeePerGas: config.maxFeePerGas,
this.defaultGasProvider = new DefaultGasProvider(this.provider, {
maxFeePerGasCap: config.maxFeePerGasCap,
gasEstimationPercentile: config.gasEstimationPercentile,
enforceMaxGasFee: config.enforceMaxGasFee,
});
this.lineaGasProvider = new LineaGasProvider(this.chainQuerier, {
maxFeePerGas: config.maxFeePerGas,
this.lineaGasProvider = new LineaGasProvider(this.provider, {
maxFeePerGasCap: config.maxFeePerGasCap,
enforceMaxGasFee: config.enforceMaxGasFee,
});
}
@@ -56,7 +57,7 @@ export class GasProvider implements IGasProvider<TransactionRequest> {
return this.lineaGasProvider.getGasFees(transactionRequest);
} else {
const fees = await this.defaultGasProvider.getGasFees();
const gasLimit = await this.chainQuerier.estimateGas({
const gasLimit = await this.provider.estimateGas({
...transactionRequest,
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
maxFeePerGas: fees.maxFeePerGas,

View File

@@ -1,11 +1,12 @@
import { Block, JsonRpcProvider, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { Block, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import {
LineaEstimateGasResponse,
LineaGasFees,
ILineaGasProvider,
LineaGasProviderConfig,
} from "../../../core/clients/blockchain/IGasProvider";
import { IChainQuerier } from "../../../core/clients/blockchain/IChainQuerier";
} from "../../core/clients/IGasProvider";
import { IProvider } from "../../core/clients/IProvider";
import { BrowserProvider, LineaBrowserProvider, LineaProvider, Provider } from "../providers";
const BASE_FEE_MULTIPLIER = 1.35;
const PRIORITY_FEE_MULTIPLIER = 1.05;
@@ -14,16 +15,16 @@ export class LineaGasProvider implements ILineaGasProvider<TransactionRequest> {
/**
* Creates an instance of `LineaGasProvider`.
*
* @param {IChainQuerier} chainQuerier - The chain querier for interacting with the blockchain.
* @param {IProvider} provider - The provider for interacting with the blockchain.
* @param {LineaGasProviderConfig} config - The configuration for the Linea gas provider.
*/
constructor(
protected readonly chainQuerier: IChainQuerier<
protected readonly provider: IProvider<
TransactionReceipt,
Block,
TransactionRequest,
TransactionResponse,
JsonRpcProvider
LineaProvider | LineaBrowserProvider | Provider | BrowserProvider
>,
private readonly config: LineaGasProviderConfig,
) {}
@@ -40,8 +41,8 @@ export class LineaGasProvider implements ILineaGasProvider<TransactionRequest> {
if (this.config.enforceMaxGasFee) {
return {
...gasFees,
maxPriorityFeePerGas: this.config.maxFeePerGas,
maxFeePerGas: this.config.maxFeePerGas,
maxPriorityFeePerGas: this.config.maxFeePerGasCap,
maxFeePerGas: this.config.maxFeePerGasCap,
};
}
return gasFees;
@@ -87,7 +88,7 @@ export class LineaGasProvider implements ILineaGasProvider<TransactionRequest> {
data: transactionRequest.data,
};
return this.chainQuerier.sendRequest("linea_estimateGas", [params]);
return this.provider.send("linea_estimateGas", [params]);
}
/**
@@ -120,6 +121,6 @@ export class LineaGasProvider implements ILineaGasProvider<TransactionRequest> {
* @returns {bigint} The maximum fee per gas.
*/
public getMaxFeePerGas(): bigint {
return this.config.maxFeePerGas;
return this.config.maxFeePerGasCap;
}
}

View File

@@ -1,40 +1,42 @@
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 { testL1NetworkConfig } from "../../../../utils/testing/constants";
import { ChainQuerier } from "../../ChainQuerier";
import { FeeEstimationError } from "../../../core/errors/GasFeeErrors";
import { Provider } from "../../providers/provider";
import { DEFAULT_GAS_ESTIMATION_PERCENTILE } from "../../../core/constants";
const MAX_FEE_PER_GAS = 100_000_000n;
describe("DefaultGasProvider", () => {
let chainQuerierMock: MockProxy<ChainQuerier>;
let providerMock: MockProxy<Provider>;
let eip1559GasProvider: DefaultGasProvider;
beforeEach(() => {
chainQuerierMock = mock<ChainQuerier>();
eip1559GasProvider = new DefaultGasProvider(chainQuerierMock, {
maxFeePerGas: testL1NetworkConfig.claiming.maxFeePerGas,
gasEstimationPercentile: testL1NetworkConfig.claiming.gasEstimationPercentile,
providerMock = mock<Provider>();
eip1559GasProvider = new DefaultGasProvider(providerMock, {
maxFeePerGasCap: MAX_FEE_PER_GAS,
gasEstimationPercentile: DEFAULT_GAS_ESTIMATION_PERCENTILE,
enforceMaxGasFee: false,
});
});
afterEach(() => {
mockClear(chainQuerierMock);
mockClear(providerMock);
});
describe("getGasFees", () => {
it("should return fee from cache if currentBlockNumber == cacheIsValidForBlockNumber", async () => {
jest.spyOn(chainQuerierMock, "getCurrentBlockNumber").mockResolvedValueOnce(0);
jest.spyOn(providerMock, "getBlockNumber").mockResolvedValueOnce(0);
const fees = await eip1559GasProvider.getGasFees();
expect(fees).toStrictEqual({
maxFeePerGas: testL1NetworkConfig.claiming.maxFeePerGas,
maxPriorityFeePerGas: testL1NetworkConfig.claiming.maxFeePerGas,
maxFeePerGas: MAX_FEE_PER_GAS,
maxPriorityFeePerGas: MAX_FEE_PER_GAS,
});
});
it("should throw an error 'FeeEstimationError' if maxPriorityFee is greater than maxFeePerGas", async () => {
jest.spyOn(chainQuerierMock, "getCurrentBlockNumber").mockResolvedValueOnce(1);
const sendSpy = jest.spyOn(chainQuerierMock, "sendRequest").mockResolvedValueOnce({
jest.spyOn(providerMock, "getBlockNumber").mockResolvedValueOnce(1);
const sendSpy = jest.spyOn(providerMock, "send").mockResolvedValueOnce({
baseFeePerGas: ["0x3da8e7618", "0x3e1ba3b1b", "0x3dfd72b90", "0x3d64eee76", "0x3d4da2da0", "0x3ccbcac6b"],
gasUsedRatio: [0.5290747666666666, 0.49240453333333334, 0.4615576, 0.49407083333333335, 0.4669053],
oldestBlock: "0xfab8ac",
@@ -51,9 +53,9 @@ describe("DefaultGasProvider", () => {
});
it("should return maxFeePerGas and maxPriorityFeePerGas", async () => {
jest.spyOn(chainQuerierMock, "getCurrentBlockNumber").mockResolvedValueOnce(1);
const sendSpy = jest.spyOn(chainQuerierMock, "sendRequest").mockResolvedValueOnce({
baseFeePerGas: ["0x3da8e7618", "0x3e1ba3b1b", "0x3dfd72b90", "0x3d64eee76", "0x3d4da2da0", "0x3ccbcac6b"],
jest.spyOn(providerMock, "getBlockNumber").mockResolvedValueOnce(1);
const sendSpy = jest.spyOn(providerMock, "send").mockResolvedValueOnce({
baseFeePerGas: ["0x3da8e", "0x3e1ba", "0x3dfd7", "0x3d64e", "0x3d4da", "0x3ccbc"],
gasUsedRatio: [0.5290747666666666, 0.49240453333333334, 0.4615576, 0.49407083333333335, 0.4669053],
oldestBlock: "0xfab8ac",
reward: [
@@ -68,7 +70,7 @@ describe("DefaultGasProvider", () => {
const fees = await eip1559GasProvider.getGasFees();
expect(fees).toStrictEqual({
maxFeePerGas: BigInt(testL1NetworkConfig.claiming.maxFeePerGas!),
maxFeePerGas: 32_217_395n,
maxPriorityFeePerGas: 31_719_355n,
});
@@ -76,9 +78,9 @@ describe("DefaultGasProvider", () => {
});
it("should return maxFeePerGas from config when maxFeePerGas and maxPriorityFeePerGas === 0", async () => {
jest.spyOn(chainQuerierMock, "getCurrentBlockNumber").mockResolvedValueOnce(1);
jest.spyOn(providerMock, "getBlockNumber").mockResolvedValueOnce(1);
const sendSpy = jest.spyOn(chainQuerierMock, "sendRequest").mockResolvedValueOnce({
const sendSpy = jest.spyOn(providerMock, "send").mockResolvedValueOnce({
baseFeePerGas: ["0x0", "0x0", "0x0", "0x0", "0x0", "0x0"],
gasUsedRatio: [0, 0, 0, 0, 0],
oldestBlock: "0xfab8ac",
@@ -94,8 +96,8 @@ describe("DefaultGasProvider", () => {
const fees = await eip1559GasProvider.getGasFees();
expect(fees).toStrictEqual({
maxFeePerGas: testL1NetworkConfig.claiming.maxFeePerGas!,
maxPriorityFeePerGas: testL1NetworkConfig.claiming.maxFeePerGas!,
maxFeePerGas: MAX_FEE_PER_GAS,
maxPriorityFeePerGas: MAX_FEE_PER_GAS,
});
expect(sendSpy).toHaveBeenCalledTimes(1);

View File

@@ -1,11 +1,12 @@
import { describe, afterEach, it, expect, beforeEach } from "@jest/globals";
import { MockProxy, mock, mockClear } from "jest-mock-extended";
import { ChainQuerier } from "../../ChainQuerier";
import { Provider } from "../../providers/provider";
import { GasProvider } from "../GasProvider";
import { Direction } from "../../../../core/enums/MessageEnums";
import { DEFAULT_GAS_ESTIMATION_PERCENTILE, DEFAULT_MAX_FEE_PER_GAS } from "../../../../core/constants";
import { generateTransactionRequest } from "../../../../utils/testing/helpers";
import { Direction } from "../../../core/enums/message";
import { DEFAULT_GAS_ESTIMATION_PERCENTILE, DEFAULT_MAX_FEE_PER_GAS_CAP } from "../../../core/constants";
import { generateTransactionRequest } from "../../../utils/testing/helpers";
import { toBeHex } from "ethers";
import { DEFAULT_MAX_FEE_PER_GAS } from "../../../utils/testing/constants/common";
const testFeeHistory = {
baseFeePerGas: ["0x3da8e7618", "0x3e1ba3b1b", "0x3dfd72b90", "0x3d64eee76", "0x3d4da2da0", "0x3ccbcac6b"],
@@ -21,22 +22,22 @@ const testFeeHistory = {
};
describe("GasProvider", () => {
let chainQuerierMock: MockProxy<ChainQuerier>;
let providerMock: MockProxy<Provider>;
let gasProvider: GasProvider;
beforeEach(() => {
chainQuerierMock = mock<ChainQuerier>();
gasProvider = new GasProvider(chainQuerierMock, {
providerMock = mock<Provider>();
gasProvider = new GasProvider(providerMock, {
enableLineaEstimateGas: true,
direction: Direction.L1_TO_L2,
enforceMaxGasFee: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGasCap: DEFAULT_MAX_FEE_PER_GAS_CAP,
gasEstimationPercentile: DEFAULT_GAS_ESTIMATION_PERCENTILE,
});
});
afterEach(() => {
mockClear(chainQuerierMock);
mockClear(providerMock);
jest.clearAllMocks();
});
@@ -49,7 +50,7 @@ describe("GasProvider", () => {
});
it("should use LineaGasProvider when enableLineaEstimateGas is enabled", async () => {
jest.spyOn(chainQuerierMock, "sendRequest").mockResolvedValueOnce({
jest.spyOn(providerMock, "send").mockResolvedValueOnce({
gasLimit: "0x300000",
baseFeePerGas: "0x7",
priorityFeePerGas: toBeHex(DEFAULT_MAX_FEE_PER_GAS),
@@ -68,18 +69,18 @@ describe("GasProvider", () => {
});
it("should use DefaultGasProvider when enableLineaEstimateGas is disabled", async () => {
gasProvider = new GasProvider(chainQuerierMock, {
gasProvider = new GasProvider(providerMock, {
enableLineaEstimateGas: false,
direction: Direction.L1_TO_L2,
enforceMaxGasFee: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGasCap: DEFAULT_MAX_FEE_PER_GAS_CAP,
gasEstimationPercentile: DEFAULT_GAS_ESTIMATION_PERCENTILE,
});
jest.spyOn(chainQuerierMock, "getCurrentBlockNumber").mockResolvedValueOnce(1);
jest.spyOn(providerMock, "getBlockNumber").mockResolvedValueOnce(1);
const estimatedGasLimit = 50_000n;
jest.spyOn(chainQuerierMock, "estimateGas").mockResolvedValueOnce(estimatedGasLimit);
jest.spyOn(chainQuerierMock, "sendRequest").mockResolvedValueOnce(testFeeHistory);
jest.spyOn(providerMock, "estimateGas").mockResolvedValueOnce(estimatedGasLimit);
jest.spyOn(providerMock, "send").mockResolvedValueOnce(testFeeHistory);
const gasFees = await gasProvider.getGasFees();
@@ -93,16 +94,16 @@ describe("GasProvider", () => {
describe("L2 to L1", () => {
it("should use DefaultGasProvider", async () => {
gasProvider = new GasProvider(chainQuerierMock, {
gasProvider = new GasProvider(providerMock, {
enableLineaEstimateGas: false,
direction: Direction.L2_TO_L1,
enforceMaxGasFee: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGasCap: DEFAULT_MAX_FEE_PER_GAS_CAP,
gasEstimationPercentile: DEFAULT_GAS_ESTIMATION_PERCENTILE,
});
jest.spyOn(chainQuerierMock, "getCurrentBlockNumber").mockResolvedValueOnce(1);
jest.spyOn(chainQuerierMock, "sendRequest").mockResolvedValueOnce(testFeeHistory);
jest.spyOn(providerMock, "getBlockNumber").mockResolvedValueOnce(1);
jest.spyOn(providerMock, "send").mockResolvedValueOnce(testFeeHistory);
const gasFees = await gasProvider.getGasFees();
@@ -120,11 +121,11 @@ describe("GasProvider", () => {
});
it("should use DefaultGasProvider if direction == L2_TO_L1", () => {
gasProvider = new GasProvider(chainQuerierMock, {
gasProvider = new GasProvider(providerMock, {
enableLineaEstimateGas: false,
direction: Direction.L2_TO_L1,
enforceMaxGasFee: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGasCap: DEFAULT_MAX_FEE_PER_GAS_CAP,
gasEstimationPercentile: DEFAULT_GAS_ESTIMATION_PERCENTILE,
});
expect(gasProvider.getMaxFeePerGas()).toStrictEqual(DEFAULT_MAX_FEE_PER_GAS);

View File

@@ -1,37 +1,38 @@
import { describe, afterEach, jest, it, expect, beforeEach } from "@jest/globals";
import { MockProxy, mock, mockClear } from "jest-mock-extended";
import { testL1NetworkConfig } from "../../../../utils/testing/constants";
import { LineaGasProvider } from "../LineaGasProvider";
import { ChainQuerier } from "../../ChainQuerier";
import { generateTransactionRequest } from "../../../../utils/testing/helpers";
import { generateTransactionRequest } from "../../../utils/testing/helpers";
import { toBeHex } from "ethers";
import { LineaProvider } from "../../providers";
const MAX_FEE_PER_GAS = 100_000_000n;
describe("LineaGasProvider", () => {
let chainQuerierMock: MockProxy<ChainQuerier>;
let providerMock: MockProxy<LineaProvider>;
let lineaGasProvider: LineaGasProvider;
beforeEach(() => {
chainQuerierMock = mock<ChainQuerier>();
lineaGasProvider = new LineaGasProvider(chainQuerierMock, {
maxFeePerGas: testL1NetworkConfig.claiming.maxFeePerGas,
providerMock = mock<LineaProvider>();
lineaGasProvider = new LineaGasProvider(providerMock, {
maxFeePerGasCap: MAX_FEE_PER_GAS,
enforceMaxGasFee: false,
});
});
afterEach(() => {
mockClear(chainQuerierMock);
mockClear(providerMock);
});
describe("getGasFees", () => {
it("should return maxFeePerGas, maxPriorityFeePerGas from config when enforceMaxGasFee option is enabled", async () => {
lineaGasProvider = new LineaGasProvider(chainQuerierMock, {
maxFeePerGas: testL1NetworkConfig.claiming.maxFeePerGas,
lineaGasProvider = new LineaGasProvider(providerMock, {
maxFeePerGasCap: MAX_FEE_PER_GAS,
enforceMaxGasFee: true,
});
jest.spyOn(chainQuerierMock, "getCurrentBlockNumber").mockResolvedValueOnce(1);
const sendRequestSpy = jest.spyOn(chainQuerierMock, "sendRequest").mockResolvedValueOnce({
jest.spyOn(providerMock, "getBlockNumber").mockResolvedValueOnce(1);
const sendRequestSpy = jest.spyOn(providerMock, "send").mockResolvedValueOnce({
baseFeePerGas: "0x7",
priorityFeePerGas: toBeHex(testL1NetworkConfig.claiming.maxFeePerGas),
priorityFeePerGas: toBeHex(MAX_FEE_PER_GAS),
gasLimit: toBeHex(50_000n),
});
@@ -40,18 +41,18 @@ describe("LineaGasProvider", () => {
const fees = await lineaGasProvider.getGasFees(transactionRequest);
expect(fees).toStrictEqual({
maxFeePerGas: testL1NetworkConfig.claiming.maxFeePerGas,
maxPriorityFeePerGas: testL1NetworkConfig.claiming.maxFeePerGas,
maxFeePerGas: MAX_FEE_PER_GAS,
maxPriorityFeePerGas: MAX_FEE_PER_GAS,
gasLimit: 50_000n,
});
expect(sendRequestSpy).toHaveBeenCalledTimes(1);
});
it("should return maxFeePerGas, maxPriorityFeePerGas and gasLimit", async () => {
jest.spyOn(chainQuerierMock, "getCurrentBlockNumber").mockResolvedValueOnce(1);
const sendRequestSpy = jest.spyOn(chainQuerierMock, "sendRequest").mockResolvedValueOnce({
jest.spyOn(providerMock, "getBlockNumber").mockResolvedValueOnce(1);
const sendRequestSpy = jest.spyOn(providerMock, "send").mockResolvedValueOnce({
baseFeePerGas: "0x7",
priorityFeePerGas: toBeHex(testL1NetworkConfig.claiming.maxFeePerGas),
priorityFeePerGas: toBeHex(MAX_FEE_PER_GAS),
gasLimit: toBeHex(50_000n),
});
@@ -60,7 +61,7 @@ describe("LineaGasProvider", () => {
const fees = await lineaGasProvider.getGasFees(transactionRequest);
const expectedBaseFee = (BigInt("0x7") * BigInt(1.35 * 100)) / 100n;
const expectedPriorityFeePerGas = (testL1NetworkConfig.claiming.maxFeePerGas * BigInt(1.05 * 100)) / 100n;
const expectedPriorityFeePerGas = (MAX_FEE_PER_GAS * BigInt(1.05 * 100)) / 100n;
const expectedMaxFeePerGas = expectedBaseFee + expectedPriorityFeePerGas;
expect(fees).toStrictEqual({

View File

@@ -0,0 +1,3 @@
export { DefaultGasProvider } from "./DefaultGasProvider";
export { GasProvider } from "./GasProvider";
export { LineaGasProvider } from "./LineaGasProvider";

View File

@@ -1,13 +1,10 @@
import { JsonRpcProvider } from "ethers";
import { MessageSent, ServiceVersionMigrated } from "../../../core/types/Events";
import { L2MessageService, L2MessageService__factory } from "../typechain";
import {
IL2MessageServiceLogClient,
MessageSentEventFilters,
} from "../../../core/clients/blockchain/linea/IL2MessageServiceLogClient";
import { TypedContractEvent, TypedDeferredTopicFilter, TypedEventLog } from "../typechain/common";
import { MessageSentEvent, ServiceVersionMigratedEvent } from "../typechain/L2MessageService";
import { isUndefined } from "../../../core/utils/shared";
import { MessageSent, ServiceVersionMigrated } from "../../core/types";
import { L2MessageService, L2MessageService__factory } from "../../contracts/typechain";
import { IL2MessageServiceLogClient, MessageSentEventFilters } from "../../core/clients/linea";
import { TypedContractEvent, TypedDeferredTopicFilter, TypedEventLog } from "../../contracts/typechain/common";
import { MessageSentEvent, ServiceVersionMigratedEvent } from "../../contracts/typechain/L2MessageService";
import { isUndefined } from "../../core/utils";
import { LineaBrowserProvider, LineaProvider } from "../providers";
export class EthersL2MessageServiceLogClient implements IL2MessageServiceLogClient {
private l2MessageService: L2MessageService;
@@ -15,10 +12,10 @@ export class EthersL2MessageServiceLogClient implements IL2MessageServiceLogClie
/**
* Constructs a new instance of the `EthersL2MessageServiceLogClient`.
*
* @param {JsonRpcProvider} provider - The JSON RPC provider for interacting with the Ethereum network.
* @param {LineaProvider | LineaBrowserProvider} provider - The JSON RPC provider for interacting with the Ethereum network.
* @param {string} contractAddress - The address of the L2 Message Service contract.
*/
constructor(provider: JsonRpcProvider, contractAddress: string) {
constructor(provider: LineaProvider | LineaBrowserProvider, contractAddress: string) {
this.l2MessageService = L2MessageService__factory.connect(contractAddress, provider);
}

View File

@@ -1,37 +1,41 @@
import {
Overrides,
ContractTransactionResponse,
JsonRpcProvider,
TransactionReceipt,
TransactionRequest,
TransactionResponse,
Signer,
Block,
ErrorDescription,
} from "ethers";
import { L2MessageService, L2MessageService__factory } from "../typechain";
import { GasEstimationError } from "../../../core/errors/GasFeeErrors";
import { MessageProps } from "../../../core/entities/Message";
import { OnChainMessageStatus } from "../../../core/enums/MessageEnums";
import { IL2MessageServiceClient } from "../../../core/clients/blockchain/linea/IL2MessageServiceClient";
import { ZERO_ADDRESS } from "../../../core/constants";
import { BaseError } from "../../../core/errors/Base";
import { SDKMode } from "../../../sdk/config";
import { formatMessageStatus } from "../../../core/utils/message";
import { IGasProvider, LineaGasFees } from "../../../core/clients/blockchain/IGasProvider";
import { IMessageRetriever } from "../../../core/clients/blockchain/IMessageRetriever";
import { MessageSent } from "../../../core/types/Events";
import { IL2ChainQuerier } from "../../../core/clients/blockchain/linea/IL2ChainQuerier";
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";
import { ZERO_ADDRESS } from "../../core/constants";
import { formatMessageStatus, isString } from "../../core/utils";
import { IGasProvider, LineaGasFees } from "../../core/clients/IGasProvider";
import { IMessageRetriever } from "../../core/clients/IMessageRetriever";
import { LineaBrowserProvider, LineaProvider } from "../providers";
export class L2MessageServiceClient
implements
IL2MessageServiceClient<Overrides, TransactionReceipt, TransactionResponse, ContractTransactionResponse, Signer>
IL2MessageServiceClient<
Overrides,
TransactionReceipt,
TransactionResponse,
ContractTransactionResponse,
Signer,
ErrorDescription
>
{
private readonly contract: L2MessageService;
/**
* Initializes a new instance of the `L2MessageServiceClient`.
*
* @param {IL2ChainQuerier} chainQuerier - The chain querier for interacting with the blockchain.
* @param {ILineaProvider} provider - The provider for interacting with the blockchain.
* @param {string} contractAddress - The address of the L2 message service contract.
* @param {IMessageRetriever<TransactionReceipt>} messageRetriever - An instance of a class implementing the `IMessageRetriever` interface for retrieving messages.
* @param {IGasProvider<TransactionRequest>} gasFeeProvider - An instance of a class implementing the `IGasProvider` interface for providing gas fee estimates.
@@ -39,12 +43,12 @@ export class L2MessageServiceClient
* @param {Signer} [signer] - An optional Ethers.js signer object for signing transactions.
*/
constructor(
private readonly chainQuerier: IL2ChainQuerier<
private readonly provider: ILineaProvider<
TransactionReceipt,
Block,
TransactionRequest,
TransactionResponse,
JsonRpcProvider
LineaProvider | LineaBrowserProvider
>,
private readonly contractAddress: string,
private readonly messageRetriever: IMessageRetriever<TransactionReceipt>,
@@ -103,7 +107,7 @@ export class L2MessageServiceClient
*/
private getContract(contractAddress: string, signer?: Signer): L2MessageService {
if (this.mode === "read-only") {
return L2MessageService__factory.connect(contractAddress, this.chainQuerier.getProvider());
return L2MessageService__factory.connect(contractAddress, this.provider);
}
if (!signer) {
@@ -128,12 +132,12 @@ export class L2MessageServiceClient
/**
* Estimates the gas required for the `claimMessage` transaction.
*
* @param {MessageProps & { feeRecipient?: string }} message - The message information object.
* @param {Message & { feeRecipient?: string }} message - The message information object.
* @param {Overrides} [overrides={}] - Ethers payable overrides. Defaults to `{}` if not specified.
* @returns {Promise<bigint>} The `claimMessage` transaction gas estimation.
*/
public async estimateClaimGasFees(
message: MessageProps & { feeRecipient?: string },
message: Message & { feeRecipient?: string },
overrides: Overrides = {},
): Promise<LineaGasFees> {
if (this.mode === "read-only") {
@@ -160,12 +164,12 @@ export class L2MessageServiceClient
/**
* Claims the message on L2.
*
* @param {MessageProps & { feeRecipient?: string }} message - The message information object.
* @param {Message & { feeRecipient?: string }} message - The message information object.
* @param {Overrides} [overrides] - Ethers payable overrides. Defaults to `{}` if not specified.
* @returns {Promise<ContractTransactionResponse>} The claimMessage transaction info.
*/
public async claim(
message: MessageProps & { feeRecipient?: string },
message: Message & { feeRecipient?: string },
overrides: Overrides = {},
): Promise<ContractTransactionResponse> {
if (this.mode === "read-only") {
@@ -208,7 +212,7 @@ export class L2MessageServiceClient
throw new BaseError("'retryTransactionWithHigherFee' function not callable using readOnly mode.");
}
const transaction = await this.chainQuerier.getTransaction(transactionHash);
const transaction = await this.provider.getTransaction(transactionHash);
if (!transaction) {
throw new BaseError(`Transaction with hash ${transactionHash} not found.`);
@@ -218,18 +222,18 @@ export class L2MessageServiceClient
let maxFeePerGas;
if (!transaction.maxPriorityFeePerGas || !transaction.maxFeePerGas) {
const txFees = await this.chainQuerier.getFees();
const txFees = await this.provider.getFees();
maxPriorityFeePerGas = txFees.maxPriorityFeePerGas;
maxFeePerGas = txFees.maxFeePerGas;
} else {
maxPriorityFeePerGas = (transaction.maxPriorityFeePerGas * (BigInt(priceBumpPercent) + 100n)) / 100n;
maxFeePerGas = (transaction.maxFeePerGas * (BigInt(priceBumpPercent) + 100n)) / 100n;
const maxFeePerGasFromConfig = this.gasFeeProvider.getMaxFeePerGas();
if (maxPriorityFeePerGas > maxFeePerGasFromConfig) {
maxPriorityFeePerGas = maxFeePerGasFromConfig;
const maxFeePerGasCap = this.gasFeeProvider.getMaxFeePerGas();
if (maxPriorityFeePerGas > maxFeePerGasCap) {
maxPriorityFeePerGas = maxFeePerGasCap;
}
if (maxFeePerGas > maxFeePerGasFromConfig) {
maxFeePerGas = maxFeePerGasFromConfig;
if (maxFeePerGas > maxFeePerGasCap) {
maxFeePerGas = maxFeePerGasCap;
}
}
@@ -246,7 +250,7 @@ export class L2MessageServiceClient
};
const signedTransaction = await this.signer!.signTransaction(updatedTransaction);
return await this.chainQuerier.broadcastTransaction(signedTransaction);
return await this.provider.broadcastTransaction(signedTransaction);
}
/**
@@ -262,15 +266,15 @@ export class L2MessageServiceClient
}
/**
* Determines if a transaction failed due to exceeding the rate limit.
*
* @param {string} transactionHash - The hash of the transaction to check.
* @returns {Promise<boolean>} A promise that resolves to `true` if the transaction failed due to a rate limit exceedance, otherwise `false`.
* Parses the error from the transaction.
* @param {string} transactionHash - The transaction hash.
* @returns {Promise<ErrorDescription | string>} The error description or the error bytes.
*/
public async isRateLimitExceededError(transactionHash: string): Promise<boolean> {
public async parseTransactionError(transactionHash: string): Promise<ErrorDescription | string> {
let errorEncodedData = "0x";
try {
const tx = await this.chainQuerier.getTransaction(transactionHash);
const errorEncodedData = await this.chainQuerier.ethCall({
const tx = await this.provider.getTransaction(transactionHash);
errorEncodedData = await this.provider.call({
to: tx?.to,
from: tx?.from,
nonce: tx?.nonce,
@@ -283,16 +287,36 @@ export class L2MessageServiceClient
maxFeePerGas: tx?.maxFeePerGas,
});
const error = this.contract.interface.parseError(errorEncodedData);
return error?.name === "RateLimitExceeded";
if (!error) {
return errorEncodedData;
}
return error;
} catch (e) {
return errorEncodedData;
}
}
/**
* Checks if an error is of type 'RateLimitExceeded'.
* @param {string} transactionHash - The transaction hash.
* @returns {Promise<boolean>} True if the error type is 'RateLimitExceeded', false otherwise.
*/
public async isRateLimitExceededError(transactionHash: string): Promise<boolean> {
const parsedError = await this.parseTransactionError(transactionHash);
if (isString(parsedError)) {
return false;
}
return parsedError.name === "RateLimitExceeded";
}
/**
* Encodes the transaction data for claiming a message.
*
* @param {MessageProps & { feeRecipient?: string }} message - The message properties including an optional fee recipient.
* @param {Message & { feeRecipient?: string }} message - The message properties including an optional fee recipient.
* @param {string} message.messageSender - The address of the message sender.
* @param {string} message.destination - The destination address of the message.
* @param {bigint} message.fee - The fee associated with the message.
@@ -302,7 +326,7 @@ export class L2MessageServiceClient
* @param {string} [message.feeRecipient] - The optional address of the fee recipient. Defaults to ZERO_ADDRESS if not provided.
* @returns {string} The encoded transaction data for claiming the message.
*/
public encodeClaimMessageTransactionData(message: MessageProps & { feeRecipient?: string }): string {
public encodeClaimMessageTransactionData(message: Message & { feeRecipient?: string }): string {
const { messageSender, destination, fee, value, calldata, messageNonce, feeRecipient } = message;
const l2FeeRecipient = feeRecipient ?? ZERO_ADDRESS;

View File

@@ -1,11 +1,11 @@
import { Block, JsonRpcProvider, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { MessageSent } from "../../../core/types/Events";
import { MESSAGE_SENT_EVENT_SIGNATURE } from "../../../core/constants";
import { isNull } from "../../../core/utils/shared";
import { L2MessageService, L2MessageService__factory } from "../typechain";
import { IMessageRetriever } from "../../../core/clients/blockchain/IMessageRetriever";
import { IL2MessageServiceLogClient } from "../../../core/clients/blockchain/linea/IL2MessageServiceLogClient";
import { IL2ChainQuerier } from "../../../core/clients/blockchain/linea/IL2ChainQuerier";
import { Block, TransactionReceipt, TransactionRequest, TransactionResponse } from "ethers";
import { MessageSent } from "../../core/types";
import { MESSAGE_SENT_EVENT_SIGNATURE } from "../../core/constants";
import { isNull } from "../../core/utils";
import { L2MessageService, L2MessageService__factory } from "../../contracts/typechain";
import { IMessageRetriever } from "../../core/clients/IMessageRetriever";
import { ILineaProvider, IL2MessageServiceLogClient } from "../../core/clients/linea";
import { LineaBrowserProvider, LineaProvider } from "../providers";
export class L2MessageServiceMessageRetriever implements IMessageRetriever<TransactionReceipt> {
private readonly contract: L2MessageService;
@@ -13,22 +13,22 @@ export class L2MessageServiceMessageRetriever implements IMessageRetriever<Trans
/**
* Creates an instance of `L2MessageServiceMessageRetriever`.
*
* @param {IL2ChainQuerier} chainQuerier - The chain querier for interacting with the blockchain.
* @param {ILineaProvider} provider - The provider for interacting with the blockchain.
* @param {IL2MessageServiceLogClient} l2MessageServiceLogClient - An instance of a class implementing the `IL2MessageServiceLogClient` interface for fetching events from the blockchain.
* @param {string} contractAddress - The address of the L2 message service contract.
*/
constructor(
private readonly chainQuerier: IL2ChainQuerier<
private readonly provider: ILineaProvider<
TransactionReceipt,
Block,
TransactionRequest,
TransactionResponse,
JsonRpcProvider
LineaProvider | LineaBrowserProvider
>,
private readonly l2MessageServiceLogClient: IL2MessageServiceLogClient,
private readonly contractAddress: string,
) {
this.contract = L2MessageService__factory.connect(contractAddress, this.chainQuerier.getProvider());
this.contract = L2MessageService__factory.connect(contractAddress, this.provider);
}
/**
@@ -53,7 +53,7 @@ export class L2MessageServiceMessageRetriever implements IMessageRetriever<Trans
* @returns {Promise<MessageSent[] | null>} An array of message information or null if not found.
*/
public async getMessagesByTransactionHash(transactionHash: string): Promise<MessageSent[] | null> {
const receipt = await this.chainQuerier.getTransactionReceipt(transactionHash);
const receipt = await this.provider.getTransactionReceipt(transactionHash);
if (!receipt) {
return null;
}
@@ -85,7 +85,7 @@ export class L2MessageServiceMessageRetriever implements IMessageRetriever<Trans
return null;
}
const receipt = await this.chainQuerier.getTransactionReceipt(event.transactionHash);
const receipt = await this.provider.getTransactionReceipt(event.transactionHash);
if (!receipt) {
return null;

View File

@@ -1,25 +1,24 @@
import { describe, afterEach, it, expect, beforeEach } from "@jest/globals";
import { MockProxy, mock, mockClear } from "jest-mock-extended";
import { JsonRpcProvider } from "ethers";
import { EthersL2MessageServiceLogClient } from "../EthersL2MessageServiceLogClient";
import { TEST_MESSAGE_HASH, TEST_CONTRACT_ADDRESS_2 } from "../../../utils/testing/constants/common";
import {
testL2NetworkConfig,
testMessageSentEvent,
testMessageSentEventLog,
testServiceVersionMigratedEventLog,
testServiceVersionMigratedEvent,
TEST_MESSAGE_HASH,
} from "../../../../utils/testing/constants";
import { L2MessageService, L2MessageService__factory } from "../../typechain";
import { mockProperty } from "../../../../utils/testing/helpers";
testServiceVersionMigratedEventLog,
} from "../../../utils/testing/constants/events";
import { L2MessageService, L2MessageService__factory } from "../../../contracts/typechain";
import { mockProperty } from "../../../utils/testing/helpers";
import { LineaProvider } from "../../providers";
describe("TestEthersL2MessgaeServiceLogClient", () => {
let providerMock: MockProxy<JsonRpcProvider>;
let providerMock: MockProxy<LineaProvider>;
let l2MessgaeServiceMock: MockProxy<L2MessageService>;
let l2MessgaeServiceLogClient: EthersL2MessageServiceLogClient;
beforeEach(() => {
providerMock = mock<JsonRpcProvider>();
providerMock = mock<LineaProvider>();
l2MessgaeServiceMock = mock<L2MessageService>();
mockProperty(l2MessgaeServiceMock, "filters", {
...l2MessgaeServiceMock.filters,
@@ -29,10 +28,7 @@ describe("TestEthersL2MessgaeServiceLogClient", () => {
} as any);
jest.spyOn(L2MessageService__factory, "connect").mockReturnValue(l2MessgaeServiceMock);
l2MessgaeServiceLogClient = new EthersL2MessageServiceLogClient(
providerMock,
testL2NetworkConfig.messageServiceContractAddress,
);
l2MessgaeServiceLogClient = new EthersL2MessageServiceLogClient(providerMock, TEST_CONTRACT_ADDRESS_2);
});
afterEach(() => {

View File

@@ -1,37 +1,39 @@
import { describe, afterEach, it, expect, beforeEach } from "@jest/globals";
import { MockProxy, mock, mockClear, mockDeep } from "jest-mock-extended";
import { ContractTransactionResponse, FeeData, JsonRpcProvider, Wallet } from "ethers";
import { ContractTransactionResponse, Wallet } from "ethers";
import {
TEST_MESSAGE_HASH,
TEST_CONTRACT_ADDRESS_1,
TEST_TRANSACTION_HASH,
TEST_ADDRESS_2,
TEST_ADDRESS_1,
} from "../../../../utils/testing/constants";
import { L2MessageService, L2MessageService__factory } from "../../typechain";
DEFAULT_MAX_FEE_PER_GAS,
} from "../../../utils/testing/constants/common";
import { L2MessageService, L2MessageService__factory } from "../../../contracts/typechain";
import {
generateL2MessageServiceClient,
generateMessage,
generateTransactionResponse,
mockProperty,
} from "../../../../utils/testing/helpers";
} from "../../../utils/testing/helpers";
import { L2MessageServiceClient } from "../L2MessageServiceClient";
import { DEFAULT_MAX_FEE_PER_GAS, ZERO_ADDRESS } from "../../../../core/constants";
import { OnChainMessageStatus } from "../../../../core/enums/MessageEnums";
import { GasEstimationError } from "../../../../core/errors/GasFeeErrors";
import { BaseError } from "../../../../core/errors/Base";
import { LineaGasProvider } from "../../gas/LineaGasProvider";
import { ZERO_ADDRESS } from "../../../core/constants";
import { OnChainMessageStatus } from "../../../core/enums/message";
import { GasEstimationError } from "../../../core/errors/GasFeeErrors";
import { BaseError } from "../../../core/errors";
import { LineaProvider } from "../../providers";
import { GasProvider } from "../../gas";
describe("TestL2MessageServiceClient", () => {
let providerMock: MockProxy<JsonRpcProvider>;
let providerMock: MockProxy<LineaProvider>;
let walletMock: MockProxy<Wallet>;
let l2MessageServiceMock: MockProxy<L2MessageService>;
let l2MessageServiceClient: L2MessageServiceClient;
let gasFeeProvider: LineaGasProvider;
let gasFeeProvider: GasProvider;
beforeEach(() => {
providerMock = mock<JsonRpcProvider>();
providerMock = mock<LineaProvider>();
walletMock = mock<Wallet>();
l2MessageServiceMock = mockDeep<L2MessageService>();
@@ -317,7 +319,7 @@ describe("TestL2MessageServiceClient", () => {
TEST_CONTRACT_ADDRESS_1,
"read-write",
walletMock,
{ maxFeePerGas: 500000000n },
{ maxFeePerGasCap: 500000000n },
).l2MessageServiceClient;
await l2MessageServiceClient.retryTransactionWithHigherFee(TEST_TRANSACTION_HASH, 1000);
@@ -346,14 +348,13 @@ describe("TestL2MessageServiceClient", () => {
const getTransactionSpy = jest.spyOn(providerMock, "getTransaction").mockResolvedValue(transactionResponse);
const signTransactionSpy = jest.spyOn(walletMock, "signTransaction").mockResolvedValue("");
const sendTransactionSpy = jest.spyOn(providerMock, "broadcastTransaction");
const getFeeDataSpy = jest.spyOn(providerMock, "getFeeData").mockResolvedValueOnce({
const getFeeDataSpy = jest.spyOn(providerMock, "getFees").mockResolvedValueOnce({
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
gasPrice: 1n,
} as FeeData);
});
const clients = generateL2MessageServiceClient(providerMock, TEST_CONTRACT_ADDRESS_1, "read-write", walletMock, {
maxFeePerGas: 500000000n,
maxFeePerGasCap: 500000000n,
enforceMaxGasFee: true,
});

View File

@@ -1,25 +1,26 @@
import { describe, beforeEach } from "@jest/globals";
import { JsonRpcProvider, Wallet } from "ethers";
import { Wallet } from "ethers";
import { MockProxy, mock } from "jest-mock-extended";
import {
TEST_CONTRACT_ADDRESS_1,
TEST_MESSAGE_HASH,
TEST_TRANSACTION_HASH,
testMessageSentEvent,
} from "../../../../utils/testing/constants";
import { generateL2MessageServiceClient, generateTransactionReceipt } from "../../../../utils/testing/helpers";
} from "../../../utils/testing/constants/common";
import { testMessageSentEvent } from "../../../utils/testing/constants/events";
import { generateL2MessageServiceClient, generateTransactionReceipt } from "../../../utils/testing/helpers";
import { L2MessageServiceMessageRetriever } from "../L2MessageServiceMessageRetriever";
import { EthersL2MessageServiceLogClient } from "../EthersL2MessageServiceLogClient";
import { LineaProvider } from "../../providers";
describe("L2MessageServiceMessageRetriever", () => {
let providerMock: MockProxy<JsonRpcProvider>;
let providerMock: MockProxy<LineaProvider>;
let walletMock: MockProxy<Wallet>;
let messageRetriever: L2MessageServiceMessageRetriever;
let l2MessageServiceLogClient: EthersL2MessageServiceLogClient;
beforeEach(() => {
providerMock = mock<JsonRpcProvider>();
providerMock = mock<LineaProvider>();
walletMock = mock<Wallet>();
const clients = generateL2MessageServiceClient(providerMock, TEST_CONTRACT_ADDRESS_1, "read-write", walletMock);

View File

@@ -0,0 +1,3 @@
export { EthersL2MessageServiceLogClient } from "./EthersL2MessageServiceLogClient";
export { L2MessageServiceClient } from "./L2MessageServiceClient";
export { L2MessageServiceMessageRetriever } from "./L2MessageServiceMessageRetriever";

View File

@@ -0,0 +1,142 @@
import { Block, FeeData } from "ethers";
import { describe, afterEach, it, beforeEach } from "@jest/globals";
import { LineaProvider } from "..";
import { DEFAULT_MAX_FEE_PER_GAS } from "../../../utils/testing/constants/common";
describe("LineaProvider", () => {
let lineaProvider: LineaProvider;
beforeEach(() => {
lineaProvider = new LineaProvider();
});
afterEach(() => {
jest.resetAllMocks();
});
describe("getFees", () => {
it("should throw an error when getFeeData function does not return `maxPriorityFeePerGas` or `maxFeePerGas` values", async () => {
jest.spyOn(lineaProvider, "getFeeData").mockResolvedValue({
maxPriorityFeePerGas: null,
maxFeePerGas: null,
gasPrice: 10n,
} as FeeData);
await expect(lineaProvider.getFees()).rejects.toThrow("Error getting fee data");
});
it("should return `maxPriorityFeePerGas` and `maxFeePerGas` values", async () => {
jest.spyOn(lineaProvider, "getFeeData").mockResolvedValue({
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
gasPrice: 10n,
} as FeeData);
expect(await lineaProvider.getFees()).toStrictEqual({
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
});
});
});
describe("getBlockExtraData", () => {
it("should return null if getBlock returns null", async () => {
const blockMocked = null;
jest.spyOn(lineaProvider, "getBlock").mockResolvedValue(blockMocked);
expect(await lineaProvider.getBlockExtraData("latest")).toBeNull();
});
it("should put requested block number in cache if type of blockNumber param is `number`", async () => {
const blockMocked: Block = {
baseFeePerGas: 7n,
difficulty: 2n,
extraData:
"0x0100989680015eb3c80000ea600000000000000000000000000000000000000024997ceb570c667b9c369d351b384ce97dcfe0dda90696fc3b007b8d7160672548a6716cc33ffe0e4004c555a0c7edd9ddc2545a630f2276a2964dcf856e6ab501",
gasLimit: 61000000n,
gasUsed: 138144n,
hash: "0xc53d0f6b65feabf0422bb897d3f5de2c32d57612f88eb4a366d8076a040a715a",
miner: "0x0000000000000000000000000000000000000000",
nonce: "0x0000000000000000",
number: 1635519,
parentHash: "0xecc7bd0b6d533b13fc65529e5da174062d93f8f426c32929914a375f00a19cc3",
receiptsRoot: "0x0cfd91942f25f029d50078225749dbd8601dd922713e783e4210568ed45b6cd3",
stateRoot: "0xad1346e81f574b511c917cd76c5b70f1d6852b871569bedd9ef02160746e3ffa",
timestamp: 1718030601,
transactions: [],
parentBeaconBlockRoot: null,
blobGasUsed: null,
excessBlobGas: null,
} as unknown as Block;
jest.spyOn(lineaProvider, "getBlock").mockResolvedValue(blockMocked);
await lineaProvider.getBlockExtraData(10);
expect(lineaProvider.isCacheValid(10)).toBeTruthy();
});
it("should return extraData from cache if requested blockNumber param type is `number`", async () => {
const blockMocked: Block = {
baseFeePerGas: 7n,
difficulty: 2n,
extraData:
"0x0100989680015eb3c80000ea600000000000000000000000000000000000000024997ceb570c667b9c369d351b384ce97dcfe0dda90696fc3b007b8d7160672548a6716cc33ffe0e4004c555a0c7edd9ddc2545a630f2276a2964dcf856e6ab501",
gasLimit: 61000000n,
gasUsed: 138144n,
hash: "0xc53d0f6b65feabf0422bb897d3f5de2c32d57612f88eb4a366d8076a040a715a",
miner: "0x0000000000000000000000000000000000000000",
nonce: "0x0000000000000000",
number: 1635519,
parentHash: "0xecc7bd0b6d533b13fc65529e5da174062d93f8f426c32929914a375f00a19cc3",
receiptsRoot: "0x0cfd91942f25f029d50078225749dbd8601dd922713e783e4210568ed45b6cd3",
stateRoot: "0xad1346e81f574b511c917cd76c5b70f1d6852b871569bedd9ef02160746e3ffa",
timestamp: 1718030601,
transactions: [],
parentBeaconBlockRoot: null,
blobGasUsed: null,
excessBlobGas: null,
} as unknown as Block;
const getBlockSpy = jest.spyOn(lineaProvider, "getBlock").mockResolvedValue(blockMocked);
await lineaProvider.getBlockExtraData(10);
expect(lineaProvider.isCacheValid(10)).toBeTruthy();
await lineaProvider.getBlockExtraData(10);
expect(getBlockSpy).toHaveBeenCalledTimes(1);
});
it("should return block extraData", async () => {
const blockMocked: Block = {
baseFeePerGas: 7n,
difficulty: 2n,
extraData:
"0x0100989680015eb3c80000ea600000000000000000000000000000000000000024997ceb570c667b9c369d351b384ce97dcfe0dda90696fc3b007b8d7160672548a6716cc33ffe0e4004c555a0c7edd9ddc2545a630f2276a2964dcf856e6ab501",
gasLimit: 61000000n,
gasUsed: 138144n,
hash: "0xc53d0f6b65feabf0422bb897d3f5de2c32d57612f88eb4a366d8076a040a715a",
miner: "0x0000000000000000000000000000000000000000",
nonce: "0x0000000000000000",
number: 1635519,
parentHash: "0xecc7bd0b6d533b13fc65529e5da174062d93f8f426c32929914a375f00a19cc3",
receiptsRoot: "0x0cfd91942f25f029d50078225749dbd8601dd922713e783e4210568ed45b6cd3",
stateRoot: "0xad1346e81f574b511c917cd76c5b70f1d6852b871569bedd9ef02160746e3ffa",
timestamp: 1718030601,
transactions: [],
parentBeaconBlockRoot: null,
blobGasUsed: null,
excessBlobGas: null,
} as unknown as Block;
jest.spyOn(lineaProvider, "getBlock").mockResolvedValue(blockMocked);
expect(await lineaProvider.getBlockExtraData("latest")).toStrictEqual({
version: 1,
fixedCost: 10000000000,
variableCost: 22983624000,
ethGasPrice: 60000000,
});
});
});
});

View File

@@ -0,0 +1,41 @@
import { FeeData } from "ethers";
import { describe, afterEach, it, beforeEach } from "@jest/globals";
import { Provider } from "..";
import { DEFAULT_MAX_FEE_PER_GAS } from "../../../utils/testing/constants/common";
describe("Provider", () => {
let provider: Provider;
beforeEach(() => {
provider = new Provider();
});
afterEach(() => {
jest.resetAllMocks();
});
describe("getFees", () => {
it("should throw an error when getFeeData function does not return `maxPriorityFeePerGas` or `maxFeePerGas` values", async () => {
jest.spyOn(provider, "getFeeData").mockResolvedValue({
maxPriorityFeePerGas: null,
maxFeePerGas: null,
gasPrice: 10n,
} as FeeData);
await expect(provider.getFees()).rejects.toThrow("Error getting fee data");
});
it("should return `maxPriorityFeePerGas` and `maxFeePerGas` values", async () => {
jest.spyOn(provider, "getFeeData").mockResolvedValue({
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
gasPrice: 10n,
} as FeeData);
expect(await provider.getFees()).toStrictEqual({
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
});
});
});
});

View File

@@ -0,0 +1,2 @@
export { Provider, BrowserProvider } from "./provider";
export { LineaProvider, LineaBrowserProvider } from "./lineaProvider";

View File

@@ -0,0 +1,82 @@
import { BlockTag, dataSlice, ethers, toNumber } from "ethers";
import { BlockExtraData } from "../../core/clients/linea";
import { GasFees } from "../../core/clients/IGasProvider";
import { BaseError } from "../../core/errors";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = new (...args: any[]) => T;
function LineaProviderMixIn<TBase extends Constructor<ethers.Provider>>(Base: TBase) {
return class extends Base {
public blockExtraDataCache: BlockExtraData;
public cacheIsValidForBlockNumber: bigint;
/**
* Retrieves the current gas fees.
*
* @returns {Promise<GasFees>} A promise that resolves to an object containing the current gas fees.
* @throws {BaseError} If there is an error getting the fee data.
*/
public async getFees(): Promise<GasFees> {
const { maxPriorityFeePerGas, maxFeePerGas } = await this.getFeeData();
if (!maxPriorityFeePerGas || !maxFeePerGas) {
throw new BaseError("Error getting fee data");
}
return { maxPriorityFeePerGas, maxFeePerGas };
}
/**
* Fetches and formats extra data from a block.
*
* @param {BlockTag} blockNumber - The block number or tag to fetch extra data for.
* @returns {Promise<BlockExtraData | null>} A promise that resolves to an object containing the formatted block's extra data, or null if the block is not found.
*/
public async getBlockExtraData(blockNumber: BlockTag): Promise<BlockExtraData | null> {
if (typeof blockNumber === "number" && this.isCacheValid(blockNumber)) {
return this.blockExtraDataCache;
}
const block = await this.getBlock(blockNumber);
if (!block) {
return null;
}
const version = dataSlice(block.extraData, 0, 1);
const fixedCost = dataSlice(block.extraData, 1, 5);
const variableCost = dataSlice(block.extraData, 5, 9);
const ethGasPrice = dataSlice(block.extraData, 9, 13);
// Original values are in Kwei; convert them back to wei
const extraData = {
version: toNumber(version),
fixedCost: toNumber(fixedCost) * 1000,
variableCost: toNumber(variableCost) * 1000,
ethGasPrice: toNumber(ethGasPrice) * 1000,
};
if (typeof blockNumber === "number") {
this.cacheIsValidForBlockNumber = BigInt(blockNumber);
this.blockExtraDataCache = extraData;
}
return extraData;
}
/**
* Checks if the cached block extra data is still valid based on the current block number.
*
* @private
* @param {number} currentBlockNumber - The current block number.
* @returns {boolean} True if the cache is valid, false otherwise.
*/
public isCacheValid(currentBlockNumber: number): boolean {
return this.cacheIsValidForBlockNumber >= BigInt(currentBlockNumber);
}
};
}
export class LineaProvider extends LineaProviderMixIn(ethers.JsonRpcProvider) {}
export class LineaBrowserProvider extends LineaProviderMixIn(ethers.BrowserProvider) {}

View File

@@ -0,0 +1,34 @@
import { ethers } from "ethers";
import { BaseError } from "../../core/errors";
import { GasFees } from "../../core/clients/IGasProvider";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = new (...args: any[]) => T;
export function ProviderMixIn<TBase extends Constructor<ethers.Provider>>(Base: TBase) {
return class Provider extends Base {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(...args: any[]) {
super(...args);
}
/**
* Retrieves the current gas fees.
*
* @returns {Promise<GasFees>} A promise that resolves to an object containing the current gas fees.
* @throws {BaseError} If there is an error getting the fee data.
*/
public async getFees(): Promise<GasFees> {
const { maxPriorityFeePerGas, maxFeePerGas } = await this.getFeeData();
if (!maxPriorityFeePerGas || !maxFeePerGas) {
throw new BaseError("Error getting fee data");
}
return { maxPriorityFeePerGas, maxFeePerGas };
}
};
}
export class Provider extends ProviderMixIn(ethers.JsonRpcProvider) {}
export class BrowserProvider extends ProviderMixIn(ethers.BrowserProvider) {}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { Direction } from "../../enums/MessageEnums";
import { Direction } from "../enums";
export type GasFees = {
maxFeePerGas: bigint;
@@ -23,7 +23,7 @@ export type LineaEstimateGasResponse = {
};
type BaseGasProviderConfig = {
maxFeePerGas: bigint;
maxFeePerGasCap: bigint;
enforceMaxGasFee: boolean;
};

View File

@@ -1,4 +1,4 @@
import { MessageSent } from "../../types/Events";
import { MessageSent } from "../types";
export interface IMessageRetriever<TransactionReceipt> {
getMessageByMessageHash(messageHash: string): Promise<MessageSent | null>;

View File

@@ -1,22 +1,20 @@
import { OnChainMessageStatus } from "../../../core/enums/MessageEnums";
import { MessageProps } from "../../entities/Message";
import { MessageSent } from "../../types/Events";
import { OnChainMessageStatus } from "../enums";
import { Message, MessageSent } from "../types";
export interface IMessageServiceContract<
Overrides,
TransactionReceipt,
TransactionResponse,
ContractTransactionResponse,
ErrorDescription,
> {
getMessageStatus(messageHash: string, overrides?: Overrides): Promise<OnChainMessageStatus>;
getMessageByMessageHash(messageHash: string): Promise<MessageSent | null>;
getMessagesByTransactionHash(transactionHash: string): Promise<MessageSent[] | null>;
getTransactionReceiptByMessageHash(messageHash: string): Promise<TransactionReceipt | null>;
claim(
message: (MessageSent | MessageProps) & { feeRecipient?: string },
overrides?: Overrides,
): Promise<ContractTransactionResponse>;
claim(message: Message & { feeRecipient?: string }, overrides?: Overrides): Promise<ContractTransactionResponse>;
retryTransactionWithHigherFee(transactionHash: string, priceBumpPercent?: number): Promise<TransactionResponse>;
isRateLimitExceeded(messageFee: bigint, messageValue: bigint): Promise<boolean>;
isRateLimitExceededError(transactionHash: string): Promise<boolean>;
parseTransactionError(transactionHash: string): Promise<ErrorDescription | string>;
}

View File

@@ -1,16 +1,16 @@
import { GasFees } from "./IGasProvider";
export interface IChainQuerier<TransactionReceipt, Block, TransactionRequest, TransactionResponse, JsonRpcProvider> {
getCurrentNonce(accountAddress?: string): Promise<number>;
getCurrentBlockNumber(): Promise<number>;
export interface IProvider<TransactionReceipt, Block, TransactionRequest, TransactionResponse, Provider> {
getTransactionCount(address: string, blockTag: string | number | bigint): Promise<number>;
getBlockNumber(): Promise<number>;
getTransactionReceipt(txHash: string): Promise<TransactionReceipt | null>;
getBlock(blockNumber: number | bigint | string): Promise<Block | null>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendRequest(methodName: string, params: any[]): Promise<any>;
send(methodName: string, params: Array<any> | Record<string, any>): Promise<any>;
estimateGas(transactionRequest: TransactionRequest): Promise<bigint>;
getProvider(): JsonRpcProvider;
getTransaction(transactionHash: string): Promise<TransactionResponse | null>;
broadcastTransaction(signedTx: string): Promise<TransactionResponse>;
ethCall(transactionRequest: TransactionRequest): Promise<string>;
call(transactionRequest: TransactionRequest): Promise<string>;
getFees(): Promise<GasFees>;
get provider(): Provider;
}

View File

@@ -1,27 +0,0 @@
import { MessageProps } from "../../../entities/Message";
import { OnChainMessageStatus } from "../../../enums/MessageEnums";
import { IMessageServiceContract } from "../../../services/contracts/IMessageServiceContract";
import { MessageSent } from "../../../types/Events";
import { FinalizationMessagingInfo, Proof } from "./IMerkleTreeService";
export interface ILineaRollupClient<Overrides, TransactionReceipt, TransactionResponse, ContractTransactionResponse>
extends IMessageServiceContract<Overrides, TransactionReceipt, TransactionResponse, ContractTransactionResponse> {
getFinalizationMessagingInfo(transactionHash: string): Promise<FinalizationMessagingInfo>;
getL2MessageHashesInBlockRange(fromBlock: number, toBlock: number): Promise<string[]>;
getMessageSiblings(messageHash: string, messageHashes: string[], treeDepth: number): string[];
getMessageProof(messageHash: string): Promise<Proof>;
getMessageStatusUsingMessageHash(messageHash: string, overrides: Overrides): Promise<OnChainMessageStatus>;
getMessageStatusUsingMerkleTree(messageHash: string, overrides: Overrides): Promise<OnChainMessageStatus>;
estimateClaimGas(
message: (MessageSent | MessageProps) & { feeRecipient?: string },
overrides?: Overrides,
): Promise<bigint>;
estimateClaimWithoutProofGas(
message: (MessageSent | MessageProps) & { feeRecipient?: string },
overrides: Overrides,
): Promise<bigint>;
claimWithoutProof(
message: (MessageSent | MessageProps) & { feeRecipient?: string },
overrides: Overrides,
): Promise<ContractTransactionResponse>;
}

View File

@@ -1,13 +0,0 @@
import { IChainQuerier } from "../IChainQuerier";
export type BlockExtraData = {
version: number;
fixedCost: number;
variableCost: number;
ethGasPrice: number;
};
export interface IL2ChainQuerier<TransactionReceipt, Block, TransactionRequest, TransactionResponse, JsonRpcProvider>
extends IChainQuerier<TransactionReceipt, Block, TransactionRequest, TransactionResponse, JsonRpcProvider> {
getBlockExtraData(blockNumber: number | bigint | string): Promise<BlockExtraData | null>;
}

View File

@@ -1,20 +0,0 @@
import { MessageSent } from "sdk/src/core/types/Events";
import { MessageProps } from "../../../../core/entities/Message";
import { IMessageServiceContract } from "../../../services/contracts/IMessageServiceContract";
import { LineaGasFees } from "../IGasProvider";
export interface IL2MessageServiceClient<
Overrides,
TransactionReceipt,
TransactionResponse,
ContractTransactionResponse,
Signer,
> extends IMessageServiceContract<Overrides, TransactionReceipt, TransactionResponse, ContractTransactionResponse> {
encodeClaimMessageTransactionData(message: MessageProps & { feeRecipient?: string }): string;
estimateClaimGasFees(
message: (MessageSent | MessageProps) & { feeRecipient?: string },
overrides?: Overrides,
): Promise<LineaGasFees>;
getSigner(): Signer | undefined;
getContractAddress(): string;
}

View File

@@ -0,0 +1,31 @@
import { Message } from "../../types";
import { OnChainMessageStatus } from "../../enums";
import { IMessageServiceContract } from "../IMessageServiceContract";
import { FinalizationMessagingInfo, Proof } from "./IMerkleTreeService";
export interface ILineaRollupClient<
Overrides,
TransactionReceipt,
TransactionResponse,
ContractTransactionResponse,
ErrorDescription,
> extends IMessageServiceContract<
Overrides,
TransactionReceipt,
TransactionResponse,
ContractTransactionResponse,
ErrorDescription
> {
getFinalizationMessagingInfo(transactionHash: string): Promise<FinalizationMessagingInfo>;
getL2MessageHashesInBlockRange(fromBlock: number, toBlock: number): Promise<string[]>;
getMessageSiblings(messageHash: string, messageHashes: string[], treeDepth: number): string[];
getMessageProof(messageHash: string): Promise<Proof>;
getMessageStatusUsingMessageHash(messageHash: string, overrides: Overrides): Promise<OnChainMessageStatus>;
getMessageStatusUsingMerkleTree(messageHash: string, overrides: Overrides): Promise<OnChainMessageStatus>;
estimateClaimGas(message: Message & { feeRecipient?: string }, overrides?: Overrides): Promise<bigint>;
estimateClaimWithoutProofGas(message: Message & { feeRecipient?: string }, overrides: Overrides): Promise<bigint>;
claimWithoutProof(
message: Message & { feeRecipient?: string },
overrides: Overrides,
): Promise<ContractTransactionResponse>;
}

View File

@@ -1,4 +1,4 @@
import { L2MessagingBlockAnchored, MessageClaimed, MessageSent } from "../../../types/Events";
import { L2MessagingBlockAnchored, MessageClaimed, MessageSent } from "../../types";
export type MessageSentEventFilters = {
from?: string;

View File

@@ -0,0 +1,8 @@
export { ILineaRollupClient } from "./ILineaRollupClient";
export {
MessageSentEventFilters,
L2MessagingBlockAnchoredFilters,
MessageClaimedFilters,
ILineaRollupLogClient,
} from "./ILineaRollupLogClient";
export { BlockRange, FinalizationMessagingInfo, Proof, IMerkleTreeService } from "./IMerkleTreeService";

View File

@@ -0,0 +1,23 @@
import { Message } from "../../types";
import { IMessageServiceContract } from "../IMessageServiceContract";
import { LineaGasFees } from "../IGasProvider";
export interface IL2MessageServiceClient<
Overrides,
TransactionReceipt,
TransactionResponse,
ContractTransactionResponse,
Signer,
ErrorDescription,
> extends IMessageServiceContract<
Overrides,
TransactionReceipt,
TransactionResponse,
ContractTransactionResponse,
ErrorDescription
> {
encodeClaimMessageTransactionData(message: Message & { feeRecipient?: string }): string;
estimateClaimGasFees(message: Message & { feeRecipient?: string }, overrides?: Overrides): Promise<LineaGasFees>;
getSigner(): Signer | undefined;
getContractAddress(): string;
}

View File

@@ -1,4 +1,4 @@
import { MessageSent, ServiceVersionMigrated } from "../../../types/Events";
import { MessageSent, ServiceVersionMigrated } from "../../types";
export type MessageSentEventFilters = {
from?: string;

View File

@@ -0,0 +1,13 @@
import { IProvider } from "../IProvider";
export type BlockExtraData = {
version: number;
fixedCost: number;
variableCost: number;
ethGasPrice: number;
};
export interface ILineaProvider<TransactionReceipt, Block, TransactionRequest, TransactionResponse, Provider>
extends IProvider<TransactionReceipt, Block, TransactionRequest, TransactionResponse, Provider> {
getBlockExtraData(blockNumber: number | bigint | string): Promise<BlockExtraData | null>;
}

View File

@@ -0,0 +1,3 @@
export { IL2MessageServiceClient } from "./IL2MessageServiceClient";
export { MessageSentEventFilters, IL2MessageServiceLogClient } from "./IL2MessageServiceLogClient";
export { BlockExtraData, ILineaProvider } from "./ILineaProvider";

View File

@@ -1,4 +1,2 @@
export const ZERO_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000";
export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
export const MINIMUM_MARGIN = 1.2;

View File

@@ -1,25 +1,6 @@
export const DEFAULT_MESSAGE_SUBMISSION_TIMEOUT = 300000;
export const DEFAULT_LISTENER_INTERVAL = 4000;
export const DEFAULT_DB_CLEANER_ENABLED = false;
export const DEFAULT_DB_CLEANING_INTERVAL = 43200000;
export const DEFAULT_DB_DAYS_BEFORE_NOW_TO_DELETE = 14;
export const DEFAULT_MAX_BLOCKS_TO_FETCH_LOGS = 1000;
export const DEFAULT_MAX_FETCH_MESSAGES_FROM_DB = 1000;
export const DEFAULT_MAX_NONCE_DIFF = 10000;
export const DEFAULT_MAX_FEE_PER_GAS = 100000000000n;
export const DEFAULT_MAX_FEE_PER_GAS_CAP = 100000000000n;
export const DEFAULT_ENFORCE_MAX_GAS_FEE = false;
export const DEFAULT_GAS_ESTIMATION_PERCENTILE = 20;
export const DEFAULT_GAS_LIMIT = 0;
export const DEFAULT_LISTENER_BLOCK_CONFIRMATIONS = 4;
export const DEFAULT_PROFIT_MARGIN = 1.0;
export const DEFAULT_MAX_NUMBER_OF_RETRIES = 100;
export const DEFAULT_RETRY_DELAY_IN_SECONDS = 30;
export const DEFAULT_EOA_ENABLED = false;
export const DEFAULT_CALLDATA_ENABLED = false;
export const DEFAULT_RATE_LIMIT_MARGIN = 0.95;
export const DEFAULT_MAX_CLAIM_GAS_LIMIT = 100_000n;
export const DEFAULT_MAX_TX_RETRIES = 20;
export const DEFAULT_L2_MESSAGE_TREE_DEPTH = 5;
export const DEFAULT_INITIAL_FROM_BLOCK = -1;
export const PROFIT_MARGIN_MULTIPLIER = 100;
export const DEFAULT_RATE_LIMIT_MARGIN = 0.95;
export const DEFAULT_ENABLE_LINEA_ESTIMATE_GAS = false;

View File

@@ -2,3 +2,4 @@ export * from "./common";
export * from "./blockchain";
export * from "./events";
export * from "./message";
export * from "./networks";

View File

@@ -1,4 +1,4 @@
import { NetworkInfo } from "./config";
import { NetworkInfo } from "../types";
export const NETWORKS: NetworkInfo = {
["linea-mainnet"]: {

View File

@@ -1,147 +0,0 @@
import { Direction, MessageStatus } from "../enums/MessageEnums";
export type MessageProps = {
id?: number;
messageSender: string;
destination: string;
fee: bigint;
value: bigint;
messageNonce: bigint;
calldata: string;
messageHash: string;
contractAddress: string;
sentBlockNumber: number;
direction: Direction;
status: MessageStatus;
claimTxCreationDate?: Date;
claimTxGasLimit?: number;
claimTxMaxFeePerGas?: bigint;
claimTxMaxPriorityFeePerGas?: bigint;
claimTxNonce?: number;
claimTxHash?: string;
claimNumberOfRetry: number;
claimLastRetriedAt?: Date;
claimGasEstimationThreshold?: number;
compressedTransactionSize?: number;
createdAt?: Date;
updatedAt?: Date;
};
export type MessageWithProofProps = MessageProps & {
proof: string[];
leafIndex: number;
merkleRoot: string;
};
type EditableMessageProps = Omit<
MessageProps,
| "id"
| "messageSender"
| "destination"
| "fee"
| "value"
| "messageNonce"
| "calldata"
| "messageHash"
| "contractAddress"
| "sentBlockNumber"
| "direction"
| "createdAt"
| "updatedAt"
>;
export class Message {
public id?: number;
public messageSender: string;
public destination: string;
public fee: bigint;
public value: bigint;
public messageNonce: bigint;
public calldata: string;
public messageHash: string;
public contractAddress: string;
public sentBlockNumber: number;
public direction: Direction;
public status: MessageStatus;
public claimTxCreationDate?: Date;
public claimTxGasLimit?: number;
public claimTxMaxFeePerGas?: bigint;
public claimTxMaxPriorityFeePerGas?: bigint;
public claimTxNonce?: number;
public claimTxHash?: string;
public claimNumberOfRetry: number;
public claimLastRetriedAt?: Date;
public claimGasEstimationThreshold?: number;
public compressedTransactionSize?: number;
public createdAt?: Date;
public updatedAt?: Date;
constructor(props: MessageProps) {
this.id = props.id;
this.messageSender = props.messageSender;
this.destination = props.destination;
this.fee = props.fee;
this.value = props.value;
this.messageNonce = props.messageNonce;
this.calldata = props.calldata;
this.messageHash = props.messageHash;
this.contractAddress = props.contractAddress;
this.sentBlockNumber = props.sentBlockNumber;
this.direction = props.direction;
this.status = props.status;
this.claimTxCreationDate = props.claimTxCreationDate;
this.claimTxGasLimit = props.claimTxGasLimit;
this.claimTxMaxFeePerGas = props.claimTxMaxFeePerGas;
this.claimTxMaxPriorityFeePerGas = props.claimTxMaxPriorityFeePerGas;
this.claimTxNonce = props.claimTxNonce;
this.claimTxHash = props.claimTxHash;
this.claimNumberOfRetry = props.claimNumberOfRetry;
this.claimLastRetriedAt = props.claimLastRetriedAt;
this.claimGasEstimationThreshold = props.claimGasEstimationThreshold;
this.compressedTransactionSize = props.compressedTransactionSize;
this.createdAt = props.createdAt;
this.updatedAt = props.updatedAt;
}
public hasZeroFee(): boolean {
return this.fee === 0n;
}
public edit(newMessage: Partial<EditableMessageProps>) {
if (newMessage.status) this.status = newMessage.status;
if (newMessage.claimTxCreationDate) this.claimTxCreationDate = newMessage.claimTxCreationDate;
if (newMessage.claimTxGasLimit) this.claimTxGasLimit = newMessage.claimTxGasLimit;
if (newMessage.claimTxMaxFeePerGas) this.claimTxMaxFeePerGas = newMessage.claimTxMaxFeePerGas;
if (newMessage.claimTxMaxPriorityFeePerGas)
this.claimTxMaxPriorityFeePerGas = newMessage.claimTxMaxPriorityFeePerGas;
if (newMessage.claimTxNonce) this.claimTxNonce = newMessage.claimTxNonce;
if (newMessage.claimTxHash) this.claimTxHash = newMessage.claimTxHash;
if (newMessage.claimNumberOfRetry) this.claimNumberOfRetry = newMessage.claimNumberOfRetry;
if (newMessage.claimLastRetriedAt) this.claimLastRetriedAt = newMessage.claimLastRetriedAt;
if (newMessage.claimGasEstimationThreshold)
this.claimGasEstimationThreshold = newMessage.claimGasEstimationThreshold;
if (newMessage.compressedTransactionSize) this.compressedTransactionSize = newMessage.compressedTransactionSize;
this.updatedAt = new Date();
}
public toString(): string {
return `Message(messageSender=${this.messageSender}, destination=${this.destination}, fee=${
this.fee
}, value=${this.value}, messageNonce=${this.messageNonce}, calldata=${this.calldata}, messageHash=${
this.messageHash
}, contractAddress=${this.contractAddress}, sentBlockNumber=${this.sentBlockNumber}, direction=${
this.direction
}, status=${this.status}, claimTxCreationDate=${this.claimTxCreationDate?.toISOString()}, claimTxGasLimit=${
this.claimTxGasLimit
}, claimTxMaxFeePerGas=${this.claimTxMaxFeePerGas}, claimTxMaxPriorityFeePerGas=${
this.claimTxMaxPriorityFeePerGas
}, claimTxNonce=${this.claimTxNonce}, claimTransactionHash=${this.claimTxHash}, claimNumberOfRetry=${
this.claimNumberOfRetry
}, claimLastRetriedAt=${this.claimLastRetriedAt?.toISOString()}, claimGasEstimationThreshold=${
this.claimGasEstimationThreshold
}, compressedTransactionSize=${
this.compressedTransactionSize
}, createdAt=${this.createdAt?.toISOString()}, updatedAt=${this.updatedAt?.toISOString()})`;
}
}

View File

@@ -1,7 +0,0 @@
import { Message, MessageProps } from "./Message";
export class MessageFactory {
public static createMessage(params: MessageProps): Message {
return new Message(params);
}
}

View File

@@ -1,10 +0,0 @@
export enum DatabaseErrorType {
Read = "read",
Insert = "insert",
Update = "update",
Delete = "delete",
}
export enum DatabaseRepoName {
MessageRepository = "MessageRepository",
}

View File

@@ -1,23 +0,0 @@
export enum Direction {
L1_TO_L2 = "L1_TO_L2",
L2_TO_L1 = "L2_TO_L1",
}
export enum MessageStatus {
SENT = "SENT",
TRANSACTION_SIZE_COMPUTED = "TRANSACTION_SIZE_COMPUTED",
ANCHORED = "ANCHORED",
PENDING = "PENDING",
CLAIMED_SUCCESS = "CLAIMED_SUCCESS",
CLAIMED_REVERTED = "CLAIMED_REVERTED",
NON_EXECUTABLE = "NON_EXECUTABLE",
ZERO_FEE = "ZERO_FEE",
FEE_UNDERPRICED = "FEE_UNDERPRICED",
EXCLUDED = "EXCLUDED",
}
export enum OnChainMessageStatus {
UNKNOWN = "UNKNOWN",
CLAIMABLE = "CLAIMABLE",
CLAIMED = "CLAIMED",
}

View File

@@ -0,0 +1 @@
export { Direction, OnChainMessageStatus } from "./message";

View File

@@ -0,0 +1,10 @@
export enum Direction {
L1_TO_L2 = "L1_TO_L2",
L2_TO_L1 = "L2_TO_L1",
}
export enum OnChainMessageStatus {
UNKNOWN = "UNKNOWN",
CLAIMABLE = "CLAIMABLE",
CLAIMED = "CLAIMED",
}

Some files were not shown because too many files have changed in this diff Show More