From cf2727da3a763375412c7288abab3e20fb87058e Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Mon, 15 Jul 2024 16:02:18 -0700 Subject: [PATCH] fix(connector-polkadot): use dynamic import calls for ESM dependencies 1. The polkadot libraries are all ESM-only so we have to import them in a specific way in order to avoid runtime crashes. 2. This was not done in the original implementation of the polkadot connector and because of that all the tests were failing. 3. Refactoring the code so that all polkadot related dependencies are imported dynamically fixes the issue. 4. Also fixed the issue where the getRawTransaction HTTP REST handler was not `await` ing for the result of the connector's method which broke the run-transaction test case (now also fixed) Fixes #3077 Signed-off-by: Peter Somogyvari --- .../package.json | 2 +- .../plugin-ledger-connector-polkadot.ts | 156 +++++++++++++----- .../get-raw-transaction-endpoint.ts | 4 +- .../integration/run-transaction.test.ts | 44 +++-- yarn.lock | 2 +- 5 files changed, 151 insertions(+), 57 deletions(-) diff --git a/packages/cactus-plugin-ledger-connector-polkadot/package.json b/packages/cactus-plugin-ledger-connector-polkadot/package.json index 39ad00c0610..6e0177fac3f 100644 --- a/packages/cactus-plugin-ledger-connector-polkadot/package.json +++ b/packages/cactus-plugin-ledger-connector-polkadot/package.json @@ -77,7 +77,7 @@ "express-openapi-validator": "4.13.1", "form-data": "4.0.0", "fs-extra": "11.2.0", - "http-errors": "2.0.0", + "http-errors-enhanced-cjs": "2.0.1", "http-status-codes": "2.1.4", "joi": "17.13.3", "multer": "1.4.2", diff --git a/packages/cactus-plugin-ledger-connector-polkadot/src/main/typescript/plugin-ledger-connector-polkadot.ts b/packages/cactus-plugin-ledger-connector-polkadot/src/main/typescript/plugin-ledger-connector-polkadot.ts index cdab9b5236b..caf8905f3ee 100644 --- a/packages/cactus-plugin-ledger-connector-polkadot/src/main/typescript/plugin-ledger-connector-polkadot.ts +++ b/packages/cactus-plugin-ledger-connector-polkadot/src/main/typescript/plugin-ledger-connector-polkadot.ts @@ -1,11 +1,10 @@ import { Server } from "http"; import { Server as SecureServer } from "https"; import { Express } from "express"; -import { ApiPromise, Keyring } from "@polkadot/api"; -import { WsProvider } from "@polkadot/rpc-provider/ws"; -import { WeightV2 } from "@polkadot/types/interfaces"; -import { CodePromise, Abi, ContractPromise } from "@polkadot/api-contract"; -import { isHex, stringCamelCase } from "@polkadot/util"; +import createHttpError from "http-errors"; +import { InternalServerError } from "http-errors-enhanced-cjs"; +import { ServiceUnavailableError } from "http-errors-enhanced-cjs"; + import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; import { GetPrometheusMetricsEndpoint, @@ -36,6 +35,7 @@ import { Checks, LogLevelDesc, LoggerProvider, + newRex, } from "@hyperledger/cactus-common"; import { promisify } from "util"; import { @@ -87,7 +87,6 @@ import { IInvokeContractEndpointOptions, InvokeContractEndpoint, } from "./web-services/invoke-contract-endpoint"; -import createHttpError from "http-errors"; export interface IPluginLedgerConnectorPolkadotOptions extends ICactusPluginOptions { @@ -114,8 +113,8 @@ export class PluginLedgerConnectorPolkadot private readonly instanceId: string; private readonly log: Logger; private readonly pluginRegistry: PluginRegistry; - public wsProvider: WsProvider | undefined; - public api: ApiPromise | undefined; + public wsProvider: unknown; + public api?: unknown; public prometheusExporter: PrometheusExporter; private endpoints: IWebServiceEndpoint[] | undefined; private autoConnect: false | number | undefined; @@ -153,19 +152,9 @@ export class PluginLedgerConnectorPolkadot if (opts.autoConnect) { this.autoConnect = 1; } - this.setProvider(opts.wsProviderUrl); this.prometheusExporter.startMetricsCollection(); } - public setProvider(wsProviderUrl: string): void { - try { - this.wsProvider = new WsProvider(wsProviderUrl, this.autoConnect); - } catch (err) { - const errorMessage = `Could not create wsProvider. InnerException: + ${err}`; - throw createHttpError[500](errorMessage); - } - } - public async getOrCreateWebServices(): Promise { if (Array.isArray(this.endpoints)) { return this.endpoints; @@ -273,7 +262,21 @@ export class PluginLedgerConnectorPolkadot } public async onPluginInit(): Promise { + const { WsProvider } = await import("@polkadot/rpc-provider"); + const { ApiPromise } = await import("@polkadot/api"); + try { + this.wsProvider = new WsProvider( + this.opts.wsProviderUrl, + this.autoConnect, + ); + } catch (err) { + const errorMessage = `Could not create wsProvider. InnerException: + ${err}`; + throw createHttpError[500](errorMessage); + } try { + if (!(this.wsProvider instanceof WsProvider)) { + throw new InternalServerError("this.wsProvider was not a WsProvider"); + } this.api = await ApiPromise.create({ provider: this.wsProvider }); } catch (err) { const errorMessage = `Could not create API. InnerException: + ${err}`; @@ -292,24 +295,34 @@ export class PluginLedgerConnectorPolkadot return consensusHasTransactionFinality(currentConsensusAlgorithmFamily); } - public rawTransaction(req: RawTransactionRequest): RawTransactionResponse { - const fnTag = `${this.className}#rawTx()`; + public async rawTransaction( + req: RawTransactionRequest, + ): Promise { + const fnTag = `${this.className}#rawTransaction()`; Checks.truthy(req, `${fnTag} req`); if (!this.api) { - throw createHttpError[400]( - `The operation has failed because the API is not connected to Substrate Node`, - ); + throw new InternalServerError(`${fnTag} this.api is falsy.`); + } + const { ApiPromise } = await import("@polkadot/api"); + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError("this.api was not instanceof ApiPromise"); } try { const accountAddress = req.to; const transferValue = req.value; - const rawTransaction = this.api.tx.balances.transferAllowDeath( + + this.log.debug("%s transferAllowDeath %s, %d", fnTag, req.to, req.value); + + const rawTx = this.api.tx.balances.transferAllowDeath( accountAddress, transferValue, ); + + this.log.debug("%s transferAllowDeath rawTx=%o", rawTx.toHuman()); + const responseContainer = { response_data: { - rawTransaction: rawTransaction.toHex(), + rawTransaction: rawTx.toHex(), }, succeeded: true, message: "obtainRawTransaction", @@ -319,12 +332,11 @@ export class PluginLedgerConnectorPolkadot const response: RawTransactionResponse = { responseContainer: responseContainer, }; + this.log.debug("%s res %o", fnTag, response); return response; - } catch (err) { - const errorMessage = - `${fnTag} Obtaining raw transaction has failed. ` + - `InnerException: ${err}`; - throw createHttpError[500](errorMessage); + } catch (ex: unknown) { + const rex = newRex(`${fnTag} Obtaining raw transaction has failed:`, ex); + throw new InternalServerError(rex.toJSON()); } } @@ -339,8 +351,13 @@ export class PluginLedgerConnectorPolkadot ); } try { + const { Keyring } = await import("@polkadot/api"); const keyring = new Keyring({ type: "sr25519" }); const accountPair = keyring.createFromUri(req.mnemonic); + const { ApiPromise } = await import("@polkadot/api"); + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError("this.api was not instanceof ApiPromise"); + } const deserializedRawTransaction = this.api.tx(req.rawTransaction); const signedTransaction = await deserializedRawTransaction.signAsync( accountPair, @@ -427,6 +444,10 @@ export class PluginLedgerConnectorPolkadot `The operation has failed because the API is not connected to Substrate Node`, ); } + const { ApiPromise } = await import("@polkadot/api"); + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError("this.api was not instanceof ApiPromise"); + } const { transactionConfig, web3SigningCredential } = req; const { mnemonic } = web3SigningCredential as Web3SigningCredentialMnemonicString; @@ -436,6 +457,7 @@ export class PluginLedgerConnectorPolkadot ); } let success = false; + const { Keyring } = await import("@polkadot/api"); const keyring = new Keyring({ type: "sr25519" }); const accountPair = keyring.createFromUri(mnemonic); const accountAddress = transactionConfig.to; @@ -444,16 +466,19 @@ export class PluginLedgerConnectorPolkadot success: boolean; transactionHash: string; blockhash: string; - }>((resolve, reject) => { + }>(async (resolve, reject) => { if (!this.api) { reject("transaction not successful"); throw createHttpError[400]( `The operation has failed because the API is not connected to Substrate Node`, ); } + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError("this.api was not instanceof ApiPromise"); + } this.api.tx.balances .transferAllowDeath(accountAddress, transferValue) - .signAndSend(accountPair, ({ status, txHash, dispatchError }) => { + .signAndSend(accountPair, async ({ status, txHash, dispatchError }) => { if (!this.api) { throw createHttpError[400]( `The operation has failed because the API is not connected to Substrate Node`, @@ -463,6 +488,11 @@ export class PluginLedgerConnectorPolkadot if (dispatchError) { reject("transaction not successful"); if (dispatchError.isModule) { + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError( + "this.api was not instanceof ApiPromise", + ); + } const decoded = this.api.registry.findMetaError( dispatchError.asModule, ); @@ -500,6 +530,10 @@ export class PluginLedgerConnectorPolkadot req.transactionConfig.transferSubmittable, `${fnTag}:req.transactionConfig.transferSubmittable`, ); + const { ApiPromise } = await import("@polkadot/api"); + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError("this.api was not instanceof ApiPromise"); + } const signedTx = req.transactionConfig.transferSubmittable as string; this.log.debug( @@ -518,6 +552,7 @@ export class PluginLedgerConnectorPolkadot throw createHttpError[400](`${fnTag} Transaction is not signed.`); } + const { isHex } = await import("@polkadot/util"); if (!isHex(signature)) { throw createHttpError[400]( `${fnTag} Transaction signature is not valid.`, @@ -528,13 +563,16 @@ export class PluginLedgerConnectorPolkadot success: boolean; transactionHash: string; blockhash: string; - }>((resolve, reject) => { + }>(async (resolve, reject) => { if (!this.api) { reject("transaction not successful"); throw createHttpError[400]( `The operation has failed because the API is not connected to Substrate Node`, ); } + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError("this.api was not instanceof ApiPromise"); + } this.api.rpc.author.submitAndWatchExtrinsic( deserializedTransaction, ({ isInBlock, hash, asInBlock, type }) => { @@ -628,12 +666,17 @@ export class PluginLedgerConnectorPolkadot `The operation has failed because the API is not connected to Substrate Node`, ); } + const { ApiPromise } = await import("@polkadot/api"); + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError("this.api was not instanceof ApiPromise"); + } const mnemonic = await this.getMnemonicStringFromWeb3SigningCredential( fnTag, "deploy", req.web3SigningCredential, ); let success = false; + const { CodePromise, Abi } = await import("@polkadot/api-contract"); const contractAbi = new Abi( req.metadata, this.api.registry.getChainProperties(), @@ -643,17 +686,20 @@ export class PluginLedgerConnectorPolkadot contractAbi, Buffer.from(req.wasm, "base64"), ); - const gasLimit: WeightV2 = this.api.registry.createType("WeightV2", { + const gasLimit: unknown = this.api.registry.createType("WeightV2", { refTime: req.gasLimit.refTime, proofSize: req.gasLimit.proofSize, }); + const { Keyring } = await import("@polkadot/api"); + const { stringCamelCase } = await import("@polkadot/util"); + const keyring = new Keyring({ type: "sr25519" }); const accountPair = keyring.createFromUri(mnemonic); const params = req.params ?? []; const constructorMethod = req.constructorMethod ?? "new"; const tx = contractCode.tx[stringCamelCase(constructorMethod)]( { - gasLimit, + gasLimit: gasLimit as string, // FIXME storageDepositLimit: req.storageDepositLimit, salt: req.salt, value: req.balance, @@ -679,6 +725,11 @@ export class PluginLedgerConnectorPolkadot if (dispatchError) { reject("deployment not successful"); if (dispatchError.isModule) { + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError( + "this.api was not instanceof ApiPromise", + ); + } const decoded = this.api.registry.findMetaError( dispatchError.asModule, ); @@ -708,10 +759,14 @@ export class PluginLedgerConnectorPolkadot } public async isSafeToCallContractMethod( - abi: Abi, + abi: unknown, name: string, ): Promise { Checks.truthy(abi, `${this.className}#isSafeToCallContractMethod():abi`); + const { Abi } = await import("@polkadot/api-contract"); + if (!(abi instanceof Abi)) { + throw new Error("Expected abi arg as instanceof Abi"); + } Checks.truthy( abi.messages, `${this.className}#isSafeToCallContractMethod():abi.messages`, @@ -735,10 +790,16 @@ export class PluginLedgerConnectorPolkadot `The operation has failed because the API is not connected to Substrate Node`, ); } + const { ApiPromise } = await import("@polkadot/api"); + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError("this.api was not instanceof ApiPromise"); + } + const { Abi, ContractPromise } = await import("@polkadot/api-contract"); const contractAbi = new Abi( req.metadata, this.api.registry.getChainProperties(), ); + const { stringCamelCase } = await import("@polkadot/util"); const methodName = stringCamelCase(req.methodName); const isSafeToCall = await this.isSafeToCallContractMethod( contractAbi, @@ -754,7 +815,7 @@ export class PluginLedgerConnectorPolkadot req.metadata, req.contractAddress, ); - const gasLimit: WeightV2 = this.api.registry.createType("WeightV2", { + const gasLimit: unknown = this.api.registry.createType("WeightV2", { refTime: req.gasLimit.refTime, proofSize: req.gasLimit.proofSize, }); @@ -764,7 +825,7 @@ export class PluginLedgerConnectorPolkadot const query = contract.query[methodName]( req.accountAddress, { - gasLimit, + gasLimit: gasLimit as any, // FIXME, storageDepositLimit: req.storageDepositLimit, value: req.balance, }, @@ -779,13 +840,14 @@ export class PluginLedgerConnectorPolkadot "invoke", req.web3SigningCredential, ); + const { Keyring } = await import("@polkadot/api"); const keyring = new Keyring({ type: "sr25519" }); const accountPair = keyring.createFromUri(mnemonic); let success = false; const params = req.params ?? []; const tx = contract.tx[methodName]( { - gasLimit, + gasLimit: gasLimit as any, // FIXME storageDepositLimit: req.storageDepositLimit, value: req.balance, }, @@ -806,6 +868,11 @@ export class PluginLedgerConnectorPolkadot if (dispatchError) { reject("TX not successful"); if (dispatchError.isModule) { + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError( + "this.api was not instanceof ApiPromise", + ); + } const decoded = this.api.registry.findMetaError( dispatchError.asModule, ); @@ -869,11 +936,16 @@ export class PluginLedgerConnectorPolkadot } const accountAddress = req.accountAddress; const transactionExpiration = (req.transactionExpiration as number) || 50; + const { ApiPromise } = await import("@polkadot/api"); + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError("this.api was not instanceof ApiPromise"); + } try { const signedBlock = await this.api.rpc.chain.getBlock(); const nonce = (await this.api.derive.balances.account(accountAddress)) .accountNonce; const blockHash = signedBlock.block.header.hash; + const era = this.api.createType("ExtrinsicEra", { current: signedBlock.block.header.number, period: transactionExpiration, @@ -908,6 +980,12 @@ export class PluginLedgerConnectorPolkadot public async shutdownConnectionToSubstrate(): Promise { try { if (this.api) { + const { ApiPromise } = await import("@polkadot/api"); + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError( + "this.api was not instanceof ApiPromise", + ); + } this.log.info("Shutting down connection to substrate..."); this.api.disconnect(); } else { diff --git a/packages/cactus-plugin-ledger-connector-polkadot/src/main/typescript/web-services/get-raw-transaction-endpoint.ts b/packages/cactus-plugin-ledger-connector-polkadot/src/main/typescript/web-services/get-raw-transaction-endpoint.ts index 69df6aeb891..03b2c9ac567 100644 --- a/packages/cactus-plugin-ledger-connector-polkadot/src/main/typescript/web-services/get-raw-transaction-endpoint.ts +++ b/packages/cactus-plugin-ledger-connector-polkadot/src/main/typescript/web-services/get-raw-transaction-endpoint.ts @@ -84,13 +84,13 @@ export class GetRawTransactionEndpoint implements IWebServiceEndpoint { return this; } - handleRequest(req: Request, res: Response): void { + async handleRequest(req: Request, res: Response): Promise { const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); const reqBody = req.body; try { - const resBody = this.opts.connector.rawTransaction(reqBody); + const resBody = await this.opts.connector.rawTransaction(reqBody); res.json(resBody); } catch (ex) { const errorMsg = `${reqTag} ${fnTag} Failed to get Raw Transaction:`; diff --git a/packages/cactus-plugin-ledger-connector-polkadot/src/test/typescript/integration/run-transaction.test.ts b/packages/cactus-plugin-ledger-connector-polkadot/src/test/typescript/integration/run-transaction.test.ts index 5375995cc9e..767674df6a9 100644 --- a/packages/cactus-plugin-ledger-connector-polkadot/src/test/typescript/integration/run-transaction.test.ts +++ b/packages/cactus-plugin-ledger-connector-polkadot/src/test/typescript/integration/run-transaction.test.ts @@ -1,13 +1,19 @@ +import { AddressInfo } from "net"; +import http from "http"; +import "jest-extended"; +import { v4 as uuidv4 } from "uuid"; +import express from "express"; + import { IListenOptions, LogLevelDesc, Servers, } from "@hyperledger/cactus-common"; -import { SubstrateTestLedger } from "../../../../../cactus-test-tooling/src/main/typescript/substrate-test-ledger/substrate-test-ledger"; -import { v4 as uuidv4 } from "uuid"; +import { Configuration, PluginImportType } from "@hyperledger/cactus-core-api"; import { pruneDockerAllIfGithubAction } from "@hyperledger/cactus-test-tooling"; -import express from "express"; -import http from "http"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import { PluginRegistry } from "@hyperledger/cactus-core"; + import { PluginLedgerConnectorPolkadot, IPluginLedgerConnectorPolkadotOptions, @@ -15,23 +21,17 @@ import { Web3SigningCredentialType, PluginFactoryLedgerConnector, } from "../../../main/typescript/public-api"; -import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; -import { PluginRegistry } from "@hyperledger/cactus-core"; -import { AddressInfo } from "net"; -import { Configuration, PluginImportType } from "@hyperledger/cactus-core-api"; -import { Keyring } from "@polkadot/api"; import { K_CACTUS_POLKADOT_TOTAL_TX_COUNT } from "../../../main/typescript/prometheus-exporter/metrics"; -import "jest-extended"; +import { SubstrateTestLedger } from "../../../../../cactus-test-tooling/src/main/typescript/substrate-test-ledger/substrate-test-ledger"; -const testCase = "transact through all available methods"; -describe(testCase, () => { +describe("PluginLedgerConnectorPolkadot", () => { const logLevel: LogLevelDesc = "TRACE"; const DEFAULT_WSPROVIDER = "ws://127.0.0.1:9944"; const instanceId = "test-polkadot-connector"; const ledgerOptions = { publishAllPorts: false, - logLevel: logLevel, + logLevel: "INFO" as LogLevelDesc, emitContainerLogs: true, }; const ledger = new SubstrateTestLedger(ledgerOptions); @@ -54,15 +54,19 @@ describe(testCase, () => { const pruning = pruneDockerAllIfGithubAction({ logLevel }); await expect(pruning).toResolve(); }); + afterAll(async () => { await ledger.stop(); await plugin.shutdownConnectionToSubstrate(); }); + afterAll(async () => { const pruning = pruneDockerAllIfGithubAction({ logLevel }); await expect(pruning).resolves.toBeTruthy(); }); + afterAll(async () => await Servers.shutdown(server)); + beforeAll(async () => { const ledgerContainer = await ledger.start(); expect(ledgerContainer).toBeTruthy(); @@ -77,6 +81,7 @@ describe(testCase, () => { backend: new Map([[keychainEntryKey, keychainEntryValue]]), logLevel, }); + const connectorOptions: IPluginLedgerConnectorPolkadotOptions = { logLevel: logLevel, pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), @@ -104,7 +109,10 @@ describe(testCase, () => { await plugin.registerWebServices(expressApp); await plugin.getOrCreateWebServices(); }); + test("transact using pre-signed transaction", async () => { + const { Keyring } = await import("@polkadot/api"); + const keyring = new Keyring({ type: "sr25519" }); const alicePair = keyring.createFromUri("//Alice"); const bobPair = keyring.createFromUri("//Bob"); @@ -143,7 +151,7 @@ describe(testCase, () => { const signedTransactionResponse = await apiClient.signRawTransaction({ rawTransaction: rawTransaction, mnemonic: "//Alice", - signingOptions: signingOptions, + signingOptions, }); expect(signedTransactionResponse.data.success).toBeTrue(); expect(signedTransactionResponse.data.signedTransaction).toBeTruthy(); @@ -163,6 +171,8 @@ describe(testCase, () => { }); test("transact by omiting mnemonic string", async () => { + const { Keyring } = await import("@polkadot/api"); + const keyring = new Keyring({ type: "sr25519" }); const bobPair = keyring.createFromUri("//Bob"); const TransactionDetails = apiClient.runTransaction({ @@ -181,7 +191,10 @@ describe(testCase, () => { 400, ); }); + test("transact using passing mnemonic string", async () => { + const { Keyring } = await import("@polkadot/api"); + const keyring = new Keyring({ type: "sr25519" }); const bobPair = keyring.createFromUri("//Bob"); const TransactionDetails = await apiClient.runTransaction({ @@ -201,7 +214,10 @@ describe(testCase, () => { expect(transactionResponse.txHash).toBeTruthy(); expect(transactionResponse.blockHash).toBeTruthy(); }); + test("transact using passing cactus keychain ref", async () => { + const { Keyring } = await import("@polkadot/api"); + const keyring = new Keyring({ type: "sr25519" }); const bobPair = keyring.createFromUri("//Bob"); const TransactionDetails = await apiClient.runTransaction({ diff --git a/yarn.lock b/yarn.lock index 26eba0089a0..10c3772462a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10506,7 +10506,7 @@ __metadata: express-openapi-validator: "npm:4.13.1" form-data: "npm:4.0.0" fs-extra: "npm:11.2.0" - http-errors: "npm:2.0.0" + http-errors-enhanced-cjs: "npm:2.0.1" http-status-codes: "npm:2.1.4" joi: "npm:17.13.3" multer: "npm:1.4.2"