From 492b9f533f983d8dd7ce37a3c54c64c82ee144af Mon Sep 17 00:00:00 2001 From: Lezek123 Date: Thu, 12 Jan 2023 22:19:01 +0100 Subject: [PATCH 01/84] CLI: generateMessage util command --- cli/package.json | 3 +++ cli/src/commands/util/generateMessage.ts | 29 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 cli/src/commands/util/generateMessage.ts diff --git a/cli/package.json b/cli/package.json index ae2830f6f3..2601a4bcc9 100644 --- a/cli/package.json +++ b/cli/package.json @@ -141,6 +141,9 @@ }, "sign-offline": { "description": "Sign unsigned transactions, created with 'advanced-transactions', offline" + }, + "util": { + "description": "General Joystream utilities" } } }, diff --git a/cli/src/commands/util/generateMessage.ts b/cli/src/commands/util/generateMessage.ts new file mode 100644 index 0000000000..174beed1ad --- /dev/null +++ b/cli/src/commands/util/generateMessage.ts @@ -0,0 +1,29 @@ +import { flags } from '@oclif/command' +import * as all from '@joystream/metadata-protobuf' +import DefaultCommandBase from '../../base/DefaultCommandBase' +import { AnyMetadataClass } from '@joystream/metadata-protobuf/types' +import { metadataToBytes } from '../../helpers/serialization' + +export default class GenerateMessageCommand extends DefaultCommandBase { + static description = 'Generate a protobuf message hex' + + static flags = { + name: flags.enum({ + options: Object.keys(all), + required: true, + description: 'Name of the message', + }), + json: flags.string({ + required: true, + description: 'JSON-encoded message input', + }), + } + + async run(): Promise { + const { name, json } = this.parse(GenerateMessageCommand).flags + const metaClass = all[name as keyof typeof all] as AnyMetadataClass + const bytes = metadataToBytes(metaClass, JSON.parse(json)) + + this.output(bytes.toHex()) + } +} From 3bfb54e5dc182ff764657c12a5b30f4651712a18 Mon Sep 17 00:00:00 2001 From: drillprop Date: Tue, 20 Dec 2022 12:44:39 +0100 Subject: [PATCH 02/84] add app messages to metaprotocol --- metadata-protobuf/proto/Metaprotocol.proto | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/metadata-protobuf/proto/Metaprotocol.proto b/metadata-protobuf/proto/Metaprotocol.proto index b79e964488..4854a421ff 100644 --- a/metadata-protobuf/proto/Metaprotocol.proto +++ b/metadata-protobuf/proto/Metaprotocol.proto @@ -113,6 +113,37 @@ message CreateVideoCategory { optional string parent_category_id = 3; } +message AppMetadata { + // Url where user can read more about the project or company for this app + optional string website_url = 2; + // Url to the app + optional string use_uri = 3; + optional string small_icon = 4; + optional string medium_icon = 5; + optional string big_icon = 6; + // Tagline for the app + optional string one_liner = 7; + optional string description = 8; + optional string terms_of_service = 9; + optional string auth_key = 10; + // List of platforms on which the app will be available, e.g. [mobile, web, native] + repeated string platforms = 11; + // E.g messaging, adult + optional string category = 12; + } + +message CreateApp { + required string name = 1; + optional AppMetadata app_metadata = 2; +} +message UpdateApp { + required string app_id = 1; + optional AppMetadata app_metadata = 2; +} +message DeleteApp { + required string app_id = 1; +} + message MemberRemarked { // member_remark extrinsic would emit event containing // any one of the following serialized messages @@ -123,6 +154,9 @@ message MemberRemarked { EditComment edit_comment = 4; DeleteComment delete_comment = 5; CreateVideoCategory create_video_category = 6; + CreateApp create_app = 7; + UpdateApp update_app = 8; + DeleteApp delete_app = 9; } } From 9a950c0f65e2cb636f5fece93f577602c768f635 Mon Sep 17 00:00:00 2001 From: drillprop Date: Tue, 20 Dec 2022 12:44:55 +0100 Subject: [PATCH 03/84] add App entity --- query-node/schemas/app.graphql | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 query-node/schemas/app.graphql diff --git a/query-node/schemas/app.graphql b/query-node/schemas/app.graphql new file mode 100644 index 0000000000..fbbe4e0763 --- /dev/null +++ b/query-node/schemas/app.graphql @@ -0,0 +1,21 @@ +type App @entity { + id: ID! + name: String! + + # Url where user can read more about the project or company for this app + websiteUrl: String + # Url to the app + useUrI: String + # what should I to do with icons? + smallIcon: String + mediumIcon: String + bigIcon: String + + oneLiner: String + description: String + termsOfService: String + + # List of platforms on which the app will be available, e.g. [mobile, web, native] + platforms: [String] + category: String +} From deadaeb8ca352571128672400d2abacae9f03e19 Mon Sep 17 00:00:00 2001 From: drillprop Date: Wed, 28 Dec 2022 15:47:15 +0100 Subject: [PATCH 04/84] create app related queries --- tests/network-tests/src/QueryNodeApi.ts | 15 ++ .../src/graphql/generated/queries.ts | 59 ++++++ .../src/graphql/generated/schema.ts | 200 ++++++++++++++++++ .../src/graphql/queries/app.graphql | 26 +++ 4 files changed, 300 insertions(+) create mode 100644 tests/network-tests/src/graphql/queries/app.graphql diff --git a/tests/network-tests/src/QueryNodeApi.ts b/tests/network-tests/src/QueryNodeApi.ts index 847d547711..cf503f74ff 100644 --- a/tests/network-tests/src/QueryNodeApi.ts +++ b/tests/network-tests/src/QueryNodeApi.ts @@ -424,6 +424,13 @@ import { GetVideoByIdQuery, GetVideoByIdQueryVariables, GetVideoById, + GetAppByIdQuery, + GetAppByIdQueryVariables, + GetAppById, + AppFieldsFragment, + GetAppsByNameQuery, + GetAppsByNameQueryVariables, + GetAppsByName, } from './graphql/generated/queries' import { Maybe } from './graphql/generated/schema' import { OperationDefinitionNode } from 'graphql' @@ -1465,4 +1472,12 @@ export class QueryNodeApi { GetDistributionFamiliesAdndBucketsQueryVariables >(GetDistributionFamiliesAdndBuckets, {}, 'distributionBucketFamilies') } + + public async getAppById(id: string): Promise { + return this.uniqueEntityQuery(GetAppById, { id }, 'appByUniqueInput') + } + + public async getAppsByName(name: string): Promise { + return this.multipleEntitiesQuery(GetAppsByName, { name }, 'apps') + } } diff --git a/tests/network-tests/src/graphql/generated/queries.ts b/tests/network-tests/src/graphql/generated/queries.ts index 41fee3a0dd..52f9da48f8 100644 --- a/tests/network-tests/src/graphql/generated/queries.ts +++ b/tests/network-tests/src/graphql/generated/queries.ts @@ -1,6 +1,33 @@ import * as Types from './schema' import gql from 'graphql-tag' +export type AppFieldsFragment = { + id: string + name: string + websiteUrl?: Types.Maybe + useUrI?: Types.Maybe + smallIcon?: Types.Maybe + mediumIcon?: Types.Maybe + bigIcon?: Types.Maybe + oneLiner?: Types.Maybe + description?: Types.Maybe + termsOfService?: Types.Maybe + platforms?: Types.Maybe> + category?: Types.Maybe +} + +export type GetAppByIdQueryVariables = Types.Exact<{ + id: Types.Scalars['ID'] +}> + +export type GetAppByIdQuery = { appByUniqueInput?: Types.Maybe } + +export type GetAppsByNameQueryVariables = Types.Exact<{ + name: Types.Scalars['String'] +}> + +export type GetAppsByNameQuery = { apps: Array } + type DataObjectTypeFields_DataObjectTypeChannelAvatar_Fragment = { __typename: 'DataObjectTypeChannelAvatar' channel?: Types.Maybe<{ id: string }> @@ -2491,6 +2518,22 @@ export type GetBudgetSpendingEventsByEventIdsQueryVariables = Types.Exact<{ export type GetBudgetSpendingEventsByEventIdsQuery = { budgetSpendingEvents: Array } +export const AppFields = gql` + fragment AppFields on App { + id + name + websiteUrl + useUrI + smallIcon + mediumIcon + bigIcon + oneLiner + description + termsOfService + platforms + category + } +` export const DataObjectTypeFields = gql` fragment DataObjectTypeFields on DataObjectType { __typename @@ -4839,6 +4882,22 @@ export const BudgetSpendingEventFields = gql` rationale } ` +export const GetAppById = gql` + query getAppById($id: ID!) { + appByUniqueInput(where: { id: $id }) { + ...AppFields + } + } + ${AppFields} +` +export const GetAppsByName = gql` + query getAppsByName($name: String!) { + apps(where: { name_eq: $name }) { + ...AppFields + } + } + ${AppFields} +` export const GetChannelById = gql` query getChannelById($id: ID!) { channelByUniqueInput(where: { id: $id }) { diff --git a/tests/network-tests/src/graphql/generated/schema.ts b/tests/network-tests/src/graphql/generated/schema.ts index 9036e64f70..fea45fd43a 100644 --- a/tests/network-tests/src/graphql/generated/schema.ts +++ b/tests/network-tests/src/graphql/generated/schema.ts @@ -141,6 +141,183 @@ export type AnnouncingPeriodStartedEventWhereUniqueInput = { id: Scalars['ID'] } +export type App = BaseGraphQlObject & { + id: Scalars['ID'] + createdAt: Scalars['DateTime'] + createdById: Scalars['ID'] + updatedAt?: Maybe + updatedById?: Maybe + deletedAt?: Maybe + deletedById?: Maybe + version: Scalars['Int'] + name: Scalars['String'] + websiteUrl?: Maybe + useUrI?: Maybe + smallIcon?: Maybe + mediumIcon?: Maybe + bigIcon?: Maybe + oneLiner?: Maybe + description?: Maybe + termsOfService?: Maybe + platforms?: Maybe> + category?: Maybe +} + +export type AppConnection = { + totalCount: Scalars['Int'] + edges: Array + pageInfo: PageInfo +} + +export type AppCreateInput = { + name: Scalars['String'] + websiteUrl?: Maybe + useUrI?: Maybe + smallIcon?: Maybe + mediumIcon?: Maybe + bigIcon?: Maybe + oneLiner?: Maybe + description?: Maybe + termsOfService?: Maybe + platforms?: Maybe> + category?: Maybe +} + +export type AppEdge = { + node: App + cursor: Scalars['String'] +} + +export enum AppOrderByInput { + CreatedAtAsc = 'createdAt_ASC', + CreatedAtDesc = 'createdAt_DESC', + UpdatedAtAsc = 'updatedAt_ASC', + UpdatedAtDesc = 'updatedAt_DESC', + DeletedAtAsc = 'deletedAt_ASC', + DeletedAtDesc = 'deletedAt_DESC', + NameAsc = 'name_ASC', + NameDesc = 'name_DESC', + WebsiteUrlAsc = 'websiteUrl_ASC', + WebsiteUrlDesc = 'websiteUrl_DESC', + UseUrIAsc = 'useUrI_ASC', + UseUrIDesc = 'useUrI_DESC', + SmallIconAsc = 'smallIcon_ASC', + SmallIconDesc = 'smallIcon_DESC', + MediumIconAsc = 'mediumIcon_ASC', + MediumIconDesc = 'mediumIcon_DESC', + BigIconAsc = 'bigIcon_ASC', + BigIconDesc = 'bigIcon_DESC', + OneLinerAsc = 'oneLiner_ASC', + OneLinerDesc = 'oneLiner_DESC', + DescriptionAsc = 'description_ASC', + DescriptionDesc = 'description_DESC', + TermsOfServiceAsc = 'termsOfService_ASC', + TermsOfServiceDesc = 'termsOfService_DESC', + CategoryAsc = 'category_ASC', + CategoryDesc = 'category_DESC', +} + +export type AppUpdateInput = { + name?: Maybe + websiteUrl?: Maybe + useUrI?: Maybe + smallIcon?: Maybe + mediumIcon?: Maybe + bigIcon?: Maybe + oneLiner?: Maybe + description?: Maybe + termsOfService?: Maybe + platforms?: Maybe> + category?: Maybe +} + +export type AppWhereInput = { + id_eq?: Maybe + id_in?: Maybe> + createdAt_eq?: Maybe + createdAt_lt?: Maybe + createdAt_lte?: Maybe + createdAt_gt?: Maybe + createdAt_gte?: Maybe + createdById_eq?: Maybe + createdById_in?: Maybe> + updatedAt_eq?: Maybe + updatedAt_lt?: Maybe + updatedAt_lte?: Maybe + updatedAt_gt?: Maybe + updatedAt_gte?: Maybe + updatedById_eq?: Maybe + updatedById_in?: Maybe> + deletedAt_all?: Maybe + deletedAt_eq?: Maybe + deletedAt_lt?: Maybe + deletedAt_lte?: Maybe + deletedAt_gt?: Maybe + deletedAt_gte?: Maybe + deletedById_eq?: Maybe + deletedById_in?: Maybe> + name_eq?: Maybe + name_contains?: Maybe + name_startsWith?: Maybe + name_endsWith?: Maybe + name_in?: Maybe> + websiteUrl_eq?: Maybe + websiteUrl_contains?: Maybe + websiteUrl_startsWith?: Maybe + websiteUrl_endsWith?: Maybe + websiteUrl_in?: Maybe> + useUrI_eq?: Maybe + useUrI_contains?: Maybe + useUrI_startsWith?: Maybe + useUrI_endsWith?: Maybe + useUrI_in?: Maybe> + smallIcon_eq?: Maybe + smallIcon_contains?: Maybe + smallIcon_startsWith?: Maybe + smallIcon_endsWith?: Maybe + smallIcon_in?: Maybe> + mediumIcon_eq?: Maybe + mediumIcon_contains?: Maybe + mediumIcon_startsWith?: Maybe + mediumIcon_endsWith?: Maybe + mediumIcon_in?: Maybe> + bigIcon_eq?: Maybe + bigIcon_contains?: Maybe + bigIcon_startsWith?: Maybe + bigIcon_endsWith?: Maybe + bigIcon_in?: Maybe> + oneLiner_eq?: Maybe + oneLiner_contains?: Maybe + oneLiner_startsWith?: Maybe + oneLiner_endsWith?: Maybe + oneLiner_in?: Maybe> + description_eq?: Maybe + description_contains?: Maybe + description_startsWith?: Maybe + description_endsWith?: Maybe + description_in?: Maybe> + termsOfService_eq?: Maybe + termsOfService_contains?: Maybe + termsOfService_startsWith?: Maybe + termsOfService_endsWith?: Maybe + termsOfService_in?: Maybe> + platforms_containsAll?: Maybe> + platforms_containsNone?: Maybe> + platforms_containsAny?: Maybe> + category_eq?: Maybe + category_contains?: Maybe + category_startsWith?: Maybe + category_endsWith?: Maybe + category_in?: Maybe> + AND?: Maybe> + OR?: Maybe> + NOT?: Maybe> +} + +export type AppWhereUniqueInput = { + id: Scalars['ID'] +} + export type ApplicationFormQuestion = BaseGraphQlObject & { id: Scalars['ID'] createdAt: Scalars['DateTime'] @@ -20370,6 +20547,9 @@ export type Query = { announcingPeriodStartedEvents: Array announcingPeriodStartedEventByUniqueInput?: Maybe announcingPeriodStartedEventsConnection: AnnouncingPeriodStartedEventConnection + apps: Array + appByUniqueInput?: Maybe + appsConnection: AppConnection applicationFormQuestionAnswers: Array applicationFormQuestionAnswerByUniqueInput?: Maybe applicationFormQuestionAnswersConnection: ApplicationFormQuestionAnswerConnection @@ -21009,6 +21189,26 @@ export type QueryAnnouncingPeriodStartedEventsConnectionArgs = { orderBy?: Maybe> } +export type QueryAppsArgs = { + offset?: Maybe + limit?: Maybe + where?: Maybe + orderBy?: Maybe> +} + +export type QueryAppByUniqueInputArgs = { + where: AppWhereUniqueInput +} + +export type QueryAppsConnectionArgs = { + first?: Maybe + after?: Maybe + last?: Maybe + before?: Maybe + where?: Maybe + orderBy?: Maybe> +} + export type QueryApplicationFormQuestionAnswersArgs = { offset?: Maybe limit?: Maybe diff --git a/tests/network-tests/src/graphql/queries/app.graphql b/tests/network-tests/src/graphql/queries/app.graphql new file mode 100644 index 0000000000..6719db518f --- /dev/null +++ b/tests/network-tests/src/graphql/queries/app.graphql @@ -0,0 +1,26 @@ +fragment AppFields on App { + id + name + websiteUrl + useUrI + smallIcon + mediumIcon + bigIcon + oneLiner + description + termsOfService + platforms + category +} + +query getAppById($id: ID!) { + appByUniqueInput(where: { id: $id }) { + ...AppFields + } +} + +query getAppsByName($name: String!) { + apps(where: { name_eq: $name }) { + ...AppFields + } +} From b1e844edccb307037e652936265b6af3a16049b3 Mon Sep 17 00:00:00 2001 From: drillprop Date: Mon, 2 Jan 2023 15:30:57 +0100 Subject: [PATCH 05/84] update app entity, update app queries --- query-node/schemas/app.graphql | 4 ++- .../src/graphql/generated/queries.ts | 6 ++-- .../src/graphql/generated/schema.ts | 30 ++++++++++++------- .../src/graphql/queries/app.graphql | 3 +- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/query-node/schemas/app.graphql b/query-node/schemas/app.graphql index fbbe4e0763..5f7980fd5d 100644 --- a/query-node/schemas/app.graphql +++ b/query-node/schemas/app.graphql @@ -5,7 +5,7 @@ type App @entity { # Url where user can read more about the project or company for this app websiteUrl: String # Url to the app - useUrI: String + useUri: String # what should I to do with icons? smallIcon: String mediumIcon: String @@ -18,4 +18,6 @@ type App @entity { # List of platforms on which the app will be available, e.g. [mobile, web, native] platforms: [String] category: String + + authKey: String } diff --git a/tests/network-tests/src/graphql/generated/queries.ts b/tests/network-tests/src/graphql/generated/queries.ts index 52f9da48f8..d57f0605d0 100644 --- a/tests/network-tests/src/graphql/generated/queries.ts +++ b/tests/network-tests/src/graphql/generated/queries.ts @@ -5,13 +5,14 @@ export type AppFieldsFragment = { id: string name: string websiteUrl?: Types.Maybe - useUrI?: Types.Maybe + useUri?: Types.Maybe smallIcon?: Types.Maybe mediumIcon?: Types.Maybe bigIcon?: Types.Maybe oneLiner?: Types.Maybe description?: Types.Maybe termsOfService?: Types.Maybe + authKey?: Types.Maybe platforms?: Types.Maybe> category?: Types.Maybe } @@ -2523,13 +2524,14 @@ export const AppFields = gql` id name websiteUrl - useUrI + useUri smallIcon mediumIcon bigIcon oneLiner description termsOfService + authKey platforms category } diff --git a/tests/network-tests/src/graphql/generated/schema.ts b/tests/network-tests/src/graphql/generated/schema.ts index fea45fd43a..c8c7c203eb 100644 --- a/tests/network-tests/src/graphql/generated/schema.ts +++ b/tests/network-tests/src/graphql/generated/schema.ts @@ -152,7 +152,7 @@ export type App = BaseGraphQlObject & { version: Scalars['Int'] name: Scalars['String'] websiteUrl?: Maybe - useUrI?: Maybe + useUri?: Maybe smallIcon?: Maybe mediumIcon?: Maybe bigIcon?: Maybe @@ -161,6 +161,7 @@ export type App = BaseGraphQlObject & { termsOfService?: Maybe platforms?: Maybe> category?: Maybe + authKey?: Maybe } export type AppConnection = { @@ -172,7 +173,7 @@ export type AppConnection = { export type AppCreateInput = { name: Scalars['String'] websiteUrl?: Maybe - useUrI?: Maybe + useUri?: Maybe smallIcon?: Maybe mediumIcon?: Maybe bigIcon?: Maybe @@ -181,6 +182,7 @@ export type AppCreateInput = { termsOfService?: Maybe platforms?: Maybe> category?: Maybe + authKey?: Maybe } export type AppEdge = { @@ -199,8 +201,8 @@ export enum AppOrderByInput { NameDesc = 'name_DESC', WebsiteUrlAsc = 'websiteUrl_ASC', WebsiteUrlDesc = 'websiteUrl_DESC', - UseUrIAsc = 'useUrI_ASC', - UseUrIDesc = 'useUrI_DESC', + UseUriAsc = 'useUri_ASC', + UseUriDesc = 'useUri_DESC', SmallIconAsc = 'smallIcon_ASC', SmallIconDesc = 'smallIcon_DESC', MediumIconAsc = 'mediumIcon_ASC', @@ -215,12 +217,14 @@ export enum AppOrderByInput { TermsOfServiceDesc = 'termsOfService_DESC', CategoryAsc = 'category_ASC', CategoryDesc = 'category_DESC', + AuthKeyAsc = 'authKey_ASC', + AuthKeyDesc = 'authKey_DESC', } export type AppUpdateInput = { name?: Maybe websiteUrl?: Maybe - useUrI?: Maybe + useUri?: Maybe smallIcon?: Maybe mediumIcon?: Maybe bigIcon?: Maybe @@ -229,6 +233,7 @@ export type AppUpdateInput = { termsOfService?: Maybe platforms?: Maybe> category?: Maybe + authKey?: Maybe } export type AppWhereInput = { @@ -266,11 +271,11 @@ export type AppWhereInput = { websiteUrl_startsWith?: Maybe websiteUrl_endsWith?: Maybe websiteUrl_in?: Maybe> - useUrI_eq?: Maybe - useUrI_contains?: Maybe - useUrI_startsWith?: Maybe - useUrI_endsWith?: Maybe - useUrI_in?: Maybe> + useUri_eq?: Maybe + useUri_contains?: Maybe + useUri_startsWith?: Maybe + useUri_endsWith?: Maybe + useUri_in?: Maybe> smallIcon_eq?: Maybe smallIcon_contains?: Maybe smallIcon_startsWith?: Maybe @@ -309,6 +314,11 @@ export type AppWhereInput = { category_startsWith?: Maybe category_endsWith?: Maybe category_in?: Maybe> + authKey_eq?: Maybe + authKey_contains?: Maybe + authKey_startsWith?: Maybe + authKey_endsWith?: Maybe + authKey_in?: Maybe> AND?: Maybe> OR?: Maybe> NOT?: Maybe> diff --git a/tests/network-tests/src/graphql/queries/app.graphql b/tests/network-tests/src/graphql/queries/app.graphql index 6719db518f..046bff7a86 100644 --- a/tests/network-tests/src/graphql/queries/app.graphql +++ b/tests/network-tests/src/graphql/queries/app.graphql @@ -2,13 +2,14 @@ fragment AppFields on App { id name websiteUrl - useUrI + useUri smallIcon mediumIcon bigIcon oneLiner description termsOfService + authKey platforms category } From 658eac44281783fd1dae3ee96a86a823d0d92234 Mon Sep 17 00:00:00 2001 From: drillprop Date: Mon, 2 Jan 2023 15:31:24 +0100 Subject: [PATCH 06/84] add mappings --- query-node/mappings/src/content/app.ts | 123 +++++++++++++++++++++++++ query-node/mappings/src/membership.ts | 19 ++++ 2 files changed, 142 insertions(+) create mode 100644 query-node/mappings/src/content/app.ts diff --git a/query-node/mappings/src/content/app.ts b/query-node/mappings/src/content/app.ts new file mode 100644 index 0000000000..f259fee7e9 --- /dev/null +++ b/query-node/mappings/src/content/app.ts @@ -0,0 +1,123 @@ +import { DatabaseManager, SubstrateEvent } from '@joystream/hydra-common' +import { CreateApp, ICreateApp, IDeleteApp, IUpdateApp } from '@joystream/metadata-protobuf' +import { MemberId } from '@joystream/types/primitives' +import { App } from 'query-node/dist/model' +import { logger } from '../common' + +export async function processCreateAppMessage( + store: DatabaseManager, + event: SubstrateEvent, + memberId: MemberId, + message: ICreateApp +): Promise { + const { name, appMetadata } = message + const appId = await getAppId(store, event) + + const isAppExists = await store.get(App, { + where: { + name: name, + }, + }) + + if (isAppExists) { + logger.error('App already exists', { name }) + return + } + + const newApp = new App({ + name, + id: appId, + createdById: memberId.toString(), + websiteUrl: appMetadata?.websiteUrl || undefined, + useUri: appMetadata?.useUri || undefined, + smallIcon: appMetadata?.smallIcon || undefined, + mediumIcon: appMetadata?.mediumIcon || undefined, + bigIcon: appMetadata?.bigIcon || undefined, + oneLiner: appMetadata?.oneLiner || undefined, + description: appMetadata?.description || undefined, + termsOfService: appMetadata?.termsOfService || undefined, + platforms: appMetadata?.platforms || undefined, + category: appMetadata?.category || undefined, + authKey: appMetadata?.authKey || undefined, + }) + await store.save(newApp) + logger.info('App has been created', { name }) +} + +export async function processUpdateAppMessage( + store: DatabaseManager, + event: SubstrateEvent, + memberId: MemberId, + message: IUpdateApp +): Promise { + const { appId, appMetadata } = message + + const app = await getAppByIdAndMemberId(store, appId, memberId) + + if (!app) { + logger.error("App doesn't exists or doesn't belong to the member", { appId, memberId: memberId.toString() }) + return + } + + app.websiteUrl = appMetadata?.websiteUrl || app.websiteUrl + app.useUri = appMetadata?.useUri || app.useUri + app.smallIcon = appMetadata?.smallIcon || app.smallIcon + app.mediumIcon = appMetadata?.mediumIcon || app.mediumIcon + app.bigIcon = appMetadata?.bigIcon || app.bigIcon + app.oneLiner = appMetadata?.oneLiner || app.oneLiner + app.description = appMetadata?.description || app.description + app.termsOfService = appMetadata?.termsOfService || app.termsOfService + app.platforms = appMetadata?.platforms || app.platforms + app.category = appMetadata?.category || app.category + app.authKey = appMetadata?.authKey || app.authKey + + await store.save(app) + logger.info('App has been updated', { appId }) +} + +export async function processDeleteAppMessage( + store: DatabaseManager, + event: SubstrateEvent, + memberId: MemberId, + message: IDeleteApp +): Promise { + const { appId } = message + + const app = await getAppByIdAndMemberId(store, appId, memberId) + + if (!app) { + logger.error("App doesn't exists or doesn't belong to the member", { appId, memberId: memberId.toString() }) + return + } + + await store.remove(new App({ id: appId })) + logger.info('App has been removed', { appId }) +} + +async function getAppId(store: DatabaseManager, event: SubstrateEvent): Promise { + let appId = `${event.blockNumber}-${event.indexInBlock}` + let tries = 0 + + // make sure app id is unique + while (await store.get(App, { where: { id: appId } })) { + tries++ + appId = `${event.blockNumber}-${event.indexInBlock}-${tries}` + } + + return appId +} + +async function getAppByIdAndMemberId( + store: DatabaseManager, + appId: string, + memberId: MemberId +): Promise { + const app = await store.get(App, { + where: { + id: appId, + createdById: memberId.toString(), + }, + }) + + return app +} diff --git a/query-node/mappings/src/membership.ts b/query-node/mappings/src/membership.ts index 6c11f6fa3c..3d9e4d610e 100644 --- a/query-node/mappings/src/membership.ts +++ b/query-node/mappings/src/membership.ts @@ -70,6 +70,7 @@ import { createVideoCategory } from './content/videoCategory' import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types' import { membershipConfig } from './bootstrap-data' import { BN } from 'bn.js' +import { processCreateAppMessage, processDeleteAppMessage, processUpdateAppMessage } from './content/app' // FIXME: Should be emitted as part of MemberInvited event, but this requires a runtime upgrade async function initialInvitationBalance(store: DatabaseManager) { @@ -691,6 +692,24 @@ async function processMemberRemark( return { videoCategoryCreatedId: videoCategory.id } } + if (messageType === 'createApp') { + await processCreateAppMessage(store, event, memberId, decodedMessage.createApp!) + + return {} + } + + if (messageType === 'updateApp') { + await processUpdateAppMessage(store, event, memberId, decodedMessage.updateApp!) + + return {} + } + + if (messageType === 'deleteApp') { + await processDeleteAppMessage(store, event, memberId, decodedMessage.deleteApp!) + + return {} + } + // unknown message type return inconsistentState('Unsupported message type in member_remark action', messageType) } From 1c26f6fbe1618575b1388d2e602b9c1bbbab77ae Mon Sep 17 00:00:00 2001 From: drillprop Date: Mon, 2 Jan 2023 15:31:38 +0100 Subject: [PATCH 07/84] add tests --- tests/network-tests/src/Api.ts | 65 ++++++++++++++++++- .../src/flows/membership/createApp.ts | 41 ++++++++++++ .../src/flows/membership/deleteApp.ts | 45 +++++++++++++ .../src/flows/membership/updateApp.ts | 63 ++++++++++++++++++ tests/network-tests/src/scenarios/app.ts | 16 +++++ 5 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 tests/network-tests/src/flows/membership/createApp.ts create mode 100644 tests/network-tests/src/flows/membership/deleteApp.ts create mode 100644 tests/network-tests/src/flows/membership/updateApp.ts create mode 100644 tests/network-tests/src/scenarios/app.ts diff --git a/tests/network-tests/src/Api.ts b/tests/network-tests/src/Api.ts index 4db72335d8..280887bfcf 100644 --- a/tests/network-tests/src/Api.ts +++ b/tests/network-tests/src/Api.ts @@ -61,7 +61,14 @@ import { workingGroupNameByModuleName, } from './consts' -import { CreateVideoCategory, MemberRemarked } from '@joystream/metadata-protobuf' +import { + CreateApp, + CreateVideoCategory, + DeleteApp, + IAppMetadata, + MemberRemarked, + UpdateApp, +} from '@joystream/metadata-protobuf' import { PERBILL_ONE_PERCENT } from '../../../query-node/mappings/src/temporaryConstants' import { createType, JOYSTREAM_ADDRESS_PREFIX } from '@joystream/types' @@ -798,6 +805,62 @@ export class Api { return event.data[2] } + async createApp(memberId: u64, name: string, appMetadata?: IAppMetadata): Promise { + const memberAccount = await this.getMemberControllerAccount(memberId.toNumber()) + if (!memberAccount) { + throw new Error('invalid member id') + } + + const meta = new MemberRemarked({ + createApp: new CreateApp({ + name, + appMetadata, + }), + }) + + return this.sender.signAndSend( + this.api.tx.members.memberRemark(memberId, Utils.metadataToBytes(MemberRemarked, meta)), + memberAccount.toString() + ) + } + + async updateApp(memberId: u64, appId: string, appMetadata: IAppMetadata): Promise { + const memberAccount = await this.getMemberControllerAccount(memberId.toNumber()) + if (!memberAccount) { + throw new Error('invalid member id') + } + + const meta = new MemberRemarked({ + updateApp: new UpdateApp({ + appId, + appMetadata, + }), + }) + + return this.sender.signAndSend( + this.api.tx.members.memberRemark(memberId, Utils.metadataToBytes(MemberRemarked, meta)), + memberAccount.toString() + ) + } + + async deleteApp(memberId: u64, appId: string): Promise { + const memberAccount = await this.getMemberControllerAccount(memberId.toNumber()) + if (!memberAccount) { + throw new Error('invalid member id') + } + + const meta = new MemberRemarked({ + deleteApp: new DeleteApp({ + appId, + }), + }) + + return this.sender.signAndSend( + this.api.tx.members.memberRemark(memberId, Utils.metadataToBytes(MemberRemarked, meta)), + memberAccount.toString() + ) + } + async createVideoCategory(memberId: u64, name: string): Promise { const memberAccount = await this.getMemberControllerAccount(memberId.toNumber()) diff --git a/tests/network-tests/src/flows/membership/createApp.ts b/tests/network-tests/src/flows/membership/createApp.ts new file mode 100644 index 0000000000..34812a1d7e --- /dev/null +++ b/tests/network-tests/src/flows/membership/createApp.ts @@ -0,0 +1,41 @@ +import { AppMetadata } from '@joystream/metadata-protobuf' +import BN from 'bn.js' +import { assert } from 'chai' +import { extendDebug } from '../../Debugger' +import { FixtureRunner } from '../../Fixture' +import { CreateMembersFixture } from '../../fixtures/content' +import { FlowProps } from '../../Flow' + +export async function createApp({ api, query }: FlowProps): Promise { + const debug = extendDebug('flow:create-app') + debug('Started') + + const createMembersFixture = new CreateMembersFixture(api, query, 1, 0, new BN(10_000_000_000)) + await new FixtureRunner(createMembersFixture).run() + const [member] = createMembersFixture.getCreatedItems().members + + const newAppName = 'create_me' + const newAppMetadata: Partial = { + category: 'blockchain', + oneLiner: 'best blokchain video platform', + description: 'long description', + platforms: ['web', 'mobile'], + } + + await api.createApp(member.memberId, newAppName, newAppMetadata) + + await query.tryQueryWithTimeout( + () => query.getAppsByName(newAppName), + (appsByName) => { + assert.equal(appsByName?.[0]?.name, newAppName) + assert.equal(appsByName?.[0]?.category, newAppMetadata.category) + assert.equal(appsByName?.[0]?.oneLiner, newAppMetadata.oneLiner) + assert.equal(appsByName?.[0]?.description, newAppMetadata.description) + assert.equal(appsByName?.[0]?.termsOfService, null) + assert.equal(appsByName?.[0]?.websiteUrl, null) + assert.deepEqual(appsByName?.[0]?.platforms, newAppMetadata.platforms) + } + ) + + debug('done') +} diff --git a/tests/network-tests/src/flows/membership/deleteApp.ts b/tests/network-tests/src/flows/membership/deleteApp.ts new file mode 100644 index 0000000000..37bb1f4b3c --- /dev/null +++ b/tests/network-tests/src/flows/membership/deleteApp.ts @@ -0,0 +1,45 @@ +import BN from 'bn.js' +import { extendDebug } from '../../Debugger' +import { FlowProps } from '../../Flow' +import { FixtureRunner } from '../../Fixture' +import { CreateMembersFixture } from '../../fixtures/content' +import { assert } from 'chai' +import { AppMetadata } from '@joystream/metadata-protobuf' + +export async function deleteApp({ api, query }: FlowProps): Promise { + const debug = extendDebug('flow:delete-app') + debug('Started') + + const createMembersFixture = new CreateMembersFixture(api, query, 1, 0, new BN(10_000_000_000)) + await new FixtureRunner(createMembersFixture).run() + const [member] = createMembersFixture.getCreatedItems().members + + const appToDeleteName = 'delete_me' + const appToDeleteMetadata: Partial = { + category: 'blockchain', + oneLiner: 'best blokchain video platform', + description: 'long description', + platforms: ['web', 'mobile'], + } + + await api.createApp(member.memberId, appToDeleteName, appToDeleteMetadata) + + const apps = await query.tryQueryWithTimeout( + () => query.getAppsByName(appToDeleteName), + (appsByName) => { + assert.equal(appsByName?.[0]?.name, appToDeleteName) + } + ) + + if (apps?.[0]?.id) { + await api.deleteApp(member.memberId, apps?.[0]?.id) + + await query.tryQueryWithTimeout( + () => query.getAppsByName(appToDeleteName), + (appsByName) => { + assert.equal(appsByName?.length, 0) + } + ) + } + debug('done') +} diff --git a/tests/network-tests/src/flows/membership/updateApp.ts b/tests/network-tests/src/flows/membership/updateApp.ts new file mode 100644 index 0000000000..624817e5a5 --- /dev/null +++ b/tests/network-tests/src/flows/membership/updateApp.ts @@ -0,0 +1,63 @@ +import BN from 'bn.js' +import { extendDebug } from '../../Debugger' +import { FlowProps } from '../../Flow' +import { FixtureRunner } from '../../Fixture' +import { CreateMembersFixture } from '../../fixtures/content' +import { assert } from 'chai' +import { AppMetadata } from '@joystream/metadata-protobuf' + +export async function updateApp({ api, query }: FlowProps): Promise { + const debug = extendDebug('flow:update-app') + debug('Started') + + const createMembersFixture = new CreateMembersFixture(api, query, 1, 0, new BN(10_000_000_000)) + await new FixtureRunner(createMembersFixture).run() + const [member] = createMembersFixture.getCreatedItems().members + + const newAppName = 'update_me' + const newAppMetadata: Partial = { + category: 'blockchain', + oneLiner: 'best blockchain video platform', + description: 'long description', + platforms: ['web', 'mobile'], + useUri: 'http://example.com', + } + + await api.createApp(member.memberId, newAppName, newAppMetadata) + + const apps = await query.tryQueryWithTimeout( + () => query.getAppsByName(newAppName), + (appsByName) => { + assert.equal(appsByName?.[0]?.name, newAppName) + assert.equal(appsByName?.[0]?.oneLiner, newAppMetadata.oneLiner) + assert.equal(appsByName?.[0]?.description, newAppMetadata.description) + assert.deepEqual(appsByName?.[0]?.platforms, newAppMetadata.platforms) + } + ) + + const updatedMetadata: Partial = { + description: 'updated description', + oneLiner: 'updated one liner', + platforms: [], + } + + const newAppId = apps?.[0]?.id + if (newAppId) { + await api.updateApp(member.memberId, newAppId, updatedMetadata) + } + + await query.tryQueryWithTimeout( + () => query.getAppsByName(newAppName), + (appsByName) => { + assert.equal(appsByName?.[0]?.id, newAppId) + assert.equal(appsByName?.[0]?.name, newAppName) + assert.equal(appsByName?.[0]?.useUri, newAppMetadata.useUri) + assert.equal(appsByName?.[0]?.category, newAppMetadata.category) + assert.equal(appsByName?.[0]?.description, updatedMetadata.description) + assert.equal(appsByName?.[0]?.oneLiner, updatedMetadata.oneLiner) + assert.deepEqual(appsByName?.[0]?.platforms, updatedMetadata.platforms) + } + ) + + debug('done') +} diff --git a/tests/network-tests/src/scenarios/app.ts b/tests/network-tests/src/scenarios/app.ts new file mode 100644 index 0000000000..aafdb965e4 --- /dev/null +++ b/tests/network-tests/src/scenarios/app.ts @@ -0,0 +1,16 @@ +import leaderSetup from '../flows/working-groups/leadOpening' +import initFaucet from '../flows/faucet/initFaucet' +import { scenario } from '../Scenario' +import { createApp } from '../flows/membership/createApp' +import { updateApp } from '../flows/membership/updateApp' +import { deleteApp } from '../flows/membership/deleteApp' + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +scenario('App', async ({ job }) => { + job('Initialize Faucet', initFaucet) + + const leads = job('Set WorkingGroup Leads', leaderSetup(true)) + job('Create app', createApp).after(leads) + job('Update app', updateApp).after(leads) + job('Delete app', deleteApp).after(leads) +}) From d601a1672ed6bde8f80bd9b3648a05da05fd532a Mon Sep 17 00:00:00 2001 From: drillprop Date: Tue, 17 Jan 2023 20:42:21 +0100 Subject: [PATCH 08/84] move create app and update app to ChannelOwnerRemarked --- metadata-protobuf/proto/Metaprotocol.proto | 55 ++++++++++------------ 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/metadata-protobuf/proto/Metaprotocol.proto b/metadata-protobuf/proto/Metaprotocol.proto index 4854a421ff..4ca68bb759 100644 --- a/metadata-protobuf/proto/Metaprotocol.proto +++ b/metadata-protobuf/proto/Metaprotocol.proto @@ -113,6 +113,28 @@ message CreateVideoCategory { optional string parent_category_id = 3; } + +message MemberRemarked { + // member_remark extrinsic would emit event containing + // any one of the following serialized messages + oneof member_remarked { + ReactVideo react_video = 1; + ReactComment react_comment = 2; + CreateComment create_comment = 3; + EditComment edit_comment = 4; + DeleteComment delete_comment = 5; + CreateVideoCategory create_video_category = 6; + } +} + +message ChannelModeratorRemarked { + // channel_moderator_remark extrinsic would emit event containing + // any one of the following serialized messages + oneof channel_moderator_remarked { + ModerateComment moderate_comment = 1; + } +} + message AppMetadata { // Url where user can read more about the project or company for this app optional string website_url = 2; @@ -134,38 +156,11 @@ message AppMetadata { message CreateApp { required string name = 1; - optional AppMetadata app_metadata = 2; + optional AppMetadata app_metadata = 3; } message UpdateApp { required string app_id = 1; - optional AppMetadata app_metadata = 2; -} -message DeleteApp { - required string app_id = 1; -} - -message MemberRemarked { - // member_remark extrinsic would emit event containing - // any one of the following serialized messages - oneof member_remarked { - ReactVideo react_video = 1; - ReactComment react_comment = 2; - CreateComment create_comment = 3; - EditComment edit_comment = 4; - DeleteComment delete_comment = 5; - CreateVideoCategory create_video_category = 6; - CreateApp create_app = 7; - UpdateApp update_app = 8; - DeleteApp delete_app = 9; - } -} - -message ChannelModeratorRemarked { - // channel_moderator_remark extrinsic would emit event containing - // any one of the following serialized messages - oneof channel_moderator_remarked { - ModerateComment moderate_comment = 1; - } + optional AppMetadata app_metadata = 3; } message ChannelOwnerRemarked { @@ -176,5 +171,7 @@ message ChannelOwnerRemarked { BanOrUnbanMemberFromChannel ban_or_unban_member_from_channel = 2; VideoReactionsPreference video_reactions_preference = 3; ModerateComment moderate_comment = 5; + CreateApp create_app = 6; + UpdateApp update_app = 7; } } From 6cd76f47e7dee55ce15c8fe5b03e9471720198e9 Mon Sep 17 00:00:00 2001 From: drillprop Date: Tue, 17 Jan 2023 21:04:21 +0100 Subject: [PATCH 09/84] update schema and test queries --- query-node/schemas/app.graphql | 23 -------------- query-node/schemas/content.graphql | 25 +++++++++++++++ .../src/graphql/generated/queries.ts | 17 ++++++++-- .../src/graphql/generated/schema.ts | 31 +++++++++++++++++++ .../src/graphql/queries/app.graphql | 6 ++++ .../src/graphql/queries/content.graphql | 3 ++ 6 files changed, 80 insertions(+), 25 deletions(-) delete mode 100644 query-node/schemas/app.graphql diff --git a/query-node/schemas/app.graphql b/query-node/schemas/app.graphql deleted file mode 100644 index 5f7980fd5d..0000000000 --- a/query-node/schemas/app.graphql +++ /dev/null @@ -1,23 +0,0 @@ -type App @entity { - id: ID! - name: String! - - # Url where user can read more about the project or company for this app - websiteUrl: String - # Url to the app - useUri: String - # what should I to do with icons? - smallIcon: String - mediumIcon: String - bigIcon: String - - oneLiner: String - description: String - termsOfService: String - - # List of platforms on which the app will be available, e.g. [mobile, web, native] - platforms: [String] - category: String - - authKey: String -} diff --git a/query-node/schemas/content.graphql b/query-node/schemas/content.graphql index 8dec9b5346..82d789ab48 100644 --- a/query-node/schemas/content.graphql +++ b/query-node/schemas/content.graphql @@ -8,6 +8,29 @@ type Language @entity { createdInBlock: Int! } +type App @entity { + id: ID! + name: String! + ownerMember: Membership + ownerCuratorGroup: CuratorGroup + # Url where user can read more about the project or company for this app + websiteUrl: String + # Url to the app + useUri: String + # what should I to do with icons? + smallIcon: String + mediumIcon: String + bigIcon: String + oneLiner: String + description: String + termsOfService: String + # List of platforms on which the app will be available, e.g. [mobile, web, native] + platforms: [String] + category: String + authKey: String + channel: Channel! +} + type Channel @entity { "Runtime entity identifier (EntityId)" id: ID! @@ -67,6 +90,8 @@ type Channel @entity { "Channel's privilege level" privilegeLevel: Int + + app: App @derivedFrom(field: "channel") } type VideoCategory @entity { diff --git a/tests/network-tests/src/graphql/generated/queries.ts b/tests/network-tests/src/graphql/generated/queries.ts index d57f0605d0..4688d26808 100644 --- a/tests/network-tests/src/graphql/generated/queries.ts +++ b/tests/network-tests/src/graphql/generated/queries.ts @@ -21,13 +21,13 @@ export type GetAppByIdQueryVariables = Types.Exact<{ id: Types.Scalars['ID'] }> -export type GetAppByIdQuery = { appByUniqueInput?: Types.Maybe } +export type GetAppByIdQuery = { appByUniqueInput?: Types.Maybe<{ channel: ChannelFieldsFragment } & AppFieldsFragment> } export type GetAppsByNameQueryVariables = Types.Exact<{ name: Types.Scalars['String'] }> -export type GetAppsByNameQuery = { apps: Array } +export type GetAppsByNameQuery = { apps: Array<{ channel: ChannelFieldsFragment } & AppFieldsFragment> } type DataObjectTypeFields_DataObjectTypeChannelAvatar_Fragment = { __typename: 'DataObjectTypeChannelAvatar' @@ -90,6 +90,7 @@ export type ChannelFieldsFragment = { isCensored: boolean rewardAccount: string language?: Types.Maybe<{ iso: string }> + app?: Types.Maybe ownerMember?: Types.Maybe<{ id: string }> ownerCuratorGroup?: Types.Maybe<{ id: string }> avatarPhoto?: Types.Maybe @@ -2595,6 +2596,9 @@ export const ChannelFields = gql` iso } isCensored + app { + ...AppFields + } ownerMember { id } @@ -2612,6 +2616,7 @@ export const ChannelFields = gql` } rewardAccount } + ${AppFields} ${StorageDataObjectFields} ` export const LicenseFields = gql` @@ -4888,17 +4893,25 @@ export const GetAppById = gql` query getAppById($id: ID!) { appByUniqueInput(where: { id: $id }) { ...AppFields + channel { + ...ChannelFields + } } } ${AppFields} + ${ChannelFields} ` export const GetAppsByName = gql` query getAppsByName($name: String!) { apps(where: { name_eq: $name }) { ...AppFields + channel { + ...ChannelFields + } } } ${AppFields} + ${ChannelFields} ` export const GetChannelById = gql` query getChannelById($id: ID!) { diff --git a/tests/network-tests/src/graphql/generated/schema.ts b/tests/network-tests/src/graphql/generated/schema.ts index c8c7c203eb..355d1b1e15 100644 --- a/tests/network-tests/src/graphql/generated/schema.ts +++ b/tests/network-tests/src/graphql/generated/schema.ts @@ -151,6 +151,10 @@ export type App = BaseGraphQlObject & { deletedById?: Maybe version: Scalars['Int'] name: Scalars['String'] + ownerMember?: Maybe + ownerMemberId?: Maybe + ownerCuratorGroup?: Maybe + ownerCuratorGroupId?: Maybe websiteUrl?: Maybe useUri?: Maybe smallIcon?: Maybe @@ -162,6 +166,8 @@ export type App = BaseGraphQlObject & { platforms?: Maybe> category?: Maybe authKey?: Maybe + channel: Channel + channelId: Scalars['String'] } export type AppConnection = { @@ -172,6 +178,8 @@ export type AppConnection = { export type AppCreateInput = { name: Scalars['String'] + ownerMember?: Maybe + ownerCuratorGroup?: Maybe websiteUrl?: Maybe useUri?: Maybe smallIcon?: Maybe @@ -183,6 +191,7 @@ export type AppCreateInput = { platforms?: Maybe> category?: Maybe authKey?: Maybe + channel: Scalars['ID'] } export type AppEdge = { @@ -199,6 +208,10 @@ export enum AppOrderByInput { DeletedAtDesc = 'deletedAt_DESC', NameAsc = 'name_ASC', NameDesc = 'name_DESC', + OwnerMemberAsc = 'ownerMember_ASC', + OwnerMemberDesc = 'ownerMember_DESC', + OwnerCuratorGroupAsc = 'ownerCuratorGroup_ASC', + OwnerCuratorGroupDesc = 'ownerCuratorGroup_DESC', WebsiteUrlAsc = 'websiteUrl_ASC', WebsiteUrlDesc = 'websiteUrl_DESC', UseUriAsc = 'useUri_ASC', @@ -219,10 +232,14 @@ export enum AppOrderByInput { CategoryDesc = 'category_DESC', AuthKeyAsc = 'authKey_ASC', AuthKeyDesc = 'authKey_DESC', + ChannelAsc = 'channel_ASC', + ChannelDesc = 'channel_DESC', } export type AppUpdateInput = { name?: Maybe + ownerMember?: Maybe + ownerCuratorGroup?: Maybe websiteUrl?: Maybe useUri?: Maybe smallIcon?: Maybe @@ -234,6 +251,7 @@ export type AppUpdateInput = { platforms?: Maybe> category?: Maybe authKey?: Maybe + channel?: Maybe } export type AppWhereInput = { @@ -319,6 +337,9 @@ export type AppWhereInput = { authKey_startsWith?: Maybe authKey_endsWith?: Maybe authKey_in?: Maybe> + ownerMember?: Maybe + ownerCuratorGroup?: Maybe + channel?: Maybe AND?: Maybe> OR?: Maybe> NOT?: Maybe> @@ -6347,6 +6368,7 @@ export type Channel = BaseGraphQlObject & { channelStateBloatBond: Scalars['BigInt'] /** Channel's privilege level */ privilegeLevel?: Maybe + app?: Maybe commentcreatedeventvideoChannel?: Maybe> commentdeletedeventvideoChannel?: Maybe> commentmoderatedeventvideoChannel?: Maybe> @@ -7081,6 +7103,7 @@ export type ChannelWhereInput = { channelNftCollectors_none?: Maybe channelNftCollectors_some?: Maybe channelNftCollectors_every?: Maybe + app?: Maybe commentcreatedeventvideoChannel_none?: Maybe commentcreatedeventvideoChannel_some?: Maybe commentcreatedeventvideoChannel_every?: Maybe @@ -9121,6 +9144,7 @@ export type CuratorGroup = BaseGraphQlObject & { channels: Array nftCollectorInChannels: Array curators: Array + appownerCuratorGroup?: Maybe> auctionbidcanceledeventownerCuratorGroup?: Maybe> auctionbidmadeeventownerCuratorGroup?: Maybe> auctioncanceledeventownerCuratorGroup?: Maybe> @@ -9207,6 +9231,9 @@ export type CuratorGroupWhereInput = { curators_none?: Maybe curators_some?: Maybe curators_every?: Maybe + appownerCuratorGroup_none?: Maybe + appownerCuratorGroup_some?: Maybe + appownerCuratorGroup_every?: Maybe auctionbidcanceledeventownerCuratorGroup_none?: Maybe auctionbidcanceledeventownerCuratorGroup_some?: Maybe auctionbidcanceledeventownerCuratorGroup_every?: Maybe @@ -13843,6 +13870,7 @@ export type Membership = BaseGraphQlObject & { memberEnglishAuctionSettledEvents: Array memberOpenAuctionAcceptedBidEvents: Array memberBidMadeCompletingAuctionEvents: Array + appownerMember?: Maybe> auctioninitialOwner?: Maybe> auctionwinningMember?: Maybe> auctionbidcanceledeventmember?: Maybe> @@ -14699,6 +14727,9 @@ export type MembershipWhereInput = { memberBidMadeCompletingAuctionEvents_none?: Maybe memberBidMadeCompletingAuctionEvents_some?: Maybe memberBidMadeCompletingAuctionEvents_every?: Maybe + appownerMember_none?: Maybe + appownerMember_some?: Maybe + appownerMember_every?: Maybe auctioninitialOwner_none?: Maybe auctioninitialOwner_some?: Maybe auctioninitialOwner_every?: Maybe diff --git a/tests/network-tests/src/graphql/queries/app.graphql b/tests/network-tests/src/graphql/queries/app.graphql index 046bff7a86..5dba609266 100644 --- a/tests/network-tests/src/graphql/queries/app.graphql +++ b/tests/network-tests/src/graphql/queries/app.graphql @@ -17,11 +17,17 @@ fragment AppFields on App { query getAppById($id: ID!) { appByUniqueInput(where: { id: $id }) { ...AppFields + channel { + ...ChannelFields + } } } query getAppsByName($name: String!) { apps(where: { name_eq: $name }) { ...AppFields + channel { + ...ChannelFields + } } } diff --git a/tests/network-tests/src/graphql/queries/content.graphql b/tests/network-tests/src/graphql/queries/content.graphql index b52551e53b..b87dd8efa9 100644 --- a/tests/network-tests/src/graphql/queries/content.graphql +++ b/tests/network-tests/src/graphql/queries/content.graphql @@ -53,6 +53,9 @@ fragment ChannelFields on Channel { iso } isCensored + app { + ...AppFields + } ownerMember { id } From fe3ce45d74a4328556b777f802bd2cb249da3696 Mon Sep 17 00:00:00 2001 From: drillprop Date: Wed, 18 Jan 2023 09:27:58 +0100 Subject: [PATCH 10/84] work in progress, tests --- query-node/mappings/src/content/app.ts | 144 ++++++++---------- query-node/mappings/src/content/channel.ts | 10 +- query-node/mappings/src/membership.ts | 19 --- tests/network-tests/src/Api.ts | 36 ++--- tests/network-tests/src/QueryNodeApi.ts | 2 +- .../{membership => content}/createApp.ts | 43 +++++- .../src/flows/membership/deleteApp.ts | 45 ------ tests/network-tests/src/scenarios/app.ts | 10 +- 8 files changed, 125 insertions(+), 184 deletions(-) rename tests/network-tests/src/flows/{membership => content}/createApp.ts (51%) delete mode 100644 tests/network-tests/src/flows/membership/deleteApp.ts diff --git a/query-node/mappings/src/content/app.ts b/query-node/mappings/src/content/app.ts index f259fee7e9..9f1704583a 100644 --- a/query-node/mappings/src/content/app.ts +++ b/query-node/mappings/src/content/app.ts @@ -1,17 +1,18 @@ import { DatabaseManager, SubstrateEvent } from '@joystream/hydra-common' -import { CreateApp, ICreateApp, IDeleteApp, IUpdateApp } from '@joystream/metadata-protobuf' -import { MemberId } from '@joystream/types/primitives' -import { App } from 'query-node/dist/model' -import { logger } from '../common' +import { ICreateApp } from '@joystream/metadata-protobuf' +import { ChannelId } from '@joystream/types/primitives' +import { logger } from '@joystream/warthog' +import { Channel, App } from 'query-node/dist/model' +import { inconsistentState } from '../common' export async function processCreateAppMessage( store: DatabaseManager, event: SubstrateEvent, - memberId: MemberId, + channelId: ChannelId, message: ICreateApp ): Promise { const { name, appMetadata } = message - const appId = await getAppId(store, event) + const appId = await getAppId(event) const isAppExists = await store.get(App, { where: { @@ -24,10 +25,19 @@ export async function processCreateAppMessage( return } + // load channel + const channel = await store.get(Channel, { + where: { id: channelId.toString() }, + }) + + // ensure channel exists + if (!channel) { + return inconsistentState('Non-existing channel update requested', channelId) + } + const newApp = new App({ name, id: appId, - createdById: memberId.toString(), websiteUrl: appMetadata?.websiteUrl || undefined, useUri: appMetadata?.useUri || undefined, smallIcon: appMetadata?.smallIcon || undefined, @@ -40,84 +50,56 @@ export async function processCreateAppMessage( category: appMetadata?.category || undefined, authKey: appMetadata?.authKey || undefined, }) - await store.save(newApp) + await store.save(newApp) logger.info('App has been created', { name }) -} - -export async function processUpdateAppMessage( - store: DatabaseManager, - event: SubstrateEvent, - memberId: MemberId, - message: IUpdateApp -): Promise { - const { appId, appMetadata } = message - - const app = await getAppByIdAndMemberId(store, appId, memberId) - if (!app) { - logger.error("App doesn't exists or doesn't belong to the member", { appId, memberId: memberId.toString() }) - return - } + channel.app = newApp - app.websiteUrl = appMetadata?.websiteUrl || app.websiteUrl - app.useUri = appMetadata?.useUri || app.useUri - app.smallIcon = appMetadata?.smallIcon || app.smallIcon - app.mediumIcon = appMetadata?.mediumIcon || app.mediumIcon - app.bigIcon = appMetadata?.bigIcon || app.bigIcon - app.oneLiner = appMetadata?.oneLiner || app.oneLiner - app.description = appMetadata?.description || app.description - app.termsOfService = appMetadata?.termsOfService || app.termsOfService - app.platforms = appMetadata?.platforms || app.platforms - app.category = appMetadata?.category || app.category - app.authKey = appMetadata?.authKey || app.authKey - - await store.save(app) - logger.info('App has been updated', { appId }) + await store.save(channel) + logger.info('Channel has been updated', { channel }) } -export async function processDeleteAppMessage( - store: DatabaseManager, - event: SubstrateEvent, - memberId: MemberId, - message: IDeleteApp -): Promise { - const { appId } = message - - const app = await getAppByIdAndMemberId(store, appId, memberId) - - if (!app) { - logger.error("App doesn't exists or doesn't belong to the member", { appId, memberId: memberId.toString() }) - return - } - - await store.remove(new App({ id: appId })) - logger.info('App has been removed', { appId }) +async function getAppId(event: SubstrateEvent): Promise { + return `${event.blockNumber}-${event.indexInBlock}` } -async function getAppId(store: DatabaseManager, event: SubstrateEvent): Promise { - let appId = `${event.blockNumber}-${event.indexInBlock}` - let tries = 0 - - // make sure app id is unique - while (await store.get(App, { where: { id: appId } })) { - tries++ - appId = `${event.blockNumber}-${event.indexInBlock}-${tries}` - } - - return appId -} - -async function getAppByIdAndMemberId( - store: DatabaseManager, - appId: string, - memberId: MemberId -): Promise { - const app = await store.get(App, { - where: { - id: appId, - createdById: memberId.toString(), - }, - }) - - return app -} +// export async function processUpdateAppMessage(store: DatabaseManager, message: IUpdateApp): Promise { +// const { appId, appMetadata } = message + +// const app = await getAppByIdAndMemberId(store, appId) + +// if (!app) { +// logger.error("App doesn't exists or doesn't belong to the member", { appId }) +// return +// } + +// app.websiteUrl = appMetadata?.websiteUrl || app.websiteUrl +// app.useUri = appMetadata?.useUri || app.useUri +// app.smallIcon = appMetadata?.smallIcon || app.smallIcon +// app.mediumIcon = appMetadata?.mediumIcon || app.mediumIcon +// app.bigIcon = appMetadata?.bigIcon || app.bigIcon +// app.oneLiner = appMetadata?.oneLiner || app.oneLiner +// app.description = appMetadata?.description || app.description +// app.termsOfService = appMetadata?.termsOfService || app.termsOfService +// app.platforms = appMetadata?.platforms || app.platforms +// app.category = appMetadata?.category || app.category +// app.authKey = appMetadata?.authKey || app.authKey + +// await store.save(app) +// logger.info('App has been updated', { appId }) +// } + +// async function getAppByIdAndMemberId( +// store: DatabaseManager, +// appId: string, +// memberId: MemberId +// ): Promise { +// const app = await store.get(App, { +// where: { +// id: appId, +// createdById: memberId.toString(), +// }, +// }) + +// return app +// } diff --git a/query-node/mappings/src/content/channel.ts b/query-node/mappings/src/content/channel.ts index b75339fd3b..7ff0708864 100644 --- a/query-node/mappings/src/content/channel.ts +++ b/query-node/mappings/src/content/channel.ts @@ -3,7 +3,7 @@ eslint-disable @typescript-eslint/naming-convention */ import { DatabaseManager, EventContext, StoreContext, SubstrateEvent } from '@joystream/hydra-common' import { ChannelMetadata, ChannelModeratorRemarked, ChannelOwnerRemarked } from '@joystream/metadata-protobuf' -import { ChannelId, DataObjectId } from '@joystream/types/primitives' +import { ChannelId, DataObjectId, MemberId } from '@joystream/types/primitives' import { Channel, Collaborator, @@ -18,6 +18,7 @@ import { ChannelAssetsDeletedByModeratorEvent, ChannelDeletedByModeratorEvent, ChannelVisibilitySetByModeratorEvent, + App, } from 'query-node/dist/model' import { In } from 'typeorm' import { Content } from '../../generated/types' @@ -46,6 +47,7 @@ import { import { BTreeMap, BTreeSet, u64 } from '@polkadot/types' // Joystream types import { PalletContentIterableEnumsChannelActionPermission } from '@polkadot/types/lookup' +import { processCreateAppMessage } from './app' export async function content_ChannelCreated(ctx: EventContext & StoreContext): Promise { const { store, event } = ctx @@ -387,6 +389,12 @@ async function processOwnerRemark( return { commentModeratedId: comment.id } } + if (messageType === 'createApp') { + await processCreateAppMessage(store, event, channelId, decodedMessage.createApp!) + + return {} + } + return inconsistentState('Unsupported message type in channel owner remark action', messageType) } diff --git a/query-node/mappings/src/membership.ts b/query-node/mappings/src/membership.ts index 3d9e4d610e..6c11f6fa3c 100644 --- a/query-node/mappings/src/membership.ts +++ b/query-node/mappings/src/membership.ts @@ -70,7 +70,6 @@ import { createVideoCategory } from './content/videoCategory' import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types' import { membershipConfig } from './bootstrap-data' import { BN } from 'bn.js' -import { processCreateAppMessage, processDeleteAppMessage, processUpdateAppMessage } from './content/app' // FIXME: Should be emitted as part of MemberInvited event, but this requires a runtime upgrade async function initialInvitationBalance(store: DatabaseManager) { @@ -692,24 +691,6 @@ async function processMemberRemark( return { videoCategoryCreatedId: videoCategory.id } } - if (messageType === 'createApp') { - await processCreateAppMessage(store, event, memberId, decodedMessage.createApp!) - - return {} - } - - if (messageType === 'updateApp') { - await processUpdateAppMessage(store, event, memberId, decodedMessage.updateApp!) - - return {} - } - - if (messageType === 'deleteApp') { - await processDeleteAppMessage(store, event, memberId, decodedMessage.deleteApp!) - - return {} - } - // unknown message type return inconsistentState('Unsupported message type in member_remark action', messageType) } diff --git a/tests/network-tests/src/Api.ts b/tests/network-tests/src/Api.ts index 280887bfcf..f8b3a3a4bd 100644 --- a/tests/network-tests/src/Api.ts +++ b/tests/network-tests/src/Api.ts @@ -62,10 +62,10 @@ import { } from './consts' import { - CreateApp, + ChannelOwnerRemarked, CreateVideoCategory, - DeleteApp, IAppMetadata, + IChannelOwnerRemarked, MemberRemarked, UpdateApp, } from '@joystream/metadata-protobuf' @@ -811,15 +811,15 @@ export class Api { throw new Error('invalid member id') } - const meta = new MemberRemarked({ - createApp: new CreateApp({ + const msg: IChannelOwnerRemarked = { + createApp: { name, appMetadata, - }), - }) + }, + } return this.sender.signAndSend( - this.api.tx.members.memberRemark(memberId, Utils.metadataToBytes(MemberRemarked, meta)), + this.api.tx.members.memberRemark(memberId, Utils.metadataToBytes(ChannelOwnerRemarked, msg)), memberAccount.toString() ) } @@ -830,7 +830,7 @@ export class Api { throw new Error('invalid member id') } - const meta = new MemberRemarked({ + const meta = new ChannelOwnerRemarked({ updateApp: new UpdateApp({ appId, appMetadata, @@ -838,25 +838,7 @@ export class Api { }) return this.sender.signAndSend( - this.api.tx.members.memberRemark(memberId, Utils.metadataToBytes(MemberRemarked, meta)), - memberAccount.toString() - ) - } - - async deleteApp(memberId: u64, appId: string): Promise { - const memberAccount = await this.getMemberControllerAccount(memberId.toNumber()) - if (!memberAccount) { - throw new Error('invalid member id') - } - - const meta = new MemberRemarked({ - deleteApp: new DeleteApp({ - appId, - }), - }) - - return this.sender.signAndSend( - this.api.tx.members.memberRemark(memberId, Utils.metadataToBytes(MemberRemarked, meta)), + this.api.tx.content.channelOwnerRemark(memberId, Utils.metadataToBytes(ChannelOwnerRemarked, meta)), memberAccount.toString() ) } diff --git a/tests/network-tests/src/QueryNodeApi.ts b/tests/network-tests/src/QueryNodeApi.ts index cf503f74ff..ff182ab3f7 100644 --- a/tests/network-tests/src/QueryNodeApi.ts +++ b/tests/network-tests/src/QueryNodeApi.ts @@ -455,7 +455,7 @@ export class QueryNodeApi { query: () => Promise, assertResultIsValid: (res: QueryResultT) => void, retryTimeMs = BLOCKTIME * 9, - retries = 6 + retries = 20 ): Promise { const label = query.toString().replace(/^.*\.([A-za-z0-9]+\(.*\))$/g, '$1') const debug = this.tryDebug.extend(label) diff --git a/tests/network-tests/src/flows/membership/createApp.ts b/tests/network-tests/src/flows/content/createApp.ts similarity index 51% rename from tests/network-tests/src/flows/membership/createApp.ts rename to tests/network-tests/src/flows/content/createApp.ts index 34812a1d7e..057395e5e8 100644 --- a/tests/network-tests/src/flows/membership/createApp.ts +++ b/tests/network-tests/src/flows/content/createApp.ts @@ -3,17 +3,54 @@ import BN from 'bn.js' import { assert } from 'chai' import { extendDebug } from '../../Debugger' import { FixtureRunner } from '../../Fixture' -import { CreateMembersFixture } from '../../fixtures/content' +import { + CreateChannelsAndVideosFixture, + CreateContentStructureFixture, + CreateMembersFixture, +} from '../../fixtures/content' import { FlowProps } from '../../Flow' +import { createJoystreamCli } from '../utils' export async function createApp({ api, query }: FlowProps): Promise { const debug = extendDebug('flow:create-app') debug('Started') + const joystreamCli = await createJoystreamCli() - const createMembersFixture = new CreateMembersFixture(api, query, 1, 0, new BN(10_000_000_000)) + // settings + const videoCategoryCount = 2 + const sufficientTopupAmount = new BN(10_000_000_000_000) // some very big number to cover fees of all transactions + + // create channel categories and video categories + const createContentStructureFixture = new CreateContentStructureFixture(api, query, joystreamCli, videoCategoryCount) + await new FixtureRunner(createContentStructureFixture).run() + + // create author of channels and videos + const createMembersFixture = new CreateMembersFixture(api, query, 3, 0, sufficientTopupAmount) await new FixtureRunner(createMembersFixture).run() const [member] = createMembersFixture.getCreatedItems().members + await joystreamCli.createChannel( + { + title: 'App channel', + description: 'Channel for testing apps', + isPublic: true, + language: 'en', + }, + ['--context', 'Member', '--useMemberId', member.memberId.toString()] + ) + + // // create channels and videos + // const createChannelsAndVideos = new CreateChannelsAndVideosFixture( + // api, + // query, + // joystreamCli, + // channelCount, + // videoCount, + // videoCategoryIds[0], + // member + // ) + // await new FixtureRunner(createChannelsAndVideos).run() + const newAppName = 'create_me' const newAppMetadata: Partial = { category: 'blockchain', @@ -22,7 +59,7 @@ export async function createApp({ api, query }: FlowProps): Promise { platforms: ['web', 'mobile'], } - await api.createApp(member.memberId, newAppName, newAppMetadata) + await api.createApp(member.memberId, newAppName) await query.tryQueryWithTimeout( () => query.getAppsByName(newAppName), diff --git a/tests/network-tests/src/flows/membership/deleteApp.ts b/tests/network-tests/src/flows/membership/deleteApp.ts deleted file mode 100644 index 37bb1f4b3c..0000000000 --- a/tests/network-tests/src/flows/membership/deleteApp.ts +++ /dev/null @@ -1,45 +0,0 @@ -import BN from 'bn.js' -import { extendDebug } from '../../Debugger' -import { FlowProps } from '../../Flow' -import { FixtureRunner } from '../../Fixture' -import { CreateMembersFixture } from '../../fixtures/content' -import { assert } from 'chai' -import { AppMetadata } from '@joystream/metadata-protobuf' - -export async function deleteApp({ api, query }: FlowProps): Promise { - const debug = extendDebug('flow:delete-app') - debug('Started') - - const createMembersFixture = new CreateMembersFixture(api, query, 1, 0, new BN(10_000_000_000)) - await new FixtureRunner(createMembersFixture).run() - const [member] = createMembersFixture.getCreatedItems().members - - const appToDeleteName = 'delete_me' - const appToDeleteMetadata: Partial = { - category: 'blockchain', - oneLiner: 'best blokchain video platform', - description: 'long description', - platforms: ['web', 'mobile'], - } - - await api.createApp(member.memberId, appToDeleteName, appToDeleteMetadata) - - const apps = await query.tryQueryWithTimeout( - () => query.getAppsByName(appToDeleteName), - (appsByName) => { - assert.equal(appsByName?.[0]?.name, appToDeleteName) - } - ) - - if (apps?.[0]?.id) { - await api.deleteApp(member.memberId, apps?.[0]?.id) - - await query.tryQueryWithTimeout( - () => query.getAppsByName(appToDeleteName), - (appsByName) => { - assert.equal(appsByName?.length, 0) - } - ) - } - debug('done') -} diff --git a/tests/network-tests/src/scenarios/app.ts b/tests/network-tests/src/scenarios/app.ts index aafdb965e4..7bcc5c6ebd 100644 --- a/tests/network-tests/src/scenarios/app.ts +++ b/tests/network-tests/src/scenarios/app.ts @@ -1,16 +1,12 @@ -import leaderSetup from '../flows/working-groups/leadOpening' +import { createApp } from '../flows/content/createApp' import initFaucet from '../flows/faucet/initFaucet' +import leaderSetup from '../flows/working-groups/leadOpening' import { scenario } from '../Scenario' -import { createApp } from '../flows/membership/createApp' -import { updateApp } from '../flows/membership/updateApp' -import { deleteApp } from '../flows/membership/deleteApp' // eslint-disable-next-line @typescript-eslint/no-floating-promises scenario('App', async ({ job }) => { job('Initialize Faucet', initFaucet) - const leads = job('Set WorkingGroup Leads', leaderSetup(true)) + job('Create app', createApp).after(leads) - job('Update app', updateApp).after(leads) - job('Delete app', deleteApp).after(leads) }) From c790c651454cf2856bdcda0fa681c908505bdd37 Mon Sep 17 00:00:00 2001 From: drillprop Date: Wed, 18 Jan 2023 22:28:56 +0100 Subject: [PATCH 11/84] create an app --- query-node/mappings/src/content/app.ts | 3 ++ query-node/mappings/src/content/channel.ts | 1 - tests/network-tests/src/Api.ts | 18 +++++++-- .../flows/content/{createApp.ts => app.ts} | 38 +++++++++---------- tests/network-tests/src/scenarios/app.ts | 2 +- 5 files changed, 35 insertions(+), 27 deletions(-) rename tests/network-tests/src/flows/content/{createApp.ts => app.ts} (76%) diff --git a/query-node/mappings/src/content/app.ts b/query-node/mappings/src/content/app.ts index 9f1704583a..d4fc31fb29 100644 --- a/query-node/mappings/src/content/app.ts +++ b/query-node/mappings/src/content/app.ts @@ -49,6 +49,9 @@ export async function processCreateAppMessage( platforms: appMetadata?.platforms || undefined, category: appMetadata?.category || undefined, authKey: appMetadata?.authKey || undefined, + ownerCuratorGroup: channel.ownerCuratorGroup, + ownerMember: channel.ownerMember, + channel, }) await store.save(newApp) logger.info('App has been created', { name }) diff --git a/query-node/mappings/src/content/channel.ts b/query-node/mappings/src/content/channel.ts index 7ff0708864..2b57a8128f 100644 --- a/query-node/mappings/src/content/channel.ts +++ b/query-node/mappings/src/content/channel.ts @@ -273,7 +273,6 @@ export async function content_ChannelOwnerRemarked(ctx: EventContext & StoreCont try { const decodedMessage = ChannelOwnerRemarked.decode(message.toU8a(true)) const contentActor = getContentActor(channel.ownerMember, channel.ownerCuratorGroup) - const metaTransactionInfo = await processOwnerRemark(store, event, channelId, contentActor, decodedMessage) await saveMetaprotocolTransactionSuccessful(store, event, metaTransactionInfo) diff --git a/tests/network-tests/src/Api.ts b/tests/network-tests/src/Api.ts index f8b3a3a4bd..e1dd3549ce 100644 --- a/tests/network-tests/src/Api.ts +++ b/tests/network-tests/src/Api.ts @@ -805,7 +805,12 @@ export class Api { return event.data[2] } - async createApp(memberId: u64, name: string, appMetadata?: IAppMetadata): Promise { + async createApp( + memberId: u64, + channelId: number, + name: string, + appMetadata?: IAppMetadata + ): Promise { const memberAccount = await this.getMemberControllerAccount(memberId.toNumber()) if (!memberAccount) { throw new Error('invalid member id') @@ -819,12 +824,17 @@ export class Api { } return this.sender.signAndSend( - this.api.tx.members.memberRemark(memberId, Utils.metadataToBytes(ChannelOwnerRemarked, msg)), + this.api.tx.content.channelOwnerRemark(channelId, Utils.metadataToBytes(ChannelOwnerRemarked, msg)), memberAccount.toString() ) } - async updateApp(memberId: u64, appId: string, appMetadata: IAppMetadata): Promise { + async updateApp( + memberId: u64, + channelId: number, + appId: string, + appMetadata: IAppMetadata + ): Promise { const memberAccount = await this.getMemberControllerAccount(memberId.toNumber()) if (!memberAccount) { throw new Error('invalid member id') @@ -838,7 +848,7 @@ export class Api { }) return this.sender.signAndSend( - this.api.tx.content.channelOwnerRemark(memberId, Utils.metadataToBytes(ChannelOwnerRemarked, meta)), + this.api.tx.content.channelOwnerRemark(channelId, Utils.metadataToBytes(ChannelOwnerRemarked, meta)), memberAccount.toString() ) } diff --git a/tests/network-tests/src/flows/content/createApp.ts b/tests/network-tests/src/flows/content/app.ts similarity index 76% rename from tests/network-tests/src/flows/content/createApp.ts rename to tests/network-tests/src/flows/content/app.ts index 057395e5e8..fa88409e3a 100644 --- a/tests/network-tests/src/flows/content/createApp.ts +++ b/tests/network-tests/src/flows/content/app.ts @@ -17,39 +17,35 @@ export async function createApp({ api, query }: FlowProps): Promise { const joystreamCli = await createJoystreamCli() // settings - const videoCategoryCount = 2 + const videoCount = 0 + const videoCategoryCount = 0 + const channelCount = 1 const sufficientTopupAmount = new BN(10_000_000_000_000) // some very big number to cover fees of all transactions // create channel categories and video categories const createContentStructureFixture = new CreateContentStructureFixture(api, query, joystreamCli, videoCategoryCount) await new FixtureRunner(createContentStructureFixture).run() + const { videoCategoryIds } = createContentStructureFixture.getCreatedItems() + // create author of channels and videos const createMembersFixture = new CreateMembersFixture(api, query, 3, 0, sufficientTopupAmount) await new FixtureRunner(createMembersFixture).run() const [member] = createMembersFixture.getCreatedItems().members - await joystreamCli.createChannel( - { - title: 'App channel', - description: 'Channel for testing apps', - isPublic: true, - language: 'en', - }, - ['--context', 'Member', '--useMemberId', member.memberId.toString()] + // create channels and videos + const createChannelsAndVideos = new CreateChannelsAndVideosFixture( + api, + query, + joystreamCli, + channelCount, + videoCount, + videoCategoryIds[0], + member ) + await new FixtureRunner(createChannelsAndVideos).run() - // // create channels and videos - // const createChannelsAndVideos = new CreateChannelsAndVideosFixture( - // api, - // query, - // joystreamCli, - // channelCount, - // videoCount, - // videoCategoryIds[0], - // member - // ) - // await new FixtureRunner(createChannelsAndVideos).run() + const [channelId] = createChannelsAndVideos.getCreatedItems().channelIds const newAppName = 'create_me' const newAppMetadata: Partial = { @@ -59,7 +55,7 @@ export async function createApp({ api, query }: FlowProps): Promise { platforms: ['web', 'mobile'], } - await api.createApp(member.memberId, newAppName) + await api.createApp(member.memberId, channelId, newAppName, newAppMetadata) await query.tryQueryWithTimeout( () => query.getAppsByName(newAppName), diff --git a/tests/network-tests/src/scenarios/app.ts b/tests/network-tests/src/scenarios/app.ts index 7bcc5c6ebd..327af8f215 100644 --- a/tests/network-tests/src/scenarios/app.ts +++ b/tests/network-tests/src/scenarios/app.ts @@ -1,4 +1,4 @@ -import { createApp } from '../flows/content/createApp' +import { createApp } from '../flows/content/app' import initFaucet from '../flows/faucet/initFaucet' import leaderSetup from '../flows/working-groups/leadOpening' import { scenario } from '../Scenario' From 90909d40a45c9cc4ad623917683183cb515f2ad5 Mon Sep 17 00:00:00 2001 From: drillprop Date: Thu, 19 Jan 2023 11:19:00 +0100 Subject: [PATCH 12/84] fix updateApp, refactoring --- query-node/mappings/src/content/app.ts | 82 ++++++------ query-node/mappings/src/content/channel.ts | 11 +- tests/network-tests/src/flows/content/app.ts | 124 ++++++++++++++---- .../src/flows/membership/updateApp.ts | 63 --------- tests/network-tests/src/scenarios/app.ts | 3 +- 5 files changed, 150 insertions(+), 133 deletions(-) delete mode 100644 tests/network-tests/src/flows/membership/updateApp.ts diff --git a/query-node/mappings/src/content/app.ts b/query-node/mappings/src/content/app.ts index d4fc31fb29..2821f6db5e 100644 --- a/query-node/mappings/src/content/app.ts +++ b/query-node/mappings/src/content/app.ts @@ -1,5 +1,5 @@ import { DatabaseManager, SubstrateEvent } from '@joystream/hydra-common' -import { ICreateApp } from '@joystream/metadata-protobuf' +import { ICreateApp, IUpdateApp } from '@joystream/metadata-protobuf' import { ChannelId } from '@joystream/types/primitives' import { logger } from '@joystream/warthog' import { Channel, App } from 'query-node/dist/model' @@ -12,7 +12,7 @@ export async function processCreateAppMessage( message: ICreateApp ): Promise { const { name, appMetadata } = message - const appId = await getAppId(event) + const appId = await createAppId(event) const isAppExists = await store.get(App, { where: { @@ -21,8 +21,7 @@ export async function processCreateAppMessage( }) if (isAppExists) { - logger.error('App already exists', { name }) - return + inconsistentState(`App with this name already exists:`, name) } // load channel @@ -62,47 +61,48 @@ export async function processCreateAppMessage( logger.info('Channel has been updated', { channel }) } -async function getAppId(event: SubstrateEvent): Promise { +async function createAppId(event: SubstrateEvent): Promise { return `${event.blockNumber}-${event.indexInBlock}` } -// export async function processUpdateAppMessage(store: DatabaseManager, message: IUpdateApp): Promise { -// const { appId, appMetadata } = message - -// const app = await getAppByIdAndMemberId(store, appId) - -// if (!app) { -// logger.error("App doesn't exists or doesn't belong to the member", { appId }) -// return -// } +export async function proccessUpdateApp( + store: DatabaseManager, + channelId: ChannelId, + message: IUpdateApp +): Promise { + const { appId, appMetadata } = message -// app.websiteUrl = appMetadata?.websiteUrl || app.websiteUrl -// app.useUri = appMetadata?.useUri || app.useUri -// app.smallIcon = appMetadata?.smallIcon || app.smallIcon -// app.mediumIcon = appMetadata?.mediumIcon || app.mediumIcon -// app.bigIcon = appMetadata?.bigIcon || app.bigIcon -// app.oneLiner = appMetadata?.oneLiner || app.oneLiner -// app.description = appMetadata?.description || app.description -// app.termsOfService = appMetadata?.termsOfService || app.termsOfService -// app.platforms = appMetadata?.platforms || app.platforms -// app.category = appMetadata?.category || app.category -// app.authKey = appMetadata?.authKey || app.authKey + const app = await getAppById(store, appId) -// await store.save(app) -// logger.info('App has been updated', { appId }) -// } + if (!app) { + inconsistentState("App doesn't exists; appId:", appId) + } + if (app?.channel.id !== channelId.toString()) { + inconsistentState(`Cannot update app; app does not belong to the channelId: `, channelId) + } -// async function getAppByIdAndMemberId( -// store: DatabaseManager, -// appId: string, -// memberId: MemberId -// ): Promise { -// const app = await store.get(App, { -// where: { -// id: appId, -// createdById: memberId.toString(), -// }, -// }) + app.websiteUrl = appMetadata?.websiteUrl || app.websiteUrl + app.useUri = appMetadata?.useUri || app.useUri + app.smallIcon = appMetadata?.smallIcon || app.smallIcon + app.mediumIcon = appMetadata?.mediumIcon || app.mediumIcon + app.bigIcon = appMetadata?.bigIcon || app.bigIcon + app.oneLiner = appMetadata?.oneLiner || app.oneLiner + app.description = appMetadata?.description || app.description + app.termsOfService = appMetadata?.termsOfService || app.termsOfService + app.platforms = appMetadata?.platforms || app.platforms + app.category = appMetadata?.category || app.category + app.authKey = appMetadata?.authKey || app.authKey + + await store.save(app) + logger.info('App has been updated', { appId }) +} -// return app -// } +async function getAppById(store: DatabaseManager, appId: string): Promise { + const app = await store.get(App, { + where: { + id: appId, + }, + relations: ['channel'], + }) + return app +} diff --git a/query-node/mappings/src/content/channel.ts b/query-node/mappings/src/content/channel.ts index 2b57a8128f..a63a3dd503 100644 --- a/query-node/mappings/src/content/channel.ts +++ b/query-node/mappings/src/content/channel.ts @@ -3,7 +3,7 @@ eslint-disable @typescript-eslint/naming-convention */ import { DatabaseManager, EventContext, StoreContext, SubstrateEvent } from '@joystream/hydra-common' import { ChannelMetadata, ChannelModeratorRemarked, ChannelOwnerRemarked } from '@joystream/metadata-protobuf' -import { ChannelId, DataObjectId, MemberId } from '@joystream/types/primitives' +import { ChannelId, DataObjectId } from '@joystream/types/primitives' import { Channel, Collaborator, @@ -47,7 +47,8 @@ import { import { BTreeMap, BTreeSet, u64 } from '@polkadot/types' // Joystream types import { PalletContentIterableEnumsChannelActionPermission } from '@polkadot/types/lookup' -import { processCreateAppMessage } from './app' +import BN from 'bn.js' +import { proccessUpdateApp, processCreateAppMessage } from './app' export async function content_ChannelCreated(ctx: EventContext & StoreContext): Promise { const { store, event } = ctx @@ -394,6 +395,12 @@ async function processOwnerRemark( return {} } + if (messageType === 'updateApp') { + await proccessUpdateApp(store, channelId, decodedMessage.updateApp!) + + return {} + } + return inconsistentState('Unsupported message type in channel owner remark action', messageType) } diff --git a/tests/network-tests/src/flows/content/app.ts b/tests/network-tests/src/flows/content/app.ts index fa88409e3a..f1a024a8c9 100644 --- a/tests/network-tests/src/flows/content/app.ts +++ b/tests/network-tests/src/flows/content/app.ts @@ -1,51 +1,31 @@ import { AppMetadata } from '@joystream/metadata-protobuf' import BN from 'bn.js' import { assert } from 'chai' +import { Api } from 'src/Api' +import { QueryNodeApi } from 'src/QueryNodeApi' import { extendDebug } from '../../Debugger' import { FixtureRunner } from '../../Fixture' import { CreateChannelsAndVideosFixture, CreateContentStructureFixture, CreateMembersFixture, + IMember, } from '../../fixtures/content' import { FlowProps } from '../../Flow' import { createJoystreamCli } from '../utils' +const sufficientTopupAmount = new BN(10_000_000_000_000) + export async function createApp({ api, query }: FlowProps): Promise { const debug = extendDebug('flow:create-app') debug('Started') - const joystreamCli = await createJoystreamCli() - - // settings - const videoCount = 0 - const videoCategoryCount = 0 - const channelCount = 1 - const sufficientTopupAmount = new BN(10_000_000_000_000) // some very big number to cover fees of all transactions - - // create channel categories and video categories - const createContentStructureFixture = new CreateContentStructureFixture(api, query, joystreamCli, videoCategoryCount) - await new FixtureRunner(createContentStructureFixture).run() - - const { videoCategoryIds } = createContentStructureFixture.getCreatedItems() // create author of channels and videos const createMembersFixture = new CreateMembersFixture(api, query, 3, 0, sufficientTopupAmount) await new FixtureRunner(createMembersFixture).run() const [member] = createMembersFixture.getCreatedItems().members - // create channels and videos - const createChannelsAndVideos = new CreateChannelsAndVideosFixture( - api, - query, - joystreamCli, - channelCount, - videoCount, - videoCategoryIds[0], - member - ) - await new FixtureRunner(createChannelsAndVideos).run() - - const [channelId] = createChannelsAndVideos.getCreatedItems().channelIds + const channelId = await getChannelId(api, query, member) const newAppName = 'create_me' const newAppMetadata: Partial = { @@ -72,3 +52,95 @@ export async function createApp({ api, query }: FlowProps): Promise { debug('done') } + +export async function updateApp({ api, query }: FlowProps): Promise { + const debug = extendDebug('flow:update-app') + debug('Started') + + // create author of channels and videos + const createMembersFixture = new CreateMembersFixture(api, query, 3, 0, sufficientTopupAmount) + await new FixtureRunner(createMembersFixture).run() + const [member] = createMembersFixture.getCreatedItems().members + + const channelId = await getChannelId(api, query, member) + + const appToUpdateName = 'update_me' + const appMetadataToUpdate: Partial = { + category: 'blockchain', + oneLiner: 'best blokchain video platform', + description: 'long description', + platforms: ['web', 'mobile'], + } + + await api.createApp(member.memberId, channelId, appToUpdateName, appMetadataToUpdate) + + const apps = await query.tryQueryWithTimeout( + () => query.getAppsByName(appToUpdateName), + (appsByName) => { + assert.equal(appsByName?.[0]?.name, appToUpdateName) + assert.equal(appsByName?.[0]?.category, appMetadataToUpdate.category) + assert.equal(appsByName?.[0]?.oneLiner, appMetadataToUpdate.oneLiner) + assert.equal(appsByName?.[0]?.description, appMetadataToUpdate.description) + assert.equal(appsByName?.[0]?.termsOfService, null) + assert.equal(appsByName?.[0]?.websiteUrl, null) + assert.deepEqual(appsByName?.[0]?.platforms, appMetadataToUpdate.platforms) + } + ) + + const updatedAppData: Partial = { + category: 'sport', + oneLiner: 'best sport video platform', + description: 'short description', + websiteUrl: 'http://example.com', + platforms: ['web', 'mobile'], + } + + const newAppId = apps?.[0]?.id + if (newAppId) { + await api.updateApp(member.memberId, channelId, newAppId, updatedAppData) + } + + await query.tryQueryWithTimeout( + () => query.getAppsByName(appToUpdateName), + (appsByName) => { + assert.equal(appsByName?.[0]?.name, appToUpdateName) + assert.equal(appsByName?.[0]?.category, updatedAppData.category) + assert.equal(appsByName?.[0]?.oneLiner, updatedAppData.oneLiner) + assert.equal(appsByName?.[0]?.description, updatedAppData.description) + assert.equal(appsByName?.[0]?.termsOfService, null) + assert.equal(appsByName?.[0]?.websiteUrl, updatedAppData.websiteUrl) + assert.deepEqual(appsByName?.[0]?.platforms, updatedAppData.platforms) + } + ) + + debug('done') +} + +async function getChannelId(api: Api, query: QueryNodeApi, member: IMember) { + const videoCount = 0 + const videoCategoryCount = 0 + const channelCount = 1 + + // create channel categories and video categories + const joystreamCli = await createJoystreamCli() + const createContentStructureFixture = new CreateContentStructureFixture(api, query, joystreamCli, videoCategoryCount) + await new FixtureRunner(createContentStructureFixture).run() + + const { videoCategoryIds } = createContentStructureFixture.getCreatedItems() + + // create channels and videos + const createChannelsAndVideos = new CreateChannelsAndVideosFixture( + api, + query, + joystreamCli, + channelCount, + videoCount, + videoCategoryIds[0], + member + ) + await new FixtureRunner(createChannelsAndVideos).run() + + const [channelId] = createChannelsAndVideos.getCreatedItems().channelIds + + return channelId +} diff --git a/tests/network-tests/src/flows/membership/updateApp.ts b/tests/network-tests/src/flows/membership/updateApp.ts deleted file mode 100644 index 624817e5a5..0000000000 --- a/tests/network-tests/src/flows/membership/updateApp.ts +++ /dev/null @@ -1,63 +0,0 @@ -import BN from 'bn.js' -import { extendDebug } from '../../Debugger' -import { FlowProps } from '../../Flow' -import { FixtureRunner } from '../../Fixture' -import { CreateMembersFixture } from '../../fixtures/content' -import { assert } from 'chai' -import { AppMetadata } from '@joystream/metadata-protobuf' - -export async function updateApp({ api, query }: FlowProps): Promise { - const debug = extendDebug('flow:update-app') - debug('Started') - - const createMembersFixture = new CreateMembersFixture(api, query, 1, 0, new BN(10_000_000_000)) - await new FixtureRunner(createMembersFixture).run() - const [member] = createMembersFixture.getCreatedItems().members - - const newAppName = 'update_me' - const newAppMetadata: Partial = { - category: 'blockchain', - oneLiner: 'best blockchain video platform', - description: 'long description', - platforms: ['web', 'mobile'], - useUri: 'http://example.com', - } - - await api.createApp(member.memberId, newAppName, newAppMetadata) - - const apps = await query.tryQueryWithTimeout( - () => query.getAppsByName(newAppName), - (appsByName) => { - assert.equal(appsByName?.[0]?.name, newAppName) - assert.equal(appsByName?.[0]?.oneLiner, newAppMetadata.oneLiner) - assert.equal(appsByName?.[0]?.description, newAppMetadata.description) - assert.deepEqual(appsByName?.[0]?.platforms, newAppMetadata.platforms) - } - ) - - const updatedMetadata: Partial = { - description: 'updated description', - oneLiner: 'updated one liner', - platforms: [], - } - - const newAppId = apps?.[0]?.id - if (newAppId) { - await api.updateApp(member.memberId, newAppId, updatedMetadata) - } - - await query.tryQueryWithTimeout( - () => query.getAppsByName(newAppName), - (appsByName) => { - assert.equal(appsByName?.[0]?.id, newAppId) - assert.equal(appsByName?.[0]?.name, newAppName) - assert.equal(appsByName?.[0]?.useUri, newAppMetadata.useUri) - assert.equal(appsByName?.[0]?.category, newAppMetadata.category) - assert.equal(appsByName?.[0]?.description, updatedMetadata.description) - assert.equal(appsByName?.[0]?.oneLiner, updatedMetadata.oneLiner) - assert.deepEqual(appsByName?.[0]?.platforms, updatedMetadata.platforms) - } - ) - - debug('done') -} diff --git a/tests/network-tests/src/scenarios/app.ts b/tests/network-tests/src/scenarios/app.ts index 327af8f215..4ce844b8bc 100644 --- a/tests/network-tests/src/scenarios/app.ts +++ b/tests/network-tests/src/scenarios/app.ts @@ -1,4 +1,4 @@ -import { createApp } from '../flows/content/app' +import { createApp, updateApp } from '../flows/content/app' import initFaucet from '../flows/faucet/initFaucet' import leaderSetup from '../flows/working-groups/leadOpening' import { scenario } from '../Scenario' @@ -9,4 +9,5 @@ scenario('App', async ({ job }) => { const leads = job('Set WorkingGroup Leads', leaderSetup(true)) job('Create app', createApp).after(leads) + job('Update app', updateApp).after(leads) }) From 0f9a22376d2beb4cdb035e7d2ea7aac4418eb35b Mon Sep 17 00:00:00 2001 From: drillprop Date: Thu, 19 Jan 2023 11:26:11 +0100 Subject: [PATCH 13/84] add new scenarios to full scenario --- tests/network-tests/src/scenarios/full.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/network-tests/src/scenarios/full.ts b/tests/network-tests/src/scenarios/full.ts index 1aca727d80..620004a1b3 100644 --- a/tests/network-tests/src/scenarios/full.ts +++ b/tests/network-tests/src/scenarios/full.ts @@ -37,6 +37,7 @@ import updatingVerificationStatus from '../flows/membership/updateVerificationSt import commentsAndReactions from '../flows/content/commentsAndReactions' import addAndUpdateVideoSubtitles from '../flows/content/videoSubtitles' import { testVideoCategories } from '../flows/content/videoCategories' +import { createApp, updateApp } from '../flows/content/app' // eslint-disable-next-line @typescript-eslint/no-floating-promises scenario('Full', async ({ job, env }) => { @@ -110,6 +111,8 @@ scenario('Full', async ({ job, env }) => { const commentsAndReactionsJob = job('video comments and reactions', commentsAndReactions).after( nftAuctionAndOffersJob ) + job('create app', createApp).after(sudoHireLead) + job('update app', updateApp).after(sudoHireLead) const contentDirectoryJob = commentsAndReactionsJob // keep updated to last job above From d276eac59380dd7275c24c3acc5b275a53aac834 Mon Sep 17 00:00:00 2001 From: drillprop Date: Thu, 19 Jan 2023 11:32:23 +0100 Subject: [PATCH 14/84] switch back retries to 6 --- tests/network-tests/src/QueryNodeApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/network-tests/src/QueryNodeApi.ts b/tests/network-tests/src/QueryNodeApi.ts index ff182ab3f7..cf503f74ff 100644 --- a/tests/network-tests/src/QueryNodeApi.ts +++ b/tests/network-tests/src/QueryNodeApi.ts @@ -455,7 +455,7 @@ export class QueryNodeApi { query: () => Promise, assertResultIsValid: (res: QueryResultT) => void, retryTimeMs = BLOCKTIME * 9, - retries = 20 + retries = 6 ): Promise { const label = query.toString().replace(/^.*\.([A-za-z0-9]+\(.*\))$/g, '$1') const debug = this.tryDebug.extend(label) From 943db9999729c187d3baf4c01f92117d875be777 Mon Sep 17 00:00:00 2001 From: drillprop Date: Fri, 20 Jan 2023 15:10:52 +0100 Subject: [PATCH 15/84] cr fixes --- query-node/mappings/src/content/app.ts | 7 +------ query-node/mappings/src/content/channel.ts | 5 ++--- query-node/schemas/content.graphql | 12 ++++++++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/query-node/mappings/src/content/app.ts b/query-node/mappings/src/content/app.ts index 2821f6db5e..f42307fd2a 100644 --- a/query-node/mappings/src/content/app.ts +++ b/query-node/mappings/src/content/app.ts @@ -54,18 +54,13 @@ export async function processCreateAppMessage( }) await store.save(newApp) logger.info('App has been created', { name }) - - channel.app = newApp - - await store.save(channel) - logger.info('Channel has been updated', { channel }) } async function createAppId(event: SubstrateEvent): Promise { return `${event.blockNumber}-${event.indexInBlock}` } -export async function proccessUpdateApp( +export async function processUpdateApp( store: DatabaseManager, channelId: ChannelId, message: IUpdateApp diff --git a/query-node/mappings/src/content/channel.ts b/query-node/mappings/src/content/channel.ts index a63a3dd503..072262cc06 100644 --- a/query-node/mappings/src/content/channel.ts +++ b/query-node/mappings/src/content/channel.ts @@ -18,7 +18,6 @@ import { ChannelAssetsDeletedByModeratorEvent, ChannelDeletedByModeratorEvent, ChannelVisibilitySetByModeratorEvent, - App, } from 'query-node/dist/model' import { In } from 'typeorm' import { Content } from '../../generated/types' @@ -48,7 +47,7 @@ import { BTreeMap, BTreeSet, u64 } from '@polkadot/types' // Joystream types import { PalletContentIterableEnumsChannelActionPermission } from '@polkadot/types/lookup' import BN from 'bn.js' -import { proccessUpdateApp, processCreateAppMessage } from './app' +import { processUpdateApp, processCreateAppMessage } from './app' export async function content_ChannelCreated(ctx: EventContext & StoreContext): Promise { const { store, event } = ctx @@ -396,7 +395,7 @@ async function processOwnerRemark( } if (messageType === 'updateApp') { - await proccessUpdateApp(store, channelId, decodedMessage.updateApp!) + await processUpdateApp(store, channelId, decodedMessage.updateApp!) return {} } diff --git a/query-node/schemas/content.graphql b/query-node/schemas/content.graphql index 82d789ab48..ce38af3fa1 100644 --- a/query-node/schemas/content.graphql +++ b/query-node/schemas/content.graphql @@ -9,22 +9,26 @@ type Language @entity { } type App @entity { + "Runtime entity identifier (EntityId)" id: ID! + "The name of the App" name: String! + "Member owning the App (if any)" ownerMember: Membership + "Curator group owning the channel (if any)" ownerCuratorGroup: CuratorGroup - # Url where user can read more about the project or company for this app + "Url where user can read more about the project or company for this app" websiteUrl: String - # Url to the app + "Url to the app" useUri: String - # what should I to do with icons? smallIcon: String mediumIcon: String bigIcon: String + "Tagline of the app" oneLiner: String description: String termsOfService: String - # List of platforms on which the app will be available, e.g. [mobile, web, native] + "List of platforms on which the app will be available, e.g. [mobile, web, native]" platforms: [String] category: String authKey: String From 31f2b57014aa5482de0e476a4c851f629eaa03fc Mon Sep 17 00:00:00 2001 From: drillprop Date: Tue, 31 Jan 2023 15:31:43 +0100 Subject: [PATCH 16/84] fix upate app, update tests, fix schema --- query-node/mappings/src/content/app.ts | 27 +++++++++++-------- query-node/mappings/src/content/channel.ts | 1 - tests/network-tests/src/flows/content/app.ts | 4 +-- .../src/graphql/generated/schema.ts | 5 ++++ 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/query-node/mappings/src/content/app.ts b/query-node/mappings/src/content/app.ts index f42307fd2a..d60869f875 100644 --- a/query-node/mappings/src/content/app.ts +++ b/query-node/mappings/src/content/app.ts @@ -1,5 +1,6 @@ import { DatabaseManager, SubstrateEvent } from '@joystream/hydra-common' import { ICreateApp, IUpdateApp } from '@joystream/metadata-protobuf' +import { integrateMeta } from '@joystream/metadata-protobuf/utils' import { ChannelId } from '@joystream/types/primitives' import { logger } from '@joystream/warthog' import { Channel, App } from 'query-node/dist/model' @@ -76,17 +77,21 @@ export async function processUpdateApp( inconsistentState(`Cannot update app; app does not belong to the channelId: `, channelId) } - app.websiteUrl = appMetadata?.websiteUrl || app.websiteUrl - app.useUri = appMetadata?.useUri || app.useUri - app.smallIcon = appMetadata?.smallIcon || app.smallIcon - app.mediumIcon = appMetadata?.mediumIcon || app.mediumIcon - app.bigIcon = appMetadata?.bigIcon || app.bigIcon - app.oneLiner = appMetadata?.oneLiner || app.oneLiner - app.description = appMetadata?.description || app.description - app.termsOfService = appMetadata?.termsOfService || app.termsOfService - app.platforms = appMetadata?.platforms || app.platforms - app.category = appMetadata?.category || app.category - app.authKey = appMetadata?.authKey || app.authKey + if (appMetadata) { + integrateMeta(app, appMetadata, [ + 'websiteUrl', + 'useUri', + 'smallIcon', + 'mediumIcon', + 'bigIcon', + 'oneLiner', + 'description', + 'termsOfService', + 'platforms', + 'category', + 'authKey', + ]) + } await store.save(app) logger.info('App has been updated', { appId }) diff --git a/query-node/mappings/src/content/channel.ts b/query-node/mappings/src/content/channel.ts index 072262cc06..acbd120eb0 100644 --- a/query-node/mappings/src/content/channel.ts +++ b/query-node/mappings/src/content/channel.ts @@ -46,7 +46,6 @@ import { import { BTreeMap, BTreeSet, u64 } from '@polkadot/types' // Joystream types import { PalletContentIterableEnumsChannelActionPermission } from '@polkadot/types/lookup' -import BN from 'bn.js' import { processUpdateApp, processCreateAppMessage } from './app' export async function content_ChannelCreated(ctx: EventContext & StoreContext): Promise { diff --git a/tests/network-tests/src/flows/content/app.ts b/tests/network-tests/src/flows/content/app.ts index f1a024a8c9..4007fbe1ab 100644 --- a/tests/network-tests/src/flows/content/app.ts +++ b/tests/network-tests/src/flows/content/app.ts @@ -90,7 +90,7 @@ export async function updateApp({ api, query }: FlowProps): Promise { const updatedAppData: Partial = { category: 'sport', oneLiner: 'best sport video platform', - description: 'short description', + description: '', websiteUrl: 'http://example.com', platforms: ['web', 'mobile'], } @@ -107,7 +107,7 @@ export async function updateApp({ api, query }: FlowProps): Promise { assert.equal(appsByName?.[0]?.category, updatedAppData.category) assert.equal(appsByName?.[0]?.oneLiner, updatedAppData.oneLiner) assert.equal(appsByName?.[0]?.description, updatedAppData.description) - assert.equal(appsByName?.[0]?.termsOfService, null) + assert.equal(appsByName?.[0]?.termsOfService, '') assert.equal(appsByName?.[0]?.websiteUrl, updatedAppData.websiteUrl) assert.deepEqual(appsByName?.[0]?.platforms, updatedAppData.platforms) } diff --git a/tests/network-tests/src/graphql/generated/schema.ts b/tests/network-tests/src/graphql/generated/schema.ts index 355d1b1e15..47e5346ff4 100644 --- a/tests/network-tests/src/graphql/generated/schema.ts +++ b/tests/network-tests/src/graphql/generated/schema.ts @@ -150,19 +150,24 @@ export type App = BaseGraphQlObject & { deletedAt?: Maybe deletedById?: Maybe version: Scalars['Int'] + /** The name of the App */ name: Scalars['String'] ownerMember?: Maybe ownerMemberId?: Maybe ownerCuratorGroup?: Maybe ownerCuratorGroupId?: Maybe + /** Url where user can read more about the project or company for this app */ websiteUrl?: Maybe + /** Url to the app */ useUri?: Maybe smallIcon?: Maybe mediumIcon?: Maybe bigIcon?: Maybe + /** Tagline of the app */ oneLiner?: Maybe description?: Maybe termsOfService?: Maybe + /** List of platforms on which the app will be available, e.g. [mobile, web, native] */ platforms?: Maybe> category?: Maybe authKey?: Maybe From b2665dab1b808c6d50a62539a8191ae968070fdd Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Thu, 12 Jan 2023 11:52:55 +0100 Subject: [PATCH 17/84] Protobuf changes --- metadata-protobuf/proto/App.proto | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 metadata-protobuf/proto/App.proto diff --git a/metadata-protobuf/proto/App.proto b/metadata-protobuf/proto/App.proto new file mode 100644 index 0000000000..b7679456ca --- /dev/null +++ b/metadata-protobuf/proto/App.proto @@ -0,0 +1,20 @@ +syntax = "proto2"; + +import "proto/Video.proto"; +import "proto/Channel.proto"; + +message AppAction { + // ID of application + required string app_id = 1; + + // Metadata + optional bytes metadata = 2; + + // Raw metadata of wrapped action + oneof raw_action { + ContentMetadata content_metadata = 3; + ChannelMetadata channel_metadata = 4; + } + + required string signature = 5; +} From 1aafb6e08d61a029cef36d43244aba7f3610b65e Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Fri, 13 Jan 2023 14:33:15 +0100 Subject: [PATCH 18/84] Initial setup for appactions --- metadata-protobuf/proto/App.proto | 10 ++- query-node/mappings/src/content/channel.ts | 14 ++++- query-node/mappings/src/content/utils.ts | 71 ++++++++++++++++++++++ query-node/mappings/src/content/video.ts | 17 ++++-- query-node/schemas/content.graphql | 6 ++ 5 files changed, 106 insertions(+), 12 deletions(-) diff --git a/metadata-protobuf/proto/App.proto b/metadata-protobuf/proto/App.proto index b7679456ca..31198b5084 100644 --- a/metadata-protobuf/proto/App.proto +++ b/metadata-protobuf/proto/App.proto @@ -3,12 +3,16 @@ syntax = "proto2"; import "proto/Video.proto"; import "proto/Channel.proto"; +message AppActionMetadata { + optional string nonceId = 1; +} + message AppAction { // ID of application - required string app_id = 1; + optional string app_id = 1; // Metadata - optional bytes metadata = 2; + optional AppActionMetadata metadata = 2; // Raw metadata of wrapped action oneof raw_action { @@ -16,5 +20,5 @@ message AppAction { ChannelMetadata channel_metadata = 4; } - required string signature = 5; + optional string signature = 5; } diff --git a/query-node/mappings/src/content/channel.ts b/query-node/mappings/src/content/channel.ts index acbd120eb0..a891597a67 100644 --- a/query-node/mappings/src/content/channel.ts +++ b/query-node/mappings/src/content/channel.ts @@ -2,7 +2,12 @@ eslint-disable @typescript-eslint/naming-convention */ import { DatabaseManager, EventContext, StoreContext, SubstrateEvent } from '@joystream/hydra-common' -import { ChannelMetadata, ChannelModeratorRemarked, ChannelOwnerRemarked } from '@joystream/metadata-protobuf' +import { + AppAction, + ChannelMetadata, + ChannelModeratorRemarked, + ChannelOwnerRemarked, +} from '@joystream/metadata-protobuf' import { ChannelId, DataObjectId } from '@joystream/types/primitives' import { Channel, @@ -42,6 +47,7 @@ import { processChannelMetadata, unsetAssetRelations, mapAgentPermission, + processAppActionMetadata, } from './utils' import { BTreeMap, BTreeSet, u64 } from '@polkadot/types' // Joystream types @@ -78,8 +84,10 @@ export async function content_ChannelCreated(ctx: EventContext & StoreContext): inconsistentState(`storageBag for channel ${channelId} does not exist`) } - const metadata = deserializeMetadata(ChannelMetadata, channelCreationParameters.meta.unwrap()) || {} - await processChannelMetadata(ctx, channel, metadata, dataObjects) + const appAction = deserializeMetadata(AppAction, channelCreationParameters.meta.unwrap()) || {} + await processAppActionMetadata(ctx, channel, channel.ownerMember?.id, appAction, (entity: Channel) => + processChannelMetadata(ctx, entity, appAction.channelMetadata ?? {}, dataObjects) + ) } // save entity diff --git a/query-node/mappings/src/content/utils.ts b/query-node/mappings/src/content/utils.ts index 662c472e59..949b18ed25 100644 --- a/query-node/mappings/src/content/utils.ts +++ b/query-node/mappings/src/content/utils.ts @@ -6,8 +6,10 @@ import { IMediaType, IChannelMetadata, ISubtitleMetadata, + IAppAction, } from '@joystream/metadata-protobuf' import { integrateMeta, isSet, isValidLanguageCode } from '@joystream/metadata-protobuf/utils' +import { ed25519Verify } from '@polkadot/util-crypto' import { invalidMetadata, inconsistentState, logger, deterministicEntityId, EntityType } from '../common' import { // primary entities @@ -166,6 +168,75 @@ async function processVideoSubtitleAssets( } } +async function checkAppActionNonce( + ctx: EventContext & StoreContext, + entity: T, + actionOwnerId: string | undefined, + appAction: DecodedMetadataObject +): Promise { + if (!appAction.metadata?.nonceId || !actionOwnerId) { + return false + } + + if (appAction.contentMetadata?.videoMetadata) { + return ( + (await ctx.store.getMany(Video, { where: { channel: { ownerMember: { id: actionOwnerId } } } })).length === + parseInt(appAction.metadata.nonceId) + ) + } + + if (appAction.channelMetadata) { + return ( + (await ctx.store.getMany(Channel, { where: { ownerMember: { id: actionOwnerId } } })).length === + parseInt(appAction.metadata.nonceId) + ) + } + + return false +} + +async function validateAppSignature( + ctx: EventContext & StoreContext, + entity: T, + actionOwnerId: string | undefined, + appAction: DecodedMetadataObject +) { + // If one is missing we cannot verify the signature + if (!appAction.appId || !appAction.signature || !appAction.metadata) { + return false + } + + // todo change to App after https://github.com/Joystream/joystream/pull/4504 + const app = await ctx.store.get(Channel, { where: { id: appAction.appId } }) + + if (!app || !(await checkAppActionNonce(ctx, entity, actionOwnerId, appAction))) { + return false + } + + // todo create commitment based on the metadata + const appCommitment = 'commitment' + + // todo: change to app.authKey after https://github.com/Joystream/joystream/pull/4504 + return ed25519Verify(appCommitment, appAction.signature, app.title ?? '') +} + +export async function processAppActionMetadata( + ctx: EventContext & StoreContext, + entity: T, + actionOwnerId: string | undefined, + meta: DecodedMetadataObject, + entityMetadataProcessor: (entity: T) => Promise +): Promise { + if (!(await validateAppSignature(ctx, entity, actionOwnerId, meta))) { + invalidMetadata('Invalid application signature') + return entityMetadataProcessor(entity) + } + + integrateMeta(entity, meta, ['appId']) + + return entityMetadataProcessor(entity) +} + export async function processChannelMetadata( ctx: EventContext & StoreContext, channel: Channel, diff --git a/query-node/mappings/src/content/video.ts b/query-node/mappings/src/content/video.ts index 90956b9e87..3b5d5bf9ea 100644 --- a/query-node/mappings/src/content/video.ts +++ b/query-node/mappings/src/content/video.ts @@ -2,7 +2,7 @@ eslint-disable @typescript-eslint/naming-convention */ import { DatabaseManager, EventContext, StoreContext } from '@joystream/hydra-common' -import { ContentMetadata, IVideoMetadata } from '@joystream/metadata-protobuf' +import { AppAction, ContentMetadata, IAppAction, IVideoMetadata } from '@joystream/metadata-protobuf' import { ChannelId, DataObjectId, VideoId } from '@joystream/types/primitives' import { PalletContentPermissionsContentActor as ContentActor, @@ -43,6 +43,7 @@ import { createNft } from './nft' import { convertContentActor, convertContentActorToChannelOrNftOwner, + processAppActionMetadata, processVideoMetadata, unsetAssetRelations, videoRelationsForCounters, @@ -93,19 +94,20 @@ export async function content_ContentCreated(ctx: EventContext & StoreContext): } // deserialize & process metadata - const contentMetadata = meta.isSome ? deserializeMetadata(ContentMetadata, meta.unwrap()) : undefined + const appAction = meta.isSome ? deserializeMetadata(AppAction, meta.unwrap()) : undefined + // const contentMetadata = meta.isSome ? deserializeMetadata(ContentMetadata, meta.unwrap()) : undefined // Content Creation Preference // 1. metadata == `VideoMetadata` || undefined -> create Video // 1. metadata == `PlaylistMetadata` -> create Playlist (Not Supported Yet) - await processCreateVideoMessage(ctx, channel, contentMetadata?.videoMetadata || undefined, contentCreatedEventData) + await processCreateVideoMessage(ctx, channel, appAction ?? undefined, contentCreatedEventData) } export async function processCreateVideoMessage( ctx: EventContext & StoreContext, channel: Channel, - metadata: DecodedMetadataObject | undefined, + appAction: DecodedMetadataObject | undefined, contentCreatedEventData: ContentCreatedEventData ): Promise { const { store, event } = ctx @@ -123,8 +125,11 @@ export async function processCreateVideoMessage( reactionsCount: 0, }) - if (metadata) { - await processVideoMetadata(ctx, video, metadata, newDataObjectIds) + if (appAction) { + const videoMetadata = appAction.contentMetadata?.videoMetadata ?? {} + await processAppActionMetadata(ctx, video, appAction, (entity) => + processVideoMetadata(ctx, entity, videoMetadata, newDataObjectIds) + ) } // save video diff --git a/query-node/schemas/content.graphql b/query-node/schemas/content.graphql index ce38af3fa1..28d2939227 100644 --- a/query-node/schemas/content.graphql +++ b/query-node/schemas/content.graphql @@ -39,6 +39,9 @@ type Channel @entity { "Runtime entity identifier (EntityId)" id: ID! + "Entity indentifier for Application used for channel creation" + appId: ID + "Member owning the channel (if any)" ownerMember: Membership @@ -125,6 +128,9 @@ type Video @entity { "Runtime identifier" id: ID! + "Entity indentifier for Application used for channel creation" + appId: ID + "Reference to videos's channel" channel: Channel! From 706c673710e2d82c802dc601fbfcdb633f0dd281 Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Mon, 23 Jan 2023 08:03:26 +0100 Subject: [PATCH 19/84] Add app commitment check --- query-node/mappings/src/content/channel.ts | 27 ++++++++++---- query-node/mappings/src/content/utils.ts | 41 ++++++++++++++++------ query-node/mappings/src/content/video.ts | 20 +++++++++-- 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/query-node/mappings/src/content/channel.ts b/query-node/mappings/src/content/channel.ts index a891597a67..385e49f46d 100644 --- a/query-node/mappings/src/content/channel.ts +++ b/query-node/mappings/src/content/channel.ts @@ -4,6 +4,7 @@ eslint-disable @typescript-eslint/naming-convention import { DatabaseManager, EventContext, StoreContext, SubstrateEvent } from '@joystream/hydra-common' import { AppAction, + AppActionMetadata, ChannelMetadata, ChannelModeratorRemarked, ChannelOwnerRemarked, @@ -48,6 +49,8 @@ import { unsetAssetRelations, mapAgentPermission, processAppActionMetadata, + generateAppActionCommitment, + wrapMetadata, } from './utils' import { BTreeMap, BTreeSet, u64 } from '@polkadot/types' // Joystream types @@ -60,6 +63,9 @@ export async function content_ChannelCreated(ctx: EventContext & StoreContext): const [channelId, { owner, dataObjects, channelStateBloatBond }, channelCreationParameters, rewardAccount] = new Content.ChannelCreatedEvent(event).params + // prepare channel owner (handles fields `ownerMember` and `ownerCuratorGroup`) + const channelOwner = await convertChannelOwnerToMemberOrCuratorGroup(store, owner) + // create entity const channel = new Channel({ // main data @@ -68,10 +74,7 @@ export async function content_ChannelCreated(ctx: EventContext & StoreContext): videos: [], createdInBlock: event.blockNumber, activeVideosCounter: 0, - - // prepare channel owner (handles fields `ownerMember` and `ownerCuratorGroup`) - ...(await convertChannelOwnerToMemberOrCuratorGroup(store, owner)), - + ...channelOwner, rewardAccount: rewardAccount.toString(), channelStateBloatBond: channelStateBloatBond.amount, }) @@ -85,8 +88,20 @@ export async function content_ChannelCreated(ctx: EventContext & StoreContext): } const appAction = deserializeMetadata(AppAction, channelCreationParameters.meta.unwrap()) || {} - await processAppActionMetadata(ctx, channel, channel.ownerMember?.id, appAction, (entity: Channel) => - processChannelMetadata(ctx, entity, appAction.channelMetadata ?? {}, dataObjects) + + const appCommitment = generateAppActionCommitment( + channelOwner.ownerMember?.id ?? channelOwner.ownerCuratorGroup?.id ?? '', + channelCreationParameters.assets, + // after ephesus merge can be replaced with `metadataToBytes` helper fn from joystream/js + wrapMetadata(ChannelMetadata.encode(appAction.channelMetadata ?? {}).finish()), + wrapMetadata(AppActionMetadata.encode(appAction.metadata ?? {}).finish()) + ) + await processAppActionMetadata( + ctx, + channel, + appAction, + { actionOwnerId: channel.ownerMember?.id, appCommitment }, + (entity: Channel) => processChannelMetadata(ctx, entity, appAction.channelMetadata ?? {}, dataObjects) ) } diff --git a/query-node/mappings/src/content/utils.ts b/query-node/mappings/src/content/utils.ts index 949b18ed25..c99a944042 100644 --- a/query-node/mappings/src/content/utils.ts +++ b/query-node/mappings/src/content/utils.ts @@ -42,13 +42,15 @@ import { PalletContentChannelOwner as ChannelOwner, PalletContentPermissionsContentActor as ContentActor, PalletContentIterableEnumsChannelActionPermission, + PalletContentStorageAssetsRecord, } from '@polkadot/types/lookup' import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types' import BN from 'bn.js' import _ from 'lodash' import { getSortedDataObjectsByIds } from '../storage/utils' -import { BTreeSet } from '@polkadot/types' +import { BTreeSet, Bytes, Option } from '@polkadot/types' import { DataObjectId } from '@joystream/types/primitives' +import { createType } from '@joystream/types' const ASSET_TYPES = { channel: [ @@ -198,36 +200,39 @@ async function checkAppActionNonce( async function validateAppSignature( ctx: EventContext & StoreContext, entity: T, - actionOwnerId: string | undefined, + validationContext: { + actionOwnerId: string | undefined + appCommitment: string | undefined + }, appAction: DecodedMetadataObject ) { // If one is missing we cannot verify the signature - if (!appAction.appId || !appAction.signature || !appAction.metadata) { + if (!appAction.appId || !appAction.signature || !appAction.metadata || !validationContext.appCommitment) { return false } // todo change to App after https://github.com/Joystream/joystream/pull/4504 const app = await ctx.store.get(Channel, { where: { id: appAction.appId } }) - if (!app || !(await checkAppActionNonce(ctx, entity, actionOwnerId, appAction))) { + if (!app || !(await checkAppActionNonce(ctx, entity, validationContext.actionOwnerId, appAction))) { return false } - // todo create commitment based on the metadata - const appCommitment = 'commitment' - // todo: change to app.authKey after https://github.com/Joystream/joystream/pull/4504 - return ed25519Verify(appCommitment, appAction.signature, app.title ?? '') + return ed25519Verify(validationContext.appCommitment, appAction.signature, app.title ?? '') } export async function processAppActionMetadata( ctx: EventContext & StoreContext, entity: T, - actionOwnerId: string | undefined, meta: DecodedMetadataObject, + validationContext: { + actionOwnerId: string | undefined + appCommitment: string | undefined + }, entityMetadataProcessor: (entity: T) => Promise ): Promise { - if (!(await validateAppSignature(ctx, entity, actionOwnerId, meta))) { + if (!(await validateAppSignature(ctx, entity, validationContext, meta))) { invalidMetadata('Invalid application signature') return entityMetadataProcessor(entity) } @@ -759,3 +764,19 @@ export async function unsetAssetRelations(store: DatabaseManager, dataObject: St export function mapAgentPermission(permission: PalletContentIterableEnumsChannelActionPermission): string { return permission.toString() } + +// this ideally should be transfered to @joystream-js +export function generateAppActionCommitment( + creatorId: string, + assets: Option, + rawAction: Option, + rawAppMetadata: Option +): string { + return JSON.stringify([creatorId, assets.toString(), rawAction, rawAppMetadata]) +} + +export const wrapMetadata = (metadata: Uint8Array): Option => { + const metadataRaw = createType('Raw', metadata) + const metadataBytes = createType('Bytes', metadataRaw) + return createType('Option', metadataBytes) +} diff --git a/query-node/mappings/src/content/video.ts b/query-node/mappings/src/content/video.ts index 3b5d5bf9ea..61e5f75683 100644 --- a/query-node/mappings/src/content/video.ts +++ b/query-node/mappings/src/content/video.ts @@ -2,7 +2,14 @@ eslint-disable @typescript-eslint/naming-convention */ import { DatabaseManager, EventContext, StoreContext } from '@joystream/hydra-common' -import { AppAction, ContentMetadata, IAppAction, IVideoMetadata } from '@joystream/metadata-protobuf' +import { + AppAction, + AppActionMetadata, + ContentMetadata, + IAppAction, + IVideoMetadata, + VideoMetadata, +} from '@joystream/metadata-protobuf' import { ChannelId, DataObjectId, VideoId } from '@joystream/types/primitives' import { PalletContentPermissionsContentActor as ContentActor, @@ -43,10 +50,12 @@ import { createNft } from './nft' import { convertContentActor, convertContentActorToChannelOrNftOwner, + generateAppActionCommitment, processAppActionMetadata, processVideoMetadata, unsetAssetRelations, videoRelationsForCounters, + wrapMetadata, } from './utils' import { BTreeSet } from '@polkadot/types' @@ -127,7 +136,14 @@ export async function processCreateVideoMessage( if (appAction) { const videoMetadata = appAction.contentMetadata?.videoMetadata ?? {} - await processAppActionMetadata(ctx, video, appAction, (entity) => + const appCommitment = generateAppActionCommitment( + channel.id ?? '', + contentCreationParameters.assets, + // after ephesus merge can be replaced with `metadataToBytes` helper fn from joystream/js + wrapMetadata(VideoMetadata.encode(videoMetadata as IVideoMetadata).finish()), + wrapMetadata(AppActionMetadata.encode(appAction.metadata ?? {}).finish()) + ) + await processAppActionMetadata(ctx, video, appAction, { actionOwnerId: channel.id, appCommitment }, (entity) => processVideoMetadata(ctx, entity, videoMetadata, newDataObjectIds) ) } From b2b5d08540b8c70decf5f16f7b5137c7ed710c5c Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Mon, 23 Jan 2023 08:41:37 +0100 Subject: [PATCH 20/84] Adapt changes to ephesus + app creation PR --- joystreamjs/src/utils/serialization.ts | 0 query-node/mappings/src/content/app.ts | 2 +- query-node/mappings/src/content/channel.ts | 12 ++++----- query-node/mappings/src/content/utils.ts | 29 ++++------------------ query-node/mappings/src/content/video.ts | 26 ++++++++----------- 5 files changed, 22 insertions(+), 47 deletions(-) create mode 100644 joystreamjs/src/utils/serialization.ts diff --git a/joystreamjs/src/utils/serialization.ts b/joystreamjs/src/utils/serialization.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/query-node/mappings/src/content/app.ts b/query-node/mappings/src/content/app.ts index d60869f875..9e295b37c1 100644 --- a/query-node/mappings/src/content/app.ts +++ b/query-node/mappings/src/content/app.ts @@ -97,7 +97,7 @@ export async function processUpdateApp( logger.info('App has been updated', { appId }) } -async function getAppById(store: DatabaseManager, appId: string): Promise { +export async function getAppById(store: DatabaseManager, appId: string): Promise { const app = await store.get(App, { where: { id: appId, diff --git a/query-node/mappings/src/content/channel.ts b/query-node/mappings/src/content/channel.ts index 385e49f46d..fd14212125 100644 --- a/query-node/mappings/src/content/channel.ts +++ b/query-node/mappings/src/content/channel.ts @@ -49,13 +49,12 @@ import { unsetAssetRelations, mapAgentPermission, processAppActionMetadata, - generateAppActionCommitment, - wrapMetadata, } from './utils' import { BTreeMap, BTreeSet, u64 } from '@polkadot/types' // Joystream types import { PalletContentIterableEnumsChannelActionPermission } from '@polkadot/types/lookup' import { processUpdateApp, processCreateAppMessage } from './app' +import { generateAppActionCommitment, metadataToBytes } from '@joystream/js/lib/utils' export async function content_ChannelCreated(ctx: EventContext & StoreContext): Promise { const { store, event } = ctx @@ -92,9 +91,8 @@ export async function content_ChannelCreated(ctx: EventContext & StoreContext): const appCommitment = generateAppActionCommitment( channelOwner.ownerMember?.id ?? channelOwner.ownerCuratorGroup?.id ?? '', channelCreationParameters.assets, - // after ephesus merge can be replaced with `metadataToBytes` helper fn from joystream/js - wrapMetadata(ChannelMetadata.encode(appAction.channelMetadata ?? {}).finish()), - wrapMetadata(AppActionMetadata.encode(appAction.metadata ?? {}).finish()) + metadataToBytes(ChannelMetadata, appAction.channelMetadata ?? {}), + metadataToBytes(AppActionMetadata, appAction.metadata ?? {}) ) await processAppActionMetadata( ctx, @@ -141,8 +139,8 @@ export async function content_ChannelUpdated(ctx: EventContext & StoreContext): inconsistentState(`storageBag for channel ${channelId} does not exist`) } - const newMetadata = deserializeMetadata(ChannelMetadata, newMetadataBytes) || {} - await processChannelMetadata(ctx, channel, newMetadata, newDataObjects) + const newMetadata = deserializeMetadata(AppAction, newMetadataBytes) || {} + await processChannelMetadata(ctx, channel, newMetadata.channelMetadata ?? {}, newDataObjects) } // save channel diff --git a/query-node/mappings/src/content/utils.ts b/query-node/mappings/src/content/utils.ts index c99a944042..b6b8929cdd 100644 --- a/query-node/mappings/src/content/utils.ts +++ b/query-node/mappings/src/content/utils.ts @@ -42,15 +42,14 @@ import { PalletContentChannelOwner as ChannelOwner, PalletContentPermissionsContentActor as ContentActor, PalletContentIterableEnumsChannelActionPermission, - PalletContentStorageAssetsRecord, } from '@polkadot/types/lookup' import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types' import BN from 'bn.js' import _ from 'lodash' import { getSortedDataObjectsByIds } from '../storage/utils' -import { BTreeSet, Bytes, Option } from '@polkadot/types' +import { BTreeSet } from '@polkadot/types' import { DataObjectId } from '@joystream/types/primitives' -import { createType } from '@joystream/types' +import { getAppById } from './app' const ASSET_TYPES = { channel: [ @@ -211,15 +210,13 @@ async function validateAppSignature( return false } - // todo change to App after https://github.com/Joystream/joystream/pull/4504 - const app = await ctx.store.get(Channel, { where: { id: appAction.appId } }) + const app = await getAppById(ctx.store, appAction.appId) - if (!app || !(await checkAppActionNonce(ctx, entity, validationContext.actionOwnerId, appAction))) { + if (!app || !app.authKey || !(await checkAppActionNonce(ctx, entity, validationContext.actionOwnerId, appAction))) { return false } - // todo: change to app.authKey after https://github.com/Joystream/joystream/pull/4504 - return ed25519Verify(validationContext.appCommitment, appAction.signature, app.title ?? '') + return ed25519Verify(validationContext.appCommitment, appAction.signature, app.authKey) } export async function processAppActionMetadata( @@ -764,19 +761,3 @@ export async function unsetAssetRelations(store: DatabaseManager, dataObject: St export function mapAgentPermission(permission: PalletContentIterableEnumsChannelActionPermission): string { return permission.toString() } - -// this ideally should be transfered to @joystream-js -export function generateAppActionCommitment( - creatorId: string, - assets: Option, - rawAction: Option, - rawAppMetadata: Option -): string { - return JSON.stringify([creatorId, assets.toString(), rawAction, rawAppMetadata]) -} - -export const wrapMetadata = (metadata: Uint8Array): Option => { - const metadataRaw = createType('Raw', metadata) - const metadataBytes = createType('Bytes', metadataRaw) - return createType('Option', metadataBytes) -} diff --git a/query-node/mappings/src/content/video.ts b/query-node/mappings/src/content/video.ts index 61e5f75683..7efd9cf1eb 100644 --- a/query-node/mappings/src/content/video.ts +++ b/query-node/mappings/src/content/video.ts @@ -2,14 +2,7 @@ eslint-disable @typescript-eslint/naming-convention */ import { DatabaseManager, EventContext, StoreContext } from '@joystream/hydra-common' -import { - AppAction, - AppActionMetadata, - ContentMetadata, - IAppAction, - IVideoMetadata, - VideoMetadata, -} from '@joystream/metadata-protobuf' +import { AppAction, AppActionMetadata, IAppAction, IVideoMetadata, VideoMetadata } from '@joystream/metadata-protobuf' import { ChannelId, DataObjectId, VideoId } from '@joystream/types/primitives' import { PalletContentPermissionsContentActor as ContentActor, @@ -50,14 +43,13 @@ import { createNft } from './nft' import { convertContentActor, convertContentActorToChannelOrNftOwner, - generateAppActionCommitment, processAppActionMetadata, processVideoMetadata, unsetAssetRelations, videoRelationsForCounters, - wrapMetadata, } from './utils' import { BTreeSet } from '@polkadot/types' +import { metadataToBytes, generateAppActionCommitment } from '@joystream/js/lib/utils' interface ContentCreatedEventData { contentActor: ContentActor @@ -139,9 +131,8 @@ export async function processCreateVideoMessage( const appCommitment = generateAppActionCommitment( channel.id ?? '', contentCreationParameters.assets, - // after ephesus merge can be replaced with `metadataToBytes` helper fn from joystream/js - wrapMetadata(VideoMetadata.encode(videoMetadata as IVideoMetadata).finish()), - wrapMetadata(AppActionMetadata.encode(appAction.metadata ?? {}).finish()) + metadataToBytes(VideoMetadata, videoMetadata as IVideoMetadata), + metadataToBytes(AppActionMetadata, appAction.metadata ?? {}) ) await processAppActionMetadata(ctx, video, appAction, { actionOwnerId: channel.id, appCommitment }, (entity) => processVideoMetadata(ctx, entity, videoMetadata, newDataObjectIds) @@ -205,9 +196,14 @@ export async function content_ContentUpdated(ctx: EventContext & StoreContext): }) if (video) { - const contentMetadata = newMeta.isSome ? deserializeMetadata(ContentMetadata, newMeta.unwrap()) : undefined + const contentMetadata = newMeta.isSome ? deserializeMetadata(AppAction, newMeta.unwrap()) : undefined - await processUpdateVideoMessage(ctx, video, contentMetadata?.videoMetadata || undefined, contentUpdatedEventData) + await processUpdateVideoMessage( + ctx, + video, + contentMetadata?.contentMetadata?.videoMetadata || undefined, + contentUpdatedEventData + ) return } From a90343543197963c138d90a05d3abb2518bcd361 Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Mon, 23 Jan 2023 11:35:20 +0100 Subject: [PATCH 21/84] Back compatibility --- query-node/mappings/src/content/channel.ts | 45 ++++++++++++++-------- query-node/mappings/src/content/video.ts | 30 +++++++++++---- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/query-node/mappings/src/content/channel.ts b/query-node/mappings/src/content/channel.ts index fd14212125..691c0e6a79 100644 --- a/query-node/mappings/src/content/channel.ts +++ b/query-node/mappings/src/content/channel.ts @@ -86,21 +86,26 @@ export async function content_ChannelCreated(ctx: EventContext & StoreContext): inconsistentState(`storageBag for channel ${channelId} does not exist`) } - const appAction = deserializeMetadata(AppAction, channelCreationParameters.meta.unwrap()) || {} - - const appCommitment = generateAppActionCommitment( - channelOwner.ownerMember?.id ?? channelOwner.ownerCuratorGroup?.id ?? '', - channelCreationParameters.assets, - metadataToBytes(ChannelMetadata, appAction.channelMetadata ?? {}), - metadataToBytes(AppActionMetadata, appAction.metadata ?? {}) - ) - await processAppActionMetadata( - ctx, - channel, - appAction, - { actionOwnerId: channel.ownerMember?.id, appCommitment }, - (entity: Channel) => processChannelMetadata(ctx, entity, appAction.channelMetadata ?? {}, dataObjects) - ) + const appAction = deserializeMetadata(AppAction, channelCreationParameters.meta.unwrap()) + + if (appAction && Object.keys(appAction).length) { + const appCommitment = generateAppActionCommitment( + channelOwner.ownerMember?.id ?? channelOwner.ownerCuratorGroup?.id ?? '', + channelCreationParameters.assets, + metadataToBytes(ChannelMetadata, appAction.channelMetadata ?? {}), + metadataToBytes(AppActionMetadata, appAction.metadata ?? {}) + ) + await processAppActionMetadata( + ctx, + channel, + appAction, + { actionOwnerId: channel.ownerMember?.id, appCommitment }, + (entity: Channel) => processChannelMetadata(ctx, entity, appAction.channelMetadata ?? {}, dataObjects) + ) + } else { + const channelMetadata = deserializeMetadata(ChannelMetadata, channelCreationParameters.meta.unwrap()) ?? {} + await processChannelMetadata(ctx, channel, channelMetadata, dataObjects) + } } // save entity @@ -139,8 +144,14 @@ export async function content_ChannelUpdated(ctx: EventContext & StoreContext): inconsistentState(`storageBag for channel ${channelId} does not exist`) } - const newMetadata = deserializeMetadata(AppAction, newMetadataBytes) || {} - await processChannelMetadata(ctx, channel, newMetadata.channelMetadata ?? {}, newDataObjects) + const newMetadata = deserializeMetadata(AppAction, newMetadataBytes) + + if (newMetadata && Object.keys(newMetadata)) { + await processChannelMetadata(ctx, channel, newMetadata.channelMetadata ?? {}, newDataObjects) + } else { + const realNewMetadata = deserializeMetadata(ChannelMetadata, newMetadataBytes) + await processChannelMetadata(ctx, channel, realNewMetadata ?? {}, newDataObjects) + } } // save channel diff --git a/query-node/mappings/src/content/video.ts b/query-node/mappings/src/content/video.ts index 7efd9cf1eb..70a0a7ed63 100644 --- a/query-node/mappings/src/content/video.ts +++ b/query-node/mappings/src/content/video.ts @@ -2,7 +2,14 @@ eslint-disable @typescript-eslint/naming-convention */ import { DatabaseManager, EventContext, StoreContext } from '@joystream/hydra-common' -import { AppAction, AppActionMetadata, IAppAction, IVideoMetadata, VideoMetadata } from '@joystream/metadata-protobuf' +import { + AppAction, + AppActionMetadata, + ContentMetadata, + IAppAction, + IVideoMetadata, + VideoMetadata, +} from '@joystream/metadata-protobuf' import { ChannelId, DataObjectId, VideoId } from '@joystream/types/primitives' import { PalletContentPermissionsContentActor as ContentActor, @@ -96,19 +103,24 @@ export async function content_ContentCreated(ctx: EventContext & StoreContext): // deserialize & process metadata const appAction = meta.isSome ? deserializeMetadata(AppAction, meta.unwrap()) : undefined - // const contentMetadata = meta.isSome ? deserializeMetadata(ContentMetadata, meta.unwrap()) : undefined + const contentMetadata = meta.isSome ? deserializeMetadata(ContentMetadata, meta.unwrap()) : undefined // Content Creation Preference // 1. metadata == `VideoMetadata` || undefined -> create Video // 1. metadata == `PlaylistMetadata` -> create Playlist (Not Supported Yet) - await processCreateVideoMessage(ctx, channel, appAction ?? undefined, contentCreatedEventData) + await processCreateVideoMessage( + ctx, + channel, + appAction && Object.keys(appAction).length ? appAction : contentMetadata?.videoMetadata ?? undefined, + contentCreatedEventData + ) } export async function processCreateVideoMessage( ctx: EventContext & StoreContext, channel: Channel, - appAction: DecodedMetadataObject | undefined, + metadata: DecodedMetadataObject | DecodedMetadataObject | undefined, contentCreatedEventData: ContentCreatedEventData ): Promise { const { store, event } = ctx @@ -126,17 +138,19 @@ export async function processCreateVideoMessage( reactionsCount: 0, }) - if (appAction) { - const videoMetadata = appAction.contentMetadata?.videoMetadata ?? {} + if (metadata && 'appId' in metadata) { + const videoMetadata = metadata.contentMetadata?.videoMetadata ?? {} const appCommitment = generateAppActionCommitment( channel.id ?? '', contentCreationParameters.assets, metadataToBytes(VideoMetadata, videoMetadata as IVideoMetadata), - metadataToBytes(AppActionMetadata, appAction.metadata ?? {}) + metadataToBytes(AppActionMetadata, metadata.metadata ?? {}) ) - await processAppActionMetadata(ctx, video, appAction, { actionOwnerId: channel.id, appCommitment }, (entity) => + await processAppActionMetadata(ctx, video, metadata, { actionOwnerId: channel.id, appCommitment }, (entity) => processVideoMetadata(ctx, entity, videoMetadata, newDataObjectIds) ) + } else if (metadata) { + await processVideoMetadata(ctx, video, metadata as DecodedMetadataObject, newDataObjectIds) } // save video From 97f518f382a868d0d52fe2fe66a4375424376988 Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Tue, 24 Jan 2023 17:49:08 +0100 Subject: [PATCH 22/84] Test for create channel app action --- query-node/mappings/src/content/channel.ts | 3 +- query-node/mappings/src/content/utils.ts | 58 ++++++++-- query-node/mappings/src/content/video.ts | 29 +++-- query-node/schemas/content.graphql | 10 +- tests/network-tests/package.json | 1 + tests/network-tests/src/Api.ts | 13 ++- tests/network-tests/src/flows/content/app.ts | 112 ++++++++++++++++++- tests/network-tests/src/scenarios/app.ts | 3 +- tests/network-tests/src/scenarios/full.ts | 3 +- yarn.lock | 2 +- 10 files changed, 198 insertions(+), 36 deletions(-) diff --git a/query-node/mappings/src/content/channel.ts b/query-node/mappings/src/content/channel.ts index 691c0e6a79..52c9d3661a 100644 --- a/query-node/mappings/src/content/channel.ts +++ b/query-node/mappings/src/content/channel.ts @@ -49,12 +49,13 @@ import { unsetAssetRelations, mapAgentPermission, processAppActionMetadata, + generateAppActionCommitment, + metadataToBytes, } from './utils' import { BTreeMap, BTreeSet, u64 } from '@polkadot/types' // Joystream types import { PalletContentIterableEnumsChannelActionPermission } from '@polkadot/types/lookup' import { processUpdateApp, processCreateAppMessage } from './app' -import { generateAppActionCommitment, metadataToBytes } from '@joystream/js/lib/utils' export async function content_ChannelCreated(ctx: EventContext & StoreContext): Promise { const { store, event } = ctx diff --git a/query-node/mappings/src/content/utils.ts b/query-node/mappings/src/content/utils.ts index b6b8929cdd..971d2068bd 100644 --- a/query-node/mappings/src/content/utils.ts +++ b/query-node/mappings/src/content/utils.ts @@ -21,6 +21,7 @@ import { Language, License, VideoMediaMetadata, + App, // asset Membership, VideoMediaEncoding, @@ -42,14 +43,17 @@ import { PalletContentChannelOwner as ChannelOwner, PalletContentPermissionsContentActor as ContentActor, PalletContentIterableEnumsChannelActionPermission, + PalletContentStorageAssetsRecord, } from '@polkadot/types/lookup' -import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types' +import { AnyMetadataClass, DecodedMetadataObject } from '@joystream/metadata-protobuf/types' import BN from 'bn.js' import _ from 'lodash' import { getSortedDataObjectsByIds } from '../storage/utils' -import { BTreeSet } from '@polkadot/types' +import { BTreeSet, Option } from '@polkadot/types' import { DataObjectId } from '@joystream/types/primitives' import { getAppById } from './app' +import { Bytes } from '@polkadot/types/primitive' +import { createType } from '@joystream/types' const ASSET_TYPES = { channel: [ @@ -196,7 +200,7 @@ async function checkAppActionNonce( return false } -async function validateAppSignature( +async function validateAndGetApp( ctx: EventContext & StoreContext, entity: T, validationContext: { @@ -204,22 +208,35 @@ async function validateAppSignature( appCommitment: string | undefined }, appAction: DecodedMetadataObject -) { +): Promise { // If one is missing we cannot verify the signature if (!appAction.appId || !appAction.signature || !appAction.metadata || !validationContext.appCommitment) { - return false + invalidMetadata('Missing action fields to verify app') + return undefined } const app = await getAppById(ctx.store, appAction.appId) - if (!app || !app.authKey || !(await checkAppActionNonce(ctx, entity, validationContext.actionOwnerId, appAction))) { - return false + if (!app || !app.authKey) { + invalidMetadata('No app of given id') + return undefined + } + + if (!(await checkAppActionNonce(ctx, entity, validationContext.actionOwnerId, appAction))) { + invalidMetadata('Invalid app action nonce') + + return undefined } - return ed25519Verify(validationContext.appCommitment, appAction.signature, app.authKey) + try { + return ed25519Verify(validationContext.appCommitment, appAction.signature, app.authKey) ? app : undefined + } catch (e) { + invalidMetadata((e as Error)?.message) + return undefined + } } -export async function processAppActionMetadata( +export async function processAppActionMetadata( ctx: EventContext & StoreContext, entity: T, meta: DecodedMetadataObject, @@ -229,12 +246,12 @@ export async function processAppActionMetadata( }, entityMetadataProcessor: (entity: T) => Promise ): Promise { - if (!(await validateAppSignature(ctx, entity, validationContext, meta))) { - invalidMetadata('Invalid application signature') + const app = await validateAndGetApp(ctx, entity, validationContext, meta) + if (!app) { return entityMetadataProcessor(entity) } - integrateMeta(entity, meta, ['appId']) + integrateMeta(entity, { entryApp: app }, ['entryApp']) return entityMetadataProcessor(entity) } @@ -761,3 +778,20 @@ export async function unsetAssetRelations(store: DatabaseManager, dataObject: St export function mapAgentPermission(permission: PalletContentIterableEnumsChannelActionPermission): string { return permission.toString() } + +export function generateAppActionCommitment( + creatorId: string, + assets: Option, + rawAction: Bytes, + rawAppMetadata: Bytes +): string { + return JSON.stringify([creatorId, assets.toString(), rawAction, rawAppMetadata]) +} + +export function metadataToBytes(metaClass: AnyMetadataClass, obj: T): Bytes { + return createType('Bytes', metadataToString(metaClass, obj)) +} + +export function metadataToString(metaClass: AnyMetadataClass, obj: T): string { + return '0x' + Buffer.from(metaClass.encode(obj).finish()).toString('hex') +} diff --git a/query-node/mappings/src/content/video.ts b/query-node/mappings/src/content/video.ts index 70a0a7ed63..362a737158 100644 --- a/query-node/mappings/src/content/video.ts +++ b/query-node/mappings/src/content/video.ts @@ -43,20 +43,21 @@ import { VideoSubtitle, } from 'query-node/dist/model' import { Content } from '../../generated/types' -import { bytesToString, deserializeMetadata, genericEventFields, inconsistentState, logger } from '../common' +import { bytesToString, deserializeMetadata, genericEventFields, inconsistentState, invalidMetadata,logger } from '../common' import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types' import { getAllManagers } from '../derivedPropertiesManager/applications' import { createNft } from './nft' import { convertContentActor, convertContentActorToChannelOrNftOwner, + generateAppActionCommitment, + metadataToBytes, processAppActionMetadata, processVideoMetadata, unsetAssetRelations, videoRelationsForCounters, } from './utils' import { BTreeSet } from '@polkadot/types' -import { metadataToBytes, generateAppActionCommitment } from '@joystream/js/lib/utils' interface ContentCreatedEventData { contentActor: ContentActor @@ -112,7 +113,7 @@ export async function content_ContentCreated(ctx: EventContext & StoreContext): await processCreateVideoMessage( ctx, channel, - appAction && Object.keys(appAction).length ? appAction : contentMetadata?.videoMetadata ?? undefined, + appAction && 'signature' in appAction ? appAction : contentMetadata?.videoMetadata ?? undefined, contentCreatedEventData ) } @@ -138,7 +139,7 @@ export async function processCreateVideoMessage( reactionsCount: 0, }) - if (metadata && 'appId' in metadata) { + if (metadata && 'signature' in metadata) { const videoMetadata = metadata.contentMetadata?.videoMetadata ?? {} const appCommitment = generateAppActionCommitment( channel.id ?? '', @@ -210,14 +211,18 @@ export async function content_ContentUpdated(ctx: EventContext & StoreContext): }) if (video) { - const contentMetadata = newMeta.isSome ? deserializeMetadata(AppAction, newMeta.unwrap()) : undefined - - await processUpdateVideoMessage( - ctx, - video, - contentMetadata?.contentMetadata?.videoMetadata || undefined, - contentUpdatedEventData - ) + const appAction = newMeta.isSome ? deserializeMetadata(AppAction, newMeta.unwrap()) : undefined + if (appAction && 'signature' in appAction) { + await processUpdateVideoMessage( + ctx, + video, + appAction?.contentMetadata?.videoMetadata || undefined, + contentUpdatedEventData + ) + } else { + const contentMetadata = newMeta.isSome ? deserializeMetadata(ContentMetadata, newMeta.unwrap()) : undefined + await processUpdateVideoMessage(ctx, video, contentMetadata?.videoMetadata || undefined, contentUpdatedEventData) + } return } diff --git a/query-node/schemas/content.graphql b/query-node/schemas/content.graphql index 28d2939227..7826d796e6 100644 --- a/query-node/schemas/content.graphql +++ b/query-node/schemas/content.graphql @@ -33,14 +33,16 @@ type App @entity { category: String authKey: String channel: Channel! + appVideos: [Video!] @derivedFrom(field: "entryApp") + appChannels: [Channel!] @derivedFrom(field: "entryApp") } type Channel @entity { "Runtime entity identifier (EntityId)" id: ID! - "Entity indentifier for Application used for channel creation" - appId: ID + "Application used for channel creation" + entryApp: App "Member owning the channel (if any)" ownerMember: Membership @@ -128,8 +130,8 @@ type Video @entity { "Runtime identifier" id: ID! - "Entity indentifier for Application used for channel creation" - appId: ID + "Application used for video creation" + entryApp: App "Reference to videos's channel" channel: Channel! diff --git a/tests/network-tests/package.json b/tests/network-tests/package.json index 90fe09e539..0d55aca52b 100644 --- a/tests/network-tests/package.json +++ b/tests/network-tests/package.json @@ -19,6 +19,7 @@ "@joystream/types": "^0.20.5", "@polkadot/api": "8.9.1", "@polkadot/keyring": "9.5.1", + "@polkadot/util-crypto": "^10.2.6", "@types/async-lock": "^1.1.3", "@types/bmp-js": "^0.1.0", "@types/bn.js": "^5.1.0", diff --git a/tests/network-tests/src/Api.ts b/tests/network-tests/src/Api.ts index e1dd3549ce..96a4e0992a 100644 --- a/tests/network-tests/src/Api.ts +++ b/tests/network-tests/src/Api.ts @@ -1,5 +1,5 @@ import { ApiPromise, WsProvider, Keyring } from '@polkadot/api' -import { u32, u64, u128, BTreeSet, Option, Vec } from '@polkadot/types' +import { u32, u64, u128, BTreeSet, Option, Vec, Bytes } from '@polkadot/types' import { ISubmittableResult } from '@polkadot/types/types' import { KeyringPair } from '@polkadot/keyring/types' import { @@ -62,8 +62,10 @@ import { } from './consts' import { + AppAction, ChannelOwnerRemarked, CreateVideoCategory, + IAppAction, IAppMetadata, IChannelOwnerRemarked, MemberRemarked, @@ -760,7 +762,8 @@ export class Api { distributionBucketFamilyId: number distributionBucketIndex: number }[], - memberControllerAccount?: string + memberControllerAccount?: string, + channelMeta?: IAppAction ): Promise { memberControllerAccount = memberControllerAccount || (await this.getMemberControllerAccount(memberId)) @@ -768,14 +771,18 @@ export class Api { throw new Error('invalid member id') } + const dataObjectPerMegabyteFee = await this.api.query.storage.dataObjectPerMegabyteFee() + const channelStateBloatBondValue = await this.api.query.content.channelStateBloatBondValue() // Create a channel without any assets const tx = this.api.tx.content.createChannel( { Member: memberId }, { assets: null, - meta: null, + meta: channelMeta ? Utils.metadataToBytes(AppAction, channelMeta) : null, storageBuckets, distributionBuckets, + expectedChannelStateBloatBond: channelStateBloatBondValue, + expectedDataObjectStateBloatBond: dataObjectPerMegabyteFee, } ) diff --git a/tests/network-tests/src/flows/content/app.ts b/tests/network-tests/src/flows/content/app.ts index 4007fbe1ab..9270e1186c 100644 --- a/tests/network-tests/src/flows/content/app.ts +++ b/tests/network-tests/src/flows/content/app.ts @@ -1,4 +1,4 @@ -import { AppMetadata } from '@joystream/metadata-protobuf' +import { AppActionMetadata, AppMetadata, ChannelMetadata } from '@joystream/metadata-protobuf' import BN from 'bn.js' import { assert } from 'chai' import { Api } from 'src/Api' @@ -13,6 +13,15 @@ import { } from '../../fixtures/content' import { FlowProps } from '../../Flow' import { createJoystreamCli } from '../utils' +import { Utils } from '../../utils' +import { Option } from '@polkadot/types' +import { AnyMetadataClass } from '@joystream/metadata-protobuf/types' +import { PalletContentStorageAssetsRecord } from '@polkadot/types/lookup' +import { Bytes } from '@polkadot/types/primitive' +import { createType } from '@joystream/types' +import { ed25519PairFromString, ed25519Sign } from '@polkadot/util-crypto' +import { u8aToHex } from '@polkadot/util' +import { CreateChannelsAsMemberFixture } from '../../misc/createChannelsAsMemberFixture' const sufficientTopupAmount = new BN(10_000_000_000_000) @@ -116,6 +125,90 @@ export async function updateApp({ api, query }: FlowProps): Promise { debug('done') } +export async function createAppAction({ api, query }: FlowProps): Promise { + const debug = extendDebug('flow:create-app-actions') + debug('Started') + // create author of channels and videos + const createMembersFixture = new CreateMembersFixture(api, query, 3, 0, sufficientTopupAmount) + await new FixtureRunner(createMembersFixture).run() + const [member] = createMembersFixture.getCreatedItems().members + const createChannelFixture = new CreateChannelsAsMemberFixture(api, query, member.memberId.toNumber(), 2) + + const appChannelId = await getChannelId(api, query, member) + + const keypair = ed25519PairFromString('fake_secret') + const newAppName = 'app_action_1' + const newAppMetadata: Partial = { + category: 'blockchain', + oneLiner: 'best blokchain video platform', + description: 'description', + platforms: ['web'], + authKey: u8aToHex(keypair.publicKey), + } + + await api.createApp(member.memberId, appChannelId, newAppName, newAppMetadata) + const appFragment = await query.tryQueryWithTimeout( + () => query.getAppsByName(newAppName), + (apps) => { + assert.equal(apps?.[0].name, newAppName) + } + ) + + // const nonce = await query.chan([member.memberId.toString()]) + + const channelInput = { + title: `Channel from ${appFragment?.[0].name} app`, + description: 'This is the app channel', + isPublic: true, + language: 'en', + } + const appActionMeta = { + // todo change naming + nonceId: '1', + } + const appChannelCommitment = generateAppActionCommitment( + member.memberId.toString(), + createType('Option', null), + metadataToBytes(ChannelMetadata, channelInput), + metadataToBytes(AppActionMetadata, appActionMeta) + ) + const signature = u8aToHex(ed25519Sign(appChannelCommitment, keypair, true)) + const appChannelInput = { + appId: appFragment?.[0].id, + channelMetadata: channelInput, + signature, + metadata: appActionMeta, + } + debug(appChannelInput) + debug(keypair) + const storageBuckets = await createChannelFixture.selectStorageBucketsForNewChannel() + const distBuckets = await createChannelFixture.selectDistributionBucketsForNewChannel() + const account = await api.getMemberControllerAccount(member.memberId.toNumber()) + + const channelId = await api.createMockChannel( + member.memberId.toNumber(), + storageBuckets, + distBuckets, + account, + appChannelInput + ) + debug(channelId) + + await query.tryQueryWithTimeout( + () => query.channelById(channelId.toString()), + (channel) => { + Utils.assert(channel, 'Channel not found') + assert.equal(channel.title, appChannelInput.channelMetadata?.title) + assert.equal(channel.description, appChannelInput.channelMetadata?.description) + assert.equal(channel.isPublic, appChannelInput.channelMetadata?.isPublic) + assert.equal(channel.language?.iso, appChannelInput.channelMetadata?.language) + assert.equal(channel.app?.id, appFragment?.[0].id) + } + ) + + debug('done') +} + async function getChannelId(api: Api, query: QueryNodeApi, member: IMember) { const videoCount = 0 const videoCategoryCount = 0 @@ -144,3 +237,20 @@ async function getChannelId(api: Api, query: QueryNodeApi, member: IMember) { return channelId } + +export function generateAppActionCommitment( + creatorId: string, + assets: Option, + rawAction: Bytes, + rawAppMetadata: Bytes +): string { + return JSON.stringify([creatorId, assets.toString(), rawAction, rawAppMetadata]) +} + +export function metadataToBytes(metaClass: AnyMetadataClass, obj: T): Bytes { + return createType('Bytes', metadataToString(metaClass, obj)) +} + +export function metadataToString(metaClass: AnyMetadataClass, obj: T): string { + return '0x' + Buffer.from(metaClass.encode(obj).finish()).toString('hex') +} diff --git a/tests/network-tests/src/scenarios/app.ts b/tests/network-tests/src/scenarios/app.ts index 4ce844b8bc..72f87673af 100644 --- a/tests/network-tests/src/scenarios/app.ts +++ b/tests/network-tests/src/scenarios/app.ts @@ -1,4 +1,4 @@ -import { createApp, updateApp } from '../flows/content/app' +import { createApp, createAppAction, updateApp } from '../flows/content/app' import initFaucet from '../flows/faucet/initFaucet' import leaderSetup from '../flows/working-groups/leadOpening' import { scenario } from '../Scenario' @@ -10,4 +10,5 @@ scenario('App', async ({ job }) => { job('Create app', createApp).after(leads) job('Update app', updateApp).after(leads) + job('Update app', createAppAction).after(leads) }) diff --git a/tests/network-tests/src/scenarios/full.ts b/tests/network-tests/src/scenarios/full.ts index 620004a1b3..29029cbc19 100644 --- a/tests/network-tests/src/scenarios/full.ts +++ b/tests/network-tests/src/scenarios/full.ts @@ -37,7 +37,7 @@ import updatingVerificationStatus from '../flows/membership/updateVerificationSt import commentsAndReactions from '../flows/content/commentsAndReactions' import addAndUpdateVideoSubtitles from '../flows/content/videoSubtitles' import { testVideoCategories } from '../flows/content/videoCategories' -import { createApp, updateApp } from '../flows/content/app' +import { createApp, createAppAction, updateApp } from '../flows/content/app' // eslint-disable-next-line @typescript-eslint/no-floating-promises scenario('Full', async ({ job, env }) => { @@ -113,6 +113,7 @@ scenario('Full', async ({ job, env }) => { ) job('create app', createApp).after(sudoHireLead) job('update app', updateApp).after(sudoHireLead) + job('create channel app action', createAppAction).after(sudoHireLead) const contentDirectoryJob = commentsAndReactionsJob // keep updated to last job above diff --git a/yarn.lock b/yarn.lock index 1ac072c709..7556aedee9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2876,7 +2876,7 @@ "@polkadot/util-crypto" "^9.5.1" rxjs "^7.5.5" -"@polkadot/util-crypto@9.5.1", "@polkadot/util-crypto@^9.5.1": +"@polkadot/util-crypto@9.5.1", "@polkadot/util-crypto@^10.2.6", "@polkadot/util-crypto@^9.5.1": version "9.5.1" resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-9.5.1.tgz#f69bdccd7043620c9bd905b169ec50bf97bf6ffb" integrity sha512-4YwJJ2/mXx3PXTy4WLekQOo1MlDtQzYgTZsjOagi3Uz3Q/ITvS+/iu6eF/H6Tz0uEQjwX6t9tsMkM5FWk/XoGg== From 5ef7eeecd8178ef16136ce3e5d8fa169a7e940f3 Mon Sep 17 00:00:00 2001 From: WRadoslaw Date: Wed, 25 Jan 2023 13:32:28 +0100 Subject: [PATCH 23/84] Schema changes --- .../src/graphql/generated/queries.ts | 9 ++++++++ .../src/graphql/generated/schema.ts | 22 +++++++++++++++++++ .../src/graphql/queries/content.graphql | 6 +++++ 3 files changed, 37 insertions(+) diff --git a/tests/network-tests/src/graphql/generated/queries.ts b/tests/network-tests/src/graphql/generated/queries.ts index 4688d26808..332ce47e88 100644 --- a/tests/network-tests/src/graphql/generated/queries.ts +++ b/tests/network-tests/src/graphql/generated/queries.ts @@ -90,6 +90,7 @@ export type ChannelFieldsFragment = { isCensored: boolean rewardAccount: string language?: Types.Maybe<{ iso: string }> + entryApp?: Types.Maybe app?: Types.Maybe ownerMember?: Types.Maybe<{ id: string }> ownerCuratorGroup?: Types.Maybe<{ id: string }> @@ -162,6 +163,7 @@ export type VideoFieldsFragment = { commentsCount: number reactionsCount: number isCommentSectionEnabled: boolean + entryApp?: Types.Maybe license?: Types.Maybe mediaMetadata?: Types.Maybe media?: Types.Maybe @@ -2596,6 +2598,9 @@ export const ChannelFields = gql` iso } isCensored + entryApp { + ...AppFields + } app { ...AppFields } @@ -2716,6 +2721,9 @@ export const VideoFields = gql` isPublic isExplicit hasMarketing + entryApp { + ...AppFields + } license { ...LicenseFields } @@ -2750,6 +2758,7 @@ export const VideoFields = gql` ...VideoSubtitleFields } } + ${AppFields} ${LicenseFields} ${VideoMediaMetadataFields} ${StorageDataObjectFields} diff --git a/tests/network-tests/src/graphql/generated/schema.ts b/tests/network-tests/src/graphql/generated/schema.ts index 47e5346ff4..6e6bceaf42 100644 --- a/tests/network-tests/src/graphql/generated/schema.ts +++ b/tests/network-tests/src/graphql/generated/schema.ts @@ -173,6 +173,8 @@ export type App = BaseGraphQlObject & { authKey?: Maybe channel: Channel channelId: Scalars['String'] + appVideos: Array