diff --git a/constants/constants.js b/constants/constants.js index 6600366c..45ad7256 100644 --- a/constants/constants.js +++ b/constants/constants.js @@ -161,6 +161,11 @@ export const GRAPH_STATES = { HISTORICAL: 'HISTORICAL', }; +export const GAS_MODES = { + LEGACY: 'legacy', + EIP1559: 'eip1559', +}; + export const OT_NODE_TRIPLE_STORE_REPOSITORIES = { PUBLIC_CURRENT: 'publicCurrent', PUBLIC_HISTORY: 'publicHistory', @@ -220,6 +225,7 @@ export const DEFAULT_PARAMETERS = { FORCE_REPLACE_TXS: false, GAS_LIMIT_MULTIPLIER: 1, RETRY_TX_GAS_PRICE_MULTIPLIER: 3, + GAS_MODE: GAS_MODES.EIP1559, }; export const DEFAULT_GAS_PRICE = { diff --git a/managers/asset-operations-manager.js b/managers/asset-operations-manager.js index d4391294..01aab1cd 100644 --- a/managers/asset-operations-manager.js +++ b/managers/asset-operations-manager.js @@ -207,14 +207,13 @@ export default class AssetOperationsManager { } /** - * Creates a new knowledge collection. + * Phase 1 of asset creation: validate input, build dataset, and publish to the node. * @async - * @param {Object} content - The content of the knowledge collection to be created, contains public, private or both keys. - * @param {Object} [options={}] - Additional options for knowledge collection creation. - * @param {Object} [stepHooks=emptyHooks] - Hooks to execute during knowledge collection creation. - * @returns {Object} Object containing UAL, publicAssertionId and operation status. + * @param {Object|string} content - The content of the knowledge collection. + * @param {Object} [options={}] - Options for knowledge collection creation. + * @returns {Object} Publish phase output including dataset info and publish operation data. */ - async create(content, options = {}, stepHooks = emptyHooks) { + async publishAssetPhase(content, options = {}) { this.validationService.validateJsonldOrNquads(content); const { blockchain, @@ -367,17 +366,52 @@ export default class AssetOperationsManager { publishOperationId, ); - if ( - publishOperationResult.status !== OPERATION_STATUSES.COMPLETED && - !publishOperationResult.data.minAcksReached - ) { - return { - datasetRoot, - operation: { - publish: getOperationStatusObject(publishOperationResult, publishOperationId), - }, - }; - } + return { + dataset, + datasetRoot, + datasetSize, + publishOperationId, + publishOperationResult, + contentAssetStorageAddress, + blockchain, + endpoint, + port, + maxNumberOfRetries, + frequency, + authToken, + epochsNum, + hashFunctionId, + scoreFunctionId, + immutable, + tokenAmount, + payer, + minimumNumberOfFinalizationConfirmations, + minimumNumberOfNodeReplications, + }; + } + + /** + * Phase 2 of asset creation: mint the knowledge collection on chain using publish output. + * @async + * @param {Object} publishPayload - Output of publishAssetPhase. + * @param {Object} [options={}] - Options affecting minting (e.g., minimumBlockConfirmations). + * @param {Object} [stepHooks=emptyHooks] - Hooks to execute during minting. + * @returns {Object} Mint phase output including UAL and mint receipt. + */ + async mintKnowledgeCollectionPhase(publishPayload, options = {}, stepHooks = emptyHooks) { + const { + dataset, + datasetRoot, + datasetSize, + publishOperationId, + publishOperationResult, + contentAssetStorageAddress, + blockchain, + epochsNum, + immutable, + tokenAmount, + payer, + } = publishPayload; const { signatures } = publishOperationResult.data; @@ -479,6 +513,45 @@ export default class AssetOperationsManager { const UAL = deriveUAL(blockchain.name, contentAssetStorageAddress, knowledgeCollectionId); + return { + UAL, + knowledgeCollectionId, + mintKnowledgeCollectionReceipt, + datasetRoot, + publishOperationId, + publishOperationResult, + }; + } + + /** + * Phase 3 of asset creation: poll node finality status for the minted asset. + * @async + * @param {string} UAL - Universal Asset Locator returned from minting. + * @param {Object} [options={}] - Finality options. + * @returns {Object} Finality status details. + */ + async finalizePublishPhase(UAL, options = {}) { + // UAL should point to a knowledge collection (kcUAL), not a knowledge asset (kaUAL). + this.validationService.validateUAL(UAL); + + const { + endpoint, + port, + maxNumberOfRetries, + frequency, + minimumNumberOfFinalizationConfirmations, + authToken, + } = this.inputService.getPublishFinalityArguments(options); + + this.validationService.validatePublishFinality( + endpoint, + port, + maxNumberOfRetries, + frequency, + minimumNumberOfFinalizationConfirmations, + authToken, + ); + let finalityStatusResult = 0; if (minimumNumberOfFinalizationConfirmations > 0) { finalityStatusResult = await this.nodeApiService.finalityStatus( @@ -493,20 +566,60 @@ export default class AssetOperationsManager { } return { - UAL, - datasetRoot, + status: + finalityStatusResult >= minimumNumberOfFinalizationConfirmations + ? 'FINALIZED' + : 'NOT FINALIZED', + numberOfConfirmations: finalityStatusResult, + requiredConfirmations: minimumNumberOfFinalizationConfirmations, + }; + } + + /** + * Creates a new knowledge collection. + * @async + * @param {Object} content - The content of the knowledge collection to be created, contains public, private or both keys. + * @param {Object} [options={}] - Additional options for knowledge collection creation. + * @param {Object} [stepHooks=emptyHooks] - Hooks to execute during knowledge collection creation. + * @returns {Object} Object containing UAL, publicAssertionId and operation status. + */ + async create(content, options = {}, stepHooks = emptyHooks) { + const publishOperationOutput = await this.publishAssetPhase(content, options); + const { datasetRoot, publishOperationId, publishOperationResult } = publishOperationOutput; + + if ( + publishOperationResult.status !== OPERATION_STATUSES.COMPLETED && + !publishOperationResult.data.minAcksReached + ) { + return { + datasetRoot, + operation: { + publish: getOperationStatusObject(publishOperationResult, publishOperationId), + }, + }; + } + + const mintOperationOutput = await this.mintKnowledgeCollectionPhase( + publishOperationOutput, + options, + stepHooks, + ); + + const finalityOperationOutput = await this.finalizePublishPhase( + mintOperationOutput.UAL, + options, + ); + + return { + UAL: mintOperationOutput.UAL, + datasetRoot: mintOperationOutput.datasetRoot, signatures: publishOperationResult.data.signatures, operation: { - mintKnowledgeCollection: mintKnowledgeCollectionReceipt, + mintKnowledgeCollection: mintOperationOutput.mintKnowledgeCollectionReceipt, publish: getOperationStatusObject(publishOperationResult, publishOperationId), - finality: { - status: - finalityStatusResult >= minimumNumberOfFinalizationConfirmations - ? 'FINALIZED' - : 'NOT FINALIZED', - }, - numberOfConfirmations: finalityStatusResult, - requiredConfirmations: minimumNumberOfFinalizationConfirmations, + finality: { status: finalityOperationOutput.status }, + numberOfConfirmations: finalityOperationOutput.numberOfConfirmations, + requiredConfirmations: finalityOperationOutput.requiredConfirmations, }, }; } diff --git a/package-lock.json b/package-lock.json index ab03b50c..f236d1d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dkg.js", - "version": "8.2.2", + "version": "8.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dkg.js", - "version": "8.2.2", + "version": "8.2.3", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 7419d0db..febbe4d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dkg.js", - "version": "8.2.2", + "version": "8.2.3", "description": "Javascript library for interaction with the OriginTrail Decentralized Knowledge Graph", "main": "index.js", "exports": { diff --git a/services/blockchain-service/blockchain-service-base.js b/services/blockchain-service/blockchain-service-base.js index cf8da638..f8936b1a 100644 --- a/services/blockchain-service/blockchain-service-base.js +++ b/services/blockchain-service/blockchain-service-base.js @@ -10,6 +10,8 @@ import { ZERO_ADDRESS, NEUROWEB_INCENTIVE_TYPE_CHAINS, FEE_HISTORY_BLOCK_COUNT, + GAS_MODES, + DEFAULT_PARAMETERS, } from '../../constants/constants.js'; import emptyHooks from '../../util/empty-hooks.js'; import { sleepForMilliseconds } from '../utilities.js'; @@ -185,63 +187,66 @@ export default class BlockchainServiceBase { ); gasLimit = Math.round(gasLimit * blockchain.gasLimitMultiplier); + // Retry bumping is disabled by default. If you want to re-enable it, consider bumping + // legacy gasPrice or EIP-1559 maxFeePerGas/maxPriorityFeePerGas with retryTxGasPriceMultiplier. + // Example (legacy-only): // let gasPrice; - /*if (blockchain.previousTxGasPrice && blockchain.retryTx) { - // Increase previous tx gas price by retryTxGasPriceMultiplier - gasPrice = Math.round(blockchain.previousTxGasPrice * blockchain.retryTxGasPriceMultiplier); - } else if (blockchain.forceReplaceTxs) { - // Get the current transaction count (nonce) of the wallet, including pending transactions - const currentNonce = await web3Instance.eth.getTransactionCount(publicKey, 'pending'); - - // Get the transaction count of the wallet excluding pending transactions - const confirmedNonce = await web3Instance.eth.getTransactionCount(publicKey, 'latest'); - - // If there are any pending transactions - if (currentNonce > confirmedNonce) { - const pendingBlock = await web3Instance.eth.getBlock('pending', true); - - // Search for pending tx in the pending block - const pendingTx = Object.values(pendingBlock.transactions).find( - (tx) => - tx.from.toLowerCase() === publicKey.toLowerCase() && - tx.nonce === confirmedNonce, - ); - - if (pendingTx) { - // If found, increase gas price of pending tx by retryTxGasPriceMultiplier - gasPrice = Math.round(Number(pendingTx.gasPrice) * blockchain.retryTxGasPriceMultiplier); - } else { - // If not found, use default/network gas price increased by retryTxGasPriceMultiplier - // Theoretically this should never happen - gasPrice = Math.round( - (blockchain.gasPrice || (await this.getSmartGasPrice(blockchain))) * blockchain.retryTxGasPriceMultiplier, - ); - } - } else { - gasPrice = blockchain.gasPrice || (await this.getSmartGasPrice(blockchain)); - } - } else { - gasPrice = blockchain.gasPrice || (await this.getSmartGasPrice(blockchain)); - }*/ - - const gasPrice = blockchain.gasPrice ?? (await this.getSmartGasPrice(blockchain)); + // if (blockchain.previousTxGasPrice && blockchain.retryTx) { + // gasPrice = Math.round(blockchain.previousTxGasPrice * blockchain.retryTxGasPriceMultiplier); + // } else if (blockchain.forceReplaceTxs) { + // const currentNonce = await web3Instance.eth.getTransactionCount(publicKey, 'pending'); + // const confirmedNonce = await web3Instance.eth.getTransactionCount(publicKey, 'latest'); + // if (currentNonce > confirmedNonce) { + // const pendingBlock = await web3Instance.eth.getBlock('pending', true); + // const pendingTx = Object.values(pendingBlock.transactions).find( + // (tx) => tx.from.toLowerCase() === publicKey.toLowerCase() && tx.nonce === confirmedNonce, + // ); + // if (pendingTx) { + // gasPrice = Math.round(Number(pendingTx.gasPrice) * blockchain.retryTxGasPriceMultiplier); + // } else { + // gasPrice = Math.round( + // (blockchain.gasPrice || (await this.getGasPriceWeiWithFallback(blockchain))) * + // blockchain.retryTxGasPriceMultiplier, + // ); + // } + // } else { + // gasPrice = blockchain.gasPrice || (await this.getGasPriceWeiWithFallback(blockchain)); + // } + // } else { + // gasPrice = blockchain.gasPrice || (await this.getGasPriceWeiWithFallback(blockchain)); + // } + + const gasFeeOptions = await this.getGasFeeOptions(blockchain); if (blockchain.simulateTxs) { - await web3Instance.eth.call({ + const simulationTx = { to: contractInstance.options.address, data: encodedABI, from: publicKey, - gasPrice, gas: gasLimit, - }); + }; + + if (gasFeeOptions.type === GAS_MODES.EIP1559) { + simulationTx.maxFeePerGas = gasFeeOptions.maxFeePerGas; + simulationTx.maxPriorityFeePerGas = gasFeeOptions.maxPriorityFeePerGas; + } else { + simulationTx.gasPrice = gasFeeOptions.gasPrice; + } + + await web3Instance.eth.call(simulationTx); } return { from: publicKey, to: contractInstance.options.address, data: encodedABI, - gasPrice, gas: gasLimit, + ...(gasFeeOptions.type === GAS_MODES.EIP1559 + ? { + maxFeePerGas: gasFeeOptions.maxFeePerGas, + maxPriorityFeePerGas: gasFeeOptions.maxPriorityFeePerGas, + } + : { gasPrice: gasFeeOptions.gasPrice }), }; } @@ -1403,7 +1408,9 @@ export default class BlockchainServiceBase { // eth_feeHistory params: blockCount, newestBlock, rewardPercentiles // [50] = median priority fee per block const priorityFeePercentile = blockchain.priorityFeePercentile ?? 80; - const feeHistory = await web3Instance.eth.getFeeHistory(blockCount, 'latest', [priorityFeePercentile]); + const feeHistory = await web3Instance.eth.getFeeHistory(blockCount, 'latest', [ + priorityFeePercentile, + ]); // Extract median priority fees from each block (reward[blockIndex][percentileIndex]) const priorityFees = feeHistory.reward @@ -1434,57 +1441,55 @@ export default class BlockchainServiceBase { */ applyGasPriceBuffer(maxBaseFee, maxPriorityFee, gasPriceBufferPercent) { if (!gasPriceBufferPercent) return maxBaseFee + maxPriorityFee; - return ((maxBaseFee * BigInt(100 + Number(gasPriceBufferPercent))) / 100n) + maxPriorityFee; + return (maxBaseFee * BigInt(100 + Number(gasPriceBufferPercent))) / 100n + maxPriorityFee; + } + + buildEip1559FeesFromHistory(feeHistory, gasPriceBufferPercent = 0) { + const baseFees = Array.from(feeHistory.baseFeePerGas ?? []); + const priorityFees = Array.from(feeHistory.priorityFees ?? []); + + if (baseFees.length === 0 || priorityFees.length === 0) { + throw new Error('Fee history data is empty'); + } + + const maxBaseFee = baseFees.reduce((max, bf) => (bf > max ? bf : max), 0n); + const maxPriorityFeePerGas = priorityFees.reduce((max, pf) => (pf > max ? pf : max), 0n); + + const maxFeePerGas = this.applyGasPriceBuffer( + maxBaseFee, + maxPriorityFeePerGas, + gasPriceBufferPercent, + ); + + return { + maxFeePerGas, + maxPriorityFeePerGas, + }; } /** - * Estimate safe gas price using eth_feeHistory (EIP-1559 style) - * Takes max base fee from last N blocks, adds a buffer for volatility, - * and includes the priority fee (tip) for validator incentive + * Estimate safe gas fees using eth_feeHistory (EIP-1559 style) * @param {Object} blockchain - Blockchain configuration - * @returns {Promise} Estimated gas price in wei + * @returns {Promise<{maxFeePerGas: bigint, maxPriorityFeePerGas: bigint}>} */ - async estimateGasPriceFromFeeHistory(blockchain) { - const { gasPriceBufferPercent } = blockchain; + async estimateEip1559Fees(blockchain) { const feeHistory = await this.getFeeHistory(blockchain, FEE_HISTORY_BLOCK_COUNT); - - // Fallback to network gas price if feeHistory not supported or empty if (!feeHistory.supported) { - return this.applyGasPriceBuffer( - 0n, - BigInt(await this.getNetworkGasPrice(blockchain)), - gasPriceBufferPercent, - ); - } - - const baseFees = Array.from(feeHistory.baseFeePerGas); - const priorityFees = Array.from(feeHistory.priorityFees); - - if (baseFees.length === 0 || priorityFees.length === 0) { - return this.applyGasPriceBuffer( - 0n, - BigInt(await this.getNetworkGasPrice(blockchain)), - gasPriceBufferPercent, - ); + throw new Error('eth_feeHistory not supported'); } - // Find max base fee and priority fee from recent blocks - const maxBaseFee = baseFees.reduce((max, bf) => (bf > max ? bf : max), 0n); - const maxPriorityFee = priorityFees.reduce((max, pf) => (pf > max ? pf : max), 0n); - - return this.applyGasPriceBuffer(maxBaseFee, maxPriorityFee, gasPriceBufferPercent); + return this.buildEip1559FeesFromHistory(feeHistory, blockchain.gasPriceBufferPercent ?? 0); } /** - * Get gas price with EIP-1559 estimation (with fallback) - * Tries eth_feeHistory first, falls back to legacy methods + * Get preferred gas price in wei: try EIP-1559 fee history, fall back to legacy network gas price. * @param {Object} blockchain - Blockchain configuration * @returns {Promise} Gas price in wei (as string for web3 compatibility) */ - async getSmartGasPrice(blockchain) { + async getGasPriceWeiWithFallback(blockchain) { try { - const estimatedPrice = await this.estimateGasPriceFromFeeHistory(blockchain); - return estimatedPrice.toString(); + const { maxFeePerGas } = await this.estimateEip1559Fees(blockchain); + return maxFeePerGas.toString(); } catch (eip1559Error) { try { return await this.getNetworkGasPrice(blockchain); @@ -1498,6 +1503,59 @@ export default class BlockchainServiceBase { } } + normalizeGasMode(gasMode) { + const requested = (gasMode || '').toLowerCase(); + if (Object.values(GAS_MODES).includes(requested)) { + return requested; + } + return DEFAULT_PARAMETERS.GAS_MODE; + } + + /** + * Resolve gas fee fields based on configured gas mode and network support + * @param {Object} blockchain - Blockchain configuration + * @returns {Promise} Gas fee fields to merge into tx (legacy or EIP-1559) + */ + async getGasFeeOptions(blockchain) { + const desiredMode = this.normalizeGasMode(blockchain.gasMode); + const feeHistory = await this.getFeeHistory(blockchain, FEE_HISTORY_BLOCK_COUNT); + const supportsEip1559 = + feeHistory.supported && + feeHistory.baseFeePerGas?.length && + feeHistory.priorityFees?.length; + + if (desiredMode === GAS_MODES.EIP1559 && supportsEip1559) { + const { maxFeePerGas, maxPriorityFeePerGas } = this.buildEip1559FeesFromHistory( + feeHistory, + blockchain.gasPriceBufferPercent ?? 0, + ); + + return { + type: GAS_MODES.EIP1559, + maxFeePerGas: maxFeePerGas.toString(), + maxPriorityFeePerGas: maxPriorityFeePerGas.toString(), + }; + } + + if (desiredMode === GAS_MODES.EIP1559 && !supportsEip1559) { + // eslint-disable-next-line no-console + console.warn( + 'EIP-1559 gas mode requested but eth_feeHistory is unsupported; skipping feeHistory retry and falling back to legacy gasPrice', + ); + } + + const legacyGasPrice = + blockchain.gasPrice ?? + (supportsEip1559 + ? await this.getGasPriceWeiWithFallback(blockchain) + : await this.getNetworkGasPrice(blockchain)); + + return { + type: GAS_MODES.LEGACY, + gasPrice: legacyGasPrice?.toString?.() ?? legacyGasPrice, + }; + } + async getWalletBalances(blockchain) { await this.ensureBlockchainInfo(blockchain); const web3Instance = await this.getWeb3Instance(blockchain); diff --git a/services/blockchain-service/implementations/node-blockchain-service.js b/services/blockchain-service/implementations/node-blockchain-service.js index e5007eee..59e32235 100644 --- a/services/blockchain-service/implementations/node-blockchain-service.js +++ b/services/blockchain-service/implementations/node-blockchain-service.js @@ -1,7 +1,10 @@ /* eslint-disable no-param-reassign */ /* eslint-disable no-await-in-loop */ import Web3 from 'web3'; -import { TRANSACTION_RETRY_ERRORS, WEBSOCKET_PROVIDER_OPTIONS } from '../../../constants/constants.js'; +import { + TRANSACTION_RETRY_ERRORS, + WEBSOCKET_PROVIDER_OPTIONS, +} from '../../../constants/constants.js'; import BlockchainServiceBase from '../blockchain-service-base.js'; export default class NodeBlockchainService extends BlockchainServiceBase { @@ -20,6 +23,8 @@ export default class NodeBlockchainService extends BlockchainServiceBase { }; }, ); + + this.nextNonces = new Map(); } initializeWeb3(blockchainName, blockchainRpc, blockchainOptions) { @@ -59,13 +64,30 @@ export default class NodeBlockchainService extends BlockchainServiceBase { return blockchain?.publicKey; } + async allocateNonce(blockchain) { + const address = (await this.getPublicKey(blockchain))?.toLowerCase(); + if (!address) throw new Error('Missing public key for nonce allocation'); + + if (!this.nextNonces.has(address)) { + const web3Instance = await this.getWeb3Instance(blockchain); + // Seed the local nonce tracker from the pending nonce to avoid collisions across sequential txs. + const startingNonce = await web3Instance.eth.getTransactionCount(address, 'pending'); + this.nextNonces.set(address, startingNonce); + } + + const nonce = this.nextNonces.get(address); + // Increment locally so concurrent sends reuse the monotonic nonce without extra RPC calls. + this.nextNonces.set(address, nonce + 1); + return nonce; + } + async executeContractFunction(contractName, functionName, args, blockchain) { await this.ensureBlockchainInfo(blockchain); const web3Instance = await this.getWeb3Instance(blockchain); let contractInstance = await this.getContractInstance(contractName, blockchain); let receipt; - let previousTxGasPrice; + let lastSentGasPrice; let simulationSucceeded = false; let transactionRetried = false; @@ -77,17 +99,25 @@ export default class NodeBlockchainService extends BlockchainServiceBase { args, blockchain, ); - previousTxGasPrice = tx.gasPrice; + const nonce = await this.allocateNonce(blockchain); + // Track what we sent in case we need to retry without a receipt. + lastSentGasPrice = tx.gasPrice ?? tx.maxFeePerGas; simulationSucceeded = true; const createdTransaction = await web3Instance.eth.accounts.signTransaction( - tx, + { ...tx, nonce }, blockchain.privateKey, ); receipt = await web3Instance.eth.sendSignedTransaction( createdTransaction.rawTransaction, ); + + const actualGasPrice = + receipt?.effectiveGasPrice ?? receipt?.gasPrice ?? lastSentGasPrice; + lastSentGasPrice = actualGasPrice; + blockchain.previousTxGasPrice = actualGasPrice; + if (blockchain.name.startsWith('otp') && blockchain.waitNeurowebTxFinalization) { receipt = await this.waitForTransactionFinalization(receipt, blockchain); } @@ -102,7 +132,8 @@ export default class NodeBlockchainService extends BlockchainServiceBase { ) { transactionRetried = true; blockchain.retryTx = true; - blockchain.previousTxGasPrice = previousTxGasPrice; + // Prefer actual paid price; fall back to what we sent if no receipt. + blockchain.previousTxGasPrice = lastSentGasPrice; } else if (!transactionRetried && /revert|VM Exception/i.test(error.message)) { let status; try { diff --git a/services/input-service.js b/services/input-service.js index 28f99ae9..232b9d17 100644 --- a/services/input-service.js +++ b/services/input-service.js @@ -7,6 +7,7 @@ import { PARANET_MINERS_ACCESS_POLICY, PARANET_KC_SUBMISSION_POLICY, ZERO_ADDRESS, + GAS_MODES, } from '../constants/constants.js'; export default class InputService { @@ -192,6 +193,19 @@ export default class InputService { BLOCKCHAINS[environment][name]?.gasPriceOracleLink ?? undefined; + const getEnvGasMode = () => + typeof process !== 'undefined' && process?.env ? process.env.DKG_GAS_MODE : undefined; + + const requestedGasMode = + options.blockchain?.gasMode ?? + this.config.blockchain?.gasMode ?? + getEnvGasMode() ?? + DEFAULT_PARAMETERS.GAS_MODE; + const normalizedRequestedGasMode = (requestedGasMode || '').toLowerCase(); + const normalizedGasMode = Object.values(GAS_MODES).includes(normalizedRequestedGasMode) + ? normalizedRequestedGasMode + : DEFAULT_PARAMETERS.GAS_MODE; + const maxAllowance = options.blockchain?.maxAllowance ?? this.config.blockchain?.maxAllowance ?? undefined; const gasPriceBufferPercent = @@ -220,6 +234,7 @@ export default class InputService { simulateTxs, forceReplaceTxs, gasPriceOracleLink, + gasMode: normalizedGasMode, maxAllowance, gasPriceBufferPercent, priorityFeePercentile, diff --git a/services/node-api-service/implementations/http-service.js b/services/node-api-service/implementations/http-service.js index fc269119..c3374fc2 100644 --- a/services/node-api-service/implementations/http-service.js +++ b/services/node-api-service/implementations/http-service.js @@ -57,6 +57,22 @@ export default class HttpService { return response.data.operationId; } catch (error) { + const status = error?.response?.status; + const body = error?.response?.data; + const url = `${this.getBaseUrl(endpoint, port)}/publish`; + console.error( + 'Unable to publish', + JSON.stringify( + { + url, + status, + body, + message: error?.message, + }, + null, + 2, + ), + ); throw Error(`Unable to publish: ${error.message}`); } }