[Feat] Postman Sponsor (#860)

* remove isTokenCanonicalUSDC

* move logic to handleTokenClick

* add clause

* remove string for ClaimType

* remove todos

* rename for clarity

* added switch for auto_free

* replaced AUTO_PAID

* some more auto_free

* fix typo

* did free text

* comment fixes

* add test todos

* added config values to postman

* added isForSponsorship

* add isForSponsorship condition to MessageClaimingProcessor

* removed config from messageclaimingprocessor

* did tests for transactionvalidationservice

* did one messageclaimingprocessor test

* added cases to MessageClaimingProcessor test

* rm valueAndFee magic value from e2e test

* new test

* working e2e test

* test refactor

* test refactor

* new e2e test

* add new e2e case

* remove .only

* new test case

* test 2 workers for e2e

* Revert "test 2 workers for e2e"

This reverts commit 8256043df9613015e98c8b5507fcf87f3a8ccc06.

* empty

* adjust for comments 1

* empty

* adjust for comment 2
This commit is contained in:
kyzooghost
2025-04-22 19:28:53 +10:00
committed by GitHub
parent 04db5c7204
commit 9a16c5e152
47 changed files with 703 additions and 240 deletions

View File

@@ -42,6 +42,9 @@ DB_CLEANER_ENABLED=false
DB_CLEANING_INTERVAL=10000
DB_DAYS_BEFORE_NOW_TO_DELETE=1
ENABLE_LINEA_ESTIMATE_GAS=false
L1_L2_ENABLE_POSTMAN_SPONSORING=true
L2_L1_ENABLE_POSTMAN_SPONSORING=false
MAX_POSTMAN_SPONSOR_GAS_LIMIT=250000
# Optional event filter params
L1_EVENT_FILTER_FROM_ADDRESS=<FROM_ADDRESS>

View File

@@ -70,6 +70,7 @@ All messages are stored in a configurable Postgres DB.
- `MAX_NUMBER_OF_RETRIES`: Maximum retry attempts
- `RETRY_DELAY_IN_SECONDS`: Delay between retries
- `MAX_CLAIM_GAS_LIMIT`: Maximum gas limit for claim transactions
- `MAX_POSTMAN_SPONSOR_GAS_LIMIT`: Maximum gas limit for sponsored Postman claim transactions
#### Feature Flags
- `L1_L2_EOA_ENABLED`: Enable L1->L2 EOA messages
@@ -80,6 +81,8 @@ All messages are stored in a configurable Postgres DB.
- `L2_L1_AUTO_CLAIM_ENABLED`: Enable auto-claiming for L2->L1 messages
- `ENABLE_LINEA_ESTIMATE_GAS`: Enable `linea_estimateGas`endpoint usage for L2 chain gas fees estimation
- `DB_CLEANER_ENABLED`: Enable DB cleaning to delete old claimed messages
- `L1_L2_ENABLE_POSTMAN_SPONSORING`: Enable L1->L2 Postman sponsoring for claiming messages
- `L2_L1_ENABLE_POSTMAN_SPONSORING`: Enable L2->L1 Postman sponsoring for claiming messages
#### DB cleaning
- `DB_CLEANING_INTERVAL`: DB cleaning polling interval (ms)

View File

@@ -62,6 +62,10 @@ async function main() {
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",
isPostmanSponsorshipEnabled: process.env.L2_L1_ENABLE_POSTMAN_SPONSORING === "true",
maxPostmanSponsorGasLimit: process.env.MAX_POSTMAN_SPONSOR_GAS_LIMIT
? BigInt(process.env.MAX_POSTMAN_SPONSOR_GAS_LIMIT)
: undefined,
},
},
l2Options: {
@@ -120,6 +124,10 @@ async function main() {
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",
isPostmanSponsorshipEnabled: process.env.L1_L2_ENABLE_POSTMAN_SPONSORING === "true",
maxPostmanSponsorGasLimit: process.env.MAX_POSTMAN_SPONSOR_GAS_LIMIT
? BigInt(process.env.MAX_POSTMAN_SPONSOR_GAS_LIMIT)
: undefined,
},
l2MessageTreeDepth: process.env.L2_MESSAGE_TREE_DEPTH ? parseInt(process.env.L2_MESSAGE_TREE_DEPTH) : undefined,
enableLineaEstimateGas: process.env.ENABLE_LINEA_ESTIMATE_GAS === "true",

View File

@@ -105,7 +105,7 @@ export class PostmanServiceClient {
this.db = DB.create(config.databaseOptions);
const messageRepository = new TypeOrmMessageRepository(this.db);
const lineaMessageDBService = new LineaMessageDBService(l2Provider, messageRepository);
const lineaMessageDBService = new LineaMessageDBService(messageRepository);
const ethereumMessageDBService = new EthereumMessageDBService(l1GasProvider, messageRepository);
// L1 -> L2 flow
@@ -162,6 +162,8 @@ export class PostmanServiceClient {
{
profitMargin: config.l2Config.claiming.profitMargin,
maxClaimGasLimit: BigInt(config.l2Config.claiming.maxClaimGasLimit),
isPostmanSponsorshipEnabled: config.l2Config.claiming.isPostmanSponsorshipEnabled,
maxPostmanSponsorGasLimit: config.l2Config.claiming.maxPostmanSponsorGasLimit,
},
l2Provider,
l2MessageServiceClient,
@@ -287,6 +289,8 @@ export class PostmanServiceClient {
const l1TransactionValidationService = new EthereumTransactionValidationService(lineaRollupClient, l1GasProvider, {
profitMargin: config.l1Config.claiming.profitMargin,
maxClaimGasLimit: BigInt(config.l1Config.claiming.maxClaimGasLimit),
isPostmanSponsorshipEnabled: config.l1Config.claiming.isPostmanSponsorshipEnabled,
maxPostmanSponsorGasLimit: config.l1Config.claiming.maxPostmanSponsorGasLimit,
});
const l1MessageClaimingProcessor = new MessageClaimingProcessor(
@@ -296,13 +300,13 @@ export class PostmanServiceClient {
l1TransactionValidationService,
{
direction: Direction.L2_TO_L1,
originContractAddress: config.l2Config.messageServiceContractAddress,
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),
);

View File

@@ -59,6 +59,8 @@ const postmanServiceClientOptions: PostmanOptions = {
profitMargin: 1.0,
maxNumberOfRetries: 100,
retryDelayInSeconds: 30,
isPostmanSponsorshipEnabled: false,
maxPostmanSponsorGasLimit: 250000n,
},
},
l2Options: {
@@ -81,6 +83,8 @@ const postmanServiceClientOptions: PostmanOptions = {
maxNumberOfRetries: 100,
retryDelayInSeconds: 30,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
isPostmanSponsorshipEnabled: false,
maxPostmanSponsorGasLimit: 250000n,
},
},
l1L2AutoClaimEnabled: true,

View File

@@ -14,6 +14,7 @@ import {
DEFAULT_DB_CLEANER_ENABLED,
DEFAULT_DB_CLEANING_INTERVAL,
DEFAULT_DB_DAYS_BEFORE_NOW_TO_DELETE,
DEFAULT_ENABLE_POSTMAN_SPONSORING,
DEFAULT_ENFORCE_MAX_GAS_FEE,
DEFAULT_EOA_ENABLED,
DEFAULT_GAS_ESTIMATION_PERCENTILE,
@@ -27,6 +28,7 @@ import {
DEFAULT_MAX_FETCH_MESSAGES_FROM_DB,
DEFAULT_MAX_NONCE_DIFF,
DEFAULT_MAX_NUMBER_OF_RETRIES,
DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
DEFAULT_MAX_TX_RETRIES,
DEFAULT_MESSAGE_SUBMISSION_TIMEOUT,
DEFAULT_PROFIT_MARGIN,
@@ -82,6 +84,8 @@ describe("Config utils", () => {
profitMargin: DEFAULT_PROFIT_MARGIN,
retryDelayInSeconds: DEFAULT_RETRY_DELAY_IN_SECONDS,
signerPrivateKey: TEST_L1_SIGNER_PRIVATE_KEY,
isPostmanSponsorshipEnabled: DEFAULT_ENABLE_POSTMAN_SPONSORING,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
},
isCalldataEnabled: DEFAULT_CALLDATA_ENABLED,
isEOAEnabled: DEFAULT_EOA_ENABLED,
@@ -110,6 +114,8 @@ describe("Config utils", () => {
profitMargin: DEFAULT_PROFIT_MARGIN,
retryDelayInSeconds: DEFAULT_RETRY_DELAY_IN_SECONDS,
signerPrivateKey: TEST_L2_SIGNER_PRIVATE_KEY,
isPostmanSponsorshipEnabled: DEFAULT_ENABLE_POSTMAN_SPONSORING,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
},
enableLineaEstimateGas: false,
isCalldataEnabled: DEFAULT_CALLDATA_ENABLED,
@@ -187,6 +193,8 @@ describe("Config utils", () => {
profitMargin: DEFAULT_PROFIT_MARGIN,
retryDelayInSeconds: DEFAULT_RETRY_DELAY_IN_SECONDS,
signerPrivateKey: TEST_L1_SIGNER_PRIVATE_KEY,
isPostmanSponsorshipEnabled: DEFAULT_ENABLE_POSTMAN_SPONSORING,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
},
isCalldataEnabled: DEFAULT_CALLDATA_ENABLED,
isEOAEnabled: DEFAULT_EOA_ENABLED,
@@ -215,6 +223,8 @@ describe("Config utils", () => {
profitMargin: DEFAULT_PROFIT_MARGIN,
retryDelayInSeconds: DEFAULT_RETRY_DELAY_IN_SECONDS,
signerPrivateKey: TEST_L2_SIGNER_PRIVATE_KEY,
isPostmanSponsorshipEnabled: DEFAULT_ENABLE_POSTMAN_SPONSORING,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
},
enableLineaEstimateGas: true,
isCalldataEnabled: DEFAULT_CALLDATA_ENABLED,

View File

@@ -84,6 +84,8 @@ export type ClaimingOptions = {
retryDelayInSeconds?: number;
maxClaimGasLimit?: bigint;
maxTxRetries?: number;
isPostmanSponsorshipEnabled?: boolean;
maxPostmanSponsorGasLimit?: bigint;
};
export type ClaimingConfig = Omit<Required<ClaimingOptions>, "feeRecipientAddress"> & {

View File

@@ -2,6 +2,7 @@ import { Interface, isAddress } from "ethers";
import { compileExpression, useDotAccessOperator } from "filtrex";
import {
DEFAULT_CALLDATA_ENABLED,
DEFAULT_ENABLE_POSTMAN_SPONSORING,
DEFAULT_EOA_ENABLED,
DEFAULT_GAS_ESTIMATION_PERCENTILE,
DEFAULT_INITIAL_FROM_BLOCK,
@@ -14,6 +15,7 @@ import {
DEFAULT_MAX_FETCH_MESSAGES_FROM_DB,
DEFAULT_MAX_NONCE_DIFF,
DEFAULT_MAX_NUMBER_OF_RETRIES,
DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
DEFAULT_MAX_TX_RETRIES,
DEFAULT_MESSAGE_SUBMISSION_TIMEOUT,
DEFAULT_PROFIT_MARGIN,
@@ -73,6 +75,10 @@ export function getConfig(postmanOptions: PostmanOptions): PostmanConfig {
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,
isPostmanSponsorshipEnabled:
l1Options.claiming.isPostmanSponsorshipEnabled ?? DEFAULT_ENABLE_POSTMAN_SPONSORING,
maxPostmanSponsorGasLimit:
l1Options.claiming.maxPostmanSponsorGasLimit ?? DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
},
},
l2Config: {
@@ -103,6 +109,10 @@ export function getConfig(postmanOptions: PostmanOptions): PostmanConfig {
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,
isPostmanSponsorshipEnabled:
l1Options.claiming.isPostmanSponsorshipEnabled ?? DEFAULT_ENABLE_POSTMAN_SPONSORING,
maxPostmanSponsorGasLimit:
l1Options.claiming.maxPostmanSponsorGasLimit ?? DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
},
},
l1L2AutoClaimEnabled,

View File

@@ -153,11 +153,6 @@ export class TypeOrmMessageRepository<TransactionResponse extends ContractTransa
messageStatuses: MessageStatus[],
maxRetry: number,
retryDelay: number,
feeEstimationOptions: {
minimumMargin: number;
extraDataVariableCost: number;
extraDataFixedCost: number;
},
): Promise<Message | null> {
try {
const message = await this.createQueryBuilder("message")
@@ -174,15 +169,7 @@ export class TypeOrmMessageRepository<TransactionResponse extends ContractTransa
});
}),
)
.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")
.orderBy("CAST(message.status as CHAR)", "DESC")
.addOrderBy("CAST(message.fee AS numeric)", "DESC")
.addOrderBy("message.sentBlockNumber", "ASC")
.getOne();

View File

@@ -17,9 +17,11 @@ 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_CLAIM_GAS_LIMIT = 500_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 DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT = 250_000n; // Should be < DEFAULT_MAX_CLAIM_GAS_LIMIT
export const DEFAULT_ENABLE_POSTMAN_SPONSORING = false;
export const PROFIT_MARGIN_MULTIPLIER = 100;

View File

@@ -22,11 +22,6 @@ export interface IMessageRepository<ContractTransactionResponse> {
messageStatuses: MessageStatus[],
maxRetry: number,
retryDelay: number,
feeEstimationOptions: {
minimumMargin: number;
extraDataVariableCost: number;
extraDataFixedCost: number;
},
): Promise<Message | null>;
getLatestMessageSent(direction: Direction, contractAddress: string): Promise<Message | null>;
getNFirstMessagesByStatus(

View File

@@ -8,6 +8,7 @@ export interface ITransactionValidationService {
hasZeroFee: boolean;
isUnderPriced: boolean;
isRateLimitExceeded: boolean;
isForSponsorship: boolean;
estimatedGasLimit: bigint | null;
threshold: number;
maxPriorityFeePerGas: bigint;
@@ -18,4 +19,6 @@ export interface ITransactionValidationService {
export type TransactionValidationServiceConfig = {
profitMargin: number;
maxClaimGasLimit: bigint;
isPostmanSponsorshipEnabled: boolean;
maxPostmanSponsorGasLimit: bigint;
};

View File

@@ -44,6 +44,7 @@ export class EthereumTransactionValidationService implements ITransactionValidat
* hasZeroFee: boolean;
* isUnderPriced: boolean;
* isRateLimitExceeded: boolean;
* isForSponsorship: boolean;
* estimatedGasLimit: bigint | null;
* threshold: number;
* maxPriorityFeePerGas: bigint;
@@ -57,6 +58,7 @@ export class EthereumTransactionValidationService implements ITransactionValidat
hasZeroFee: boolean;
isUnderPriced: boolean;
isRateLimitExceeded: boolean;
isForSponsorship: boolean;
estimatedGasLimit: bigint | null;
threshold: number;
maxPriorityFeePerGas: bigint;
@@ -75,11 +77,17 @@ export class EthereumTransactionValidationService implements ITransactionValidat
const isUnderPriced = this.isUnderPriced(gasLimit, message.fee, maxFeePerGas);
const hasZeroFee = this.hasZeroFee(message);
const isRateLimitExceeded = await this.isRateLimitExceeded(message.fee, message.value);
const isForSponsorship = this.isForSponsorship(
gasLimit,
this.config.isPostmanSponsorshipEnabled,
this.config.maxPostmanSponsorGasLimit,
);
return {
hasZeroFee,
isUnderPriced,
isRateLimitExceeded,
isForSponsorship,
estimatedGasLimit,
threshold,
maxPriorityFeePerGas,
@@ -143,4 +151,21 @@ export class EthereumTransactionValidationService implements ITransactionValidat
private async isRateLimitExceeded(messageFee: bigint, messageValue: bigint): Promise<boolean> {
return this.lineaRollupClient.isRateLimitExceeded(messageFee, messageValue);
}
/**
* Determines if the claim transaction is for sponsorship
*
* @param {bigint} gasLimit - The gas limit for the transaction.
* @param {boolean} isPostmanSponsorshipEnabled - `true` if Postman sponsorship is enabled, `false` otherwise
* @param {bigint} maxPostmanSponsorGasLimit - Maximum gas limit for sponsored Postman claim transactions
* @returns {boolean} `true` if the message is for sponsorsing, `false` otherwise.
*/
private isForSponsorship(
gasLimit: bigint,
isPostmanSponsorshipEnabled: boolean,
maxPostmanSponsorGasLimit: bigint,
): boolean {
if (!isPostmanSponsorshipEnabled) return false;
return gasLimit < maxPostmanSponsorGasLimit;
}
}

View File

@@ -55,6 +55,7 @@ export class LineaTransactionValidationService implements ITransactionValidation
* hasZeroFee: boolean;
* isUnderPriced: boolean;
* isRateLimitExceeded: boolean;
* isForSponsorship: boolean;
* estimatedGasLimit: bigint | null;
* threshold: number;
* maxPriorityFeePerGas: bigint;
@@ -68,6 +69,7 @@ export class LineaTransactionValidationService implements ITransactionValidation
hasZeroFee: boolean;
isUnderPriced: boolean;
isRateLimitExceeded: boolean;
isForSponsorship: boolean;
estimatedGasLimit: bigint | null;
threshold: number;
maxPriorityFeePerGas: bigint;
@@ -83,11 +85,17 @@ export class LineaTransactionValidationService implements ITransactionValidation
const isUnderPriced = await this.isUnderPriced(gasLimit, message.fee, message.compressedTransactionSize!);
const hasZeroFee = this.hasZeroFee(message);
const isRateLimitExceeded = await this.isRateLimitExceeded(message.fee, message.value);
const isForSponsorship = this.isForSponsorship(
gasLimit,
this.config.isPostmanSponsorshipEnabled,
this.config.maxPostmanSponsorGasLimit,
);
return {
hasZeroFee,
isUnderPriced,
isRateLimitExceeded,
isForSponsorship,
estimatedGasLimit,
threshold,
maxPriorityFeePerGas,
@@ -147,6 +155,23 @@ export class LineaTransactionValidationService implements ITransactionValidation
return this.l2MessageServiceClient.isRateLimitExceeded(messageFee, messageValue);
}
/**
* Determines if the claim transaction is for sponsorship
*
* @param {bigint} gasLimit - The gas limit for the transaction.
* @param {boolean} isPostmanSponsorshipEnabled - `true` if Postman sponsorship is enabled, `false` otherwise
* @param {bigint} maxPostmanSponsorGasLimit - Maximum gas limit for sponsored Postman claim transactions
* @returns {boolean} `true` if the message is for sponsorsing, `false` otherwise.
*/
private isForSponsorship(
gasLimit: bigint,
isPostmanSponsorshipEnabled: boolean,
maxPostmanSponsorGasLimit: bigint,
): boolean {
if (!isPostmanSponsorshipEnabled) return false;
return gasLimit < maxPostmanSponsorGasLimit;
}
/**
* Calculates the gas estimation threshold based on the message fee and gas limit.
*

View File

@@ -1,38 +1,18 @@
import {
Block,
ContractTransactionResponse,
JsonRpcProvider,
TransactionReceipt,
TransactionRequest,
TransactionResponse,
} from "ethers";
import { ContractTransactionResponse } from "ethers";
import { Direction } from "@consensys/linea-sdk";
import { Message } from "../../core/entities/Message";
import { MessageStatus } from "../../core/enums";
import { IMessageRepository } from "../../core/persistence/IMessageRepository";
import { ILineaProvider } from "../../core/clients/blockchain/linea/ILineaProvider";
import { BaseError } from "../../core/errors";
import { IMessageDBService } from "../../core/persistence/IMessageDBService";
import { MessageDBService } from "./MessageDBService";
import { MINIMUM_MARGIN } from "../../core/constants";
export class LineaMessageDBService extends MessageDBService implements IMessageDBService<ContractTransactionResponse> {
/**
* Creates an instance of `LineaMessageDBService`.
*
* @param {ILineaProvider} provider - The provider for interacting with the blockchain.
* @param {IMessageRepository} messageRepository - The message repository for interacting with the message database.
*/
constructor(
private readonly provider: ILineaProvider<
TransactionReceipt,
Block,
TransactionRequest,
TransactionResponse,
JsonRpcProvider
>,
messageRepository: IMessageRepository<ContractTransactionResponse>,
) {
constructor(messageRepository: IMessageRepository<ContractTransactionResponse>) {
super(messageRepository);
}
@@ -67,40 +47,12 @@ export class LineaMessageDBService extends MessageDBService implements IMessageD
maxRetry: number,
retryDelay: number,
): Promise<Message | null> {
const feeEstimationOptions = await this.getClaimDBQueryFeeOptions();
return this.messageRepository.getFirstMessageToClaimOnL2(
Direction.L1_TO_L2,
contractAddress,
[MessageStatus.TRANSACTION_SIZE_COMPUTED, MessageStatus.FEE_UNDERPRICED],
maxRetry,
retryDelay,
feeEstimationOptions,
);
}
/**
* Retrieves fee estimation options for querying the database.
*
* @private
* @returns {Promise<{ minimumMargin: number; extraDataVariableCost: number; extraDataFixedCost: number }>} A promise that resolves to an object containing fee estimation options.
* @throws {BaseError} If no extra data is available.
*/
private async getClaimDBQueryFeeOptions(): Promise<{
minimumMargin: number;
extraDataVariableCost: number;
extraDataFixedCost: number;
}> {
const minimumMargin = MINIMUM_MARGIN;
const blockNumber = await this.provider.getBlockNumber();
const extraData = await this.provider.getBlockExtraData(blockNumber);
if (!extraData) {
throw new BaseError("no extra data.");
}
return {
minimumMargin,
extraDataVariableCost: extraData.variableCost,
extraDataFixedCost: extraData.fixedCost,
};
}
}

View File

@@ -87,19 +87,30 @@ export class MessageClaimingProcessor implements IMessageClaimingProcessor {
return;
}
const { hasZeroFee, isUnderPriced, isRateLimitExceeded, estimatedGasLimit, threshold, ...claimTxFees } =
await this.transactionValidationService.evaluateTransaction(
nextMessageToClaim,
this.config.feeRecipientAddress,
);
const {
hasZeroFee,
isUnderPriced,
isRateLimitExceeded,
isForSponsorship,
estimatedGasLimit,
threshold,
...claimTxFees
} = await this.transactionValidationService.evaluateTransaction(
nextMessageToClaim,
this.config.feeRecipientAddress,
);
if (await this.handleZeroFee(hasZeroFee, nextMessageToClaim)) return;
// If isForSponsorship = true, then we ignore hasZeroFee and isUnderPriced
if (!isForSponsorship && (await this.handleZeroFee(hasZeroFee, nextMessageToClaim))) return;
if (await this.handleNonExecutable(nextMessageToClaim, estimatedGasLimit)) return;
nextMessageToClaim.edit({ claimGasEstimationThreshold: threshold });
await this.databaseService.updateMessage(nextMessageToClaim);
if (await this.handleUnderpriced(nextMessageToClaim, isUnderPriced, estimatedGasLimit, claimTxFees.maxFeePerGas))
if (
!isForSponsorship &&
(await this.handleUnderpriced(nextMessageToClaim, isUnderPriced, estimatedGasLimit, claimTxFees.maxFeePerGas))
)
return;
if (this.handleRateLimitExceeded(nextMessageToClaim, isRateLimitExceeded)) return;

View File

@@ -17,9 +17,11 @@ import {
testMessage,
} from "../../../utils/testing/constants";
import {
DEFAULT_ENABLE_POSTMAN_SPONSORING,
DEFAULT_GAS_ESTIMATION_PERCENTILE,
DEFAULT_MAX_CLAIM_GAS_LIMIT,
DEFAULT_MAX_FEE_PER_GAS_CAP,
DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
DEFAULT_PROFIT_MARGIN,
} from "../../../core/constants";
import { EthereumTransactionValidationService } from "../../EthereumTransactionValidationService";
@@ -57,6 +59,8 @@ describe("EthereumTransactionValidationService", () => {
lineaTransactionValidationService = new EthereumTransactionValidationService(lineaRollupClient, gasProvider, {
profitMargin: DEFAULT_PROFIT_MARGIN,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
isPostmanSponsorshipEnabled: DEFAULT_ENABLE_POSTMAN_SPONSORING,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
});
jest.spyOn(gasProvider, "getGasFees").mockResolvedValueOnce({
@@ -82,6 +86,7 @@ describe("EthereumTransactionValidationService", () => {
estimatedGasLimit: estimatedGasLimit,
hasZeroFee: true,
isRateLimitExceeded: false,
isForSponsorship: false,
isUnderPriced: true,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
@@ -102,6 +107,7 @@ describe("EthereumTransactionValidationService", () => {
hasZeroFee: false,
isRateLimitExceeded: false,
isUnderPriced: true,
isForSponsorship: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
threshold: 0,
@@ -120,6 +126,7 @@ describe("EthereumTransactionValidationService", () => {
hasZeroFee: false,
isRateLimitExceeded: false,
isUnderPriced: true,
isForSponsorship: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
threshold: 0,
@@ -138,6 +145,7 @@ describe("EthereumTransactionValidationService", () => {
hasZeroFee: false,
isRateLimitExceeded: true,
isUnderPriced: true,
isForSponsorship: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
threshold: 0,
@@ -157,10 +165,55 @@ describe("EthereumTransactionValidationService", () => {
hasZeroFee: false,
isRateLimitExceeded: false,
isUnderPriced: false,
isForSponsorship: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
threshold: 2000000000000000,
});
});
it("When isPostmanSponsorshipEnabled is false, should return transaction evaluation criteria with isForSponsorship = false", async () => {
const estimatedGasLimit = 50_000n;
jest.spyOn(lineaRollupClient, "estimateClaimGas").mockResolvedValueOnce(estimatedGasLimit);
jest.spyOn(lineaRollupClient, "isRateLimitExceeded").mockResolvedValueOnce(false);
testMessage.fee = 0n;
const criteria = await lineaTransactionValidationService.evaluateTransaction(testMessage);
expect(criteria.isForSponsorship).toBe(false);
});
describe("isPostmanSponsorshipEnabled is true", () => {
beforeEach(() => {
lineaTransactionValidationService = new EthereumTransactionValidationService(lineaRollupClient, gasProvider, {
profitMargin: DEFAULT_PROFIT_MARGIN,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
isPostmanSponsorshipEnabled: true,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
});
});
it("When gas limit < sponsor threshold, should return transaction evaluation criteria with isForSponsorship = true", async () => {
const estimatedGasLimit = DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT - 1n;
jest.spyOn(lineaRollupClient, "estimateClaimGas").mockResolvedValueOnce(estimatedGasLimit);
jest.spyOn(lineaRollupClient, "isRateLimitExceeded").mockResolvedValueOnce(false);
testMessage.fee = 0n;
const criteria = await lineaTransactionValidationService.evaluateTransaction(testMessage);
expect(criteria.isForSponsorship).toBe(true);
});
it("When gas limit > sponsor threshold, should return transaction evaluation criteria with isForSponsorship = false", async () => {
const estimatedGasLimit = DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT + 1n;
jest.spyOn(lineaRollupClient, "estimateClaimGas").mockResolvedValueOnce(estimatedGasLimit);
jest.spyOn(lineaRollupClient, "isRateLimitExceeded").mockResolvedValueOnce(false);
testMessage.fee = 0n;
const criteria = await lineaTransactionValidationService.evaluateTransaction(testMessage);
expect(criteria.isForSponsorship).toBe(false);
});
});
});
});

View File

@@ -17,8 +17,10 @@ import {
testMessage,
} from "../../../utils/testing/constants";
import {
DEFAULT_ENABLE_POSTMAN_SPONSORING,
DEFAULT_MAX_CLAIM_GAS_LIMIT,
DEFAULT_MAX_FEE_PER_GAS_CAP,
DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
DEFAULT_PROFIT_MARGIN,
} from "../../../core/constants";
import { IL2MessageServiceClient } from "../../../core/clients/blockchain/linea/IL2MessageServiceClient";
@@ -37,6 +39,24 @@ describe("LineaTransactionValidationService", () => {
>;
let provider: MockProxy<LineaProvider>;
const setup = (estimatedGasLimit: bigint, isNullExtraData = false) => {
jest.spyOn(gasProvider, "getGasFees").mockResolvedValueOnce({
gasLimit: estimatedGasLimit,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
});
jest.spyOn(provider, "getBlockExtraData").mockResolvedValueOnce(
isNullExtraData
? null
: {
version: 1,
variableCost: 1_000_000,
fixedCost: 1_000_000,
ethGasPrice: 1_000_000,
},
);
};
beforeEach(() => {
provider = mock<LineaProvider>();
const clients = testingHelpers.generateL2MessageServiceClient(
@@ -57,10 +77,14 @@ describe("LineaTransactionValidationService", () => {
{
profitMargin: DEFAULT_PROFIT_MARGIN,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
isPostmanSponsorshipEnabled: DEFAULT_ENABLE_POSTMAN_SPONSORING,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
},
provider,
l2ContractClient,
);
jest.spyOn(l2ContractClient, "getSigner").mockReturnValueOnce(new Wallet(TEST_L2_SIGNER_PRIVATE_KEY));
});
afterEach(() => {
@@ -69,34 +93,17 @@ describe("LineaTransactionValidationService", () => {
describe("evaluateTransaction", () => {
it("Should throw an error when there is no extraData in the L2 block", async () => {
jest.spyOn(l2ContractClient, "getSigner").mockReturnValueOnce(new Wallet(TEST_L2_SIGNER_PRIVATE_KEY));
const estimatedGasLimit = 50_000n;
jest.spyOn(gasProvider, "getGasFees").mockResolvedValueOnce({
gasLimit: estimatedGasLimit,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
});
jest.spyOn(provider, "getBlockExtraData").mockResolvedValueOnce(null);
setup(estimatedGasLimit, true);
await expect(lineaTransactionValidationService.evaluateTransaction(testMessage)).rejects.toThrow("No extra data");
});
it("Should return transaction evaluation criteria with hasZeroFee = true", async () => {
jest.spyOn(l2ContractClient, "getSigner").mockReturnValueOnce(new Wallet(TEST_L2_SIGNER_PRIVATE_KEY));
const estimatedGasLimit = 50_000n;
jest.spyOn(gasProvider, "getGasFees").mockResolvedValueOnce({
gasLimit: estimatedGasLimit,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
});
jest.spyOn(provider, "getBlockExtraData").mockResolvedValueOnce({
version: 1,
variableCost: 1_000_000,
fixedCost: 1_000_000,
ethGasPrice: 1_000_000,
});
setup(estimatedGasLimit);
testMessage.fee = 0n;
const criteria = await lineaTransactionValidationService.evaluateTransaction(testMessage);
expect(criteria).toStrictEqual({
@@ -104,6 +111,7 @@ describe("LineaTransactionValidationService", () => {
hasZeroFee: true,
isRateLimitExceeded: false,
isUnderPriced: true,
isForSponsorship: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
threshold: 0,
@@ -111,21 +119,10 @@ describe("LineaTransactionValidationService", () => {
});
it("Should return transaction evaluation criteria with isUnderPriced = true", async () => {
jest.spyOn(l2ContractClient, "getSigner").mockReturnValueOnce(new Wallet(TEST_L2_SIGNER_PRIVATE_KEY));
const estimatedGasLimit = 50_000n;
jest.spyOn(gasProvider, "getGasFees").mockResolvedValueOnce({
gasLimit: estimatedGasLimit,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
});
jest.spyOn(provider, "getBlockExtraData").mockResolvedValueOnce({
version: 1,
variableCost: 1_000_000,
fixedCost: 1_000_000,
ethGasPrice: 1_000_000,
});
setup(estimatedGasLimit);
testMessage.fee = 1n;
const criteria = await lineaTransactionValidationService.evaluateTransaction(testMessage);
expect(criteria).toStrictEqual({
@@ -133,6 +130,7 @@ describe("LineaTransactionValidationService", () => {
hasZeroFee: false,
isRateLimitExceeded: false,
isUnderPriced: true,
isForSponsorship: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
threshold: 0,
@@ -140,18 +138,8 @@ describe("LineaTransactionValidationService", () => {
});
it("Should return transaction evaluation criteria with estimatedGasLimit = null", async () => {
jest.spyOn(l2ContractClient, "getSigner").mockReturnValueOnce(new Wallet(TEST_L2_SIGNER_PRIVATE_KEY));
jest.spyOn(gasProvider, "getGasFees").mockResolvedValueOnce({
gasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT + 1n,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
});
jest.spyOn(provider, "getBlockExtraData").mockResolvedValueOnce({
version: 1,
variableCost: 1_000_000,
fixedCost: 1_000_000,
ethGasPrice: 1_000_000,
});
const estimatedGasLimit = DEFAULT_MAX_CLAIM_GAS_LIMIT + 1n;
setup(estimatedGasLimit);
const criteria = await lineaTransactionValidationService.evaluateTransaction(testMessage);
@@ -160,6 +148,7 @@ describe("LineaTransactionValidationService", () => {
hasZeroFee: false,
isRateLimitExceeded: false,
isUnderPriced: true,
isForSponsorship: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
threshold: 0,
@@ -167,21 +156,10 @@ describe("LineaTransactionValidationService", () => {
});
it("Should return transaction evaluation criteria for a valid message", async () => {
jest.spyOn(l2ContractClient, "getSigner").mockReturnValueOnce(new Wallet(TEST_L2_SIGNER_PRIVATE_KEY));
const estimatedGasLimit = 50_000n;
jest.spyOn(gasProvider, "getGasFees").mockResolvedValueOnce({
gasLimit: estimatedGasLimit,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
});
jest.spyOn(provider, "getBlockExtraData").mockResolvedValueOnce({
version: 1,
variableCost: 1_000_000,
fixedCost: 1_000_000,
ethGasPrice: 1_000_000,
});
setup(estimatedGasLimit);
testMessage.fee = 100000000000000000000n;
const criteria = await lineaTransactionValidationService.evaluateTransaction(testMessage);
expect(criteria).toStrictEqual({
@@ -189,10 +167,56 @@ describe("LineaTransactionValidationService", () => {
hasZeroFee: false,
isRateLimitExceeded: false,
isUnderPriced: false,
isForSponsorship: false,
maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
threshold: 2000000000000000,
});
});
it("When isPostmanSponsorshipEnabled is false, should return transaction evaluation criteria with isForSponsorship = false", async () => {
const estimatedGasLimit = 50_000n;
setup(estimatedGasLimit);
testMessage.fee = 0n;
const criteria = await lineaTransactionValidationService.evaluateTransaction(testMessage);
expect(criteria.isForSponsorship).toBe(false);
});
describe("isPostmanSponsorshipEnabled is true", () => {
beforeEach(() => {
lineaTransactionValidationService = new LineaTransactionValidationService(
{
profitMargin: DEFAULT_PROFIT_MARGIN,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
isPostmanSponsorshipEnabled: true,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
},
provider,
l2ContractClient,
);
});
it("When gas limit < sponsor threshold, should return transaction evaluation criteria with isForSponsorship = true", async () => {
const estimatedGasLimit = DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT - 1n;
setup(estimatedGasLimit);
testMessage.fee = 0n;
const criteria = await lineaTransactionValidationService.evaluateTransaction(testMessage);
expect(criteria.isForSponsorship).toBe(true);
});
it("When gas limit > sponsor threshold, should return transaction evaluation criteria with isForSponsorship = false", async () => {
const estimatedGasLimit = DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT + 1n;
setup(estimatedGasLimit);
testMessage.fee = 0n;
const criteria = await lineaTransactionValidationService.evaluateTransaction(testMessage);
expect(criteria.isForSponsorship).toBe(false);
});
});
});
});

View File

@@ -30,10 +30,12 @@ import { Message } from "../../../core/entities/Message";
import { ErrorParser } from "../../../utils/ErrorParser";
import { EthereumMessageDBService } from "../../persistence/EthereumMessageDBService";
import {
DEFAULT_ENABLE_POSTMAN_SPONSORING,
DEFAULT_GAS_ESTIMATION_PERCENTILE,
DEFAULT_MAX_CLAIM_GAS_LIMIT,
DEFAULT_MAX_FEE_PER_GAS_CAP,
DEFAULT_MAX_NUMBER_OF_RETRIES,
DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
DEFAULT_PROFIT_MARGIN,
DEFAULT_RETRY_DELAY_IN_SECONDS,
} from "../../../core/constants";
@@ -72,6 +74,8 @@ describe("TestMessageClaimingProcessor", () => {
transactionValidationService = new EthereumTransactionValidationService(lineaRollupContractMock, gasProvider, {
profitMargin: DEFAULT_PROFIT_MARGIN,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
isPostmanSponsorshipEnabled: DEFAULT_ENABLE_POSTMAN_SPONSORING,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
});
messageClaimingProcessor = new MessageClaimingProcessor(
lineaRollupContractMock,
@@ -141,6 +145,7 @@ describe("TestMessageClaimingProcessor", () => {
hasZeroFee: true,
isRateLimitExceeded: false,
isUnderPriced: false,
isForSponsorship: false,
estimatedGasLimit: 50_000n,
threshold: 5,
maxPriorityFeePerGas: DEFAULT_MAX_FEE_PER_GAS,
@@ -203,7 +208,7 @@ describe("TestMessageClaimingProcessor", () => {
.mockResolvedValue({ maxFeePerGas: 1000000000n, maxPriorityFeePerGas: 1000000000n });
jest.spyOn(databaseService, "getMessageToClaim").mockResolvedValue(testAnchoredMessage);
jest.spyOn(lineaRollupContractMock, "getMessageStatus").mockResolvedValue(OnChainMessageStatus.CLAIMABLE);
jest.spyOn(lineaRollupContractMock, "estimateClaimGas").mockResolvedValue(200_000n);
jest.spyOn(lineaRollupContractMock, "estimateClaimGas").mockResolvedValue(DEFAULT_MAX_CLAIM_GAS_LIMIT * 2n);
const expectedLoggingMessage = new Message(testAnchoredMessage);
const expectedSavedMessage = new Message({
...testAnchoredMessage,
@@ -219,7 +224,7 @@ describe("TestMessageClaimingProcessor", () => {
"Estimated gas limit is higher than the max allowed gas limit for this message: messageHash=%s messageInfo=%s estimatedGasLimit=%s maxAllowedGasLimit=%s",
expectedLoggingMessage.messageHash,
expectedLoggingMessage.toString(),
undefined, //"200000",
undefined, // DEFAULT_MAX_CLAIM_GAS_LIMIT * 2n,
testL2NetworkConfig.claiming.maxClaimGasLimit!.toString(),
);
expect(messageRepositorySaveSpy).toHaveBeenCalledTimes(1);
@@ -374,4 +379,115 @@ describe("TestMessageClaimingProcessor", () => {
});
});
});
describe("process with sponsorship", () => {
beforeEach(() => {
transactionValidationService = new EthereumTransactionValidationService(lineaRollupContractMock, gasProvider, {
profitMargin: DEFAULT_PROFIT_MARGIN,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
isPostmanSponsorshipEnabled: true,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
});
messageClaimingProcessor = new MessageClaimingProcessor(
lineaRollupContractMock,
signer,
databaseService,
transactionValidationService,
{
maxNonceDiff: 5,
profitMargin: DEFAULT_PROFIT_MARGIN,
maxNumberOfRetries: DEFAULT_MAX_NUMBER_OF_RETRIES,
retryDelayInSeconds: DEFAULT_RETRY_DELAY_IN_SECONDS,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
direction: Direction.L2_TO_L1,
originContractAddress: TEST_CONTRACT_ADDRESS_2,
},
logger,
);
});
it("Should successfully claim message with fee", async () => {
const lineaRollupContractMsgStatusSpy = jest.spyOn(lineaRollupContractMock, "getMessageStatus");
const l2MessageServiceContractClaimSpy = jest.spyOn(lineaRollupContractMock, "claim");
const messageRepositorySaveSpy = jest.spyOn(databaseService, "updateMessage");
const messageRepositoryUpdateAtomicSpy = jest.spyOn(databaseService, "updateMessageWithClaimTxAtomic");
jest.spyOn(databaseService, "getLastClaimTxNonce").mockResolvedValue(100);
jest.spyOn(signer, "getNonce").mockResolvedValue(99);
jest
.spyOn(gasProvider, "getGasFees")
.mockResolvedValue({ maxFeePerGas: 1000000000n, maxPriorityFeePerGas: 1000000000n });
jest.spyOn(databaseService, "getMessageToClaim").mockResolvedValue(testAnchoredMessage);
jest.spyOn(lineaRollupContractMock, "getMessageStatus").mockResolvedValue(OnChainMessageStatus.CLAIMABLE);
jest.spyOn(lineaRollupContractMock, "estimateClaimGas").mockResolvedValue(100_000n);
jest.spyOn(lineaRollupContractMock, "isRateLimitExceeded").mockResolvedValue(false);
const expectedLoggingMessage = new Message({
...testAnchoredMessage,
claimGasEstimationThreshold: 10000000000,
updatedAt: mockedDate,
});
await messageClaimingProcessor.process();
expect(lineaRollupContractMsgStatusSpy).toHaveBeenCalledTimes(1);
expect(messageRepositorySaveSpy).toHaveBeenCalledTimes(1);
expect(messageRepositorySaveSpy).toHaveBeenCalledWith(expectedLoggingMessage);
expect(l2MessageServiceContractClaimSpy).toHaveBeenCalledTimes(1);
expect(messageRepositoryUpdateAtomicSpy).toHaveBeenCalledTimes(1);
});
it("Should successfully claim message with zero fee", async () => {
const lineaRollupContractMsgStatusSpy = jest.spyOn(lineaRollupContractMock, "getMessageStatus");
const l2MessageServiceContractClaimSpy = jest.spyOn(lineaRollupContractMock, "claim");
const messageRepositorySaveSpy = jest.spyOn(databaseService, "updateMessage");
const messageRepositoryUpdateAtomicSpy = jest.spyOn(databaseService, "updateMessageWithClaimTxAtomic");
jest.spyOn(databaseService, "getLastClaimTxNonce").mockResolvedValue(100);
jest.spyOn(signer, "getNonce").mockResolvedValue(99);
jest
.spyOn(gasProvider, "getGasFees")
.mockResolvedValue({ maxFeePerGas: 1000000000n, maxPriorityFeePerGas: 1000000000n });
jest.spyOn(databaseService, "getMessageToClaim").mockResolvedValue(testZeroFeeAnchoredMessage);
jest.spyOn(lineaRollupContractMock, "getMessageStatus").mockResolvedValue(OnChainMessageStatus.CLAIMABLE);
jest.spyOn(lineaRollupContractMock, "estimateClaimGas").mockResolvedValue(100_000n);
jest.spyOn(lineaRollupContractMock, "isRateLimitExceeded").mockResolvedValue(false);
const expectedLoggingMessage = new Message({
...testZeroFeeAnchoredMessage,
updatedAt: mockedDate,
});
await messageClaimingProcessor.process();
expect(lineaRollupContractMsgStatusSpy).toHaveBeenCalledTimes(1);
expect(messageRepositorySaveSpy).toHaveBeenCalledTimes(1);
expect(messageRepositorySaveSpy).toHaveBeenCalledWith(expectedLoggingMessage);
expect(l2MessageServiceContractClaimSpy).toHaveBeenCalledTimes(1);
expect(messageRepositoryUpdateAtomicSpy).toHaveBeenCalledTimes(1);
});
it("Should successfully claim message with underpriced fee", async () => {
const lineaRollupContractMsgStatusSpy = jest.spyOn(lineaRollupContractMock, "getMessageStatus");
const l2MessageServiceContractClaimSpy = jest.spyOn(lineaRollupContractMock, "claim");
const messageRepositorySaveSpy = jest.spyOn(databaseService, "updateMessage");
const messageRepositoryUpdateAtomicSpy = jest.spyOn(databaseService, "updateMessageWithClaimTxAtomic");
jest.spyOn(databaseService, "getLastClaimTxNonce").mockResolvedValue(100);
jest.spyOn(signer, "getNonce").mockResolvedValue(99);
jest
.spyOn(gasProvider, "getGasFees")
.mockResolvedValue({ maxFeePerGas: 1000000000n, maxPriorityFeePerGas: 1000000000n });
jest.spyOn(databaseService, "getMessageToClaim").mockResolvedValue(testUnderpricedAnchoredMessage);
jest.spyOn(lineaRollupContractMock, "getMessageStatus").mockResolvedValue(OnChainMessageStatus.CLAIMABLE);
jest.spyOn(lineaRollupContractMock, "estimateClaimGas").mockResolvedValue(100_000n);
const expectedLoggingMessage = new Message({
...testUnderpricedAnchoredMessage,
claimGasEstimationThreshold: 10,
updatedAt: mockedDate,
});
await messageClaimingProcessor.process();
expect(lineaRollupContractMsgStatusSpy).toHaveBeenCalledTimes(1);
expect(messageRepositorySaveSpy).toHaveBeenCalledTimes(1);
expect(messageRepositorySaveSpy).toHaveBeenCalledWith(expectedLoggingMessage);
expect(l2MessageServiceContractClaimSpy).toHaveBeenCalledTimes(1);
expect(messageRepositoryUpdateAtomicSpy).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -3,6 +3,8 @@ import { L1NetworkConfig, L2NetworkConfig } from "../../application/postman/app/
import { Message, MessageProps } from "../../core/entities/Message";
import { MessageStatus } from "../../core/enums";
import {
DEFAULT_ENABLE_POSTMAN_SPONSORING,
DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
DEFAULT_INITIAL_FROM_BLOCK,
DEFAULT_L2_MESSAGE_TREE_DEPTH,
DEFAULT_LISTENER_BLOCK_CONFIRMATIONS,
@@ -201,6 +203,8 @@ export const testL1NetworkConfig: L1NetworkConfig = {
retryDelayInSeconds: DEFAULT_RETRY_DELAY_IN_SECONDS,
maxClaimGasLimit: DEFAULT_MAX_CLAIM_GAS_LIMIT,
maxTxRetries: DEFAULT_MAX_TX_RETRIES,
isPostmanSponsorshipEnabled: DEFAULT_ENABLE_POSTMAN_SPONSORING,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
},
listener: {
pollingInterval: 4000,
@@ -228,6 +232,8 @@ export const testL2NetworkConfig: L2NetworkConfig = {
maxNumberOfRetries: DEFAULT_MAX_NUMBER_OF_RETRIES,
retryDelayInSeconds: DEFAULT_RETRY_DELAY_IN_SECONDS,
maxTxRetries: DEFAULT_MAX_TX_RETRIES,
isPostmanSponsorshipEnabled: DEFAULT_ENABLE_POSTMAN_SPONSORING,
maxPostmanSponsorGasLimit: DEFAULT_MAX_POSTMAN_SPONSOR_GAS_LIMIT,
},
listener: {
pollingInterval: 100,