From e00cd07df92329d95d430350cdb68fff10713902 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 | 3 +- .../plugin-ledger-connector-polkadot.ts | 301 ++++++++++-------- .../get-raw-transaction-endpoint.ts | 4 +- .../integration/run-transaction.test.ts | 44 ++- yarn.lock | 3 +- 5 files changed, 208 insertions(+), 147 deletions(-) diff --git a/packages/cactus-plugin-ledger-connector-polkadot/package.json b/packages/cactus-plugin-ledger-connector-polkadot/package.json index 39ad00c061..6784334d48 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", @@ -94,7 +94,6 @@ "@hyperledger/cactus-plugin-keychain-memory": "2.0.0-rc.2", "@hyperledger/cactus-test-tooling": "2.0.0-rc.2", "@types/express": "4.17.21", - "@types/http-errors": "2.0.4", "@types/joi": "17.2.3", "@types/multer": "1.4.7", "@types/ssh2": "0.5.44", 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 cdab9b5236..cf7f585a7f 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,12 @@ 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 { + BadRequestError, + InternalServerError, + ServiceUnavailableError, +} from "http-errors-enhanced-cjs"; + import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; import { GetPrometheusMetricsEndpoint, @@ -36,6 +37,7 @@ import { Checks, LogLevelDesc, LoggerProvider, + newRex, } from "@hyperledger/cactus-common"; import { promisify } from "util"; import { @@ -87,7 +89,6 @@ import { IInvokeContractEndpointOptions, InvokeContractEndpoint, } from "./web-services/invoke-contract-endpoint"; -import createHttpError from "http-errors"; export interface IPluginLedgerConnectorPolkadotOptions extends ICactusPluginOptions { @@ -114,8 +115,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 +154,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,11 +264,25 @@ 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 (ex: unknown) { + const rex = newRex(`Could not create WsProvider. Inner exception:`, ex); + throw new InternalServerError(rex.toJSON()); + } + 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}`; - throw createHttpError[500](errorMessage); + } catch (ex: unknown) { + const rex = newRex(`Could not create API. Inner exception:`, ex); + throw new InternalServerError(rex.toJSON()); } } @@ -292,24 +297,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 +334,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()); } } @@ -334,13 +348,16 @@ export class PluginLedgerConnectorPolkadot const fnTag = `${this.className}#signTx()`; 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 ServiceUnavailableError("Substrate Node connection is down."); } 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, @@ -352,11 +369,10 @@ export class PluginLedgerConnectorPolkadot signedTransaction: serializedSignedTransaction, }; return response; - } catch (err) { - const errorMessage = - `${fnTag} signing raw transaction has failed. ` + - `InnerException: ${err}`; - throw createHttpError[500](errorMessage); + } catch (ex: unknown) { + const errorMsg = `${fnTag} signing raw Polkadot transaction failed.`; + const rex = newRex(errorMsg, ex); + throw new InternalServerError(rex.toJSON()); } } @@ -380,7 +396,7 @@ export class PluginLedgerConnectorPolkadot `${fnTag} Expected pre-signed raw transaction ` + ` since signing credential is specified as` + `Web3SigningCredentialType.NONE`; - throw createHttpError[400](errorMessage); + throw new BadRequestError(errorMessage); } } default: { @@ -388,7 +404,7 @@ export class PluginLedgerConnectorPolkadot `${fnTag} Unrecognized Web3SigningCredentialType: ` + `Supported ones are: ` + `${Object.values(Web3SigningCredentialType).join(";")}`; - throw createHttpError[400](errorMessage); + throw new BadRequestError(errorMessage); } } } @@ -423,19 +439,20 @@ export class PluginLedgerConnectorPolkadot const fnTag = `${this.className}#transactMnemonicString()`; 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 BadRequestError(`Connection to Substrate Node is down.`); + } + 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; if (!mnemonic) { - throw createHttpError[400]( - `cannot perform transaction without mnemonic string`, - ); + throw new BadRequestError(`Need mnemonic string for transaction`); } 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,34 +461,40 @@ 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`, + reject( + new ServiceUnavailableError(`Connection to Substrate Node down.`), ); } + 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 }) => { - if (!this.api) { - throw createHttpError[400]( - `The operation has failed because the API is not connected to Substrate Node`, - ); - } + .signAndSend(accountPair, async ({ status, txHash, dispatchError }) => { if (status.isInBlock) { if (dispatchError) { - reject("transaction not successful"); + reject( + new ServiceUnavailableError( + `Connection to Substrate Node down.`, + ), + ); 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, ); const { docs, name, section } = decoded; - throw createHttpError[400]( + throw new BadRequestError( `${section}.${name}: ${docs.join(" ")}`, ); } else { - throw createHttpError[400](dispatchError.toString()); + throw new BadRequestError(dispatchError.toString()); } } this.prometheusExporter.addCurrentTransaction(); @@ -500,6 +523,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( @@ -508,33 +535,32 @@ export class PluginLedgerConnectorPolkadot let success = false; 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 ServiceUnavailableError(`Connection to Substrate Node down.`); } const deserializedTransaction = this.api.tx(signedTx); const signature = deserializedTransaction.signature.toHex(); if (!signature) { - throw createHttpError[400](`${fnTag} Transaction is not signed.`); + throw new BadRequestError(`${fnTag} Transaction is not signed.`); } + const { isHex } = await import("@polkadot/util"); if (!isHex(signature)) { - throw createHttpError[400]( - `${fnTag} Transaction signature is not valid.`, - ); + throw new BadRequestError(`${fnTag} Transaction signature is not valid.`); } const txResult = await new Promise<{ 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`, + reject( + new ServiceUnavailableError(`Connection to Substrate Node down.`), ); } + if (!(this.api instanceof ApiPromise)) { + throw new InternalServerError("this.api was not instanceof ApiPromise"); + } this.api.rpc.author.submitAndWatchExtrinsic( deserializedTransaction, ({ isInBlock, hash, asInBlock, type }) => { @@ -546,9 +572,8 @@ export class PluginLedgerConnectorPolkadot transactionHash: hash.toHex(), }); } else { - reject("transaction not successful"); const errorMessage = `transaction not submitted with status: ${type}`; - throw new createHttpError[400](errorMessage); + reject(new BadRequestError(errorMessage)); } }, ); @@ -566,7 +591,7 @@ export class PluginLedgerConnectorPolkadot web3SigningCredential: Web3SigningCredential, ): Promise { if (isWeb3SigningCredentialNone(web3SigningCredential)) { - throw createHttpError[400]( + throw new BadRequestError( `${fnTag} Cannot ${type} contract with pre-signed TX`, ); } @@ -577,7 +602,7 @@ export class PluginLedgerConnectorPolkadot mnemonic = Credential.mnemonic; if (!mnemonic) { const errorMessage = `${fnTag} Cannot ${type} contract without mnemonic string.`; - throw createHttpError[400](errorMessage); + throw new BadRequestError(errorMessage); } return mnemonic; } else if (isWeb3SigningCredentialCactusRef(web3SigningCredential)) { @@ -586,7 +611,7 @@ export class PluginLedgerConnectorPolkadot const { keychainEntryKey, keychainId } = Credential; if (!keychainId || !keychainEntryKey) { const errorMessage = `${fnTag} Cannot ${type} contract without keychainId and the keychainEntryKey.`; - throw createHttpError[400](errorMessage); + throw new BadRequestError(errorMessage); } // locate the keychain plugin that has access to the keychain backend // denoted by the keychainID from the request. @@ -596,7 +621,7 @@ export class PluginLedgerConnectorPolkadot const errorMessage = `${fnTag} The plugin registry does not contain` + ` a keychain plugin for ID:"${keychainId}"`; - throw createHttpError[400](errorMessage); + throw new BadRequestError(errorMessage); } // Now use the found keychain plugin to actually perform the lookup of // the private key that we need to run the transaction. @@ -605,7 +630,7 @@ export class PluginLedgerConnectorPolkadot const errorMessage = `${fnTag} Cannot ${type} contract because` + `the mnemonic string does not exist on the keychain`; - throw new createHttpError[400](errorMessage); + throw new BadRequestError(errorMessage); } return mnemonic; } else { @@ -613,7 +638,7 @@ export class PluginLedgerConnectorPolkadot `${fnTag} Unrecognized Web3SigningCredentialType: ` + `Supported ones are: ` + `${Object.values(Web3SigningCredentialType).join(";")}`; - throw createHttpError[400](errorMessage); + throw new BadRequestError(errorMessage); } } @@ -624,9 +649,11 @@ export class PluginLedgerConnectorPolkadot const fnTag = `${this.className}#deployContract()`; 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 ServiceUnavailableError(`Connection to Substrate Node down.`); + } + 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, @@ -634,6 +661,7 @@ export class PluginLedgerConnectorPolkadot 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 +671,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, @@ -670,24 +701,24 @@ export class PluginLedgerConnectorPolkadot // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore ({ contract, status, dispatchError }) => { - if (!this.api) { - throw createHttpError[400]( - `The operation has failed because the API is not connected to Substrate Node`, - ); - } if (status.isInBlock || status.isFinalized) { if (dispatchError) { - reject("deployment not successful"); + reject(new BadRequestError("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, ); const { docs, name, section } = decoded; - throw createHttpError[400]( + throw new BadRequestError( `${section}.${name}: ${docs.join(" ")}`, ); } else { - throw createHttpError[400](dispatchError.toString()); + throw new BadRequestError(dispatchError.toString()); } } this.prometheusExporter.addCurrentTransaction(); @@ -708,10 +739,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`, @@ -731,21 +766,25 @@ export class PluginLedgerConnectorPolkadot const fnTag = `${this.className}#invokeContract()`; 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 ServiceUnavailableError(`Connection to Substrate Node down.`); } + 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, methodName, ); if (!isSafeToCall) { - throw createHttpError[400]( + throw new BadRequestError( `Invalid method name provided in request. ${req.methodName} does not exist on the contract abi.messages object's "method" property.`, ); } @@ -754,7 +793,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 +803,7 @@ export class PluginLedgerConnectorPolkadot const query = contract.query[methodName]( req.accountAddress, { - gasLimit, + gasLimit: gasLimit as string, // FIXME, storageDepositLimit: req.storageDepositLimit, value: req.balance, }, @@ -779,13 +818,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 string, // FIXME storageDepositLimit: req.storageDepositLimit, value: req.balance, }, @@ -798,23 +838,26 @@ export class PluginLedgerConnectorPolkadot }>((resolve, reject) => { tx.signAndSend(accountPair, ({ status, txHash, dispatchError }) => { if (!this.api) { - throw createHttpError[400]( - `The operation has failed because the API is not connected to Substrate Node`, - ); + throw new ServiceUnavailableError(`Substrate node connection down`); } if (status.isInBlock || status.isFinalized) { 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, ); const { docs, name, section } = decoded; - throw createHttpError[400]( + throw new BadRequestError( `${section}.${name}: ${docs.join(" ")}`, ); } else { - throw createHttpError[400](dispatchError.toString()); + throw new BadRequestError(dispatchError.toString()); } } this.prometheusExporter.addCurrentTransaction(); @@ -831,7 +874,7 @@ export class PluginLedgerConnectorPolkadot const blockHash = txResult.blockHash; return { success, txHash, blockHash }; } else { - throw createHttpError[400]( + throw new BadRequestError( `${fnTag} Unsupported invocation type ${req.invocationType}`, ); } @@ -842,16 +885,14 @@ export class PluginLedgerConnectorPolkadot } public async getPrometheusExporterMetrics(): Promise { - const fnTag = `${this.className}#getPrometheusExporterMetrics()`; + const fn = `${this.className}#getPrometheusExporterMetrics()`; try { const res: string = await this.prometheusExporter.getPrometheusMetrics(); this.log.debug(`getPrometheusExporterMetrics() response: %o`, res); return res; - } catch (err) { - throw createHttpError[500]( - `${fnTag} Obtaining Prometheus Exporter Metrics has failed. ` + - `InnerException: ${err}`, - ); + } catch (ex: unknown) { + const rex = newRex(`${fn} Failed to get Prometheus exporter metrics`, ex); + throw new InternalServerError(rex.toJSON()); } } @@ -863,17 +904,20 @@ export class PluginLedgerConnectorPolkadot Checks.truthy(req, `${fnTag} req`); this.log.info(`getTxFee`); if (!this.api) { - throw createHttpError[400]( - `The operation has failed because the API is not connected to Substrate Node`, - ); + throw new ServiceUnavailableError(`Substrate node connection down`); } 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, @@ -897,17 +941,21 @@ export class PluginLedgerConnectorPolkadot }; return response; - } catch (err) { - throw createHttpError[500]( - `${fnTag} Obtaining info for this transaction has failed. ` + - `InnerException: ${err}`, - ); + } catch (ex: unknown) { + const rex = newRex(`${fnTag} Obtaining info for transaction failed.`, ex); + throw new InternalServerError(rex.toJSON()); } } 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 { @@ -915,11 +963,10 @@ export class PluginLedgerConnectorPolkadot "Trying to shutdown connection to substrate, but no connection is available", ); } - } catch (err) { - this.log.error("Could not disconnect from Substrate Ledger"); - throw createHttpError[500]( - `Could not disconnect from Substrate Ledger. InnerException: ${err} `, - ); + } catch (ex: unknown) { + this.log.error("Could not disconnect from Substrate Ledger:", ex); + const rex = newRex("Could not disconnect from Substrate Ledger", ex); + throw new InternalServerError(rex.toJSON()); } } } 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 69df6aeb89..03b2c9ac56 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 5375995cc9..767674df6a 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 26eba0089a..ef7c6a3f17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10493,7 +10493,6 @@ __metadata: "@polkadot/types": "npm:10.9.1" "@polkadot/util": "npm:12.6.2" "@types/express": "npm:4.17.21" - "@types/http-errors": "npm:2.0.4" "@types/joi": "npm:17.2.3" "@types/multer": "npm:1.4.7" "@types/ssh2": "npm:0.5.44" @@ -10506,7 +10505,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"