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/index.cjs CHANGED
@@ -222,6 +222,11 @@ const GRAPH_STATES = {
222
222
  HISTORICAL: 'HISTORICAL',
223
223
  };
224
224
 
225
+ const GAS_MODES = {
226
+ LEGACY: 'legacy',
227
+ EIP1559: 'eip1559',
228
+ };
229
+
225
230
  const QUERY_TYPES = {
226
231
  CONSTRUCT: 'CONSTRUCT',
227
232
  SELECT: 'SELECT',
@@ -274,6 +279,7 @@ const DEFAULT_PARAMETERS = {
274
279
  FORCE_REPLACE_TXS: false,
275
280
  GAS_LIMIT_MULTIPLIER: 1,
276
281
  RETRY_TX_GAS_PRICE_MULTIPLIER: 3,
282
+ GAS_MODE: GAS_MODES.EIP1559,
277
283
  };
278
284
 
279
285
  const DEFAULT_GAS_PRICE = {
@@ -575,14 +581,13 @@ class AssetOperationsManager {
575
581
  }
576
582
 
577
583
  /**
578
- * Creates a new knowledge collection.
584
+ * Phase 1 of asset creation: validate input, build dataset, and publish to the node.
579
585
  * @async
580
- * @param {Object} content - The content of the knowledge collection to be created, contains public, private or both keys.
581
- * @param {Object} [options={}] - Additional options for knowledge collection creation.
582
- * @param {Object} [stepHooks=emptyHooks] - Hooks to execute during knowledge collection creation.
583
- * @returns {Object} Object containing UAL, publicAssertionId and operation status.
586
+ * @param {Object|string} content - The content of the knowledge collection.
587
+ * @param {Object} [options={}] - Options for knowledge collection creation.
588
+ * @returns {Object} Publish phase output including dataset info and publish operation data.
584
589
  */
585
- async create(content, options = {}, stepHooks = emptyHooks$1) {
590
+ async publishAssetPhase(content, options = {}) {
586
591
  this.validationService.validateJsonldOrNquads(content);
587
592
  const {
588
593
  blockchain,
@@ -735,17 +740,52 @@ class AssetOperationsManager {
735
740
  publishOperationId,
736
741
  );
737
742
 
738
- if (
739
- publishOperationResult.status !== OPERATION_STATUSES$1.COMPLETED &&
740
- !publishOperationResult.data.minAcksReached
741
- ) {
742
- return {
743
- datasetRoot,
744
- operation: {
745
- publish: getOperationStatusObject$1(publishOperationResult, publishOperationId),
746
- },
747
- };
748
- }
743
+ return {
744
+ dataset,
745
+ datasetRoot,
746
+ datasetSize,
747
+ publishOperationId,
748
+ publishOperationResult,
749
+ contentAssetStorageAddress,
750
+ blockchain,
751
+ endpoint,
752
+ port,
753
+ maxNumberOfRetries,
754
+ frequency,
755
+ authToken,
756
+ epochsNum,
757
+ hashFunctionId,
758
+ scoreFunctionId,
759
+ immutable,
760
+ tokenAmount,
761
+ payer,
762
+ minimumNumberOfFinalizationConfirmations,
763
+ minimumNumberOfNodeReplications,
764
+ };
765
+ }
766
+
767
+ /**
768
+ * Phase 2 of asset creation: mint the knowledge collection on chain using publish output.
769
+ * @async
770
+ * @param {Object} publishPayload - Output of publishAssetPhase.
771
+ * @param {Object} [options={}] - Options affecting minting (e.g., minimumBlockConfirmations).
772
+ * @param {Object} [stepHooks=emptyHooks] - Hooks to execute during minting.
773
+ * @returns {Object} Mint phase output including UAL and mint receipt.
774
+ */
775
+ async mintKnowledgeCollectionPhase(publishPayload, options = {}, stepHooks = emptyHooks$1) {
776
+ const {
777
+ dataset,
778
+ datasetRoot,
779
+ datasetSize,
780
+ publishOperationId,
781
+ publishOperationResult,
782
+ contentAssetStorageAddress,
783
+ blockchain,
784
+ epochsNum,
785
+ immutable,
786
+ tokenAmount,
787
+ payer,
788
+ } = publishPayload;
749
789
 
750
790
  const { signatures } = publishOperationResult.data;
751
791
 
@@ -847,6 +887,45 @@ class AssetOperationsManager {
847
887
 
848
888
  const UAL = deriveUAL$1(blockchain.name, contentAssetStorageAddress, knowledgeCollectionId);
849
889
 
890
+ return {
891
+ UAL,
892
+ knowledgeCollectionId,
893
+ mintKnowledgeCollectionReceipt,
894
+ datasetRoot,
895
+ publishOperationId,
896
+ publishOperationResult,
897
+ };
898
+ }
899
+
900
+ /**
901
+ * Phase 3 of asset creation: poll node finality status for the minted asset.
902
+ * @async
903
+ * @param {string} UAL - Universal Asset Locator returned from minting.
904
+ * @param {Object} [options={}] - Finality options.
905
+ * @returns {Object} Finality status details.
906
+ */
907
+ async finalizePublishPhase(UAL, options = {}) {
908
+ // UAL should point to a knowledge collection (kcUAL), not a knowledge asset (kaUAL).
909
+ this.validationService.validateUAL(UAL);
910
+
911
+ const {
912
+ endpoint,
913
+ port,
914
+ maxNumberOfRetries,
915
+ frequency,
916
+ minimumNumberOfFinalizationConfirmations,
917
+ authToken,
918
+ } = this.inputService.getPublishFinalityArguments(options);
919
+
920
+ this.validationService.validatePublishFinality(
921
+ endpoint,
922
+ port,
923
+ maxNumberOfRetries,
924
+ frequency,
925
+ minimumNumberOfFinalizationConfirmations,
926
+ authToken,
927
+ );
928
+
850
929
  let finalityStatusResult = 0;
851
930
  if (minimumNumberOfFinalizationConfirmations > 0) {
852
931
  finalityStatusResult = await this.nodeApiService.finalityStatus(
@@ -861,20 +940,60 @@ class AssetOperationsManager {
861
940
  }
862
941
 
863
942
  return {
864
- UAL,
865
- datasetRoot,
943
+ status:
944
+ finalityStatusResult >= minimumNumberOfFinalizationConfirmations
945
+ ? 'FINALIZED'
946
+ : 'NOT FINALIZED',
947
+ numberOfConfirmations: finalityStatusResult,
948
+ requiredConfirmations: minimumNumberOfFinalizationConfirmations,
949
+ };
950
+ }
951
+
952
+ /**
953
+ * Creates a new knowledge collection.
954
+ * @async
955
+ * @param {Object} content - The content of the knowledge collection to be created, contains public, private or both keys.
956
+ * @param {Object} [options={}] - Additional options for knowledge collection creation.
957
+ * @param {Object} [stepHooks=emptyHooks] - Hooks to execute during knowledge collection creation.
958
+ * @returns {Object} Object containing UAL, publicAssertionId and operation status.
959
+ */
960
+ async create(content, options = {}, stepHooks = emptyHooks$1) {
961
+ const publishOperationOutput = await this.publishAssetPhase(content, options);
962
+ const { datasetRoot, publishOperationId, publishOperationResult } = publishOperationOutput;
963
+
964
+ if (
965
+ publishOperationResult.status !== OPERATION_STATUSES$1.COMPLETED &&
966
+ !publishOperationResult.data.minAcksReached
967
+ ) {
968
+ return {
969
+ datasetRoot,
970
+ operation: {
971
+ publish: getOperationStatusObject$1(publishOperationResult, publishOperationId),
972
+ },
973
+ };
974
+ }
975
+
976
+ const mintOperationOutput = await this.mintKnowledgeCollectionPhase(
977
+ publishOperationOutput,
978
+ options,
979
+ stepHooks,
980
+ );
981
+
982
+ const finalityOperationOutput = await this.finalizePublishPhase(
983
+ mintOperationOutput.UAL,
984
+ options,
985
+ );
986
+
987
+ return {
988
+ UAL: mintOperationOutput.UAL,
989
+ datasetRoot: mintOperationOutput.datasetRoot,
866
990
  signatures: publishOperationResult.data.signatures,
867
991
  operation: {
868
- mintKnowledgeCollection: mintKnowledgeCollectionReceipt,
992
+ mintKnowledgeCollection: mintOperationOutput.mintKnowledgeCollectionReceipt,
869
993
  publish: getOperationStatusObject$1(publishOperationResult, publishOperationId),
870
- finality: {
871
- status:
872
- finalityStatusResult >= minimumNumberOfFinalizationConfirmations
873
- ? 'FINALIZED'
874
- : 'NOT FINALIZED',
875
- },
876
- numberOfConfirmations: finalityStatusResult,
877
- requiredConfirmations: minimumNumberOfFinalizationConfirmations,
994
+ finality: { status: finalityOperationOutput.status },
995
+ numberOfConfirmations: finalityOperationOutput.numberOfConfirmations,
996
+ requiredConfirmations: finalityOperationOutput.requiredConfirmations,
878
997
  },
879
998
  };
880
999
  }
@@ -3113,6 +3232,22 @@ class HttpService {
3113
3232
 
3114
3233
  return response.data.operationId;
3115
3234
  } catch (error) {
3235
+ const status = error?.response?.status;
3236
+ const body = error?.response?.data;
3237
+ const url = `${this.getBaseUrl(endpoint, port)}/publish`;
3238
+ console.error(
3239
+ 'Unable to publish',
3240
+ JSON.stringify(
3241
+ {
3242
+ url,
3243
+ status,
3244
+ body,
3245
+ message: error?.message,
3246
+ },
3247
+ null,
3248
+ 2,
3249
+ ),
3250
+ );
3116
3251
  throw Error(`Unable to publish: ${error.message}`);
3117
3252
  }
3118
3253
  }
@@ -3510,63 +3645,66 @@ class BlockchainServiceBase {
3510
3645
  );
3511
3646
  gasLimit = Math.round(gasLimit * blockchain.gasLimitMultiplier);
3512
3647
 
3648
+ // Retry bumping is disabled by default. If you want to re-enable it, consider bumping
3649
+ // legacy gasPrice or EIP-1559 maxFeePerGas/maxPriorityFeePerGas with retryTxGasPriceMultiplier.
3650
+ // Example (legacy-only):
3513
3651
  // let gasPrice;
3514
- /*if (blockchain.previousTxGasPrice && blockchain.retryTx) {
3515
- // Increase previous tx gas price by retryTxGasPriceMultiplier
3516
- gasPrice = Math.round(blockchain.previousTxGasPrice * blockchain.retryTxGasPriceMultiplier);
3517
- } else if (blockchain.forceReplaceTxs) {
3518
- // Get the current transaction count (nonce) of the wallet, including pending transactions
3519
- const currentNonce = await web3Instance.eth.getTransactionCount(publicKey, 'pending');
3520
-
3521
- // Get the transaction count of the wallet excluding pending transactions
3522
- const confirmedNonce = await web3Instance.eth.getTransactionCount(publicKey, 'latest');
3523
-
3524
- // If there are any pending transactions
3525
- if (currentNonce > confirmedNonce) {
3526
- const pendingBlock = await web3Instance.eth.getBlock('pending', true);
3527
-
3528
- // Search for pending tx in the pending block
3529
- const pendingTx = Object.values(pendingBlock.transactions).find(
3530
- (tx) =>
3531
- tx.from.toLowerCase() === publicKey.toLowerCase() &&
3532
- tx.nonce === confirmedNonce,
3533
- );
3534
-
3535
- if (pendingTx) {
3536
- // If found, increase gas price of pending tx by retryTxGasPriceMultiplier
3537
- gasPrice = Math.round(Number(pendingTx.gasPrice) * blockchain.retryTxGasPriceMultiplier);
3538
- } else {
3539
- // If not found, use default/network gas price increased by retryTxGasPriceMultiplier
3540
- // Theoretically this should never happen
3541
- gasPrice = Math.round(
3542
- (blockchain.gasPrice || (await this.getSmartGasPrice(blockchain))) * blockchain.retryTxGasPriceMultiplier,
3543
- );
3544
- }
3545
- } else {
3546
- gasPrice = blockchain.gasPrice || (await this.getSmartGasPrice(blockchain));
3547
- }
3548
- } else {
3549
- gasPrice = blockchain.gasPrice || (await this.getSmartGasPrice(blockchain));
3550
- }*/
3652
+ // if (blockchain.previousTxGasPrice && blockchain.retryTx) {
3653
+ // gasPrice = Math.round(blockchain.previousTxGasPrice * blockchain.retryTxGasPriceMultiplier);
3654
+ // } else if (blockchain.forceReplaceTxs) {
3655
+ // const currentNonce = await web3Instance.eth.getTransactionCount(publicKey, 'pending');
3656
+ // const confirmedNonce = await web3Instance.eth.getTransactionCount(publicKey, 'latest');
3657
+ // if (currentNonce > confirmedNonce) {
3658
+ // const pendingBlock = await web3Instance.eth.getBlock('pending', true);
3659
+ // const pendingTx = Object.values(pendingBlock.transactions).find(
3660
+ // (tx) => tx.from.toLowerCase() === publicKey.toLowerCase() && tx.nonce === confirmedNonce,
3661
+ // );
3662
+ // if (pendingTx) {
3663
+ // gasPrice = Math.round(Number(pendingTx.gasPrice) * blockchain.retryTxGasPriceMultiplier);
3664
+ // } else {
3665
+ // gasPrice = Math.round(
3666
+ // (blockchain.gasPrice || (await this.getGasPriceWeiWithFallback(blockchain))) *
3667
+ // blockchain.retryTxGasPriceMultiplier,
3668
+ // );
3669
+ // }
3670
+ // } else {
3671
+ // gasPrice = blockchain.gasPrice || (await this.getGasPriceWeiWithFallback(blockchain));
3672
+ // }
3673
+ // } else {
3674
+ // gasPrice = blockchain.gasPrice || (await this.getGasPriceWeiWithFallback(blockchain));
3675
+ // }
3551
3676
 
3552
- const gasPrice = blockchain.gasPrice ?? (await this.getSmartGasPrice(blockchain));
3677
+ const gasFeeOptions = await this.getGasFeeOptions(blockchain);
3553
3678
 
3554
3679
  if (blockchain.simulateTxs) {
3555
- await web3Instance.eth.call({
3680
+ const simulationTx = {
3556
3681
  to: contractInstance.options.address,
3557
3682
  data: encodedABI,
3558
3683
  from: publicKey,
3559
- gasPrice,
3560
3684
  gas: gasLimit,
3561
- });
3685
+ };
3686
+
3687
+ if (gasFeeOptions.type === GAS_MODES.EIP1559) {
3688
+ simulationTx.maxFeePerGas = gasFeeOptions.maxFeePerGas;
3689
+ simulationTx.maxPriorityFeePerGas = gasFeeOptions.maxPriorityFeePerGas;
3690
+ } else {
3691
+ simulationTx.gasPrice = gasFeeOptions.gasPrice;
3692
+ }
3693
+
3694
+ await web3Instance.eth.call(simulationTx);
3562
3695
  }
3563
3696
 
3564
3697
  return {
3565
3698
  from: publicKey,
3566
3699
  to: contractInstance.options.address,
3567
3700
  data: encodedABI,
3568
- gasPrice,
3569
3701
  gas: gasLimit,
3702
+ ...(gasFeeOptions.type === GAS_MODES.EIP1559
3703
+ ? {
3704
+ maxFeePerGas: gasFeeOptions.maxFeePerGas,
3705
+ maxPriorityFeePerGas: gasFeeOptions.maxPriorityFeePerGas,
3706
+ }
3707
+ : { gasPrice: gasFeeOptions.gasPrice }),
3570
3708
  };
3571
3709
  }
3572
3710
 
@@ -3830,6 +3968,25 @@ class BlockchainServiceBase {
3830
3968
  if (requestData?.paymaster && requestData?.paymaster !== ZERO_ADDRESS) {
3831
3969
  // Handle the case when payer is passed
3832
3970
  } else {
3971
+ const senderBalance = await this.callContractFunction(
3972
+ 'Token',
3973
+ 'balanceOf',
3974
+ [sender],
3975
+ blockchain,
3976
+ );
3977
+
3978
+ if (BigInt(senderBalance) < BigInt(requestData.tokenAmount)) {
3979
+ const balance = Number(senderBalance) / 1e18;
3980
+ const required = Number(requestData.tokenAmount) / 1e18;
3981
+
3982
+ throw new Error(
3983
+ `Insufficient TRAC token balance to publish. ` +
3984
+ `Wallet ${sender} has ${balance} TRAC, ` +
3985
+ `but the publish operation requires ${required} TRAC. ` +
3986
+ `Please fund your wallet with more TRAC tokens to proceed.`,
3987
+ );
3988
+ }
3989
+
3833
3990
  await this.increaseKnowledgeCollectionAllowance(
3834
3991
  sender,
3835
3992
  requestData.tokenAmount,
@@ -4727,7 +4884,10 @@ class BlockchainServiceBase {
4727
4884
  try {
4728
4885
  // eth_feeHistory params: blockCount, newestBlock, rewardPercentiles
4729
4886
  // [50] = median priority fee per block
4730
- const feeHistory = await web3Instance.eth.getFeeHistory(blockCount, 'latest', [50]);
4887
+ const priorityFeePercentile = blockchain.priorityFeePercentile ?? 80;
4888
+ const feeHistory = await web3Instance.eth.getFeeHistory(blockCount, 'latest', [
4889
+ priorityFeePercentile,
4890
+ ]);
4731
4891
 
4732
4892
  // Extract median priority fees from each block (reward[blockIndex][percentileIndex])
4733
4893
  const priorityFees = feeHistory.reward
@@ -4751,61 +4911,62 @@ class BlockchainServiceBase {
4751
4911
 
4752
4912
  /**
4753
4913
  * Apply buffer percentage to a gas price
4754
- * @param {BigInt} gasPrice - Gas price in wei
4914
+ * @param {BigInt} maxBaseFee - base fee in wei
4915
+ * @param {BigInt} maxPriorityFee - priority fee in wei
4755
4916
  * @param {number} gasPriceBufferPercent - Buffer percentage to add
4756
4917
  * @returns {BigInt} Gas price with buffer applied
4757
4918
  */
4758
- applyGasPriceBuffer(gasPrice, gasPriceBufferPercent) {
4759
- if (!gasPriceBufferPercent) return gasPrice;
4760
- return (gasPrice * BigInt(100 + Number(gasPriceBufferPercent))) / 100n;
4919
+ applyGasPriceBuffer(maxBaseFee, maxPriorityFee, gasPriceBufferPercent) {
4920
+ if (!gasPriceBufferPercent) return maxBaseFee + maxPriorityFee;
4921
+ return (maxBaseFee * BigInt(100 + Number(gasPriceBufferPercent))) / 100n + maxPriorityFee;
4922
+ }
4923
+
4924
+ buildEip1559FeesFromHistory(feeHistory, gasPriceBufferPercent = 0) {
4925
+ const baseFees = Array.from(feeHistory.baseFeePerGas ?? []);
4926
+ const priorityFees = Array.from(feeHistory.priorityFees ?? []);
4927
+
4928
+ if (baseFees.length === 0 || priorityFees.length === 0) {
4929
+ throw new Error('Fee history data is empty');
4930
+ }
4931
+
4932
+ const maxBaseFee = baseFees.reduce((max, bf) => (bf > max ? bf : max), 0n);
4933
+ const maxPriorityFeePerGas = priorityFees.reduce((max, pf) => (pf > max ? pf : max), 0n);
4934
+
4935
+ const maxFeePerGas = this.applyGasPriceBuffer(
4936
+ maxBaseFee,
4937
+ maxPriorityFeePerGas,
4938
+ gasPriceBufferPercent,
4939
+ );
4940
+
4941
+ return {
4942
+ maxFeePerGas,
4943
+ maxPriorityFeePerGas,
4944
+ };
4761
4945
  }
4762
4946
 
4763
4947
  /**
4764
- * Estimate safe gas price using eth_feeHistory (EIP-1559 style)
4765
- * Takes max base fee from last N blocks, adds a buffer for volatility,
4766
- * and includes the priority fee (tip) for validator incentive
4948
+ * Estimate safe gas fees using eth_feeHistory (EIP-1559 style)
4767
4949
  * @param {Object} blockchain - Blockchain configuration
4768
- * @returns {Promise<BigInt>} Estimated gas price in wei
4950
+ * @returns {Promise<{maxFeePerGas: bigint, maxPriorityFeePerGas: bigint}>}
4769
4951
  */
4770
- async estimateGasPriceFromFeeHistory(blockchain) {
4771
- const { gasPriceBufferPercent } = blockchain;
4952
+ async estimateEip1559Fees(blockchain) {
4772
4953
  const feeHistory = await this.getFeeHistory(blockchain, FEE_HISTORY_BLOCK_COUNT);
4773
-
4774
- // Fallback to network gas price if feeHistory not supported or empty
4775
4954
  if (!feeHistory.supported) {
4776
- return this.applyGasPriceBuffer(
4777
- BigInt(await this.getNetworkGasPrice(blockchain)),
4778
- gasPriceBufferPercent,
4779
- );
4780
- }
4781
-
4782
- const baseFees = Array.from(feeHistory.baseFeePerGas);
4783
- const priorityFees = Array.from(feeHistory.priorityFees);
4784
-
4785
- if (baseFees.length === 0 || priorityFees.length === 0) {
4786
- return this.applyGasPriceBuffer(
4787
- BigInt(await this.getNetworkGasPrice(blockchain)),
4788
- gasPriceBufferPercent,
4789
- );
4955
+ throw new Error('eth_feeHistory not supported');
4790
4956
  }
4791
4957
 
4792
- // Find max base fee and priority fee from recent blocks
4793
- const maxBaseFee = baseFees.reduce((max, bf) => (bf > max ? bf : max), 0n);
4794
- const maxPriorityFee = priorityFees.reduce((max, pf) => (pf > max ? pf : max), 0n);
4795
-
4796
- return this.applyGasPriceBuffer(maxBaseFee + maxPriorityFee, gasPriceBufferPercent);
4958
+ return this.buildEip1559FeesFromHistory(feeHistory, blockchain.gasPriceBufferPercent ?? 0);
4797
4959
  }
4798
4960
 
4799
4961
  /**
4800
- * Get gas price with EIP-1559 estimation (with fallback)
4801
- * Tries eth_feeHistory first, falls back to legacy methods
4962
+ * Get preferred gas price in wei: try EIP-1559 fee history, fall back to legacy network gas price.
4802
4963
  * @param {Object} blockchain - Blockchain configuration
4803
4964
  * @returns {Promise<string>} Gas price in wei (as string for web3 compatibility)
4804
4965
  */
4805
- async getSmartGasPrice(blockchain) {
4966
+ async getGasPriceWeiWithFallback(blockchain) {
4806
4967
  try {
4807
- const estimatedPrice = await this.estimateGasPriceFromFeeHistory(blockchain);
4808
- return estimatedPrice.toString();
4968
+ const { maxFeePerGas } = await this.estimateEip1559Fees(blockchain);
4969
+ return maxFeePerGas.toString();
4809
4970
  } catch (eip1559Error) {
4810
4971
  try {
4811
4972
  return await this.getNetworkGasPrice(blockchain);
@@ -4819,6 +4980,59 @@ class BlockchainServiceBase {
4819
4980
  }
4820
4981
  }
4821
4982
 
4983
+ normalizeGasMode(gasMode) {
4984
+ const requested = (gasMode || '').toLowerCase();
4985
+ if (Object.values(GAS_MODES).includes(requested)) {
4986
+ return requested;
4987
+ }
4988
+ return DEFAULT_PARAMETERS.GAS_MODE;
4989
+ }
4990
+
4991
+ /**
4992
+ * Resolve gas fee fields based on configured gas mode and network support
4993
+ * @param {Object} blockchain - Blockchain configuration
4994
+ * @returns {Promise<Object>} Gas fee fields to merge into tx (legacy or EIP-1559)
4995
+ */
4996
+ async getGasFeeOptions(blockchain) {
4997
+ const desiredMode = this.normalizeGasMode(blockchain.gasMode);
4998
+ const feeHistory = await this.getFeeHistory(blockchain, FEE_HISTORY_BLOCK_COUNT);
4999
+ const supportsEip1559 =
5000
+ feeHistory.supported &&
5001
+ feeHistory.baseFeePerGas?.length &&
5002
+ feeHistory.priorityFees?.length;
5003
+
5004
+ if (desiredMode === GAS_MODES.EIP1559 && supportsEip1559) {
5005
+ const { maxFeePerGas, maxPriorityFeePerGas } = this.buildEip1559FeesFromHistory(
5006
+ feeHistory,
5007
+ blockchain.gasPriceBufferPercent ?? 0,
5008
+ );
5009
+
5010
+ return {
5011
+ type: GAS_MODES.EIP1559,
5012
+ maxFeePerGas: maxFeePerGas.toString(),
5013
+ maxPriorityFeePerGas: maxPriorityFeePerGas.toString(),
5014
+ };
5015
+ }
5016
+
5017
+ if (desiredMode === GAS_MODES.EIP1559 && !supportsEip1559) {
5018
+ // eslint-disable-next-line no-console
5019
+ console.warn(
5020
+ 'EIP-1559 gas mode requested but eth_feeHistory is unsupported; skipping feeHistory retry and falling back to legacy gasPrice',
5021
+ );
5022
+ }
5023
+
5024
+ const legacyGasPrice =
5025
+ blockchain.gasPrice ??
5026
+ (supportsEip1559
5027
+ ? await this.getGasPriceWeiWithFallback(blockchain)
5028
+ : await this.getNetworkGasPrice(blockchain));
5029
+
5030
+ return {
5031
+ type: GAS_MODES.LEGACY,
5032
+ gasPrice: legacyGasPrice?.toString?.() ?? legacyGasPrice,
5033
+ };
5034
+ }
5035
+
4822
5036
  async getWalletBalances(blockchain) {
4823
5037
  await this.ensureBlockchainInfo(blockchain);
4824
5038
  const web3Instance = await this.getWeb3Instance(blockchain);
@@ -5051,6 +5265,8 @@ class NodeBlockchainService extends BlockchainServiceBase {
5051
5265
  };
5052
5266
  },
5053
5267
  );
5268
+
5269
+ this.nextNonces = new Map();
5054
5270
  }
5055
5271
 
5056
5272
  initializeWeb3(blockchainName, blockchainRpc, blockchainOptions) {
@@ -5090,13 +5306,30 @@ class NodeBlockchainService extends BlockchainServiceBase {
5090
5306
  return blockchain?.publicKey;
5091
5307
  }
5092
5308
 
5309
+ async allocateNonce(blockchain) {
5310
+ const address = (await this.getPublicKey(blockchain))?.toLowerCase();
5311
+ if (!address) throw new Error('Missing public key for nonce allocation');
5312
+
5313
+ if (!this.nextNonces.has(address)) {
5314
+ const web3Instance = await this.getWeb3Instance(blockchain);
5315
+ // Seed the local nonce tracker from the pending nonce to avoid collisions across sequential txs.
5316
+ const startingNonce = await web3Instance.eth.getTransactionCount(address, 'pending');
5317
+ this.nextNonces.set(address, startingNonce);
5318
+ }
5319
+
5320
+ const nonce = this.nextNonces.get(address);
5321
+ // Increment locally so concurrent sends reuse the monotonic nonce without extra RPC calls.
5322
+ this.nextNonces.set(address, nonce + 1);
5323
+ return nonce;
5324
+ }
5325
+
5093
5326
  async executeContractFunction(contractName, functionName, args, blockchain) {
5094
5327
  await this.ensureBlockchainInfo(blockchain);
5095
5328
  const web3Instance = await this.getWeb3Instance(blockchain);
5096
5329
  let contractInstance = await this.getContractInstance(contractName, blockchain);
5097
5330
 
5098
5331
  let receipt;
5099
- let previousTxGasPrice;
5332
+ let lastSentGasPrice;
5100
5333
  let simulationSucceeded = false;
5101
5334
  let transactionRetried = false;
5102
5335
 
@@ -5108,17 +5341,25 @@ class NodeBlockchainService extends BlockchainServiceBase {
5108
5341
  args,
5109
5342
  blockchain,
5110
5343
  );
5111
- previousTxGasPrice = tx.gasPrice;
5344
+ const nonce = await this.allocateNonce(blockchain);
5345
+ // Track what we sent in case we need to retry without a receipt.
5346
+ lastSentGasPrice = tx.gasPrice ?? tx.maxFeePerGas;
5112
5347
  simulationSucceeded = true;
5113
5348
 
5114
5349
  const createdTransaction = await web3Instance.eth.accounts.signTransaction(
5115
- tx,
5350
+ { ...tx, nonce },
5116
5351
  blockchain.privateKey,
5117
5352
  );
5118
5353
 
5119
5354
  receipt = await web3Instance.eth.sendSignedTransaction(
5120
5355
  createdTransaction.rawTransaction,
5121
5356
  );
5357
+
5358
+ const actualGasPrice =
5359
+ receipt?.effectiveGasPrice ?? receipt?.gasPrice ?? lastSentGasPrice;
5360
+ lastSentGasPrice = actualGasPrice;
5361
+ blockchain.previousTxGasPrice = actualGasPrice;
5362
+
5122
5363
  if (blockchain.name.startsWith('otp') && blockchain.waitNeurowebTxFinalization) {
5123
5364
  receipt = await this.waitForTransactionFinalization(receipt, blockchain);
5124
5365
  }
@@ -5133,7 +5374,8 @@ class NodeBlockchainService extends BlockchainServiceBase {
5133
5374
  ) {
5134
5375
  transactionRetried = true;
5135
5376
  blockchain.retryTx = true;
5136
- blockchain.previousTxGasPrice = previousTxGasPrice;
5377
+ // Prefer actual paid price; fall back to what we sent if no receipt.
5378
+ blockchain.previousTxGasPrice = lastSentGasPrice;
5137
5379
  } else if (!transactionRetried && /revert|VM Exception/i.test(error.message)) {
5138
5380
  let status;
5139
5381
  try {
@@ -6231,12 +6473,29 @@ class InputService {
6231
6473
  BLOCKCHAINS[environment][name]?.gasPriceOracleLink ??
6232
6474
  undefined;
6233
6475
 
6476
+ const getEnvGasMode = () =>
6477
+ typeof process !== 'undefined' && process?.env ? process.env.DKG_GAS_MODE : undefined;
6478
+
6479
+ const requestedGasMode =
6480
+ options.blockchain?.gasMode ??
6481
+ this.config.blockchain?.gasMode ??
6482
+ getEnvGasMode() ??
6483
+ DEFAULT_PARAMETERS.GAS_MODE;
6484
+ const normalizedRequestedGasMode = (requestedGasMode || '').toLowerCase();
6485
+ const normalizedGasMode = Object.values(GAS_MODES).includes(normalizedRequestedGasMode)
6486
+ ? normalizedRequestedGasMode
6487
+ : DEFAULT_PARAMETERS.GAS_MODE;
6488
+
6234
6489
  const maxAllowance =
6235
6490
  options.blockchain?.maxAllowance ?? this.config.blockchain?.maxAllowance ?? undefined;
6236
6491
  const gasPriceBufferPercent =
6237
6492
  options.blockchain?.gasPriceBufferPercent ??
6238
6493
  this.config.blockchain?.gasPriceBufferPercent ??
6239
6494
  undefined;
6495
+ const priorityFeePercentile =
6496
+ options.blockchain?.priorityFeePercentile ??
6497
+ this.config.blockchain?.priorityFeePercentile ??
6498
+ undefined;
6240
6499
  const retryTxGasPriceMultiplier =
6241
6500
  options.blockchain?.retryTxGasPriceMultiplier ??
6242
6501
  this.config.blockchain?.retryTxGasPriceMultiplier ??
@@ -6255,8 +6514,10 @@ class InputService {
6255
6514
  simulateTxs,
6256
6515
  forceReplaceTxs,
6257
6516
  gasPriceOracleLink,
6517
+ gasMode: normalizedGasMode,
6258
6518
  maxAllowance,
6259
6519
  gasPriceBufferPercent,
6520
+ priorityFeePercentile,
6260
6521
  retryTxGasPriceMultiplier,
6261
6522
  };
6262
6523