|
@@ -8,7 +8,7 @@
|
|
|
* (at your option) any later version.
|
|
|
*
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
* GNU General Public License for more details.
|
|
|
*
|
|
@@ -16,70 +16,66 @@
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
*/
|
|
|
|
|
|
-'use strict';
|
|
|
+'use strict'
|
|
|
|
|
|
-const debug = require('debug')('joystream:runtime:base');
|
|
|
+const debug = require('debug')('joystream:runtime:base')
|
|
|
|
|
|
-const { registerJoystreamTypes } = require('@joystream/types');
|
|
|
-const { ApiPromise, WsProvider } = require('@polkadot/api');
|
|
|
+const { registerJoystreamTypes } = require('@joystream/types')
|
|
|
+const { ApiPromise, WsProvider } = require('@polkadot/api')
|
|
|
|
|
|
-const { IdentitiesApi } = require('@joystream/runtime-api/identities');
|
|
|
-const { BalancesApi } = require('@joystream/runtime-api/balances');
|
|
|
-const { WorkersApi } = require('@joystream/runtime-api/workers');
|
|
|
-const { AssetsApi } = require('@joystream/runtime-api/assets');
|
|
|
-const { DiscoveryApi } = require('@joystream/runtime-api/discovery');
|
|
|
-const AsyncLock = require('async-lock');
|
|
|
+const { IdentitiesApi } = require('@joystream/runtime-api/identities')
|
|
|
+const { BalancesApi } = require('@joystream/runtime-api/balances')
|
|
|
+const { WorkersApi } = require('@joystream/runtime-api/workers')
|
|
|
+const { AssetsApi } = require('@joystream/runtime-api/assets')
|
|
|
+const { DiscoveryApi } = require('@joystream/runtime-api/discovery')
|
|
|
+const AsyncLock = require('async-lock')
|
|
|
|
|
|
/*
|
|
|
* Initialize runtime (substrate) API and keyring.
|
|
|
*/
|
|
|
-class RuntimeApi
|
|
|
-{
|
|
|
- static async create(options)
|
|
|
- {
|
|
|
- const runtime_api = new RuntimeApi();
|
|
|
- await runtime_api.init(options || {});
|
|
|
- return runtime_api;
|
|
|
+class RuntimeApi {
|
|
|
+ static async create (options) {
|
|
|
+ const runtime_api = new RuntimeApi()
|
|
|
+ await runtime_api.init(options || {})
|
|
|
+ return runtime_api
|
|
|
}
|
|
|
|
|
|
- async init(options)
|
|
|
- {
|
|
|
- debug('Init');
|
|
|
+ async init (options) {
|
|
|
+ debug('Init')
|
|
|
|
|
|
- options = options || {};
|
|
|
+ options = options || {}
|
|
|
|
|
|
// Register joystream types
|
|
|
- registerJoystreamTypes();
|
|
|
+ registerJoystreamTypes()
|
|
|
|
|
|
- const provider = new WsProvider(options.provider_url || 'ws://localhost:9944');
|
|
|
+ const provider = new WsProvider(options.provider_url || 'ws://localhost:9944')
|
|
|
|
|
|
// Create the API instrance
|
|
|
- this.api = await ApiPromise.create({ provider });
|
|
|
+ this.api = await ApiPromise.create({ provider })
|
|
|
|
|
|
- this.asyncLock = new AsyncLock();
|
|
|
+ this.asyncLock = new AsyncLock()
|
|
|
|
|
|
// Keep track locally of account nonces.
|
|
|
- this.nonces = {};
|
|
|
+ this.nonces = {}
|
|
|
|
|
|
// Ok, create individual APIs
|
|
|
this.identities = await IdentitiesApi.create(this, {
|
|
|
account_file: options.account_file,
|
|
|
passphrase: options.passphrase,
|
|
|
canPromptForPassphrase: options.canPromptForPassphrase
|
|
|
- });
|
|
|
- this.balances = await BalancesApi.create(this);
|
|
|
- this.workers = await WorkersApi.create(this);
|
|
|
- this.assets = await AssetsApi.create(this);
|
|
|
- this.discovery = await DiscoveryApi.create(this);
|
|
|
+ })
|
|
|
+ this.balances = await BalancesApi.create(this)
|
|
|
+ this.workers = await WorkersApi.create(this)
|
|
|
+ this.assets = await AssetsApi.create(this)
|
|
|
+ this.discovery = await DiscoveryApi.create(this)
|
|
|
}
|
|
|
|
|
|
- disconnect()
|
|
|
- {
|
|
|
- this.api.disconnect();
|
|
|
+ disconnect () {
|
|
|
+ this.api.disconnect()
|
|
|
}
|
|
|
|
|
|
- executeWithAccountLock(account_id, func) {
|
|
|
- return this.asyncLock.acquire(`${account_id}`, func);
|
|
|
+ executeWithAccountLock (account_id, func) {
|
|
|
+ return this.asyncLock.acquire(`${account_id}`, func)
|
|
|
}
|
|
|
|
|
|
/*
|
|
@@ -89,47 +85,45 @@ class RuntimeApi
|
|
|
* The result of the Promise is an array containing first the full event
|
|
|
* name, and then the event fields as an object.
|
|
|
*/
|
|
|
- async waitForEvent(module, name)
|
|
|
- {
|
|
|
- return this.waitForEvents([[module, name]]);
|
|
|
+ async waitForEvent (module, name) {
|
|
|
+ return this.waitForEvents([[module, name]])
|
|
|
}
|
|
|
|
|
|
- _matchingEvents(subscribed, events)
|
|
|
- {
|
|
|
- debug(`Number of events: ${events.length}; subscribed to ${subscribed}`);
|
|
|
+ _matchingEvents(subscribed, events) {
|
|
|
+ debug(`Number of events: ${events.length} subscribed to ${subscribed}`)
|
|
|
|
|
|
const filtered = events.filter((record) => {
|
|
|
- const { event, phase } = record;
|
|
|
+ const { event, phase } = record
|
|
|
|
|
|
// Show what we are busy with
|
|
|
- debug(`\t${event.section}:${event.method}:: (phase=${phase.toString()})`);
|
|
|
- debug(`\t\t${event.meta.documentation.toString()}`);
|
|
|
+ debug(`\t${event.section}:${event.method}:: (phase=${phase.toString()})`)
|
|
|
+ debug(`\t\t${event.meta.documentation.toString()}`)
|
|
|
|
|
|
// Skip events we're not interested in.
|
|
|
const matching = subscribed.filter((value) => {
|
|
|
- return event.section == value[0] && event.method == value[1];
|
|
|
- });
|
|
|
- return matching.length > 0;
|
|
|
- });
|
|
|
- debug(`Filtered: ${filtered.length}`);
|
|
|
+ return event.section === value[0] && event.method === value[1]
|
|
|
+ })
|
|
|
+ return matching.length > 0
|
|
|
+ })
|
|
|
+ debug(`Filtered: ${filtered.length}`)
|
|
|
|
|
|
const mapped = filtered.map((record) => {
|
|
|
- const { event } = record;
|
|
|
- const types = event.typeDef;
|
|
|
+ const { event } = record
|
|
|
+ const types = event.typeDef
|
|
|
|
|
|
// Loop through each of the parameters, displaying the type and data
|
|
|
- const payload = {};
|
|
|
+ const payload = {}
|
|
|
event.data.forEach((data, index) => {
|
|
|
- debug(`\t\t\t${types[index].type}: ${data.toString()}`);
|
|
|
- payload[types[index].type] = data;
|
|
|
- });
|
|
|
+ debug(`\t\t\t${types[index].type}: ${data.toString()}`)
|
|
|
+ payload[types[index].type] = data
|
|
|
+ })
|
|
|
|
|
|
- const full_name = `${event.section}.${event.method}`;
|
|
|
- return [full_name, payload];
|
|
|
- });
|
|
|
- debug('Mapped', mapped);
|
|
|
+ const full_name = `${event.section}.${event.method}`
|
|
|
+ return [full_name, payload]
|
|
|
+ })
|
|
|
+ debug('Mapped', mapped)
|
|
|
|
|
|
- return mapped;
|
|
|
+ return mapped
|
|
|
}
|
|
|
|
|
|
/*
|
|
@@ -139,16 +133,15 @@ class RuntimeApi
|
|
|
*
|
|
|
* Returns the first matched event *only*.
|
|
|
*/
|
|
|
- async waitForEvents(subscribed)
|
|
|
- {
|
|
|
+ async waitForEvents (subscribed) {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
this.api.query.system.events((events) => {
|
|
|
- const matches = this._matchingEvents(subscribed, events);
|
|
|
+ const matches = this._matchingEvents(subscribed, events)
|
|
|
if (matches && matches.length) {
|
|
|
- resolve(matches);
|
|
|
+ resolve(matches)
|
|
|
}
|
|
|
- });
|
|
|
- });
|
|
|
+ })
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
/*
|
|
@@ -159,68 +152,67 @@ class RuntimeApi
|
|
|
* If the subscribed events are given, and a callback as well, then the
|
|
|
* callback is invoked with matching events.
|
|
|
*/
|
|
|
- async signAndSend(accountId, tx, attempts, subscribed, callback)
|
|
|
- {
|
|
|
+ async signAndSend (accountId, tx, attempts, subscribed, callback) {
|
|
|
// Prepare key
|
|
|
- const from_key = this.identities.keyring.getPair(accountId);
|
|
|
+ const from_key = this.identities.keyring.getPair(accountId)
|
|
|
|
|
|
if (from_key.isLocked) {
|
|
|
- throw new Error('Must unlock key before using it to sign!');
|
|
|
+ throw new Error('Must unlock key before using it to sign!')
|
|
|
}
|
|
|
|
|
|
- const finalizedPromise = newExternallyControlledPromise();
|
|
|
+ const finalizedPromise = newExternallyControlledPromise()
|
|
|
|
|
|
- let unsubscribe = await this.executeWithAccountLock(accountId, async () => {
|
|
|
+ let unsubscribe = await this.executeWithAccountLock(accountId, async () => {
|
|
|
// Try to get the next nonce to use
|
|
|
- let nonce = this.nonces[accountId];
|
|
|
+ let nonce = this.nonces[accountId]
|
|
|
|
|
|
let incrementNonce = () => {
|
|
|
// only increment once
|
|
|
- incrementNonce = () => {}; // turn it into a no-op
|
|
|
- nonce = nonce.addn(1);
|
|
|
- this.nonces[accountId] = nonce;
|
|
|
+ incrementNonce = () => {} // turn it into a no-op
|
|
|
+ nonce = nonce.addn(1)
|
|
|
+ this.nonces[accountId] = nonce
|
|
|
}
|
|
|
|
|
|
// If the nonce isn't available, get it from chain.
|
|
|
if (!nonce) {
|
|
|
// current nonce
|
|
|
- nonce = await this.api.query.system.accountNonce(accountId);
|
|
|
- debug(`Got nonce for ${accountId} from chain: ${nonce}`);
|
|
|
+ nonce = await this.api.query.system.accountNonce(accountId)
|
|
|
+ debug(`Got nonce for ${accountId} from chain: ${nonce}`)
|
|
|
}
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
- debug('Signing and sending tx');
|
|
|
+ debug('Signing and sending tx')
|
|
|
// send(statusUpdates) returns a function for unsubscribing from status updates
|
|
|
let unsubscribe = tx.sign(from_key, { nonce })
|
|
|
.send(({events = [], status}) => {
|
|
|
- debug(`TX status: ${status.type}`);
|
|
|
+ debug(`TX status: ${status.type}`)
|
|
|
|
|
|
// Whatever events we get, process them if there's someone interested.
|
|
|
// It is critical that this event handling doesn't prevent
|
|
|
try {
|
|
|
if (subscribed && callback) {
|
|
|
- const matched = this._matchingEvents(subscribed, events);
|
|
|
- debug('Matching events:', matched);
|
|
|
+ const matched = this._matchingEvents(subscribed, events)
|
|
|
+ debug('Matching events:', matched)
|
|
|
if (matched.length) {
|
|
|
- callback(matched);
|
|
|
+ callback(matched)
|
|
|
}
|
|
|
}
|
|
|
- } catch(err) {
|
|
|
+ } catch (err) {
|
|
|
debug(`Error handling events ${err.stack}`)
|
|
|
}
|
|
|
|
|
|
// We want to release lock as early as possible, sometimes Ready status
|
|
|
// doesn't occur, so we do it on Broadcast instead
|
|
|
if (status.isReady) {
|
|
|
- debug('TX Ready.');
|
|
|
- incrementNonce();
|
|
|
- resolve(unsubscribe); //releases lock
|
|
|
+ debug('TX Ready.')
|
|
|
+ incrementNonce()
|
|
|
+ resolve(unsubscribe) // releases lock
|
|
|
} else if (status.isBroadcast) {
|
|
|
- debug('TX Broadcast.');
|
|
|
- incrementNonce();
|
|
|
- resolve(unsubscribe); //releases lock
|
|
|
+ debug('TX Broadcast.')
|
|
|
+ incrementNonce()
|
|
|
+ resolve(unsubscribe) // releases lock
|
|
|
} else if (status.isFinalized) {
|
|
|
- debug('TX Finalized.');
|
|
|
+ debug('TX Finalized.')
|
|
|
finalizedPromise.resolve(status)
|
|
|
} else if (status.isFuture) {
|
|
|
// comes before ready.
|
|
@@ -228,10 +220,10 @@ class RuntimeApi
|
|
|
// nonce was set in the future. Treating it as an error for now.
|
|
|
debug('TX Future!')
|
|
|
// nonce is likely out of sync, delete it so we reload it from chain on next attempt
|
|
|
- delete this.nonces[accountId];
|
|
|
- const err = new Error('transaction nonce set in future');
|
|
|
- finalizedPromise.reject(err);
|
|
|
- reject(err);
|
|
|
+ delete this.nonces[accountId]
|
|
|
+ const err = new Error('transaction nonce set in future')
|
|
|
+ finalizedPromise.reject(err)
|
|
|
+ reject(err)
|
|
|
}
|
|
|
|
|
|
/* why don't we see these status updates on local devchain (single node)
|
|
@@ -247,45 +239,45 @@ class RuntimeApi
|
|
|
// Remember this can also happen if in the past we sent a tx with a future nonce, and the current nonce
|
|
|
// now matches it.
|
|
|
if (err) {
|
|
|
- const errstr = err.toString();
|
|
|
+ const errstr = err.toString()
|
|
|
// not the best way to check error code.
|
|
|
// https://github.com/polkadot-js/api/blob/master/packages/rpc-provider/src/coder/index.ts#L52
|
|
|
if (errstr.indexOf('Error: 1014:') < 0 && // low priority
|
|
|
errstr.indexOf('Error: 1010:') < 0) // bad transaction
|
|
|
{
|
|
|
// Error but not nonce related. (bad arguments maybe)
|
|
|
- debug('TX error', err);
|
|
|
+ debug('TX error', err)
|
|
|
} else {
|
|
|
// nonce is likely out of sync, delete it so we reload it from chain on next attempt
|
|
|
- delete this.nonces[accountId];
|
|
|
+ delete this.nonces[accountId]
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- finalizedPromise.reject(err);
|
|
|
+ finalizedPromise.reject(err)
|
|
|
// releases lock
|
|
|
- reject(err);
|
|
|
- });
|
|
|
- });
|
|
|
+ reject(err)
|
|
|
+ })
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
// when does it make sense to manyally unsubscribe?
|
|
|
// at this point unsubscribe.then and unsubscribe.catch have been deleted
|
|
|
- // unsubscribe(); // don't unsubscribe if we want to wait for additional status
|
|
|
+ // unsubscribe() // don't unsubscribe if we want to wait for additional status
|
|
|
// updates to know when the tx has been finalized
|
|
|
- return finalizedPromise.promise;
|
|
|
+ return finalizedPromise.promise
|
|
|
}
|
|
|
}
|
|
|
|
|
|
module.exports = {
|
|
|
- RuntimeApi: RuntimeApi,
|
|
|
+ RuntimeApi
|
|
|
}
|
|
|
|
|
|
function newExternallyControlledPromise () {
|
|
|
// externally controlled promise
|
|
|
- let resolve, reject;
|
|
|
+ let resolve, reject
|
|
|
const promise = new Promise((res, rej) => {
|
|
|
- resolve = res;
|
|
|
- reject = rej;
|
|
|
- });
|
|
|
- return ({resolve, reject, promise});
|
|
|
+ resolve = res
|
|
|
+ reject = rej
|
|
|
+ })
|
|
|
+ return ({resolve, reject, promise})
|
|
|
}
|