mirror of
https://github.com/vacp2p/linea-monorepo.git
synced 2026-01-09 04:08:01 -05:00
[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:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -84,6 +84,8 @@ export type ClaimingOptions = {
|
||||
retryDelayInSeconds?: number;
|
||||
maxClaimGasLimit?: bigint;
|
||||
maxTxRetries?: number;
|
||||
isPostmanSponsorshipEnabled?: boolean;
|
||||
maxPostmanSponsorGasLimit?: bigint;
|
||||
};
|
||||
|
||||
export type ClaimingConfig = Omit<Required<ClaimingOptions>, "feeRecipientAddress"> & {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user