Browse Source

Api.ts - final refactoring

Leszek Wiesner 4 years ago
parent
commit
6e1309cb5a

+ 57 - 66
cli/src/Api.ts

@@ -4,6 +4,7 @@ import { ApiPromise, WsProvider } from '@polkadot/api';
 import { formatBalance } from '@polkadot/util';
 import { Balance, Hash } from '@polkadot/types/interfaces';
 import { KeyringPair } from '@polkadot/keyring/types';
+import { Codec } from '@polkadot/types/types';
 import { AccountBalances, CouncilInfoObj, CouncilInfoTuple, AccountSummary, createCouncilInfoObj } from './Types';
 import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types';
 import { CLIError } from '@oclif/errors';
@@ -19,10 +20,13 @@ const TOKEN_SYMBOL = 'JOY';
 // Api wrapper for handling most common api calls and allowing easy API implementation switch in the future
 
 export default class Api {
-    private _api: ApiPromise | null = null;
-    private initializing: boolean = false;
+    private _api: ApiPromise;
 
-    private async initApi(): Promise<ApiPromise> {
+    private constructor(originalApi:ApiPromise) {
+        this._api = originalApi;
+    }
+
+    private static async initApi(): Promise<ApiPromise> {
         formatBalance.setDefaults({ unit: TOKEN_SYMBOL });
         const wsProvider:WsProvider = new WsProvider(API_URL);
         registerJoystreamTypes();
@@ -30,42 +34,42 @@ export default class Api {
         return await ApiPromise.create({ provider: wsProvider });
     }
 
-    private async getApi(): Promise<ApiPromise> {
-        while (this.initializing) {
-            await new Promise((res,rej) => setTimeout(res, 10));
-        }
-        if (!this._api) {
-            this.initializing = true;
-            this._api = await this.initApi();
-            this.initializing = false;
-        }
+    static async create(): Promise<Api> {
+        const originalApi: ApiPromise = await Api.initApi();
+        return new Api(originalApi);
+    }
 
-        return this._api;
+    private async queryMultiOnce(queries: Parameters<typeof ApiPromise.prototype.queryMulti>[0]): Promise<Codec[]> {
+        let results: Codec[] = [];
+
+        const unsub = await this._api.queryMulti(
+            queries,
+            (res) => { results = res }
+        );
+        unsub();
+
+        if (!results.length || results.length !== queries.length) {
+            throw new CLIError('API querying issue', { exit: ExitCodes.ApiError });
+        }
+            return results;
     }
 
     async getAccountsBalancesInfo(accountAddresses:string[]): Promise<AccountBalances[]> {
-        const api: ApiPromise = await this.getApi();
-
         let apiQueries:any = [];
         for (let address of accountAddresses) {
-            apiQueries.push([api.query.balances.freeBalance, address]);
-            apiQueries.push([api.query.balances.reservedBalance, address]);
+            apiQueries.push([this._api.query.balances.freeBalance, address]);
+            apiQueries.push([this._api.query.balances.reservedBalance, address]);
         }
 
         let balances: AccountBalances[] = [];
-        const unsub = await api.queryMulti(
-            apiQueries,
-            (balancesRes) => {
-                for (let key in accountAddresses) {
-                    let numKey: number = parseInt(key);
-                    const free: Balance = <Balance> balancesRes[numKey*2];
-                    const reserved: Balance = <Balance> balancesRes[numKey*2 + 1];
-                    const total: Balance = api.createType('Balance', free.add(reserved));
-                    balances[key] = { free, reserved, total };
-                }
-            }
-        );
-        unsub();
+        let balancesRes = await this.queryMultiOnce(apiQueries);
+        for (let key in accountAddresses) {
+            let numKey: number = parseInt(key);
+            const free: Balance = <Balance> balancesRes[numKey*2];
+            const reserved: Balance = <Balance> balancesRes[numKey*2 + 1];
+            const total: Balance = this._api.createType('Balance', free.add(reserved));
+            balances[key] = { free, reserved, total };
+        }
 
         return balances;
     }
@@ -73,51 +77,40 @@ export default class Api {
     // Get on-chain data related to given account.
     // For now it's just account balances
     async getAccountSummary(accountAddresses:string): Promise<AccountSummary> {
-        const api: ApiPromise = await this.getApi();
-        const balances: DerivedBalances = await api.derive.balances.all(accountAddresses);
+        const balances: DerivedBalances = await this._api.derive.balances.all(accountAddresses);
 
         return { balances };
     }
 
     async getCouncilInfo(): Promise<CouncilInfoObj> {
-        const api: ApiPromise = await this.getApi();
-        let infoObj: CouncilInfoObj | null = null;
-        const unsub = await api.queryMulti(
-            [
-                api.query.council.activeCouncil,
-                api.query.council.termEndsAt,
-                api.query.councilElection.autoStart,
-                api.query.councilElection.newTermDuration,
-                api.query.councilElection.candidacyLimit,
-                api.query.councilElection.councilSize,
-                api.query.councilElection.minCouncilStake,
-                api.query.councilElection.minVotingStake,
-                api.query.councilElection.announcingPeriod,
-                api.query.councilElection.votingPeriod,
-                api.query.councilElection.revealingPeriod,
-                api.query.councilElection.round,
-                api.query.councilElection.stage
-            ],
-            ((res) => {
-                infoObj = createCouncilInfoObj(...(<CouncilInfoTuple> res));
-            })
-        );
-        unsub();
-
-        if (infoObj) return infoObj;
-        // Probably never happens, but otherwise TS will see it as error
-        else throw new CLIError('Unexpected API error', { exit: ExitCodes.ApiError });
+        const results = await this.queryMultiOnce([
+            this._api.query.council.activeCouncil,
+            this._api.query.council.termEndsAt,
+            this._api.query.councilElection.autoStart,
+            this._api.query.councilElection.newTermDuration,
+            this._api.query.councilElection.candidacyLimit,
+            this._api.query.councilElection.councilSize,
+            this._api.query.councilElection.minCouncilStake,
+            this._api.query.councilElection.minVotingStake,
+            this._api.query.councilElection.announcingPeriod,
+            this._api.query.councilElection.votingPeriod,
+            this._api.query.councilElection.revealingPeriod,
+            this._api.query.councilElection.round,
+            this._api.query.councilElection.stage
+        ]);
+
+        let infoObj: CouncilInfoObj = createCouncilInfoObj(...(<CouncilInfoTuple> results));
+
+        return infoObj;
     }
 
     // TODO: This formula is probably not too good, so some better implementation will be required in the future
     async estimateFee(account: KeyringPair, recipientAddr: string, amount: BN): Promise<BN> {
-        const api: ApiPromise = await this.getApi();
-
-        const transfer = api.tx.balances.transfer(recipientAddr, amount);
+        const transfer = this._api.tx.balances.transfer(recipientAddr, amount);
         const signature = account.sign(transfer.toU8a());
         const transactionByteSize:BN = new BN(transfer.encodedLength + signature.length);
 
-        const fees: DerivedFees = await api.derive.balances.fees();
+        const fees: DerivedFees = await this._api.derive.balances.fees();
 
         const estimatedFee = fees.transactionBaseFee.add(fees.transactionByteFee.mul(transactionByteSize));
 
@@ -125,9 +118,7 @@ export default class Api {
     }
 
     async transfer(account: KeyringPair, recipientAddr: string, amount: BN): Promise<Hash> {
-        const api: ApiPromise = await this.getApi();
-
-        const txHash = await api.tx.balances
+        const txHash = await this._api.tx.balances
             .transfer(recipientAddr, amount)
             .signAndSend(account);
         return txHash;

+ 8 - 2
cli/src/base/AccountsCommandBase.ts

@@ -16,6 +16,12 @@ type StateObject = { selectedAccountFilename: string };
 export default abstract class AccountsCommandBase extends Command {
     static ACCOUNTS_DIRNAME = '/accounts';
     static STATE_FILE = '/state.json';
+    private api: Api | null = null;
+
+    getApi(): Api {
+        if (!this.api) throw new CLIError('Tried to get API before initialization.', { exit: ExitCodes.ApiError });
+        return this.api;
+    }
 
     getAccountsDirPath(): string {
         return path.join(this.config.dataDir, AccountsCommandBase.ACCOUNTS_DIRNAME);
@@ -213,8 +219,7 @@ export default abstract class AccountsCommandBase extends Command {
     ): Promise<NamedKeyringPair> {
         let balances: AccountBalances[];
         if (showBalances) {
-            const api: Api = new Api();
-            balances = await api.getAccountsBalancesInfo(accounts.map(acc => acc.address));
+            balances = await this.getApi().getAccountsBalancesInfo(accounts.map(acc => acc.address));
         }
         const { chosenAccountFilename } = await inquirer.prompt([{
             name: 'chosenAccountFilename',
@@ -243,6 +248,7 @@ export default abstract class AccountsCommandBase extends Command {
     }
 
     async init() {
+        this.api = await Api.create();
         try {
             this.initDataDir();
         } catch (e) {

+ 1 - 2
cli/src/commands/account/current.ts

@@ -10,9 +10,8 @@ export default class AccountCurrent extends AccountsCommandBase {
     static aliases = ['account:info', 'account:default'];
 
     async run() {
-        const api: Api = new Api();
         const currentAccount: NamedKeyringPair = await this.getRequiredSelectedAccount(false);
-        const summary: AccountSummary = await api.getAccountSummary(currentAccount.address);
+        const summary: AccountSummary = await this.getApi().getAccountSummary(currentAccount.address);
 
         displayHeader('Account information');
         const creationDate: string = currentAccount.meta.whenCreated ?

+ 3 - 4
cli/src/commands/account/transferTokens.ts

@@ -31,13 +31,12 @@ export default class AccountTransferTokens extends AccountsCommandBase {
 
     async run() {
         const args: AccountTransferArgs = <AccountTransferArgs> this.parse(AccountTransferTokens).args;
-        const api: Api = new Api();
         const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
         const amountBN: BN = new BN(args.amount);
 
         // Initial validation
         validateAddress(args.recipient, 'Invalid recipient address');
-        const accBalances: AccountBalances = (await api.getAccountsBalancesInfo([ selectedAccount.address ]))[0];
+        const accBalances: AccountBalances = (await this.getApi().getAccountsBalancesInfo([ selectedAccount.address ]))[0];
         checkBalance(accBalances, amountBN);
 
         await this.requestAccountDecoding(selectedAccount);
@@ -45,7 +44,7 @@ export default class AccountTransferTokens extends AccountsCommandBase {
         this.log(chalk.white('Estimating fee...'));
         let estimatedFee: BN;
         try {
-            estimatedFee = await api.estimateFee(selectedAccount, args.recipient, amountBN);
+            estimatedFee = await this.getApi().estimateFee(selectedAccount, args.recipient, amountBN);
         }
         catch (e) {
             this.error('Could not estimate the fee.', { exit: ExitCodes.UnexpectedException });
@@ -59,7 +58,7 @@ export default class AccountTransferTokens extends AccountsCommandBase {
         await this.requireConfirmation('Do you confirm the transfer?');
 
         try {
-            const txHash: Hash = await api.transfer(selectedAccount, args.recipient, amountBN);
+            const txHash: Hash = await this.getApi().transfer(selectedAccount, args.recipient, amountBN);
             this.log(chalk.greenBright('Transaction succesfully sent!'));
             this.log(chalk.white('Hash:', txHash.toString()));
         } catch (e) {

+ 1 - 1
cli/src/commands/council/info.ts

@@ -52,7 +52,7 @@ export default class CouncilInfo extends Command {
     }
 
     async run() {
-        const api = new Api();
+        const api = await Api.create();
         const infoObj = await api.getCouncilInfo();
         this.displayInfo(infoObj);
         this.exit();