dkg.js 8.2.1 → 8.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/check-package-lock.yml +75 -0
- package/constants/constants.js +6 -0
- package/index.cjs +372 -111
- package/managers/asset-operations-manager.js +141 -28
- package/package.json +2 -2
- package/services/blockchain-service/blockchain-service-base.js +161 -80
- package/services/blockchain-service/implementations/node-blockchain-service.js +36 -5
- package/services/input-service.js +20 -0
- package/services/node-api-service/implementations/http-service.js +16 -0
|
@@ -207,14 +207,13 @@ export default class AssetOperationsManager {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
/**
|
|
210
|
-
*
|
|
210
|
+
* Phase 1 of asset creation: validate input, build dataset, and publish to the node.
|
|
211
211
|
* @async
|
|
212
|
-
* @param {Object} content - The content of the knowledge collection
|
|
213
|
-
* @param {Object} [options={}] -
|
|
214
|
-
* @
|
|
215
|
-
* @returns {Object} Object containing UAL, publicAssertionId and operation status.
|
|
212
|
+
* @param {Object|string} content - The content of the knowledge collection.
|
|
213
|
+
* @param {Object} [options={}] - Options for knowledge collection creation.
|
|
214
|
+
* @returns {Object} Publish phase output including dataset info and publish operation data.
|
|
216
215
|
*/
|
|
217
|
-
async
|
|
216
|
+
async publishAssetPhase(content, options = {}) {
|
|
218
217
|
this.validationService.validateJsonldOrNquads(content);
|
|
219
218
|
const {
|
|
220
219
|
blockchain,
|
|
@@ -367,17 +366,52 @@ export default class AssetOperationsManager {
|
|
|
367
366
|
publishOperationId,
|
|
368
367
|
);
|
|
369
368
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
369
|
+
return {
|
|
370
|
+
dataset,
|
|
371
|
+
datasetRoot,
|
|
372
|
+
datasetSize,
|
|
373
|
+
publishOperationId,
|
|
374
|
+
publishOperationResult,
|
|
375
|
+
contentAssetStorageAddress,
|
|
376
|
+
blockchain,
|
|
377
|
+
endpoint,
|
|
378
|
+
port,
|
|
379
|
+
maxNumberOfRetries,
|
|
380
|
+
frequency,
|
|
381
|
+
authToken,
|
|
382
|
+
epochsNum,
|
|
383
|
+
hashFunctionId,
|
|
384
|
+
scoreFunctionId,
|
|
385
|
+
immutable,
|
|
386
|
+
tokenAmount,
|
|
387
|
+
payer,
|
|
388
|
+
minimumNumberOfFinalizationConfirmations,
|
|
389
|
+
minimumNumberOfNodeReplications,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Phase 2 of asset creation: mint the knowledge collection on chain using publish output.
|
|
395
|
+
* @async
|
|
396
|
+
* @param {Object} publishPayload - Output of publishAssetPhase.
|
|
397
|
+
* @param {Object} [options={}] - Options affecting minting (e.g., minimumBlockConfirmations).
|
|
398
|
+
* @param {Object} [stepHooks=emptyHooks] - Hooks to execute during minting.
|
|
399
|
+
* @returns {Object} Mint phase output including UAL and mint receipt.
|
|
400
|
+
*/
|
|
401
|
+
async mintKnowledgeCollectionPhase(publishPayload, options = {}, stepHooks = emptyHooks) {
|
|
402
|
+
const {
|
|
403
|
+
dataset,
|
|
404
|
+
datasetRoot,
|
|
405
|
+
datasetSize,
|
|
406
|
+
publishOperationId,
|
|
407
|
+
publishOperationResult,
|
|
408
|
+
contentAssetStorageAddress,
|
|
409
|
+
blockchain,
|
|
410
|
+
epochsNum,
|
|
411
|
+
immutable,
|
|
412
|
+
tokenAmount,
|
|
413
|
+
payer,
|
|
414
|
+
} = publishPayload;
|
|
381
415
|
|
|
382
416
|
const { signatures } = publishOperationResult.data;
|
|
383
417
|
|
|
@@ -479,6 +513,45 @@ export default class AssetOperationsManager {
|
|
|
479
513
|
|
|
480
514
|
const UAL = deriveUAL(blockchain.name, contentAssetStorageAddress, knowledgeCollectionId);
|
|
481
515
|
|
|
516
|
+
return {
|
|
517
|
+
UAL,
|
|
518
|
+
knowledgeCollectionId,
|
|
519
|
+
mintKnowledgeCollectionReceipt,
|
|
520
|
+
datasetRoot,
|
|
521
|
+
publishOperationId,
|
|
522
|
+
publishOperationResult,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Phase 3 of asset creation: poll node finality status for the minted asset.
|
|
528
|
+
* @async
|
|
529
|
+
* @param {string} UAL - Universal Asset Locator returned from minting.
|
|
530
|
+
* @param {Object} [options={}] - Finality options.
|
|
531
|
+
* @returns {Object} Finality status details.
|
|
532
|
+
*/
|
|
533
|
+
async finalizePublishPhase(UAL, options = {}) {
|
|
534
|
+
// UAL should point to a knowledge collection (kcUAL), not a knowledge asset (kaUAL).
|
|
535
|
+
this.validationService.validateUAL(UAL);
|
|
536
|
+
|
|
537
|
+
const {
|
|
538
|
+
endpoint,
|
|
539
|
+
port,
|
|
540
|
+
maxNumberOfRetries,
|
|
541
|
+
frequency,
|
|
542
|
+
minimumNumberOfFinalizationConfirmations,
|
|
543
|
+
authToken,
|
|
544
|
+
} = this.inputService.getPublishFinalityArguments(options);
|
|
545
|
+
|
|
546
|
+
this.validationService.validatePublishFinality(
|
|
547
|
+
endpoint,
|
|
548
|
+
port,
|
|
549
|
+
maxNumberOfRetries,
|
|
550
|
+
frequency,
|
|
551
|
+
minimumNumberOfFinalizationConfirmations,
|
|
552
|
+
authToken,
|
|
553
|
+
);
|
|
554
|
+
|
|
482
555
|
let finalityStatusResult = 0;
|
|
483
556
|
if (minimumNumberOfFinalizationConfirmations > 0) {
|
|
484
557
|
finalityStatusResult = await this.nodeApiService.finalityStatus(
|
|
@@ -493,20 +566,60 @@ export default class AssetOperationsManager {
|
|
|
493
566
|
}
|
|
494
567
|
|
|
495
568
|
return {
|
|
496
|
-
|
|
497
|
-
|
|
569
|
+
status:
|
|
570
|
+
finalityStatusResult >= minimumNumberOfFinalizationConfirmations
|
|
571
|
+
? 'FINALIZED'
|
|
572
|
+
: 'NOT FINALIZED',
|
|
573
|
+
numberOfConfirmations: finalityStatusResult,
|
|
574
|
+
requiredConfirmations: minimumNumberOfFinalizationConfirmations,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Creates a new knowledge collection.
|
|
580
|
+
* @async
|
|
581
|
+
* @param {Object} content - The content of the knowledge collection to be created, contains public, private or both keys.
|
|
582
|
+
* @param {Object} [options={}] - Additional options for knowledge collection creation.
|
|
583
|
+
* @param {Object} [stepHooks=emptyHooks] - Hooks to execute during knowledge collection creation.
|
|
584
|
+
* @returns {Object} Object containing UAL, publicAssertionId and operation status.
|
|
585
|
+
*/
|
|
586
|
+
async create(content, options = {}, stepHooks = emptyHooks) {
|
|
587
|
+
const publishOperationOutput = await this.publishAssetPhase(content, options);
|
|
588
|
+
const { datasetRoot, publishOperationId, publishOperationResult } = publishOperationOutput;
|
|
589
|
+
|
|
590
|
+
if (
|
|
591
|
+
publishOperationResult.status !== OPERATION_STATUSES.COMPLETED &&
|
|
592
|
+
!publishOperationResult.data.minAcksReached
|
|
593
|
+
) {
|
|
594
|
+
return {
|
|
595
|
+
datasetRoot,
|
|
596
|
+
operation: {
|
|
597
|
+
publish: getOperationStatusObject(publishOperationResult, publishOperationId),
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const mintOperationOutput = await this.mintKnowledgeCollectionPhase(
|
|
603
|
+
publishOperationOutput,
|
|
604
|
+
options,
|
|
605
|
+
stepHooks,
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
const finalityOperationOutput = await this.finalizePublishPhase(
|
|
609
|
+
mintOperationOutput.UAL,
|
|
610
|
+
options,
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
UAL: mintOperationOutput.UAL,
|
|
615
|
+
datasetRoot: mintOperationOutput.datasetRoot,
|
|
498
616
|
signatures: publishOperationResult.data.signatures,
|
|
499
617
|
operation: {
|
|
500
|
-
mintKnowledgeCollection: mintKnowledgeCollectionReceipt,
|
|
618
|
+
mintKnowledgeCollection: mintOperationOutput.mintKnowledgeCollectionReceipt,
|
|
501
619
|
publish: getOperationStatusObject(publishOperationResult, publishOperationId),
|
|
502
|
-
finality: {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
? 'FINALIZED'
|
|
506
|
-
: 'NOT FINALIZED',
|
|
507
|
-
},
|
|
508
|
-
numberOfConfirmations: finalityStatusResult,
|
|
509
|
-
requiredConfirmations: minimumNumberOfFinalizationConfirmations,
|
|
620
|
+
finality: { status: finalityOperationOutput.status },
|
|
621
|
+
numberOfConfirmations: finalityOperationOutput.numberOfConfirmations,
|
|
622
|
+
requiredConfirmations: finalityOperationOutput.requiredConfirmations,
|
|
510
623
|
},
|
|
511
624
|
};
|
|
512
625
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dkg.js",
|
|
3
|
-
"version": "8.2.
|
|
3
|
+
"version": "8.2.4",
|
|
4
4
|
"description": "Javascript library for interaction with the OriginTrail Decentralized Knowledge Graph",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"web3": "^1.7.3"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
+
"@rollup/rollup-linux-x64-gnu": "^4.28.1",
|
|
56
57
|
"assert": "^2.0.0",
|
|
57
58
|
"buffer": "^6.0.3",
|
|
58
59
|
"crypto-browserify": "^3.12.0",
|
|
@@ -63,7 +64,6 @@
|
|
|
63
64
|
"os-browserify": "^0.3.0",
|
|
64
65
|
"prettier": "^2.7.1",
|
|
65
66
|
"rollup": "^4.28.1",
|
|
66
|
-
"@rollup/rollup-linux-x64-gnu": "^4.53.1",
|
|
67
67
|
"stream-browserify": "^3.0.0",
|
|
68
68
|
"stream-http": "^3.2.0",
|
|
69
69
|
"terser-webpack-plugin": "^5.3.6",
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
ZERO_ADDRESS,
|
|
11
11
|
NEUROWEB_INCENTIVE_TYPE_CHAINS,
|
|
12
12
|
FEE_HISTORY_BLOCK_COUNT,
|
|
13
|
+
GAS_MODES,
|
|
14
|
+
DEFAULT_PARAMETERS,
|
|
13
15
|
} from '../../constants/constants.js';
|
|
14
16
|
import emptyHooks from '../../util/empty-hooks.js';
|
|
15
17
|
import { sleepForMilliseconds } from '../utilities.js';
|
|
@@ -185,63 +187,66 @@ export default class BlockchainServiceBase {
|
|
|
185
187
|
);
|
|
186
188
|
gasLimit = Math.round(gasLimit * blockchain.gasLimitMultiplier);
|
|
187
189
|
|
|
190
|
+
// Retry bumping is disabled by default. If you want to re-enable it, consider bumping
|
|
191
|
+
// legacy gasPrice or EIP-1559 maxFeePerGas/maxPriorityFeePerGas with retryTxGasPriceMultiplier.
|
|
192
|
+
// Example (legacy-only):
|
|
188
193
|
// let gasPrice;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Theoretically this should never happen
|
|
216
|
-
gasPrice = Math.round(
|
|
217
|
-
(blockchain.gasPrice || (await this.getSmartGasPrice(blockchain))) * blockchain.retryTxGasPriceMultiplier,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
} else {
|
|
221
|
-
gasPrice = blockchain.gasPrice || (await this.getSmartGasPrice(blockchain));
|
|
222
|
-
}
|
|
223
|
-
} else {
|
|
224
|
-
gasPrice = blockchain.gasPrice || (await this.getSmartGasPrice(blockchain));
|
|
225
|
-
}*/
|
|
226
|
-
|
|
227
|
-
const gasPrice = blockchain.gasPrice ?? (await this.getSmartGasPrice(blockchain));
|
|
194
|
+
// if (blockchain.previousTxGasPrice && blockchain.retryTx) {
|
|
195
|
+
// gasPrice = Math.round(blockchain.previousTxGasPrice * blockchain.retryTxGasPriceMultiplier);
|
|
196
|
+
// } else if (blockchain.forceReplaceTxs) {
|
|
197
|
+
// const currentNonce = await web3Instance.eth.getTransactionCount(publicKey, 'pending');
|
|
198
|
+
// const confirmedNonce = await web3Instance.eth.getTransactionCount(publicKey, 'latest');
|
|
199
|
+
// if (currentNonce > confirmedNonce) {
|
|
200
|
+
// const pendingBlock = await web3Instance.eth.getBlock('pending', true);
|
|
201
|
+
// const pendingTx = Object.values(pendingBlock.transactions).find(
|
|
202
|
+
// (tx) => tx.from.toLowerCase() === publicKey.toLowerCase() && tx.nonce === confirmedNonce,
|
|
203
|
+
// );
|
|
204
|
+
// if (pendingTx) {
|
|
205
|
+
// gasPrice = Math.round(Number(pendingTx.gasPrice) * blockchain.retryTxGasPriceMultiplier);
|
|
206
|
+
// } else {
|
|
207
|
+
// gasPrice = Math.round(
|
|
208
|
+
// (blockchain.gasPrice || (await this.getGasPriceWeiWithFallback(blockchain))) *
|
|
209
|
+
// blockchain.retryTxGasPriceMultiplier,
|
|
210
|
+
// );
|
|
211
|
+
// }
|
|
212
|
+
// } else {
|
|
213
|
+
// gasPrice = blockchain.gasPrice || (await this.getGasPriceWeiWithFallback(blockchain));
|
|
214
|
+
// }
|
|
215
|
+
// } else {
|
|
216
|
+
// gasPrice = blockchain.gasPrice || (await this.getGasPriceWeiWithFallback(blockchain));
|
|
217
|
+
// }
|
|
218
|
+
|
|
219
|
+
const gasFeeOptions = await this.getGasFeeOptions(blockchain);
|
|
228
220
|
|
|
229
221
|
if (blockchain.simulateTxs) {
|
|
230
|
-
|
|
222
|
+
const simulationTx = {
|
|
231
223
|
to: contractInstance.options.address,
|
|
232
224
|
data: encodedABI,
|
|
233
225
|
from: publicKey,
|
|
234
|
-
gasPrice,
|
|
235
226
|
gas: gasLimit,
|
|
236
|
-
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if (gasFeeOptions.type === GAS_MODES.EIP1559) {
|
|
230
|
+
simulationTx.maxFeePerGas = gasFeeOptions.maxFeePerGas;
|
|
231
|
+
simulationTx.maxPriorityFeePerGas = gasFeeOptions.maxPriorityFeePerGas;
|
|
232
|
+
} else {
|
|
233
|
+
simulationTx.gasPrice = gasFeeOptions.gasPrice;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await web3Instance.eth.call(simulationTx);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
return {
|
|
240
240
|
from: publicKey,
|
|
241
241
|
to: contractInstance.options.address,
|
|
242
242
|
data: encodedABI,
|
|
243
|
-
gasPrice,
|
|
244
243
|
gas: gasLimit,
|
|
244
|
+
...(gasFeeOptions.type === GAS_MODES.EIP1559
|
|
245
|
+
? {
|
|
246
|
+
maxFeePerGas: gasFeeOptions.maxFeePerGas,
|
|
247
|
+
maxPriorityFeePerGas: gasFeeOptions.maxPriorityFeePerGas,
|
|
248
|
+
}
|
|
249
|
+
: { gasPrice: gasFeeOptions.gasPrice }),
|
|
245
250
|
};
|
|
246
251
|
}
|
|
247
252
|
|
|
@@ -505,6 +510,25 @@ export default class BlockchainServiceBase {
|
|
|
505
510
|
if (requestData?.paymaster && requestData?.paymaster !== ZERO_ADDRESS) {
|
|
506
511
|
// Handle the case when payer is passed
|
|
507
512
|
} else {
|
|
513
|
+
const senderBalance = await this.callContractFunction(
|
|
514
|
+
'Token',
|
|
515
|
+
'balanceOf',
|
|
516
|
+
[sender],
|
|
517
|
+
blockchain,
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
if (BigInt(senderBalance) < BigInt(requestData.tokenAmount)) {
|
|
521
|
+
const balance = Number(senderBalance) / 1e18;
|
|
522
|
+
const required = Number(requestData.tokenAmount) / 1e18;
|
|
523
|
+
|
|
524
|
+
throw new Error(
|
|
525
|
+
`Insufficient TRAC token balance to publish. ` +
|
|
526
|
+
`Wallet ${sender} has ${balance} TRAC, ` +
|
|
527
|
+
`but the publish operation requires ${required} TRAC. ` +
|
|
528
|
+
`Please fund your wallet with more TRAC tokens to proceed.`,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
508
532
|
await this.increaseKnowledgeCollectionAllowance(
|
|
509
533
|
sender,
|
|
510
534
|
requestData.tokenAmount,
|
|
@@ -1402,7 +1426,10 @@ export default class BlockchainServiceBase {
|
|
|
1402
1426
|
try {
|
|
1403
1427
|
// eth_feeHistory params: blockCount, newestBlock, rewardPercentiles
|
|
1404
1428
|
// [50] = median priority fee per block
|
|
1405
|
-
const
|
|
1429
|
+
const priorityFeePercentile = blockchain.priorityFeePercentile ?? 80;
|
|
1430
|
+
const feeHistory = await web3Instance.eth.getFeeHistory(blockCount, 'latest', [
|
|
1431
|
+
priorityFeePercentile,
|
|
1432
|
+
]);
|
|
1406
1433
|
|
|
1407
1434
|
// Extract median priority fees from each block (reward[blockIndex][percentileIndex])
|
|
1408
1435
|
const priorityFees = feeHistory.reward
|
|
@@ -1426,61 +1453,62 @@ export default class BlockchainServiceBase {
|
|
|
1426
1453
|
|
|
1427
1454
|
/**
|
|
1428
1455
|
* Apply buffer percentage to a gas price
|
|
1429
|
-
* @param {BigInt}
|
|
1456
|
+
* @param {BigInt} maxBaseFee - base fee in wei
|
|
1457
|
+
* @param {BigInt} maxPriorityFee - priority fee in wei
|
|
1430
1458
|
* @param {number} gasPriceBufferPercent - Buffer percentage to add
|
|
1431
1459
|
* @returns {BigInt} Gas price with buffer applied
|
|
1432
1460
|
*/
|
|
1433
|
-
applyGasPriceBuffer(
|
|
1434
|
-
if (!gasPriceBufferPercent) return
|
|
1435
|
-
return (
|
|
1461
|
+
applyGasPriceBuffer(maxBaseFee, maxPriorityFee, gasPriceBufferPercent) {
|
|
1462
|
+
if (!gasPriceBufferPercent) return maxBaseFee + maxPriorityFee;
|
|
1463
|
+
return (maxBaseFee * BigInt(100 + Number(gasPriceBufferPercent))) / 100n + maxPriorityFee;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
buildEip1559FeesFromHistory(feeHistory, gasPriceBufferPercent = 0) {
|
|
1467
|
+
const baseFees = Array.from(feeHistory.baseFeePerGas ?? []);
|
|
1468
|
+
const priorityFees = Array.from(feeHistory.priorityFees ?? []);
|
|
1469
|
+
|
|
1470
|
+
if (baseFees.length === 0 || priorityFees.length === 0) {
|
|
1471
|
+
throw new Error('Fee history data is empty');
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const maxBaseFee = baseFees.reduce((max, bf) => (bf > max ? bf : max), 0n);
|
|
1475
|
+
const maxPriorityFeePerGas = priorityFees.reduce((max, pf) => (pf > max ? pf : max), 0n);
|
|
1476
|
+
|
|
1477
|
+
const maxFeePerGas = this.applyGasPriceBuffer(
|
|
1478
|
+
maxBaseFee,
|
|
1479
|
+
maxPriorityFeePerGas,
|
|
1480
|
+
gasPriceBufferPercent,
|
|
1481
|
+
);
|
|
1482
|
+
|
|
1483
|
+
return {
|
|
1484
|
+
maxFeePerGas,
|
|
1485
|
+
maxPriorityFeePerGas,
|
|
1486
|
+
};
|
|
1436
1487
|
}
|
|
1437
1488
|
|
|
1438
1489
|
/**
|
|
1439
|
-
* Estimate safe gas
|
|
1440
|
-
* Takes max base fee from last N blocks, adds a buffer for volatility,
|
|
1441
|
-
* and includes the priority fee (tip) for validator incentive
|
|
1490
|
+
* Estimate safe gas fees using eth_feeHistory (EIP-1559 style)
|
|
1442
1491
|
* @param {Object} blockchain - Blockchain configuration
|
|
1443
|
-
* @returns {Promise<
|
|
1492
|
+
* @returns {Promise<{maxFeePerGas: bigint, maxPriorityFeePerGas: bigint}>}
|
|
1444
1493
|
*/
|
|
1445
|
-
async
|
|
1446
|
-
const { gasPriceBufferPercent } = blockchain;
|
|
1494
|
+
async estimateEip1559Fees(blockchain) {
|
|
1447
1495
|
const feeHistory = await this.getFeeHistory(blockchain, FEE_HISTORY_BLOCK_COUNT);
|
|
1448
|
-
|
|
1449
|
-
// Fallback to network gas price if feeHistory not supported or empty
|
|
1450
1496
|
if (!feeHistory.supported) {
|
|
1451
|
-
|
|
1452
|
-
BigInt(await this.getNetworkGasPrice(blockchain)),
|
|
1453
|
-
gasPriceBufferPercent,
|
|
1454
|
-
);
|
|
1497
|
+
throw new Error('eth_feeHistory not supported');
|
|
1455
1498
|
}
|
|
1456
1499
|
|
|
1457
|
-
|
|
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);
|
|
1500
|
+
return this.buildEip1559FeesFromHistory(feeHistory, blockchain.gasPriceBufferPercent ?? 0);
|
|
1472
1501
|
}
|
|
1473
1502
|
|
|
1474
1503
|
/**
|
|
1475
|
-
* Get gas price
|
|
1476
|
-
* Tries eth_feeHistory first, falls back to legacy methods
|
|
1504
|
+
* Get preferred gas price in wei: try EIP-1559 fee history, fall back to legacy network gas price.
|
|
1477
1505
|
* @param {Object} blockchain - Blockchain configuration
|
|
1478
1506
|
* @returns {Promise<string>} Gas price in wei (as string for web3 compatibility)
|
|
1479
1507
|
*/
|
|
1480
|
-
async
|
|
1508
|
+
async getGasPriceWeiWithFallback(blockchain) {
|
|
1481
1509
|
try {
|
|
1482
|
-
const
|
|
1483
|
-
return
|
|
1510
|
+
const { maxFeePerGas } = await this.estimateEip1559Fees(blockchain);
|
|
1511
|
+
return maxFeePerGas.toString();
|
|
1484
1512
|
} catch (eip1559Error) {
|
|
1485
1513
|
try {
|
|
1486
1514
|
return await this.getNetworkGasPrice(blockchain);
|
|
@@ -1494,6 +1522,59 @@ export default class BlockchainServiceBase {
|
|
|
1494
1522
|
}
|
|
1495
1523
|
}
|
|
1496
1524
|
|
|
1525
|
+
normalizeGasMode(gasMode) {
|
|
1526
|
+
const requested = (gasMode || '').toLowerCase();
|
|
1527
|
+
if (Object.values(GAS_MODES).includes(requested)) {
|
|
1528
|
+
return requested;
|
|
1529
|
+
}
|
|
1530
|
+
return DEFAULT_PARAMETERS.GAS_MODE;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Resolve gas fee fields based on configured gas mode and network support
|
|
1535
|
+
* @param {Object} blockchain - Blockchain configuration
|
|
1536
|
+
* @returns {Promise<Object>} Gas fee fields to merge into tx (legacy or EIP-1559)
|
|
1537
|
+
*/
|
|
1538
|
+
async getGasFeeOptions(blockchain) {
|
|
1539
|
+
const desiredMode = this.normalizeGasMode(blockchain.gasMode);
|
|
1540
|
+
const feeHistory = await this.getFeeHistory(blockchain, FEE_HISTORY_BLOCK_COUNT);
|
|
1541
|
+
const supportsEip1559 =
|
|
1542
|
+
feeHistory.supported &&
|
|
1543
|
+
feeHistory.baseFeePerGas?.length &&
|
|
1544
|
+
feeHistory.priorityFees?.length;
|
|
1545
|
+
|
|
1546
|
+
if (desiredMode === GAS_MODES.EIP1559 && supportsEip1559) {
|
|
1547
|
+
const { maxFeePerGas, maxPriorityFeePerGas } = this.buildEip1559FeesFromHistory(
|
|
1548
|
+
feeHistory,
|
|
1549
|
+
blockchain.gasPriceBufferPercent ?? 0,
|
|
1550
|
+
);
|
|
1551
|
+
|
|
1552
|
+
return {
|
|
1553
|
+
type: GAS_MODES.EIP1559,
|
|
1554
|
+
maxFeePerGas: maxFeePerGas.toString(),
|
|
1555
|
+
maxPriorityFeePerGas: maxPriorityFeePerGas.toString(),
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
if (desiredMode === GAS_MODES.EIP1559 && !supportsEip1559) {
|
|
1560
|
+
// eslint-disable-next-line no-console
|
|
1561
|
+
console.warn(
|
|
1562
|
+
'EIP-1559 gas mode requested but eth_feeHistory is unsupported; skipping feeHistory retry and falling back to legacy gasPrice',
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const legacyGasPrice =
|
|
1567
|
+
blockchain.gasPrice ??
|
|
1568
|
+
(supportsEip1559
|
|
1569
|
+
? await this.getGasPriceWeiWithFallback(blockchain)
|
|
1570
|
+
: await this.getNetworkGasPrice(blockchain));
|
|
1571
|
+
|
|
1572
|
+
return {
|
|
1573
|
+
type: GAS_MODES.LEGACY,
|
|
1574
|
+
gasPrice: legacyGasPrice?.toString?.() ?? legacyGasPrice,
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1497
1578
|
async getWalletBalances(blockchain) {
|
|
1498
1579
|
await this.ensureBlockchainInfo(blockchain);
|
|
1499
1580
|
const web3Instance = await this.getWeb3Instance(blockchain);
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/* eslint-disable no-param-reassign */
|
|
2
2
|
/* eslint-disable no-await-in-loop */
|
|
3
3
|
import Web3 from 'web3';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
TRANSACTION_RETRY_ERRORS,
|
|
6
|
+
WEBSOCKET_PROVIDER_OPTIONS,
|
|
7
|
+
} from '../../../constants/constants.js';
|
|
5
8
|
import BlockchainServiceBase from '../blockchain-service-base.js';
|
|
6
9
|
|
|
7
10
|
export default class NodeBlockchainService extends BlockchainServiceBase {
|
|
@@ -20,6 +23,8 @@ export default class NodeBlockchainService extends BlockchainServiceBase {
|
|
|
20
23
|
};
|
|
21
24
|
},
|
|
22
25
|
);
|
|
26
|
+
|
|
27
|
+
this.nextNonces = new Map();
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
initializeWeb3(blockchainName, blockchainRpc, blockchainOptions) {
|
|
@@ -59,13 +64,30 @@ export default class NodeBlockchainService extends BlockchainServiceBase {
|
|
|
59
64
|
return blockchain?.publicKey;
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
async allocateNonce(blockchain) {
|
|
68
|
+
const address = (await this.getPublicKey(blockchain))?.toLowerCase();
|
|
69
|
+
if (!address) throw new Error('Missing public key for nonce allocation');
|
|
70
|
+
|
|
71
|
+
if (!this.nextNonces.has(address)) {
|
|
72
|
+
const web3Instance = await this.getWeb3Instance(blockchain);
|
|
73
|
+
// Seed the local nonce tracker from the pending nonce to avoid collisions across sequential txs.
|
|
74
|
+
const startingNonce = await web3Instance.eth.getTransactionCount(address, 'pending');
|
|
75
|
+
this.nextNonces.set(address, startingNonce);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const nonce = this.nextNonces.get(address);
|
|
79
|
+
// Increment locally so concurrent sends reuse the monotonic nonce without extra RPC calls.
|
|
80
|
+
this.nextNonces.set(address, nonce + 1);
|
|
81
|
+
return nonce;
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
async executeContractFunction(contractName, functionName, args, blockchain) {
|
|
63
85
|
await this.ensureBlockchainInfo(blockchain);
|
|
64
86
|
const web3Instance = await this.getWeb3Instance(blockchain);
|
|
65
87
|
let contractInstance = await this.getContractInstance(contractName, blockchain);
|
|
66
88
|
|
|
67
89
|
let receipt;
|
|
68
|
-
let
|
|
90
|
+
let lastSentGasPrice;
|
|
69
91
|
let simulationSucceeded = false;
|
|
70
92
|
let transactionRetried = false;
|
|
71
93
|
|
|
@@ -77,17 +99,25 @@ export default class NodeBlockchainService extends BlockchainServiceBase {
|
|
|
77
99
|
args,
|
|
78
100
|
blockchain,
|
|
79
101
|
);
|
|
80
|
-
|
|
102
|
+
const nonce = await this.allocateNonce(blockchain);
|
|
103
|
+
// Track what we sent in case we need to retry without a receipt.
|
|
104
|
+
lastSentGasPrice = tx.gasPrice ?? tx.maxFeePerGas;
|
|
81
105
|
simulationSucceeded = true;
|
|
82
106
|
|
|
83
107
|
const createdTransaction = await web3Instance.eth.accounts.signTransaction(
|
|
84
|
-
tx,
|
|
108
|
+
{ ...tx, nonce },
|
|
85
109
|
blockchain.privateKey,
|
|
86
110
|
);
|
|
87
111
|
|
|
88
112
|
receipt = await web3Instance.eth.sendSignedTransaction(
|
|
89
113
|
createdTransaction.rawTransaction,
|
|
90
114
|
);
|
|
115
|
+
|
|
116
|
+
const actualGasPrice =
|
|
117
|
+
receipt?.effectiveGasPrice ?? receipt?.gasPrice ?? lastSentGasPrice;
|
|
118
|
+
lastSentGasPrice = actualGasPrice;
|
|
119
|
+
blockchain.previousTxGasPrice = actualGasPrice;
|
|
120
|
+
|
|
91
121
|
if (blockchain.name.startsWith('otp') && blockchain.waitNeurowebTxFinalization) {
|
|
92
122
|
receipt = await this.waitForTransactionFinalization(receipt, blockchain);
|
|
93
123
|
}
|
|
@@ -102,7 +132,8 @@ export default class NodeBlockchainService extends BlockchainServiceBase {
|
|
|
102
132
|
) {
|
|
103
133
|
transactionRetried = true;
|
|
104
134
|
blockchain.retryTx = true;
|
|
105
|
-
|
|
135
|
+
// Prefer actual paid price; fall back to what we sent if no receipt.
|
|
136
|
+
blockchain.previousTxGasPrice = lastSentGasPrice;
|
|
106
137
|
} else if (!transactionRetried && /revert|VM Exception/i.test(error.message)) {
|
|
107
138
|
let status;
|
|
108
139
|
try {
|