From 9a7dc2c14145ca9042dd01b5b2518d9c58ec08f2 Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Mon, 21 Aug 2023 15:37:53 +0000 Subject: [PATCH] feat(cactus-plugin-ledger-connector-ethereum): support London fork gas prices - Add legacy and EIP1559 gas configuration options to transaction requests. - Legacy gas configuration is updated to EIP1559 using the same logic as web3 libraries. - Update the tests to work with new API. - Added test suite to test new features - `geth-transact-and-gas-fees.test.ts` Depends on: #2581 Signed-off-by: Michal Bajer --- .../src/main/json/openapi.json | 104 ++++-- .../generated/openapi/typescript-axios/api.ts | 117 ++++--- .../plugin-ledger-connector-ethereum.ts | 97 ++++-- .../typescript/types/model-type-guards.ts | 24 ++ ...oy-and-invoke-using-json-object-v1.test.ts | 14 +- ...eploy-and-invoke-using-keychain-v1.test.ts | 24 +- .../geth-invoke-web3-contract-v1.test.ts | 1 - .../geth-transact-and-gas-fees.test.ts | 326 ++++++++++++++++++ ...h-alchemy-integration-manual-check.test.ts | 3 - .../typescript/unit/model-type-guards.test.ts | 62 ++++ ...ntegration-with-ethereum-connector.test.ts | 1 - 11 files changed, 637 insertions(+), 136 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-transact-and-gas-fees.test.ts diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.json index 3baf09b50e..0f067b2855 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.json @@ -207,10 +207,59 @@ } } }, + "GasTransactionConfigLegacy": { + "type": "object", + "description": "Transaction gas settings in networks before EIP-1559 (London fork).", + "required": [], + "properties": { + "gas": { + "type": "string", + "description": "A maximum amount of gas a user is willing to provide for the execution of the transaction. (gasLimit)" + }, + "gasPrice": { + "type": "string", + "description": "A price (in Wei) a user is willing to pay for each unit of gas used during the execution of the transaction. In EIP-1559 (London fork) networks, it will be set as both maxFeePerGas and maxPriorityFeePerGas." + } + } + }, + "GasTransactionConfigEIP1559": { + "type": "object", + "description": "Transaction gas settings in networks after EIP-1559 (London fork).", + "required": [], + "properties": { + "gasLimit": { + "type": "string", + "description": "A maximum amount of gas a user is willing to provide for the execution of the transaction.", + "nullable": false + }, + "maxFeePerGas": { + "type": "string", + "description": "A maximum fee (including the base fee and the tip) a user is willing to pay per unit of gas.", + "nullable": false + }, + "maxPriorityFeePerGas": { + "type": "string", + "description": "A maximum tip amount a user is willing to pay per unit of gas.", + "nullable": false + } + } + }, + "GasTransactionConfig": { + "type": "object", + "description": "Transaction gas settings.", + "required": [], + "oneOf": [ + { + "$ref": "#/components/schemas/GasTransactionConfigLegacy" + }, + { + "$ref": "#/components/schemas/GasTransactionConfigEIP1559" + } + ] + }, "EthereumTransactionConfig": { "type": "object", "required": [], - "additionalProperties": true, "properties": { "rawTransaction": { "type": "string", @@ -225,17 +274,15 @@ "value": { "type": "string" }, - "gas": { - "type": "string" - }, - "gasPrice": { - "type": "string" - }, "nonce": { "type": "string" }, "data": { "type": "string" + }, + "gasConfig": { + "$ref": "#/components/schemas/GasTransactionConfig", + "nullable": false } } }, @@ -450,12 +497,8 @@ "maxLength": 100, "nullable": false }, - "gas": { - "type": "number", - "nullable": false - }, - "gasPrice": { - "type": "number", + "gasConfig": { + "$ref": "#/components/schemas/GasTransactionConfig", "nullable": false }, "nonce": { @@ -504,12 +547,8 @@ "$ref": "#/components/schemas/Web3SigningCredential", "nullable": false }, - "gas": { - "type": "number", - "nullable": false - }, - "gasPrice": { - "type": "string", + "gasConfig": { + "$ref": "#/components/schemas/GasTransactionConfig", "nullable": false }, "timeoutMs": { @@ -574,11 +613,9 @@ "value": { "type": "string" }, - "gas": { - "type": "string" - }, - "gasPrice": { - "type": "string" + "gasConfig": { + "$ref": "#/components/schemas/GasTransactionConfig", + "nullable": false }, "nonce": { "type": "string" @@ -640,11 +677,9 @@ "value": { "type": "string" }, - "gas": { - "type": "string" - }, - "gasPrice": { - "type": "string" + "gasConfig": { + "$ref": "#/components/schemas/GasTransactionConfig", + "nullable": false }, "nonce": { "type": "string" @@ -799,12 +834,7 @@ }, "Web3BlockHeader": { "type": "object", - "required": [ - "sha3Uncles", - "transactionRoot", - "gasLimit", - "gasUsed" - ], + "required": ["sha3Uncles", "transactionRoot", "gasLimit", "gasUsed"], "properties": { "number": { "type": "string" @@ -1145,7 +1175,7 @@ } }, "operationId": "invokeContractV1", - "summary": "Invokes a contract on a besu ledger", + "summary": "Invokes a contract on an ethereum ledger", "parameters": [], "requestBody": { "content": { @@ -1179,7 +1209,7 @@ } }, "operationId": "invokeContractV1NoKeychain", - "summary": "Invokes a contract on a besu ledger", + "summary": "Invokes a contract on an ethereum ledger", "parameters": [], "requestBody": { "content": { diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/generated/openapi/typescript-axios/api.ts index 49cdcaa635..59d01be720 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -124,16 +124,10 @@ export interface DeployContractSolidityBytecodeJsonObjectV1Request { 'web3SigningCredential': Web3SigningCredential; /** * - * @type {number} + * @type {GasTransactionConfig} * @memberof DeployContractSolidityBytecodeJsonObjectV1Request */ - 'gas'?: number; - /** - * - * @type {string} - * @memberof DeployContractSolidityBytecodeJsonObjectV1Request - */ - 'gasPrice'?: string; + 'gasConfig'?: GasTransactionConfig; /** * The amount of milliseconds to wait for a transaction receipt with theaddress of the contract(which indicates successful deployment) beforegiving up and crashing. * @type {number} @@ -191,16 +185,10 @@ export interface DeployContractSolidityBytecodeV1Request { 'keychainId': string; /** * - * @type {number} + * @type {GasTransactionConfig} * @memberof DeployContractSolidityBytecodeV1Request */ - 'gas'?: number; - /** - * - * @type {number} - * @memberof DeployContractSolidityBytecodeV1Request - */ - 'gasPrice'?: number; + 'gasConfig'?: GasTransactionConfig; /** * * @type {number} @@ -281,8 +269,6 @@ export type EthContractInvocationWeb3Method = typeof EthContractInvocationWeb3Me * @interface EthereumTransactionConfig */ export interface EthereumTransactionConfig { - [key: string]: any; - /** * * @type {string} @@ -312,25 +298,70 @@ export interface EthereumTransactionConfig { * @type {string} * @memberof EthereumTransactionConfig */ - 'gas'?: string; + 'nonce'?: string; /** * * @type {string} * @memberof EthereumTransactionConfig */ - 'gasPrice'?: string; + 'data'?: string; /** * - * @type {string} + * @type {GasTransactionConfig} * @memberof EthereumTransactionConfig */ - 'nonce'?: string; + 'gasConfig'?: GasTransactionConfig; +} +/** + * @type GasTransactionConfig + * Transaction gas settings. + * @export + */ +export type GasTransactionConfig = GasTransactionConfigEIP1559 | GasTransactionConfigLegacy; + +/** + * Transaction gas settings in networks after EIP-1559 (London fork). + * @export + * @interface GasTransactionConfigEIP1559 + */ +export interface GasTransactionConfigEIP1559 { /** - * + * A maximum amount of gas a user is willing to provide for the execution of the transaction. * @type {string} - * @memberof EthereumTransactionConfig + * @memberof GasTransactionConfigEIP1559 */ - 'data'?: string; + 'gasLimit'?: string; + /** + * A maximum fee (including the base fee and the tip) a user is willing to pay per unit of gas. + * @type {string} + * @memberof GasTransactionConfigEIP1559 + */ + 'maxFeePerGas'?: string; + /** + * A maximum tip amount a user is willing to pay per unit of gas. + * @type {string} + * @memberof GasTransactionConfigEIP1559 + */ + 'maxPriorityFeePerGas'?: string; +} +/** + * Transaction gas settings in networks before EIP-1559 (London fork). + * @export + * @interface GasTransactionConfigLegacy + */ +export interface GasTransactionConfigLegacy { + /** + * A maximum amount of gas a user is willing to provide for the execution of the transaction. (gasLimit) + * @type {string} + * @memberof GasTransactionConfigLegacy + */ + 'gas'?: string; + /** + * A price (in Wei) a user is willing to pay for each unit of gas used during the execution of the transaction. In EIP-1559 (London fork) networks, it will be set as both maxFeePerGas and maxPriorityFeePerGas. + * @type {string} + * @memberof GasTransactionConfigLegacy + */ + 'gasPrice'?: string; } /** * @@ -376,16 +407,10 @@ export interface InvokeContractJsonObjectV1Request { 'value'?: string; /** * - * @type {string} - * @memberof InvokeContractJsonObjectV1Request - */ - 'gas'?: string; - /** - * - * @type {string} + * @type {GasTransactionConfig} * @memberof InvokeContractJsonObjectV1Request */ - 'gasPrice'?: string; + 'gasConfig'?: GasTransactionConfig; /** * * @type {string} @@ -451,16 +476,10 @@ export interface InvokeContractV1Request { 'value'?: string; /** * - * @type {string} - * @memberof InvokeContractV1Request - */ - 'gas'?: string; - /** - * - * @type {string} + * @type {GasTransactionConfig} * @memberof InvokeContractV1Request */ - 'gasPrice'?: string; + 'gasConfig'?: GasTransactionConfig; /** * * @type {string} @@ -1521,7 +1540,7 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati }, /** * - * @summary Invokes a contract on a besu ledger + * @summary Invokes a contract on an ethereum ledger * @param {InvokeContractV1Request} [invokeContractV1Request] * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -1555,7 +1574,7 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati }, /** * - * @summary Invokes a contract on a besu ledger + * @summary Invokes a contract on an ethereum ledger * @param {InvokeContractJsonObjectV1Request} [invokeContractJsonObjectV1Request] * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -1733,7 +1752,7 @@ export const DefaultApiFp = function(configuration?: Configuration) { }, /** * - * @summary Invokes a contract on a besu ledger + * @summary Invokes a contract on an ethereum ledger * @param {InvokeContractV1Request} [invokeContractV1Request] * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -1744,7 +1763,7 @@ export const DefaultApiFp = function(configuration?: Configuration) { }, /** * - * @summary Invokes a contract on a besu ledger + * @summary Invokes a contract on an ethereum ledger * @param {InvokeContractJsonObjectV1Request} [invokeContractJsonObjectV1Request] * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -1827,7 +1846,7 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa }, /** * - * @summary Invokes a contract on a besu ledger + * @summary Invokes a contract on an ethereum ledger * @param {InvokeContractV1Request} [invokeContractV1Request] * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -1837,7 +1856,7 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa }, /** * - * @summary Invokes a contract on a besu ledger + * @summary Invokes a contract on an ethereum ledger * @param {InvokeContractJsonObjectV1Request} [invokeContractJsonObjectV1Request] * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -1922,7 +1941,7 @@ export class DefaultApi extends BaseAPI { /** * - * @summary Invokes a contract on a besu ledger + * @summary Invokes a contract on an ethereum ledger * @param {InvokeContractV1Request} [invokeContractV1Request] * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -1934,7 +1953,7 @@ export class DefaultApi extends BaseAPI { /** * - * @summary Invokes a contract on a besu ledger + * @summary Invokes a contract on an ethereum ledger * @param {InvokeContractJsonObjectV1Request} [invokeContractJsonObjectV1Request] * @param {*} [options] Override http request option. * @throws {RequiredError} diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/plugin-ledger-connector-ethereum.ts b/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/plugin-ledger-connector-ethereum.ts index 2903d084b9..76ccdf5441 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/plugin-ledger-connector-ethereum.ts +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/plugin-ledger-connector-ethereum.ts @@ -4,7 +4,7 @@ import type { } from "socket.io"; import { Express } from "express"; -import Web3, { TransactionReceiptBase } from "web3"; +import Web3, { Transaction, TransactionReceiptBase } from "web3"; import { Contract, PayableMethodObject } from "web3-eth-contract"; import OAS from "../json/openapi.json"; @@ -52,6 +52,7 @@ import { WatchBlocksV1Options, InvokeRawWeb3EthMethodV1Request, InvokeRawWeb3EthContractV1Request, + EthereumTransactionConfig, } from "./generated/openapi/typescript-axios"; import { RunTransactionEndpoint } from "./web-services/run-transaction-endpoint"; @@ -62,7 +63,11 @@ import { GetPrometheusExporterMetricsEndpointV1 } from "./web-services/get-prome import { InvokeRawWeb3EthMethodEndpoint } from "./web-services/invoke-raw-web3eth-method-v1-endpoint"; import { InvokeRawWeb3EthContractEndpoint } from "./web-services/invoke-raw-web3eth-contract-v1-endpoint"; -import { isWeb3SigningCredentialNone } from "./types/model-type-guards"; +import { + isGasTransactionConfigEIP1559, + isGasTransactionConfigLegacy, + isWeb3SigningCredentialNone, +} from "./types/model-type-guards"; import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; import { RuntimeError } from "run-time-error"; import { @@ -411,7 +416,7 @@ export class PluginLedgerConnectorEthereum "Can't estimate maxFeePerGas - could not get recent baseFeePerGas", ); } - const estimate = BigInt(2) * baseFee + BigInt(priorityFee); + const estimate = baseFee + BigInt(priorityFee); return estimate.toString(); } @@ -464,18 +469,10 @@ export class PluginLedgerConnectorEthereum | Web3SigningCredentialPrivateKeyHex | Web3SigningCredentialCactusKeychainRef; - if (!req.gas) { - const estimatedGas = await method.estimateGas(); - req.gas = estimatedGas.toString(); - } - - const maxFeePerGas = await this.estimateMaxFeePerGas(); const transactionConfig = { from: web3SigningCredential.ethAccount, to: contractAddress, - maxPriorityFeePerGas: req.gasPrice ?? 0, - maxFeePerGas, - gasLimit: req.gas, + gasConfig: req.gasConfig, value: req.value, nonce: req.nonce, data: method.encodeABI(), @@ -568,7 +565,7 @@ export class PluginLedgerConnectorEthereum try { const txHash = await this.web3.eth.personal.sendTransaction( - transactionConfig, + await this.getTransactionFromTxConfig(transactionConfig), secret, ); const transactionReceipt = await this.pollForTxReceipt(txHash); @@ -596,7 +593,7 @@ export class PluginLedgerConnectorEthereum } = web3SigningCredential as Web3SigningCredentialPrivateKeyHex; const signedTx = await this.web3.eth.accounts.signTransaction( - transactionConfig, + await this.getTransactionFromTxConfig(transactionConfig), secret, ); @@ -632,18 +629,6 @@ export class PluginLedgerConnectorEthereum // the private key that we need to run the transaction. const privateKeyHex = await keychainPlugin?.get(keychainEntryKey as string); - if (!transactionConfig.gas) { - this.log.debug( - `${fnTag} Gas not specified in the transaction values. Using the estimate from web3`, - ); - const estimatedGas = await this.web3.eth.estimateGas(transactionConfig); - transactionConfig.gas = estimatedGas.toString(); - this.log.debug( - `${fnTag} Gas estimated from web3 is: `, - transactionConfig.gas, - ); - } - return this.transactPrivateKey({ transactionConfig, web3SigningCredential: { @@ -707,8 +692,7 @@ export class PluginLedgerConnectorEthereum transactionConfig: { data: bytecode, from: web3SigningCredential.ethAccount, - gas: req.gas, - gasPrice: req.gasPrice, + gasConfig: req.gasConfig, }, web3SigningCredential, }); @@ -851,4 +835,61 @@ export class PluginLedgerConnectorEthereum args.invocationParams, ); } + + /** + * Convert connector transaction config to web3js transaction object. + * @param txConfig connector transaction config + * @returns web3js transaction + */ + private async getTransactionFromTxConfig( + txConfig: EthereumTransactionConfig, + ): Promise { + const tx: Transaction = { + from: txConfig.from, + to: txConfig.to, + value: txConfig.value, + nonce: txConfig.nonce, + data: txConfig.data, + }; + + // Apply gas config to the transaction + if (txConfig.gasConfig) { + if (isGasTransactionConfigLegacy(txConfig.gasConfig)) { + if (isGasTransactionConfigEIP1559(txConfig.gasConfig)) { + throw new RuntimeError( + `Detected mixed gasConfig! Use either legacy or EIP-1559 mode. gasConfig - ${JSON.stringify( + txConfig.gasConfig, + )}`, + ); + } + tx.maxPriorityFeePerGas = txConfig.gasConfig.gasPrice; + tx.maxFeePerGas = txConfig.gasConfig.gasPrice; + tx.gasLimit = txConfig.gasConfig.gas; + } else { + tx.maxPriorityFeePerGas = txConfig.gasConfig.maxPriorityFeePerGas; + tx.maxFeePerGas = txConfig.gasConfig.maxFeePerGas; + tx.gasLimit = txConfig.gasConfig.gasLimit; + } + } + + if (tx.maxPriorityFeePerGas && !tx.maxFeePerGas) { + tx.maxFeePerGas = await this.estimateMaxFeePerGas( + tx.maxPriorityFeePerGas.toString(), + ); + this.log.info( + `Estimated maxFeePerGas of ${tx.maxFeePerGas} becuase maxPriorityFeePerGas was provided.`, + ); + } + + // Fill missing gas fields (do it last) + if (!tx.gasLimit) { + const estimatedGas = await this.web3.eth.estimateGas(tx); + this.log.debug( + `Gas not specified in the transaction values, estimated ${estimatedGas.toString()}`, + ); + tx.gasLimit = estimatedGas; + } + + return tx; + } } diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/types/model-type-guards.ts b/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/types/model-type-guards.ts index 14f74a868a..fe9f1216a8 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/types/model-type-guards.ts +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/main/typescript/types/model-type-guards.ts @@ -1,4 +1,7 @@ import { + GasTransactionConfig, + GasTransactionConfigEIP1559, + GasTransactionConfigLegacy, Web3SigningCredentialCactusKeychainRef, Web3SigningCredentialGethKeychainPassword, Web3SigningCredentialNone, @@ -40,3 +43,24 @@ export function isWeb3SigningCredentialCactusKeychainRef(x?: { x?.keychainId.trim().length > 0 ); } + +export function isGasTransactionConfigLegacy( + gasConfig: GasTransactionConfig, +): gasConfig is GasTransactionConfigLegacy { + const typedGasConfig = gasConfig as GasTransactionConfigLegacy; + return ( + typeof typedGasConfig.gas !== "undefined" || + typeof typedGasConfig.gasPrice !== "undefined" + ); +} + +export function isGasTransactionConfigEIP1559( + gasConfig: GasTransactionConfig, +): gasConfig is GasTransactionConfigEIP1559 { + const typedGasConfig = gasConfig as GasTransactionConfigEIP1559; + return ( + typeof typedGasConfig.gasLimit !== "undefined" || + typeof typedGasConfig.maxFeePerGas !== "undefined" || + typeof typedGasConfig.maxPriorityFeePerGas !== "undefined" + ); +} diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-contract-deploy-and-invoke-using-json-object-v1.test.ts b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-contract-deploy-and-invoke-using-json-object-v1.test.ts index 732b691de9..e5372ce940 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-contract-deploy-and-invoke-using-json-object-v1.test.ts +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-contract-deploy-and-invoke-using-json-object-v1.test.ts @@ -171,7 +171,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { secret: "", type: Web3SigningCredentialType.GethKeychainPassword, }, - gas: 1000000, contractJSON: HelloWorldContractJson, }); expect(deployOut).toBeTruthy(); @@ -194,7 +193,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { secret: "", type: Web3SigningCredentialType.GethKeychainPassword, }, - gas: "1000000", contractJSON: HelloWorldContractJson, }); expect(invokeOut).toBeTruthy(); @@ -210,7 +208,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { secret: "", type: Web3SigningCredentialType.GethKeychainPassword, }, - gas: 1000000, constructorArgs: ["Johnny"], contractJSON: HelloWorldWithArgContractJson, }); @@ -228,7 +225,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { secret: "", type: Web3SigningCredentialType.GethKeychainPassword, }, - gas: 1000000, } as DeployContractSolidityBytecodeJsonObjectV1Request); fail( "Expected deployContractSolBytecodeJsonObjectV1 call to fail but it succeeded.", @@ -288,7 +284,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { methodName: "setName", contractAddress, params: [newName], - gas: "1000000", web3SigningCredential: { ethAccount: WHALE_ACCOUNT_ADDRESS, secret: "", @@ -336,6 +331,7 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { }); test("invoke Web3SigningCredentialType.PrivateKeyHex", async () => { + const priorityFee = web3.utils.toWei(2, "gwei"); const nonce = await web3.eth.getTransactionCount(testEthAccount.address); const newName = `DrCactus${uuidV4()}`; const setNameOut = await apiClient.invokeContractV1NoKeychain({ @@ -344,6 +340,9 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { methodName: "setName", contractAddress, params: [newName], + gasConfig: { + maxPriorityFeePerGas: priorityFee, + }, web3SigningCredential: { ethAccount: testEthAccount.address, secret: testEthAccount.privateKey, @@ -361,7 +360,9 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { methodName: "setName", contractAddress, params: [newName], - gas: "1000000", + gasConfig: { + maxPriorityFeePerGas: priorityFee, + }, web3SigningCredential: { ethAccount: testEthAccount.address, secret: testEthAccount.privateKey, @@ -380,7 +381,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { methodName: "getName", contractAddress, params: [], - gas: "1000000", web3SigningCredential: { ethAccount: testEthAccount.address, secret: testEthAccount.privateKey, diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-contract-deploy-and-invoke-using-keychain-v1.test.ts b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-contract-deploy-and-invoke-using-keychain-v1.test.ts index d49d748c9a..3a493c3618 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-contract-deploy-and-invoke-using-keychain-v1.test.ts +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-contract-deploy-and-invoke-using-keychain-v1.test.ts @@ -175,7 +175,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { secret: "", type: Web3SigningCredentialType.GethKeychainPassword, }, - gas: 1000000, }); expect(deployOut).toBeTruthy(); expect(deployOut.data).toBeTruthy(); @@ -196,7 +195,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { secret: "", type: Web3SigningCredentialType.GethKeychainPassword, }, - gas: "1000000", }); expect(invokeOut).toBeTruthy(); expect(invokeOut.data).toBeTruthy(); @@ -213,7 +211,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { secret: "", type: Web3SigningCredentialType.GethKeychainPassword, }, - gas: 1000000, constructorArgs: ["Johnny"], }); expect(deployOut).toBeTruthy(); @@ -231,7 +228,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { secret: "", type: Web3SigningCredentialType.GethKeychainPassword, }, - gas: 1000000, } as DeployContractSolidityBytecodeV1Request); fail( "Expected deployContractSolBytecodeV1 call to fail but it succeeded.", @@ -292,7 +288,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { methodName: "setName", keychainId: keychainPlugin.getKeychainId(), params: [newName], - gas: "1000000", web3SigningCredential: { ethAccount: WHALE_ACCOUNT_ADDRESS, secret: "", @@ -385,6 +380,7 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { }); test("invoke Web3SigningCredentialType.PrivateKeyHex", async () => { + const priorityFee = web3.utils.toWei(2, "gwei"); const nonce = await web3.eth.getTransactionCount(testEthAccount.address); const newName = `DrCactus${uuidV4()}`; const setNameOut = await apiClient.invokeContractV1({ @@ -393,6 +389,9 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { methodName: "setName", keychainId: keychainPlugin.getKeychainId(), params: [newName], + gasConfig: { + maxPriorityFeePerGas: priorityFee, + }, web3SigningCredential: { ethAccount: testEthAccount.address, secret: testEthAccount.privateKey, @@ -410,7 +409,9 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { methodName: "setName", keychainId: keychainPlugin.getKeychainId(), params: [newName], - gas: "1000000", + gasConfig: { + maxPriorityFeePerGas: priorityFee, + }, web3SigningCredential: { ethAccount: testEthAccount.address, secret: testEthAccount.privateKey, @@ -429,7 +430,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { methodName: "getName", keychainId: keychainPlugin.getKeychainId(), params: [], - gas: "1000000", web3SigningCredential: { ethAccount: testEthAccount.address, secret: testEthAccount.privateKey, @@ -445,6 +445,7 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { test("invoke Web3SigningCredentialType.CactusKeychainRef", async () => { const newName = `DrCactus${uuidV4()}`; const nonce = await web3.eth.getTransactionCount(testEthAccount.address); + const priorityFee = web3.utils.toWei(2, "gwei"); const web3SigningCredential: Web3SigningCredentialCactusKeychainRef = { ethAccount: testEthAccount.address, @@ -460,7 +461,9 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { methodName: "setName", keychainId: keychainPlugin.getKeychainId(), params: [newName], - gas: "1000000", + gasConfig: { + maxPriorityFeePerGas: priorityFee, + }, web3SigningCredential, nonce: nonce.toString(), }); @@ -474,7 +477,9 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { methodName: "setName", keychainId: keychainPlugin.getKeychainId(), params: [newName], - gas: "1000000", + gasConfig: { + maxPriorityFeePerGas: priorityFee, + }, web3SigningCredential: { ethAccount: WHALE_ACCOUNT_ADDRESS, secret: "", @@ -493,7 +498,6 @@ describe("Ethereum contract deploy and invoke using keychain tests", () => { methodName: "getName", keychainId: keychainPlugin.getKeychainId(), params: [], - gas: "1000000", web3SigningCredential, }); expect(invokeGetNameOut).toBeTruthy(); diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-invoke-web3-contract-v1.test.ts b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-invoke-web3-contract-v1.test.ts index e1c301a660..fb30b14df4 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-invoke-web3-contract-v1.test.ts +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-invoke-web3-contract-v1.test.ts @@ -93,7 +93,6 @@ describe("invokeRawWeb3EthContract Tests", () => { secret: "", type: Web3SigningCredentialType.GethKeychainPassword, }, - gas: 1000000, }); expect(deployOut).toBeTruthy(); expect(deployOut.transactionReceipt).toBeTruthy(); diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-transact-and-gas-fees.test.ts b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-transact-and-gas-fees.test.ts new file mode 100644 index 0000000000..6bf4aca3b5 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/integration/geth-transact-and-gas-fees.test.ts @@ -0,0 +1,326 @@ +/** + * Tests for running transactions with different gas configurations (both legacy and EIP-1559) + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Log settings +const testLogLevel: LogLevelDesc = "info"; + +import "jest-extended"; +import express from "express"; +import bodyParser from "body-parser"; +import http from "http"; +import { v4 as uuidV4 } from "uuid"; +import { AddressInfo } from "net"; +import { Server as SocketIoServer } from "socket.io"; +import Web3 from "web3"; + +import { + LogLevelDesc, + IListenOptions, + Servers, +} from "@hyperledger/cactus-common"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { pruneDockerAllIfGithubAction } from "@hyperledger/cactus-test-tooling"; +import { + GethTestLedger, + WHALE_ACCOUNT_ADDRESS, +} from "@hyperledger/cactus-test-geth-ledger"; + +import { + PluginLedgerConnectorEthereum, + Web3SigningCredentialType, + DefaultApi as EthereumApi, +} from "../../../main/typescript/public-api"; + +const containerImageName = "ghcr.io/outsh/cactus_geth_all_in_one"; +const containerImageVersion = "test-v01"; + +describe("Running ethereum transactions with different gas configurations", () => { + let web3: Web3, + addressInfo, + address: string, + port: number, + apiHost, + apiConfig, + ledger: GethTestLedger, + apiClient: EthereumApi, + connector: PluginLedgerConnectorEthereum, + rpcApiHttpHost: string; + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + const server = http.createServer(expressApp); + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + + ////////////////////////////////// + // Setup + ////////////////////////////////// + + beforeAll(async () => { + const pruning = pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + await expect(pruning).resolves.toBeTruthy(); + + //ledger = new GethTestLedger({ emitContainerLogs: true, testLogLevel }); + ledger = new GethTestLedger({ + containerImageName, + containerImageVersion, + }); + await ledger.start(); + + const listenOptions: IListenOptions = { + hostname: "0.0.0.0", + port: 0, + server, + }; + addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + ({ address, port } = addressInfo); + apiHost = `http://${address}:${port}`; + apiConfig = new Configuration({ basePath: apiHost }); + apiClient = new EthereumApi(apiConfig); + rpcApiHttpHost = await ledger.getRpcApiHttpHost(); + web3 = new Web3(rpcApiHttpHost); + + connector = new PluginLedgerConnectorEthereum({ + instanceId: uuidV4(), + rpcApiHttpHost, + logLevel: testLogLevel, + pluginRegistry: new PluginRegistry({ plugins: [] }), + }); + await connector.getOrCreateWebServices(); + await connector.registerWebServices(expressApp, wsApi); + }); + + afterAll(async () => { + await ledger.stop(); + await ledger.destroy(); + await Servers.shutdown(server); + + const pruning = pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + await expect(pruning).resolves.toBeTruthy(); + }); + + test("sending transfer without gas config works", async () => { + const testEthAccount = web3.eth.accounts.create(); + const transferValue = web3.utils.toWei(1, "ether"); + await apiClient.runTransactionV1({ + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + transactionConfig: { + from: WHALE_ACCOUNT_ADDRESS, + to: testEthAccount.address, + value: transferValue, + }, + }); + + const balance = await web3.eth.getBalance(testEthAccount.address); + expect(balance).toBeTruthy(); + expect(balance.toString()).toEqual(transferValue); + }); + + test("sending transfer with mixed gas config fails", async () => { + const testEthAccount = web3.eth.accounts.create(); + const transferValue = web3.utils.toWei(1, "ether"); + const maxFee = await connector.estimateMaxFeePerGas( + web3.utils.toWei(2, "gwei"), + ); + + try { + await apiClient.runTransactionV1({ + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + transactionConfig: { + from: WHALE_ACCOUNT_ADDRESS, + to: testEthAccount.address, + value: transferValue, + gasConfig: { + gas: "300000", + maxFeePerGas: maxFee, + }, + }, + }); + fail( + "Expected runTransactionV1 with mixed config to fail but it succeeded.", + ); + } catch (error) { + console.log("runTransactionV1 with mixed config failed as expected"); + } + + const balance = await web3.eth.getBalance(testEthAccount.address); + expect(balance.toString()).toEqual("0"); + }); + + test("sending transfer with only legacy gas price works", async () => { + const testEthAccount = web3.eth.accounts.create(); + const transferValue = web3.utils.toWei(1, "ether"); + const maxFee = await connector.estimateMaxFeePerGas( + web3.utils.toWei(2, "gwei"), + ); + + await apiClient.runTransactionV1({ + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + transactionConfig: { + from: WHALE_ACCOUNT_ADDRESS, + to: testEthAccount.address, + value: transferValue, + gasConfig: { + gasPrice: maxFee, + }, + }, + }); + + const balance = await web3.eth.getBalance(testEthAccount.address); + expect(balance).toBeTruthy(); + expect(balance.toString()).toEqual(transferValue); + }); + + test("sending transfer with only legacy gas (limit) works", async () => { + const testEthAccount = web3.eth.accounts.create(); + const transferValue = web3.utils.toWei(1, "ether"); + + await apiClient.runTransactionV1({ + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + transactionConfig: { + from: WHALE_ACCOUNT_ADDRESS, + to: testEthAccount.address, + value: transferValue, + gasConfig: { + gas: "300000", + }, + }, + }); + + const balance = await web3.eth.getBalance(testEthAccount.address); + expect(balance).toBeTruthy(); + expect(balance.toString()).toEqual(transferValue); + }); + + test("sending transfer with both legacy gas (limit) and gas price works", async () => { + const testEthAccount = web3.eth.accounts.create(); + const transferValue = web3.utils.toWei(1, "ether"); + const maxFee = await connector.estimateMaxFeePerGas( + web3.utils.toWei(2, "gwei"), + ); + + await apiClient.runTransactionV1({ + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + transactionConfig: { + from: WHALE_ACCOUNT_ADDRESS, + to: testEthAccount.address, + value: transferValue, + gasConfig: { + gas: "300000", + gasPrice: maxFee, + }, + }, + }); + + const balance = await web3.eth.getBalance(testEthAccount.address); + expect(balance).toBeTruthy(); + expect(balance.toString()).toEqual(transferValue); + }); + + test("sending transfer with only maxFeePerGas works", async () => { + const testEthAccount = web3.eth.accounts.create(); + const transferValue = web3.utils.toWei(1, "ether"); + const maxFee = await connector.estimateMaxFeePerGas( + web3.utils.toWei(2, "gwei"), + ); + + await apiClient.runTransactionV1({ + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + transactionConfig: { + from: WHALE_ACCOUNT_ADDRESS, + to: testEthAccount.address, + value: transferValue, + gasConfig: { + maxFeePerGas: maxFee, + }, + }, + }); + + const balance = await web3.eth.getBalance(testEthAccount.address); + expect(balance).toBeTruthy(); + expect(balance.toString()).toEqual(transferValue); + }); + + test("sending transfer with only maxPriorityFeePerGas works", async () => { + const testEthAccount = web3.eth.accounts.create(); + const transferValue = web3.utils.toWei(1, "ether"); + + await connector.transact({ + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + transactionConfig: { + from: WHALE_ACCOUNT_ADDRESS, + to: testEthAccount.address, + value: transferValue, + gasConfig: { + maxPriorityFeePerGas: web3.utils.toWei(2, "gwei"), + }, + }, + }); + + const balance = await web3.eth.getBalance(testEthAccount.address); + expect(balance).toBeTruthy(); + expect(balance.toString()).toEqual(transferValue); + }); + + test("sending transfer with only maxPriorityFeePerGas works", async () => { + const testEthAccount = web3.eth.accounts.create(); + const transferValue = web3.utils.toWei(1, "ether"); + const priorityFee = web3.utils.toWei(2, "gwei"); + const maxFee = await connector.estimateMaxFeePerGas(priorityFee); + + await apiClient.runTransactionV1({ + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + transactionConfig: { + from: WHALE_ACCOUNT_ADDRESS, + to: testEthAccount.address, + value: transferValue, + gasConfig: { + maxFeePerGas: maxFee, + maxPriorityFeePerGas: priorityFee, + }, + }, + }); + + const balance = await web3.eth.getBalance(testEthAccount.address); + expect(balance).toBeTruthy(); + expect(balance.toString()).toEqual(transferValue); + }); +}); diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/manual/geth-alchemy-integration-manual-check.test.ts b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/manual/geth-alchemy-integration-manual-check.test.ts index 601e3d0df3..5ced4aeee8 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/manual/geth-alchemy-integration-manual-check.test.ts +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/manual/geth-alchemy-integration-manual-check.test.ts @@ -83,8 +83,6 @@ describe("Alchemy integration manual tests", () => { secret: ETH_PRIVATE_KEY, type: Web3SigningCredentialType.PrivateKeyHex, }, - gas: 300000, - gasPrice: 400000, }); expect(deployOut).toBeTruthy(); expect(deployOut.transactionReceipt).toBeTruthy(); @@ -109,7 +107,6 @@ describe("Alchemy integration manual tests", () => { secret: ETH_PRIVATE_KEY, type: Web3SigningCredentialType.PrivateKeyHex, }, - gas: "300000", }); expect(invokeOut).toBeTruthy(); expect(invokeOut.callOutput).toBeTruthy(); diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/unit/model-type-guards.test.ts b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/unit/model-type-guards.test.ts index ee0886dda9..983bfa69d3 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/unit/model-type-guards.test.ts +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/test/typescript/unit/model-type-guards.test.ts @@ -1,5 +1,7 @@ import "jest-extended"; import { + isGasTransactionConfigEIP1559, + isGasTransactionConfigLegacy, isWeb3SigningCredentialGethKeychainPassword, isWeb3SigningCredentialNone, isWeb3SigningCredentialPrivateKeyHex, @@ -42,4 +44,64 @@ describe("Type guards for OpenAPI spec model type definitions", () => { expect(isWeb3SigningCredentialNone(valid)).toBe(true); expect(isWeb3SigningCredentialNone({})).not.toBe(true); }); + + test("isGasTransactionConfigLegacy()", () => { + expect( + isGasTransactionConfigLegacy({ + gas: "1234", + }), + ).toBe(true); + expect( + isGasTransactionConfigLegacy({ + gasPrice: "1234", + }), + ).toBe(true); + + expect( + isGasTransactionConfigLegacy({ + gasLimit: "1234", + }), + ).toBe(false); + expect( + isGasTransactionConfigLegacy({ + maxFeePerGas: "1234", + }), + ).toBe(false); + expect( + isGasTransactionConfigLegacy({ + maxPriorityFeePerGas: "1234", + }), + ).toBe(false); + expect(isGasTransactionConfigLegacy({})).toBe(false); + }); + + test("isGasTransactionConfigEIP1559()", () => { + expect( + isGasTransactionConfigEIP1559({ + gasLimit: "1234", + }), + ).toBe(true); + expect( + isGasTransactionConfigEIP1559({ + maxFeePerGas: "1234", + }), + ).toBe(true); + expect( + isGasTransactionConfigEIP1559({ + maxPriorityFeePerGas: "1234", + }), + ).toBe(true); + + expect( + isGasTransactionConfigEIP1559({ + gas: "1234", + }), + ).toBe(false); + expect( + isGasTransactionConfigEIP1559({ + gasPrice: "1234", + }), + ).toBe(false); + expect(isGasTransactionConfigEIP1559({})).toBe(false); + }); }); diff --git a/packages/cactus-test-plugin-ledger-connector-ethereum/src/test/typescript/integration/api-client/verifier-integration-with-ethereum-connector.test.ts b/packages/cactus-test-plugin-ledger-connector-ethereum/src/test/typescript/integration/api-client/verifier-integration-with-ethereum-connector.test.ts index 53c008b12a..f5575b552d 100644 --- a/packages/cactus-test-plugin-ledger-connector-ethereum/src/test/typescript/integration/api-client/verifier-integration-with-ethereum-connector.test.ts +++ b/packages/cactus-test-plugin-ledger-connector-ethereum/src/test/typescript/integration/api-client/verifier-integration-with-ethereum-connector.test.ts @@ -249,7 +249,6 @@ describe("Verifier integration with ethereum connector tests", () => { secret: "", type: Web3SigningCredentialType.GethKeychainPassword, }, - gas: 1000000, }); expect(deployOut).toBeTruthy(); expect(deployOut.transactionReceipt).toBeTruthy();