Skip to content

Commit

Permalink
Merge pull request #23 from PolymathNetwork/feat/procedure-class
Browse files Browse the repository at this point in the history
Feat/procedure class
  • Loading branch information
monitz87 authored Feb 21, 2020
2 parents 2e7031a + 74cfd05 commit 0918842
Show file tree
Hide file tree
Showing 16 changed files with 606 additions and 117 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
],
"simple-import-sort/sort": "error",
"sort-imports": "off",
"import/order": "off"
"import/order": "off",
"no-dupe-class-members": "off"
},
"plugins": ["@typescript-eslint", "simple-import-sort"],
"parserOptions": {
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@polymathnetwork/polymesh-sdk",
"version": "0.0.0",
"description": "Template repository for typescript projects",
"description": "High-level API to interact with the Polymesh blockchain",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "Polymath Studios Inc.",
Expand All @@ -18,6 +18,7 @@
"devDependencies": {
"@commitlint/cli": "^7.6.1",
"@commitlint/config-conventional": "^7.6.0",
"@types/bn.js": "^4.11.6",
"@types/jest": "^23.3.10",
"@types/json-stable-stringify": "^1.0.32",
"@types/lodash": "^4.14.149",
Expand Down Expand Up @@ -61,8 +62,8 @@
},
"dependencies": {
"@polymathnetwork/polkadot": "0.101.0-beta.14",
"@types/bn.js": "^4.11.6",
"bn.js": "^5.1.1",
"@types/bignumber.js": "^5.0.0",
"bignumber.js": "^9.0.0",
"json-stable-stringify": "^1.0.1",
"lodash": "^4.17.15"
}
Expand Down
16 changes: 9 additions & 7 deletions src/api/entities/__tests__/Identity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import sinon from 'sinon';
import BigNumber from 'bignumber.js';
import { ImportMock } from 'ts-mock-imports';

import { Identity } from '~/api/entities';
Expand All @@ -15,6 +15,10 @@ describe('Identity class', () => {
polkadotMockFactory.reset();
});

afterAll(() => {
polkadotMockFactory.cleanup();
});

test('should extend entity', () => {
expect(Identity.prototype instanceof Entity).toBe(true);
});
Expand Down Expand Up @@ -61,13 +65,11 @@ describe('Identity class', () => {

describe('method: getPolyBalance', () => {
test("should return the identity's POLY balance", async () => {
const identityBalanceStub = polkadotMockFactory.createQueryStub(
'balances',
'identityBalance'
);
const balance = new BigNumber(3);
polkadotMockFactory.createQueryStub('balances', 'identityBalance').returns(balance);
const identity = new Identity({ did: 'abc' }, polkadotMockFactory.getContextInstance());
await identity.getPolyBalance();
sinon.assert.calledOnce(identityBalanceStub);
const result = await identity.getPolyBalance();
expect(result).toEqual(balance);
});
});
});
37 changes: 12 additions & 25 deletions src/base/PolymeshTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { PolymeshError } from '~/base';
import { ErrorCode, TransactionStatus } from '~/types';
import {
MapMaybePostTransactionValue,
MaybePostTransactionValue,
PolymeshTx,
PostTransactionValueArray,
TransactionSpec,
} from '~/types/internal';

import { PostTransactionValue } from './PostTransactionValue';
import { unwrapValue, unwrapValues } from '~/utils';

enum Event {
StatusChange = 'StatusChange',
Expand Down Expand Up @@ -53,21 +53,22 @@ export class PolymeshTransaction<Args extends unknown[], Values extends unknown[
public blockHash?: string;

/**
* arguments with which the transaction will be called
* whether this tx failing makes the entire tx queue fail or not
*/
public args: MapMaybePostTransactionValue<Args>;
public isCritical: boolean;

/**
* whether this tx failing makes the entire tx queue fail or not
* arguments arguments for the transaction. Available after the transaction starts running
* (may be Post Transaction Values from a previous transaction in the queue that haven't resolved yet)
*/
public isCritical: boolean;
public args: MapMaybePostTransactionValue<Args>;

/**
* @hidden
*
* underlying transaction to be executed
*/
protected tx: PolymeshTx<Args>;
private tx: MaybePostTransactionValue<PolymeshTx<Args>>;

/**
* @hidden
Expand Down Expand Up @@ -171,9 +172,10 @@ export class PolymeshTransaction<Args extends unknown[], Values extends unknown[
private async internalRun(): Promise<ISubmittableResult> {
this.updateStatus(TransactionStatus.Unapproved);

const unwrappedArgs = this.unwrapArgs(this.args);

const { tx } = this;
const { tx: wrappedTx } = this;
const unwrappedArgs = unwrapValues(this.args);
const tx = unwrapValue(wrappedTx);
this.args = unwrappedArgs;

const gettingReceipt: Promise<ISubmittableResult> = new Promise((resolve, reject) => {
const txWithArgs = tx(...unwrappedArgs);
Expand Down Expand Up @@ -274,19 +276,4 @@ export class PolymeshTransaction<Args extends unknown[], Values extends unknown[
}
/* eslint-enable default-case */
}

/**
* @hidden
*
* Unwrap Post Transaction Values if present in the tuple
*/
private unwrapArgs<T extends unknown[]>(args: MapMaybePostTransactionValue<T>): T {
return args.map(arg => {
if (arg instanceof PostTransactionValue) {
return arg.value;
}

return arg;
}) as T;
}
}
199 changes: 199 additions & 0 deletions src/base/Procedure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { AddressOrPair, TxTag } from '@polymathnetwork/polkadot/api/types';
import BigNumber from 'bignumber.js';

import { TransactionQueue } from '~/base';
import { Context } from '~/base/Context';
import { PolymeshError } from '~/base/PolymeshError';
import { PostTransactionValue } from '~/base/PostTransactionValue';
import { ErrorCode, Role } from '~/types';
import {
MapMaybePostTransactionValue,
MaybePostTransactionValue,
PolymeshTx,
PostTransactionValueArray,
ResolverFunctionArray,
TransactionSpec,
} from '~/types/internal';

/**
* @hidden
* Represents an operation performed on the Polymesh blockchain.
* A Procedure can be prepared to yield a [[TransactionQueue]] that can be run
*/
export class Procedure<Args extends unknown = void, ReturnValue extends unknown = void> {
private prepareTransactions: (
this: Procedure<Args, ReturnValue>,
args: Args
) => Promise<MaybePostTransactionValue<ReturnValue>>;

private checkRoles: (this: Procedure<Args, ReturnValue>, args: Args) => Promise<boolean>;

private transactions: TransactionSpec[] = [];

private fees: Array<BigNumber> = [];

public context = {} as Context;

/**
* @hidden
*/
constructor(
prepareTransactions: (
this: Procedure<Args, ReturnValue>,
args: Args
) => Promise<MaybePostTransactionValue<ReturnValue>>,
checkRoles:
| Role[]
| ((this: Procedure<Args, ReturnValue>, args: Args) => Promise<boolean>) = async (): Promise<
boolean
> => true
) {
this.prepareTransactions = prepareTransactions;

if (Array.isArray(checkRoles)) {
// TODO @monitz87: implement this for real when we have actual role-checking logic
this.checkRoles = async (): Promise<boolean> => false;
} else {
this.checkRoles = checkRoles;
}
}

/**
* @hidden
* Reset the procedure
*/
private cleanup(): void {
this.transactions = [];
this.fees = [];
this.context = {} as Context;
}

/**
* Build a [[TransactionQueue]] that can be run
*
* @param args - arguments required to prepare the queue
* @param context - context in which the resulting queue will run
*/
public async prepare(
args: Args,
context: Context
): Promise<TransactionQueue<unknown[][], ReturnValue>> {
this.context = context;

const allowed = await this.checkRoles(args);

if (!allowed) {
throw new PolymeshError({
code: ErrorCode.NotAuthorized,
message: 'Current account is not authorized to execute this procedure',
});
}

const returnValue = await this.prepareTransactions(args);
const totalFees = this.fees.reduce((acc, next) => {
return acc.plus(next);
}, new BigNumber(0));

const transactionQueue = new TransactionQueue(this.transactions, totalFees, returnValue);

this.cleanup();

return transactionQueue;
}

/**
* Appends a method (or [[PostTransactionValue]] that resolves to a method) into the TransactionQueue's queue. This defines
* what will be run by the TransactionQueue when it is started.
*
* @param method - a method that will be run in the Procedure's TransactionQueue.
* A future method is a transaction that doesn't exist at prepare time
* (for example a transaction on a module that hasn't been attached but will be by the time the previous transactions are run)
* @param options.tag - a tag for SDK users to identify this transaction, this
* can be used for doing things such as mapping descriptions to tags in the UI
* @param options.fee - value in POLY of the transaction (defaults to 0)
* @param options.resolvers - asynchronous callbacks used to return runtime data after
* the added transaction has finished successfully
* @param options.isCritical - whether this transaction failing should make the entire queue fail or not. Defaults to true
* @param options.signer - address or keyring pair of the account that will sign this transaction. Defaults to the current pair in the context
* @param args - arguments to be passed to the transaction method
*
* @returns an array of [[PostTransactionValue]]. Each element corresponds to whatever is returned by one of the resolver functions passed as options
*/
public addTransaction<TxArgs extends unknown[], Values extends unknown[] = []>(
tx: MaybePostTransactionValue<PolymeshTx<TxArgs>>,
options: {
tag: TxTag;
fee?: BigNumber;
resolvers?: ResolverFunctionArray<Values>;
isCritical?: boolean;
signer?: AddressOrPair;
},
...args: MapMaybePostTransactionValue<TxArgs>
): PostTransactionValueArray<Values> {
const {
tag,
fee = new BigNumber(0),
resolvers = ([] as unknown) as ResolverFunctionArray<Values>,
isCritical = true,
signer = this.context.currentPair,
} = options;
const postTransactionValues = resolvers.map(
resolver => new PostTransactionValue(resolver)
) as PostTransactionValueArray<Values>;

this.fees.push(fee);

const transaction = {
tx,
args,
postTransactionValues,
tag,
isCritical,
signer,
};

this.transactions.push((transaction as unknown) as TransactionSpec);

return postTransactionValues;
}

public async addProcedure<ReturnValue extends unknown>(
procedure: Procedure<void, ReturnValue>
): Promise<MaybePostTransactionValue<ReturnValue>>;

public async addProcedure<ProcArgs extends unknown, ReturnValue extends unknown>(
procedure: Procedure<ProcArgs, ReturnValue>,
args: ProcArgs
): Promise<MaybePostTransactionValue<ReturnValue>>;

/**
* Appends a Procedure into this Procedure's queue. This defines
* what will be run by the Transaction Queue when it is started.
*
* @param proc - a Procedure that will be run as part of this Procedure's Transaction Queue
* @param args - arguments to be passed to the procedure
*
* @returns whichever value is returned by the passed Procedure
*/
public async addProcedure<ReturnValue extends unknown>(
/* eslint-disable @typescript-eslint/no-explicit-any */
procedure: Procedure<any, ReturnValue>,
args: unknown = {}
): Promise<any> {
/* eslint-enable @typescript-eslint/no-explicit-any */
try {
procedure.context = this.context;
const returnValue = await procedure.prepareTransactions(args);

const { transactions, fees } = procedure;
this.fees = [...this.fees, ...fees];
this.transactions = [...this.transactions, ...transactions];

return returnValue;
} catch (err) {
throw new PolymeshError({ code: err.code || ErrorCode.FatalError, message: err.message });
} finally {
procedure.cleanup();
}
}
}
Loading

0 comments on commit 0918842

Please sign in to comment.