diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json index aeb63b5e2d..d4d767a17d 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json @@ -31,6 +31,33 @@ ], "components": { "schemas": { + "ReceiptType": { + "description": "Enumerates the possible types of receipts that can be waited for by someone or something that has requested the execution of a transaction on a ledger.", + "type": "string", + "enum": [ + "NODE_TX_POOL_ACK", + "LEDGER_BLOCK_ACK" + ] + }, + "ConsistencyStrategy": { + "type": "object", + "required": ["receiptType", "blockConfirmations"], + "properties": { + "receiptType": { + "$ref": "#/components/schemas/ReceiptType" + }, + "timeoutMs": { + "type": "integer", + "description": "The amount of milliseconds to wait for the receipt to arrive to the connector. Defaults to 0 which means to wait for an unlimited amount of time. Note that this wait may be interrupted still by other parts of the infrastructure such as load balancers cutting of HTTP requests after some time even if they are the type that is supposed to be kept alive. The question of re-entrancy is a broader topic not in scope to discuss here, but it is important to mention it.", + "minimum": 0 + }, + "blockConfirmations": { + "type": "integer", + "minimum": 0, + "description": "The number of blocks to wait to be confirmed in addition to the block containing the transaction in question. Note that if the receipt type is set to only wait for node transaction pool ACK and this parameter is set to anything, but zero then the API will not accept the request due to conflicting parameters." + } + } + }, "Web3SigningCredential": { "type": "object", "required": [ @@ -343,7 +370,8 @@ "type": "object", "required": [ "web3SigningCredential", - "transactionConfig" + "transactionConfig", + "consistencyStrategy" ], "properties": { "web3SigningCredential": { @@ -354,12 +382,8 @@ "$ref": "#/components/schemas/BesuTransactionConfig", "nullable": false }, - "timeoutMs": { - "type": "number", - "description": "The amount of milliseconds to wait for a transaction receipt with thehash of the transaction(which indicates successful execution) beforegiving up and crashing.", - "minimum": 0, - "default": 60000, - "nullable": false + "consistencyStrategy": { + "$ref": "#/components/schemas/ConsistencyStrategy" } } }, diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts index c6b9fbb46b..41b0ae1064 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -76,6 +76,31 @@ export interface BesuTransactionConfig { */ data?: string; } +/** + * + * @export + * @interface ConsistencyStrategy + */ +export interface ConsistencyStrategy { + /** + * + * @type {ReceiptType} + * @memberof ConsistencyStrategy + */ + receiptType: ReceiptType; + /** + * The amount of milliseconds to wait for the receipt to arrive to the connector. Defaults to 0 which means to wait for an unlimited amount of time. Note that this wait may be interrupted still by other parts of the infrastructure such as load balancers cutting of HTTP requests after some time even if they are the type that is supposed to be kept alive. The question of re-entrancy is a broader topic not in scope to discuss here, but it is important to mention it. + * @type {number} + * @memberof ConsistencyStrategy + */ + timeoutMs?: number; + /** + * The number of blocks to wait to be confirmed in addition to the block containing the transaction in question. Note that if the receipt type is set to only wait for node transaction pool ACK and this parameter is set to anything, but zero then the API will not accept the request due to conflicting parameters. + * @type {number} + * @memberof ConsistencyStrategy + */ + blockConfirmations: number; +} /** * * @export @@ -320,6 +345,16 @@ export interface InvokeContractV2Response { */ success: boolean; } +/** + * Enumerates the possible types of receipts that can be waited for by someone or something that has requested the execution of a transaction on a ledger. + * @export + * @enum {string} + */ +export enum ReceiptType { + NODETXPOOLACK = 'NODE_TX_POOL_ACK', + LEDGERBLOCKACK = 'LEDGER_BLOCK_ACK' +} + /** * * @export @@ -339,11 +374,11 @@ export interface RunTransactionRequest { */ transactionConfig: BesuTransactionConfig; /** - * The amount of milliseconds to wait for a transaction receipt with thehash of the transaction(which indicates successful execution) beforegiving up and crashing. - * @type {number} + * + * @type {ConsistencyStrategy} * @memberof RunTransactionRequest */ - timeoutMs?: number; + consistencyStrategy: ConsistencyStrategy; } /** * diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts index f62dd0f04f..55e2de29c3 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts @@ -38,6 +38,7 @@ import { import { DeployContractSolidityBytecodeEndpoint } from "./web-services/deploy-contract-solidity-bytecode-endpoint"; import { + ConsistencyStrategy, DeployContractSolidityBytecodeV1Request, DeployContractSolidityBytecodeV1Response, EthContractInvocationType, @@ -45,6 +46,7 @@ import { InvokeContractV2Request, InvokeContractV1Response, InvokeContractV2Response, + ReceiptType, RunTransactionRequest, RunTransactionResponse, SignTransactionRequest, @@ -238,7 +240,11 @@ export class PluginLedgerConnectorBesu const txReq: RunTransactionRequest = { transactionConfig, web3SigningCredential, - timeoutMs: req.timeoutMs || 60000, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: req.timeoutMs || 60000, + }, }; const out = await this.transact(txReq); @@ -309,7 +315,11 @@ export class PluginLedgerConnectorBesu const txReq: RunTransactionRequest = { transactionConfig, web3SigningCredential, - timeoutMs: req.timeoutMs || 60000, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: req.timeoutMs || 60000, + }, }; const out = await this.transact(txReq); //const transactionReceipt = out.transactionReceipt; @@ -340,7 +350,7 @@ export class PluginLedgerConnectorBesu } case Web3SigningCredentialType.NONE: { if (req.transactionConfig.rawTransaction) { - return this.transactSigned(req.transactionConfig.rawTransaction); + return this.transactSigned(req); } else { throw new Error( `${fnTag} Expected pre-signed raw transaction ` + @@ -360,17 +370,56 @@ export class PluginLedgerConnectorBesu } public async transactSigned( - rawTransaction: string, + req: RunTransactionRequest, ): Promise { const fnTag = `${this.className}#transactSigned()`; - const receipt = await this.web3.eth.sendSignedTransaction(rawTransaction); + Checks.truthy(req.consistencyStrategy, `${fnTag}:req.consistencyStrategy`); + Checks.truthy( + req.transactionConfig.rawTransaction, + `${fnTag}:req.transactionConfig.rawTransaction`, + ); + const rawTx = req.transactionConfig.rawTransaction as string; + this.log.debug("Starting web3.eth.sendSignedTransaction(rawTransaction) "); + const txPoolReceipt = await this.web3.eth.sendSignedTransaction(rawTx); + this.log.debug("Received preliminary receipt from Besu node."); + + if (txPoolReceipt instanceof Error) { + this.log.debug(`${fnTag} sendSignedTransaction failed`, txPoolReceipt); + throw txPoolReceipt; + } - if (receipt instanceof Error) { - this.log.debug(`${fnTag} Web3 sendSignedTransaction failed`, receipt); - throw receipt; - } else { - return { transactionReceipt: receipt }; + if ( + req.consistencyStrategy.receiptType === ReceiptType.NODETXPOOLACK && + req.consistencyStrategy.blockConfirmations > 0 + ) { + throw new Error( + `${fnTag} Conflicting parameters for consistency` + + ` strategy: Cannot wait for >0 block confirmations AND only wait ` + + ` for the tx pool ACK at the same time.`, + ); + } + + switch (req.consistencyStrategy.receiptType) { + case ReceiptType.NODETXPOOLACK: + return { transactionReceipt: txPoolReceipt }; + case ReceiptType.LEDGERBLOCKACK: + this.log.debug("Starting poll for ledger TX receipt ..."); + const txHash = txPoolReceipt.transactionHash; + const { consistencyStrategy } = req; + const ledgerReceipt = await this.pollForTxReceipt( + txHash, + consistencyStrategy, + ); + this.log.debug( + "Finished poll for ledger TX receipt: %o", + ledgerReceipt, + ); + return { transactionReceipt: ledgerReceipt }; + default: + throw new Error( + `${fnTag} Unrecognized ReceiptType: ${req.consistencyStrategy.receiptType}`, + ); } } @@ -389,7 +438,8 @@ export class PluginLedgerConnectorBesu ); if (signedTx.rawTransaction) { - return this.transactSigned(signedTx.rawTransaction); + req.transactionConfig.rawTransaction = signedTx.rawTransaction; + return this.transactSigned(req); } else { throw new Error( `${fnTag} Failed to sign eth transaction. ` + @@ -428,30 +478,46 @@ export class PluginLedgerConnectorBesu type: Web3SigningCredentialType.PRIVATEKEYHEX, secret: privateKeyHex, }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: 60000, + }, }); } public async pollForTxReceipt( txHash: string, - timeoutMs = 60000, + consistencyStrategy: ConsistencyStrategy, ): Promise { const fnTag = `${this.className}#pollForTxReceipt()`; let txReceipt; let timedOut = false; let tries = 0; + let confirmationCount = 0; + const timeoutMs = consistencyStrategy.timeoutMs || Number.MAX_SAFE_INTEGER; const startedAt = new Date(); do { - txReceipt = await this.web3.eth.getTransactionReceipt(txHash); tries++; timedOut = Date.now() >= startedAt.getTime() + timeoutMs; - } while (!timedOut && !txReceipt); + if (timedOut) { + break; + } + + txReceipt = await this.web3.eth.getTransactionReceipt(txHash); + if (!txReceipt) { + continue; + } + + const latestBlockNo = await this.web3.eth.getBlockNumber(); + confirmationCount = latestBlockNo - txReceipt.blockNumber; + } while (confirmationCount >= consistencyStrategy.blockConfirmations); if (!txReceipt) { throw new Error(`${fnTag} Timed out ${timeoutMs}ms, polls=${tries}`); - } else { - return txReceipt; } + return txReceipt; } public async deployContract( @@ -472,6 +538,11 @@ export class PluginLedgerConnectorBesu gas: req.gas, gasPrice: req.gasPrice, }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: req.timeoutMs || 60000, + }, web3SigningCredential, }); diff --git a/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/deploy-contract-from-json.test.ts b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/deploy-contract-from-json.test.ts index 6816502955..134d844a74 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/deploy-contract-from-json.test.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/deploy-contract-from-json.test.ts @@ -7,6 +7,7 @@ import { PluginLedgerConnectorBesu, PluginFactoryLedgerConnector, Web3SigningCredentialCactusKeychainRef, + ReceiptType, } from "../../../../../main/typescript/public-api"; import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; import { @@ -69,6 +70,7 @@ test(testCase, async (t: Test) => { }); const connector: PluginLedgerConnectorBesu = await factory.create({ rpcApiHttpHost, + logLevel, instanceId: uuidv4(), pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), }); @@ -85,6 +87,11 @@ test(testCase, async (t: Test) => { value: 10e9, gas: 1000000, }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: 60000, + }, }); const balance = await web3.eth.getBalance(testEthAccount.address); @@ -158,6 +165,11 @@ test(testCase, async (t: Test) => { transactionConfig: { rawTransaction, }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: 60000, + }, }); const balance2 = await web3.eth.getBalance(testEthAccount2.address); diff --git a/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/invoke-contract-v2.test.ts b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/invoke-contract-v2.test.ts index a14dafa266..00bdd313c4 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/invoke-contract-v2.test.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/invoke-contract-v2.test.ts @@ -8,6 +8,7 @@ import { PluginLedgerConnectorBesu, PluginFactoryLedgerConnector, Web3SigningCredentialCactusKeychainRef, + ReceiptType, } from "../../../../../main/typescript/public-api"; import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; import { BesuTestLedger } from "@hyperledger/cactus-test-tooling"; @@ -75,6 +76,10 @@ test("deploys contract via .json file", async (t: Test) => { secret: besuKeyPair.privateKey, type: Web3SigningCredentialType.PRIVATEKEYHEX, }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + }, transactionConfig: { from: firstHighNetWorthAccount, to: testEthAccount.address, @@ -150,6 +155,10 @@ test("deploys contract via .json file", async (t: Test) => { web3SigningCredential: { type: Web3SigningCredentialType.NONE, }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + }, transactionConfig: { rawTransaction, },