Преглед на файлове

Merge pull request #1565 from iorveth/content_directory_integration_tests

Content directory integration tests, part 1
Mokhtar Naamani преди 4 години
родител
ревизия
f794faafcf

+ 2 - 2
package.json

@@ -37,9 +37,9 @@
     "@dzlzv/hydra-indexer-lib": "0.0.19-legacy.1.26.1"
   },
   "devDependencies": {
+    "eslint": "^7.6.0",
     "husky": "^4.2.5",
-    "prettier": "2.0.2",
-    "eslint": "^7.6.0"
+    "prettier": "2.0.2"
   },
   "husky": {
     "hooks": {

+ 2 - 0
tests/network-tests/.env

@@ -1,5 +1,7 @@
 # Address of the Joystream node.
 NODE_URL = ws://127.0.0.1:9944
+# Address of the Joystream query node.
+QUERY_NODE_URL = http://127.0.0.1:8080/graphql
 # Account which is expected to provide sufficient funds to test accounts.
 TREASURY_ACCOUNT_URI = //Alice
 # Sudo Account

+ 2 - 1
tests/network-tests/package.json

@@ -21,7 +21,8 @@
     "bn.js": "^4.11.8",
     "dotenv": "^8.2.0",
     "fs": "^0.0.1-security",
-    "uuid": "^7.0.3"
+    "uuid": "^7.0.3",
+    "@apollo/client": "^3.2.5"
   },
   "devDependencies": {
     "@polkadot/ts": "^0.3.14",

+ 139 - 5
tests/network-tests/src/Api.ts

@@ -13,7 +13,7 @@ import {
   Opening as WorkingGroupOpening,
 } from '@joystream/types/working-group'
 import { ElectionStake, Seat } from '@joystream/types/council'
-import { AccountInfo, Balance, BalanceOf, BlockNumber, Event, EventRecord } from '@polkadot/types/interfaces'
+import { AccountInfo, Hash, Balance, BalanceOf, BlockNumber, Event, EventRecord } from '@polkadot/types/interfaces'
 import BN from 'bn.js'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { Sender } from './sender'
@@ -30,6 +30,12 @@ import {
 } from '@joystream/types/hiring'
 import { FillOpeningParameters, ProposalId } from '@joystream/types/proposals'
 import { v4 as uuid } from 'uuid'
+import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
+import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
+import { initializeContentDir, InputParser, ExtrinsicsHelper } from 'cd-schemas'
+import { OperationType } from '@joystream/types/content-directory'
+import { gql, ApolloClient, ApolloQueryResult, NormalizedCacheObject } from '@apollo/client'
+
 import Debugger from 'debug'
 const debug = Debugger('api')
 
@@ -39,11 +45,11 @@ export enum WorkingGroups {
 }
 
 export class Api {
-  private readonly api: ApiPromise
-  private readonly sender: Sender
-  private readonly keyring: Keyring
+  protected readonly api: ApiPromise
+  protected readonly sender: Sender
+  protected readonly keyring: Keyring
   // source of funds for all new accounts
-  private readonly treasuryAccount: string
+  protected readonly treasuryAccount: string
 
   public static async create(provider: WsProvider, treasuryAccountUri: string, sudoAccountUri: string): Promise<Api> {
     let connectAttempts = 0
@@ -1707,6 +1713,7 @@ export class Api {
     ).filter((addr) => addr !== '')
   }
   */
+
   public async terminateApplication(
     leader: string,
     applicationId: ApplicationId,
@@ -1918,4 +1925,131 @@ export class Api {
   public getMaxWorkersCount(module: WorkingGroups): BN {
     return this.api.createType('u32', this.api.consts[module].maxWorkerNumberLimit)
   }
+
+  async sendContentDirectoryTransaction(operations: OperationType[]): Promise<void> {
+    const transaction = this.api.tx.contentDirectory.transaction(
+      { Lead: null }, // We use member with id 0 as actor (in this case we assume this is Alice)
+      operations // We provide parsed operations as second argument
+    )
+    const lead = (await this.getGroupLead(WorkingGroups.ContentDirectoryWorkingGroup)) as Worker
+    await this.sender.signAndSend(transaction, lead.role_account_id, false)
+  }
+
+  public async createChannelEntity(channel: ChannelEntity): Promise<void> {
+    // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
+    const parser = InputParser.createWithKnownSchemas(
+      this.api,
+      // The second argument is an array of entity batches, following standard entity batch syntax ({ className, entries }):
+      [
+        {
+          className: 'Channel',
+          entries: [channel], // We could specify multiple entries here, but in this case we only need one
+        },
+      ]
+    )
+    // We parse the input into CreateEntity and AddSchemaSupportToEntity operations
+    const operations = await parser.getEntityBatchOperations()
+    return await this.sendContentDirectoryTransaction(operations)
+  }
+
+  public async createVideoEntity(video: VideoEntity): Promise<void> {
+    // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
+    const parser = InputParser.createWithKnownSchemas(
+      this.api,
+      // The second argument is an array of entity batches, following standard entity batch syntax ({ className, entries }):
+      [
+        {
+          className: 'Video',
+          entries: [video], // We could specify multiple entries here, but in this case we only need one
+        },
+      ]
+    )
+    // We parse the input into CreateEntity and AddSchemaSupportToEntity operations
+    const operations = await parser.getEntityBatchOperations()
+    return await this.sendContentDirectoryTransaction(operations)
+  }
+
+  public async updateChannelEntity(
+    channelUpdateInput: Record<string, any>,
+    uniquePropValue: Record<string, any>
+  ): Promise<void> {
+    // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
+    const parser = InputParser.createWithKnownSchemas(this.api)
+
+    // We can reuse InputParser's `findEntityIdByUniqueQuery` method to find entityId of the channel we
+    // created in ./createChannel.ts example (normally we would probably use some other way to do it, ie.: query node)
+    const CHANNEL_ID = await parser.findEntityIdByUniqueQuery(uniquePropValue, 'Channel') // Use getEntityUpdateOperations to parse the update input
+    const updateOperations = await parser.getEntityUpdateOperations(
+      channelUpdateInput,
+      'Channel', // Class name
+      CHANNEL_ID // Id of the entity we want to update
+    )
+    return await this.sendContentDirectoryTransaction(updateOperations)
+  }
+
+  public async initializeContentDirectory(leadKeyPair: KeyringPair) {
+    await initializeContentDir(this.api, leadKeyPair)
+  }
+}
+
+export class QueryNodeApi extends Api {
+  private readonly queryNodeProvider: ApolloClient<NormalizedCacheObject>
+
+  public static async new(
+    provider: WsProvider,
+    queryNodeProvider: ApolloClient<NormalizedCacheObject>,
+    treasuryAccountUri: string,
+    sudoAccountUri: string
+  ): Promise<QueryNodeApi> {
+    let connectAttempts = 0
+    while (true) {
+      connectAttempts++
+      debug(`Connecting to chain, attempt ${connectAttempts}..`)
+      try {
+        const api = await ApiPromise.create({ provider, types })
+
+        // Wait for api to be connected and ready
+        await api.isReady
+
+        // If a node was just started up it might take a few seconds to start producing blocks
+        // Give it a few seconds to be ready.
+        await Utils.wait(5000)
+
+        return new QueryNodeApi(api, queryNodeProvider, treasuryAccountUri, sudoAccountUri)
+      } catch (err) {
+        if (connectAttempts === 3) {
+          throw new Error('Unable to connect to chain')
+        }
+      }
+      await Utils.wait(5000)
+    }
+  }
+
+  constructor(
+    api: ApiPromise,
+    queryNodeProvider: ApolloClient<NormalizedCacheObject>,
+    treasuryAccountUri: string,
+    sudoAccountUri: string
+  ) {
+    super(api, treasuryAccountUri, sudoAccountUri)
+    this.queryNodeProvider = queryNodeProvider
+  }
+
+  public async getChannelbyTitle(title: string): Promise<ApolloQueryResult<any>> {
+    const GET_CHANNEL_BY_TITLE = gql`
+      query($title: String!) {
+        channels(where: { title_eq: $title }) {
+          title
+          description
+          coverPhotoUrl
+          avatarPhotoUrl
+          isPublic
+          isCurated
+          languageId
+        }
+      }
+    `
+
+    return await this.queryNodeProvider.query({ query: GET_CHANNEL_BY_TITLE, variables: { title } })
+  }
 }

+ 65 - 0
tests/network-tests/src/fixtures/contentDirectoryModule.ts

@@ -0,0 +1,65 @@
+import { QueryNodeApi } from '../Api'
+import BN from 'bn.js'
+import { assert } from 'chai'
+import { Seat } from '@joystream/types/council'
+import { v4 as uuid } from 'uuid'
+import { Utils } from '../utils'
+import { Fixture } from '../Fixture'
+import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
+import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
+
+export class CreateChannelFixture implements Fixture {
+  private api: QueryNodeApi
+  public channelEntity: ChannelEntity
+
+  public constructor(api: QueryNodeApi, channelEntity: ChannelEntity) {
+    this.api = api
+    this.channelEntity = channelEntity
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    await this.api.createChannelEntity(this.channelEntity)
+
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class CreateVideoFixture implements Fixture {
+  private api: QueryNodeApi
+  private videoEntity: VideoEntity
+
+  public constructor(api: QueryNodeApi, videoEntity: VideoEntity) {
+    this.api = api
+    this.videoEntity = videoEntity
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    await this.api.createVideoEntity(this.videoEntity)
+
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class UpdateChannelFixture implements Fixture {
+  private api: QueryNodeApi
+  private channelUpdateInput: Record<string, any>
+  private uniquePropValue: Record<string, any>
+
+  public constructor(api: QueryNodeApi, channelUpdateInput: Record<string, any>, uniquePropValue: Record<string, any>) {
+    this.api = api
+    this.channelUpdateInput = channelUpdateInput
+    this.uniquePropValue = uniquePropValue
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    await this.api.updateChannelEntity(this.channelUpdateInput, this.uniquePropValue)
+
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}

+ 7 - 0
tests/network-tests/src/flows/contentDirectory/contentDirectoryInitialization.ts

@@ -0,0 +1,7 @@
+import { Api, WorkingGroups } from '../../Api'
+import { assert } from 'chai'
+import { KeyringPair } from '@polkadot/keyring/types'
+
+export default async function initializeContentDirectory(api: Api, leadKeyPair: KeyringPair) {
+  await api.initializeContentDirectory(leadKeyPair)
+}

+ 43 - 0
tests/network-tests/src/flows/contentDirectory/creatingChannel.ts

@@ -0,0 +1,43 @@
+import { QueryNodeApi } from '../../Api'
+import { Utils } from '../../utils'
+import { CreateChannelFixture } from '../../fixtures/contentDirectoryModule'
+import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
+import { assert } from 'chai'
+import { KeyringPair } from '@polkadot/keyring/types'
+
+export function createSimpleChannelFixture(api: QueryNodeApi): CreateChannelFixture {
+  const channelEntity: ChannelEntity = {
+    title: 'Example channel',
+    description: 'This is an example channel',
+    // We can use "existing" syntax to reference either an on-chain entity or other entity that's part of the same batch.
+    // Here we reference language that we assume was added by initialization script (initialize:dev), as it is part of
+    // input/entityBatches/LanguageBatch.json
+    language: { existing: { code: 'EN' } },
+    coverPhotoUrl: '',
+    avatarPhotoURL: '',
+    isPublic: true,
+  }
+  return new CreateChannelFixture(api, channelEntity)
+}
+
+export default async function channelCreation(api: QueryNodeApi) {
+  const createChannelHappyCaseFixture = createSimpleChannelFixture(api)
+
+  await createChannelHappyCaseFixture.runner(false)
+
+  // Temporary solution (wait 2 minutes)
+  await Utils.wait(120000)
+
+  // Ensure newly created channel was parsed by query node
+  const result = await api.getChannelbyTitle(createChannelHappyCaseFixture.channelEntity.title)
+  const queriedChannel = result.data.channels[0]
+
+  assert(queriedChannel.title === createChannelHappyCaseFixture.channelEntity.title, 'Should be equal')
+  assert(queriedChannel.description === createChannelHappyCaseFixture.channelEntity.description, 'Should be equal')
+  assert(queriedChannel.coverPhotoUrl === createChannelHappyCaseFixture.channelEntity.coverPhotoUrl, 'Should be equal')
+  assert(
+    queriedChannel.avatarPhotoUrl === createChannelHappyCaseFixture.channelEntity.avatarPhotoURL,
+    'Should be equal'
+  )
+  assert(queriedChannel.isPublic === createChannelHappyCaseFixture.channelEntity.isPublic, 'Should be equal')
+}

+ 49 - 0
tests/network-tests/src/flows/contentDirectory/creatingVideo.ts

@@ -0,0 +1,49 @@
+import { QueryNodeApi, WorkingGroups } from '../../Api'
+import { CreateVideoFixture } from '../../fixtures/contentDirectoryModule'
+import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
+import { assert } from 'chai'
+
+export function createVideoReferencingChannelFixture(api: QueryNodeApi): CreateVideoFixture {
+  const videoEntity: VideoEntity = {
+    title: 'Example video',
+    description: 'This is an example video',
+    // We reference existing language and category by their unique properties with "existing" syntax
+    // (those referenced here are part of inputs/entityBatches)
+    language: { existing: { code: 'EN' } },
+    category: { existing: { name: 'Education' } },
+    // We use the same "existing" syntax to reference a channel by unique property (title)
+    // In this case it's a channel that we created in createChannel example
+    channel: { existing: { title: 'Example channel' } },
+    media: {
+      // We use "new" syntax to sygnalize we want to create a new VideoMedia entity that will be related to this Video entity
+      new: {
+        // We use "exisiting" enconding from inputs/entityBatches/VideoMediaEncodingBatch.json
+        encoding: { existing: { name: 'H.263_MP4' } },
+        pixelHeight: 600,
+        pixelWidth: 800,
+        // We create nested VideoMedia->MediaLocation->HttpMediaLocation relations using the "new" syntax
+        location: { new: { httpMediaLocation: { new: { url: 'https://testnet.joystream.org/' } } } },
+      },
+    },
+    // Here we use combined "new" and "existing" syntaxes to create Video->License->KnownLicense relations
+    license: {
+      new: {
+        knownLicense: {
+          // This license can be found in inputs/entityBatches/KnownLicenseBatch.json
+          existing: { code: 'CC_BY' },
+        },
+      },
+    },
+    duration: 3600,
+    thumbnailURL: '',
+    isExplicit: false,
+    isPublic: true,
+  }
+  return new CreateVideoFixture(api, videoEntity)
+}
+
+export default async function createVideo(api: QueryNodeApi) {
+  const createVideoHappyCaseFixture = createVideoReferencingChannelFixture(api)
+
+  await createVideoHappyCaseFixture.runner(false)
+}

+ 21 - 0
tests/network-tests/src/flows/contentDirectory/updatingChannel.ts

@@ -0,0 +1,21 @@
+import { QueryNodeApi, WorkingGroups } from '../../Api'
+import { UpdateChannelFixture } from '../../fixtures/contentDirectoryModule'
+import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
+import { assert } from 'chai'
+
+export function createUpdateChannelTitleFixture(api: QueryNodeApi): UpdateChannelFixture {
+  // Create partial channel entity, only containing the fields we wish to update
+  const channelUpdateInput: Partial<ChannelEntity> = {
+    title: 'Updated channel title',
+  }
+
+  const uniquePropVal: Record<string, any> = { title: 'Example channel' }
+
+  return new UpdateChannelFixture(api, channelUpdateInput, uniquePropVal)
+}
+
+export default async function updateChannel(api: QueryNodeApi) {
+  const createVideoHappyCaseFixture = createUpdateChannelTitleFixture(api)
+
+  await createVideoHappyCaseFixture.runner(false)
+}

+ 10 - 4
tests/network-tests/src/flows/workingGroup/leaderSetup.ts

@@ -3,13 +3,17 @@ import BN from 'bn.js'
 import { PaidTermId } from '@joystream/types/members'
 import { SudoHireLeadFixture } from '../../fixtures/sudoHireLead'
 import { assert } from 'chai'
+import { KeyringPair } from '@polkadot/keyring/types'
 
 // Worker application happy case scenario
-export default async function leaderSetup(api: Api, env: NodeJS.ProcessEnv, group: WorkingGroups) {
+export default async function leaderSetup(
+  api: Api,
+  env: NodeJS.ProcessEnv,
+  group: WorkingGroups
+): Promise<KeyringPair> {
   const lead = await api.getGroupLead(group)
-  if (lead) {
-    return
-  }
+
+  assert(!lead, `Lead is already set`)
 
   const leadKeyPair = api.createKeyPairs(1)[0]
   const paidTerms: PaidTermId = api.createPaidTermId(new BN(+env.MEMBERSHIP_PAID_TERMS!))
@@ -37,4 +41,6 @@ export default async function leaderSetup(api: Api, env: NodeJS.ProcessEnv, grou
   const hiredLead = await api.getGroupLead(group)
   assert(hiredLead, `${group} group Lead was not hired!`)
   assert(hiredLead!.role_account_id.eq(leadKeyPair.address))
+
+  return leadKeyPair
 }

+ 29 - 3
tests/network-tests/src/scenarios/content-directory.ts

@@ -1,7 +1,12 @@
 import { WsProvider } from '@polkadot/api'
-import { Api, WorkingGroups } from '../Api'
+import { Api, QueryNodeApi, WorkingGroups } from '../Api'
 import { config } from 'dotenv'
 import leaderSetup from '../flows/workingGroup/leaderSetup'
+import initializeContentDirectory from '../flows/contentDirectory/contentDirectoryInitialization'
+import createChannel from '../flows/contentDirectory/creatingChannel'
+import createVideo from '../flows/contentDirectory/creatingVideo'
+import updateChannel from '../flows/contentDirectory/updatingChannel'
+import { ApolloClient, InMemoryCache } from '@apollo/client'
 
 const scenario = async () => {
   // Load env variables
@@ -11,13 +16,34 @@ const scenario = async () => {
   // Connect api to the chain
   const nodeUrl: string = env.NODE_URL || 'ws://127.0.0.1:9944'
   const provider = new WsProvider(nodeUrl)
-  const api: Api = await Api.create(provider, env.TREASURY_ACCOUNT_URI || '//Alice', env.SUDO_ACCOUNT_URI || '//Alice')
 
-  await leaderSetup(api, env, WorkingGroups.ContentDirectoryWorkingGroup)
+  const queryNodeUrl: string = env.QUERY_NODE_URL || 'http://127.0.0.1:8080/graphql'
+
+  const queryNodeProvider = new ApolloClient({
+    uri: queryNodeUrl,
+    cache: new InMemoryCache(),
+  })
+
+  const api: QueryNodeApi = await QueryNodeApi.new(
+    provider,
+    queryNodeProvider,
+    env.TREASURY_ACCOUNT_URI || '//Alice',
+    env.SUDO_ACCOUNT_URI || '//Alice'
+  )
+
+  const leadKeyPair = await leaderSetup(api, env, WorkingGroups.ContentDirectoryWorkingGroup)
 
   // Some flows that use the curator lead to perform some tests...
   //
 
+  await initializeContentDirectory(api, leadKeyPair)
+
+  await createChannel(api)
+
+  await createVideo(api)
+
+  await updateChannel(api)
+
   // Note: disconnecting and then reconnecting to the chain in the same process
   // doesn't seem to work!
   api.close()

+ 1 - 1
utils/api-scripts/src/status.ts

@@ -12,7 +12,7 @@ async function main() {
 
   // Create the API and wait until ready
   let api: ApiPromise
-  let retry = 3
+  let retry = 6
   while (true) {
     try {
       api = await ApiPromise.create({ provider, types })

+ 78 - 8
yarn.lock

@@ -23,6 +23,25 @@
     call-me-maybe "^1.0.1"
     js-yaml "^3.13.1"
 
+"@apollo/client@^3.2.5":
+  version "3.2.5"
+  resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.2.5.tgz#24e0a6faa1d231ab44807af237c6227410c75c4d"
+  integrity sha512-zpruxnFMz6K94gs2pqc3sidzFDbQpKT5D6P/J/I9s8ekHZ5eczgnRp6pqXC86Bh7+44j/btpmOT0kwiboyqTnA==
+  dependencies:
+    "@graphql-typed-document-node/core" "^3.0.0"
+    "@types/zen-observable" "^0.8.0"
+    "@wry/context" "^0.5.2"
+    "@wry/equality" "^0.2.0"
+    fast-json-stable-stringify "^2.0.0"
+    graphql-tag "^2.11.0"
+    hoist-non-react-statics "^3.3.2"
+    optimism "^0.13.0"
+    prop-types "^15.7.2"
+    symbol-observable "^2.0.0"
+    ts-invariant "^0.4.4"
+    tslib "^1.10.0"
+    zen-observable "^0.8.14"
+
 "@apollo/protobufjs@^1.0.3":
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.0.5.tgz#a78b726147efc0795e74c8cb8a11aafc6e02f773"
@@ -1396,7 +1415,7 @@
     typeorm-model-generator "^0.4.2"
     warthog "https://github.com/metmirr/warthog/releases/download/v2.20.0/warthog-v2.20.0.tgz"
 
-"@dzlzv/hydra-indexer-lib@0.0.19-legacy.1.26.1", "@dzlzv/hydra-indexer-lib@^0.0.19-legacy.1.26.1":
+"@dzlzv/hydra-indexer-lib@^0.0.19-legacy.1.26.1":
   version "0.0.19-legacy.1.26.1"
   resolved "https://registry.yarnpkg.com/@dzlzv/hydra-indexer-lib/-/hydra-indexer-lib-0.0.19-legacy.1.26.1.tgz#346b564845b2014f7a4d9b3976c03e30da8dd309"
   integrity sha512-4pwaSDRIo/1MqxjfSotjv91fkIj/bfZcZx5nqjB9gRT85X28b3WqkqTFrzlGsGGbvUFWAx4WIeQKnY1yrpX89Q==
@@ -1683,6 +1702,11 @@
   dependencies:
     prop-types "^15.7.2"
 
+"@graphql-typed-document-node/core@^3.0.0":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.0.tgz#0eee6373e11418bfe0b5638f654df7a4ca6a3950"
+  integrity sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg==
+
 "@hapi/address@2.x.x":
   version "2.1.4"
   resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@@ -3376,7 +3400,7 @@
     is-ipfs "^0.6.0"
     recursive-fs "^1.1.2"
 
-"@polkadot/api-contract@1.26.1", "@polkadot/api-contract@^1.26.1":
+"@polkadot/api-contract@^1.26.1":
   version "1.26.1"
   resolved "https://registry.yarnpkg.com/@polkadot/api-contract/-/api-contract-1.26.1.tgz#a8b52ef469ab8bbddb83191f8d451e31ffd76142"
   integrity sha512-zLGA/MHUJf12vanUEUBBRqpHVAONHWztoHS0JTIWUUS2+3GEXk6hGw+7PPtBDfDsLj0LgU/Qna1bLalC/zyl5w==
@@ -5458,6 +5482,11 @@
   resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.37.tgz#7a52854ac602ba0dc969bebc960559f7464a1686"
   integrity sha512-cDqR/ez4+iAVQYOwadXjKX4Dq1frtnDGV2GNVKj3aUVKVCKRvsr8esFk66j+LgeeJGmrMcBkkfCf3zk13MjV7A==
 
+"@types/zen-observable@^0.8.0":
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.1.tgz#5668c0bce55a91f2b9566b1d8a4c0a8dbbc79764"
+  integrity sha512-wmk0xQI6Yy7Fs/il4EpOcflG4uonUpYGqvZARESLc2oy4u69fkatFLbJOeW4Q6awO15P4rduAe6xkwHevpXcUQ==
+
 "@typescript-eslint/eslint-plugin@3.8.0":
   version "3.8.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.8.0.tgz#f82947bcdd9a4e42be7ad80dfd61f1dc411dd1df"
@@ -5937,6 +5966,13 @@
     "@webassemblyjs/wast-parser" "1.9.0"
     "@xtuc/long" "4.2.2"
 
+"@wry/context@^0.5.2":
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.5.2.tgz#f2a5d5ab9227343aa74c81e06533c1ef84598ec7"
+  integrity sha512-B/JLuRZ/vbEKHRUiGj6xiMojST1kHhu4WcreLfNN7q9DqQFrb97cWgf/kiYsPSUCAMVN0HzfFc8XjJdzgZzfjw==
+  dependencies:
+    tslib "^1.9.3"
+
 "@wry/equality@^0.1.2":
   version "0.1.11"
   resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.11.tgz#35cb156e4a96695aa81a9ecc4d03787bc17f1790"
@@ -5944,6 +5980,13 @@
   dependencies:
     tslib "^1.9.3"
 
+"@wry/equality@^0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.2.0.tgz#a312d1b6a682d0909904c2bcd355b02303104fb7"
+  integrity sha512-Y4d+WH6hs+KZJUC8YKLYGarjGekBrhslDbf/R20oV+AakHPINSitHfDRQz3EGcEWc1luXYNUvMhawWtZVWNGvQ==
+  dependencies:
+    tslib "^1.9.3"
+
 "@xtuc/ieee754@^1.2.0":
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -7836,11 +7879,21 @@ bluebird@^3.1.1, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 
-bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.8, bn.js@^4.4.0, bn.js@^5.1.1, bn.js@^5.1.2, bn.js@^5.1.3:
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.8, bn.js@^4.4.0:
+  version "4.11.9"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
+  integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
+
+bn.js@^5.1.1, bn.js@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0"
   integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==
 
+bn.js@^5.1.3:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
+  integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
+
 body-parser@1.19.0, body-parser@^1.18.3, body-parser@^1.19.0:
   version "1.19.0"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@@ -14311,7 +14364,7 @@ graphql-subscriptions@^1.0.0, graphql-subscriptions@^1.1.0:
   dependencies:
     iterall "^1.2.1"
 
-graphql-tag@^2.9.2:
+graphql-tag@^2.11.0, graphql-tag@^2.9.2:
   version "2.11.0"
   resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.11.0.tgz#1deb53a01c46a7eb401d6cb59dec86fa1cccbffd"
   integrity sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA==
@@ -14663,7 +14716,7 @@ hogan.js@^3.0.2:
     mkdirp "0.3.0"
     nopt "1.0.10"
 
-hoist-non-react-statics@^3.0.0:
+hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
   integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -21577,6 +21630,13 @@ opn@^5.5.0:
   dependencies:
     is-wsl "^1.1.0"
 
+optimism@^0.13.0:
+  version "0.13.0"
+  resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.13.0.tgz#c08904e1439a0eb9e7f86183dafa06cc715ff351"
+  integrity sha512-6JAh3dH+YUE4QUdsgUw8nUQyrNeBKfAEKOHMlLkQ168KhIYFIxzPsHakWrRXDnTO+x61RJrS3/2uEt6W0xlocA==
+  dependencies:
+    "@wry/context" "^0.5.2"
+
 optimist@^0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@@ -27023,6 +27083,11 @@ symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0:
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
   integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
 
+symbol-observable@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-2.0.3.tgz#5b521d3d07a43c351055fa43b8355b62d33fd16a"
+  integrity sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==
+
 symbol-tree@^3.2.2, symbol-tree@^3.2.4:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@@ -27702,7 +27767,7 @@ ts-dedent@^1.1.0:
   resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-1.1.1.tgz#68fad040d7dbd53a90f545b450702340e17d18f3"
   integrity sha512-UGTRZu1evMw4uTPyYF66/KFd22XiU+jMaIuHrkIHQ2GivAXVlLV0v/vHrpOuTRf9BmpNHi/SO7Vd0rLu0y57jg==
 
-ts-invariant@^0.4.0:
+ts-invariant@^0.4.0, ts-invariant@^0.4.4:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
   integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==
@@ -28098,7 +28163,12 @@ typescript-formatter@^7.2.2:
     commandpost "^1.0.0"
     editorconfig "^0.15.0"
 
-typescript@3.5.2, typescript@^3.0.3, typescript@^3.7.2, typescript@^3.7.5, typescript@^3.8.3, typescript@^3.9.5, typescript@^3.9.6, typescript@^3.9.7:
+typescript@3.5.2:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.2.tgz#a09e1dc69bc9551cadf17dba10ee42cf55e5d56c"
+  integrity sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==
+
+typescript@^3.0.3, typescript@^3.7.2, typescript@^3.7.5, typescript@^3.8.3, typescript@^3.9.5, typescript@^3.9.6, typescript@^3.9.7:
   version "3.9.7"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
   integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
@@ -30136,7 +30206,7 @@ zen-observable-ts@^0.8.21:
     tslib "^1.9.3"
     zen-observable "^0.8.0"
 
-zen-observable@^0.8.0:
+zen-observable@^0.8.0, zen-observable@^0.8.14:
   version "0.8.15"
   resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
   integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==