Skip to content

Commit 88aaddc

Browse files
authored
Merge pull request #266 from OriginTrail/fix/allowance-setting-logic-update
gas and allowance fixes
2 parents 84321e8 + 8877fdb commit 88aaddc

File tree

5 files changed

+188
-48
lines changed

5 files changed

+188
-48
lines changed

constants/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export const DEFAULT_PARAMETERS = {
219219
SIMULATE_TXS: false,
220220
FORCE_REPLACE_TXS: false,
221221
GAS_LIMIT_MULTIPLIER: 1,
222+
RETRY_TX_GAS_PRICE_MULTIPLIER: 3,
222223
};
223224

224225
export const DEFAULT_GAS_PRICE = {
@@ -236,3 +237,5 @@ export const PARANET_KNOWLEDGE_ASSET_ACCESS_POLICY = {
236237
};
237238

238239
export const CHUNK_BYTE_SIZE = 32;
240+
241+
export const FEE_HISTORY_BLOCK_COUNT = 5;

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dkg.js",
3-
"version": "8.2.0",
3+
"version": "8.2.1",
44
"description": "Javascript library for interaction with the OriginTrail Decentralized Knowledge Graph",
55
"main": "index.js",
66
"exports": {

services/blockchain-service/blockchain-service-base.js

Lines changed: 168 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import { createRequire } from 'module';
77
import {
88
OPERATIONS_STEP_STATUS,
99
DEFAULT_GAS_PRICE,
10-
DEFAULT_GAS_PRICE_WEI,
1110
ZERO_ADDRESS,
1211
NEUROWEB_INCENTIVE_TYPE_CHAINS,
12+
FEE_HISTORY_BLOCK_COUNT,
1313
} from '../../constants/constants.js';
1414
import emptyHooks from '../../util/empty-hooks.js';
1515
import { sleepForMilliseconds } from '../utilities.js';
@@ -185,10 +185,10 @@ export default class BlockchainServiceBase {
185185
);
186186
gasLimit = Math.round(gasLimit * blockchain.gasLimitMultiplier);
187187

188-
let gasPrice;
189-
if (blockchain.previousTxGasPrice && blockchain.retryTx) {
190-
// Increase previous tx gas price by 20%
191-
gasPrice = Math.round(blockchain.previousTxGasPrice * 1.2);
188+
// let gasPrice;
189+
/*if (blockchain.previousTxGasPrice && blockchain.retryTx) {
190+
// Increase previous tx gas price by retryTxGasPriceMultiplier
191+
gasPrice = Math.round(blockchain.previousTxGasPrice * blockchain.retryTxGasPriceMultiplier);
192192
} else if (blockchain.forceReplaceTxs) {
193193
// Get the current transaction count (nonce) of the wallet, including pending transactions
194194
const currentNonce = await web3Instance.eth.getTransactionCount(publicKey, 'pending');
@@ -208,21 +208,23 @@ export default class BlockchainServiceBase {
208208
);
209209
210210
if (pendingTx) {
211-
// If found, increase gas price of pending tx by 20%
212-
gasPrice = Math.round(Number(pendingTx.gasPrice) * 1.2);
211+
// If found, increase gas price of pending tx by retryTxGasPriceMultiplier
212+
gasPrice = Math.round(Number(pendingTx.gasPrice) * blockchain.retryTxGasPriceMultiplier);
213213
} else {
214-
// If not found, use default/network gas price increased by 20%
214+
// If not found, use default/network gas price increased by retryTxGasPriceMultiplier
215215
// Theoretically this should never happen
216216
gasPrice = Math.round(
217-
(blockchain.gasPrice || (await this.getNetworkGasPrice(blockchain))) * 1.2,
217+
(blockchain.gasPrice || (await this.getSmartGasPrice(blockchain))) * blockchain.retryTxGasPriceMultiplier,
218218
);
219219
}
220220
} else {
221-
gasPrice = blockchain.gasPrice || (await this.getNetworkGasPrice(blockchain));
221+
gasPrice = blockchain.gasPrice || (await this.getSmartGasPrice(blockchain));
222222
}
223223
} else {
224-
gasPrice = blockchain.gasPrice || (await this.getNetworkGasPrice(blockchain));
225-
}
224+
gasPrice = blockchain.gasPrice || (await this.getSmartGasPrice(blockchain));
225+
}*/
226+
227+
const gasPrice = blockchain.gasPrice ?? (await this.getSmartGasPrice(blockchain));
226228

227229
if (blockchain.simulateTxs) {
228230
await web3Instance.eth.call({
@@ -317,7 +319,13 @@ export default class BlockchainServiceBase {
317319
}
318320
}
319321

320-
async waitForEventFinality(initialReceipt, eventName, expectedEventId, blockchain, confirmations = 1) {
322+
async waitForEventFinality(
323+
initialReceipt,
324+
eventName,
325+
expectedEventId,
326+
blockchain,
327+
confirmations = 1,
328+
) {
321329
await this.ensureBlockchainInfo(blockchain);
322330
const web3Instance = await this.getWeb3Instance(blockchain);
323331

@@ -330,7 +338,10 @@ export default class BlockchainServiceBase {
330338
// eslint-disable-next-line no-constant-condition
331339
while (true) {
332340
// 1. Wait until the block containing the tx is at the required depth
333-
while (await web3Instance.eth.getBlockNumber() < receipt.blockNumber + confirmations) {
341+
while (
342+
(await web3Instance.eth.getBlockNumber()) <
343+
receipt.blockNumber + confirmations
344+
) {
334345
await sleepForMilliseconds(polling);
335346
}
336347

@@ -352,7 +363,9 @@ export default class BlockchainServiceBase {
352363

353364
const idMatches =
354365
expectedEventId == null ||
355-
(eventData && eventData.id != null && eventData.id.toString() === expectedEventId.toString());
366+
(eventData &&
367+
eventData.id != null &&
368+
eventData.id.toString() === expectedEventId.toString());
356369

357370
if (eventData && idMatches) {
358371
return { receipt: currentReceipt, eventData };
@@ -432,39 +445,49 @@ export default class BlockchainServiceBase {
432445
);
433446
}
434447

448+
async needsMoreAllowance(sender, tokenAmount, blockchain, knowledgeCollectionAddress) {
449+
const allowance = await this.callContractFunction(
450+
'Token',
451+
'allowance',
452+
[sender, knowledgeCollectionAddress],
453+
blockchain,
454+
);
455+
456+
if (BigInt(allowance) < BigInt(tokenAmount)) return true;
457+
458+
return false;
459+
}
460+
461+
async maxAllowancePerTransaction(sender, blockchain) {
462+
if (blockchain.maxAllowance) {
463+
return blockchain.maxAllowance;
464+
} else {
465+
return await this.callContractFunction('Token', 'balanceOf', [sender], blockchain);
466+
}
467+
}
468+
435469
async increaseKnowledgeCollectionAllowance(sender, tokenAmount, blockchain) {
436470
const knowledgeCollectionAddress = await this.getContractAddress(
437471
'KnowledgeCollection',
438472
blockchain,
439473
);
440474

441-
const allowance = await this.callContractFunction(
442-
'Token',
443-
'allowance',
444-
[sender, knowledgeCollectionAddress],
475+
const needsMoreAllowance = await this.needsMoreAllowance(
476+
sender,
477+
tokenAmount,
445478
blockchain,
479+
knowledgeCollectionAddress,
446480
);
447481

448-
const allowanceGap = BigInt(tokenAmount) - BigInt(allowance);
449-
450-
if (allowanceGap > 0) {
482+
if (needsMoreAllowance) {
483+
const allowanceThreshold = await this.maxAllowancePerTransaction(sender, blockchain);
451484
await this.executeContractFunction(
452485
'Token',
453-
'increaseAllowance',
454-
[knowledgeCollectionAddress, allowanceGap],
486+
'approve',
487+
[knowledgeCollectionAddress, allowanceThreshold],
455488
blockchain,
456489
);
457-
458-
return {
459-
allowanceIncreased: true,
460-
allowanceGap,
461-
};
462490
}
463-
464-
return {
465-
allowanceIncreased: false,
466-
allowanceGap,
467-
};
468491
}
469492

470493
// Knowledge assets operations
@@ -477,19 +500,16 @@ export default class BlockchainServiceBase {
477500
stepHooks = emptyHooks,
478501
) {
479502
const sender = await this.getPublicKey(blockchain);
480-
let allowanceIncreased = false;
481-
let allowanceGap = 0;
482503

483504
try {
484505
if (requestData?.paymaster && requestData?.paymaster !== ZERO_ADDRESS) {
485506
// Handle the case when payer is passed
486507
} else {
487-
({ allowanceIncreased, allowanceGap } =
488-
await this.increaseKnowledgeCollectionAllowance(
489-
sender,
490-
requestData.tokenAmount,
491-
blockchain,
492-
));
508+
await this.increaseKnowledgeCollectionAllowance(
509+
sender,
510+
requestData.tokenAmount,
511+
blockchain,
512+
);
493513
}
494514

495515
stepHooks.afterHook({
@@ -528,9 +548,7 @@ export default class BlockchainServiceBase {
528548

529549
return { knowledgeCollectionId: id, receipt };
530550
} catch (error) {
531-
if (allowanceIncreased) {
532-
await this.decreaseKnowledgeCollectionAllowance(allowanceGap, blockchain);
533-
}
551+
console.error('createKnowledgeCollection failed:', error);
534552
throw error;
535553
}
536554
}
@@ -1371,6 +1389,111 @@ export default class BlockchainServiceBase {
13711389
}
13721390
}
13731391

1392+
/**
1393+
* Get fee history from the last N blocks using eth_feeHistory RPC call
1394+
* @param {Object} blockchain - Blockchain configuration
1395+
* @param {number} blockCount - Number of blocks to fetch (default: 5)
1396+
* @returns {Promise<Object>} Fee history data with baseFeePerGas and priorityFees arrays
1397+
*/
1398+
async getFeeHistory(blockchain, blockCount = 5) {
1399+
await this.ensureBlockchainInfo(blockchain);
1400+
const web3Instance = await this.getWeb3Instance(blockchain);
1401+
1402+
try {
1403+
// eth_feeHistory params: blockCount, newestBlock, rewardPercentiles
1404+
// [50] = median priority fee per block
1405+
const feeHistory = await web3Instance.eth.getFeeHistory(blockCount, 'latest', [50]);
1406+
1407+
// Extract median priority fees from each block (reward[blockIndex][percentileIndex])
1408+
const priorityFees = feeHistory.reward
1409+
? feeHistory.reward.map((blockRewards) => BigInt(blockRewards[0] || 0))
1410+
: [];
1411+
1412+
return {
1413+
supported: true,
1414+
oldestBlock: parseInt(feeHistory.oldestBlock, 16),
1415+
baseFeePerGas: feeHistory.baseFeePerGas.map((bf) => BigInt(bf)),
1416+
priorityFees,
1417+
};
1418+
} catch (error) {
1419+
// eth_feeHistory not supported on this network
1420+
return {
1421+
supported: false,
1422+
error: error.message,
1423+
};
1424+
}
1425+
}
1426+
1427+
/**
1428+
* Apply buffer percentage to a gas price
1429+
* @param {BigInt} gasPrice - Gas price in wei
1430+
* @param {number} gasPriceBufferPercent - Buffer percentage to add
1431+
* @returns {BigInt} Gas price with buffer applied
1432+
*/
1433+
applyGasPriceBuffer(gasPrice, gasPriceBufferPercent) {
1434+
if (!gasPriceBufferPercent) return gasPrice;
1435+
return (gasPrice * BigInt(100 + Number(gasPriceBufferPercent))) / 100n;
1436+
}
1437+
1438+
/**
1439+
* Estimate safe gas price using eth_feeHistory (EIP-1559 style)
1440+
* Takes max base fee from last N blocks, adds a buffer for volatility,
1441+
* and includes the priority fee (tip) for validator incentive
1442+
* @param {Object} blockchain - Blockchain configuration
1443+
* @returns {Promise<BigInt>} Estimated gas price in wei
1444+
*/
1445+
async estimateGasPriceFromFeeHistory(blockchain) {
1446+
const { gasPriceBufferPercent } = blockchain;
1447+
const feeHistory = await this.getFeeHistory(blockchain, FEE_HISTORY_BLOCK_COUNT);
1448+
1449+
// Fallback to network gas price if feeHistory not supported or empty
1450+
if (!feeHistory.supported) {
1451+
return this.applyGasPriceBuffer(
1452+
BigInt(await this.getNetworkGasPrice(blockchain)),
1453+
gasPriceBufferPercent,
1454+
);
1455+
}
1456+
1457+
const baseFees = Array.from(feeHistory.baseFeePerGas);
1458+
const priorityFees = Array.from(feeHistory.priorityFees);
1459+
1460+
if (baseFees.length === 0 || priorityFees.length === 0) {
1461+
return this.applyGasPriceBuffer(
1462+
BigInt(await this.getNetworkGasPrice(blockchain)),
1463+
gasPriceBufferPercent,
1464+
);
1465+
}
1466+
1467+
// Find max base fee and priority fee from recent blocks
1468+
const maxBaseFee = baseFees.reduce((max, bf) => (bf > max ? bf : max), 0n);
1469+
const maxPriorityFee = priorityFees.reduce((max, pf) => (pf > max ? pf : max), 0n);
1470+
1471+
return this.applyGasPriceBuffer(maxBaseFee + maxPriorityFee, gasPriceBufferPercent);
1472+
}
1473+
1474+
/**
1475+
* Get gas price with EIP-1559 estimation (with fallback)
1476+
* Tries eth_feeHistory first, falls back to legacy methods
1477+
* @param {Object} blockchain - Blockchain configuration
1478+
* @returns {Promise<string>} Gas price in wei (as string for web3 compatibility)
1479+
*/
1480+
async getSmartGasPrice(blockchain) {
1481+
try {
1482+
const estimatedPrice = await this.estimateGasPriceFromFeeHistory(blockchain);
1483+
return estimatedPrice.toString();
1484+
} catch (eip1559Error) {
1485+
try {
1486+
return await this.getNetworkGasPrice(blockchain);
1487+
} catch (fallbackError) {
1488+
throw new Error(
1489+
`All gas price estimation methods failed. ` +
1490+
`EIP-1559: ${eip1559Error?.message || 'N/A'}. ` +
1491+
`Fallback: ${fallbackError.message}`,
1492+
);
1493+
}
1494+
}
1495+
}
1496+
13741497
async getWalletBalances(blockchain) {
13751498
await this.ensureBlockchainInfo(blockchain);
13761499
const web3Instance = await this.getWeb3Instance(blockchain);

services/input-service.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,17 @@ export default class InputService {
192192
BLOCKCHAINS[environment][name]?.gasPriceOracleLink ??
193193
undefined;
194194

195+
const maxAllowance =
196+
options.blockchain?.maxAllowance ?? this.config.blockchain?.maxAllowance ?? undefined;
197+
const gasPriceBufferPercent =
198+
options.blockchain?.gasPriceBufferPercent ??
199+
this.config.blockchain?.gasPriceBufferPercent ??
200+
undefined;
201+
const retryTxGasPriceMultiplier =
202+
options.blockchain?.retryTxGasPriceMultiplier ??
203+
this.config.blockchain?.retryTxGasPriceMultiplier ??
204+
DEFAULT_PARAMETERS.RETRY_TX_GAS_PRICE_MULTIPLIER; // e.g., 1.2
205+
195206
const blockchainConfig = {
196207
name,
197208
rpc,
@@ -205,6 +216,9 @@ export default class InputService {
205216
simulateTxs,
206217
forceReplaceTxs,
207218
gasPriceOracleLink,
219+
maxAllowance,
220+
gasPriceBufferPercent,
221+
retryTxGasPriceMultiplier,
208222
};
209223

210224
if (name && name.startsWith('otp')) {

0 commit comments

Comments
 (0)