Skip to content
2 changes: 2 additions & 0 deletions constants/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ export const DEFAULT_PARAMETERS = {
SIMULATE_TXS: false,
FORCE_REPLACE_TXS: false,
GAS_LIMIT_MULTIPLIER: 1,
BUFFER_PERCENT: 10,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's name this variable "GAS_PRICE_BUFFER_PERCENT" or similar, so it's obvious what this buffer is for

in the words of late Phil Karlton "There are only two hard things in Computer Science: cache invalidation and naming things."

RETRY_TX_GAS_PRICE_MULTIPLIER: 3
};

export const DEFAULT_GAS_PRICE = {
Expand Down
180 changes: 148 additions & 32 deletions services/blockchain-service/blockchain-service-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ export default class BlockchainServiceBase {
gasLimit = Math.round(gasLimit * blockchain.gasLimitMultiplier);

let gasPrice;
if (blockchain.previousTxGasPrice && blockchain.retryTx) {
// Increase previous tx gas price by 20%
gasPrice = Math.round(blockchain.previousTxGasPrice * 1.2);
/*if (blockchain.previousTxGasPrice && blockchain.retryTx) {
// Increase previous tx gas price by retryTxGasPriceMultiplier
gasPrice = Math.round(blockchain.previousTxGasPrice * blockchain.retryTxGasPriceMultiplier);
} else if (blockchain.forceReplaceTxs) {
// Get the current transaction count (nonce) of the wallet, including pending transactions
const currentNonce = await web3Instance.eth.getTransactionCount(publicKey, 'pending');
Expand All @@ -208,21 +208,23 @@ export default class BlockchainServiceBase {
);

if (pendingTx) {
// If found, increase gas price of pending tx by 20%
gasPrice = Math.round(Number(pendingTx.gasPrice) * 1.2);
// If found, increase gas price of pending tx by retryTxGasPriceMultiplier
gasPrice = Math.round(Number(pendingTx.gasPrice) * blockchain.retryTxGasPriceMultiplier);
} else {
// If not found, use default/network gas price increased by 20%
// If not found, use default/network gas price increased by retryTxGasPriceMultiplier
// Theoretically this should never happen
gasPrice = Math.round(
(blockchain.gasPrice || (await this.getNetworkGasPrice(blockchain))) * 1.2,
(blockchain.gasPrice || (await this.getSmartGasPrice(blockchain))) * blockchain.retryTxGasPriceMultiplier,
);
}
} else {
gasPrice = blockchain.gasPrice || (await this.getNetworkGasPrice(blockchain));
gasPrice = blockchain.gasPrice || (await this.getSmartGasPrice(blockchain));
}
} else {
gasPrice = blockchain.gasPrice || (await this.getNetworkGasPrice(blockchain));
}
gasPrice = blockchain.gasPrice || (await this.getSmartGasPrice(blockchain));
}*/
Comment on lines 189 to +225
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this commented out, do we have the retry logic anywhere else?
Can we remove the previousTxGasPrice and retryTx flags to simplify the code then?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: forceReplaceTxs configuration option is now ignored

The forceReplaceTxs option is still accepted in the input service and included in the blockchain config, but the entire code block that handles this feature (lines 193-223) is now commented out. Users who configure forceReplaceTxs to replace pending transactions with higher gas prices will find this feature silently no longer works.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Commented out code should have been deleted

A large block of code (lines 188-225) is commented out instead of being removed. The PR discussion explicitly asks "I guess we remove all this commented out code?" suggesting this commented code was supposed to be deleted entirely, not left in place. Leaving commented-out code in production creates maintenance burden and confusion about what the code is supposed to do.

Fix in Cursor Fix in Web


gasPrice = await this.getSmartGasPrice(blockchain);

if (blockchain.simulateTxs) {
await web3Instance.eth.call({
Expand All @@ -238,7 +240,7 @@ export default class BlockchainServiceBase {
from: publicKey,
to: contractInstance.options.address,
data: encodedABI,
gasPrice,
gasPrice: gasPrice,
gas: gasLimit,
};
}
Expand Down Expand Up @@ -282,7 +284,7 @@ export default class BlockchainServiceBase {
while (
!currentReceipt &&
Date.now() - reminingStartTime <
blockchain.transactionReminingMaxWaitTime
blockchain.transactionReminingMaxWaitTime
) {
await sleepForMilliseconds(
blockchain.transactionReminingPollingInterval,
Expand Down Expand Up @@ -432,11 +434,7 @@ export default class BlockchainServiceBase {
);
}

async increaseKnowledgeCollectionAllowance(sender, tokenAmount, blockchain) {
const knowledgeCollectionAddress = await this.getContractAddress(
'KnowledgeCollection',
blockchain,
);
async needsMoreAllowance(sender, tokenAmount, blockchain, knowledgeCollectionAddress) {

const allowance = await this.callContractFunction(
'Token',
Expand All @@ -445,25 +443,51 @@ export default class BlockchainServiceBase {
blockchain,
);

const allowanceGap = BigInt(tokenAmount) - BigInt(allowance);
if (BigInt(allowance) < BigInt(tokenAmount))
return true;
else
return false;
}

if (allowanceGap > 0) {
await this.executeContractFunction(
async maxAllowancePerTransaction(sender, blockchain) {
if (blockchain.maxAllowance) {
return blockchain.maxAllowance;
} else {
return await this.callContractFunction(
'Token',
'increaseAllowance',
[knowledgeCollectionAddress, allowanceGap],
'balanceOf',
[sender],
blockchain,
);
}
}

async increaseKnowledgeCollectionAllowance(sender, tokenAmount, blockchain) {
const knowledgeCollectionAddress = await this.getContractAddress(
'KnowledgeCollection',
blockchain,
);

const needsMoreAllowance = await this.needsMoreAllowance(sender, tokenAmount, blockchain, knowledgeCollectionAddress);

let allowanceThreshold = await this.maxAllowancePerTransaction(sender, blockchain);

if (needsMoreAllowance) {
await this.executeContractFunction(
'Token',
'approve',
[knowledgeCollectionAddress, allowanceThreshold],
blockchain,
);
return {
allowanceIncreased: true,
allowanceGap,
allowanceCurrent: allowanceThreshold,
};
}

return {
allowanceIncreased: false,
allowanceGap,
allowanceCurrent: allowanceThreshold,
};
}

Expand All @@ -478,13 +502,13 @@ export default class BlockchainServiceBase {
) {
const sender = await this.getPublicKey(blockchain);
let allowanceIncreased = false;
let allowanceGap = 0;
let allowanceCurrent = 0;

try {
if (requestData?.paymaster && requestData?.paymaster !== ZERO_ADDRESS) {
// Handle the case when payer is passed
} else {
({ allowanceIncreased, allowanceGap } =
({ allowanceIncreased, allowanceCurrent } =
await this.increaseKnowledgeCollectionAllowance(
sender,
requestData.tokenAmount,
Expand Down Expand Up @@ -528,9 +552,11 @@ export default class BlockchainServiceBase {

return { knowledgeCollectionId: id, receipt };
} catch (error) {
if (allowanceIncreased) {
await this.decreaseKnowledgeCollectionAllowance(allowanceGap, blockchain);
/*if (allowanceIncreased) {
await this.decreaseKnowledgeCollectionAllowance(allowanceCurrent, blockchain);
}
throw error;*/
console.error('createKnowledgeCollection failed:', error);
throw error;
}
}
Expand Down Expand Up @@ -1043,7 +1069,7 @@ export default class BlockchainServiceBase {

if (
this[blockchain.name].contractAddresses[blockchain.hubContract][
'ParanetIncentivesPoolStorage'
'ParanetIncentivesPoolStorage'
] !== contractAddress
) {
this[blockchain.name].contractAddresses[blockchain.hubContract][
Expand All @@ -1055,7 +1081,7 @@ export default class BlockchainServiceBase {
] = await new web3Instance.eth.Contract(
this.abis['ParanetIncentivesPoolStorage'],
this[blockchain.name].contractAddresses[blockchain.hubContract][
'ParanetIncentivesPoolStorage'
'ParanetIncentivesPoolStorage'
],
{ from: blockchain.publicKey },
);
Expand Down Expand Up @@ -1090,7 +1116,7 @@ export default class BlockchainServiceBase {

if (
this[blockchain.name].contractAddresses[blockchain.hubContract][
'ParanetIncentivesPool'
'ParanetIncentivesPool'
] !== contractAddress
) {
this[blockchain.name].contractAddresses[blockchain.hubContract][
Expand All @@ -1101,7 +1127,7 @@ export default class BlockchainServiceBase {
await new web3Instance.eth.Contract(
this.abis['ParanetIncentivesPool'],
this[blockchain.name].contractAddresses[blockchain.hubContract][
'ParanetIncentivesPool'
'ParanetIncentivesPool'
],
{ from: blockchain.publicKey },
);
Expand Down Expand Up @@ -1371,6 +1397,96 @@ export default class BlockchainServiceBase {
}
}

/**
* Get fee history from the last N blocks using eth_feeHistory RPC call
* @param {Object} blockchain - Blockchain configuration
* @param {number} blockCount - Number of blocks to fetch (default: 5)
* @returns {Promise<Object>} Fee history data with baseFeePerGas array
*/
async getFeeHistory(blockchain, blockCount = 5) {
await this.ensureBlockchainInfo(blockchain);
const web3Instance = await this.getWeb3Instance(blockchain);

try {
const latestBlock = await web3Instance.eth.getBlockNumber();

// eth_feeHistory params: blockCount (hex), newestBlock (hex), rewardPercentiles
const feeHistory = await web3Instance.eth.getFeeHistory(blockCount, 'latest', []);
console.log('feeHistory', feeHistory);

return {
supported: true,
oldestBlock: parseInt(feeHistory.oldestBlock, 16),
baseFeePerGas: feeHistory.baseFeePerGas.map(bf => BigInt(bf))
};

} catch (error) {
// eth_feeHistory not supported on this network
return {
supported: false,
error: error.message,
};
}
}

/**
* Estimate safe gas price using eth_feeHistory (EIP-1559 style)
* Takes max base fee from last N blocks and adds a buffer
* @param {Object} blockchain - Blockchain configuration
* @param {Object} options - Options
* @param {number} options.blockCount - Number of blocks to analyze (default: 5)
* @param {number} options.bufferPercent - Buffer percentage to add (default: 10)
* @returns {Promise<BigInt>} Estimated gas price in wei
*/
async estimateGasPriceFromFeeHistory(blockchain, options = {}) {
const { blockCount = 5, bufferPercent = 10 } = options;

const feeHistory = await this.getFeeHistory(blockchain, blockCount);

if (!feeHistory.supported) {
// Fallback to existing method if eth_feeHistory not supported
console.warn(`eth_feeHistory not supported: ${feeHistory.error}. Using fallback.`);
return BigInt(await this.getNetworkGasPrice(blockchain));
}

// Get base fees (exclude last element - it's for the next block)
const baseFees = feeHistory.baseFeePerGas.slice(0, -1);

if (baseFees.length === 0) {
return BigInt(await this.getNetworkGasPrice(blockchain));
}

// Find max base fee from recent blocks
let maxBaseFee = baseFees.reduce((max, bf) => bf > max ? bf : max, 0n);

// Add buffer (e.g., 20% = multiply by 120, divide by 100)
let safeGasPrice = (maxBaseFee * BigInt(100 + bufferPercent)) / 100n;

if(this.isGnosis(blockchain.name)) {
safeGasPrice = safeGasPrice + 1n;
}

return safeGasPrice;
}

/**
* Get gas price with EIP-1559 estimation (with fallback)
* Tries eth_feeHistory first, falls back to legacy methods
* @param {Object} blockchain - Blockchain configuration
* @returns {Promise<string>} Gas price in wei (as string for web3 compatibility)
*/
async getSmartGasPrice(blockchain) {
// Only use EIP-1559 estimation for chains that support it

try {
const estimatedPrice = await this.estimateGasPriceFromFeeHistory(blockchain, blockchain.bufferPercent ? { bufferPercent: blockchain.bufferPercent } : {});
console.log('estimatedPrice', estimatedPrice);
return estimatedPrice.toString();
} catch (error) {
console.warn(`EIP-1559 gas estimation failed: ${error.message}. Using fallback.`);
}
}

async getWalletBalances(blockchain) {
await this.ensureBlockchainInfo(blockchain);
const web3Instance = await this.getWeb3Instance(blockchain);
Expand Down
14 changes: 14 additions & 0 deletions services/input-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,17 @@ export default class InputService {
BLOCKCHAINS[environment][name]?.gasPriceOracleLink ??
undefined;

const maxAllowance =
options.blockchain?.maxAllowance ?? this.config.blockchain?.maxAllowance ?? undefined;
const bufferPercent =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's name this variable "gasPriceBufferPercent" or similar, so it's obvious what this buffer is for

in the words of late Phil Karlton "There are only two hard things in Computer Science: cache invalidation and naming things."

options.blockchain?.bufferPercent ??
this.config.blockchain?.bufferPercent ??
DEFAULT_PARAMETERS.BUFFER_PERCENT; // e.g., 50
const retryTxGasPriceMultiplier =
options.blockchain?.retryTxGasPriceMultiplier ??
this.config.blockchain?.retryTxGasPriceMultiplier ??
DEFAULT_PARAMETERS.RETRY_TX_GAS_PRICE_MULTIPLIER; // e.g., 1.2
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: New retryTxGasPriceMultiplier option is dead code

A new retryTxGasPriceMultiplier configuration option is added along with a default constant, but the only code that would use this value is inside a commented-out block in prepareTransaction. This option has no effect and cannot function until the commented code is re-enabled.

Additional Locations (1)

Fix in Cursor Fix in Web


const blockchainConfig = {
name,
rpc,
Expand All @@ -205,6 +216,9 @@ export default class InputService {
simulateTxs,
forceReplaceTxs,
gasPriceOracleLink,
maxAllowance,
bufferPercent,
retryTxGasPriceMultiplier
};

if (name && name.startsWith('otp')) {
Expand Down