diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 9c72c72..7d685de 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -14,6 +14,8 @@ services: frequency: image: dsnp/instant-seal-node-with-deployed-schemas:latest + hostname: frequency + container_name: frequency # We need to specify the platform because it's the only image # built by Frequency at the moment, and auto-pull won't work otherwise platform: linux/amd64 @@ -36,7 +38,7 @@ services: container_name: webhook hostname: webhook build: - context: webhook-specification/mock-webhook-server + context: . dockerfile: webhook-specification/mock-webhook-server/Dockerfile ports: - 3001:3001 diff --git a/package.json b/package.json index 755ad0f..0af2db8 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "lint:fix": "prettier --write ./src && eslint \"./src/**/*.ts\" --fix", "pretest": "cp env.template .env", "test": "jest --coverage --verbose", + "create-accounts": "set -a ; . ./setup/testing/.env ; tsx setup/testing/create-accounts.ts", + "clear-graphs": "set -a ; . ./setup/testing/.env ; tsx setup/testing/clear-graphs.ts", "chain-setup": "tsx setup/testing/index.ts", "reset-chain": "setup/testing/reset_chain" }, diff --git a/setup/testing/clear-graphs.ts b/setup/testing/clear-graphs.ts new file mode 100644 index 0000000..41a1db0 --- /dev/null +++ b/setup/testing/clear-graphs.ts @@ -0,0 +1,134 @@ +import { ExtrinsicHelper } from '@amplica-labs/frequency-scenario-template'; +import { createAddGraphKeyExtrinsic, createClearGraphExtrinsics, graphKeypair } from './graph'; +import { addExtrinsicToTrack, api, famousUser, followers, graph, initScenario, provider, submitAndTrackExtrinsics } from './utils'; +import { ProviderGraph, GraphKeyPair as ProviderGraphKeyPair } from 'reconnection-service/src/interfaces/provider-graph.interface'; +import fs from 'node:fs'; +import * as ScenarioTypes from './types'; +import * as ScenarioConstants from './constants'; + +async function main() { + await initScenario(); + + const followersToProcess = [...followers.values()].filter((follower) => follower?.msaId); + if (famousUser?.msaId) { + followersToProcess.push(famousUser); + } + + // Create graph extrinsics + await Promise.all( + followersToProcess.map(async (follower) => { + await createAddGraphKeyExtrinsic(api, graph, graphKeypair, follower); + await createClearGraphExtrinsics(api, follower); + }), + ); + let graphKeysToUpdate = 0; + let graphsToClear = 0; + + for (const follower of followers.values()) { + if (follower?.addGraphKey) { + addExtrinsicToTrack(follower.addGraphKey()); + graphKeysToUpdate++; + } + + if (follower?.resetGraph) { + follower.resetGraph.forEach((e) => addExtrinsicToTrack(e())); + graphsToClear++; + } + } + + if (famousUser?.addGraphKey) { + addExtrinsicToTrack(famousUser.addGraphKey()); + graphKeysToUpdate++; + } + + if (famousUser?.resetGraph) { + famousUser.resetGraph.forEach((e) => addExtrinsicToTrack(e())); + graphsToClear++; + } + + console.log(` +Users requiring public graph key update: ${graphKeysToUpdate} +Users requiring graph reset: ${graphsToClear} +`); + + await submitAndTrackExtrinsics(api, provider); + + // Create JSON responses for each follower + if (!fs.existsSync('webhook-specification/mock-webhook-server/responses')) { + fs.mkdirSync('webhook-specification/mock-webhook-server/responses'); + } + followers.forEach((follower) => { + const response: ScenarioTypes.ProviderResponse = { + dsnpId: follower.msaId!.toString(), + connections: { + data: [ + { + dsnpId: famousUser.msaId!.toString(), + privacyType: 'Private', + direction: 'connectionTo', + connectionType: 'Follow', + }, + ], + }, + graphKeyPairs: [ + { + keyType: 'X25519', + publicKey: ScenarioConstants.graphPublicKey, + privateKey: ScenarioConstants.graphPrivateKey, + } as ProviderGraphKeyPair, + ], + }; + + fs.writeFileSync(`webhook-specification/mock-webhook-server/responses/response.${follower.msaId?.toString()}.json`, JSON.stringify(response, undefined, 4)); + }); + + const famousUserResponse = { + dsnpId: famousUser.msaId!.toString(), + graphKeyPairs: [ + { + keyType: 'X25519', + publicKey: ScenarioConstants.graphPublicKey, + privateKey: ScenarioConstants.graphPrivateKey, + } as ProviderGraphKeyPair, + ], + }; + + const responses: ScenarioTypes.ProviderResponse[] = []; + const allFollowers = [...followers.values()]; + while (allFollowers.length > 0) { + const followerSlice = allFollowers.splice(0, Math.min(allFollowers.length, 50)); + const response = { + ...famousUserResponse, + connections: { + data: followerSlice.flatMap((follower) => [ + { + dsnpId: follower.msaId!.toString(), + privacyType: 'Private', + direction: 'connectionFrom', + connectionType: 'Follow', + } as ProviderGraph, + { + dsnpId: follower.msaId!.toString(), + privacyType: 'Private', + direction: 'connectionTo', + connectionType: 'Follow', + } as ProviderGraph, + ]), + }, + }; + responses.push(response); + } + + responses.forEach((response, index) => { + response.connections.pagination = { + pageNumber: index + 1, + pageCount: responses.length, + pageSize: response.connections.data.length, + }; + fs.writeFileSync(`webhook-specification/mock-webhook-server/responses/response.${famousUser.msaId!.toString()}.${index + 1}.json`, JSON.stringify(response, undefined, 4)); + }); +} + +main() + .catch((e) => console.error(e)) + .finally(async () => ExtrinsicHelper.disconnect()); diff --git a/setup/testing/constants.ts b/setup/testing/constants.ts new file mode 100644 index 0000000..02cae3a --- /dev/null +++ b/setup/testing/constants.ts @@ -0,0 +1,8 @@ +import { HexString } from '@polkadot/util/types'; + +export const graphPublicKey: HexString = '0x0514f63edc89d414061bf451cc99b1f2b43fac920c351be60774559a31523c75'; +export const graphPrivateKey: HexString = '0x1c15b6d1af4716615a4eb83a2dfba3284e1c0a199603572e7b95c164f7ad90e3'; +export const CAPACITY_AMOUNT_TO_STAKE = 1_000_000_000_000_000n; +export const DEFAULT_SCHEMAS = [5, 7, 8, 9, 10]; +export const NUM_FOLLOWERS = 10; +export const MAX_EXTRINSICS_TO_SUBMIT = 100; diff --git a/setup/testing/create-accounts.ts b/setup/testing/create-accounts.ts new file mode 100644 index 0000000..fe2d86a --- /dev/null +++ b/setup/testing/create-accounts.ts @@ -0,0 +1,89 @@ +import { ApiPromise } from '@polkadot/api'; +import * as ScenarioTypes from './types'; +import { ExtrinsicHelper } from '@amplica-labs/frequency-scenario-template'; +import { getAddGraphKeyPayload, graphKeypair } from './graph'; +import { getAddProviderPayload, getCreateUserExtrinsic } from './msa'; +import { Graph } from '@dsnp/graph-sdk'; +import { addExtrinsicToTrack, api, famousUser, followers, initScenario, provider, submitAndTrackExtrinsics } from './utils'; +import { Block, Event } from '@polkadot/types/interfaces'; +import { GenericExtrinsic } from '@polkadot/types'; +import { AnyTuple } from '@polkadot/types/types'; + +async function createUser(api: ApiPromise, graph: Graph, user: ScenarioTypes.ChainUser, provider: ScenarioTypes.ChainUser) { + if (!user.msaId) { + const { payload: addProviderPayload, proof } = await getAddProviderPayload(ExtrinsicHelper.apiPromise, user, provider); + user.create = () => api.tx.msa.createSponsoredAccountWithDelegation(user.keys!.publicKey, proof, addProviderPayload); + + const graphKeyPayload = await getAddGraphKeyPayload(ExtrinsicHelper.apiPromise, graph, graphKeypair, user); + if (graphKeyPayload) { + const { payload: addGraphKeyPayload, proof: addGraphKeyProof } = graphKeyPayload; + user.addGraphKey = () => ExtrinsicHelper.apiPromise.tx.statefulStorage.applyItemActionsWithSignatureV2(user.keys!.publicKey, addGraphKeyProof, addGraphKeyPayload); + } + } +} + +async function main() { + await initScenario(); + + let followersToCreate = 0; + + // Create followers + await Promise.all([ + [...followers.keys()].map((a) => { + const follower = followers.get(a)!; + return getCreateUserExtrinsic(api, follower, provider); + }), + getCreateUserExtrinsic(api, famousUser, provider), + ]); + + for (const follower of followers.values()) { + if (follower?.create) { + addExtrinsicToTrack(follower.create()); + followersToCreate++; + } + } + + if (famousUser?.create) { + addExtrinsicToTrack(famousUser.create()); + followersToCreate++; + } + + console.log(` +MSAs to create: ${followersToCreate} +`); + + await submitAndTrackExtrinsics(api, provider, (block: Block, _extrinsic: GenericExtrinsic, event: Event) => { + if (api.events.msa.MsaCreated.is(event)) { + const { msaId, key } = event.data; + const address = key.toString(); + const follower = followers.get(address); + if (follower) { + follower.msaId = msaId; + follower.createdAtBlock = block.header.number.toNumber(); + followers.set(address, follower); + } else if (address === famousUser.keys!.address) { + famousUser.msaId = msaId; + famousUser.createdAtBlock = block.header.number.toNumber(); + } else { + console.error('Cannot find follower ', address); + } + } + }); + + // console.dir( + // [...followers.values()].map((follower) => follower.createdAtBlock), + // { maxArrayLength: null }, + // ); + const firstBlock = Math.min(...[...followers.values()].map((follower) => follower.createdAtBlock || 0)); + if (firstBlock) { + console.log(`First user created at block ${firstBlock}`); + } + + if (famousUser.createdAtBlock) { + console.log(`Last (famous) user ${famousUser.msaId!.toString()} created at block ${famousUser.createdAtBlock}`); + } +} + +main() + .catch((e) => console.error(e)) + .finally(async () => ExtrinsicHelper.disconnect()); diff --git a/setup/testing/graph.ts b/setup/testing/graph.ts new file mode 100644 index 0000000..506acd8 --- /dev/null +++ b/setup/testing/graph.ts @@ -0,0 +1,120 @@ +import '@frequency-chain/api-augment'; +import { AddGraphKeyAction, AddKeyUpdate, DsnpKeys, Graph, GraphKeyPair, GraphKeyType, ImportBundleBuilder, KeyData } from '@dsnp/graph-sdk'; +import { ApiPromise } from '@polkadot/api'; +import { AnyNumber, ISubmittableResult } from '@polkadot/types/types'; +import { hexToU8a, u8aToHex } from '@polkadot/util'; +import * as ScenarioTypes from './types'; +import { HexString } from '@polkadot/util/types'; +import { ItemizedSignaturePayload, Sr25519Signature, signPayloadSr25519 } from '@amplica-labs/frequency-scenario-template'; +import { getExpiration, privateFollowSchemaId, publicGraphKeySchemaId } from './utils'; +import { ItemizedStoragePageResponse, PageHash } from '@frequency-chain/api-augment/interfaces'; +import { SubmittableExtrinsic } from '@polkadot/api-base/types'; + +export const graphPublicKey: HexString = '0x0514f63edc89d414061bf451cc99b1f2b43fac920c351be60774559a31523c75'; +export const graphPrivateKey: HexString = '0x1c15b6d1af4716615a4eb83a2dfba3284e1c0a199603572e7b95c164f7ad90e3'; +export const graphKeypair: GraphKeyPair = { + keyType: GraphKeyType.X25519, + publicKey: hexToU8a(graphPublicKey), + secretKey: hexToU8a(graphPrivateKey), +}; + +export function createGraphKeyPair(): GraphKeyPair { + return Graph.generateKeyPair(GraphKeyType.X25519); +} + +async function isGraphKeyCurrent(api: ApiPromise, graph: Graph, keypair: GraphKeyPair, msaId: AnyNumber): Promise<[boolean, PageHash]> { + const itemizedResponse: ItemizedStoragePageResponse = await api.rpc.statefulStorage.getItemizedStorage(msaId, publicGraphKeySchemaId); + const keyData: KeyData[] = itemizedResponse.items.toArray().map((publicKey) => ({ + index: publicKey.index.toNumber(), + content: hexToU8a(publicKey.payload.toHex()), + })); + const dsnpKeys: DsnpKeys = { + dsnpUserId: msaId.toString(), + keysHash: itemizedResponse.content_hash.toNumber(), + keys: keyData, + }; + + const bundle = new ImportBundleBuilder().withDsnpUserId(msaId.toString()).withDsnpKeys(dsnpKeys).build(); + graph.importUserData([bundle]); + + const keys = graph.getPublicKeys(msaId.toString()); + const latestKey = u8aToHex(keys.pop()?.key); + if (latestKey === u8aToHex(keypair.publicKey)) { + return [true, itemizedResponse.content_hash]; + } + + return [false, itemizedResponse.content_hash]; +} + +export async function getAddGraphKeyPayload( + api: ApiPromise, + graph: Graph, + keypair: GraphKeyPair, + user: ScenarioTypes.ChainUser, +): Promise<{ payload: ItemizedSignaturePayload; proof: Sr25519Signature } | null> { + const [keyIsCurrent, targetHash] = await isGraphKeyCurrent(api, graph, keypair, user.msaId!); + if (keyIsCurrent) { + return null; + } + + const actions = [ + { + type: 'AddGraphKey', + ownerDsnpUserId: user.msaId?.toString(), + newPublicKey: hexToU8a(graphPublicKey), + } as AddGraphKeyAction, + ]; + + graph.applyActions(actions); + const keyExport = graph.exportUserGraphUpdates(user.msaId!.toString()); + + if (keyExport.length > 0) { + const bundle = keyExport[0] as AddKeyUpdate; + + const addAction = [ + { + Add: { + data: u8aToHex(bundle.payload), + }, + }, + ]; + + const graphKeyAction = { + targetHash: targetHash, + schemaId: publicGraphKeySchemaId, + actions: addAction, + expiration: await getExpiration(api), + }; + + const payloadBytes = api.registry.createType('PalletStatefulStorageItemizedSignaturePayloadV2', graphKeyAction); + const proof = signPayloadSr25519(user.keys!, payloadBytes); + + return { payload: { ...graphKeyAction }, proof }; + } + + return null; +} + +export async function createAddGraphKeyExtrinsic(api: ApiPromise, graph: Graph, keypair: GraphKeyPair, user: ScenarioTypes.ChainUser): Promise { + const [keyIsCurrent] = await isGraphKeyCurrent(api, graph, keypair, user.msaId!); + if (!keyIsCurrent) { + const result = await getAddGraphKeyPayload(api, graph, keypair, user); + if (result) { + const { payload, proof } = result; + user.addGraphKey = () => api.tx.statefulStorage.applyItemActionsWithSignatureV2(user.keys!.publicKey, proof, payload); + } + } +} + +export async function createClearGraphExtrinsics(api: ApiPromise, user: ScenarioTypes.ChainUser) { + const clearGraphExtrinsics: (() => SubmittableExtrinsic<'promise', ISubmittableResult>)[] = []; + + const pages = await api.rpc.statefulStorage.getPaginatedStorage(user.msaId!, privateFollowSchemaId); + pages.toArray().forEach((page) => { + clearGraphExtrinsics.push(() => api.tx.statefulStorage.deletePage(user.msaId!, privateFollowSchemaId, page.page_id, page.content_hash)); + }); + + if (clearGraphExtrinsics.length > 0) { + user.resetGraph = clearGraphExtrinsics; + } +} diff --git a/setup/testing/index.ts b/setup/testing/index.ts index d9b0c1f..95a5432 100644 --- a/setup/testing/index.ts +++ b/setup/testing/index.ts @@ -2,138 +2,41 @@ /* eslint-disable no-restricted-syntax */ /* eslint-disable no-await-in-loop */ import '@frequency-chain/api-augment'; -import { MessageSourceId } from '@frequency-chain/api-augment/interfaces'; import { Keyring } from '@polkadot/keyring'; -import { KeyringPair } from '@polkadot/keyring/types'; -import { Vec } from '@polkadot/types'; -import { cryptoWaitReady } from '@polkadot/util-crypto'; -import { - AddProviderPayload, - ExtrinsicHelper, - UserBuilder, - signPayloadSr25519, - initialize, - Sr25519Signature, - ItemizedSignaturePayload, - EventError, -} from '@amplica-labs/frequency-scenario-template'; +import { cryptoWaitReady, mnemonicGenerate } from '@polkadot/util-crypto'; +import { ExtrinsicHelper, UserBuilder, initialize } from '@amplica-labs/frequency-scenario-template'; import log from 'loglevel'; import { ProviderGraph, GraphKeyPair as ProviderGraphKeyPair } from 'reconnection-service/src/interfaces/provider-graph.interface'; import fs from 'node:fs'; -import { SubmittableExtrinsic } from '@polkadot/api-base/types'; -import { ISubmittableResult } from '@polkadot/types/types'; -import { FrameSystemEventRecord } from '@polkadot/types/lookup'; -import { hexToU8a, u8aToHex } from '@polkadot/util'; -import { AddGraphKeyAction, AddKeyUpdate, ConnectionType, EnvironmentInterface, EnvironmentType, Graph, PrivacyType } from '@dsnp/graph-sdk'; +import { ConnectionType, EnvironmentInterface, EnvironmentType, Graph, PrivacyType } from '@dsnp/graph-sdk'; import { HexString } from '@polkadot/util/types'; +import * as ScenarioTypes from './types'; +import * as ScenarioConstants from './constants'; +import { getAddGraphKeyPayload, createClearGraphExtrinsics, graphKeypair } from './graph'; +import { getAddProviderPayload } from './msa'; +import { addExtrinsicToTrack, submitAndTrackExtrinsics } from './utils'; +import { Event } from '@polkadot/types/interfaces'; -type ChainUser = { - uri?: string; - keys?: KeyringPair; - msaId?: MessageSourceId; - create?: () => SubmittableExtrinsic<'promise', ISubmittableResult>; - addGraphKey?: () => SubmittableExtrinsic<'promise', ISubmittableResult>; - resetGraph?: (() => SubmittableExtrinsic<'promise', ISubmittableResult>)[]; -}; - -type ProviderResponse = { - dsnpId: string; - connections: { - data: ProviderGraph[]; - pagination?: { - pageNumber: number; - pageSize: number; - pageCount: number; - }; - }; - graphKeyPairs: ProviderGraphKeyPair[]; -}; - -let graphPublicKey: HexString = '0x0514f63edc89d414061bf451cc99b1f2b43fac920c351be60774559a31523c75'; -let graphPrivateKey: HexString = '0x1c15b6d1af4716615a4eb83a2dfba3284e1c0a199603572e7b95c164f7ad90e3'; let privateFollowSchemaId: number; -const CAPACITY_AMOUNT_TO_STAKE = 1_000_000_000_000_000n; const keyring = new Keyring({ type: 'sr25519' }); -const DEFAULT_SCHEMAS = [5, 7, 8, 9, 10]; -let nonce: number; let graph: Graph; -let graphKeyAction: any; -async function getAddGraphKeyPayload(graph: Graph, user: ChainUser): Promise<{ payload: ItemizedSignaturePayload; proof: Sr25519Signature }> { - if (!graphKeyAction) { - const actions = [ - { - type: 'AddGraphKey', - ownerDsnpUserId: '1', - newPublicKey: hexToU8a(graphPublicKey), - } as AddGraphKeyAction, - ]; - graph.applyActions(actions); - const keyExport = graph.exportUserGraphUpdates('1'); - - const bundle = keyExport[0] as AddKeyUpdate; - - const addAction = [ - { - Add: { - data: u8aToHex(bundle.payload), - }, - }, - ]; - graphKeyAction = { - targetHash: 0, - schemaId: 7, - actions: addAction, - }; - } - - const currentBlockNumber = (await ExtrinsicHelper.apiPromise.rpc.chain.getBlock()).block.header.number.toNumber(); - graphKeyAction.expiration = currentBlockNumber + ExtrinsicHelper.apiPromise.consts.msa.mortalityWindowSize.toNumber(); - const payloadBytes = ExtrinsicHelper.api.registry.createType('PalletStatefulStorageItemizedSignaturePayloadV2', graphKeyAction); - const proof = signPayloadSr25519(user.keys!, payloadBytes); - return { payload: { ...graphKeyAction }, proof }; -} - -async function getAddProviderPayload(user: ChainUser, provider: ChainUser): Promise<{ payload: AddProviderPayload; proof: Sr25519Signature }> { - const block = await ExtrinsicHelper.apiPromise.rpc.chain.getBlock(); - const blockNumber = block.block.header.number.toNumber(); - const mortalityWindowSize = ExtrinsicHelper.apiPromise.consts.msa.mortalityWindowSize.toNumber(); - const addProvider: AddProviderPayload = { - authorizedMsaId: provider.msaId, - schemaIds: DEFAULT_SCHEMAS, - expiration: blockNumber + mortalityWindowSize, - }; - const payload = ExtrinsicHelper.apiPromise.registry.createType('PalletMsaAddProvider', addProvider); - const proof = signPayloadSr25519(user.keys!, payload); - - return { payload: addProvider, proof }; -} - -async function populateExtrinsics(follower: ChainUser, provider: ChainUser): Promise { +async function populateExtrinsics(follower: ScenarioTypes.ChainUser, provider: ScenarioTypes.ChainUser): Promise { if (follower.msaId) { - // const existingUser = await ExtrinsicHelper.apiPromise.query.msa.publicKeyToMsaId(follower.keys!.publicKey); - // if (existingUser.isSome) { - // follower.msaId = existingUser.unwrap(); console.log(`Existing user ${follower.msaId.toString()} found`); - follower.resetGraph = []; - - const pages = await ExtrinsicHelper.apiPromise.rpc.statefulStorage.getPaginatedStorage(follower.msaId, privateFollowSchemaId); - // console.log('pages: ', pages.toHuman()); - pages.toArray().forEach((page) => { - console.log(`Enqueuing graph page removal for user ${follower.msaId?.toString()}, page ${page.page_id}, content_hash: ${page.content_hash.toNumber()}`); - follower.resetGraph?.push(() => - ExtrinsicHelper.apiPromise.tx.statefulStorage.deletePage(follower.msaId, privateFollowSchemaId, page.page_id.toNumber(), page.content_hash.toNumber()), - ); - }); + follower.resetGraph = await createClearGraphExtrinsics(ExtrinsicHelper.apiPromise, follower.msaId); return; } - const { payload: addProviderPayload, proof } = await getAddProviderPayload(follower, provider); + const { payload: addProviderPayload, proof } = await getAddProviderPayload(ExtrinsicHelper.apiPromise, follower, provider); follower.create = () => ExtrinsicHelper.apiPromise.tx.msa.createSponsoredAccountWithDelegation(follower.keys!.publicKey, proof, addProviderPayload); - const { payload: addGraphKeyPayload, proof: addGraphKeyProof } = await getAddGraphKeyPayload(graph, follower); - follower.addGraphKey = () => ExtrinsicHelper.apiPromise.tx.statefulStorage.applyItemActionsWithSignatureV2(follower.keys!.publicKey, addGraphKeyProof, addGraphKeyPayload); + const graphKeyPayload = await getAddGraphKeyPayload(ExtrinsicHelper.apiPromise, graph, graphKeypair, follower); + if (graphKeyPayload) { + const { payload: addGraphKeyPayload, proof: addGraphKeyProof } = graphKeyPayload; + follower.addGraphKey = () => ExtrinsicHelper.apiPromise.tx.statefulStorage.applyItemActionsWithSignatureV2(follower.keys!.publicKey, addGraphKeyProof, addGraphKeyPayload); + } } async function main() { @@ -143,9 +46,16 @@ async function main() { log.setLevel('trace'); const { apiPromise } = ExtrinsicHelper; - const provider: ChainUser = { keys: keyring.createFromUri('//Alice') }; - const famousUser: ChainUser = { uri: '//Bob', keys: keyring.createFromUri('//Bob') }; - const followers = new Map(); + const provider: ScenarioTypes.ChainUser = { + keys: keyring.createFromUri( + 'credit silent bridge invite buffalo start sell cement naive arch elbow frame also innocent unaware useless cabin mechanic beach suspect thrive crawl essay taxi', + ), + }; + // const provider: ChainUser = { keys: keyring.createFromUri('//Alice') }; + const bobSeed = mnemonicGenerate(); + // const famousUser: ChainUser = { uri: '//Bob', keys: keyring.createFromUri('//Bob') }; + const famousUser: ScenarioTypes.ChainUser = { uri: bobSeed, keys: keyring.createFromUri(bobSeed) }; + const followers = new Map(); // Create provider console.log('Creating provider...'); @@ -156,16 +66,19 @@ async function main() { // Ensure provider is staked const capacity = await ExtrinsicHelper.apiPromise.query.capacity.capacityLedger(provider.msaId); - if (capacity.isNone || capacity.unwrap().totalTokensStaked.toBigInt() < CAPACITY_AMOUNT_TO_STAKE) { - await ExtrinsicHelper.stake(provider.keys!, provider.msaId, CAPACITY_AMOUNT_TO_STAKE).signAndSend(); + if (capacity.isNone || capacity.unwrap().totalTokensStaked.toBigInt() < ScenarioConstants.CAPACITY_AMOUNT_TO_STAKE) { + await ExtrinsicHelper.stake(provider.keys!, provider.msaId, ScenarioConstants.CAPACITY_AMOUNT_TO_STAKE).signAndSend(); console.log(`Staked to provider`); } // Create all keypairs console.log('Creating keypairs...'); - new Array(7000).fill(0).forEach((_, index) => { - const keys = keyring.createFromUri(`//Charlie//${index}`); - followers.set(keys.address, { uri: `//Charlie//${index}`, keys, resetGraph: [] }); + new Array(10).fill(0).forEach((_) => { + // const keys = keyring.createFromUri(`//Charlie//${index}`); + // followers.set(keys.address, { uri: `//Charlie//${index}`, keys, resetGraph: [] }); + const seed = mnemonicGenerate(); + const keys = keyring.createFromUri(seed); + followers.set(keys.address, { keys, resetGraph: [] }); }); const followerAddresses = [...followers.keys()]; console.log('Created keypairs'); @@ -183,21 +96,12 @@ async function main() { followers.get(address)!.msaId = allMsas[index].unwrap(); } }); - const famouseMsa = await ExtrinsicHelper.apiPromise.query.msa.publicKeyToMsaId(famousUser.keys!.address); - if (famouseMsa.isSome) { - famousUser.msaId = famouseMsa.unwrap(); + const famousMsa = await ExtrinsicHelper.apiPromise.query.msa.publicKeyToMsaId(famousUser.keys!.address); + if (famousMsa.isSome) { + famousUser.msaId = famousMsa.unwrap(); console.log(`Found famous user ${famousUser.msaId.toString()}`); } - interface PromiseTracker { - resolve?: () => void; - reject?: (reason?: any) => void; - promise?: Promise; - numberPending: number; - } - - const allBatchesTracker: PromiseTracker = { numberPending: 0 }; - let followersToCreate = 0; let graphsToClear = 0; @@ -209,30 +113,30 @@ async function main() { }), populateExtrinsics(famousUser, provider), ]); - const extrinsics: SubmittableExtrinsic<'promise', ISubmittableResult>[] = []; + for (const follower of followers.values()) { if (follower?.create) { - extrinsics.push(follower.create()); + addExtrinsicToTrack(follower.create()); followersToCreate++; } if (follower?.addGraphKey) { - extrinsics.push(follower.addGraphKey()); + addExtrinsicToTrack(follower.addGraphKey()); } if (follower?.resetGraph?.length && follower.resetGraph?.length > 0) { - extrinsics.push(...follower.resetGraph.map((e) => e())); + follower.resetGraph.forEach((e) => addExtrinsicToTrack(e())); graphsToClear++; } } if (famousUser?.create) { - extrinsics.push(famousUser.create()); + addExtrinsicToTrack(famousUser.create()); followersToCreate++; } if (famousUser?.addGraphKey) { - extrinsics.push(famousUser.addGraphKey()); + addExtrinsicToTrack(famousUser.addGraphKey()); } if (famousUser?.resetGraph?.length && famousUser.resetGraph?.length > 0) { - extrinsics.push(...famousUser.resetGraph.map((e) => e())); + famousUser.resetGraph.forEach((e) => addExtrinsicToTrack(e())); graphsToClear++; } @@ -243,86 +147,22 @@ User graphs to clear: ${graphsToClear} let famousUserCreatedBlockHash: HexString | undefined; - if (extrinsics.length !== 0) { - console.log(`Enqueuing ${extrinsics.length} extrinsics for execution`); - - const maxBatch = apiPromise.consts.frequencyTxPayment.maximumCapacityBatchLength.toNumber(); - allBatchesTracker.promise = new Promise((resolve, reject) => { - allBatchesTracker.resolve = resolve; - allBatchesTracker.reject = reject; - allBatchesTracker.numberPending = 0; - }); - - // Subscribe to events on-chain and update accounts as MSAs are created - const unsubscribeEvents = await ExtrinsicHelper.apiPromise.query.system.events((events: Vec) => { - events.forEach((eventRecord) => { - const { event } = eventRecord; - if (ExtrinsicHelper.api.events.utility.BatchCompleted.is(event)) { - allBatchesTracker.numberPending -= 1; - if (allBatchesTracker.numberPending < 1) { - allBatchesTracker.numberPending = 0; - (allBatchesTracker?.resolve ?? (() => { }))(); - } - } else if (ExtrinsicHelper.api.events.msa.MsaCreated.is(event)) { - const { msaId, key } = event.data; - const address = key.toString(); - const follower = followers.get(address); - if (follower) { - follower.msaId = msaId; - followers.set(address, follower); - } else if (address === famousUser.keys!.address) { - famousUser.msaId = msaId; - famousUserCreatedBlockHash = events.createdAtHash?.toHex(); - } else { - console.error('Cannot find follower ', address); - } - } - }); - }); - - const unsubBlocks = await ExtrinsicHelper.apiPromise.rpc.chain.subscribeFinalizedHeads(() => { - const count = extrinsics.length + allBatchesTracker.numberPending; - console.log(`Extrinsincs remaining: ${count}`); - if (count === 0) { - unsubBlocks(); - } - }); - - nonce = (await apiPromise.query.system.account(provider.keys!.publicKey)).nonce.toNumber(); - - while (extrinsics.length > 0) { - if (allBatchesTracker.numberPending < 100) { - const xToPost = extrinsics.splice(0, maxBatch); - allBatchesTracker.numberPending += 1; - // eslint-disable-next-line no-loop-func - const unsub = await ExtrinsicHelper.apiPromise.tx.frequencyTxPayment.payWithCapacityBatchAll(xToPost).signAndSend(provider.keys!, { nonce: nonce++ }, (x) => { - const { status } = x; - // console.log(status.type); - // console.dir(x.toHuman()); - // x.events.forEach((e) => console.dir(e.event.toHuman())); - if (x.dispatchError) { - unsub(); - (allBatchesTracker?.reject ?? (() => { }))(new EventError(x.dispatchError)); - } else if (status.isInvalid) { - unsub(); - console.log(x.toHuman()); - (allBatchesTracker?.reject ?? (() => { }))(new Error('Extrinsic failed: Invalid')); - } else if (x.isFinalized) { - unsub(); - } - }); + await submitAndTrackExtrinsics(apiPromise, provider, (event: Event) => { + if (apiPromise.events.msa.MsaCreated.is(event)) { + const { msaId, key } = event.data; + const address = key.toString(); + const follower = followers.get(address); + if (follower) { + follower.msaId = msaId; + followers.set(address, follower); + } else if (address === famousUser.keys!.address) { + famousUser.msaId = msaId; + famousUserCreatedBlockHash = event.createdAtHash?.toHex(); } else { - await allBatchesTracker.promise; - allBatchesTracker.promise = new Promise((resolve, reject) => { - allBatchesTracker.resolve = resolve; - allBatchesTracker.reject = reject; - }); + console.error('Cannot find follower ', address); } } - - await allBatchesTracker.promise; - unsubscribeEvents(); - } + }); if (famousUserCreatedBlockHash) { const block = await apiPromise.rpc.chain.getBlock(famousUserCreatedBlockHash); @@ -334,7 +174,7 @@ User graphs to clear: ${graphsToClear} fs.mkdirSync('webhook-specification/mock-webhook-server/responses'); } followers.forEach((follower) => { - const response: ProviderResponse = { + const response: ScenarioTypes.ProviderResponse = { dsnpId: follower.msaId!.toString(), connections: { data: [ @@ -349,8 +189,8 @@ User graphs to clear: ${graphsToClear} graphKeyPairs: [ { keyType: 'X25519', - publicKey: graphPublicKey, - privateKey: graphPrivateKey, + publicKey: ScenarioConstants.graphPublicKey, + privateKey: ScenarioConstants.graphPrivateKey, } as ProviderGraphKeyPair, ], }; @@ -363,14 +203,14 @@ User graphs to clear: ${graphsToClear} graphKeyPairs: [ { keyType: 'X25519', - publicKey: graphPublicKey, - privateKey: graphPrivateKey, + publicKey: ScenarioConstants.graphPublicKey, + privateKey: ScenarioConstants.graphPrivateKey, } as ProviderGraphKeyPair, ], }; - const responses: ProviderResponse[] = []; - const allFollowers = [...followers.values()] + const responses: ScenarioTypes.ProviderResponse[] = []; + const allFollowers = [...followers.values()]; while (allFollowers.length > 0) { const followerSlice = allFollowers.splice(0, Math.min(allFollowers.length, 50)); const response = { diff --git a/setup/testing/msa.ts b/setup/testing/msa.ts new file mode 100644 index 0000000..2cabde6 --- /dev/null +++ b/setup/testing/msa.ts @@ -0,0 +1,32 @@ +import { ApiPromise } from '@polkadot/api'; +import * as ScenarioConstants from './constants'; +import * as ScenarioTypes from './types'; +import { AddProviderPayload, Sr25519Signature, signPayloadSr25519 } from '@amplica-labs/frequency-scenario-template'; +import { getExpiration } from './utils'; + +export async function getAddProviderPayload( + api: ApiPromise, + user: ScenarioTypes.ChainUser, + provider: ScenarioTypes.ChainUser, +): Promise<{ payload: AddProviderPayload; proof: Sr25519Signature }> { + const addProvider: AddProviderPayload = { + authorizedMsaId: provider.msaId, + schemaIds: ScenarioConstants.DEFAULT_SCHEMAS, + expiration: await getExpiration(api), + }; + const payload = api.registry.createType('PalletMsaAddProvider', addProvider); + const proof = signPayloadSr25519(user.keys!, payload); + + return { payload: addProvider, proof }; +} + +export async function getCreateUserExtrinsic(api: ApiPromise, user: ScenarioTypes.ChainUser, provider: ScenarioTypes.ChainUser): Promise { + if (user?.msaId) { + return; + } + + const { payload, proof } = await getAddProviderPayload(api, user, provider); + user.create = () => api.tx.msa.createSponsoredAccountWithDelegation(user.keys!.publicKey, proof, payload); +} + +export async function resolveUsers(api: ApiPromise) {} diff --git a/setup/testing/package-lock.json b/setup/testing/package-lock.json index 9fa5615..836e107 100644 --- a/setup/testing/package-lock.json +++ b/setup/testing/package-lock.json @@ -45,10 +45,10 @@ "@nestjs/schedule": "^4.0.2", "@nestjs/testing": "^10.3.8", "@nestjs/typeorm": "^10.0.2", - "@polkadot/api": "^11.0.2", - "@polkadot/api-base": "^11.0.2", + "@polkadot/api": "^10.12.4", + "@polkadot/api-base": "^10.12.4", "@polkadot/keyring": "^12.6.2", - "@polkadot/types": "^11.0.2", + "@polkadot/types": "^10.12.4", "@polkadot/util": "^12.6.2", "@polkadot/util-crypto": "^12.6.2", "@songkeys/nestjs-redis": "^10.0.0", @@ -64,7 +64,6 @@ "devDependencies": { "@eslint/js": "^9.2.0", "@jest/globals": "^29.7.0", - "@polkadot/typegen": "11.0.2", "@types/jest": "^29.5.12", "@types/time-constants": "^1.0.2", "dotenv": "^16.4.5", @@ -3525,123 +3524,6 @@ "node": ">=18" } }, - "../../node_modules/@polkadot/typegen": { - "version": "11.0.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@polkadot/api": "11.0.2", - "@polkadot/api-augment": "11.0.2", - "@polkadot/rpc-augment": "11.0.2", - "@polkadot/rpc-provider": "11.0.2", - "@polkadot/types": "11.0.2", - "@polkadot/types-augment": "11.0.2", - "@polkadot/types-codec": "11.0.2", - "@polkadot/types-create": "11.0.2", - "@polkadot/types-support": "11.0.2", - "@polkadot/util": "^12.6.2", - "@polkadot/util-crypto": "^12.6.2", - "@polkadot/x-ws": "^12.6.2", - "handlebars": "^4.7.8", - "tslib": "^2.6.2", - "yargs": "^17.7.2" - }, - "bin": { - "polkadot-types-chain-info": "scripts/polkadot-types-chain-info.mjs", - "polkadot-types-from-chain": "scripts/polkadot-types-from-chain.mjs", - "polkadot-types-from-defs": "scripts/polkadot-types-from-defs.mjs", - "polkadot-types-internal-interfaces": "scripts/polkadot-types-internal-interfaces.mjs", - "polkadot-types-internal-metadata": "scripts/polkadot-types-internal-metadata.mjs" - }, - "engines": { - "node": ">=18" - } - }, - "../../node_modules/@polkadot/typegen/node_modules/@polkadot-api/json-rpc-provider": { - "version": "0.0.1", - "dev": true, - "license": "MIT", - "optional": true - }, - "../../node_modules/@polkadot/typegen/node_modules/@polkadot-api/json-rpc-provider-proxy": { - "version": "0.0.1", - "dev": true, - "license": "MIT", - "optional": true - }, - "../../node_modules/@polkadot/typegen/node_modules/@polkadot-api/substrate-client": { - "version": "0.0.1", - "dev": true, - "license": "MIT", - "optional": true - }, - "../../node_modules/@polkadot/typegen/node_modules/@polkadot/rpc-provider": { - "version": "11.0.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@polkadot/keyring": "^12.6.2", - "@polkadot/types": "11.0.2", - "@polkadot/types-support": "11.0.2", - "@polkadot/util": "^12.6.2", - "@polkadot/util-crypto": "^12.6.2", - "@polkadot/x-fetch": "^12.6.2", - "@polkadot/x-global": "^12.6.2", - "@polkadot/x-ws": "^12.6.2", - "eventemitter3": "^5.0.1", - "mock-socket": "^9.3.1", - "nock": "^13.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@substrate/connect": "0.8.10" - } - }, - "../../node_modules/@polkadot/typegen/node_modules/@polkadot/types-support": { - "version": "11.0.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@polkadot/util": "^12.6.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18" - } - }, - "../../node_modules/@polkadot/typegen/node_modules/@substrate/connect": { - "version": "0.8.10", - "dev": true, - "license": "GPL-3.0-only", - "optional": true, - "dependencies": { - "@substrate/connect-extension-protocol": "^2.0.0", - "@substrate/connect-known-chains": "^1.1.4", - "@substrate/light-client-extension-helpers": "^0.0.6", - "smoldot": "2.0.22" - } - }, - "../../node_modules/@polkadot/typegen/node_modules/@substrate/light-client-extension-helpers": { - "version": "0.0.6", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@polkadot-api/json-rpc-provider": "0.0.1", - "@polkadot-api/json-rpc-provider-proxy": "0.0.1", - "@polkadot-api/observable-client": "0.1.0", - "@polkadot-api/substrate-client": "0.0.1", - "@substrate/connect-extension-protocol": "^2.0.0", - "@substrate/connect-known-chains": "^1.1.4", - "rxjs": "^7.8.1" - }, - "peerDependencies": { - "smoldot": "2.x" - } - }, "../../node_modules/@polkadot/types": { "version": "11.0.2", "license": "Apache-2.0", @@ -7234,26 +7116,6 @@ "dev": true, "license": "MIT" }, - "../../node_modules/handlebars": { - "version": "4.7.8", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, "../../node_modules/has-flag": { "version": "4.0.0", "license": "MIT", @@ -8731,11 +8593,6 @@ "node": ">= 0.6" } }, - "../../node_modules/neo-async": { - "version": "2.6.2", - "dev": true, - "license": "MIT" - }, "../../node_modules/no-case": { "version": "3.0.4", "dev": true, @@ -10575,18 +10432,6 @@ } } }, - "../../node_modules/uglify-js": { - "version": "3.17.4", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "../../node_modules/uid": { "version": "2.0.2", "license": "MIT", @@ -10764,11 +10609,6 @@ "node": ">=0.10.0" } }, - "../../node_modules/wordwrap": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, "../../node_modules/wrap-ansi": { "version": "7.0.0", "license": "MIT", diff --git a/setup/testing/package.json b/setup/testing/package.json index 3c94ca8..0bd22c3 100644 --- a/setup/testing/package.json +++ b/setup/testing/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "e2e test setup", "main": "index.ts", + "type": "module", "scripts": { "start": "tsx index.ts" }, diff --git a/setup/testing/reset_chain b/setup/testing/reset_chain index 48f9ceb..c106232 100755 --- a/setup/testing/reset_chain +++ b/setup/testing/reset_chain @@ -1,5 +1,5 @@ #/bin/bash -docker container stop frequency-interval +docker container stop frequency docker run --rm -v reconnection-service_chainstorage:/data busybox rm -rf /data/chains -docker container start frequency-interval +docker container start frequency diff --git a/setup/testing/tsconfig.json b/setup/testing/tsconfig.json new file mode 100644 index 0000000..97833fd --- /dev/null +++ b/setup/testing/tsconfig.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Base", + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "baseUrl": "./src", + "esModuleInterop": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "module": "CommonJS", + "moduleResolution": "node", + "noImplicitAny": false, + "noImplicitThis": false, + "outDir": "dist", + "paths": { + "#app/*": [ + "*" + ] + }, + "resolveJsonModule": true, + "sourceMap": true, + "strict": true, + "skipLibCheck": true, + "strictPropertyInitialization": false, + "target": "es2022", + "typeRoots": [ + "node_modules/@types" + ], + "noEmit": false + }, + "include": [ + "./**/*.ts" + ], + "exclude": [ + "node_modules/**", + "./dist/**", + "/tools/**", + "./setup/**", + "./**/*.spec.ts", + "./jest.init.ts" + ] +} diff --git a/setup/testing/types.ts b/setup/testing/types.ts new file mode 100644 index 0000000..1285cfb --- /dev/null +++ b/setup/testing/types.ts @@ -0,0 +1,49 @@ +import '@frequency-chain/api-augment'; +import { MessageSourceId } from '@frequency-chain/api-augment/interfaces'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { SubmittableExtrinsic } from '@polkadot/api-base/types'; +import { ISubmittableResult } from '@polkadot/types/types'; +import { ProviderGraph, GraphKeyPair as ProviderGraphKeyPair } from 'reconnection-service/src/interfaces/provider-graph.interface'; +import { HexString } from '@polkadot/util/types'; + +export interface ChainUser { + uri?: string; + keys?: KeyringPair; + msaId?: MessageSourceId; + createdAtBlock?: number; + create?: () => SubmittableExtrinsic<'promise', ISubmittableResult>; + addGraphKey?: () => SubmittableExtrinsic<'promise', ISubmittableResult>; + resetGraph?: (() => SubmittableExtrinsic<'promise', ISubmittableResult>)[]; +} + +export interface ProviderResponse { + dsnpId: string; + connections: { + data: ProviderGraph[]; + pagination?: { + pageNumber: number; + pageSize: number; + pageCount: number; + }; + }; + graphKeyPairs: ProviderGraphKeyPair[]; +} + +export class PromiseTracker { + resolve: () => void; + reject: (reason?: any) => void; + promise: Promise; + pendingTxnCount = 0; + pendingExtrinsics = new Set(); + + constructor() { + this.resetPromise(); + } + + resetPromise() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } +} diff --git a/setup/testing/utils.ts b/setup/testing/utils.ts new file mode 100644 index 0000000..0fb4c6a --- /dev/null +++ b/setup/testing/utils.ts @@ -0,0 +1,197 @@ +import { ApiPromise } from '@polkadot/api'; +import { SubmittableExtrinsic } from '@polkadot/api-base/types'; +import { GenericExtrinsic, u16 } from '@polkadot/types'; +import { AnyTuple, ISubmittableResult } from '@polkadot/types/types'; +import * as ScenarioTypes from './types'; +import * as ScenarioConstants from './constants'; +import { Block, Event, Header } from '@polkadot/types/interfaces'; +import { EventError, ExtrinsicHelper, UserBuilder, initialize, keyring } from '@amplica-labs/frequency-scenario-template'; +import { HexString } from '@polkadot/util/types'; +import { EnvironmentInterface, EnvironmentType, Graph } from '@dsnp/graph-sdk'; +import { cryptoWaitReady } from '@polkadot/util-crypto'; +import log from 'loglevel'; + +export let privateFollowSchemaId: u16; +export let publicGraphKeySchemaId: u16; +export let provider: ScenarioTypes.ChainUser; +export let famousUser: ScenarioTypes.ChainUser; +export const followers = new Map(); +export let api: ApiPromise; +const graphEnvironment: EnvironmentInterface = { environmentType: EnvironmentType.TestnetPaseo }; +export const graph = new Graph(graphEnvironment); + +const extrinsics: SubmittableExtrinsic<'promise', ISubmittableResult>[] = []; + +export async function initScenario() { + const frequencyUrl = process.env['FREQUENCY_URL']; + const seedPhrase = process.env['PASEO_PROVIDER_SEED_PHRASE']; + if (!frequencyUrl) { + throw new Error('FREQUENCY_URL not defined'); + } + if (!seedPhrase) { + throw new Error('PASEO_PROVIDER_SEED_PHRASE not defined'); + } + + await cryptoWaitReady(); + console.log('Connecting...'); + await initialize(frequencyUrl); + log.setLevel('trace'); + api = ExtrinsicHelper.apiPromise; + + await initializeSchemaIds(api); + + provider = { + keys: keyring.createFromUri(seedPhrase), + }; + + const famousUserUri = `${seedPhrase}//famous`; + famousUser = { uri: famousUserUri, keys: keyring.createFromUri(famousUserUri) }; + + // Create provider + console.log('Resolving/creating provider...'); + const builder = new UserBuilder(); + const providerUser = await builder.withKeypair(provider.keys!).asProvider('Test Provider').withFundingSource(provider.keys).build(); + provider.msaId = providerUser.msaId; + console.log(`Found or created provider ${provider.msaId!.toString()}`); + + // Ensure provider is staked + const capacity = await api.query.capacity.capacityLedger(provider.msaId); + if (capacity.isNone || capacity.unwrap().totalTokensStaked.toBigInt() < ScenarioConstants.CAPACITY_AMOUNT_TO_STAKE) { + await ExtrinsicHelper.stake(provider.keys!, provider.msaId, ScenarioConstants.CAPACITY_AMOUNT_TO_STAKE).signAndSend(); + console.log(`Brought provider capacity stake up to ${ScenarioConstants.CAPACITY_AMOUNT_TO_STAKE}`); + } + + // Create all keypairs + console.log(`Creating ${ScenarioConstants.NUM_FOLLOWERS} keypairs...`); + new Array(ScenarioConstants.NUM_FOLLOWERS).fill(0).forEach((_, index) => { + const followerSeed = `${seedPhrase}//${index}`; + const keys = keyring.createFromUri(followerSeed); + followers.set(keys.address, { uri: followerSeed, keys, resetGraph: [] }); + followers.set(keys.address, { keys }); + }); + const followerAddresses = [...followers.keys()]; + console.log(`Created ${ScenarioConstants.NUM_FOLLOWERS} keypairs`); + + // Get any existing users so we don't create new ones + const allMsas = await api.query.msa.publicKeyToMsaId.multi([...followerAddresses]); + followerAddresses.forEach((address, index) => { + if (!allMsas[index].isNone) { + followers.get(address)!.msaId = allMsas[index].unwrap(); + } + }); + const famousMsa = await api.query.msa.publicKeyToMsaId(famousUser.keys!.address); + if (famousMsa.isSome) { + famousUser.msaId = famousMsa.unwrap(); + console.log(`Found famous user ${famousUser.msaId.toString()}`); + } +} + +export async function initializeSchemaIds(api: ApiPromise) { + let schemaId = (await api.query.schemas.schemaNameToIds('dsnp', 'public-key-key-agreement')).ids.pop(); + if (schemaId) { + publicGraphKeySchemaId = schemaId; + } + + schemaId = (await api.query.schemas.schemaNameToIds('dsnp', 'private-follows')).ids.pop(); + if (schemaId) { + privateFollowSchemaId = schemaId; + } + + if (!privateFollowSchemaId || !publicGraphKeySchemaId) { + throw new Error('Unable to determine schema IDs'); + } +} + +export async function getExpiration(api: ApiPromise, blocks?: number): Promise { + const currentBlockNumber = (await api.rpc.chain.getBlock()).block.header.number.toNumber(); + + return currentBlockNumber + (blocks ? blocks : api.consts.msa.mortalityWindowSize.toNumber()); +} + +export function addExtrinsicToTrack(extrinsic: SubmittableExtrinsic<'promise', ISubmittableResult>) { + extrinsics.push(extrinsic); +} + +export async function submitAndTrackExtrinsics( + api: ApiPromise, + provider: ScenarioTypes.ChainUser, + eventCallback?: (block: Block, extrinsic: GenericExtrinsic, eventRecord: Event) => void, +) { + if (!extrinsics.length) { + return; + } + + console.log(`Enqueueing ${extrinsics.length} transactions for execution`); + + const maxBatch = api.consts.frequencyTxPayment.maximumCapacityBatchLength.toNumber(); + const tracker = new ScenarioTypes.PromiseTracker(); + + // Log how many tracked extrinsics remain after each new block + const unsubBlocks = await api.rpc.chain.subscribeFinalizedHeads((header: Header) => { + api.rpc.chain.getBlock(header.hash).then((signedBlock) => { + api.at(header.hash).then((apiAt) => { + apiAt.query.system.events().then((allEvents) => { + // Map events to their extrinsics & filter to only ones we're watching + signedBlock.block.extrinsics.forEach((extrinsic, index) => { + if (tracker.pendingExtrinsics.has(extrinsic.hash.toString() as HexString)) { + allEvents + .filter(({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.eq(index)) + .forEach((eventRecord) => { + const { event } = eventRecord; + if (eventCallback) { + eventCallback(signedBlock.block, extrinsic, event); + } + + if (api.events.utility.BatchCompleted.is(event)) { + tracker.pendingExtrinsics.delete(extrinsic.hash.toString() as HexString); + } + + if (api.events.utility.ItemCompleted.is(event)) { + tracker.pendingTxnCount -= 1; + if (tracker.pendingTxnCount < 1) { + tracker.pendingTxnCount = 0; + tracker.resolve(); + } + } + }); + } + }); + + const count = extrinsics.length + tracker.pendingTxnCount; + console.log(`Transactions remaining: ${count}`); + if (count === 0) { + unsubBlocks(); + } + }); + }); + }); + }); + + // TODO: Nonce only works on Testnet if nobody else is executing extrinsics for this provider + let nonce = (await api.query.system.account(provider.keys!.publicKey)).nonce.toNumber(); + + while (extrinsics.length > 0) { + if (tracker.pendingExtrinsics.size < ScenarioConstants.MAX_EXTRINSICS_TO_SUBMIT) { + const xToPost = extrinsics.splice(0, maxBatch); + tracker.pendingTxnCount += xToPost.length; + const unsubTx = await api.tx.frequencyTxPayment.payWithCapacityBatchAll(xToPost).signAndSend(provider.keys!, { nonce: nonce++ }, (x) => { + tracker.pendingExtrinsics.add(x.txHash.toString() as HexString); + if (x.dispatchError) { + unsubTx(); + tracker.reject(new EventError(x.dispatchError)); + } else if (x.status.isInvalid) { + unsubTx(); + console.log(x.toHuman()); + tracker.reject(new Error('Extrinsic failed: Invalid')); + } else if (x.isFinalized) { + unsubTx(); + } + }); + } else { + await tracker.promise; + tracker.resetPromise(); + } + } + + await tracker.promise; +}