Browse Source

Merge pull request #1544 from mnaamani/network-tests/runtime-upgrade

Network-tests: runtime ugprade from alexandria
Bedeho Mender 4 years ago
parent
commit
a77ce888ce

+ 28 - 9
.github/workflows/run-network-tests.yml

@@ -1,7 +1,7 @@
 name: run-network-tests
 on:
   pull_request:
-    types: [opened, labeled, synchronize]
+    types: [opened, synchronize]
 
   workflow_dispatch:
     # TODO: add an input so dispatcher can specify a list of tests to run,
@@ -79,8 +79,7 @@ jobs:
           path: joystream-node-docker-image.tar.gz
   
   network_tests_1:
-    name: Network Integration Runtime Tests
-    if: contains(github.event.pull_request.labels.*.name, 'run-network-tests')
+    name: Integration Tests (Runtime Upgrade)
     needs: build_images
     runs-on: ubuntu-latest
     steps:
@@ -104,8 +103,31 @@ jobs:
         run: tests/network-tests/run-tests.sh
 
   network_tests_2:
+    name: Integration Tests (New Chain)
+    needs: build_images
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions/setup-node@v1
+        with:
+          node-version: '12.x'
+      - name: Get artifacts
+        uses: actions/download-artifact@v2
+        with:
+          name: ${{ needs.build_images.outputs.use_artifact }}
+      - name: Install artifacts
+        run: |
+          docker load --input joystream-node-docker-image.tar.gz
+          docker images
+      - name: Install packages and dependencies
+        run: yarn install --frozen-lockfile
+      - name: Ensure tests are runnable
+        run: yarn workspace network-tests build
+      - name: Execute network tests
+        run: RUNTIME=latest tests/network-tests/run-tests.sh
+
+  network_tests_3:
     name: Content Directory Initialization
-    if: contains(github.event.pull_request.labels.*.name, 'run-network-tests')
     needs: build_images
     runs-on: ubuntu-latest
     steps:
@@ -130,9 +152,8 @@ jobs:
       - name: Initialize the content directory
         run: yarn workspace cd-schemas initialize:dev
 
-  network_tests_3:
+  network_tests_4:
     name: Storage Node Tests
-    if: contains(github.event.pull_request.labels.*.name, 'run-network-tests')
     needs: build_images
     runs-on: ubuntu-latest
     steps:
@@ -149,9 +170,7 @@ jobs:
           docker load --input joystream-node-docker-image.tar.gz
           docker images
       - name: Install packages and dependencies
-        run: |
-          yarn install --frozen-lockfile
-          yarn workspace storage-node build
+        run: yarn install --frozen-lockfile
       - name: Build storage node
         run: yarn workspace storage-node build
       - name: Start Services

+ 1 - 1
package.json

@@ -18,7 +18,7 @@
     "devops/prettier-config",
     "pioneer",
     "pioneer/packages/*",
-    "utils/api-examples",
+    "utils/api-scripts",
     "content-directory-schemas"
   ],
   "resolutions": {

+ 36 - 5
tests/network-tests/run-tests.sh

@@ -5,11 +5,18 @@ set -e
 # This is how we access the initial members and balances files from
 # the containers and where generated chainspec files will be located.
 DATA_PATH=${DATA_PATH:=~/tmp}
+
+# Initial account balance for Alice
+# Alice is the source of funds for all new accounts that are created in the tests.
 ALICE_INITIAL_BALANCE=${ALICE_INITIAL_BALANCE:=100000000}
 
+# The docker image tag to use for joystream/node as the starting chain
+# that will be upgraded to the latest runtime.
+RUNTIME=${RUNTIME:=alexandria}
+TARGET_RUNTIME=${TARGET_RUNTIME:=latest}
+
 mkdir -p ${DATA_PATH}
 
-# Alice is the source of funds for all new members that are created in the tests.
 echo "{
   \"balances\":[
     [\"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY\", ${ALICE_INITIAL_BALANCE}]
@@ -30,7 +37,7 @@ echo '
 ' > ${DATA_PATH}/initial-members.json
 
 # Create a chain spec file
-docker run --rm -v ${DATA_PATH}:/data --entrypoint ./chain-spec-builder joystream/node \
+docker run --rm -v ${DATA_PATH}:/data --entrypoint ./chain-spec-builder joystream/node:${RUNTIME} \
   new \
   --authority-seeds Alice \
   --sudo-account  5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY \
@@ -40,13 +47,14 @@ docker run --rm -v ${DATA_PATH}:/data --entrypoint ./chain-spec-builder joystrea
   --initial-members-path /data/initial-members.json
 
 # Convert the chain spec file to a raw chainspec file
-docker run --rm -v ${DATA_PATH}:/data joystream/node build-spec \
+docker run --rm -v ${DATA_PATH}:/data joystream/node:${RUNTIME} build-spec \
   --raw --disable-default-bootnode \
   --chain /data/chain-spec.json > ~/tmp/chain-spec-raw.json
 
 # Start a chain with generated chain spec
-CONTAINER_ID=`docker run -d -v ${DATA_PATH}:/data -p 9944:9944 joystream/node \
-  --validator --alice --unsafe-ws-external --rpc-cors=all --log runtime \
+# Add "-l ws=trace,ws::handler=info" to get websocket trace logs
+CONTAINER_ID=`docker run -d -v ${DATA_PATH}:/data -p 9944:9944 joystream/node:${RUNTIME} \
+  --validator --alice --unsafe-ws-external --rpc-cors=all -l runtime \
   --chain /data/chain-spec-raw.json`
 
 function cleanup() {
@@ -57,5 +65,28 @@ function cleanup() {
 
 trap cleanup EXIT
 
+if [ "$TARGET_RUNTIME" == "$RUNTIME" ]; then
+  echo "Not Performing a runtime upgrade."
+else
+  # Copy new runtime wasm file from target joystream/node image
+  echo "Extracting wasm blob from target joystream/node image."
+  mkdir -p .tmp/
+  id=`docker create joystream/node:${TARGET_RUNTIME}`
+  docker cp $id:/joystream/runtime.compact.wasm .tmp/
+  docker rm $id
+
+  # Display runtime version before runtime upgrade
+  yarn workspace api-scripts tsnode-strict src/status.ts | grep Runtime
+
+  echo "Performing runtime upgrade."
+  DEBUG=* yarn workspace api-scripts tsnode-strict \
+    src/dev-set-runtime-code.ts -- `pwd`/.tmp/runtime.compact.wasm
+
+  echo "Runtime upgraded."
+fi
+
+# Display runtime version
+yarn workspace api-scripts tsnode-strict src/status.ts | grep Runtime
+
 # Execute the tests
 time DEBUG=* yarn workspace network-tests test-run src/scenarios/full.ts

+ 0 - 20
utils/api-examples/src/get-code.ts

@@ -1,20 +0,0 @@
-import { ApiPromise, WsProvider } from '@polkadot/api'
-import { types } from '@joystream/types'
-
-async function main() {
-  const provider = new WsProvider('ws://127.0.0.1:9944')
-
-  const api = await ApiPromise.create({ provider, types })
-
-  const currentBlockHash = await api.rpc.chain.getBlockHash(1)
-
-  console.log('getting code as of block hash', currentBlockHash.toString())
-
-  const substrateWasm = await api.query.substrate.code.at(currentBlockHash)
-
-  console.log(substrateWasm.toHex())
-
-  api.disconnect()
-}
-
-main()

+ 0 - 30
utils/api-examples/src/tohex.ts

@@ -1,30 +0,0 @@
-import { CuratorApplicationId } from '@joystream/types/content-working-group'
-import { BTreeSet, createType, TypeRegistry } from '@polkadot/types'
-import { types } from '@joystream/types'
-
-async function main() {
-  const wgId = [1, 2]
-
-  const registry = new TypeRegistry()
-  registry.register(types)
-
-  const set = new BTreeSet<CuratorApplicationId>(registry, CuratorApplicationId, [])
-
-  wgId.forEach((id) => {
-    set.add(createType(registry, 'CuratorApplicationId', id))
-  })
-
-  /*
-    Replace the integers inside the bracket in:
-    let wgId:number[] = [1, 2];
-    With the "WG ID"s of the curators you wish to hire, in ascending order.
-
-    To hire "WG ID" 18 21 and 16:
-    let wgId:number[] = [16, 18, 21];
-    */
-
-  console.log('copy/paste the output below to hire curator applicant(s) with WG IDs:', wgId)
-  console.log(set.toHex())
-}
-
-main()

+ 0 - 0
utils/api-examples/README.md → utils/api-scripts/README.md


+ 3 - 2
utils/api-examples/package.json → utils/api-scripts/package.json

@@ -1,11 +1,12 @@
 {
-  "name": "api-examples",
+  "name": "api-scripts",
   "private": true,
   "version": "0.1.0",
   "license": "GPL-3.0-only",
   "scripts": {
     "status": "ts-node src/status",
-    "script": "ts-node src/script"
+    "script": "ts-node src/script",
+    "tsnode-strict": "node -r ts-node/register --unhandled-rejections=strict"
   },
   "dependencies": {
     "@joystream/types": "^0.14.0",

+ 0 - 0
utils/api-examples/scripts/example.js → utils/api-scripts/scripts/example.js


+ 0 - 0
utils/api-examples/scripts/export-data-directory.js → utils/api-scripts/scripts/export-data-directory.js


+ 0 - 1
utils/api-examples/scripts/index.js → utils/api-scripts/scripts/index.js

@@ -5,6 +5,5 @@ exportedScripts.exportDataDirectory = require('./export-data-directory.js')
 exportedScripts.injectDataObjects = require('./inject-data-objects.js')
 exportedScripts.listDataDirectory = require('./list-data-directory.js')
 exportedScripts.testTransfer = require('./transfer.js')
-exportedScripts.initNewContentDir = require('./init-new-content-directory')
 
 module.exports = exportedScripts

+ 0 - 0
utils/api-examples/scripts/inject-data-objects.js → utils/api-scripts/scripts/inject-data-objects.js


+ 0 - 0
utils/api-examples/scripts/list-data-directory.js → utils/api-scripts/scripts/list-data-directory.js


+ 0 - 0
utils/api-examples/scripts/transfer.js → utils/api-scripts/scripts/transfer.js


+ 111 - 0
utils/api-scripts/src/dev-set-runtime-code.ts

@@ -0,0 +1,111 @@
+import { ApiPromise } from '@polkadot/api'
+import { types } from '@joystream/types'
+import { Keyring } from '@polkadot/keyring'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { DispatchError, DispatchResult } from '@polkadot/types/interfaces/system'
+import { TypeRegistry } from '@polkadot/types'
+import fs from 'fs'
+import { compactAddLength } from '@polkadot/util'
+
+// Patched WsProvider with larger fragment size for messages
+import WsProvider from './patched-ws-provider'
+
+function onApiDisconnected() {
+  process.exit(2)
+}
+
+function onApiError() {
+  process.exit(3)
+}
+
+async function main() {
+  const file = process.argv[2]
+
+  if (!file) {
+    console.log('No wasm file argument provided.')
+    process.exit(1)
+  }
+
+  const wasm = Uint8Array.from(fs.readFileSync(file))
+  console.log('WASM bytes:', wasm.byteLength)
+
+  const provider = new WsProvider('ws://127.0.0.1:9944')
+
+  let api: ApiPromise
+  let retry = 3
+  while (true) {
+    try {
+      api = await ApiPromise.create({ provider, types })
+      await api.isReady
+      break
+    } catch (err) {
+      // failed to connect to node
+    }
+
+    if (retry-- === 0) {
+      process.exit(-1)
+    }
+
+    await new Promise((resolve) => {
+      setTimeout(resolve, 5000)
+    })
+  }
+
+  const keyring = new Keyring()
+  const sudo = keyring.addFromUri('//Alice', undefined, 'sr25519')
+
+  // DO NOT SET UNCHECKED!
+  // const tx = api.tx.system.setCodeWithoutChecks(wasm)
+
+  const setCodeTx = api.tx.system.setCode(compactAddLength(wasm))
+  const sudoTx = api.tx.sudo.sudoUncheckedWeight(setCodeTx, 1)
+  const nonce = (await api.query.system.account(sudo.address)).nonce
+  const signedTx = sudoTx.sign(sudo, { nonce })
+
+  // console.log('Tx size:', signedTx.length)
+  // const wasmCodeInTxArg = (signedTx.method.args[0] as Call).args[0]
+  // console.log('WASM code arg byte length:', (wasmCodeInTxArg as Bytes).byteLength)
+
+  await signedTx.send((result: ISubmittableResult) => {
+    if (result.status.isInBlock && result.events !== undefined) {
+      result.events.forEach((event) => {
+        if (event.event.method === 'ExtrinsicFailed') {
+          console.log('ExtrinsicFailed', (event.event.data[0] as DispatchError).toHuman())
+          process.exit(4)
+        }
+
+        if (event.event.method === 'Sudid') {
+          const result = event.event.data[0] as DispatchResult
+          if (result.isOk) {
+            process.exit(0)
+          } else if (result.isError) {
+            const err = result.asError
+            console.log('Error:', err.toHuman())
+            if (err.isModule) {
+              const { name, documentation } = (api.registry as TypeRegistry).findMetaError(err.asModule)
+              console.log(`${name}\n${documentation}`)
+            }
+            process.exit(5)
+          } else {
+            console.log('Sudid result:', result.toHuman())
+            process.exit(-1)
+          }
+        }
+      })
+
+      // Wait a few seconds to display new runtime changes
+      setTimeout(() => {
+        process.exit(0)
+      }, 12000)
+    }
+  })
+
+  api.on('disconnected', onApiDisconnected)
+  api.on('error', onApiError)
+
+  await new Promise(() => {
+    // wait until transaction finalizes
+  })
+}
+
+main()

+ 460 - 0
utils/api-scripts/src/patched-ws-provider.ts

@@ -0,0 +1,460 @@
+// Copyright 2017-2020 @polkadot/rpc-provider authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+/* eslint-disable camelcase */
+
+// WsProvider implementation copied from @polkadot/api v1.26.1
+// It wasn't possible to extend or even monkey-patch it because of the
+// use of ECMAScript Private Fields.
+// The only modification is construction of the `w3cwebsocket` instance
+// in the connect() method to increase the fragment size to 256K from the
+// default of 16K
+
+import {
+  JsonRpcResponse,
+  ProviderInterface,
+  ProviderInterfaceCallback,
+  ProviderInterfaceEmitted,
+  ProviderInterfaceEmitCb,
+} from '@polkadot/rpc-provider/types'
+
+import EventEmitter from 'eventemitter3'
+import { assert, isNull, isUndefined, isChildClass, logger } from '@polkadot/util'
+
+import Coder from '@polkadot/rpc-provider/coder'
+import defaults from '@polkadot/rpc-provider/defaults'
+import getWSClass from '@polkadot/rpc-provider/ws/getWSClass'
+
+interface SubscriptionHandler {
+  callback: ProviderInterfaceCallback
+  type: string
+}
+
+interface WsStateAwaiting {
+  callback: ProviderInterfaceCallback
+  method: string
+  params: any[]
+  subscription?: SubscriptionHandler
+}
+
+interface WsStateSubscription extends SubscriptionHandler {
+  method: string
+  params: any[]
+}
+
+interface WSProviderInterface extends ProviderInterface {
+  connect(): void
+}
+
+const ALIASSES: { [index: string]: string } = {
+  chain_finalisedHead: 'chain_finalizedHead',
+  chain_subscribeFinalisedHeads: 'chain_subscribeFinalizedHeads',
+  chain_unsubscribeFinalisedHeads: 'chain_unsubscribeFinalizedHeads',
+}
+
+const l = logger('api-ws')
+
+/**
+ * # @polkadot/rpc-provider/ws
+ *
+ * @name WsProvider
+ *
+ * @description The WebSocket Provider allows sending requests using WebSocket to a WebSocket RPC server TCP port. Unlike the [[HttpProvider]], it does support subscriptions and allows listening to events such as new blocks or balance changes.
+ *
+ * @example
+ * <BR>
+ *
+ * ```javascript
+ * import Api from '@polkadot/api/promise';
+ * import WsProvider from '@polkadot/rpc-provider/ws';
+ *
+ * const provider = new WsProvider('ws://127.0.0.1:9944');
+ * const api = new Api(provider);
+ * ```
+ *
+ * @see [[HttpProvider]]
+ */
+export default class WsProvider implements WSProviderInterface {
+  readonly coder: Coder
+
+  readonly endpoints: string[]
+
+  readonly headers: Record<string, string>
+
+  readonly eventemitter: EventEmitter
+
+  readonly handlers: Record<string, WsStateAwaiting> = {}
+
+  readonly queued: Record<string, string> = {}
+
+  readonly waitingForId: Record<string, JsonRpcResponse> = {}
+
+  private autoConnectMs: number
+
+  private endpointIndex: number
+
+  private _isConnected = false
+
+  private subscriptions: Record<string, WsStateSubscription> = {}
+
+  private websocket: WebSocket | null
+
+  /**
+   * @param {string | string[]}  endpoint    The endpoint url. Usually `ws://ip:9944` or `wss://ip:9944`, may provide an array of endpoint strings.
+   * @param {boolean} autoConnect Whether to connect automatically or not.
+   */
+  constructor(
+    endpoint: string | string[] = defaults.WS_URL,
+    autoConnectMs: number | false = 1000,
+    headers: Record<string, string> = {}
+  ) {
+    const endpoints = Array.isArray(endpoint) ? endpoint : [endpoint]
+
+    assert(endpoints.length !== 0, 'WsProvider requires at least one Endpoint')
+
+    endpoints.forEach((endpoint) => {
+      assert(/^(wss|ws):\/\//.test(endpoint), `Endpoint should start with 'ws://', received '${endpoint}'`)
+    })
+
+    this.eventemitter = new EventEmitter()
+    this.autoConnectMs = autoConnectMs || 0
+    this.coder = new Coder()
+    this.endpointIndex = -1
+    this.endpoints = endpoints
+    this.headers = headers
+    this.websocket = null
+
+    if (autoConnectMs > 0) {
+      // eslint-disable-next-line @typescript-eslint/no-floating-promises
+      this.connect()
+    }
+  }
+
+  /**
+   * @summary `true` when this provider supports subscriptions
+   */
+  public get hasSubscriptions(): boolean {
+    return true
+  }
+
+  /**
+   * @description Returns a clone of the object
+   */
+  public clone(): WsProvider {
+    return new WsProvider(this.endpoints)
+  }
+
+  /**
+   * @summary Manually connect
+   * @description The [[WsProvider]] connects automatically by default, however if you decided otherwise, you may
+   * connect manually using this method.
+   */
+  public async connect(): Promise<void> {
+    try {
+      this.endpointIndex = (this.endpointIndex + 1) % this.endpoints.length
+
+      const WS = await getWSClass()
+
+      this.websocket =
+        typeof WebSocket !== 'undefined' && isChildClass(WebSocket, WS)
+          ? new WS(this.endpoints[this.endpointIndex])
+          : // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+            // @ts-ignore - WS may be an instance of w3cwebsocket, which supports headers
+            new WS(this.endpoints[this.endpointIndex], undefined, undefined, this.headers, undefined, {
+              // default: true
+              fragmentOutgoingMessages: true,
+              // default: 16K
+              fragmentationThreshold: 256 * 1024,
+            })
+      this.websocket.onclose = this.onSocketClose
+      this.websocket.onerror = this.onSocketError
+      this.websocket.onmessage = this.onSocketMessage
+      this.websocket.onopen = this.onSocketOpen
+    } catch (error) {
+      l.error(error)
+    }
+  }
+
+  /**
+   * @description Manually disconnect from the connection, clearing autoconnect logic
+   */
+  public disconnect(): void {
+    if (isNull(this.websocket)) {
+      throw new Error('Cannot disconnect on a non-open websocket')
+    }
+
+    // switch off autoConnect, we are in manual mode now
+    this.autoConnectMs = 0
+
+    // 1000 - Normal closure; the connection successfully completed
+    this.websocket.close(1000)
+    this.websocket = null
+  }
+
+  /**
+   * @summary Whether the node is connected or not.
+   * @return {boolean} true if connected
+   */
+  public isConnected(): boolean {
+    return this._isConnected
+  }
+
+  /**
+   * @summary Listens on events after having subscribed using the [[subscribe]] function.
+   * @param  {ProviderInterfaceEmitted} type Event
+   * @param  {ProviderInterfaceEmitCb}  sub  Callback
+   * @return unsubscribe function
+   */
+  public on(type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): () => void {
+    this.eventemitter.on(type, sub)
+
+    return (): void => {
+      this.eventemitter.removeListener(type, sub)
+    }
+  }
+
+  /**
+   * @summary Send JSON data using WebSockets to configured HTTP Endpoint or queue.
+   * @param method The RPC methods to execute
+   * @param params Encoded paramaters as appliucable for the method
+   * @param subscription Subscription details (internally used)
+   */
+  public send(method: string, params: any[], subscription?: SubscriptionHandler): Promise<any> {
+    return new Promise((resolve, reject): void => {
+      try {
+        const json = this.coder.encodeJson(method, params)
+        const id = this.coder.getId()
+
+        const callback = (error?: Error | null, result?: any): void => {
+          error ? reject(error) : resolve(result)
+        }
+
+        l.debug((): string[] => ['calling', method, json])
+
+        this.handlers[id] = {
+          callback,
+          method,
+          params,
+          subscription,
+        }
+
+        if (this.isConnected() && !isNull(this.websocket)) {
+          this.websocket.send(json)
+        } else {
+          this.queued[id] = json
+        }
+      } catch (error) {
+        reject(error)
+      }
+    })
+  }
+
+  /**
+   * @name subscribe
+   * @summary Allows subscribing to a specific event.
+   * @param  {string}                     type     Subscription type
+   * @param  {string}                     method   Subscription method
+   * @param  {any[]}                 params   Parameters
+   * @param  {ProviderInterfaceCallback} callback Callback
+   * @return {Promise<number>}                     Promise resolving to the dd of the subscription you can use with [[unsubscribe]].
+   *
+   * @example
+   * <BR>
+   *
+   * ```javascript
+   * const provider = new WsProvider('ws://127.0.0.1:9944');
+   * const rpc = new Rpc(provider);
+   *
+   * rpc.state.subscribeStorage([[storage.system.account, <Address>]], (_, values) => {
+   *   console.log(values)
+   * }).then((subscriptionId) => {
+   *   console.log('balance changes subscription id: ', subscriptionId)
+   * })
+   * ```
+   */
+  public async subscribe(
+    type: string,
+    method: string,
+    params: any[],
+    callback: ProviderInterfaceCallback
+  ): Promise<number | string> {
+    const id = (await this.send(method, params, { callback, type })) as Promise<number | string>
+
+    return id
+  }
+
+  /**
+   * @summary Allows unsubscribing to subscriptions made with [[subscribe]].
+   */
+  public async unsubscribe(type: string, method: string, id: number | string): Promise<boolean> {
+    const subscription = `${type}::${id}`
+
+    // FIXME This now could happen with re-subscriptions. The issue is that with a re-sub
+    // the assigned id now does not match what the API user originally received. It has
+    // a slight complication in solving - since we cannot rely on the send id, but rather
+    // need to find the actual subscription id to map it
+    if (isUndefined(this.subscriptions[subscription])) {
+      l.debug((): string => `Unable to find active subscription=${subscription}`)
+
+      return false
+    }
+
+    delete this.subscriptions[subscription]
+
+    const result = (await this.send(method, [id])) as Promise<boolean>
+
+    return result
+  }
+
+  emit = (type: ProviderInterfaceEmitted, ...args: any[]): void => {
+    this.eventemitter.emit(type, ...args)
+  }
+
+  onSocketClose = (event: CloseEvent): void => {
+    if (this.autoConnectMs > 0) {
+      l.error(`disconnected from ${this.endpoints[this.endpointIndex]} code: '${event.code}' reason: '${event.reason}'`)
+    }
+
+    this._isConnected = false
+    this.emit('disconnected')
+
+    if (this.autoConnectMs > 0) {
+      setTimeout((): void => {
+        // eslint-disable-next-line @typescript-eslint/no-floating-promises
+        this.connect()
+      }, this.autoConnectMs)
+    }
+  }
+
+  onSocketError = (error: Event): void => {
+    l.debug((): any => ['socket error', error])
+    this.emit('error', error)
+  }
+
+  onSocketMessage = (message: MessageEvent): void => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    l.debug(() => ['received', message.data])
+
+    const response = JSON.parse(message.data as string) as JsonRpcResponse
+
+    return isUndefined(response.method) ? this.onSocketMessageResult(response) : this.onSocketMessageSubscribe(response)
+  }
+
+  onSocketMessageResult = (response: JsonRpcResponse): void => {
+    const handler = this.handlers[response.id]
+
+    if (!handler) {
+      l.debug((): string => `Unable to find handler for id=${response.id}`)
+
+      return
+    }
+
+    try {
+      const { method, params, subscription } = handler
+      const result = this.coder.decodeResponse(response) as string
+
+      // first send the result - in case of subs, we may have an update
+      // immediately if we have some queued results already
+      handler.callback(null, result)
+
+      if (subscription) {
+        const subId = `${subscription.type}::${result}`
+
+        this.subscriptions[subId] = {
+          ...subscription,
+          method,
+          params,
+        }
+
+        // if we have a result waiting for this subscription already
+        if (this.waitingForId[subId]) {
+          this.onSocketMessageSubscribe(this.waitingForId[subId])
+        }
+      }
+    } catch (error) {
+      handler.callback(error, undefined)
+    }
+
+    delete this.handlers[response.id]
+  }
+
+  onSocketMessageSubscribe = (response: JsonRpcResponse): void => {
+    const method = ALIASSES[response.method as string] || response.method || 'invalid'
+    const subId = `${method}::${response.params.subscription}`
+    const handler = this.subscriptions[subId]
+
+    if (!handler) {
+      // store the JSON, we could have out-of-order subid coming in
+      this.waitingForId[subId] = response
+
+      l.debug((): string => `Unable to find handler for subscription=${subId}`)
+
+      return
+    }
+
+    // housekeeping
+    delete this.waitingForId[subId]
+
+    try {
+      const result = this.coder.decodeResponse(response)
+
+      handler.callback(null, result)
+    } catch (error) {
+      handler.callback(error, undefined)
+    }
+  }
+
+  onSocketOpen = (): boolean => {
+    assert(!isNull(this.websocket), 'WebSocket cannot be null in onOpen')
+
+    l.debug((): any[] => ['connected to', this.endpoints[this.endpointIndex]])
+
+    this._isConnected = true
+
+    this.emit('connected')
+    this.sendQueue()
+    this.resubscribe()
+
+    return true
+  }
+
+  resubscribe = (): void => {
+    const subscriptions = this.subscriptions
+
+    this.subscriptions = {}
+
+    // eslint-disable-next-line @typescript-eslint/no-misused-promises
+    Object.keys(subscriptions).forEach(
+      async (id): Promise<void> => {
+        const { callback, method, params, type } = subscriptions[id]
+
+        // only re-create subscriptions which are not in author (only area where
+        // transactions are created, i.e. submissions such as 'author_submitAndWatchExtrinsic'
+        // are not included (and will not be re-broadcast)
+        if (type.startsWith('author_')) {
+          return
+        }
+
+        try {
+          await this.subscribe(type, method, params, callback)
+        } catch (error) {
+          l.error(error)
+        }
+      }
+    )
+  }
+
+  sendQueue = (): void => {
+    Object.keys(this.queued).forEach((id): void => {
+      try {
+        // we have done the websocket check in onSocketOpen, if an issue, will catch it
+        ;(this.websocket as WebSocket).send(this.queued[id])
+
+        delete this.queued[id]
+      } catch (error) {
+        l.error(error)
+      }
+    })
+  }
+}

+ 2 - 0
utils/api-examples/src/script.ts → utils/api-scripts/src/script.ts

@@ -24,6 +24,8 @@ async function main() {
 
   const api = await ApiPromise.create({ provider, types: joyTypes })
 
+  await api.isReady
+
   // We don't pass a custom signer to the api so we must use a keyPair
   // when calling signAndSend on transactions
   const keyring = new Keyring()

+ 19 - 1
utils/api-examples/src/status.ts → utils/api-scripts/src/status.ts

@@ -11,7 +11,25 @@ async function main() {
   const provider = new WsProvider('ws://127.0.0.1:9944')
 
   // Create the API and wait until ready
-  const api = await ApiPromise.create({ provider, types })
+  let api: ApiPromise
+  let retry = 3
+  while (true) {
+    try {
+      api = await ApiPromise.create({ provider, types })
+      await api.isReady
+      break
+    } catch (err) {
+      // failed to connect to node
+    }
+
+    if (retry-- === 0) {
+      process.exit(-1)
+    }
+
+    await new Promise((resolve) => {
+      setTimeout(resolve, 5000)
+    })
+  }
 
   // Retrieve the chain & node information information via rpc calls
   const [chain, nodeName, nodeVersion] = await Promise.all([

+ 2 - 1
utils/api-examples/tsconfig.json → utils/api-scripts/tsconfig.json

@@ -12,7 +12,8 @@
     "noUnusedLocals": true,
     "baseUrl": "./",
     "paths": {
-      "@polkadot/types/augment": ["../../types/augment-codec/augment-types.ts"]
+      "@polkadot/types/augment": ["../../types/augment-codec/augment-types.ts"],
+      "@polkadot/api/augment": ["../../types/augment-codec/augment-api.ts"]
     }
   },
   "include": ["src/**/*"]