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.
@@ -207,14 +207,13 @@ export default class AssetOperationsManager {
207
207
  }
208
208
 
209
209
  /**
210
- * Creates a new knowledge collection.
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 to be created, contains public, private or both keys.
213
- * @param {Object} [options={}] - Additional options for knowledge collection creation.
214
- * @param {Object} [stepHooks=emptyHooks] - Hooks to execute during knowledge collection creation.
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 create(content, options = {}, stepHooks = emptyHooks) {
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
- if (
371
- publishOperationResult.status !== OPERATION_STATUSES.COMPLETED &&
372
- !publishOperationResult.data.minAcksReached
373
- ) {
374
- return {
375
- datasetRoot,
376
- operation: {
377
- publish: getOperationStatusObject(publishOperationResult, publishOperationId),
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
- UAL,
497
- datasetRoot,
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
- status:
504
- finalityStatusResult >= minimumNumberOfFinalizationConfirmations
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.1",
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
- /*if (blockchain.previousTxGasPrice && blockchain.retryTx) {
190
- // Increase previous tx gas price by retryTxGasPriceMultiplier
191
- gasPrice = Math.round(blockchain.previousTxGasPrice * blockchain.retryTxGasPriceMultiplier);
192
- } else if (blockchain.forceReplaceTxs) {
193
- // Get the current transaction count (nonce) of the wallet, including pending transactions
194
- const currentNonce = await web3Instance.eth.getTransactionCount(publicKey, 'pending');
195
-
196
- // Get the transaction count of the wallet excluding pending transactions
197
- const confirmedNonce = await web3Instance.eth.getTransactionCount(publicKey, 'latest');
198
-
199
- // If there are any pending transactions
200
- if (currentNonce > confirmedNonce) {
201
- const pendingBlock = await web3Instance.eth.getBlock('pending', true);
202
-
203
- // Search for pending tx in the pending block
204
- const pendingTx = Object.values(pendingBlock.transactions).find(
205
- (tx) =>
206
- tx.from.toLowerCase() === publicKey.toLowerCase() &&
207
- tx.nonce === confirmedNonce,
208
- );
209
-
210
- if (pendingTx) {
211
- // If found, increase gas price of pending tx by retryTxGasPriceMultiplier
212
- gasPrice = Math.round(Number(pendingTx.gasPrice) * blockchain.retryTxGasPriceMultiplier);
213
- } else {
214
- // If not found, use default/network gas price increased by retryTxGasPriceMultiplier
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
- await web3Instance.eth.call({
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 feeHistory = await web3Instance.eth.getFeeHistory(blockCount, 'latest', [50]);
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} gasPrice - Gas price in wei
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(gasPrice, gasPriceBufferPercent) {
1434
- if (!gasPriceBufferPercent) return gasPrice;
1435
- return (gasPrice * BigInt(100 + Number(gasPriceBufferPercent))) / 100n;
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 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
1490
+ * Estimate safe gas fees using eth_feeHistory (EIP-1559 style)
1442
1491
  * @param {Object} blockchain - Blockchain configuration
1443
- * @returns {Promise<BigInt>} Estimated gas price in wei
1492
+ * @returns {Promise<{maxFeePerGas: bigint, maxPriorityFeePerGas: bigint}>}
1444
1493
  */
1445
- async estimateGasPriceFromFeeHistory(blockchain) {
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
- return this.applyGasPriceBuffer(
1452
- BigInt(await this.getNetworkGasPrice(blockchain)),
1453
- gasPriceBufferPercent,
1454
- );
1497
+ throw new Error('eth_feeHistory not supported');
1455
1498
  }
1456
1499
 
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);
1500
+ return this.buildEip1559FeesFromHistory(feeHistory, blockchain.gasPriceBufferPercent ?? 0);
1472
1501
  }
1473
1502
 
1474
1503
  /**
1475
- * Get gas price with EIP-1559 estimation (with fallback)
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 getSmartGasPrice(blockchain) {
1508
+ async getGasPriceWeiWithFallback(blockchain) {
1481
1509
  try {
1482
- const estimatedPrice = await this.estimateGasPriceFromFeeHistory(blockchain);
1483
- return estimatedPrice.toString();
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 { TRANSACTION_RETRY_ERRORS, WEBSOCKET_PROVIDER_OPTIONS } from '../../../constants/constants.js';
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 previousTxGasPrice;
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
- previousTxGasPrice = tx.gasPrice;
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
- blockchain.previousTxGasPrice = previousTxGasPrice;
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 {