Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions openapi/openapi-proposal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,10 @@ paths:
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/TransactionDryRun'
"500":
description: failed to dry-run transaction
content:
Expand Down Expand Up @@ -1737,6 +1741,42 @@ components:
items:
type: string
format: ss58
TransactionDryRun:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice docs 🎉

type: object
properties:
resultType:
type: string
enum:
- DispatchOutcome
- TransactionValidityError
description: Either `DispatchOutcome` if the transaction is valid or `TransactionValidityError` if the result is invalid.
result:
type: string
enum:
- Ok
- CannotLookup
- NoUnsignedValidator
- Custom(u8)
- Call
- Payment
- Future
- Stale
- BadProof
- AncientBirthBlock
- ExhaustsResources
- BadMandatory
- MandatoryDispatch
description: 'If there was an error it will be the cause of the error. If the
transaction executed correctly it will be `Ok: []`'
validityErrorType:
type: string
enum:
- InvalidTransaction
- UnknownTransaction
description: >-
References:
- `UnknownTransaction`: https://crates.parity.io/sp_runtime/transaction_validity/enum.UnknownTransaction.html
- `InvalidTransaction`: https://crates.parity.io/sp_runtime/transaction_validity/enum.InvalidTransaction.html
Transaction:
type: object
properties:
Expand Down
41 changes: 41 additions & 0 deletions src/controllers/transaction/TransactionDryRunController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ApiPromise } from '@polkadot/api';
import { BadRequest } from 'http-errors';

import { TransactionDryRunService } from '../../services';
import { IPostRequestHandler, ITx } from '../../types/requests';
import AbstractController from '../AbstractController';

export default class TransactionDryRunController extends AbstractController<
TransactionDryRunService
> {
constructor(api: ApiPromise) {
super(api, '/transaction/dry-run', new TransactionDryRunService(api));
this.initRoutes();
}

protected initRoutes(): void {
this.router.post(
this.path,
TransactionDryRunController.catchWrap(this.dryRunTransaction)
);
}

private dryRunTransaction: IPostRequestHandler<ITx> = async (
{ body: { tx }, query: { at } },
res
): Promise<void> => {
if (!tx) {
throw new BadRequest('Missing field `tx` on request body.');
}

const hash =
typeof at === 'string'
? await this.getHashForBlock(at)
: await this.api.rpc.chain.getFinalizedHead();

TransactionDryRunController.sanitizedSend(
res,
await this.service.dryRuntExtrinsic(hash, tx)
);
};
}
1 change: 1 addition & 0 deletions src/controllers/transaction/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as TransactionFeeEstimate } from './TransactionFeeEstimateController';
export { default as TransactionSubmit } from './TransactionSubmitController';
export { default as TransactionMaterial } from './TransactionMaterialController';
export { default as TransactionDryRun } from './TransactionDryRunController';
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ async function main() {
const txArtifactsController = new controllers.TransactionMaterial(api);
const txFeeEstimateController = new controllers.TransactionFeeEstimate(api);
const txSubmitController = new controllers.TransactionSubmit(api);
const transactionDryRunController = new controllers.TransactionDryRun(api);

// Create our App
const app = new App({
Expand All @@ -101,6 +102,7 @@ async function main() {
txArtifactsController,
txFeeEstimateController,
txSubmitController,
transactionDryRunController,
],
postMiddleware: [
middleware.txError,
Expand Down
83 changes: 83 additions & 0 deletions src/services/transaction/TransactionDryRunService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { BlockHash } from '@polkadot/types/interfaces';

import {
ITransactionDryRun,
TransactionResultType,
ValidityErrorType,
} from '../../types/responses';
import { AbstractService } from '../AbstractService';
import { extractCauseAndStack } from './extractCauseAndStack';

/**
* Dry run an extrinsic.
*
* Returns:
* - `at`:
* - `hash`: The block's hash.
* - `height`: The block's height.
* - `dryRunResult`:
* - `resultType`: Either `DispatchOutcome` if the construction is valid
* or `TransactionValidityError` if the transaction has invalid construction.
* - `result`: If there was an error it will be the cause of the error. If the
* transaction executed correctly it will be `Ok: []`.
* - `validityErrorType`: Only present if the `resultType` is
* `TransactionValidityError`. Either `InvalidTransaction` or `UnknownTransaction`.
*
* References:
* - `UnknownTransaction`: https://crates.parity.io/sp_runtime/transaction_validity/enum.UnknownTransaction.html
* - `InvalidTransaction`: https://crates.parity.io/sp_runtime/transaction_validity/enum.InvalidTransaction.html
*/
export class TransactionDryRunService extends AbstractService {
async dryRuntExtrinsic(
hash: BlockHash,
extrinsic: string
): Promise<ITransactionDryRun> {
const api = await this.ensureMeta(hash);

try {
const [applyExtrinsicResult, { number }] = await Promise.all([
api.rpc.system.dryRun(extrinsic, hash),
api.rpc.chain.getHeader(hash),
]);

let dryRunResult;
if (applyExtrinsicResult.isOk) {
dryRunResult = {
resultType: TransactionResultType.DispatchOutcome,
result: applyExtrinsicResult.asOk,
};
} else {
const { asError } = applyExtrinsicResult;
dryRunResult = {
resultType: TransactionResultType.TransactionValidityError,
result: asError.isInvalid
? asError.asInvalid
: asError.asUnknown,
validityErrorType: asError.isInvalid
? ValidityErrorType.Invalid
: ValidityErrorType.Unknown,
};
}

return {
at: {
hash,
height: number.unwrap().toString(10),
},
dryRunResult,
};
} catch (err) {
const { cause, stack } = extractCauseAndStack(err);

throw {
at: {
hash,
},
error: 'Unable to dry-run transaction',
extrinsic,
cause,
stack,
};
}
}
}
1 change: 1 addition & 0 deletions src/services/transaction/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './TransactionSubmitService';
export * from './TransactionFeeEstimateService';
export * from './TransactionMaterialService';
export * from './TransactionDryRunService';
31 changes: 31 additions & 0 deletions src/types/responses/TransactionDryRun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
DispatchOutcome,
InvalidTransaction,
UnknownTransaction,
} from '@polkadot/types/interfaces';

import { IAt } from '.';

export enum TransactionResultType {
TransactionValidityError = 'TransactionValidityError',
DispatchOutcome = 'DispatchOutcome',
}

export enum ValidityErrorType {
Invalid = 'InvalidTransaction',
Unknown = 'UnknownTransaction',
}

export type TransactionResult =
| DispatchOutcome
| InvalidTransaction
| UnknownTransaction;

export interface ITransactionDryRun {
at: IAt;
dryRunResult: {
resultType: TransactionResultType;
result: TransactionResult;
validityErrorType?: ValidityErrorType;
};
}
1 change: 1 addition & 0 deletions src/types/responses/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from './AccountStakingPayouts';
export * from './NodeNetwork';
export * from './NodeVersion';
export * from './NodeTransactionPool';
export * from './TransactionDryRun';