|
@@ -30,7 +30,14 @@ const { AssetsApi } = require('@joystream/storage-runtime-api/assets')
|
|
|
const { DiscoveryApi } = require('@joystream/storage-runtime-api/discovery')
|
|
|
const { SystemApi } = require('@joystream/storage-runtime-api/system')
|
|
|
const AsyncLock = require('async-lock')
|
|
|
-const { newExternallyControlledPromise } = require('@joystream/storage-utils/externalPromise')
|
|
|
+const Promise = require('bluebird')
|
|
|
+
|
|
|
+Promise.config({
|
|
|
+ cancellation: true,
|
|
|
+})
|
|
|
+
|
|
|
+
|
|
|
+const TX_TIMEOUT = 20 * 1000
|
|
|
|
|
|
|
|
|
* Initialize runtime (substrate) API and keyring.
|
|
@@ -55,6 +62,7 @@ class RuntimeApi {
|
|
|
|
|
|
this.api = await ApiPromise.create({ provider })
|
|
|
|
|
|
+
|
|
|
this.asyncLock = new AsyncLock()
|
|
|
|
|
|
|
|
@@ -84,15 +92,9 @@ class RuntimeApi {
|
|
|
return this.asyncLock.acquire(`${accountId}`, func)
|
|
|
}
|
|
|
|
|
|
- static matchingEvents(subscribed, events) {
|
|
|
- if (!events.length) return []
|
|
|
-
|
|
|
+ static matchingEvents(subscribed = [], events = []) {
|
|
|
const filtered = events.filter((record) => {
|
|
|
- const { event } = record
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
+ const { event } = record
|
|
|
|
|
|
|
|
|
const matching = subscribed.filter((value) => {
|
|
@@ -105,41 +107,26 @@ class RuntimeApi {
|
|
|
const { event } = record
|
|
|
const types = event.typeDef
|
|
|
|
|
|
-
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
const payload = {}
|
|
|
event.data.forEach((data, index) => {
|
|
|
-
|
|
|
- payload[types[index].type] = data
|
|
|
+ const type = types[index].type
|
|
|
+ payload[type] = payload[type] || data
|
|
|
})
|
|
|
|
|
|
const fullName = `${event.section}.${event.method}`
|
|
|
return [fullName, payload]
|
|
|
})
|
|
|
|
|
|
- debug('Events', JSON.stringify(mapped))
|
|
|
+ mapped.length && debug('Mapped Events', JSON.stringify(mapped))
|
|
|
|
|
|
return mapped
|
|
|
}
|
|
|
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- static makeEventsHandler(subscribed, callback) {
|
|
|
- return function eventsHandler(events) {
|
|
|
- try {
|
|
|
- if (subscribed && callback) {
|
|
|
- const matched = RuntimeApi.matchingEvents(subscribed, events)
|
|
|
- if (matched.length) {
|
|
|
- callback(matched)
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (err) {
|
|
|
- debug(`Error handling events ${err.stack}`)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
|
|
|
* signAndSend() with nonce tracking, to enable concurrent sending of transacctions
|
|
|
* so that they can be included in the same block. Allows you to use the accountId instead
|
|
@@ -148,88 +135,124 @@ 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, subscribed, callback) {
|
|
|
+ async signAndSend(accountId, tx, subscribed) {
|
|
|
+
|
|
|
accountId = this.identities.keyring.encodeAddress(accountId)
|
|
|
|
|
|
-
|
|
|
+
|
|
|
const fromKey = this.identities.keyring.getPair(accountId)
|
|
|
|
|
|
+
|
|
|
if (fromKey.isLocked) {
|
|
|
throw new Error('Must unlock key before using it to sign!')
|
|
|
}
|
|
|
|
|
|
-
|
|
|
-
|
|
|
- const finalizedPromise = newExternallyControlledPromise()
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ let onFinalizedSuccess
|
|
|
+
|
|
|
+ let onFinalizedFailed
|
|
|
|
|
|
-
|
|
|
-
|
|
|
+
|
|
|
+
|
|
|
let unsubscribe
|
|
|
|
|
|
- const handleEvents = RuntimeApi.makeEventsHandler(subscribed, callback)
|
|
|
+ let lastTxUpdateResult
|
|
|
|
|
|
- const handleTxUpdates = ({ events = [], status }) => {
|
|
|
-
|
|
|
-
|
|
|
- handleEvents(events)
|
|
|
+ const handleTxUpdates = (result) => {
|
|
|
+ const { events = [], status } = result
|
|
|
+ lastTxUpdateResult = result
|
|
|
+ debug(status.type)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if (status.isUsurped) {
|
|
|
+ debug(JSON.stringify(result))
|
|
|
+ onFinalizedFailed({ err: 'Usurped' })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (status.isDropped) {
|
|
|
+ debug(JSON.stringify(result))
|
|
|
+ onFinalizedFailed({ err: 'Dropped' })
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if (status.isInvalid) {
|
|
|
+ debug(JSON.stringify(result))
|
|
|
+ }
|
|
|
|
|
|
if (status.isFinalized) {
|
|
|
-
|
|
|
-
|
|
|
unsubscribe()
|
|
|
- finalizedPromise.resolve(status.asFinalized)
|
|
|
- } else if (status.isFuture) {
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
+ const mappedEvents = RuntimeApi.matchingEvents(subscribed, events)
|
|
|
|
|
|
-
|
|
|
+ const failed = result.findRecord('system', 'ExtrinsicFailed')
|
|
|
+ const success = result.findRecord('system', 'ExtrinsicSuccess')
|
|
|
|
|
|
- debug(`== Error: Submitted transaction with future nonce ==`)
|
|
|
- delete this.nonces[accountId]
|
|
|
- finalizedPromise.reject('Future Tx Nonce')
|
|
|
+ if (failed) {
|
|
|
+ onFinalizedFailed({ err: 'ExtrinsicFailed', result, tx: status.asFinalized })
|
|
|
+ } else if (success) {
|
|
|
+
|
|
|
+ onFinalizedSuccess({ mappedEvents, result, tx: status.asFinalized })
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
await this.executeWithAccountLock(accountId, async () => {
|
|
|
-
|
|
|
- let nonce = this.nonces[accountId]
|
|
|
-
|
|
|
- const nonceWasCached = nonce !== undefined
|
|
|
-
|
|
|
- nonce = this.nonces[accountId] = nonce || (await this.api.query.system.accountNonce(accountId))
|
|
|
+ const nonce = this.nonces[accountId] || (await this.api.query.system.accountNonce(accountId))
|
|
|
|
|
|
try {
|
|
|
unsubscribe = await tx.sign(fromKey, { nonce }).send(handleTxUpdates)
|
|
|
-
|
|
|
-
|
|
|
- if (this.nonces[accountId] !== undefined) {
|
|
|
- this.nonces[accountId] = nonce.addn(1)
|
|
|
- }
|
|
|
+ debug('TransactionSubmitted')
|
|
|
+
|
|
|
+ this.nonces[accountId] = nonce.addn(1)
|
|
|
} catch (err) {
|
|
|
- debug('Transaction Rejected:', err.toString())
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- if (nonceWasCached) {
|
|
|
+ const errstr = err.toString()
|
|
|
+ debug('TransactionRejected:', errstr)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if (errstr.indexOf('ExtrinsicStatus:: 1010: Invalid Transaction: Stale') !== -1) {
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
delete this.nonces[accountId]
|
|
|
}
|
|
|
|
|
|
- finalizedPromise.reject(err)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if (errstr.indexOf('ExtrinsicStatus:: 1014: Priority is too low') !== -1) {
|
|
|
+ delete this.nonces[accountId]
|
|
|
+ }
|
|
|
+
|
|
|
+ throw err
|
|
|
}
|
|
|
})
|
|
|
|
|
|
- return finalizedPromise.promise
|
|
|
+
|
|
|
+ if (lastTxUpdateResult.status.isFuture) {
|
|
|
+ debug('Warning: Submitted extrinsic with future nonce')
|
|
|
+ return {}
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ onFinalizedSuccess = resolve
|
|
|
+ onFinalizedFailed = reject
|
|
|
+ }).timeout(TX_TIMEOUT)
|
|
|
}
|
|
|
|
|
|
|
|
@@ -237,23 +260,32 @@ class RuntimeApi {
|
|
|
* module and return eventProperty from the event.
|
|
|
*/
|
|
|
async signAndSendThenGetEventResult(senderAccountId, tx, { eventModule, eventName, eventProperty }) {
|
|
|
+ if (!eventModule || !eventName || !eventProperty) {
|
|
|
+ throw new Error('MissingSubscribeEventDetails')
|
|
|
+ }
|
|
|
+
|
|
|
|
|
|
const subscribed = [[eventModule, eventName]]
|
|
|
-
|
|
|
-
|
|
|
- return new Promise(async (resolve, reject) => {
|
|
|
- try {
|
|
|
- await this.signAndSend(senderAccountId, tx, subscribed, (events) => {
|
|
|
- events.forEach((event) => {
|
|
|
-
|
|
|
-
|
|
|
- resolve(event[1][eventProperty])
|
|
|
- })
|
|
|
- })
|
|
|
- } catch (err) {
|
|
|
- reject(err)
|
|
|
- }
|
|
|
- })
|
|
|
+
|
|
|
+ const { mappedEvents } = await this.signAndSend(senderAccountId, tx, subscribed)
|
|
|
+
|
|
|
+ if (!mappedEvents) {
|
|
|
+
|
|
|
+ throw new Error('NoEventsCanBeCaptured')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!mappedEvents.length) {
|
|
|
+
|
|
|
+ throw new Error('ExpectedEventNotFound')
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ const firstEvent = mappedEvents[0]
|
|
|
+ const payload = firstEvent[1]
|
|
|
+
|
|
|
+
|
|
|
+ return payload[eventProperty]
|
|
|
}
|
|
|
}
|
|
|
|