diff --git a/README.md b/README.md index b6a2983d6..da0e33722 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ a 32-byte hex string (`0x` followed by 64 hexadecimal digits) that denotes the b - [`/node/version` fetch information about the Substrates node's implementation and versioning.](src/controllers/node/NodeVersionController.ts) -- [`/runtime/metadata` fetch the runtime metadata in decoded, JSON form.](src/controllers/runtime/RuntimeMetadataController.ts) (replaces `/metadata`) +- [`/runtime/metadata` fetch the runtime metadata in decoded, JSON form.](src/controllers/runtime/RuntimeMetadataController.ts) (Replaces `/metadata`.) - [`/runtime/code` fetch the Wasm code blob of the Substrate runtime.](src/controllers/runtime/RuntimeCodeController.ts) @@ -116,15 +116,13 @@ a 32-byte hex string (`0x` followed by 64 hexadecimal digits) that denotes the b - [`/claims/ADDRESS/NUMBER` fetch claims data for an Ethereum `ADDRESS` at the block identified by 'NUMBER`.](src/controllers/claims/ClaimsController.ts) -- [`/tx/artifacts/` fetch artifacts used for creating transactions at latest finalized block.](src/controllers/transaction/TransactionMaterialController.ts) +- [`/transaction/material` fetch all the network information needed to construct a transaction offline.](src/controllers/transaction/TransactionMaterialController.ts) (Replaces `/tx/artifacts`.) -- [`/tx/artifacts/NUMBER` fetch artifacts used for creating transactions at the block identified by 'NUMBER`.](src/controllers/transaction/TransactionMaterialController.ts) - -- [`/tx/fee-estimate` submit a transaction in order to get back a fee estimation.](src/controllers/transaction/TransactionFeeEstimateController.ts) Expects a string +- [`/transaction/fee-estimate` submit a transaction in order to get back a fee estimation.](src/controllers/transaction/TransactionFeeEstimateController.ts) (Replaces `/tx/fee-estimate`.) Expects a string with a hex-encoded transaction in a JSON POST body: ``` - curl localhost:8080/tx/fee-estimate -X POST --data '{"tx": "0x..."}' -H 'Content-Type: application/json' + curl localhost:8080/transaction/fee-estimate -X POST --data '{"tx": "0x..."}' -H 'Content-Type: application/json' ``` Expected result is a JSON with fee information: @@ -137,10 +135,10 @@ a 32-byte hex string (`0x` followed by 64 hexadecimal digits) that denotes the b } ``` -- [`/tx/` submit a signed transaction.](src/controllers/transaction/TransactionSubmitController.ts) Expects a string with hex-encoded transaction in a JSON POST +- [`/transaction` submit a signed transaction.](src/controllers/transaction/TransactionSubmitController.ts) (Replaces `/tx`.) Expects a string with hex-encoded transaction in a JSON POST body: ``` - curl localhost:8080/tx/ -X POST --data '{"tx": "0x..."}' -H 'Content-Type: application/json' + curl localhost:8080/transaction -X POST --data '{"tx": "0x..."}' -H 'Content-Type: application/json' ``` Expected result is a JSON with transaction hash: ``` @@ -149,7 +147,7 @@ a 32-byte hex string (`0x` followed by 64 hexadecimal digits) that denotes the b } ``` -- [`transaction/dry-run` dry run a transaction to check if it is valid.](src/controllers/transaction/TransactionDryRunController.ts) +- [`/transaction/dry-run` dry run a transaction to check if it is valid.](src/controllers/transaction/TransactionDryRunController.ts) Expects a string with hex-encoded transaction in a JSON POST body: ``` diff --git a/openapi/openapi-proposal.yaml b/openapi/openapi-proposal.yaml index 23077e419..0534eb0ed 100755 --- a/openapi/openapi-proposal.yaml +++ b/openapi/openapi-proposal.yaml @@ -366,8 +366,15 @@ paths: tags: - transaction summary: Receive a fee estimate for a transaction. - description: Send a serialized transaction and receive back a naive fee - estimate. Replaces `/tx/fee-estimate` from versions < v1.0.0. + description: >- + Send a serialized transaction and receive back a naive fee estimate. + Note: `partialFee` does not include any tips that you may add to increase + a transaction's priority. See the reference on `compute_fee`. + Replaces `/tx/fee-estimate` from versions < v1.0.0. + Substrate Reference: + - `RuntimeDispatchInfo`: https://crates.parity.io/pallet_transaction_payment_rpc_runtime_api/struct.RuntimeDispatchInfo.html + - `query_info`: https://crates.parity.io/pallet_transaction_payment/struct.Module.html#method.query_info + - `compute_fee`: https://crates.parity.io/pallet_transaction_payment/struct.Module.html#method.compute_fee operationId: feeEstimateTransaction requestBody: $ref: '#/components/requestBodies/Transaction' @@ -388,7 +395,7 @@ paths: get: tags: - transaction - summary: Get the baseline material to construct a transaction. + summary: Get all the network information needed to construct a transaction offline. description: Returns the material that is universal to constructing any signed transaction offline. Replaces `/tx/artifacts` from versions < v1.0.0. operationId: getTransactionMaterial @@ -402,6 +409,13 @@ paths: type: string description: Block identifier, as the block height or block hash. format: unsignedInteger or $hex + - name: noMeta + in: query + schema: + type: boolean + description: If true, does not return metadata hex. This is useful when + metadata is not needed and response time is a concern. Defaults to false. + default: false responses: "200": description: successful operation @@ -1784,7 +1798,7 @@ components: Transaction: type: object properties: - transaction: + tx: type: string format: hex TransactionMaterial: @@ -1814,12 +1828,16 @@ components: metadata: type: string description: The chain's metadata in hex format. - format: hexScaleEncoded + format: hex description: >- Note: `chainName`, `specName`, and `specVersion` are used to define a type registry with a set of signed extensions and types. For Polkadot and Kusama, `chainName` is not used in defining this registry, but in other Substrate-based chains that re-launch their network without changing the `specName`, the `chainName` would be needed to create the correct registry. + Substrate Reference: + - `RuntimeVersion`: https://crates.parity.io/sp_version/struct.RuntimeVersion.html + - `SignedExtension`: https://crates.parity.io/sp_runtime/traits/trait.SignedExtension.html + - FRAME Support: https://crates.parity.io/frame_support/metadata/index.html TransactionSuccess: type: object properties: @@ -1831,7 +1849,7 @@ components: properties: code: type: number - message: + error: type: string description: >- `Failed to parse a tx.` @@ -1848,9 +1866,9 @@ components: properties: code: type: number - message: + error: type: string - description: Failed to submit a tx + description: Failed to submit transaction. transaction: type: string format: hex @@ -1869,9 +1887,14 @@ components: properties: code: type: number - message: + at: + type: object + properties: + hash: + type: string + error: type: string - description: Unable to fetch fee info + description: Error description. transaction: type: string format: hex @@ -1880,6 +1903,7 @@ components: description: Block hash of the block fee estimation was attempted at. cause: type: string + description: Error message from the client. stack: type: string TransactionFeeEstimate: diff --git a/src/controllers/transaction/TransactionFeeEstimateController.ts b/src/controllers/transaction/TransactionFeeEstimateController.ts new file mode 100644 index 000000000..6be63917c --- /dev/null +++ b/src/controllers/transaction/TransactionFeeEstimateController.ts @@ -0,0 +1,76 @@ +import { ApiPromise } from '@polkadot/api'; + +import { TransactionFeeEstimateService } from '../../services'; +import { IPostRequestHandler, ITx } from '../../types/requests'; +import AbstractController from '../AbstractController'; + +/** + * POST a serialized transaction and receive a fee estimate. + * + * Post info: + * - `data`: Expects a hex-encoded transaction, e.g. '{"tx": "0x..."}'. + * - `headers`: Expects 'Content-Type: application/json'. + * + * Returns: + * - Success: + * - `weight`: Extrinsic weight. + * - `class`: Extrinsic class, one of 'Normal', 'Operational', or 'Mandatory'. + * - `partialFee`: _Expected_ inclusion fee for the transaction. Note that the fee rate changes + * up to 30% in a 24 hour period and this will not be the exact fee. + * - Failure: + * - `error`: Error description. + * - `extrinsic`: The extrinsic and reference block hash. + * - `cause`: Error message from the client. + * + * Note: `partialFee` does not include any tips that you may add to increase a transaction's + * priority. See the reference on `compute_fee`. + * + * Substrate Reference: + * - `RuntimeDispatchInfo`: https://crates.parity.io/pallet_transaction_payment_rpc_runtime_api/struct.RuntimeDispatchInfo.html + * - `query_info`: https://crates.parity.io/pallet_transaction_payment/struct.Module.html#method.query_info + * - `compute_fee`: https://crates.parity.io/pallet_transaction_payment/struct.Module.html#method.compute_fee + */ +export default class TransactionFeeEstimateController extends AbstractController< + TransactionFeeEstimateService +> { + constructor(api: ApiPromise) { + super( + api, + '/transaction/fee-estimate', + new TransactionFeeEstimateService(api) + ); + this.initRoutes(); + } + + protected initRoutes(): void { + this.router.post( + this.path, + TransactionFeeEstimateController.catchWrap(this.txFeeEstimate) + ); + } + + /** + * Submit a serialized transaction in order to receive an estimate for its + * partial fees. + * + * @param req Sidecar TxRequest + * @param res Express Response + */ + private txFeeEstimate: IPostRequestHandler = async ( + { body: { tx } }, + res + ): Promise => { + if (!tx) { + throw { + error: 'Missing field `tx` on request body.', + }; + } + + const hash = await this.api.rpc.chain.getFinalizedHead(); + + TransactionFeeEstimateController.sanitizedSend( + res, + await this.service.fetchTransactionFeeEstimate(hash, tx) + ); + }; +} diff --git a/src/controllers/transaction/TransactionMaterialController.ts b/src/controllers/transaction/TransactionMaterialController.ts new file mode 100644 index 000000000..7fd9f1a69 --- /dev/null +++ b/src/controllers/transaction/TransactionMaterialController.ts @@ -0,0 +1,72 @@ +import { ApiPromise } from '@polkadot/api'; +import { RequestHandler } from 'express'; + +import { TransactionMaterialService } from '../../services'; +import AbstractController from '../AbstractController'; + +/** + * GET all the network information needed to construct a transaction offline. + * + * Query + * - (Optional) `noMeta`: If true, does not return metadata hex. This is useful when metadata is not + * needed and response time is a concern. Defaults to false. + * - (Optional) `at`: Block hash or number at which to query. If not provided, queries + * finalized head. + * + * Returns: + * - `at`: Block number and hash at which the call was made. + * - `genesisHash`: The hash of the chain's genesis block. + * - `chainName`: The chain's name. + * - `specName`: The chain's spec. + * - `specVersion`: The spec version. Always increased in a runtime upgrade. + * - `txversion`: The transaction version. Common `txVersion` numbers indicate that the + * transaction encoding format and method indices are the same. Needed for decoding in an + * offline environment. Adding new transactions does not change `txVersion`. + * - `metadata`: The chain's metadata in hex format. + * + * Note: `chainName`, `specName`, and `specVersion` are used to define a type registry with a set + * of signed extensions and types. For Polkadot and Kusama, `chainName` is not used in defining + * this registry, but in other Substrate-based chains that re-launch their network without + * changing the `specName`, the `chainName` would be needed to create the correct registry. + * + * Substrate Reference: + * - `RuntimeVersion`: https://crates.parity.io/sp_version/struct.RuntimeVersion.html + * - `SignedExtension`: https://crates.parity.io/sp_runtime/traits/trait.SignedExtension.html + * - FRAME Support: https://crates.parity.io/frame_support/metadata/index.html + */ +export default class TransactionMaterialController extends AbstractController< + TransactionMaterialService +> { + constructor(api: ApiPromise) { + super( + api, + '/transaction/material', + new TransactionMaterialService(api) + ); + this.initRoutes(); + } + + protected initRoutes(): void { + this.safeMountAsyncGetHandlers([['', this.getTransactionMaterial]]); + } + + /** + * GET all the network information needed to construct a transaction offline. + * + * @param _req Express Request + * @param res Express Response + */ + private getTransactionMaterial: RequestHandler = async ( + { query: { noMeta, at } }, + res + ): Promise => { + const hash = await this.getHashFromAt(at); + + const noMetaArg = noMeta === 'true' ? true : false; + + TransactionMaterialController.sanitizedSend( + res, + await this.service.fetchTransactionMaterial(hash, noMetaArg) + ); + }; +} diff --git a/src/controllers/transaction/TransactionSubmitController.ts b/src/controllers/transaction/TransactionSubmitController.ts new file mode 100644 index 000000000..956f1ddca --- /dev/null +++ b/src/controllers/transaction/TransactionSubmitController.ts @@ -0,0 +1,59 @@ +import { ApiPromise } from '@polkadot/api'; + +import { TransactionSubmitService } from '../../services'; +import { IPostRequestHandler, ITx } from '../../types/requests'; +import AbstractController from '../AbstractController'; + +/** + * POST a serialized transaction to submit to the transaction queue. + * + * Post info: + * - `data`: Expects a hex-encoded transaction, e.g. '{"tx": "0x..."}'. + * - `headers`: Expects 'Content-Type: application/json'. + * + * Returns: + * - Success: + * - `hash`: The hash of the encoded transaction. + * - Failure: + * - `error`: 'Failed to parse transaction' or 'Failed to submit transaction'. In the case of the former, + * Sidecar was unable to parse the transaction and never submitted it to the client. In + * the case of the latter, the transaction queue rejected the transaction. + * - `extrinsic`: The hex-encoded extrinsic. Only present if Sidecar fails to parse a transaction. + * - `cause`: The error message from parsing or from the client. + */ +export default class TransactionSubmitController extends AbstractController< + TransactionSubmitService +> { + constructor(api: ApiPromise) { + super(api, '/transaction', new TransactionSubmitService(api)); + this.initRoutes(); + } + + protected initRoutes(): void { + this.router.post( + this.path, + TransactionSubmitController.catchWrap(this.txSubmit) + ); + } + + /** + * Submit a serialized transaction to the transaction queue. + * + * @param req Sidecar TxRequest + * @param res Express Response + */ + private txSubmit: IPostRequestHandler = async ( + { body: { tx } }, + res + ): Promise => { + if (!tx) { + throw { + error: 'Missing field `tx` on request body.', + }; + } + + const hash = await this.api.rpc.chain.getFinalizedHead(); + + res.send(await this.service.submitTransaction(hash, tx)); + }; +} diff --git a/src/controllers/transaction/index.ts b/src/controllers/transaction/index.ts index b93f2cd90..3ea343316 100644 --- a/src/controllers/transaction/index.ts +++ b/src/controllers/transaction/index.ts @@ -1 +1,4 @@ export { default as TransactionDryRun } from './TransactionDryRunController'; +export { default as TransactionMaterial } from './TransactionMaterialController'; +export { default as TransactionFeeEstimate } from './TransactionFeeEstimateController'; +export { default as TransactionSubmit } from './TransactionSubmitController'; diff --git a/src/main.ts b/src/main.ts index 45cbadc52..9e7a85ee3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -102,6 +102,13 @@ async function main() { const runtimeSpecController = new controllers.RuntimeSpec(api); const runtimeMetadataController = new controllers.RuntimeMetadata(api); const transactionDryRunController = new controllers.TransactionDryRun(api); + const transactionMaterialController = new controllers.TransactionMaterial( + api + ); + const transactionFeeEstimateController = new controllers.TransactionFeeEstimate( + api + ); + const transactionSubmitController = new controllers.TransactionSubmit(api); // Create our App const app = new App({ @@ -116,6 +123,9 @@ async function main() { runtimeSpecController, runtimeMetadataController, transactionDryRunController, + transactionMaterialController, + transactionFeeEstimateController, + transactionSubmitController, ...v0Controllers, ], postMiddleware: [ diff --git a/src/middleware/error/txErrorMiddleware.ts b/src/middleware/error/txErrorMiddleware.ts index 0094267ee..b32d3cd23 100644 --- a/src/middleware/error/txErrorMiddleware.ts +++ b/src/middleware/error/txErrorMiddleware.ts @@ -3,7 +3,7 @@ import { ErrorRequestHandler } from 'express'; import { isTxLegacyError } from '../../types/errors'; /** - * Handle errors from tx POST methods + * Handle errors from transaction POST methods * * @param exception unknown * @param _req Express Request @@ -20,12 +20,13 @@ export const txErrorMiddleware: ErrorRequestHandler = ( return next(err); } - const { error, data, cause, stack } = err; + const { error, data, cause, stack, transaction } = err; res.status(500).send({ code: 500, error, data, + transaction, cause, stack, }); diff --git a/src/services/test-helpers/responses/transaction/feeEstimateInvalid.json b/src/services/test-helpers/responses/transaction/feeEstimateInvalid.json index b2f2a32db..ae3d4f729 100644 --- a/src/services/test-helpers/responses/transaction/feeEstimateInvalid.json +++ b/src/services/test-helpers/responses/transaction/feeEstimateInvalid.json @@ -1,9 +1,9 @@ { "error": "Unable to fetch fee info", - "data": { - "extrinsic": "0x250284d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01022f4deae1532ddd0", - "block": "0x7b713de604a99857f6c25eacc115a4f28d2611a23d9ddff99ab0e4f1c17a8578" + "at": { + "hash": "0x7b713de604a99857f6c25eacc115a4f28d2611a23d9ddff99ab0e4f1c17a8578" }, + "transaction": "0x250284d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01022f4deae1532ddd0", "cause": "2: Unable to query dispatch info.: Invalid transaction version", "stack": "Error: 2: Unable to query dispatch info.: Invalid transaction version\n ... this is a unit test mock" } diff --git a/src/services/test-helpers/responses/transaction/submitFailParse.json b/src/services/test-helpers/responses/transaction/submitFailParse.json index 9739010eb..5992dfa95 100644 --- a/src/services/test-helpers/responses/transaction/submitFailParse.json +++ b/src/services/test-helpers/responses/transaction/submitFailParse.json @@ -1,6 +1,6 @@ { - "error": "Failed to parse a tx", - "data": "0x250284d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01022f4deae1532ddd0", + "error": "Failed to parse transaction.", + "transaction": "0x250284d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01022f4deae1532ddd0", "cause": "createType(ExtrinsicV4):: Struct: failed on 'signature':: Struct: cannot decode type Type with value \"0x250284d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01022f4deae1532ddd0\"", "stack": "Error: createType(ExtrinsicV4):: Struct: failed ... this is a unit test mock" } diff --git a/src/services/test-helpers/responses/transaction/submitNodeReject.json b/src/services/test-helpers/responses/transaction/submitNodeReject.json index e607dbd70..c5cd58a10 100644 --- a/src/services/test-helpers/responses/transaction/submitNodeReject.json +++ b/src/services/test-helpers/responses/transaction/submitNodeReject.json @@ -1,6 +1,6 @@ { - "error": "Failed to submit a tx", - "data": "0x250284d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01022f4deae1532ddd01b88c4897151447ecfad96e18cf91088716a29ed00a6f514c56612f92650f66df1719a04935ce0b9c23b07ed81ae038844e33ee733bd588a501000005008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4830", + "error": "Failed to submit transaction.", + "transaction": "0x250284d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01022f4deae1532ddd01b88c4897151447ecfad96e18cf91088716a29ed00a6f514c56612f92650f66df1719a04935ce0b9c23b07ed81ae038844e33ee733bd588a501000005008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4830", "cause": "1012: Transaction is temporarily banned", "stack": "Error: 1012: Transaction is temporarily banned ... this is a unit test mock" } diff --git a/src/services/transaction/TransactionDryRunService.ts b/src/services/transaction/TransactionDryRunService.ts index 4e7422d35..8527948ec 100644 --- a/src/services/transaction/TransactionDryRunService.ts +++ b/src/services/transaction/TransactionDryRunService.ts @@ -11,14 +11,14 @@ import { extractCauseAndStack } from './extractCauseAndStack'; export class TransactionDryRunService extends AbstractService { async dryRuntExtrinsic( hash: BlockHash, - extrinsic: string + transaction: string ): Promise { // const api = await this.ensureMeta(hash); const { api } = this; try { const [applyExtrinsicResult, { number }] = await Promise.all([ - api.rpc.system.dryRun(extrinsic, hash), + api.rpc.system.dryRun(transaction, hash), api.rpc.chain.getHeader(hash), ]); @@ -56,7 +56,7 @@ export class TransactionDryRunService extends AbstractService { hash, }, error: 'Unable to dry-run transaction', - extrinsic, + transaction, cause, stack, }; diff --git a/src/services/v0/v0transaction/TransactionFeeEstimateService.spec.ts b/src/services/transaction/TransactionFeeEstimateService.spec.ts similarity index 82% rename from src/services/v0/v0transaction/TransactionFeeEstimateService.spec.ts rename to src/services/transaction/TransactionFeeEstimateService.spec.ts index 9f69d3178..60d2c2287 100644 --- a/src/services/v0/v0transaction/TransactionFeeEstimateService.spec.ts +++ b/src/services/transaction/TransactionFeeEstimateService.spec.ts @@ -1,16 +1,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { sanitizeNumbers } from '../../../sanitize/sanitizeNumbers'; +import { sanitizeNumbers } from '../../sanitize/sanitizeNumbers'; import { balancesTransferInvalid, balancesTransferValid, blockHash789629, mockApi, queryInfoBalancesTransfer, -} from '../../test-helpers/mock'; -import * as invalidResponse from '../../test-helpers/responses/transaction/feeEstimateInvalid.json'; -import * as validResponse from '../../test-helpers/responses/transaction/feeEstimateValid.json'; +} from '../test-helpers/mock'; +import * as invalidResponse from '../test-helpers/responses/transaction/feeEstimateInvalid.json'; +import * as validResponse from '../test-helpers/responses/transaction/feeEstimateValid.json'; import { TransactionFeeEstimateService } from './TransactionFeeEstimateService'; const transactionFeeEstimateService = new TransactionFeeEstimateService( diff --git a/src/services/transaction/TransactionFeeEstimateService.ts b/src/services/transaction/TransactionFeeEstimateService.ts new file mode 100644 index 000000000..6e7bd560e --- /dev/null +++ b/src/services/transaction/TransactionFeeEstimateService.ts @@ -0,0 +1,36 @@ +import { BlockHash, RuntimeDispatchInfo } from '@polkadot/types/interfaces'; + +import { AbstractService } from '../AbstractService'; +import { extractCauseAndStack } from './extractCauseAndStack'; + +export class TransactionFeeEstimateService extends AbstractService { + /** + * Fetch estimated fee information for a SCALE-encoded extrinsic at a given + * block. + * + * @param hash `BlockHash` to make call at + * @param extrinsic scale encoded extrinsic to get a fee estimate for + */ + async fetchTransactionFeeEstimate( + hash: BlockHash, + transaction: string + ): Promise { + const api = await this.ensureMeta(hash); + + try { + return await api.rpc.payment.queryInfo(transaction, hash); + } catch (err) { + const { cause, stack } = extractCauseAndStack(err); + + throw { + at: { + hash: hash.toString(), + }, + error: 'Unable to fetch fee info', + transaction, + cause, + stack, + }; + } + } +} diff --git a/src/services/transaction/TransactionMaterialService.spec.ts b/src/services/transaction/TransactionMaterialService.spec.ts new file mode 100644 index 000000000..f13b52b81 --- /dev/null +++ b/src/services/transaction/TransactionMaterialService.spec.ts @@ -0,0 +1,21 @@ +import { sanitizeNumbers } from '../../sanitize/sanitizeNumbers'; +import { blockHash789629, mockApi } from '../test-helpers/mock'; +import * as response789629 from '../test-helpers/responses/transaction/material789629.json'; +import { TransactionMaterialService } from './TransactionMaterialService'; + +const transactionMaterialService = new TransactionMaterialService(mockApi); + +describe('TransactionMaterialService', () => { + describe('getTransactionMaterial', () => { + it('works when ApiPromise works (block 789629)', async () => { + expect( + sanitizeNumbers( + await transactionMaterialService.fetchTransactionMaterial( + blockHash789629, + false + ) + ) + ).toStrictEqual(response789629); + }); + }); +}); diff --git a/src/services/transaction/TransactionMaterialService.ts b/src/services/transaction/TransactionMaterialService.ts new file mode 100644 index 000000000..fb1d4e55a --- /dev/null +++ b/src/services/transaction/TransactionMaterialService.ts @@ -0,0 +1,70 @@ +import { BlockHash } from '@polkadot/types/interfaces'; +import { ITransactionMaterial } from 'src/types/responses'; + +import { AbstractService } from '../AbstractService'; + +export class TransactionMaterialService extends AbstractService { + /** + * Fetch all the network information needed to construct a transaction offline. + * + * @param hash `BlockHash` to make call at + */ + async fetchTransactionMaterial( + hash: BlockHash, + noMeta: boolean + ): Promise { + const api = await this.ensureMeta(hash); + + if (noMeta) { + const [header, genesisHash, name, version] = await Promise.all([ + api.rpc.chain.getHeader(hash), + api.rpc.chain.getBlockHash(0), + api.rpc.system.chain(), + api.rpc.state.getRuntimeVersion(hash), + ]); + + const at = { + hash, + height: header.number.toNumber().toString(10), + }; + + return { + at, + genesisHash, + chainName: name.toString(), + specName: version.specName.toString(), + specVersion: version.specVersion, + txVersion: version.transactionVersion, + }; + } + + const [ + header, + metadata, + genesisHash, + name, + version, + ] = await Promise.all([ + api.rpc.chain.getHeader(hash), + api.rpc.state.getMetadata(hash), + api.rpc.chain.getBlockHash(0), + api.rpc.system.chain(), + api.rpc.state.getRuntimeVersion(hash), + ]); + + const at = { + hash, + height: header.number.toNumber().toString(10), + }; + + return { + at, + genesisHash, + chainName: name.toString(), + specName: version.specName.toString(), + specVersion: version.specVersion, + txVersion: version.transactionVersion, + metadata: metadata.toHex(), + }; + } +} diff --git a/src/services/v0/v0transaction/TransactionSubmitService.spec.ts b/src/services/transaction/TransactionSubmitService.spec.ts similarity index 76% rename from src/services/v0/v0transaction/TransactionSubmitService.spec.ts rename to src/services/transaction/TransactionSubmitService.spec.ts index 61cfd2b3a..8815e1d64 100644 --- a/src/services/v0/v0transaction/TransactionSubmitService.spec.ts +++ b/src/services/transaction/TransactionSubmitService.spec.ts @@ -1,15 +1,16 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { polkadotRegistry } from '../../../test-helpers/registries'; +import { polkadotRegistry } from '../../test-helpers/registries'; import { balancesTransferInvalid, balancesTransferValid, + blockHash789629, mockApi, submitExtrinsic, tx, -} from '../../test-helpers/mock'; -import * as failParseResponse from '../../test-helpers/responses/transaction/submitFailParse.json'; -import * as nodeRejectResponse from '../../test-helpers/responses/transaction/submitNodeReject.json'; +} from '../test-helpers/mock'; +import * as failParseResponse from '../test-helpers/responses/transaction/submitFailParse.json'; +import * as nodeRejectResponse from '../test-helpers/responses/transaction/submitNodeReject.json'; import { TransactionSubmitService } from './TransactionSubmitService'; const transactionSubmitService = new TransactionSubmitService(mockApi); @@ -19,6 +20,7 @@ describe('TransactionSubmitService', () => { it('works with a valid a transaction', async () => { return expect( transactionSubmitService.submitTransaction( + blockHash789629, balancesTransferValid ) ).resolves.toStrictEqual({ @@ -26,7 +28,7 @@ describe('TransactionSubmitService', () => { }); }); - it('throws with "Failed to parse a Tx" when tx is not parsable', async () => { + it('throws with "Failed to parse a transaction" when tx is not parsable', async () => { const err = new Error( // eslint-disable-next-line no-useless-escape `createType(ExtrinsicV4):: Struct: failed on 'signature':: Struct: cannot decode type Type with value \"0x250284d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01022f4deae1532ddd0\"` @@ -40,6 +42,7 @@ describe('TransactionSubmitService', () => { await expect( transactionSubmitService.submitTransaction( + blockHash789629, balancesTransferInvalid ) ).rejects.toStrictEqual(failParseResponse); @@ -47,7 +50,7 @@ describe('TransactionSubmitService', () => { (mockApi as any).tx = tx; }); - it('throws with "Failed to submit tx" when the node rejects the transaction', async () => { + it('throws with "Failed to submit transaction" when the node rejects the transaction', async () => { const err = new Error('1012: Transaction is temporarily banned'); err.stack = 'Error: 1012: Transaction is temporarily banned ... this is a unit test mock'; @@ -59,6 +62,7 @@ describe('TransactionSubmitService', () => { await expect( transactionSubmitService.submitTransaction( + blockHash789629, balancesTransferValid ) ).rejects.toStrictEqual(nodeRejectResponse); diff --git a/src/services/transaction/TransactionSubmitService.ts b/src/services/transaction/TransactionSubmitService.ts new file mode 100644 index 000000000..04fda359c --- /dev/null +++ b/src/services/transaction/TransactionSubmitService.ts @@ -0,0 +1,51 @@ +import { Hash } from '@polkadot/types/interfaces'; +import { BlockHash } from '@polkadot/types/interfaces'; + +import { AbstractService } from '../AbstractService'; +import { extractCauseAndStack } from './extractCauseAndStack'; + +export class TransactionSubmitService extends AbstractService { + /** + * Submit a fully formed SCALE-encoded extrinsic for block inclusion. + * + * @param extrinsic scale encoded extrinsic to submit + */ + async submitTransaction( + hash: BlockHash, + transaction: string + ): Promise<{ hash: Hash }> { + const api = await this.ensureMeta(hash); + + let tx; + + try { + tx = api.tx(transaction); + } catch (err) { + const { cause, stack } = extractCauseAndStack(err); + + throw { + error: 'Failed to parse transaction.', + transaction, + cause, + stack, + }; + } + + try { + const hash = await api.rpc.author.submitExtrinsic(tx); + + return { + hash, + }; + } catch (err) { + const { cause, stack } = extractCauseAndStack(err); + + throw { + error: 'Failed to submit transaction.', + transaction, + cause, + stack, + }; + } + } +} diff --git a/src/services/transaction/index.ts b/src/services/transaction/index.ts index db0e855ef..be49dd501 100644 --- a/src/services/transaction/index.ts +++ b/src/services/transaction/index.ts @@ -1 +1,4 @@ export * from './TransactionDryRunService'; +export * from './TransactionMaterialService'; +export * from './TransactionFeeEstimateService'; +export * from './TransactionSubmitService'; diff --git a/src/services/v0/v0transaction/TransactionSubmitService.ts b/src/services/v0/v0transaction/TransactionSubmitService.ts index 692b744df..95507520f 100644 --- a/src/services/v0/v0transaction/TransactionSubmitService.ts +++ b/src/services/v0/v0transaction/TransactionSubmitService.ts @@ -1,7 +1,7 @@ import { Hash } from '@polkadot/types/interfaces'; import { AbstractService } from '../../AbstractService'; -import { extractCauseAndStack } from './extractCauseAndStack'; +import { extractCauseAndStack } from '../../transaction/extractCauseAndStack'; export class TransactionSubmitService extends AbstractService { /** diff --git a/src/types/errors/TxLegacyError.ts b/src/types/errors/TxLegacyError.ts index 8159a563b..fc6873bd5 100644 --- a/src/types/errors/TxLegacyError.ts +++ b/src/types/errors/TxLegacyError.ts @@ -4,7 +4,8 @@ import { IBasicLegacyError } from './BasicLegacyError'; * Error from tx POST methods */ export interface ITxLegacyError extends IBasicLegacyError { - data: string; // extrinsic + data?: string; // deprecated + transaction?: string; cause: string | unknown; stack: string; }