From 0a9538d6814c8ab608c7be2fd21de6b2e3e5a769 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Tue, 28 May 2024 17:54:29 +0200 Subject: [PATCH 01/58] initial agents baseline #79 --- packages/server/guides/SUBSCRIPTION.md | 2 +- packages/server/src/agents/local.ts | 47 + packages/server/src/agents/plugin.ts | 45 + packages/server/src/agents/types.ts | 19 + .../xcm}/matching.spec.ts | 6 +- .../monitoring => agents/xcm}/matching.ts | 10 +- .../xcm}/ops/bridge.spec.ts | 0 .../xcm}/ops/common.spec.ts | 0 .../monitoring => agents/xcm}/ops/common.ts | 7 +- .../monitoring => agents/xcm}/ops/criteria.ts | 5 +- .../monitoring => agents/xcm}/ops/dmp.spec.ts | 0 .../monitoring => agents/xcm}/ops/dmp.ts | 9 +- .../xcm}/ops/pk-bridge.ts | 12 +- .../xcm}/ops/relay.spec.ts | 0 .../monitoring => agents/xcm}/ops/relay.ts | 6 +- .../monitoring => agents/xcm}/ops/ump.spec.ts | 0 .../monitoring => agents/xcm}/ops/ump.ts | 10 +- .../xcm}/ops/util.spec.ts | 0 .../monitoring => agents/xcm}/ops/util.ts | 7 +- .../xcm}/ops/xcm-format.spec.ts | 0 .../xcm}/ops/xcm-format.ts | 0 .../xcm}/ops/xcm-types.ts | 0 .../xcm}/ops/xcmp.spec.ts | 0 .../monitoring => agents/xcm}/ops/xcmp.ts | 10 +- .../xcm}/types-augmented.ts | 4 +- packages/server/src/agents/xcm/types.ts | 796 ++++++++++++++++ packages/server/src/agents/xcm/xcm-agent.ts | 851 +++++++++++++++++ packages/server/src/lib.ts | 7 +- packages/server/src/server.ts | 4 + .../src/services/monitoring/api/routes.ts | 36 +- .../src/services/monitoring/api/ws/plugin.ts | 8 +- .../services/monitoring/api/ws/protocol.ts | 34 +- .../server/src/services/monitoring/plugin.ts | 2 +- .../src/services/monitoring/switchboard.ts | 873 +----------------- .../server/src/services/monitoring/types.ts | 793 +--------------- .../server/src/services/notification/hub.ts | 3 +- .../server/src/services/notification/log.ts | 6 +- .../server/src/services/notification/types.ts | 3 +- .../src/services/notification/webhook.ts | 7 +- .../server/src/services/persistence/subs.ts | 66 +- .../src/services/telemetry/metrics/engine.ts | 2 +- .../src/services/telemetry/metrics/index.ts | 2 +- .../src/services/telemetry/metrics/ws.ts | 4 +- .../server/src/services/telemetry/types.ts | 12 +- packages/server/src/services/types.ts | 6 +- packages/server/src/types.ts | 9 + 46 files changed, 1959 insertions(+), 1764 deletions(-) create mode 100644 packages/server/src/agents/local.ts create mode 100644 packages/server/src/agents/plugin.ts create mode 100644 packages/server/src/agents/types.ts rename packages/server/src/{services/monitoring => agents/xcm}/matching.spec.ts (98%) rename packages/server/src/{services/monitoring => agents/xcm}/matching.ts (99%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/bridge.spec.ts (100%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/common.spec.ts (100%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/common.ts (95%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/criteria.ts (87%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/dmp.spec.ts (100%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/dmp.ts (98%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/pk-bridge.ts (96%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/relay.spec.ts (100%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/relay.ts (93%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/ump.spec.ts (100%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/ump.ts (95%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/util.spec.ts (100%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/util.ts (97%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/xcm-format.spec.ts (100%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/xcm-format.ts (100%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/xcm-types.ts (100%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/xcmp.spec.ts (100%) rename packages/server/src/{services/monitoring => agents/xcm}/ops/xcmp.ts (95%) rename packages/server/src/{services/monitoring => agents/xcm}/types-augmented.ts (85%) create mode 100644 packages/server/src/agents/xcm/types.ts create mode 100644 packages/server/src/agents/xcm/xcm-agent.ts diff --git a/packages/server/guides/SUBSCRIPTION.md b/packages/server/guides/SUBSCRIPTION.md index 7362921f..6974ab8c 100644 --- a/packages/server/guides/SUBSCRIPTION.md +++ b/packages/server/guides/SUBSCRIPTION.md @@ -88,7 +88,7 @@ You can check the [Hurl requests](/~https://github.com/sodazone/ocelloids-services `POST /subs` ```shell -curl 'http://127.0.0.1:3000/subs' \ +curl -H "Content-Type: application/json" 'http://127.0.0.1:3000/subs' \ --data '{ "id": "test-sub", "origin": "urn:ocn:polkadot:0", diff --git a/packages/server/src/agents/local.ts b/packages/server/src/agents/local.ts new file mode 100644 index 00000000..dfc4e23d --- /dev/null +++ b/packages/server/src/agents/local.ts @@ -0,0 +1,47 @@ +import { Logger, Services } from '../services/index.js' +import { AgentId } from '../services/monitoring/types.js' +import { AgentServiceOptions } from '../types.js' +import { Agent, AgentService } from './types.js' +import { XCMAgent } from './xcm/xcm-agent.js' + +export class LocalAgentService implements AgentService { + readonly #log: Logger + readonly #agents: Record + + constructor(ctx: Services, _options: AgentServiceOptions) { + this.#log = ctx.log + this.#agents = this.#loadAgents(ctx) + } + + getAgentIds(): AgentId[] { + return Object.keys(this.#agents) + } + + getAgentById(agentId: AgentId): Agent { + if (this.#agents[agentId]) { + return this.#agents[agentId] + } + throw new Error(`Agent not found with id=${agentId}`) + } + + async start() { + for (const [id, agent] of Object.entries(this.#agents)) { + this.#log.info('[agents:local] Starting agent %s', id) + await agent.start() + } + } + + async stop() { + for (const [id, agent] of Object.entries(this.#agents)) { + this.#log.info('[agents:local] Stopping agent %s', id) + await agent.stop() + } + } + + #loadAgents(ctx: Services) { + const xcm = new XCMAgent(ctx) + return { + [xcm.id]: xcm, + } + } +} diff --git a/packages/server/src/agents/plugin.ts b/packages/server/src/agents/plugin.ts new file mode 100644 index 00000000..800f8b17 --- /dev/null +++ b/packages/server/src/agents/plugin.ts @@ -0,0 +1,45 @@ +import { FastifyPluginAsync } from 'fastify' +import fp from 'fastify-plugin' + +import { AgentServiceMode, AgentServiceOptions } from '../types.js' +import { LocalAgentService } from './local.js' +import { AgentService } from './types.js' + +declare module 'fastify' { + interface FastifyInstance { + agentService: AgentService + } +} + +/** + * Fastify plug-in for instantiating an {@link AgentService} instance. + * + * @param fastify The Fastify instance. + * @param options Options for configuring the IngressConsumer. + */ +const agentServicePlugin: FastifyPluginAsync = async (fastify, options) => { + if (options.mode !== AgentServiceMode.local) { + throw new Error('Only local agent service is supported') + } + const service: AgentService = new LocalAgentService(fastify, options) + + fastify.addHook('onClose', (server, done) => { + service + .stop() + .then(() => { + server.log.info('Agent service stopped') + }) + .catch((error: any) => { + server.log.error(error, 'Error while stopping agent service') + }) + .finally(() => { + done() + }) + }) + + fastify.decorate('agentService', service) + + await service.start() +} + +export default fp(agentServicePlugin, { fastify: '>=4.x', name: 'agent-service' }) diff --git a/packages/server/src/agents/types.ts b/packages/server/src/agents/types.ts new file mode 100644 index 00000000..928761af --- /dev/null +++ b/packages/server/src/agents/types.ts @@ -0,0 +1,19 @@ +import { AgentId, Subscription } from '../services/monitoring/types.js' + +export interface AgentService { + getAgentById(agentId: AgentId): Agent + getAgentIds(): AgentId[] + start(): Promise + stop(): Promise +} + +export interface Agent { + get id(): AgentId + getSubscriptionHandler(subscriptionId: string): Subscription + subscribe(subscription: Subscription): Promise + // TODO + // update(s: ):void + unsubscribe(subscriptionId: string): Promise + stop(): Promise + start(): Promise +} diff --git a/packages/server/src/services/monitoring/matching.spec.ts b/packages/server/src/agents/xcm/matching.spec.ts similarity index 98% rename from packages/server/src/services/monitoring/matching.spec.ts rename to packages/server/src/agents/xcm/matching.spec.ts index 56c8c1ae..304ab2a3 100644 --- a/packages/server/src/services/monitoring/matching.spec.ts +++ b/packages/server/src/agents/xcm/matching.spec.ts @@ -3,13 +3,13 @@ import { jest } from '@jest/globals' import { MemoryLevel as Level } from 'memory-level' import { AbstractSublevel } from 'abstract-level' +import { XcmInbound, XcmNotificationType, XcmNotifyMessage, XcmSent } from '../../services/monitoring/types.js' +import { Janitor } from '../../services/persistence/janitor.js' +import { jsonEncoded, prefixes } from '../../services/types.js' import { matchBridgeMessages } from '../../testing/bridge/matching.js' import { matchHopMessages, matchMessages, realHopMessages } from '../../testing/matching.js' import { _services } from '../../testing/services.js' -import { Janitor } from '../persistence/janitor.js' -import { jsonEncoded, prefixes } from '../types.js' import { MatchingEngine } from './matching.js' -import { XcmInbound, XcmNotificationType, XcmNotifyMessage, XcmSent } from './types.js' describe('message matching engine', () => { let engine: MatchingEngine diff --git a/packages/server/src/services/monitoring/matching.ts b/packages/server/src/agents/xcm/matching.ts similarity index 99% rename from packages/server/src/services/monitoring/matching.ts rename to packages/server/src/agents/xcm/matching.ts index dfea2421..85dffff9 100644 --- a/packages/server/src/services/monitoring/matching.ts +++ b/packages/server/src/agents/xcm/matching.ts @@ -3,7 +3,6 @@ import EventEmitter from 'node:events' import { AbstractSublevel } from 'abstract-level' import { Mutex } from 'async-mutex' -import { DB, Logger, Services, jsonEncoded, prefixes } from '../types.js' import { GenericXcmBridge, GenericXcmHop, @@ -23,11 +22,12 @@ import { XcmSent, XcmTimeout, XcmWaypointContext, -} from './types.js' +} from 'agents/xcm/types.js' +import { DB, Logger, Services, jsonEncoded, prefixes } from '../../services/types.js' -import { getRelayId, isOnSameConsensus } from '../config.js' -import { Janitor, JanitorTask } from '../persistence/janitor.js' -import { TelemetryEventEmitter } from '../telemetry/types.js' +import { getRelayId, isOnSameConsensus } from '../../services/config.js' +import { Janitor, JanitorTask } from '../../services/persistence/janitor.js' +import { TelemetryEventEmitter } from '../../services/telemetry/types.js' export type XcmMatchedReceiver = (message: XcmNotifyMessage) => Promise | void type SubLevel = AbstractSublevel diff --git a/packages/server/src/services/monitoring/ops/bridge.spec.ts b/packages/server/src/agents/xcm/ops/bridge.spec.ts similarity index 100% rename from packages/server/src/services/monitoring/ops/bridge.spec.ts rename to packages/server/src/agents/xcm/ops/bridge.spec.ts diff --git a/packages/server/src/services/monitoring/ops/common.spec.ts b/packages/server/src/agents/xcm/ops/common.spec.ts similarity index 100% rename from packages/server/src/services/monitoring/ops/common.spec.ts rename to packages/server/src/agents/xcm/ops/common.spec.ts diff --git a/packages/server/src/services/monitoring/ops/common.ts b/packages/server/src/agents/xcm/ops/common.ts similarity index 95% rename from packages/server/src/services/monitoring/ops/common.ts rename to packages/server/src/agents/xcm/ops/common.ts index 9cfae3a1..3a76f817 100644 --- a/packages/server/src/services/monitoring/ops/common.ts +++ b/packages/server/src/agents/xcm/ops/common.ts @@ -6,9 +6,10 @@ import { hexToU8a, stringToU8a, u8aConcat } from '@polkadot/util' import { blake2AsHex } from '@polkadot/util-crypto' import { types } from '@sodazone/ocelloids-sdk' -import { createNetworkId, getChainId, getConsensus, isOnSameConsensus } from '../../config.js' -import { NetworkURN } from '../../types.js' -import { AnyJson, GenericXcmSent, HexString, Leg, XcmSent, XcmSentWithContext } from '../types.js' +import { GenericXcmSent, Leg, XcmSent, XcmSentWithContext } from 'agents/xcm/types.js' +import { createNetworkId, getChainId, getConsensus, isOnSameConsensus } from '../../../services/config.js' +import { AnyJson, HexString } from '../../../services/monitoring/types.js' +import { NetworkURN } from '../../../services/types.js' import { getBridgeHubNetworkId, getParaIdFromJunctions, diff --git a/packages/server/src/services/monitoring/ops/criteria.ts b/packages/server/src/agents/xcm/ops/criteria.ts similarity index 87% rename from packages/server/src/services/monitoring/ops/criteria.ts rename to packages/server/src/agents/xcm/ops/criteria.ts index b7c42bd7..9a991bfe 100644 --- a/packages/server/src/services/monitoring/ops/criteria.ts +++ b/packages/server/src/agents/xcm/ops/criteria.ts @@ -1,6 +1,7 @@ import { ControlQuery, Criteria } from '@sodazone/ocelloids-sdk' -import { NetworkURN } from '../../types.js' -import { SignerData, XcmSent } from '../types.js' +import { XcmSent } from 'agents/xcm/types.js' +import { SignerData } from '../../../services/monitoring/types.js' +import { NetworkURN } from '../../../services/types.js' export function sendersCriteria(senders?: string[] | '*'): Criteria { if (senders === undefined || senders === '*') { diff --git a/packages/server/src/services/monitoring/ops/dmp.spec.ts b/packages/server/src/agents/xcm/ops/dmp.spec.ts similarity index 100% rename from packages/server/src/services/monitoring/ops/dmp.spec.ts rename to packages/server/src/agents/xcm/ops/dmp.spec.ts diff --git a/packages/server/src/services/monitoring/ops/dmp.ts b/packages/server/src/agents/xcm/ops/dmp.ts similarity index 98% rename from packages/server/src/services/monitoring/ops/dmp.ts rename to packages/server/src/agents/xcm/ops/dmp.ts index a4b1b3c9..8e74718c 100644 --- a/packages/server/src/services/monitoring/ops/dmp.ts +++ b/packages/server/src/agents/xcm/ops/dmp.ts @@ -10,16 +10,15 @@ import type { Registry } from '@polkadot/types/types' import { filterNonNull, types } from '@sodazone/ocelloids-sdk' -import { NetworkURN } from '../../types.js' -import { GetDownwardMessageQueues } from '../types-augmented.js' import { - AnyJson, GenericXcmInboundWithContext, GenericXcmSentWithContext, - SignerData, XcmInboundWithContext, XcmSentWithContext, -} from '../types.js' +} from 'agents/xcm/types.js' +import { AnyJson, SignerData } from '../../../services/monitoring/types.js' +import { NetworkURN } from '../../../services/types.js' +import { GetDownwardMessageQueues } from '../types-augmented.js' import { blockEventToHuman } from './common.js' import { getMessageId, diff --git a/packages/server/src/services/monitoring/ops/pk-bridge.ts b/packages/server/src/agents/xcm/ops/pk-bridge.ts similarity index 96% rename from packages/server/src/services/monitoring/ops/pk-bridge.ts rename to packages/server/src/agents/xcm/ops/pk-bridge.ts index 780e254a..f1ab068f 100644 --- a/packages/server/src/services/monitoring/ops/pk-bridge.ts +++ b/packages/server/src/agents/xcm/ops/pk-bridge.ts @@ -7,19 +7,19 @@ import { Observable, filter, from, mergeMap } from 'rxjs' import { types } from '@sodazone/ocelloids-sdk' -import { getConsensus } from '../../config.js' -import { NetworkURN } from '../../types.js' -import { bridgeStorageKeys } from '../storage.js' -import { GetStorageAt } from '../types-augmented.js' import { GenericXcmBridgeAcceptedWithContext, GenericXcmBridgeDeliveredWithContext, GenericXcmBridgeInboundWithContext, - HexString, XcmBridgeAcceptedWithContext, XcmBridgeDeliveredWithContext, XcmBridgeInboundWithContext, -} from '../types.js' +} from 'agents/xcm/types.js' +import { getConsensus } from '../../../services/config.js' +import { bridgeStorageKeys } from '../../../services/monitoring/storage.js' +import { HexString } from '../../../services/monitoring/types.js' +import { NetworkURN } from '../../../services/types.js' +import { GetStorageAt } from '../types-augmented.js' import { blockEventToHuman } from './common.js' import { getMessageId, getSendersFromEvent, matchEvent, networkIdFromInteriorLocation } from './util.js' import { diff --git a/packages/server/src/services/monitoring/ops/relay.spec.ts b/packages/server/src/agents/xcm/ops/relay.spec.ts similarity index 100% rename from packages/server/src/services/monitoring/ops/relay.spec.ts rename to packages/server/src/agents/xcm/ops/relay.spec.ts diff --git a/packages/server/src/services/monitoring/ops/relay.ts b/packages/server/src/agents/xcm/ops/relay.ts similarity index 93% rename from packages/server/src/services/monitoring/ops/relay.ts rename to packages/server/src/agents/xcm/ops/relay.ts index 55834365..b057d2b6 100644 --- a/packages/server/src/services/monitoring/ops/relay.ts +++ b/packages/server/src/agents/xcm/ops/relay.ts @@ -4,9 +4,9 @@ import type { PolkadotPrimitivesV6InherentData } from '@polkadot/types/lookup' import type { Registry } from '@polkadot/types/types' import { ControlQuery, filterNonNull, types } from '@sodazone/ocelloids-sdk' -import { createNetworkId, getChainId } from '../../config.js' -import { NetworkURN } from '../../types.js' -import { GenericXcmRelayedWithContext, XcmRelayedWithContext } from '../types.js' +import { GenericXcmRelayedWithContext, XcmRelayedWithContext } from 'agents/xcm/types.js' +import { createNetworkId, getChainId } from '../../../services/config.js' +import { NetworkURN } from '../../../services/types.js' import { getMessageId, matchExtrinsic } from './util.js' import { fromXcmpFormat } from './xcm-format.js' diff --git a/packages/server/src/services/monitoring/ops/ump.spec.ts b/packages/server/src/agents/xcm/ops/ump.spec.ts similarity index 100% rename from packages/server/src/services/monitoring/ops/ump.spec.ts rename to packages/server/src/agents/xcm/ops/ump.spec.ts diff --git a/packages/server/src/services/monitoring/ops/ump.ts b/packages/server/src/agents/xcm/ops/ump.ts similarity index 95% rename from packages/server/src/services/monitoring/ops/ump.ts rename to packages/server/src/agents/xcm/ops/ump.ts index 0439a15e..bf2210ee 100644 --- a/packages/server/src/services/monitoring/ops/ump.ts +++ b/packages/server/src/agents/xcm/ops/ump.ts @@ -6,16 +6,16 @@ import type { Registry } from '@polkadot/types/types' import { filterNonNull, types } from '@sodazone/ocelloids-sdk' -import { getChainId, getRelayId } from '../../config.js' -import { NetworkURN } from '../../types.js' -import { GetOutboundUmpMessages } from '../types-augmented.js' import { GenericXcmInboundWithContext, GenericXcmSentWithContext, - MessageQueueEventContext, XcmInboundWithContext, XcmSentWithContext, -} from '../types.js' +} from 'agents/xcm/types.js' +import { getChainId, getRelayId } from '../../../services/config.js' +import { NetworkURN } from '../../../services/types.js' +import { GetOutboundUmpMessages } from '../types-augmented.js' +import { MessageQueueEventContext } from '../types.js' import { blockEventToHuman, xcmMessagesSent } from './common.js' import { getMessageId, getParaIdFromOrigin, mapAssetsTrapped, matchEvent } from './util.js' import { asVersionedXcm } from './xcm-format.js' diff --git a/packages/server/src/services/monitoring/ops/util.spec.ts b/packages/server/src/agents/xcm/ops/util.spec.ts similarity index 100% rename from packages/server/src/services/monitoring/ops/util.spec.ts rename to packages/server/src/agents/xcm/ops/util.spec.ts diff --git a/packages/server/src/services/monitoring/ops/util.ts b/packages/server/src/agents/xcm/ops/util.ts similarity index 97% rename from packages/server/src/services/monitoring/ops/util.ts rename to packages/server/src/agents/xcm/ops/util.ts index abbf922c..91cdecc8 100644 --- a/packages/server/src/services/monitoring/ops/util.ts +++ b/packages/server/src/agents/xcm/ops/util.ts @@ -13,9 +13,10 @@ import type { import { types } from '@sodazone/ocelloids-sdk' -import { GlobalConsensus, createNetworkId, getConsensus, isGlobalConsensus } from '../../config.js' -import { NetworkURN } from '../../types.js' -import { AssetsTrapped, HexString, SignerData, TrappedAsset } from '../types.js' +import { AssetsTrapped, TrappedAsset } from 'agents/xcm/types.js' +import { GlobalConsensus, createNetworkId, getConsensus, isGlobalConsensus } from '../../../services/config.js' +import { HexString, SignerData } from '../../../services/monitoring/types.js' +import { NetworkURN } from '../../../services/types.js' import { VersionedInteriorLocation, XcmV4AssetAssets, diff --git a/packages/server/src/services/monitoring/ops/xcm-format.spec.ts b/packages/server/src/agents/xcm/ops/xcm-format.spec.ts similarity index 100% rename from packages/server/src/services/monitoring/ops/xcm-format.spec.ts rename to packages/server/src/agents/xcm/ops/xcm-format.spec.ts diff --git a/packages/server/src/services/monitoring/ops/xcm-format.ts b/packages/server/src/agents/xcm/ops/xcm-format.ts similarity index 100% rename from packages/server/src/services/monitoring/ops/xcm-format.ts rename to packages/server/src/agents/xcm/ops/xcm-format.ts diff --git a/packages/server/src/services/monitoring/ops/xcm-types.ts b/packages/server/src/agents/xcm/ops/xcm-types.ts similarity index 100% rename from packages/server/src/services/monitoring/ops/xcm-types.ts rename to packages/server/src/agents/xcm/ops/xcm-types.ts diff --git a/packages/server/src/services/monitoring/ops/xcmp.spec.ts b/packages/server/src/agents/xcm/ops/xcmp.spec.ts similarity index 100% rename from packages/server/src/services/monitoring/ops/xcmp.spec.ts rename to packages/server/src/agents/xcm/ops/xcmp.spec.ts diff --git a/packages/server/src/services/monitoring/ops/xcmp.ts b/packages/server/src/agents/xcm/ops/xcmp.ts similarity index 95% rename from packages/server/src/services/monitoring/ops/xcmp.ts rename to packages/server/src/agents/xcm/ops/xcmp.ts index 9fb8ea51..a7f8df49 100644 --- a/packages/server/src/services/monitoring/ops/xcmp.ts +++ b/packages/server/src/agents/xcm/ops/xcmp.ts @@ -3,16 +3,16 @@ import { Observable, bufferCount, filter, map, mergeMap } from 'rxjs' import { filterNonNull, types } from '@sodazone/ocelloids-sdk' -import { createNetworkId } from '../../config.js' -import { NetworkURN } from '../../types.js' -import { GetOutboundHrmpMessages } from '../types-augmented.js' import { GenericXcmInboundWithContext, GenericXcmSentWithContext, - MessageQueueEventContext, XcmInboundWithContext, XcmSentWithContext, -} from '../types.js' +} from 'agents/xcm/types.js' +import { createNetworkId } from '../../../services/config.js' +import { NetworkURN } from '../../../services/types.js' +import { GetOutboundHrmpMessages } from '../types-augmented.js' +import { MessageQueueEventContext } from '../types.js' import { blockEventToHuman, xcmMessagesSent } from './common.js' import { getMessageId, mapAssetsTrapped, matchEvent } from './util.js' import { fromXcmpFormat } from './xcm-format.js' diff --git a/packages/server/src/services/monitoring/types-augmented.ts b/packages/server/src/agents/xcm/types-augmented.ts similarity index 85% rename from packages/server/src/services/monitoring/types-augmented.ts rename to packages/server/src/agents/xcm/types-augmented.ts index e7a429c7..a97d8b8b 100644 --- a/packages/server/src/services/monitoring/types-augmented.ts +++ b/packages/server/src/agents/xcm/types-augmented.ts @@ -5,8 +5,8 @@ import type { PolkadotCorePrimitivesInboundDownwardMessage, PolkadotCorePrimitivesOutboundHrmpMessage, } from '@polkadot/types/lookup' -import { NetworkURN } from '../types.js' -import { HexString } from './types.js' +import { HexString } from '../../services/monitoring/types.js' +import { NetworkURN } from '../../services/types.js' export type GetOutboundHrmpMessages = (hash: HexString) => Observable> diff --git a/packages/server/src/agents/xcm/types.ts b/packages/server/src/agents/xcm/types.ts new file mode 100644 index 00000000..f4d8dc86 --- /dev/null +++ b/packages/server/src/agents/xcm/types.ts @@ -0,0 +1,796 @@ +import type { U8aFixed, bool } from '@polkadot/types-codec' +import type { + FrameSupportMessagesProcessMessageError, + PolkadotRuntimeParachainsInclusionAggregateMessageOrigin, +} from '@polkadot/types/lookup' +import { ControlQuery } from '@sodazone/ocelloids-sdk' +import { z } from 'zod' + +import { createNetworkId } from '../../services/config.js' +import { + AnyJson, + HexString, + RxSubscriptionWithId, + SignerData, + Subscription, + toHexString, +} from '../../services/monitoring/types.js' +import { NetworkURN } from '../../services/types.js' + +export type Monitor = { + subs: RxSubscriptionWithId[] + controls: Record +} + +function distinct(a: Array) { + return Array.from(new Set(a)) +} + +export type XCMSubscriptionHandler = { + originSubs: RxSubscriptionWithId[] + destinationSubs: RxSubscriptionWithId[] + bridgeSubs: BridgeSubscription[] + sendersControl: ControlQuery + messageControl: ControlQuery + descriptor: Subscription + args: XCMSubscriptionArgs + relaySub?: RxSubscriptionWithId +} + +const bridgeTypes = ['pk-bridge', 'snowbridge'] as const + +export type BridgeType = (typeof bridgeTypes)[number] + +export type BridgeSubscription = { type: BridgeType; subs: RxSubscriptionWithId[] } + +export type XcmCriteria = { + sendersControl: ControlQuery + messageControl: ControlQuery +} + +export type XcmWithContext = { + event?: AnyJson + extrinsicId?: string + blockNumber: string | number + blockHash: HexString + messageHash: HexString + messageId?: HexString +} +/** + * Represents the asset that has been trapped. + * + * @public + */ + +export type TrappedAsset = { + version: number + id: { + type: string + value: AnyJson + } + fungible: boolean + amount: string | number + assetInstance?: AnyJson +} +/** + * Event emitted when assets are trapped. + * + * @public + */ + +export type AssetsTrapped = { + assets: TrappedAsset[] + hash: HexString + event: AnyJson +} +/** + * Represents an XCM program bytes and human JSON. + */ + +export type XcmProgram = { + bytes: Uint8Array + json: AnyJson +} + +export interface XcmSentWithContext extends XcmWithContext { + messageData: Uint8Array + recipient: NetworkURN + sender?: SignerData + instructions: XcmProgram +} + +export interface XcmBridgeAcceptedWithContext extends XcmWithContext { + chainId: NetworkURN + bridgeKey: HexString + messageData: HexString + instructions: AnyJson + recipient: NetworkURN + forwardId?: HexString +} + +export interface XcmBridgeDeliveredWithContext { + chainId: NetworkURN + bridgeKey: HexString + event?: AnyJson + extrinsicId?: string + blockNumber: string | number + blockHash: HexString + sender?: SignerData +} + +export interface XcmBridgeAcceptedWithContext extends XcmWithContext { + chainId: NetworkURN + bridgeKey: HexString + messageData: HexString + instructions: AnyJson + recipient: NetworkURN + forwardId?: HexString +} + +export interface XcmBridgeDeliveredWithContext { + chainId: NetworkURN + bridgeKey: HexString + event?: AnyJson + extrinsicId?: string + blockNumber: string | number + blockHash: HexString + sender?: SignerData +} + +export interface XcmBridgeInboundWithContext { + chainId: NetworkURN + bridgeKey: HexString + blockNumber: string | number + blockHash: HexString + outcome: 'Success' | 'Fail' + error: AnyJson + event?: AnyJson + extrinsicId?: string +} + +export interface XcmBridgeInboundWithContext { + chainId: NetworkURN + bridgeKey: HexString + blockNumber: string | number + blockHash: HexString + outcome: 'Success' | 'Fail' + error: AnyJson + event?: AnyJson + extrinsicId?: string +} + +export interface XcmInboundWithContext extends XcmWithContext { + outcome: 'Success' | 'Fail' + error: AnyJson + assetsTrapped?: AssetsTrapped +} + +export interface XcmRelayedWithContext extends XcmInboundWithContext { + recipient: NetworkURN + origin: NetworkURN +} + +export class GenericXcmRelayedWithContext implements XcmRelayedWithContext { + event: AnyJson + extrinsicId?: string + blockNumber: string | number + blockHash: HexString + messageHash: HexString + messageId?: HexString + recipient: NetworkURN + origin: NetworkURN + outcome: 'Success' | 'Fail' + error: AnyJson + + constructor(msg: XcmRelayedWithContext) { + this.event = msg.event + this.messageHash = msg.messageHash + this.messageId = msg.messageId ?? msg.messageHash + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.recipient = msg.recipient + this.origin = msg.origin + this.outcome = msg.outcome + this.error = msg.error + } + + toHuman(_isExpanded?: boolean | undefined): Record { + return { + messageHash: this.messageHash, + messageId: this.messageId, + extrinsicId: this.extrinsicId, + blockHash: this.blockHash, + blockNumber: this.blockNumber, + event: this.event, + recipient: this.recipient, + origin: this.origin, + outcome: this.outcome, + error: this.error, + } + } +} + +export class GenericXcmInboundWithContext implements XcmInboundWithContext { + event: AnyJson + extrinsicId?: string | undefined + blockNumber: string + blockHash: HexString + messageHash: HexString + messageId: HexString + outcome: 'Success' | 'Fail' + error: AnyJson + assetsTrapped?: AssetsTrapped | undefined + + constructor(msg: XcmInboundWithContext) { + this.event = msg.event + this.messageHash = msg.messageHash + this.messageId = msg.messageId ?? msg.messageHash + this.outcome = msg.outcome + this.error = msg.error + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.assetsTrapped = msg.assetsTrapped + } + + toHuman(_isExpanded?: boolean | undefined): Record { + return { + messageHash: this.messageHash, + messageId: this.messageId, + extrinsicId: this.extrinsicId, + blockHash: this.blockHash, + blockNumber: this.blockNumber, + event: this.event, + outcome: this.outcome, + error: this.error, + assetsTrapped: this.assetsTrapped, + } + } +} + +export class XcmInbound { + subscriptionId: string + chainId: NetworkURN + event: AnyJson + messageHash: HexString + messageId: HexString + outcome: 'Success' | 'Fail' + error: AnyJson + blockHash: HexString + blockNumber: string + extrinsicId?: string + assetsTrapped?: AssetsTrapped + + constructor(subscriptionId: string, chainId: NetworkURN, msg: XcmInboundWithContext) { + this.subscriptionId = subscriptionId + this.chainId = chainId + this.event = msg.event + this.messageHash = msg.messageHash + this.messageId = msg.messageId ?? msg.messageHash + this.outcome = msg.outcome + this.error = msg.error + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.assetsTrapped = msg.assetsTrapped + } +} + +export class GenericXcmSentWithContext implements XcmSentWithContext { + messageData: Uint8Array + recipient: NetworkURN + instructions: XcmProgram + messageHash: HexString + event: AnyJson + blockHash: HexString + blockNumber: string + sender?: SignerData + extrinsicId?: string + messageId?: HexString + + constructor(msg: XcmSentWithContext) { + this.event = msg.event + this.messageData = msg.messageData + this.recipient = msg.recipient + this.instructions = msg.instructions + this.messageHash = msg.messageHash + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.messageId = msg.messageId + this.sender = msg.sender + } + + toHuman(_isExpanded?: boolean | undefined): Record { + return { + messageData: toHexString(this.messageData), + recipient: this.recipient, + instructions: this.instructions.json, + messageHash: this.messageHash, + event: this.event, + blockHash: this.blockHash, + blockNumber: this.blockNumber, + extrinsicId: this.extrinsicId, + messageId: this.messageId, + senders: this.sender, + } + } +} + +export class GenericXcmBridgeAcceptedWithContext implements XcmBridgeAcceptedWithContext { + chainId: NetworkURN + bridgeKey: HexString + messageData: HexString + recipient: NetworkURN + instructions: AnyJson + messageHash: HexString + event: AnyJson + blockHash: HexString + blockNumber: string + extrinsicId?: string + messageId?: HexString + forwardId?: HexString + + constructor(msg: XcmBridgeAcceptedWithContext) { + this.chainId = msg.chainId + this.bridgeKey = msg.bridgeKey + this.event = msg.event + this.messageData = msg.messageData + this.recipient = msg.recipient + this.instructions = msg.instructions + this.messageHash = msg.messageHash + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.messageId = msg.messageId + this.forwardId = msg.forwardId + } +} + +export class GenericXcmBridgeDeliveredWithContext implements XcmBridgeDeliveredWithContext { + chainId: NetworkURN + bridgeKey: HexString + event?: AnyJson + extrinsicId?: string + blockNumber: string + blockHash: HexString + sender?: SignerData + + constructor(msg: XcmBridgeDeliveredWithContext) { + this.chainId = msg.chainId + this.bridgeKey = msg.bridgeKey + this.event = msg.event + this.extrinsicId = msg.extrinsicId + this.blockNumber = msg.blockNumber.toString() + this.blockHash = msg.blockHash + this.sender = msg.sender + } +} + +export class GenericXcmBridgeInboundWithContext implements XcmBridgeInboundWithContext { + chainId: NetworkURN + bridgeKey: HexString + event: AnyJson + extrinsicId?: string | undefined + blockNumber: string + blockHash: HexString + outcome: 'Success' | 'Fail' + error: AnyJson + + constructor(msg: XcmBridgeInboundWithContext) { + this.chainId = msg.chainId + this.event = msg.event + this.outcome = msg.outcome + this.error = msg.error + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.bridgeKey = msg.bridgeKey + } +} + +export enum XcmNotificationType { + Sent = 'xcm.sent', + Received = 'xcm.received', + Relayed = 'xcm.relayed', + Timeout = 'xcm.timeout', + Hop = 'xcm.hop', + Bridge = 'xcm.bridge', +} +/** + * The terminal point of an XCM journey. + * + * @public + */ + +export type XcmTerminus = { + chainId: NetworkURN +} +/** + * The terminal point of an XCM journey with contextual information. + * + * @public + */ + +export interface XcmTerminusContext extends XcmTerminus { + blockNumber: string + blockHash: HexString + extrinsicId?: string + event: AnyJson + outcome: 'Success' | 'Fail' + error: AnyJson + messageHash: HexString + messageData: string + instructions: AnyJson +} +/** + * The contextual information of an XCM journey waypoint. + * + * @public + */ + +export interface XcmWaypointContext extends XcmTerminusContext { + legIndex: number + assetsTrapped?: AnyJson +} +/** + * Type of an XCM journey leg. + * + * @public + */ + +export const legType = ['bridge', 'hop', 'hrmp', 'vmp'] as const +/** + * A leg of an XCM journey. + * + * @public + */ + +export type Leg = { + from: NetworkURN + to: NetworkURN + relay?: NetworkURN + type: (typeof legType)[number] +} +/** + * Event emitted when an XCM is sent. + * + * @public + */ + +export interface XcmSent { + type: XcmNotificationType + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString +} + +export class GenericXcmSent implements XcmSent { + type: XcmNotificationType = XcmNotificationType.Sent + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor( + subscriptionId: string, + chainId: NetworkURN, + msg: XcmSentWithContext, + legs: Leg[], + forwardId?: HexString + ) { + this.subscriptionId = subscriptionId + this.legs = legs + this.origin = { + chainId, + blockHash: msg.blockHash, + blockNumber: msg.blockNumber.toString(), + extrinsicId: msg.extrinsicId, + event: msg.event, + outcome: 'Success', + error: null, + messageData: toHexString(msg.messageData), + instructions: msg.instructions.json, + messageHash: msg.messageHash, + } + this.destination = { + chainId: legs[legs.length - 1].to, // last stop is the destination + } + this.waypoint = { + ...this.origin, + legIndex: 0, + messageData: toHexString(msg.messageData), + instructions: msg.instructions.json, + messageHash: msg.messageHash, + } + + this.messageId = msg.messageId + this.forwardId = forwardId + this.sender = msg.sender + } +} +/** + * Event emitted when an XCM is received. + * + * @public + */ + +export interface XcmReceived { + type: XcmNotificationType + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminusContext + sender?: SignerData + messageId?: HexString + forwardId?: HexString +} +/** + * Event emitted when an XCM is not received within a specified timeframe. + * + * @public + */ + +export type XcmTimeout = XcmSent + +export class GenericXcmTimeout implements XcmTimeout { + type: XcmNotificationType = XcmNotificationType.Timeout + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor(msg: XcmSent) { + this.subscriptionId = msg.subscriptionId + this.legs = msg.legs + this.origin = msg.origin + this.destination = msg.destination + this.waypoint = msg.waypoint + this.messageId = msg.messageId + this.sender = msg.sender + this.forwardId = msg.forwardId + } +} + +export class GenericXcmReceived implements XcmReceived { + type: XcmNotificationType = XcmNotificationType.Received + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminusContext + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor(outMsg: XcmSent, inMsg: XcmInbound) { + this.subscriptionId = outMsg.subscriptionId + this.legs = outMsg.legs + this.destination = { + chainId: inMsg.chainId, + blockNumber: inMsg.blockNumber, + blockHash: inMsg.blockHash, + extrinsicId: inMsg.extrinsicId, + event: inMsg.event, + outcome: inMsg.outcome, + error: inMsg.error, + instructions: outMsg.waypoint.instructions, + messageData: outMsg.waypoint.messageData, + messageHash: outMsg.waypoint.messageHash, + } + this.origin = outMsg.origin + this.waypoint = { + ...this.destination, + legIndex: this.legs.findIndex((l) => l.to === inMsg.chainId && l.type !== 'bridge'), + instructions: outMsg.waypoint.instructions, + messageData: outMsg.waypoint.messageData, + messageHash: outMsg.waypoint.messageHash, + assetsTrapped: inMsg.assetsTrapped, + } + this.sender = outMsg.sender + this.messageId = outMsg.messageId + this.forwardId = outMsg.forwardId + } +} +/** + * Event emitted when an XCM is received on the relay chain + * for an HRMP message. + * + * @public + */ + +export type XcmRelayed = XcmSent + +export class GenericXcmRelayed implements XcmRelayed { + type: XcmNotificationType = XcmNotificationType.Relayed + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor(outMsg: XcmSent, relayMsg: XcmRelayedWithContext) { + this.subscriptionId = outMsg.subscriptionId + this.legs = outMsg.legs + this.destination = outMsg.destination + this.origin = outMsg.origin + this.waypoint = { + legIndex: outMsg.legs.findIndex((l) => l.from === relayMsg.origin && l.relay !== undefined), + chainId: createNetworkId(relayMsg.origin, '0'), // relay waypoint always at relay chain + blockNumber: relayMsg.blockNumber.toString(), + blockHash: relayMsg.blockHash, + extrinsicId: relayMsg.extrinsicId, + event: relayMsg.event, + outcome: relayMsg.outcome, + error: relayMsg.error, + instructions: outMsg.waypoint.instructions, + messageData: outMsg.waypoint.messageData, + messageHash: outMsg.waypoint.messageHash, + } + this.sender = outMsg.sender + this.messageId = outMsg.messageId + this.forwardId = outMsg.forwardId + } +} +/** + * Event emitted when an XCM is sent or received on an intermediate stop. + * + * @public + */ + +export interface XcmHop extends XcmSent { + direction: 'out' | 'in' +} + +export class GenericXcmHop implements XcmHop { + type: XcmNotificationType = XcmNotificationType.Hop + direction: 'out' | 'in' + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor(originMsg: XcmSent, hopWaypoint: XcmWaypointContext, direction: 'out' | 'in') { + this.subscriptionId = originMsg.subscriptionId + this.legs = originMsg.legs + this.origin = originMsg.origin + this.destination = originMsg.destination + this.waypoint = hopWaypoint + this.messageId = originMsg.messageId + this.sender = originMsg.sender + this.direction = direction + this.forwardId = originMsg.forwardId + } +} + +export type BridgeMessageType = 'accepted' | 'delivered' | 'received' +/** + * Event emitted when an XCM is sent or received on an intermediate stop. + * + * @public + */ + +export interface XcmBridge extends XcmSent { + bridgeKey: HexString + bridgeMessageType: BridgeMessageType +} +type XcmBridgeContext = { + bridgeMessageType: BridgeMessageType + bridgeKey: HexString + forwardId?: HexString +} + +export class GenericXcmBridge implements XcmBridge { + type: XcmNotificationType = XcmNotificationType.Bridge + bridgeMessageType: BridgeMessageType + subscriptionId: string + bridgeKey: HexString + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor( + originMsg: XcmSent, + waypoint: XcmWaypointContext, + { bridgeKey, bridgeMessageType, forwardId }: XcmBridgeContext + ) { + this.subscriptionId = originMsg.subscriptionId + this.bridgeMessageType = bridgeMessageType + this.legs = originMsg.legs + this.origin = originMsg.origin + this.destination = originMsg.destination + this.waypoint = waypoint + this.messageId = originMsg.messageId + this.sender = originMsg.sender + this.bridgeKey = bridgeKey + this.forwardId = forwardId + } +} +/** + * The XCM event types. + * + * @public + */ + +export type XcmNotifyMessage = XcmSent | XcmReceived | XcmRelayed | XcmHop | XcmBridge + +export function isXcmSent(object: any): object is XcmSent { + return object.type !== undefined && object.type === XcmNotificationType.Sent +} + +export function isXcmReceived(object: any): object is XcmReceived { + return object.type !== undefined && object.type === XcmNotificationType.Received +} + +export function isXcmHop(object: any): object is XcmHop { + return object.type !== undefined && object.type === XcmNotificationType.Hop +} + +export function isXcmRelayed(object: any): object is XcmRelayed { + return object.type !== undefined && object.type === XcmNotificationType.Relayed +} + +const XCM_NOTIFICATION_TYPE_ERROR = `at least 1 event type is required [${Object.values(XcmNotificationType).join( + ',' +)}]` + +const XCM_OUTBOUND_TTL_TYPE_ERROR = 'XCM outbound message TTL should be at least 6 seconds' + +export const $XCMSubscriptionArgs = z.object({ + origin: z + .string({ + required_error: 'origin id is required', + }) + .min(1), + senders: z.optional( + z.literal('*').or(z.array(z.string()).min(1, 'at least 1 sender address is required').transform(distinct)) + ), + destinations: z + .array( + z + .string({ + required_error: 'destination id is required', + }) + .min(1) + ) + .transform(distinct), + bridges: z.optional(z.array(z.enum(bridgeTypes)).min(1, 'Please specify at least one bridge.')), + // prevent using $refs + events: z.optional(z.literal('*').or(z.array(z.nativeEnum(XcmNotificationType)).min(1, XCM_NOTIFICATION_TYPE_ERROR))), + outboundTTL: z.optional(z.number().min(6000, XCM_OUTBOUND_TTL_TYPE_ERROR).max(Number.MAX_SAFE_INTEGER)), +}) + +export type XCMSubscriptionArgs = z.infer + +export type MessageQueueEventContext = { + id: U8aFixed + origin: PolkadotRuntimeParachainsInclusionAggregateMessageOrigin + success?: bool + error?: FrameSupportMessagesProcessMessageError +} diff --git a/packages/server/src/agents/xcm/xcm-agent.ts b/packages/server/src/agents/xcm/xcm-agent.ts new file mode 100644 index 00000000..15699611 --- /dev/null +++ b/packages/server/src/agents/xcm/xcm-agent.ts @@ -0,0 +1,851 @@ +import { Registry } from '@polkadot/types-codec/types' +import { ControlQuery, extractEvents, extractTxWithEvents, flattenCalls, types } from '@sodazone/ocelloids-sdk' +import { Observable, filter, from, map, share, switchMap } from 'rxjs' + +import { AgentId, HexString, RxSubscriptionWithId, Subscription } from '../../services/monitoring/types.js' +import { Logger, NetworkURN, Services } from '../../services/types.js' +import { extractXcmpReceive, extractXcmpSend } from './ops/xcmp.js' +import { + $XCMSubscriptionArgs, + BridgeSubscription, + BridgeType, + Monitor, + XCMSubscriptionArgs, + XCMSubscriptionHandler, + XcmBridgeAcceptedWithContext, + XcmBridgeDeliveredWithContext, + XcmBridgeInboundWithContext, + XcmInbound, + XcmInboundWithContext, + XcmNotificationType, + XcmNotifyMessage, + XcmRelayedWithContext, + XcmSentWithContext, +} from './types.js' + +import { SubsStore } from '../../services/persistence/subs.js' +import { MatchingEngine } from './matching.js' + +import { ValidationError, errorMessage } from '../../errors.js' +import { IngressConsumer } from '../../services/ingress/index.js' +import { mapXcmSent } from './ops/common.js' +import { matchMessage, matchSenders, messageCriteria, sendersCriteria } from './ops/criteria.js' +import { extractDmpReceive, extractDmpSend, extractDmpSendByEvent } from './ops/dmp.js' +import { extractRelayReceive } from './ops/relay.js' +import { extractUmpReceive, extractUmpSend } from './ops/ump.js' + +import { getChainId, getConsensus } from '../../services/config.js' +import { + dmpDownwardMessageQueuesKey, + parachainSystemHrmpOutboundMessages, + parachainSystemUpwardMessages, +} from '../../services/monitoring/storage.js' +import { Agent } from '../types.js' +import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './ops/pk-bridge.js' +import { getBridgeHubNetworkId } from './ops/util.js' +import { + GetDownwardMessageQueues, + GetOutboundHrmpMessages, + GetOutboundUmpMessages, + GetStorageAt, +} from './types-augmented.js' + +const SUB_ERROR_RETRY_MS = 5000 + +export class XCMAgent implements Agent { + readonly #subs: Record = {} + readonly #log: Logger + readonly #engine: MatchingEngine + readonly #timeouts: NodeJS.Timeout[] = [] + readonly #db: SubsStore + readonly #ingress: IngressConsumer + + #shared: { + blockEvents: Record> + blockExtrinsics: Record> + } + + constructor(ctx: Services) { + const { log, ingressConsumer, subsStore } = ctx + + this.#log = log + this.#ingress = ingressConsumer + this.#db = subsStore + this.#engine = new MatchingEngine(ctx, this.#onXcmWaypointReached.bind(this)) + + this.#shared = { + blockEvents: {}, + blockExtrinsics: {}, + } + } + + get id(): AgentId { + return 'xcm' + } + + getSubscriptionHandler(id: string): Subscription { + if (this.#subs[id]) { + return this.#subs[id].descriptor + } else { + throw Error('subscription handler not found') + } + } + + async subscribe(s: Subscription): Promise { + const args = $XCMSubscriptionArgs.parse(s.args) + // TODO validate? + // const dests = qs.destinations as NetworkURN[] + // this.#validateChainIds([origin, ...dests]) + + if (!s.ephemeral) { + await this.#db.insert(s) + } + + this.#monitor(s, args) + } + + async unsubscribe(id: string): Promise { + if (this.#subs[id] === undefined) { + this.#log.warn('unsubscribe from a non-existent subscription %s', id) + return + } + + try { + const { + descriptor: { ephemeral }, + args: { origin }, + originSubs, + destinationSubs, + relaySub, + } = this.#subs[id] + + this.#log.info('[%s] unsubscribe %s', origin, id) + + originSubs.forEach(({ sub }) => sub.unsubscribe()) + destinationSubs.forEach(({ sub }) => sub.unsubscribe()) + if (relaySub) { + relaySub.sub.unsubscribe() + } + delete this.#subs[id] + + await this.#engine.clearPendingStates(id) + + if (!ephemeral) { + await this.#db.remove(id) + } + } catch (error) { + this.#log.error(error, 'Error unsubscribing %s', id) + } + } + async stop(): Promise { + for (const { + descriptor: { id }, + originSubs, + destinationSubs, + relaySub, + } of Object.values(this.#subs)) { + this.#log.info('Unsubscribe %s', id) + + originSubs.forEach(({ sub }) => sub.unsubscribe()) + destinationSubs.forEach(({ sub }) => sub.unsubscribe()) + if (relaySub) { + relaySub.sub.unsubscribe() + } + } + + for (const t of this.#timeouts) { + t.unref() + } + + await this.#engine.stop() + } + async start(): Promise { + this.#startNetworkMonitors() + } + #onXcmWaypointReached(msg: XcmNotifyMessage) { + const { subscriptionId } = msg + if (this.#subs[subscriptionId]) { + const { args, sendersControl } = this.#subs[subscriptionId] + if ( + (args.events === undefined || args.events === '*' || args.events.includes(msg.type)) && + matchSenders(sendersControl, msg.sender) + ) { + // XXX + // this.#notifier.notify(descriptor, msg) + } + } else { + // this could happen with closed ephemeral subscriptions + this.#log.warn('Unable to find descriptor for subscription %s', subscriptionId) + } + } + + /** + * Main monitoring logic. + * + * This method sets up and manages subscriptions for XCM messages based on the provided + * subscription information. It creates subscriptions for both the origin and destination + * networks, monitors XCM message transfers, and emits events accordingly. + * + * @param {Subscription} descriptor - The subscription descriptor. + * @param {XCMSubscriptionArgs} args - The coerced subscription arguments. + * @throws {Error} If there is an error during the subscription setup process. + * @private + */ + #monitor(descriptor: Subscription, args: XCMSubscriptionArgs) { + const { id } = descriptor + + let origMonitor: Monitor = { subs: [], controls: {} } + let destMonitor: Monitor = { subs: [], controls: {} } + const bridgeSubs: BridgeSubscription[] = [] + let relaySub: RxSubscriptionWithId | undefined + + try { + origMonitor = this.#monitorOrigins(descriptor, args) + destMonitor = this.#monitorDestinations(descriptor, args) + } catch (error) { + // Clean up origin subscriptions. + origMonitor.subs.forEach(({ sub }) => { + sub.unsubscribe() + }) + throw error + } + + // Only subscribe to relay events if required by subscription. + // Contained in its own try-catch so it doesn't prevent origin-destination subs in case of error. + if (this.#shouldMonitorRelay(args)) { + try { + relaySub = this.#monitorRelay(descriptor, args) + } catch (error) { + // log instead of throw to not block OD subscriptions + this.#log.error(error, 'Error on relay subscription (%s)', id) + } + } + + if (args.bridges !== undefined) { + if (args.bridges.includes('pk-bridge')) { + try { + bridgeSubs.push(this.#monitorPkBridge(descriptor, args)) + } catch (error) { + // log instead of throw to not block OD subscriptions + this.#log.error(error, 'Error on bridge subscription (%s)', id) + } + } + } + + const { sendersControl, messageControl } = origMonitor.controls + + this.#subs[id] = { + descriptor, + args, + sendersControl, + messageControl, + originSubs: origMonitor.subs, + destinationSubs: destMonitor.subs, + bridgeSubs, + relaySub, + } + } + + /** + * Set up inbound monitors for XCM protocols. + * + * @private + */ + #monitorDestinations({ id }: Subscription, { origin, destinations }: XCMSubscriptionArgs): Monitor { + const subs: RxSubscriptionWithId[] = [] + const originId = origin as NetworkURN + try { + for (const dest of destinations as NetworkURN[]) { + const chainId = dest + if (this.#subs[id]?.destinationSubs.find((s) => s.chainId === chainId)) { + // Skip existing subscriptions + // for the same destination chain + continue + } + + const inboundObserver = { + error: (error: any) => { + this.#log.error(error, '[%s] error on destination subscription %s', chainId, id) + + /*this.emit('telemetrySubscriptionError', { + subscriptionId: id, + chainId, + direction: 'in', + })*/ + + // try recover inbound subscription + if (this.#subs[id]) { + const { destinationSubs } = this.#subs[id] + const index = destinationSubs.findIndex((s) => s.chainId === chainId) + if (index > -1) { + destinationSubs.splice(index, 1) + this.#timeouts.push( + setTimeout(() => { + this.#log.info( + '[%s] UPDATE destination subscription %s due error %s', + chainId, + id, + errorMessage(error) + ) + const updated = this.#updateDestinationSubscriptions(id) + this.#subs[id].destinationSubs = updated + }, SUB_ERROR_RETRY_MS) + ) + } + } + }, + } + + if (this.#ingress.isRelay(dest)) { + // VMP UMP + this.#log.info('[%s] subscribe inbound UMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#sharedBlockEvents(chainId) + .pipe(extractUmpReceive(originId), this.#emitInbound(id, chainId)) + .subscribe(inboundObserver), + }) + } else if (this.#ingress.isRelay(originId)) { + // VMP DMP + this.#log.info('[%s] subscribe inbound DMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#sharedBlockEvents(chainId) + .pipe(extractDmpReceive(), this.#emitInbound(id, chainId)) + .subscribe(inboundObserver), + }) + } else { + // Inbound HRMP / XCMP transport + this.#log.info('[%s] subscribe inbound HRMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#sharedBlockEvents(chainId) + .pipe(extractXcmpReceive(), this.#emitInbound(id, chainId)) + .subscribe(inboundObserver), + }) + } + } + } catch (error) { + // Clean up subscriptions. + subs.forEach(({ sub }) => { + sub.unsubscribe() + }) + throw error + } + + return { subs, controls: {} } + } + + /** + * Set up outbound monitors for XCM protocols. + * + * @private + */ + #monitorOrigins({ id }: Subscription, { origin, senders, destinations }: XCMSubscriptionArgs): Monitor { + const subs: RxSubscriptionWithId[] = [] + const chainId = origin as NetworkURN + + if (this.#subs[id]?.originSubs.find((s) => s.chainId === chainId)) { + throw new Error(`Fatal: duplicated origin monitor ${id} for chain ${chainId}`) + } + + const sendersControl = ControlQuery.from(sendersCriteria(senders)) + const messageControl = ControlQuery.from(messageCriteria(destinations as NetworkURN[])) + + const outboundObserver = { + error: (error: any) => { + this.#log.error(error, '[%s] error on origin subscription %s', chainId, id) + /* + this.emit('telemetrySubscriptionError', { + subscriptionId: id, + chainId, + direction: 'out', + })*/ + + // try recover outbound subscription + // note: there is a single origin per outbound + if (this.#subs[id]) { + const { originSubs, descriptor, args } = this.#subs[id] + const index = originSubs.findIndex((s) => s.chainId === chainId) + if (index > -1) { + this.#subs[id].originSubs = [] + this.#timeouts.push( + setTimeout(() => { + if (this.#subs[id]) { + this.#log.info('[%s] UPDATE origin subscription %s due error %s', chainId, id, errorMessage(error)) + const { subs: updated, controls } = this.#monitorOrigins(descriptor, args) + this.#subs[id].sendersControl = controls.sendersControl + this.#subs[id].messageControl = controls.messageControl + this.#subs[id].originSubs = updated + } + }, SUB_ERROR_RETRY_MS) + ) + } + } + }, + } + + try { + if (this.#ingress.isRelay(chainId)) { + // VMP DMP + this.#log.info('[%s] subscribe outbound DMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#ingress + .getRegistry(chainId) + .pipe( + switchMap((registry) => + this.#sharedBlockExtrinsics(chainId).pipe( + extractDmpSend(chainId, this.#getDmp(chainId, registry), registry), + this.#emitOutbound(id, chainId, registry, messageControl) + ) + ) + ) + .subscribe(outboundObserver), + }) + + // VMP DMP + this.#log.info('[%s] subscribe outbound DMP - by event (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#ingress + .getRegistry(chainId) + .pipe( + switchMap((registry) => + this.#sharedBlockEvents(chainId).pipe( + extractDmpSendByEvent(chainId, this.#getDmp(chainId, registry), registry), + this.#emitOutbound(id, chainId, registry, messageControl) + ) + ) + ) + .subscribe(outboundObserver), + }) + } else { + // Outbound HRMP / XCMP transport + this.#log.info('[%s] subscribe outbound HRMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#ingress + .getRegistry(chainId) + .pipe( + switchMap((registry) => + this.#sharedBlockEvents(chainId).pipe( + extractXcmpSend(chainId, this.#getHrmp(chainId, registry), registry), + this.#emitOutbound(id, chainId, registry, messageControl) + ) + ) + ) + .subscribe(outboundObserver), + }) + + // VMP UMP + this.#log.info('[%s] subscribe outbound UMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#ingress + .getRegistry(chainId) + .pipe( + switchMap((registry) => + this.#sharedBlockEvents(chainId).pipe( + extractUmpSend(chainId, this.#getUmp(chainId, registry), registry), + this.#emitOutbound(id, chainId, registry, messageControl) + ) + ) + ) + .subscribe(outboundObserver), + }) + } + } catch (error) { + // Clean up subscriptions. + subs.forEach(({ sub }) => { + sub.unsubscribe() + }) + throw error + } + + return { + subs, + controls: { + sendersControl, + messageControl, + }, + } + } + + #monitorRelay({ id }: Subscription, { origin, destinations }: XCMSubscriptionArgs) { + const chainId = origin as NetworkURN + if (this.#subs[id]?.relaySub) { + this.#log.debug('Relay subscription already exists.') + } + const messageControl = ControlQuery.from(messageCriteria(destinations as NetworkURN[])) + + const emitRelayInbound = () => (source: Observable) => + source.pipe(switchMap((message) => from(this.#engine.onRelayedMessage(id, message)))) + + const relayObserver = { + error: (error: any) => { + this.#log.error(error, '[%s] error on relay subscription s', chainId, id) + /* + this.emit('telemetrySubscriptionError', { + subscriptionId: id, + chainId, + direction: 'relay', + })*/ + + // try recover relay subscription + // there is only one subscription per subscription ID for relay + if (this.#subs[id]) { + const sub = this.#subs[id] + this.#timeouts.push( + setTimeout(async () => { + this.#log.info('[%s] UPDATE relay subscription %s due error %s', chainId, id, errorMessage(error)) + const updatedSub = await this.#monitorRelay(sub.descriptor, sub.args) + sub.relaySub = updatedSub + }, SUB_ERROR_RETRY_MS) + ) + } + }, + } + + // TODO: should resolve relay id for consensus in context + const relayIds = this.#ingress.getRelayIds() + const relayId = relayIds.find((r) => getConsensus(r) === getConsensus(chainId)) + + if (relayId === undefined) { + throw new Error(`No relay ID found for chain ${chainId}`) + } + this.#log.info('[%s] subscribe relay %s xcm events (%s)', chainId, relayId, id) + return { + chainId, + sub: this.#ingress + .getRegistry(relayId) + .pipe( + switchMap((registry) => + this.#sharedBlockExtrinsics(relayId).pipe( + extractRelayReceive(chainId, messageControl, registry), + emitRelayInbound() + ) + ) + ) + .subscribe(relayObserver), + } + } + + // Assumes only 1 pair of bridge hub origin-destination is possible + // TODO: handle possible multiple different consensus utilizing PK bridge e.g. solochains? + #monitorPkBridge({ id }: Subscription, { origin, destinations }: XCMSubscriptionArgs) { + const originBridgeHub = getBridgeHubNetworkId(origin as NetworkURN) + const dest = (destinations as NetworkURN[]).find((d) => getConsensus(d) !== getConsensus(origin as NetworkURN)) + + if (dest === undefined) { + throw new Error(`No destination on different consensus found for bridging (sub=${id})`) + } + + const destBridgeHub = getBridgeHubNetworkId(dest) + + if (originBridgeHub === undefined || destBridgeHub === undefined) { + throw new Error( + `Unable to subscribe to PK bridge due to missing bridge hub network URNs for origin=${origin} and destinations=${destinations}. (sub=${id})` + ) + } + + if (this.#subs[id]?.bridgeSubs.find((s) => s.type === 'pk-bridge')) { + throw new Error(`Fatal: duplicated PK bridge monitor ${id}`) + } + + const type: BridgeType = 'pk-bridge' + + const emitBridgeOutboundAccepted = () => (source: Observable) => + source.pipe(switchMap((message) => from(this.#engine.onBridgeOutboundAccepted(id, message)))) + + const emitBridgeOutboundDelivered = () => (source: Observable) => + source.pipe(switchMap((message) => from(this.#engine.onBridgeOutboundDelivered(id, message)))) + + const emitBridgeInbound = () => (source: Observable) => + source.pipe(switchMap((message) => from(this.#engine.onBridgeInbound(id, message)))) + + const pkBridgeObserver = { + error: (error: any) => { + this.#log.error(error, '[%s] error on PK bridge subscription s', originBridgeHub, id) + // this.emit('telemetrySubscriptionError', { + // subscriptionId: id, + // chainId: originBridgeHub, + // direction: 'bridge', + // }); + + // try recover pk bridge subscription + if (this.#subs[id]) { + const sub = this.#subs[id] + const { bridgeSubs } = sub + const index = bridgeSubs.findIndex((s) => s.type === 'pk-bridge') + if (index > -1) { + bridgeSubs.splice(index, 1) + this.#timeouts.push( + setTimeout(() => { + this.#log.info( + '[%s] UPDATE destination subscription %s due error %s', + originBridgeHub, + id, + errorMessage(error) + ) + bridgeSubs.push(this.#monitorPkBridge(sub.descriptor, sub.args)) + sub.bridgeSubs = bridgeSubs + }, SUB_ERROR_RETRY_MS) + ) + } + } + }, + } + + this.#log.info( + '[%s] subscribe PK bridge outbound accepted events on bridge hub %s (%s)', + origin, + originBridgeHub, + id + ) + const outboundAccepted: RxSubscriptionWithId = { + chainId: originBridgeHub, + sub: this.#ingress + .getRegistry(originBridgeHub) + .pipe( + switchMap((registry) => + this.#sharedBlockEvents(originBridgeHub).pipe( + extractBridgeMessageAccepted(originBridgeHub, registry, this.#getStorageAt(originBridgeHub)), + emitBridgeOutboundAccepted() + ) + ) + ) + .subscribe(pkBridgeObserver), + } + + this.#log.info( + '[%s] subscribe PK bridge outbound delivered events on bridge hub %s (%s)', + origin, + originBridgeHub, + id + ) + const outboundDelivered: RxSubscriptionWithId = { + chainId: originBridgeHub, + sub: this.#ingress + .getRegistry(originBridgeHub) + .pipe( + switchMap((registry) => + this.#sharedBlockEvents(originBridgeHub).pipe( + extractBridgeMessageDelivered(originBridgeHub, registry), + emitBridgeOutboundDelivered() + ) + ) + ) + .subscribe(pkBridgeObserver), + } + + this.#log.info('[%s] subscribe PK bridge inbound events on bridge hub %s (%s)', origin, destBridgeHub, id) + const inbound: RxSubscriptionWithId = { + chainId: destBridgeHub, + sub: this.#sharedBlockEvents(destBridgeHub) + .pipe(extractBridgeReceive(destBridgeHub), emitBridgeInbound()) + .subscribe(pkBridgeObserver), + } + + return { + type, + subs: [outboundAccepted, outboundDelivered, inbound], + } + } + + #updateDestinationSubscriptions(id: string) { + const { descriptor, args, destinationSubs } = this.#subs[id] + // Subscribe to new destinations, if any + const { subs } = this.#monitorDestinations(descriptor, args) + const updatedSubs = destinationSubs.concat(subs) + // Unsubscribe removed destinations, if any + const removed = updatedSubs.filter((s) => !args.destinations.includes(s.chainId)) + removed.forEach(({ sub }) => sub.unsubscribe()) + // Return list of updated subscriptions + return updatedSubs.filter((s) => !removed.includes(s)) + } + + /** + * Starts collecting XCM messages. + * + * Monitors all the active subscriptions. + * + * @private + */ + async #startNetworkMonitors() { + const subs = await this.#db.getByAgentId(this.id) + + this.#log.info('[%s] #subscriptions %d', this.id, subs.length) + + for (const sub of subs) { + try { + this.#monitor(sub, $XCMSubscriptionArgs.parse(sub.args)) + } catch (err) { + this.#log.error(err, 'Unable to create subscription: %j', sub) + } + } + } + + #sharedBlockEvents(chainId: NetworkURN): Observable { + if (!this.#shared.blockEvents[chainId]) { + this.#shared.blockEvents[chainId] = this.#ingress.finalizedBlocks(chainId).pipe(extractEvents(), share()) + } + return this.#shared.blockEvents[chainId] + } + + #sharedBlockExtrinsics(chainId: NetworkURN): Observable { + if (!this.#shared.blockExtrinsics[chainId]) { + this.#shared.blockExtrinsics[chainId] = this.#ingress + .finalizedBlocks(chainId) + .pipe(extractTxWithEvents(), flattenCalls(), share()) + } + return this.#shared.blockExtrinsics[chainId] + } + + /** + * Checks if relayed HRMP messages should be monitored. + * + * All of the following conditions needs to be met: + * 1. `xcm.relayed` notification event is requested in the subscription + * 2. Origin chain is not a relay chain + * 3. At least one destination chain is a parachain + * + * @param Subscription + * @returns boolean + */ + #shouldMonitorRelay({ origin, destinations, events }: XCMSubscriptionArgs) { + return ( + (events === undefined || events === '*' || events.includes(XcmNotificationType.Relayed)) && + !this.#ingress.isRelay(origin as NetworkURN) && + destinations.some((d) => !this.#ingress.isRelay(d as NetworkURN)) + ) + } + + #emitInbound(id: string, chainId: NetworkURN) { + return (source: Observable) => + source.pipe(switchMap((msg) => from(this.#engine.onInboundMessage(new XcmInbound(id, chainId, msg))))) + } + + #emitOutbound(id: string, origin: NetworkURN, registry: Registry, messageControl: ControlQuery) { + const { + args: { outboundTTL }, + } = this.#subs[id] + + return (source: Observable) => + source.pipe( + mapXcmSent(id, registry, origin), + filter((msg) => matchMessage(messageControl, msg)), + switchMap((outbound) => from(this.#engine.onOutboundMessage(outbound, outboundTTL))) + ) + } + + #getDmp(chainId: NetworkURN, registry: Registry): GetDownwardMessageQueues { + return (blockHash: HexString, networkId: NetworkURN) => { + const paraId = getChainId(networkId) + return from(this.#ingress.getStorage(chainId, dmpDownwardMessageQueuesKey(registry, paraId), blockHash)).pipe( + map((buffer) => { + return registry.createType('Vec', buffer) + }) + ) + } + } + + #getUmp(chainId: NetworkURN, registry: Registry): GetOutboundUmpMessages { + return (blockHash: HexString) => { + return from(this.#ingress.getStorage(chainId, parachainSystemUpwardMessages, blockHash)).pipe( + map((buffer) => { + return registry.createType('Vec', buffer) + }) + ) + } + } + + #getHrmp(chainId: NetworkURN, registry: Registry): GetOutboundHrmpMessages { + return (blockHash: HexString) => { + return from(this.#ingress.getStorage(chainId, parachainSystemHrmpOutboundMessages, blockHash)).pipe( + map((buffer) => { + return registry.createType('Vec', buffer) + }) + ) + } + } + + #getStorageAt(chainId: NetworkURN): GetStorageAt { + return (blockHash: HexString, key: HexString) => { + return from(this.#ingress.getStorage(chainId, key, blockHash)) + } + } + + /** + * Updates the senders control handler. + * + * Applies to the outbound extrinsic signers. + */ + updateSenders(id: string) { + const { + args: { senders }, + sendersControl, + } = this.#subs[id] + + sendersControl.change(sendersCriteria(senders)) + } + + /** + * Updates the message control handler. + * + * Updates the destination subscriptions. + */ + updateDestinations(id: string) { + const { args, messageControl } = this.#subs[id] + + messageControl.change(messageCriteria(args.destinations as NetworkURN[])) + + const updatedSubs = this.#updateDestinationSubscriptions(id) + this.#subs[id].destinationSubs = updatedSubs + } + + /** + * Updates the subscription to relayed HRMP messages in the relay chain. + */ + updateEvents(id: string) { + const { descriptor, args, relaySub } = this.#subs[id] + + if (this.#shouldMonitorRelay(args) && relaySub === undefined) { + try { + this.#subs[id].relaySub = this.#monitorRelay(descriptor, args) + } catch (error) { + // log instead of throw to not block OD subscriptions + this.#log.error(error, 'Error on relay subscription (%s)', id) + } + } else if (!this.#shouldMonitorRelay(args) && relaySub !== undefined) { + relaySub.sub.unsubscribe() + delete this.#subs[id].relaySub + } + } + + /** + * Updates a subscription descriptor. + */ + updateSubscription(sub: Subscription) { + if (this.#subs[sub.id]) { + this.#subs[sub.id].descriptor = sub + } else { + this.#log.warn('trying to update an unknown subscription %s', sub.id) + } + } + + #validateChainIds(chainIds: NetworkURN[]) { + chainIds.forEach((chainId) => { + if (!this.#ingress.isNetworkDefined(chainId)) { + throw new ValidationError('Invalid chain id:' + chainId) + } + }) + } +} diff --git a/packages/server/src/lib.ts b/packages/server/src/lib.ts index 52318df1..82ab3005 100644 --- a/packages/server/src/lib.ts +++ b/packages/server/src/lib.ts @@ -5,6 +5,10 @@ export type { AnyJson, HexString, + SignerData, +} from './services/monitoring/types.js' + +export type { XcmReceived, XcmRelayed, XcmSent, @@ -19,5 +23,4 @@ export type { XcmTerminus, XcmTerminusContext, XcmWaypointContext, - SignerData, -} from './services/monitoring/types.js' +} from './agents/xcm/types.js' diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 63b51f81..414372ad 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -11,6 +11,7 @@ import FastifySwaggerUI from '@fastify/swagger-ui' import FastifyWebsocket from '@fastify/websocket' import FastifyHealthcheck from 'fastify-healthcheck' +import AgentService from './agents/plugin.js' import { logger } from './environment.js' import { errorHandler } from './errors.js' import { @@ -28,6 +29,7 @@ import version from './version.js' import { toCorsOpts } from './cli/args.js' import { + $AgentServiceOptions, $BaseServerOptions, $ConfigServerOptions, $CorsServerOptions, @@ -48,6 +50,7 @@ export const $ServerOptions = z .merge($ConfigServerOptions) .merge($LevelServerOptions) .merge($RedisServerOptions) + .merge($AgentServiceOptions) type ServerOptions = z.infer @@ -158,6 +161,7 @@ export async function createServer(opts: ServerOptions) { await server.register(Persistence, opts) await server.register(Ingress, opts) + await server.register(AgentService, opts) await server.register(Monitoring, opts) await server.register(Administration) await server.register(Telemetry, opts) diff --git a/packages/server/src/services/monitoring/api/routes.ts b/packages/server/src/services/monitoring/api/routes.ts index 4cd6cd25..1ec65d6d 100644 --- a/packages/server/src/services/monitoring/api/routes.ts +++ b/packages/server/src/services/monitoring/api/routes.ts @@ -2,14 +2,15 @@ import { FastifyInstance } from 'fastify' import { Operation, applyPatch } from 'rfc6902' import { zodToJsonSchema } from 'zod-to-json-schema' -import { $SafeId, $Subscription, Subscription } from '../types.js' +import { $AgentId, $SafeId, $Subscription, AgentId, Subscription } from '../types.js' import $JSONPatch from './json-patch.js' +/* TODO extract this const allowedPaths = ['/senders', '/destinations', '/channels', '/events'] function hasOp(patch: Operation[], path: string) { return patch.some((op) => op.path.startsWith(path)) -} +}*/ /** * Subscriptions HTTP API. @@ -89,22 +90,22 @@ export async function SubscriptionApi(api: FastifyInstance) { }, }, async (request, reply) => { - const qs = request.body - if (Array.isArray(qs)) { - const ids = [] + const subs = request.body + if (Array.isArray(subs)) { + const tmp = [] try { - for (const q of qs) { - await switchboard.subscribe(q) - ids.push(q.id) + for (const s of subs) { + await switchboard.subscribe(s) + tmp.push(s) } } catch (error) { - for (const id of ids) { - await switchboard.unsubscribe(id) + for (const s of tmp) { + await switchboard.unsubscribe(s.agent, s.id) } throw error } } else { - await switchboard.subscribe(qs) + await switchboard.subscribe(subs) } reply.status(201).send() @@ -134,7 +135,8 @@ export async function SubscriptionApi(api: FastifyInstance) { }, }, }, - async (request, reply) => { + async (_request, reply) => { + /* const patch = request.body const { id } = request.params const sub = await subsStore.getById(id) @@ -165,7 +167,8 @@ export async function SubscriptionApi(api: FastifyInstance) { reply.status(200).send(sub) } else { reply.status(400).send('Only operations on these paths are allowed: ' + allowedPaths.join(',')) - } + }*/ + reply.status(400) } ) @@ -174,13 +177,15 @@ export async function SubscriptionApi(api: FastifyInstance) { */ api.delete<{ Params: { + agent: AgentId id: string } }>( - '/subs/:id', + '/subs/:agent/:id', { schema: { params: { + agent: zodToJsonSchema($AgentId), id: zodToJsonSchema($SafeId), }, response: { @@ -192,7 +197,8 @@ export async function SubscriptionApi(api: FastifyInstance) { }, }, async (request, reply) => { - await switchboard.unsubscribe(request.params.id) + const { agent, id } = request.params + await switchboard.unsubscribe(agent, id) reply.send() } diff --git a/packages/server/src/services/monitoring/api/ws/plugin.ts b/packages/server/src/services/monitoring/api/ws/plugin.ts index 63f900c3..ab2f625a 100644 --- a/packages/server/src/services/monitoring/api/ws/plugin.ts +++ b/packages/server/src/services/monitoring/api/ws/plugin.ts @@ -1,6 +1,7 @@ import { FastifyPluginAsync } from 'fastify' import fp from 'fastify-plugin' +import { AgentId } from 'services/monitoring/types.js' import WebsocketProtocol from './protocol.js' declare module 'fastify' { @@ -33,11 +34,12 @@ const websocketProtocolPlugin: FastifyPluginAsync = as fastify.get<{ Params: { + agent: AgentId id: string } - }>('/ws/subs/:id', { websocket: true, schema: { hide: true } }, (socket, request): void => { - const { id } = request.params - setImmediate(() => protocol.handle(socket, request, id)) + }>('/ws/subs/:agent/:id', { websocket: true, schema: { hide: true } }, (socket, request): void => { + const { id, agent } = request.params + setImmediate(() => protocol.handle(socket, request, { agent, id })) }) fastify.get('/ws/subs', { websocket: true, schema: { hide: true } }, (socket, request): void => { diff --git a/packages/server/src/services/monitoring/api/ws/protocol.ts b/packages/server/src/services/monitoring/api/ws/protocol.ts index 460be163..46e96db7 100644 --- a/packages/server/src/services/monitoring/api/ws/protocol.ts +++ b/packages/server/src/services/monitoring/api/ws/protocol.ts @@ -5,14 +5,15 @@ import { FastifyRequest } from 'fastify' import { ulid } from 'ulidx' import { z } from 'zod' +import { XcmNotifyMessage } from 'agents/xcm/types.js' import { errorMessage } from '../../../../errors.js' import { TelemetryEventEmitter, notifyTelemetryFrom } from '../../../telemetry/types.js' import { Logger } from '../../../types.js' import { Switchboard } from '../../switchboard.js' -import { $Subscription, Subscription, XcmEventListener, XcmNotifyMessage } from '../../types.js' +import { $Subscription, AgentId, Subscription, XcmEventListener } from '../../types.js' import { WebsocketProtocolOptions } from './plugin.js' -const jsonSchema = z +const $EphemeralSubscription = z .string() .transform((str, ctx) => { try { @@ -94,7 +95,14 @@ export default class WebsocketProtocol extends (EventEmitter as new () => Teleme * @param request The Fastify request * @param subscriptionId The subscription identifier */ - async handle(socket: WebSocket, request: FastifyRequest, subscriptionId?: string) { + async handle( + socket: WebSocket, + request: FastifyRequest, + subscriptionId?: { + id: string + agent: AgentId + } + ) { if (this.#clientsNum >= this.#maxClients) { socket.close(1013, 'server too busy') return @@ -102,20 +110,20 @@ export default class WebsocketProtocol extends (EventEmitter as new () => Teleme try { if (subscriptionId === undefined) { - let resolvedId: string + let resolvedId: { id: string; agent: AgentId } // on-demand ephemeral subscriptions socket.on('data', (data: Buffer) => { setImmediate(async () => { if (resolvedId) { - safeWrite(socket, { id: resolvedId }) + safeWrite(socket, resolvedId) } else { - const parsed = jsonSchema.safeParse(data.toString()) + const parsed = $EphemeralSubscription.safeParse(data.toString()) if (parsed.success) { const onDemandSub = parsed.data try { this.#addSubscriber(onDemandSub, socket, request) - resolvedId = onDemandSub.id + resolvedId = { id: onDemandSub.id, agent: onDemandSub.agent } await this.#switchboard.subscribe(onDemandSub) safeWrite(socket, onDemandSub) } catch (error) { @@ -130,12 +138,8 @@ export default class WebsocketProtocol extends (EventEmitter as new () => Teleme }) } else { // existing subscriptions - const handler = this.#switchboard.findSubscriptionHandler(subscriptionId) - if (handler === undefined) { - throw new Error('subscription not found') - } - - const subscription = handler.descriptor + const { agent, id } = subscriptionId + const subscription = this.#switchboard.findSubscriptionHandler(agent, id) this.#addSubscriber(subscription, socket, request) } } catch (error) { @@ -172,12 +176,12 @@ export default class WebsocketProtocol extends (EventEmitter as new () => Teleme socket.once('close', async () => { this.#clientsNum-- - const { id, ephemeral } = subscription + const { id, agent, ephemeral } = subscription try { if (ephemeral) { // TODO clean up pending matches - await this.#switchboard.unsubscribe(id) + await this.#switchboard.unsubscribe(agent, id) } this.emit('telemetrySocketListener', request.ip, subscription, true) diff --git a/packages/server/src/services/monitoring/plugin.ts b/packages/server/src/services/monitoring/plugin.ts index 36d30b34..752ba0c6 100644 --- a/packages/server/src/services/monitoring/plugin.ts +++ b/packages/server/src/services/monitoring/plugin.ts @@ -25,7 +25,7 @@ type MonitoringOptions = SwitchboardOptions & WebsocketProtocolOptions const monitoringPlugin: FastifyPluginAsync = async (fastify, options) => { const { log } = fastify - const subsStore = new SubsStore(fastify.log, fastify.rootStore, fastify.ingressConsumer) + const subsStore = new SubsStore(fastify.log, fastify.rootStore, fastify.agentService) fastify.decorate('subsStore', subsStore) const switchboard = new Switchboard(fastify, options) diff --git a/packages/server/src/services/monitoring/switchboard.ts b/packages/server/src/services/monitoring/switchboard.ts index 542c262d..1e3c3503 100644 --- a/packages/server/src/services/monitoring/switchboard.ts +++ b/packages/server/src/services/monitoring/switchboard.ts @@ -1,64 +1,12 @@ import EventEmitter from 'node:events' -import { Registry } from '@polkadot/types-codec/types' -import { ControlQuery, extractEvents, extractTxWithEvents, flattenCalls, types } from '@sodazone/ocelloids-sdk' -import { Observable, filter, from, map, share, switchMap } from 'rxjs' - -import { Logger, NetworkURN, Services } from '../types.js' -import { extractXcmpReceive, extractXcmpSend } from './ops/xcmp.js' -import { - BridgeSubscription, - BridgeType, - HexString, - RxSubscriptionHandler, - RxSubscriptionWithId, - Subscription, - SubscriptionStats, - XcmBridgeAcceptedWithContext, - XcmBridgeDeliveredWithContext, - XcmBridgeInboundWithContext, - XcmEventListener, - XcmInbound, - XcmInboundWithContext, - XcmNotificationType, - XcmNotifyMessage, - XcmRelayedWithContext, - XcmSentWithContext, -} from './types.js' +import { Logger, Services } from '../types.js' +import { AgentId, Subscription, SubscriptionStats, XcmEventListener } from './types.js' +import { AgentService } from 'agents/types.js' import { NotifierHub } from '../notification/hub.js' import { NotifierEvents } from '../notification/types.js' -import { SubsStore } from '../persistence/subs.js' import { TelemetryCollect, TelemetryEventEmitter } from '../telemetry/types.js' -import { MatchingEngine } from './matching.js' - -import { errorMessage } from '../../errors.js' -import { IngressConsumer } from '../ingress/index.js' -import { mapXcmSent } from './ops/common.js' -import { matchMessage, matchSenders, messageCriteria, sendersCriteria } from './ops/criteria.js' -import { extractDmpReceive, extractDmpSend, extractDmpSendByEvent } from './ops/dmp.js' -import { extractRelayReceive } from './ops/relay.js' -import { extractUmpReceive, extractUmpSend } from './ops/ump.js' - -import { getChainId, getConsensus } from '../config.js' -import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './ops/pk-bridge.js' -import { getBridgeHubNetworkId } from './ops/util.js' -import { - dmpDownwardMessageQueuesKey, - parachainSystemHrmpOutboundMessages, - parachainSystemUpwardMessages, -} from './storage.js' -import { - GetDownwardMessageQueues, - GetOutboundHrmpMessages, - GetOutboundUmpMessages, - GetStorageAt, -} from './types-augmented.js' - -type Monitor = { - subs: RxSubscriptionWithId[] - controls: Record -} export enum SubscribeErrorCodes { TOO_MANY_SUBSCRIBERS, @@ -80,8 +28,6 @@ export type SwitchboardOptions = { subscriptionMaxEphemeral?: number } -const SUB_ERROR_RETRY_MS = 5000 - /** * XCM Subscriptions Switchboard. * @@ -93,31 +39,19 @@ const SUB_ERROR_RETRY_MS = 5000 */ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitter) { readonly #log: Logger - readonly #db: SubsStore - readonly #engine: MatchingEngine - readonly #ingress: IngressConsumer readonly #notifier: NotifierHub readonly #stats: SubscriptionStats readonly #maxEphemeral: number readonly #maxPersistent: number - readonly #timeouts: NodeJS.Timeout[] = [] - - #subs: Record = {} - #shared: { - blockEvents: Record> - blockExtrinsics: Record> - } + readonly #agentService: AgentService constructor(ctx: Services, options: SwitchboardOptions) { super() - const { log, subsStore, ingressConsumer } = ctx - - this.#db = subsStore - this.#log = log + this.#log = ctx.log + this.#agentService = ctx.agentService - this.#engine = new MatchingEngine(ctx, this.#onXcmWaypointReached.bind(this)) - this.#ingress = ingressConsumer + // TODO here could be local for websockets over event emitter or remote over redis this.#notifier = new NotifierHub(ctx) this.#stats = { ephemeral: 0, @@ -125,29 +59,29 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte } this.#maxEphemeral = options.subscriptionMaxEphemeral ?? 10_000 this.#maxPersistent = options.subscriptionMaxPersistent ?? 10_000 - this.#shared = { - blockEvents: {}, - blockExtrinsics: {}, - } } /** - * Subscribes according to the given query subscription. + * Subscribes according to the given subscription. * - * @param {Subscription} qs The query subscription. + * @param {Subscription} s The subscription. * @throws {SubscribeError} If there is an error creating the subscription. */ - async subscribe(qs: Subscription) { + async subscribe(s: Subscription) { if (this.#stats.ephemeral >= this.#maxEphemeral || this.#stats.persistent >= this.#maxPersistent) { throw new SubscribeError(SubscribeErrorCodes.TOO_MANY_SUBSCRIBERS, 'too many subscriptions') } - if (!qs.ephemeral) { - await this.#db.insert(qs) - } - this.#monitor(qs) + const agent = this.#agentService.getAgentById(s.agent) + await agent.subscribe(s) + + this.#log.info('[%s] new subscription: %j', s.agent, s) - this.#log.info('[%s] new subscription: %j', qs.origin, qs) + if (s.ephemeral) { + this.#stats.ephemeral++ + } else { + this.#stats.persistent++ + } } /** @@ -171,144 +105,41 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte } /** - * Unsubscribes by subsciption identifier. + * Unsubscribes by subsciption and agent identifier. * * If the subscription does not exists just ignores it. * + * @param {AgentId} agentId The agent identifier. * @param {string} id The subscription identifier. */ - async unsubscribe(id: string) { - if (this.#subs[id] === undefined) { - this.#log.warn('unsubscribe from a non-existent subscription %s', id) - return - } - - try { - const { - descriptor: { origin, ephemeral }, - originSubs, - destinationSubs, - relaySub, - } = this.#subs[id] + async unsubscribe(agentId: AgentId, id: string) { + const agent = this.#agentService.getAgentById(agentId) + const { ephemeral } = agent.getSubscriptionHandler(id) + await agent.unsubscribe(id) - this.#log.info('[%s] unsubscribe %s', origin, id) - - originSubs.forEach(({ sub }) => sub.unsubscribe()) - destinationSubs.forEach(({ sub }) => sub.unsubscribe()) - if (relaySub) { - relaySub.sub.unsubscribe() - } - delete this.#subs[id] - - await this.#engine.clearPendingStates(id) - - if (ephemeral) { - this.#stats.ephemeral-- - } else { - this.#stats.persistent-- - await this.#db.remove(id) - } - } catch (error) { - this.#log.error(error, 'Error unsubscribing %s', id) + if (ephemeral) { + this.#stats.ephemeral-- + } else { + this.#stats.persistent-- } } async start() { - await this.#startNetworkMonitors() + // empty } /** - * Stops the switchboard and unsubscribes from the underlying - * reactive subscriptions. + * Stops the switchboard. */ async stop() { - this.#log.info('Stopping switchboard') - - for (const { - descriptor: { id }, - originSubs, - destinationSubs, - relaySub, - } of Object.values(this.#subs)) { - this.#log.info('Unsubscribe %s', id) - - originSubs.forEach(({ sub }) => sub.unsubscribe()) - destinationSubs.forEach(({ sub }) => sub.unsubscribe()) - if (relaySub) { - relaySub.sub.unsubscribe() - } - } - - for (const t of this.#timeouts) { - t.unref() - } - - await this.#engine.stop() + // empty } /** * Gets a subscription handler by id. */ - findSubscriptionHandler(id: string) { - return this.#subs[id] - } - - /** - * Updates the senders control handler. - * - * Applies to the outbound extrinsic signers. - */ - updateSenders(id: string) { - const { - descriptor: { senders }, - sendersControl, - } = this.#subs[id] - - sendersControl.change(sendersCriteria(senders)) - } - - /** - * Updates the message control handler. - * - * Updates the destination subscriptions. - */ - updateDestinations(id: string) { - const { descriptor, messageControl } = this.#subs[id] - - messageControl.change(messageCriteria(descriptor.destinations as NetworkURN[])) - - const updatedSubs = this.#updateDestinationSubscriptions(id) - this.#subs[id].destinationSubs = updatedSubs - } - - /** - * Updates the subscription to relayed HRMP messages in the relay chain. - */ - updateEvents(id: string) { - const { descriptor, relaySub } = this.#subs[id] - - if (this.#shouldMonitorRelay(descriptor) && relaySub === undefined) { - try { - this.#subs[id].relaySub = this.#monitorRelay(descriptor) - } catch (error) { - // log instead of throw to not block OD subscriptions - this.#log.error(error, 'Error on relay subscription (%s)', id) - } - } else if (!this.#shouldMonitorRelay(descriptor) && relaySub !== undefined) { - relaySub.sub.unsubscribe() - delete this.#subs[id].relaySub - } - } - - /** - * Updates a subscription descriptor. - */ - updateSubscription(sub: Subscription) { - if (this.#subs[sub.id]) { - this.#subs[sub.id].descriptor = sub - } else { - this.#log.warn('trying to update an unknown subscription %s', sub.id) - } + findSubscriptionHandler(agentId: AgentId, id: string) { + return this.#agentService.getAgentById(agentId).getSubscriptionHandler(id) } /** @@ -318,7 +149,7 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte */ collectTelemetry(collect: TelemetryCollect) { collect(this) - collect(this.#engine) + // collect(this.#engine) collect(this.#notifier) } @@ -328,638 +159,4 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte get stats() { return this.#stats } - - /** - * Main monitoring logic. - * - * This method sets up and manages subscriptions for XCM messages based on the provided - * query subscription information. It creates subscriptions for both the origin and destination - * networks, monitors XCM message transfers, and emits events accordingly. - * - * @param {Subscription} qs - The query subscription. - * @throws {Error} If there is an error during the subscription setup process. - * @private - */ - #monitor(qs: Subscription) { - const { id } = qs - - let origMonitor: Monitor = { subs: [], controls: {} } - let destMonitor: Monitor = { subs: [], controls: {} } - const bridgeSubs: BridgeSubscription[] = [] - let relaySub: RxSubscriptionWithId | undefined - - try { - origMonitor = this.#monitorOrigins(qs) - destMonitor = this.#monitorDestinations(qs) - } catch (error) { - // Clean up origin subscriptions. - origMonitor.subs.forEach(({ sub }) => { - sub.unsubscribe() - }) - throw error - } - - // Only subscribe to relay events if required by subscription. - // Contained in its own try-catch so it doesn't prevent origin-destination subs in case of error. - if (this.#shouldMonitorRelay(qs)) { - try { - relaySub = this.#monitorRelay(qs) - } catch (error) { - // log instead of throw to not block OD subscriptions - this.#log.error(error, 'Error on relay subscription (%s)', id) - } - } - - if (qs.bridges !== undefined) { - if (qs.bridges.includes('pk-bridge')) { - try { - bridgeSubs.push(this.#monitorPkBridge(qs)) - } catch (error) { - // log instead of throw to not block OD subscriptions - this.#log.error(error, 'Error on bridge subscription (%s)', id) - } - } - } - - if (qs.bridges !== undefined) { - if (qs.bridges.includes('pk-bridge')) { - try { - bridgeSubs.push(this.#monitorPkBridge(qs)) - } catch (error) { - // log instead of throw to not block OD subscriptions - this.#log.error(error, 'Error on bridge subscription (%s)', id) - } - } - } - - const { sendersControl, messageControl } = origMonitor.controls - - this.#subs[id] = { - descriptor: qs, - sendersControl, - messageControl, - originSubs: origMonitor.subs, - destinationSubs: destMonitor.subs, - bridgeSubs, - relaySub, - } - - if (qs.ephemeral) { - this.#stats.ephemeral++ - } else { - this.#stats.persistent++ - } - } - - /** - * Set up inbound monitors for XCM protocols. - * - * @private - */ - #monitorDestinations({ id, destinations, origin }: Subscription): Monitor { - const subs: RxSubscriptionWithId[] = [] - const originId = origin as NetworkURN - try { - for (const dest of destinations as NetworkURN[]) { - const chainId = dest - if (this.#subs[id]?.destinationSubs.find((s) => s.chainId === chainId)) { - // Skip existing subscriptions - // for the same destination chain - continue - } - - const inboundObserver = { - error: (error: any) => { - this.#log.error(error, '[%s] error on destination subscription %s', chainId, id) - this.emit('telemetrySubscriptionError', { - subscriptionId: id, - chainId, - direction: 'in', - }) - - // try recover inbound subscription - if (this.#subs[id]) { - const { destinationSubs } = this.#subs[id] - const index = destinationSubs.findIndex((s) => s.chainId === chainId) - if (index > -1) { - destinationSubs.splice(index, 1) - this.#timeouts.push( - setTimeout(() => { - this.#log.info( - '[%s] UPDATE destination subscription %s due error %s', - chainId, - id, - errorMessage(error) - ) - const updated = this.#updateDestinationSubscriptions(id) - this.#subs[id].destinationSubs = updated - }, SUB_ERROR_RETRY_MS) - ) - } - } - }, - } - - if (this.#ingress.isRelay(dest)) { - // VMP UMP - this.#log.info('[%s] subscribe inbound UMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#sharedBlockEvents(chainId) - .pipe(extractUmpReceive(originId), this.#emitInbound(id, chainId)) - .subscribe(inboundObserver), - }) - } else if (this.#ingress.isRelay(originId)) { - // VMP DMP - this.#log.info('[%s] subscribe inbound DMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#sharedBlockEvents(chainId) - .pipe(extractDmpReceive(), this.#emitInbound(id, chainId)) - .subscribe(inboundObserver), - }) - } else { - // Inbound HRMP / XCMP transport - this.#log.info('[%s] subscribe inbound HRMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#sharedBlockEvents(chainId) - .pipe(extractXcmpReceive(), this.#emitInbound(id, chainId)) - .subscribe(inboundObserver), - }) - } - } - } catch (error) { - // Clean up subscriptions. - subs.forEach(({ sub }) => { - sub.unsubscribe() - }) - throw error - } - - return { subs, controls: {} } - } - - /** - * Set up outbound monitors for XCM protocols. - * - * @private - */ - #monitorOrigins({ id, origin, senders, destinations }: Subscription): Monitor { - const subs: RxSubscriptionWithId[] = [] - const chainId = origin as NetworkURN - - if (this.#subs[id]?.originSubs.find((s) => s.chainId === chainId)) { - throw new Error(`Fatal: duplicated origin monitor ${id} for chain ${chainId}`) - } - - const sendersControl = ControlQuery.from(sendersCriteria(senders)) - const messageControl = ControlQuery.from(messageCriteria(destinations as NetworkURN[])) - - const outboundObserver = { - error: (error: any) => { - this.#log.error(error, '[%s] error on origin subscription %s', chainId, id) - this.emit('telemetrySubscriptionError', { - subscriptionId: id, - chainId, - direction: 'out', - }) - - // try recover outbound subscription - // note: there is a single origin per outbound - if (this.#subs[id]) { - const { originSubs, descriptor } = this.#subs[id] - const index = originSubs.findIndex((s) => s.chainId === chainId) - if (index > -1) { - this.#subs[id].originSubs = [] - this.#timeouts.push( - setTimeout(() => { - if (this.#subs[id]) { - this.#log.info('[%s] UPDATE origin subscription %s due error %s', chainId, id, errorMessage(error)) - const { subs: updated, controls } = this.#monitorOrigins(descriptor) - this.#subs[id].sendersControl = controls.sendersControl - this.#subs[id].messageControl = controls.messageControl - this.#subs[id].originSubs = updated - } - }, SUB_ERROR_RETRY_MS) - ) - } - } - }, - } - - try { - if (this.#ingress.isRelay(chainId)) { - // VMP DMP - this.#log.info('[%s] subscribe outbound DMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#ingress - .getRegistry(chainId) - .pipe( - switchMap((registry) => - this.#sharedBlockExtrinsics(chainId).pipe( - extractDmpSend(chainId, this.#getDmp(chainId, registry), registry), - this.#emitOutbound(id, chainId, registry, messageControl) - ) - ) - ) - .subscribe(outboundObserver), - }) - - // VMP DMP - this.#log.info('[%s] subscribe outbound DMP - by event (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#ingress - .getRegistry(chainId) - .pipe( - switchMap((registry) => - this.#sharedBlockEvents(chainId).pipe( - extractDmpSendByEvent(chainId, this.#getDmp(chainId, registry), registry), - this.#emitOutbound(id, chainId, registry, messageControl) - ) - ) - ) - .subscribe(outboundObserver), - }) - } else { - // Outbound HRMP / XCMP transport - this.#log.info('[%s] subscribe outbound HRMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#ingress - .getRegistry(chainId) - .pipe( - switchMap((registry) => - this.#sharedBlockEvents(chainId).pipe( - extractXcmpSend(chainId, this.#getHrmp(chainId, registry), registry), - this.#emitOutbound(id, chainId, registry, messageControl) - ) - ) - ) - .subscribe(outboundObserver), - }) - - // VMP UMP - this.#log.info('[%s] subscribe outbound UMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#ingress - .getRegistry(chainId) - .pipe( - switchMap((registry) => - this.#sharedBlockEvents(chainId).pipe( - extractUmpSend(chainId, this.#getUmp(chainId, registry), registry), - this.#emitOutbound(id, chainId, registry, messageControl) - ) - ) - ) - .subscribe(outboundObserver), - }) - } - } catch (error) { - // Clean up subscriptions. - subs.forEach(({ sub }) => { - sub.unsubscribe() - }) - throw error - } - - return { - subs, - controls: { - sendersControl, - messageControl, - }, - } - } - - #monitorRelay({ id, destinations, origin }: Subscription) { - const chainId = origin as NetworkURN - if (this.#subs[id]?.relaySub) { - this.#log.debug('Relay subscription already exists.') - } - const messageControl = ControlQuery.from(messageCriteria(destinations as NetworkURN[])) - - const emitRelayInbound = () => (source: Observable) => - source.pipe(switchMap((message) => from(this.#engine.onRelayedMessage(id, message)))) - - const relayObserver = { - error: (error: any) => { - this.#log.error(error, '[%s] error on relay subscription s', chainId, id) - this.emit('telemetrySubscriptionError', { - subscriptionId: id, - chainId, - direction: 'relay', - }) - - // try recover relay subscription - // there is only one subscription per subscription ID for relay - if (this.#subs[id]) { - this.#timeouts.push( - setTimeout(async () => { - this.#log.info('[%s] UPDATE relay subscription %s due error %s', chainId, id, errorMessage(error)) - const updatedSub = await this.#monitorRelay(this.#subs[id].descriptor) - this.#subs[id].relaySub = updatedSub - }, SUB_ERROR_RETRY_MS) - ) - } - }, - } - - // TODO: should resolve relay id for consensus in context - const relayIds = this.#ingress.getRelayIds() - const relayId = relayIds.find((r) => getConsensus(r) === getConsensus(chainId)) - - if (relayId === undefined) { - throw new Error(`No relay ID found for chain ${chainId}`) - } - this.#log.info('[%s] subscribe relay %s xcm events (%s)', chainId, relayId, id) - return { - chainId, - sub: this.#ingress - .getRegistry(relayId) - .pipe( - switchMap((registry) => - this.#sharedBlockExtrinsics(relayId).pipe( - extractRelayReceive(chainId, messageControl, registry), - emitRelayInbound() - ) - ) - ) - .subscribe(relayObserver), - } - } - - // Assumes only 1 pair of bridge hub origin-destination is possible - // TODO: handle possible multiple different consensus utilizing PK bridge e.g. solochains? - #monitorPkBridge({ id, destinations, origin }: Subscription) { - const originBridgeHub = getBridgeHubNetworkId(origin as NetworkURN) - const dest = (destinations as NetworkURN[]).find((d) => getConsensus(d) !== getConsensus(origin as NetworkURN)) - - if (dest === undefined) { - throw new Error(`No destination on different consensus found for bridging (sub=${id})`) - } - - const destBridgeHub = getBridgeHubNetworkId(dest) - - if (originBridgeHub === undefined || destBridgeHub === undefined) { - throw new Error( - `Unable to subscribe to PK bridge due to missing bridge hub network URNs for origin=${origin} and destinations=${destinations}. (sub=${id})` - ) - } - - if (this.#subs[id]?.bridgeSubs.find((s) => s.type === 'pk-bridge')) { - throw new Error(`Fatal: duplicated PK bridge monitor ${id}`) - } - - const type: BridgeType = 'pk-bridge' - - const emitBridgeOutboundAccepted = () => (source: Observable) => - source.pipe(switchMap((message) => from(this.#engine.onBridgeOutboundAccepted(id, message)))) - - const emitBridgeOutboundDelivered = () => (source: Observable) => - source.pipe(switchMap((message) => from(this.#engine.onBridgeOutboundDelivered(id, message)))) - - const emitBridgeInbound = () => (source: Observable) => - source.pipe(switchMap((message) => from(this.#engine.onBridgeInbound(id, message)))) - - const pkBridgeObserver = { - error: (error: any) => { - this.#log.error(error, '[%s] error on PK bridge subscription s', originBridgeHub, id) - // this.emit('telemetrySubscriptionError', { - // subscriptionId: id, - // chainId: originBridgeHub, - // direction: 'bridge', - // }); - - // try recover pk bridge subscription - if (this.#subs[id]) { - const { bridgeSubs } = this.#subs[id] - const index = bridgeSubs.findIndex((s) => s.type === 'pk-bridge') - if (index > -1) { - bridgeSubs.splice(index, 1) - this.#timeouts.push( - setTimeout(() => { - this.#log.info( - '[%s] UPDATE destination subscription %s due error %s', - originBridgeHub, - id, - errorMessage(error) - ) - bridgeSubs.push(this.#monitorPkBridge(this.#subs[id].descriptor)) - this.#subs[id].bridgeSubs = bridgeSubs - }, SUB_ERROR_RETRY_MS) - ) - } - } - }, - } - - this.#log.info( - '[%s] subscribe PK bridge outbound accepted events on bridge hub %s (%s)', - origin, - originBridgeHub, - id - ) - const outboundAccepted: RxSubscriptionWithId = { - chainId: originBridgeHub, - sub: this.#ingress - .getRegistry(originBridgeHub) - .pipe( - switchMap((registry) => - this.#sharedBlockEvents(originBridgeHub).pipe( - extractBridgeMessageAccepted(originBridgeHub, registry, this.#getStorageAt(originBridgeHub)), - emitBridgeOutboundAccepted() - ) - ) - ) - .subscribe(pkBridgeObserver), - } - - this.#log.info( - '[%s] subscribe PK bridge outbound delivered events on bridge hub %s (%s)', - origin, - originBridgeHub, - id - ) - const outboundDelivered: RxSubscriptionWithId = { - chainId: originBridgeHub, - sub: this.#ingress - .getRegistry(originBridgeHub) - .pipe( - switchMap((registry) => - this.#sharedBlockEvents(originBridgeHub).pipe( - extractBridgeMessageDelivered(originBridgeHub, registry), - emitBridgeOutboundDelivered() - ) - ) - ) - .subscribe(pkBridgeObserver), - } - - this.#log.info('[%s] subscribe PK bridge inbound events on bridge hub %s (%s)', origin, destBridgeHub, id) - const inbound: RxSubscriptionWithId = { - chainId: destBridgeHub, - sub: this.#sharedBlockEvents(destBridgeHub) - .pipe(extractBridgeReceive(destBridgeHub), emitBridgeInbound()) - .subscribe(pkBridgeObserver), - } - - return { - type, - subs: [outboundAccepted, outboundDelivered, inbound], - } - } - - #updateDestinationSubscriptions(id: string) { - const { descriptor, destinationSubs } = this.#subs[id] - // Subscribe to new destinations, if any - const { subs } = this.#monitorDestinations(descriptor) - const updatedSubs = destinationSubs.concat(subs) - // Unsubscribe removed destinations, if any - const removed = updatedSubs.filter((s) => !descriptor.destinations.includes(s.chainId)) - removed.forEach(({ sub }) => sub.unsubscribe()) - // Return list of updated subscriptions - return updatedSubs.filter((s) => !removed.includes(s)) - } - - /** - * Starts collecting XCM messages. - * - * Monitors all the active subscriptions for the configured networks. - * - * @private - */ - async #startNetworkMonitors() { - const chainIds = this.#ingress.getChainIds() - - for (const chainId of chainIds) { - const subs = await this.#db.getByNetworkId(chainId) - - this.#log.info('[%s] #subscriptions %d', chainId, subs.length) - - for (const sub of subs) { - try { - this.#monitor(sub) - } catch (err) { - this.#log.error(err, 'Unable to create subscription: %j', sub) - } - } - } - } - - #onXcmWaypointReached(msg: XcmNotifyMessage) { - const { subscriptionId } = msg - if (this.#subs[subscriptionId]) { - const { descriptor, sendersControl } = this.#subs[subscriptionId] - if ( - (descriptor.events === undefined || descriptor.events === '*' || descriptor.events.includes(msg.type)) && - matchSenders(sendersControl, msg.sender) - ) { - this.#notifier.notify(descriptor, msg) - } - } else { - // this could happen with closed ephemeral subscriptions - this.#log.warn('Unable to find descriptor for subscription %s', subscriptionId) - } - } - - #sharedBlockEvents(chainId: NetworkURN): Observable { - if (!this.#shared.blockEvents[chainId]) { - this.#shared.blockEvents[chainId] = this.#ingress.finalizedBlocks(chainId).pipe(extractEvents(), share()) - } - return this.#shared.blockEvents[chainId] - } - - #sharedBlockExtrinsics(chainId: NetworkURN): Observable { - if (!this.#shared.blockExtrinsics[chainId]) { - this.#shared.blockExtrinsics[chainId] = this.#ingress - .finalizedBlocks(chainId) - .pipe(extractTxWithEvents(), flattenCalls(), share()) - } - return this.#shared.blockExtrinsics[chainId] - } - - /** - * Checks if relayed HRMP messages should be monitored. - * - * All of the following conditions needs to be met: - * 1. `xcm.relayed` notification event is requested in the subscription - * 2. Origin chain is not a relay chain - * 3. At least one destination chain is a parachain - * - * @param Subscription - * @returns boolean - */ - #shouldMonitorRelay({ origin, destinations, events }: Subscription) { - return ( - (events === undefined || events === '*' || events.includes(XcmNotificationType.Relayed)) && - !this.#ingress.isRelay(origin as NetworkURN) && - destinations.some((d) => !this.#ingress.isRelay(d as NetworkURN)) - ) - } - - #emitInbound(id: string, chainId: NetworkURN) { - return (source: Observable) => - source.pipe(switchMap((msg) => from(this.#engine.onInboundMessage(new XcmInbound(id, chainId, msg))))) - } - - #emitOutbound(id: string, origin: NetworkURN, registry: Registry, messageControl: ControlQuery) { - const { - descriptor: { outboundTTL }, - } = this.#subs[id] - - return (source: Observable) => - source.pipe( - mapXcmSent(id, registry, origin), - filter((msg) => matchMessage(messageControl, msg)), - switchMap((outbound) => from(this.#engine.onOutboundMessage(outbound, outboundTTL))) - ) - } - - #getDmp(chainId: NetworkURN, registry: Registry): GetDownwardMessageQueues { - return (blockHash: HexString, networkId: NetworkURN) => { - const paraId = getChainId(networkId) - return from(this.#ingress.getStorage(chainId, dmpDownwardMessageQueuesKey(registry, paraId), blockHash)).pipe( - map((buffer) => { - return registry.createType('Vec', buffer) - }) - ) - } - } - - #getUmp(chainId: NetworkURN, registry: Registry): GetOutboundUmpMessages { - return (blockHash: HexString) => { - return from(this.#ingress.getStorage(chainId, parachainSystemUpwardMessages, blockHash)).pipe( - map((buffer) => { - return registry.createType('Vec', buffer) - }) - ) - } - } - - #getHrmp(chainId: NetworkURN, registry: Registry): GetOutboundHrmpMessages { - return (blockHash: HexString) => { - return from(this.#ingress.getStorage(chainId, parachainSystemHrmpOutboundMessages, blockHash)).pipe( - map((buffer) => { - return registry.createType('Vec', buffer) - }) - ) - } - } - - #getStorageAt(chainId: NetworkURN): GetStorageAt { - return (blockHash: HexString, key: HexString) => { - return from(this.#ingress.getStorage(chainId, key, blockHash)) - } - } } diff --git a/packages/server/src/services/monitoring/types.ts b/packages/server/src/services/monitoring/types.ts index f60fe2f0..c057b048 100644 --- a/packages/server/src/services/monitoring/types.ts +++ b/packages/server/src/services/monitoring/types.ts @@ -2,16 +2,7 @@ import z from 'zod' import { Subscription as RxSubscription } from 'rxjs' -import type { U8aFixed, bool } from '@polkadot/types-codec' -import type { - FrameSupportMessagesProcessMessageError, - PolkadotRuntimeParachainsInclusionAggregateMessageOrigin, -} from '@polkadot/types/lookup' - -import { ControlQuery } from '@sodazone/ocelloids-sdk' - -import { createNetworkId } from '../config.js' -import { NetworkURN } from '../types.js' +import { XcmNotifyMessage } from '../../agents/xcm/types.js' /** * Represents a generic JSON object. @@ -44,6 +35,7 @@ export type BlockNumberRange = { toBlockNum: string } +// "urn:ocn:agent-id:subscription-id" export const $SafeId = z .string({ required_error: 'id is required', @@ -59,7 +51,7 @@ export const $SafeId = z */ export type HexString = `0x${string}` -function toHexString(buf: Uint8Array): HexString { +export function toHexString(buf: Uint8Array): HexString { return `0x${Buffer.from(buf).toString('hex')}` } @@ -82,726 +74,6 @@ export type SignerData = { }[] } -export type XcmCriteria = { - sendersControl: ControlQuery - messageControl: ControlQuery -} - -export type XcmWithContext = { - event?: AnyJson - extrinsicId?: string - blockNumber: string | number - blockHash: HexString - messageHash: HexString - messageId?: HexString -} - -/** - * Represents the asset that has been trapped. - * - * @public - */ -export type TrappedAsset = { - version: number - id: { - type: string - value: AnyJson - } - fungible: boolean - amount: string | number - assetInstance?: AnyJson -} - -/** - * Event emitted when assets are trapped. - * - * @public - */ -export type AssetsTrapped = { - assets: TrappedAsset[] - hash: HexString - event: AnyJson -} - -/** - * Represents an XCM program bytes and human JSON. - */ -export type XcmProgram = { - bytes: Uint8Array - json: AnyJson -} - -export interface XcmSentWithContext extends XcmWithContext { - messageData: Uint8Array - recipient: NetworkURN - sender?: SignerData - instructions: XcmProgram -} - -export interface XcmBridgeAcceptedWithContext extends XcmWithContext { - chainId: NetworkURN - bridgeKey: HexString - messageData: HexString - instructions: AnyJson - recipient: NetworkURN - forwardId?: HexString -} - -export interface XcmBridgeDeliveredWithContext { - chainId: NetworkURN - bridgeKey: HexString - event?: AnyJson - extrinsicId?: string - blockNumber: string | number - blockHash: HexString - sender?: SignerData -} - -export interface XcmBridgeAcceptedWithContext extends XcmWithContext { - chainId: NetworkURN - bridgeKey: HexString - messageData: HexString - instructions: AnyJson - recipient: NetworkURN - forwardId?: HexString -} - -export interface XcmBridgeDeliveredWithContext { - chainId: NetworkURN - bridgeKey: HexString - event?: AnyJson - extrinsicId?: string - blockNumber: string | number - blockHash: HexString - sender?: SignerData -} - -export interface XcmBridgeInboundWithContext { - chainId: NetworkURN - bridgeKey: HexString - blockNumber: string | number - blockHash: HexString - outcome: 'Success' | 'Fail' - error: AnyJson - event?: AnyJson - extrinsicId?: string -} - -export interface XcmBridgeInboundWithContext { - chainId: NetworkURN - bridgeKey: HexString - blockNumber: string | number - blockHash: HexString - outcome: 'Success' | 'Fail' - error: AnyJson - event?: AnyJson - extrinsicId?: string -} - -export interface XcmInboundWithContext extends XcmWithContext { - outcome: 'Success' | 'Fail' - error: AnyJson - assetsTrapped?: AssetsTrapped -} - -export interface XcmRelayedWithContext extends XcmInboundWithContext { - recipient: NetworkURN - origin: NetworkURN -} - -export class GenericXcmRelayedWithContext implements XcmRelayedWithContext { - event: AnyJson - extrinsicId?: string - blockNumber: string | number - blockHash: HexString - messageHash: HexString - messageId?: HexString - recipient: NetworkURN - origin: NetworkURN - outcome: 'Success' | 'Fail' - error: AnyJson - - constructor(msg: XcmRelayedWithContext) { - this.event = msg.event - this.messageHash = msg.messageHash - this.messageId = msg.messageId ?? msg.messageHash - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.recipient = msg.recipient - this.origin = msg.origin - this.outcome = msg.outcome - this.error = msg.error - } - - toHuman(_isExpanded?: boolean | undefined): Record { - return { - messageHash: this.messageHash, - messageId: this.messageId, - extrinsicId: this.extrinsicId, - blockHash: this.blockHash, - blockNumber: this.blockNumber, - event: this.event, - recipient: this.recipient, - origin: this.origin, - outcome: this.outcome, - error: this.error, - } - } -} - -export class GenericXcmInboundWithContext implements XcmInboundWithContext { - event: AnyJson - extrinsicId?: string | undefined - blockNumber: string - blockHash: HexString - messageHash: HexString - messageId: HexString - outcome: 'Success' | 'Fail' - error: AnyJson - assetsTrapped?: AssetsTrapped | undefined - - constructor(msg: XcmInboundWithContext) { - this.event = msg.event - this.messageHash = msg.messageHash - this.messageId = msg.messageId ?? msg.messageHash - this.outcome = msg.outcome - this.error = msg.error - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.assetsTrapped = msg.assetsTrapped - } - - toHuman(_isExpanded?: boolean | undefined): Record { - return { - messageHash: this.messageHash, - messageId: this.messageId, - extrinsicId: this.extrinsicId, - blockHash: this.blockHash, - blockNumber: this.blockNumber, - event: this.event, - outcome: this.outcome, - error: this.error, - assetsTrapped: this.assetsTrapped, - } - } -} - -export class XcmInbound { - subscriptionId: string - chainId: NetworkURN - event: AnyJson - messageHash: HexString - messageId: HexString - outcome: 'Success' | 'Fail' - error: AnyJson - blockHash: HexString - blockNumber: string - extrinsicId?: string - assetsTrapped?: AssetsTrapped - - constructor(subscriptionId: string, chainId: NetworkURN, msg: XcmInboundWithContext) { - this.subscriptionId = subscriptionId - this.chainId = chainId - this.event = msg.event - this.messageHash = msg.messageHash - this.messageId = msg.messageId ?? msg.messageHash - this.outcome = msg.outcome - this.error = msg.error - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.assetsTrapped = msg.assetsTrapped - } -} - -export class GenericXcmSentWithContext implements XcmSentWithContext { - messageData: Uint8Array - recipient: NetworkURN - instructions: XcmProgram - messageHash: HexString - event: AnyJson - blockHash: HexString - blockNumber: string - sender?: SignerData - extrinsicId?: string - messageId?: HexString - - constructor(msg: XcmSentWithContext) { - this.event = msg.event - this.messageData = msg.messageData - this.recipient = msg.recipient - this.instructions = msg.instructions - this.messageHash = msg.messageHash - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.messageId = msg.messageId - this.sender = msg.sender - } - - toHuman(_isExpanded?: boolean | undefined): Record { - return { - messageData: toHexString(this.messageData), - recipient: this.recipient, - instructions: this.instructions.json, - messageHash: this.messageHash, - event: this.event, - blockHash: this.blockHash, - blockNumber: this.blockNumber, - extrinsicId: this.extrinsicId, - messageId: this.messageId, - senders: this.sender, - } - } -} - -export class GenericXcmBridgeAcceptedWithContext implements XcmBridgeAcceptedWithContext { - chainId: NetworkURN - bridgeKey: HexString - messageData: HexString - recipient: NetworkURN - instructions: AnyJson - messageHash: HexString - event: AnyJson - blockHash: HexString - blockNumber: string - extrinsicId?: string - messageId?: HexString - forwardId?: HexString - - constructor(msg: XcmBridgeAcceptedWithContext) { - this.chainId = msg.chainId - this.bridgeKey = msg.bridgeKey - this.event = msg.event - this.messageData = msg.messageData - this.recipient = msg.recipient - this.instructions = msg.instructions - this.messageHash = msg.messageHash - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.messageId = msg.messageId - this.forwardId = msg.forwardId - } -} - -export class GenericXcmBridgeDeliveredWithContext implements XcmBridgeDeliveredWithContext { - chainId: NetworkURN - bridgeKey: HexString - event?: AnyJson - extrinsicId?: string - blockNumber: string - blockHash: HexString - sender?: SignerData - - constructor(msg: XcmBridgeDeliveredWithContext) { - this.chainId = msg.chainId - this.bridgeKey = msg.bridgeKey - this.event = msg.event - this.extrinsicId = msg.extrinsicId - this.blockNumber = msg.blockNumber.toString() - this.blockHash = msg.blockHash - this.sender = msg.sender - } -} - -export class GenericXcmBridgeInboundWithContext implements XcmBridgeInboundWithContext { - chainId: NetworkURN - bridgeKey: HexString - event: AnyJson - extrinsicId?: string | undefined - blockNumber: string - blockHash: HexString - outcome: 'Success' | 'Fail' - error: AnyJson - - constructor(msg: XcmBridgeInboundWithContext) { - this.chainId = msg.chainId - this.event = msg.event - this.outcome = msg.outcome - this.error = msg.error - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.bridgeKey = msg.bridgeKey - } -} - -export enum XcmNotificationType { - Sent = 'xcm.sent', - Received = 'xcm.received', - Relayed = 'xcm.relayed', - Timeout = 'xcm.timeout', - Hop = 'xcm.hop', - Bridge = 'xcm.bridge', -} - -const XCM_NOTIFICATION_TYPE_ERROR = `at least 1 event type is required [${Object.values(XcmNotificationType).join( - ',' -)}]` - -const XCM_OUTBOUND_TTL_TYPE_ERROR = 'XCM outbound message TTL should be at least 6 seconds' - -/** - * The terminal point of an XCM journey. - * - * @public - */ -export type XcmTerminus = { - chainId: NetworkURN -} - -/** - * The terminal point of an XCM journey with contextual information. - * - * @public - */ -export interface XcmTerminusContext extends XcmTerminus { - blockNumber: string - blockHash: HexString - extrinsicId?: string - event: AnyJson - outcome: 'Success' | 'Fail' - error: AnyJson - messageHash: HexString - messageData: string - instructions: AnyJson -} - -/** - * The contextual information of an XCM journey waypoint. - * - * @public - */ -export interface XcmWaypointContext extends XcmTerminusContext { - legIndex: number - assetsTrapped?: AnyJson -} - -/** - * Type of an XCM journey leg. - * - * @public - */ -export const legType = ['bridge', 'hop', 'hrmp', 'vmp'] as const - -/** - * A leg of an XCM journey. - * - * @public - */ -export type Leg = { - from: NetworkURN - to: NetworkURN - relay?: NetworkURN - type: (typeof legType)[number] -} - -/** - * Event emitted when an XCM is sent. - * - * @public - */ -export interface XcmSent { - type: XcmNotificationType - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString -} - -export class GenericXcmSent implements XcmSent { - type: XcmNotificationType = XcmNotificationType.Sent - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor( - subscriptionId: string, - chainId: NetworkURN, - msg: XcmSentWithContext, - legs: Leg[], - forwardId?: HexString - ) { - this.subscriptionId = subscriptionId - this.legs = legs - this.origin = { - chainId, - blockHash: msg.blockHash, - blockNumber: msg.blockNumber.toString(), - extrinsicId: msg.extrinsicId, - event: msg.event, - outcome: 'Success', - error: null, - messageData: toHexString(msg.messageData), - instructions: msg.instructions.json, - messageHash: msg.messageHash, - } - this.destination = { - chainId: legs[legs.length - 1].to, // last stop is the destination - } - this.waypoint = { - ...this.origin, - legIndex: 0, - messageData: toHexString(msg.messageData), - instructions: msg.instructions.json, - messageHash: msg.messageHash, - } - - this.messageId = msg.messageId - this.forwardId = forwardId - this.sender = msg.sender - } -} - -/** - * Event emitted when an XCM is received. - * - * @public - */ -export interface XcmReceived { - type: XcmNotificationType - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminusContext - sender?: SignerData - messageId?: HexString - forwardId?: HexString -} - -/** - * Event emitted when an XCM is not received within a specified timeframe. - * - * @public - */ -export type XcmTimeout = XcmSent - -export class GenericXcmTimeout implements XcmTimeout { - type: XcmNotificationType = XcmNotificationType.Timeout - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor(msg: XcmSent) { - this.subscriptionId = msg.subscriptionId - this.legs = msg.legs - this.origin = msg.origin - this.destination = msg.destination - this.waypoint = msg.waypoint - this.messageId = msg.messageId - this.sender = msg.sender - this.forwardId = msg.forwardId - } -} - -export class GenericXcmReceived implements XcmReceived { - type: XcmNotificationType = XcmNotificationType.Received - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminusContext - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor(outMsg: XcmSent, inMsg: XcmInbound) { - this.subscriptionId = outMsg.subscriptionId - this.legs = outMsg.legs - this.destination = { - chainId: inMsg.chainId, - blockNumber: inMsg.blockNumber, - blockHash: inMsg.blockHash, - extrinsicId: inMsg.extrinsicId, - event: inMsg.event, - outcome: inMsg.outcome, - error: inMsg.error, - instructions: outMsg.waypoint.instructions, - messageData: outMsg.waypoint.messageData, - messageHash: outMsg.waypoint.messageHash, - } - this.origin = outMsg.origin - this.waypoint = { - ...this.destination, - legIndex: this.legs.findIndex((l) => l.to === inMsg.chainId && l.type !== 'bridge'), - instructions: outMsg.waypoint.instructions, - messageData: outMsg.waypoint.messageData, - messageHash: outMsg.waypoint.messageHash, - assetsTrapped: inMsg.assetsTrapped, - } - this.sender = outMsg.sender - this.messageId = outMsg.messageId - this.forwardId = outMsg.forwardId - } -} - -/** - * Event emitted when an XCM is received on the relay chain - * for an HRMP message. - * - * @public - */ -export type XcmRelayed = XcmSent - -export class GenericXcmRelayed implements XcmRelayed { - type: XcmNotificationType = XcmNotificationType.Relayed - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor(outMsg: XcmSent, relayMsg: XcmRelayedWithContext) { - this.subscriptionId = outMsg.subscriptionId - this.legs = outMsg.legs - this.destination = outMsg.destination - this.origin = outMsg.origin - this.waypoint = { - legIndex: outMsg.legs.findIndex((l) => l.from === relayMsg.origin && l.relay !== undefined), - chainId: createNetworkId(relayMsg.origin, '0'), // relay waypoint always at relay chain - blockNumber: relayMsg.blockNumber.toString(), - blockHash: relayMsg.blockHash, - extrinsicId: relayMsg.extrinsicId, - event: relayMsg.event, - outcome: relayMsg.outcome, - error: relayMsg.error, - instructions: outMsg.waypoint.instructions, - messageData: outMsg.waypoint.messageData, - messageHash: outMsg.waypoint.messageHash, - } - this.sender = outMsg.sender - this.messageId = outMsg.messageId - this.forwardId = outMsg.forwardId - } -} - -/** - * Event emitted when an XCM is sent or received on an intermediate stop. - * - * @public - */ -export interface XcmHop extends XcmSent { - direction: 'out' | 'in' -} - -export class GenericXcmHop implements XcmHop { - type: XcmNotificationType = XcmNotificationType.Hop - direction: 'out' | 'in' - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor(originMsg: XcmSent, hopWaypoint: XcmWaypointContext, direction: 'out' | 'in') { - this.subscriptionId = originMsg.subscriptionId - this.legs = originMsg.legs - this.origin = originMsg.origin - this.destination = originMsg.destination - this.waypoint = hopWaypoint - this.messageId = originMsg.messageId - this.sender = originMsg.sender - this.direction = direction - this.forwardId = originMsg.forwardId - } -} - -export type BridgeMessageType = 'accepted' | 'delivered' | 'received' - -/** - * Event emitted when an XCM is sent or received on an intermediate stop. - * - * @public - */ -export interface XcmBridge extends XcmSent { - bridgeKey: HexString - bridgeMessageType: BridgeMessageType -} - -type XcmBridgeContext = { - bridgeMessageType: BridgeMessageType - bridgeKey: HexString - forwardId?: HexString -} - -export class GenericXcmBridge implements XcmBridge { - type: XcmNotificationType = XcmNotificationType.Bridge - bridgeMessageType: BridgeMessageType - subscriptionId: string - bridgeKey: HexString - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor( - originMsg: XcmSent, - waypoint: XcmWaypointContext, - { bridgeKey, bridgeMessageType, forwardId }: XcmBridgeContext - ) { - this.subscriptionId = originMsg.subscriptionId - this.bridgeMessageType = bridgeMessageType - this.legs = originMsg.legs - this.origin = originMsg.origin - this.destination = originMsg.destination - this.waypoint = waypoint - this.messageId = originMsg.messageId - this.sender = originMsg.sender - this.bridgeKey = bridgeKey - this.forwardId = forwardId - } -} - -/** - * The XCM event types. - * - * @public - */ -export type XcmNotifyMessage = XcmSent | XcmReceived | XcmRelayed | XcmHop | XcmBridge - -export function isXcmSent(object: any): object is XcmSent { - return object.type !== undefined && object.type === XcmNotificationType.Sent -} - -export function isXcmReceived(object: any): object is XcmReceived { - return object.type !== undefined && object.type === XcmNotificationType.Received -} - -export function isXcmHop(object: any): object is XcmHop { - return object.type !== undefined && object.type === XcmNotificationType.Hop -} - -export function isXcmRelayed(object: any): object is XcmRelayed { - return object.type !== undefined && object.type === XcmNotificationType.Relayed -} - const $WebhookNotification = z.object({ type: z.literal('webhook'), url: z @@ -815,8 +87,6 @@ const $WebhookNotification = z.object({ .regex(/(?:application|text)\/[a-z0-9-+.]+/i) .max(250) ), - // prevent using $refs - events: z.optional(z.literal('*').or(z.array(z.nativeEnum(XcmNotificationType)).min(1, XCM_NOTIFICATION_TYPE_ERROR))), template: z.optional(z.string().min(5).max(32_000)), bearer: z.optional(z.string().min(1).max(1_000)), limit: z.optional(z.number().min(0).max(Number.MAX_SAFE_INTEGER)), @@ -830,44 +100,26 @@ const $WebsocketNotification = z.object({ type: z.literal('websocket'), }) -function distinct(a: Array) { - return Array.from(new Set(a)) -} +export const $AgentArgs = z.record( + z.string({ + required_error: 'argument name is required', + }), + z.any() +) -const bridgeTypes = ['pk-bridge', 'snowbridge'] as const +export const $AgentId = $SafeId -export type BridgeType = (typeof bridgeTypes)[number] +export type AgentId = z.infer export const $Subscription = z .object({ id: $SafeId, - origin: z - .string({ - required_error: 'origin id is required', - }) - .min(1), - senders: z.optional( - z.literal('*').or(z.array(z.string()).min(1, 'at least 1 sender address is required').transform(distinct)) - ), - destinations: z - .array( - z - .string({ - required_error: 'destination id is required', - }) - .min(1) - ) - .transform(distinct), - bridges: z.optional(z.array(z.enum(bridgeTypes)).min(1, 'Please specify at least one bridge.')), + agent: $AgentId, + args: $AgentArgs, ephemeral: z.optional(z.boolean()), channels: z .array(z.discriminatedUnion('type', [$WebhookNotification, $LogNotification, $WebsocketNotification])) .min(1), - // prevent using $refs - events: z.optional( - z.literal('*').or(z.array(z.nativeEnum(XcmNotificationType)).min(1, XCM_NOTIFICATION_TYPE_ERROR)) - ), - outboundTTL: z.optional(z.number().min(6000, XCM_OUTBOUND_TTL_TYPE_ERROR).max(Number.MAX_SAFE_INTEGER)), }) .refine( (schema) => @@ -887,18 +139,6 @@ export type RxSubscriptionWithId = { sub: RxSubscription } -export type BridgeSubscription = { type: BridgeType; subs: RxSubscriptionWithId[] } - -export type RxSubscriptionHandler = { - originSubs: RxSubscriptionWithId[] - destinationSubs: RxSubscriptionWithId[] - bridgeSubs: BridgeSubscription[] - sendersControl: ControlQuery - messageControl: ControlQuery - descriptor: Subscription - relaySub?: RxSubscriptionWithId -} - export type SubscriptionStats = { persistent: number ephemeral: number @@ -909,10 +149,3 @@ export type BinBlock = { events: Uint8Array[] author?: Uint8Array } - -export type MessageQueueEventContext = { - id: U8aFixed - origin: PolkadotRuntimeParachainsInclusionAggregateMessageOrigin - success?: bool - error?: FrameSupportMessagesProcessMessageError -} diff --git a/packages/server/src/services/notification/hub.ts b/packages/server/src/services/notification/hub.ts index 83a36767..0f946352 100644 --- a/packages/server/src/services/notification/hub.ts +++ b/packages/server/src/services/notification/hub.ts @@ -1,6 +1,7 @@ import EventEmitter from 'node:events' -import { Subscription, XcmNotifyMessage } from '../monitoring/types.js' +import { XcmNotifyMessage } from 'agents/xcm/types.js' +import { Subscription } from '../monitoring/types.js' import { TelemetryNotifierEventKeys } from '../telemetry/types.js' import { Services } from '../types.js' import { LogNotifier } from './log.js' diff --git a/packages/server/src/services/notification/log.ts b/packages/server/src/services/notification/log.ts index 28be945e..bb3ae1b8 100644 --- a/packages/server/src/services/notification/log.ts +++ b/packages/server/src/services/notification/log.ts @@ -1,8 +1,6 @@ import EventEmitter from 'node:events' -import { Logger, Services } from '../../services/types.js' import { - Subscription, XcmHop, XcmNotificationType, XcmNotifyMessage, @@ -10,7 +8,9 @@ import { isXcmReceived, isXcmRelayed, isXcmSent, -} from '../monitoring/types.js' +} from 'agents/xcm/types.js' +import { Logger, Services } from '../../services/types.js' +import { Subscription } from '../monitoring/types.js' import { NotifierHub } from './hub.js' import { Notifier, NotifierEmitter } from './types.js' diff --git a/packages/server/src/services/notification/types.ts b/packages/server/src/services/notification/types.ts index 09e52bd2..1d043953 100644 --- a/packages/server/src/services/notification/types.ts +++ b/packages/server/src/services/notification/types.ts @@ -1,5 +1,6 @@ +import { XcmNotifyMessage } from 'agents/xcm/types.js' import { TypedEventEmitter } from '../index.js' -import { Subscription, XcmNotifyMessage } from '../monitoring/types.js' +import { Subscription } from '../monitoring/types.js' import { TelemetryNotifierEvents } from '../telemetry/types.js' export type NotifierEvents = { diff --git a/packages/server/src/services/notification/webhook.ts b/packages/server/src/services/notification/webhook.ts index e3839724..fc6f7d21 100644 --- a/packages/server/src/services/notification/webhook.ts +++ b/packages/server/src/services/notification/webhook.ts @@ -3,8 +3,9 @@ import { EventEmitter } from 'node:events' import got from 'got' import { ulid } from 'ulidx' +import { XcmNotifyMessage } from 'agents/xcm/types.js' import version from '../../version.js' -import { Subscription, WebhookNotification, XcmNotifyMessage } from '../monitoring/types.js' +import { Subscription, WebhookNotification } from '../monitoring/types.js' import { Logger, Services } from '../types.js' import { Scheduled, Scheduler, SubsStore } from '../persistence/index.js' @@ -81,9 +82,7 @@ export class WebhookNotifier extends (EventEmitter as new () => NotifierEmitter) for (const chan of channels) { if (chan.type === 'webhook') { const config = chan as WebhookNotification - if (config.events === undefined || config.events === '*' || config.events.includes(scheduled.task.msg.type)) { - await this.#post(scheduled, config) - } + await this.#post(scheduled, config) } } } catch (error) { diff --git a/packages/server/src/services/persistence/subs.ts b/packages/server/src/services/persistence/subs.ts index 1f397ca0..6dfcae3c 100644 --- a/packages/server/src/services/persistence/subs.ts +++ b/packages/server/src/services/persistence/subs.ts @@ -1,7 +1,7 @@ +import { AgentService } from '../../agents/types.js' import { NotFound, ValidationError } from '../../errors.js' -import { IngressConsumer } from '../ingress/index.js' -import { Subscription } from '../monitoring/types.js' -import { BatchOperation, DB, Logger, NetworkURN, jsonEncoded, prefixes } from '../types.js' +import { AgentId, Subscription } from '../monitoring/types.js' +import { DB, Logger, NetworkURN, jsonEncoded, prefixes } from '../types.js' /** * Subscriptions persistence. @@ -11,12 +11,12 @@ import { BatchOperation, DB, Logger, NetworkURN, jsonEncoded, prefixes } from '. export class SubsStore { // readonly #log: Logger; readonly #db: DB - readonly #ingress: IngressConsumer + readonly #agentService: AgentService - constructor(_log: Logger, db: DB, ingress: IngressConsumer) { + constructor(_log: Logger, db: DB, agentService: AgentService) { // this.#log = log; this.#db = db - this.#ingress = ingress + this.#agentService = agentService } /** @@ -40,8 +40,8 @@ export class SubsStore { */ async getAll() { let subscriptions: Subscription[] = [] - for (const chainId of this.#ingress.getChainIds()) { - const subs = await this.getByNetworkId(chainId) + for (const chainId of this.#agentService.getAgentIds()) { + const subs = await this.getByAgentId(chainId) subscriptions = subscriptions.concat(subs) } @@ -49,12 +49,12 @@ export class SubsStore { } /** - * Retrieves all the subscriptions for a given network. + * Retrieves all the subscriptions for a given agent. * * @returns {Subscription[]} an array with the subscriptions */ - async getByNetworkId(chainId: NetworkURN) { - return await this.#subsFamily(chainId).values().all() + async getByAgentId(agentId: AgentId) { + return await this.#subsFamily(agentId).values().all() } /** @@ -65,9 +65,9 @@ export class SubsStore { * @throws {NotFound} if the subscription does not exist */ async getById(id: string) { - for (const chainId of this.#ingress.getChainIds()) { + for (const agentId of this.#agentService.getAgentIds()) { try { - const subscription = await this.#subsFamily(chainId).get(id) + const subscription = await this.#subsFamily(agentId).get(id) return subscription } catch { continue @@ -82,11 +82,11 @@ export class SubsStore { * * @throws {ValidationError} if there is a validation error. */ - async insert(qs: Subscription) { - if (await this.exists(qs.id)) { - throw new ValidationError(`Subscription with ID ${qs.id} already exists`) + async insert(s: Subscription) { + if (await this.exists(s.id)) { + throw new ValidationError(`Subscription with ID ${s.id} already exists`) } - await this.save(qs) + await this.save(s) } /** @@ -94,38 +94,20 @@ export class SubsStore { * * @throws {ValidationError} if there is a validation error. */ - async save(qs: Subscription) { - const origin = qs.origin as NetworkURN - const dests = qs.destinations as NetworkURN[] - this.#validateChainIds([origin, ...dests]) - const db = await this.#subsFamily(origin) - await db.put(qs.id, qs) + async save(s: Subscription) { + const db = await this.#subsFamily(s.agent) + await db.put(s.id, s) } /** * Removes a subscription for the given id. */ async remove(id: string) { - const qs = await this.getById(id) - const origin = qs.origin as NetworkURN - const ops: BatchOperation[] = [] - ops.push({ - type: 'del', - sublevel: this.#subsFamily(origin), - key: id, - }) - await this.#db.batch(ops) + const s = await this.getById(id) + await this.#subsFamily(s.agent).del(id) } - #subsFamily(chainId: NetworkURN) { - return this.#db.sublevel(prefixes.subs.family(chainId), jsonEncoded) - } - - #validateChainIds(chainIds: NetworkURN[]) { - chainIds.forEach((chainId) => { - if (!this.#ingress.isNetworkDefined(chainId)) { - throw new ValidationError('Invalid chain id:' + chainId) - } - }) + #subsFamily(agentId: AgentId) { + return this.#db.sublevel(prefixes.subs.family(agentId), jsonEncoded) } } diff --git a/packages/server/src/services/telemetry/metrics/engine.ts b/packages/server/src/services/telemetry/metrics/engine.ts index 1e4039cb..eafffe8e 100644 --- a/packages/server/src/services/telemetry/metrics/engine.ts +++ b/packages/server/src/services/telemetry/metrics/engine.ts @@ -1,6 +1,6 @@ import { Counter } from 'prom-client' -import { XcmBridge, XcmHop, XcmInbound, XcmRelayed, XcmSent, XcmTimeout } from '../../monitoring/types.js' +import { XcmBridge, XcmHop, XcmInbound, XcmRelayed, XcmSent, XcmTimeout } from 'agents/xcm/types.js' import { TelemetryEventEmitter } from '../types.js' export function engineMetrics(source: TelemetryEventEmitter) { diff --git a/packages/server/src/services/telemetry/metrics/index.ts b/packages/server/src/services/telemetry/metrics/index.ts index 3d0f4c4f..c16d663c 100644 --- a/packages/server/src/services/telemetry/metrics/index.ts +++ b/packages/server/src/services/telemetry/metrics/index.ts @@ -1,7 +1,7 @@ +import { MatchingEngine } from '../../../agents/xcm/matching.js' import { IngressConsumer } from '../../ingress/index.js' import IngressProducer from '../../ingress/producer/index.js' import { HeadCatcher } from '../../ingress/watcher/head-catcher.js' -import { MatchingEngine } from '../../monitoring/matching.js' import { Switchboard } from '../../monitoring/switchboard.js' import { NotifierHub } from '../../notification/hub.js' import { TelemetryEventEmitter } from '../types.js' diff --git a/packages/server/src/services/telemetry/metrics/ws.ts b/packages/server/src/services/telemetry/metrics/ws.ts index 5c1ac4aa..969175ac 100644 --- a/packages/server/src/services/telemetry/metrics/ws.ts +++ b/packages/server/src/services/telemetry/metrics/ws.ts @@ -13,11 +13,11 @@ export function wsMetrics(source: TelemetryEventEmitter) { }) source.on('telemetrySocketListener', (ip, sub, close = false) => { - const gauge = socketListenerCount.labels('websocket', sub.id, sub.origin, sub.destinations.join(','), ip) + /*const gauge = socketListenerCount.labels('websocket', sub.id, sub.origin, sub.destinations.join(','), ip) if (close) { gauge.dec() } else { gauge.inc() - } + }*/ }) } diff --git a/packages/server/src/services/telemetry/types.ts b/packages/server/src/services/telemetry/types.ts index fe694f7e..caaf8ff8 100644 --- a/packages/server/src/services/telemetry/types.ts +++ b/packages/server/src/services/telemetry/types.ts @@ -1,15 +1,7 @@ import type { Header } from '@polkadot/types/interfaces' -import { - Subscription, - XcmBridge, - XcmHop, - XcmInbound, - XcmNotifyMessage, - XcmRelayed, - XcmSent, - XcmTimeout, -} from '../monitoring/types.js' +import { XcmBridge, XcmHop, XcmInbound, XcmNotifyMessage, XcmRelayed, XcmSent, XcmTimeout } from 'agents/xcm/types.js' +import { Subscription } from '../monitoring/types.js' import { TypedEventEmitter } from '../types.js' export type NotifyTelemetryMessage = { diff --git a/packages/server/src/services/types.ts b/packages/server/src/services/types.ts index 62b2275e..cd8f0861 100644 --- a/packages/server/src/services/types.ts +++ b/packages/server/src/services/types.ts @@ -1,9 +1,10 @@ import { AbstractBatchOperation, AbstractLevel, AbstractSublevel } from 'abstract-level' +import { AgentService } from 'agents/types.js' import { FastifyBaseLogger } from 'fastify' import { ServiceConfiguration } from './config.js' import { IngressConsumer } from './ingress/consumer/index.js' -import { BlockNumberRange, HexString } from './monitoring/types.js' +import { AgentId, BlockNumberRange, HexString } from './monitoring/types.js' import Connector from './networking/connector.js' import { Janitor } from './persistence/janitor.js' import { Scheduler } from './persistence/scheduler.js' @@ -29,7 +30,7 @@ export enum LevelEngine { */ export const prefixes = { subs: { - family: (chainId: NetworkURN) => `su:${chainId}`, + family: (agentId: AgentId) => `su:${agentId}`, }, sched: { tasks: 'sc:tasks', @@ -99,5 +100,6 @@ export type Services = { scheduler: Scheduler localConfig: ServiceConfiguration ingressConsumer: IngressConsumer + agentService: AgentService connector: Connector } diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 29adc8e0..0365020e 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -41,9 +41,18 @@ export const $SubscriptionServerOptions = z.object({ subscriptionMaxPersistent: z.number().min(0).optional(), }) +export enum AgentServiceMode { + local = 'local', +} + +export const $AgentServiceOptions = z.object({ + mode: z.nativeEnum(AgentServiceMode).default(AgentServiceMode.local), +}) + export type CorsServerOptions = z.infer export type ConfigServerOptions = z.infer export type RedisServerOptions = z.infer export type IngressOptions = { distributed?: boolean } & RedisServerOptions +export type AgentServiceOptions = z.infer From 48427e674bf314653d2652db603b53caa671d9c1 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Tue, 28 May 2024 18:04:09 +0200 Subject: [PATCH 02/58] fix import path --- packages/server/src/agents/xcm/ops/common.ts | 2 +- packages/server/src/agents/xcm/ops/dmp.ts | 8 ++++---- packages/server/src/agents/xcm/ops/pk-bridge.ts | 12 ++++++------ packages/server/src/agents/xcm/ops/relay.ts | 2 +- packages/server/src/agents/xcm/ops/ump.ts | 8 ++++---- packages/server/src/agents/xcm/ops/util.ts | 2 +- packages/server/src/agents/xcm/ops/xcmp.ts | 8 ++++---- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/server/src/agents/xcm/ops/common.ts b/packages/server/src/agents/xcm/ops/common.ts index 3a76f817..7661f211 100644 --- a/packages/server/src/agents/xcm/ops/common.ts +++ b/packages/server/src/agents/xcm/ops/common.ts @@ -6,10 +6,10 @@ import { hexToU8a, stringToU8a, u8aConcat } from '@polkadot/util' import { blake2AsHex } from '@polkadot/util-crypto' import { types } from '@sodazone/ocelloids-sdk' -import { GenericXcmSent, Leg, XcmSent, XcmSentWithContext } from 'agents/xcm/types.js' import { createNetworkId, getChainId, getConsensus, isOnSameConsensus } from '../../../services/config.js' import { AnyJson, HexString } from '../../../services/monitoring/types.js' import { NetworkURN } from '../../../services/types.js' +import { GenericXcmSent, Leg, XcmSent, XcmSentWithContext } from '../types.js' import { getBridgeHubNetworkId, getParaIdFromJunctions, diff --git a/packages/server/src/agents/xcm/ops/dmp.ts b/packages/server/src/agents/xcm/ops/dmp.ts index 8e74718c..a96d94d2 100644 --- a/packages/server/src/agents/xcm/ops/dmp.ts +++ b/packages/server/src/agents/xcm/ops/dmp.ts @@ -10,15 +10,15 @@ import type { Registry } from '@polkadot/types/types' import { filterNonNull, types } from '@sodazone/ocelloids-sdk' +import { AnyJson, SignerData } from '../../../services/monitoring/types.js' +import { NetworkURN } from '../../../services/types.js' +import { GetDownwardMessageQueues } from '../types-augmented.js' import { GenericXcmInboundWithContext, GenericXcmSentWithContext, XcmInboundWithContext, XcmSentWithContext, -} from 'agents/xcm/types.js' -import { AnyJson, SignerData } from '../../../services/monitoring/types.js' -import { NetworkURN } from '../../../services/types.js' -import { GetDownwardMessageQueues } from '../types-augmented.js' +} from '../types.js' import { blockEventToHuman } from './common.js' import { getMessageId, diff --git a/packages/server/src/agents/xcm/ops/pk-bridge.ts b/packages/server/src/agents/xcm/ops/pk-bridge.ts index f1ab068f..4eb98154 100644 --- a/packages/server/src/agents/xcm/ops/pk-bridge.ts +++ b/packages/server/src/agents/xcm/ops/pk-bridge.ts @@ -7,6 +7,11 @@ import { Observable, filter, from, mergeMap } from 'rxjs' import { types } from '@sodazone/ocelloids-sdk' +import { getConsensus } from '../../../services/config.js' +import { bridgeStorageKeys } from '../../../services/monitoring/storage.js' +import { HexString } from '../../../services/monitoring/types.js' +import { NetworkURN } from '../../../services/types.js' +import { GetStorageAt } from '../types-augmented.js' import { GenericXcmBridgeAcceptedWithContext, GenericXcmBridgeDeliveredWithContext, @@ -14,12 +19,7 @@ import { XcmBridgeAcceptedWithContext, XcmBridgeDeliveredWithContext, XcmBridgeInboundWithContext, -} from 'agents/xcm/types.js' -import { getConsensus } from '../../../services/config.js' -import { bridgeStorageKeys } from '../../../services/monitoring/storage.js' -import { HexString } from '../../../services/monitoring/types.js' -import { NetworkURN } from '../../../services/types.js' -import { GetStorageAt } from '../types-augmented.js' +} from '../types.js' import { blockEventToHuman } from './common.js' import { getMessageId, getSendersFromEvent, matchEvent, networkIdFromInteriorLocation } from './util.js' import { diff --git a/packages/server/src/agents/xcm/ops/relay.ts b/packages/server/src/agents/xcm/ops/relay.ts index b057d2b6..d920bfc6 100644 --- a/packages/server/src/agents/xcm/ops/relay.ts +++ b/packages/server/src/agents/xcm/ops/relay.ts @@ -4,9 +4,9 @@ import type { PolkadotPrimitivesV6InherentData } from '@polkadot/types/lookup' import type { Registry } from '@polkadot/types/types' import { ControlQuery, filterNonNull, types } from '@sodazone/ocelloids-sdk' -import { GenericXcmRelayedWithContext, XcmRelayedWithContext } from 'agents/xcm/types.js' import { createNetworkId, getChainId } from '../../../services/config.js' import { NetworkURN } from '../../../services/types.js' +import { GenericXcmRelayedWithContext, XcmRelayedWithContext } from '../types.js' import { getMessageId, matchExtrinsic } from './util.js' import { fromXcmpFormat } from './xcm-format.js' diff --git a/packages/server/src/agents/xcm/ops/ump.ts b/packages/server/src/agents/xcm/ops/ump.ts index bf2210ee..a5e5cf3c 100644 --- a/packages/server/src/agents/xcm/ops/ump.ts +++ b/packages/server/src/agents/xcm/ops/ump.ts @@ -6,15 +6,15 @@ import type { Registry } from '@polkadot/types/types' import { filterNonNull, types } from '@sodazone/ocelloids-sdk' +import { getChainId, getRelayId } from '../../../services/config.js' +import { NetworkURN } from '../../../services/types.js' +import { GetOutboundUmpMessages } from '../types-augmented.js' import { GenericXcmInboundWithContext, GenericXcmSentWithContext, XcmInboundWithContext, XcmSentWithContext, -} from 'agents/xcm/types.js' -import { getChainId, getRelayId } from '../../../services/config.js' -import { NetworkURN } from '../../../services/types.js' -import { GetOutboundUmpMessages } from '../types-augmented.js' +} from '../types.js' import { MessageQueueEventContext } from '../types.js' import { blockEventToHuman, xcmMessagesSent } from './common.js' import { getMessageId, getParaIdFromOrigin, mapAssetsTrapped, matchEvent } from './util.js' diff --git a/packages/server/src/agents/xcm/ops/util.ts b/packages/server/src/agents/xcm/ops/util.ts index 91cdecc8..b379953f 100644 --- a/packages/server/src/agents/xcm/ops/util.ts +++ b/packages/server/src/agents/xcm/ops/util.ts @@ -13,10 +13,10 @@ import type { import { types } from '@sodazone/ocelloids-sdk' -import { AssetsTrapped, TrappedAsset } from 'agents/xcm/types.js' import { GlobalConsensus, createNetworkId, getConsensus, isGlobalConsensus } from '../../../services/config.js' import { HexString, SignerData } from '../../../services/monitoring/types.js' import { NetworkURN } from '../../../services/types.js' +import { AssetsTrapped, TrappedAsset } from '../types.js' import { VersionedInteriorLocation, XcmV4AssetAssets, diff --git a/packages/server/src/agents/xcm/ops/xcmp.ts b/packages/server/src/agents/xcm/ops/xcmp.ts index a7f8df49..9a61051c 100644 --- a/packages/server/src/agents/xcm/ops/xcmp.ts +++ b/packages/server/src/agents/xcm/ops/xcmp.ts @@ -3,15 +3,15 @@ import { Observable, bufferCount, filter, map, mergeMap } from 'rxjs' import { filterNonNull, types } from '@sodazone/ocelloids-sdk' +import { createNetworkId } from '../../../services/config.js' +import { NetworkURN } from '../../../services/types.js' +import { GetOutboundHrmpMessages } from '../types-augmented.js' import { GenericXcmInboundWithContext, GenericXcmSentWithContext, XcmInboundWithContext, XcmSentWithContext, -} from 'agents/xcm/types.js' -import { createNetworkId } from '../../../services/config.js' -import { NetworkURN } from '../../../services/types.js' -import { GetOutboundHrmpMessages } from '../types-augmented.js' +} from '../types.js' import { MessageQueueEventContext } from '../types.js' import { blockEventToHuman, xcmMessagesSent } from './common.js' import { getMessageId, mapAssetsTrapped, matchEvent } from './util.js' From 0be578ea12d8fef7b141c63b40fc6e25a146c318 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Tue, 28 May 2024 18:04:59 +0200 Subject: [PATCH 03/58] fix import path --- packages/server/src/agents/xcm/matching.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/agents/xcm/matching.ts b/packages/server/src/agents/xcm/matching.ts index 85dffff9..24dd72e7 100644 --- a/packages/server/src/agents/xcm/matching.ts +++ b/packages/server/src/agents/xcm/matching.ts @@ -22,7 +22,7 @@ import { XcmSent, XcmTimeout, XcmWaypointContext, -} from 'agents/xcm/types.js' +} from './types.js' import { DB, Logger, Services, jsonEncoded, prefixes } from '../../services/types.js' import { getRelayId, isOnSameConsensus } from '../../services/config.js' From a9be2514fb78e30f3c0ec1acc8a8266195fb7848 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Tue, 28 May 2024 18:06:10 +0200 Subject: [PATCH 04/58] fix comment --- packages/server/src/agents/plugin.ts | 2 +- packages/server/src/agents/xcm/matching.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/agents/plugin.ts b/packages/server/src/agents/plugin.ts index 800f8b17..d15794cf 100644 --- a/packages/server/src/agents/plugin.ts +++ b/packages/server/src/agents/plugin.ts @@ -15,7 +15,7 @@ declare module 'fastify' { * Fastify plug-in for instantiating an {@link AgentService} instance. * * @param fastify The Fastify instance. - * @param options Options for configuring the IngressConsumer. + * @param options Options for configuring the Agent Service. */ const agentServicePlugin: FastifyPluginAsync = async (fastify, options) => { if (options.mode !== AgentServiceMode.local) { diff --git a/packages/server/src/agents/xcm/matching.ts b/packages/server/src/agents/xcm/matching.ts index 24dd72e7..098c7a50 100644 --- a/packages/server/src/agents/xcm/matching.ts +++ b/packages/server/src/agents/xcm/matching.ts @@ -3,6 +3,7 @@ import EventEmitter from 'node:events' import { AbstractSublevel } from 'abstract-level' import { Mutex } from 'async-mutex' +import { DB, Logger, Services, jsonEncoded, prefixes } from '../../services/types.js' import { GenericXcmBridge, GenericXcmHop, @@ -23,7 +24,6 @@ import { XcmTimeout, XcmWaypointContext, } from './types.js' -import { DB, Logger, Services, jsonEncoded, prefixes } from '../../services/types.js' import { getRelayId, isOnSameConsensus } from '../../services/config.js' import { Janitor, JanitorTask } from '../../services/persistence/janitor.js' From ada037cadb91c0ebe8be422443411e239ae0b246 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 09:49:56 +0200 Subject: [PATCH 05/58] move api impls --- packages/server/src/agents/local.ts | 14 ++- packages/server/src/agents/types.ts | 10 +- packages/server/src/agents/xcm/xcm-agent.ts | 91 +++++++++++++++---- .../src/services/monitoring/api/routes.ts | 64 ++++--------- .../src/services/monitoring/switchboard.ts | 24 +++++ 5 files changed, 134 insertions(+), 69 deletions(-) diff --git a/packages/server/src/agents/local.ts b/packages/server/src/agents/local.ts index dfc4e23d..a615e41b 100644 --- a/packages/server/src/agents/local.ts +++ b/packages/server/src/agents/local.ts @@ -4,6 +4,9 @@ import { AgentServiceOptions } from '../types.js' import { Agent, AgentService } from './types.js' import { XCMAgent } from './xcm/xcm-agent.js' +/** + * Local agent service. + */ export class LocalAgentService implements AgentService { readonly #log: Logger readonly #agents: Record @@ -21,19 +24,24 @@ export class LocalAgentService implements AgentService { if (this.#agents[agentId]) { return this.#agents[agentId] } - throw new Error(`Agent not found with id=${agentId}`) + throw new Error(`Agent not found for id=${agentId}`) + } + + getAgentInputSchema(agentId: AgentId) { + const agent = this.getAgentById(agentId) + return agent.getInputSchema() } async start() { for (const [id, agent] of Object.entries(this.#agents)) { - this.#log.info('[agents:local] Starting agent %s', id) + this.#log.info('[local:agents] Starting agent %s', id) await agent.start() } } async stop() { for (const [id, agent] of Object.entries(this.#agents)) { - this.#log.info('[agents:local] Stopping agent %s', id) + this.#log.info('[local:agents] Stopping agent %s', id) await agent.stop() } } diff --git a/packages/server/src/agents/types.ts b/packages/server/src/agents/types.ts index 928761af..1b9e27b0 100644 --- a/packages/server/src/agents/types.ts +++ b/packages/server/src/agents/types.ts @@ -1,19 +1,25 @@ +import { z } from 'zod' + +import { Operation } from 'rfc6902' import { AgentId, Subscription } from '../services/monitoring/types.js' export interface AgentService { getAgentById(agentId: AgentId): Agent + getAgentInputSchema(agentId: AgentId): z.ZodSchema getAgentIds(): AgentId[] start(): Promise stop(): Promise } export interface Agent { + getSubscriptionById(subscriptionId: string): Promise + getAllSubscriptions(): Promise + getInputSchema(): z.ZodSchema get id(): AgentId getSubscriptionHandler(subscriptionId: string): Subscription subscribe(subscription: Subscription): Promise - // TODO - // update(s: ):void unsubscribe(subscriptionId: string): Promise + update(subscriptionId: string, patch: Operation[]): Promise stop(): Promise start(): Promise } diff --git a/packages/server/src/agents/xcm/xcm-agent.ts b/packages/server/src/agents/xcm/xcm-agent.ts index 15699611..42531fc0 100644 --- a/packages/server/src/agents/xcm/xcm-agent.ts +++ b/packages/server/src/agents/xcm/xcm-agent.ts @@ -2,7 +2,13 @@ import { Registry } from '@polkadot/types-codec/types' import { ControlQuery, extractEvents, extractTxWithEvents, flattenCalls, types } from '@sodazone/ocelloids-sdk' import { Observable, filter, from, map, share, switchMap } from 'rxjs' -import { AgentId, HexString, RxSubscriptionWithId, Subscription } from '../../services/monitoring/types.js' +import { + $Subscription, + AgentId, + HexString, + RxSubscriptionWithId, + Subscription, +} from '../../services/monitoring/types.js' import { Logger, NetworkURN, Services } from '../../services/types.js' import { extractXcmpReceive, extractXcmpSend } from './ops/xcmp.js' import { @@ -34,6 +40,8 @@ import { extractDmpReceive, extractDmpSend, extractDmpSendByEvent } from './ops/ import { extractRelayReceive } from './ops/relay.js' import { extractUmpReceive, extractUmpSend } from './ops/ump.js' +import { Operation, applyPatch } from 'rfc6902' +import { z } from 'zod' import { getChainId, getConsensus } from '../../services/config.js' import { dmpDownwardMessageQueuesKey, @@ -52,6 +60,12 @@ import { const SUB_ERROR_RETRY_MS = 5000 +const allowedPaths = ['/senders', '/destinations', '/channels', '/events'] + +function hasOp(patch: Operation[], path: string) { + return patch.some((op) => op.path.startsWith(path)) +} + export class XCMAgent implements Agent { readonly #subs: Record = {} readonly #log: Logger @@ -79,6 +93,53 @@ export class XCMAgent implements Agent { } } + async getAllSubscriptions(): Promise { + return await this.#db.getAll() + } + + async getSubscriptionById(subscriptionId: string): Promise { + return await this.#db.getById(subscriptionId) + } + + async update(subscriptionId: string, patch: Operation[]): Promise { + const sub = this.#subs[subscriptionId] + const descriptor = sub.descriptor + + // Check allowed patch ops + const allowedOps = patch.every((op) => allowedPaths.some((s) => op.path.startsWith(s))) + + if (allowedOps) { + applyPatch(descriptor, patch) + $Subscription.parse(descriptor) + const args = $XCMSubscriptionArgs.parse(descriptor.args) + + await this.#db.save(descriptor) + + sub.args = args + sub.descriptor = descriptor + + if (hasOp(patch, '/senders')) { + this.#updateSenders(subscriptionId) + } + + if (hasOp(patch, '/destinations')) { + this.#updateDestinations(subscriptionId) + } + + if (hasOp(patch, '/events')) { + this.#updateEvents(subscriptionId) + } + + return descriptor + } else { + throw Error('Only operations on these paths are allowed: ' + allowedPaths.join(',')) + } + } + + getInputSchema(): z.ZodSchema { + return $XCMSubscriptionArgs + } + get id(): AgentId { return 'xcm' } @@ -93,9 +154,10 @@ export class XCMAgent implements Agent { async subscribe(s: Subscription): Promise { const args = $XCMSubscriptionArgs.parse(s.args) - // TODO validate? - // const dests = qs.destinations as NetworkURN[] - // this.#validateChainIds([origin, ...dests]) + + const origin = args.origin as NetworkURN + const dests = args.destinations as NetworkURN[] + this.#validateChainIds([origin, ...dests]) if (!s.ephemeral) { await this.#db.insert(s) @@ -159,9 +221,11 @@ export class XCMAgent implements Agent { await this.#engine.stop() } + async start(): Promise { - this.#startNetworkMonitors() + await this.#startNetworkMonitors() } + #onXcmWaypointReached(msg: XcmNotifyMessage) { const { subscriptionId } = msg if (this.#subs[subscriptionId]) { @@ -788,7 +852,7 @@ export class XCMAgent implements Agent { * * Applies to the outbound extrinsic signers. */ - updateSenders(id: string) { + #updateSenders(id: string) { const { args: { senders }, sendersControl, @@ -802,7 +866,7 @@ export class XCMAgent implements Agent { * * Updates the destination subscriptions. */ - updateDestinations(id: string) { + #updateDestinations(id: string) { const { args, messageControl } = this.#subs[id] messageControl.change(messageCriteria(args.destinations as NetworkURN[])) @@ -814,7 +878,7 @@ export class XCMAgent implements Agent { /** * Updates the subscription to relayed HRMP messages in the relay chain. */ - updateEvents(id: string) { + #updateEvents(id: string) { const { descriptor, args, relaySub } = this.#subs[id] if (this.#shouldMonitorRelay(args) && relaySub === undefined) { @@ -830,17 +894,6 @@ export class XCMAgent implements Agent { } } - /** - * Updates a subscription descriptor. - */ - updateSubscription(sub: Subscription) { - if (this.#subs[sub.id]) { - this.#subs[sub.id].descriptor = sub - } else { - this.#log.warn('trying to update an unknown subscription %s', sub.id) - } - } - #validateChainIds(chainIds: NetworkURN[]) { chainIds.forEach((chainId) => { if (!this.#ingress.isNetworkDefined(chainId)) { diff --git a/packages/server/src/services/monitoring/api/routes.ts b/packages/server/src/services/monitoring/api/routes.ts index 1ec65d6d..c07740e9 100644 --- a/packages/server/src/services/monitoring/api/routes.ts +++ b/packages/server/src/services/monitoring/api/routes.ts @@ -1,22 +1,15 @@ import { FastifyInstance } from 'fastify' -import { Operation, applyPatch } from 'rfc6902' +import { Operation } from 'rfc6902' import { zodToJsonSchema } from 'zod-to-json-schema' import { $AgentId, $SafeId, $Subscription, AgentId, Subscription } from '../types.js' import $JSONPatch from './json-patch.js' -/* TODO extract this -const allowedPaths = ['/senders', '/destinations', '/channels', '/events'] - -function hasOp(patch: Operation[], path: string) { - return patch.some((op) => op.path.startsWith(path)) -}*/ - /** * Subscriptions HTTP API. */ export async function SubscriptionApi(api: FastifyInstance) { - const { switchboard, subsStore } = api + const { switchboard } = api /** * GET subs @@ -34,16 +27,17 @@ export async function SubscriptionApi(api: FastifyInstance) { }, }, async (_, reply) => { - reply.send(await subsStore.getAll()) + reply.send(await switchboard.getAllSubscriptions()) } ) /** - * GET subs/:id + * GET subs/:agent/:id */ api.get<{ Params: { id: string + agent: AgentId } }>( '/subs/:id', @@ -51,6 +45,7 @@ export async function SubscriptionApi(api: FastifyInstance) { schema: { params: { id: zodToJsonSchema($SafeId), + agent: zodToJsonSchema($AgentId), }, response: { 200: zodToJsonSchema($Subscription), @@ -59,7 +54,8 @@ export async function SubscriptionApi(api: FastifyInstance) { }, }, async (request, reply) => { - reply.send(await subsStore.getById(request.params.id)) + const { agent, id } = request.params + reply.send(await switchboard.getSubscriptionById(agent, id)) } ) @@ -118,14 +114,16 @@ export async function SubscriptionApi(api: FastifyInstance) { api.patch<{ Params: { id: string + agent: AgentId } Body: Operation[] }>( - '/subs/:id', + '/subs/:agent/:id', { schema: { params: { id: zodToJsonSchema($SafeId), + agent: zodToJsonSchema($AgentId), }, body: $JSONPatch, response: { @@ -135,40 +133,16 @@ export async function SubscriptionApi(api: FastifyInstance) { }, }, }, - async (_request, reply) => { - /* + async (request, reply) => { const patch = request.body - const { id } = request.params - const sub = await subsStore.getById(id) - - // Check allowed patch ops - const allowedOps = patch.every((op) => allowedPaths.some((s) => op.path.startsWith(s))) - - if (allowedOps) { - applyPatch(sub, patch) - $Subscription.parse(sub) - - await subsStore.save(sub) - - switchboard.updateSubscription(sub) - - if (hasOp(patch, '/senders')) { - switchboard.updateSenders(id) - } - - if (hasOp(patch, '/destinations')) { - switchboard.updateDestinations(id) - } - - if (hasOp(patch, '/events')) { - switchboard.updateEvents(id) - } + const { agent, id } = request.params - reply.status(200).send(sub) - } else { - reply.status(400).send('Only operations on these paths are allowed: ' + allowedPaths.join(',')) - }*/ - reply.status(400) + try { + const res = await switchboard.updateSubscription(agent, id, patch) + reply.status(200).send(res) + } catch (error) { + reply.status(400).send(error) + } } ) diff --git a/packages/server/src/services/monitoring/switchboard.ts b/packages/server/src/services/monitoring/switchboard.ts index 1e3c3503..b7857e8c 100644 --- a/packages/server/src/services/monitoring/switchboard.ts +++ b/packages/server/src/services/monitoring/switchboard.ts @@ -4,6 +4,7 @@ import { Logger, Services } from '../types.js' import { AgentId, Subscription, SubscriptionStats, XcmEventListener } from './types.js' import { AgentService } from 'agents/types.js' +import { Operation } from 'rfc6902' import { NotifierHub } from '../notification/hub.js' import { NotifierEvents } from '../notification/types.js' import { TelemetryCollect, TelemetryEventEmitter } from '../telemetry/types.js' @@ -142,6 +143,29 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte return this.#agentService.getAgentById(agentId).getSubscriptionHandler(id) } + async getAllSubscriptions(): Promise { + const subs: Subscription[][] = [] + for (const agentId of this.#agentService.getAgentIds()) { + subs.push(await this.#agentService.getAgentById(agentId).getAllSubscriptions()) + } + return subs.flat() + } + + async getSubscriptionById(agentId: AgentId, subscriptionId: string): Promise { + return await this.#agentService.getAgentById(agentId).getSubscriptionById(subscriptionId) + } + + /** + * + * @param agentId + * @param id + * @param patch + * @returns + */ + updateSubscription(agentId: AgentId, id: string, patch: Operation[]) { + return this.#agentService.getAgentById(agentId).update(id, patch) + } + /** * Calls the given collect function for each private observable component. * From bd6608f16926d0eec6df244b13fcda9b441b0434 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 11:14:17 +0200 Subject: [PATCH 06/58] extract notifier --- packages/server/src/agents/local.ts | 27 ++++- packages/server/src/agents/types.ts | 20 +++- packages/server/src/agents/xcm/matching.ts | 10 +- packages/server/src/agents/xcm/types.ts | 3 +- packages/server/src/agents/xcm/xcm-agent.ts | 32 +++-- .../services/monitoring/api/ws/protocol.ts | 22 ++-- .../src/services/monitoring/switchboard.ts | 58 +++++---- .../server/src/services/monitoring/types.ts | 4 +- .../server/src/services/notification/hub.ts | 7 +- .../server/src/services/notification/log.ts | 12 +- .../server/src/services/notification/types.ts | 20 +++- .../src/services/notification/webhook.ts | 24 ++-- .../src/services/telemetry/metrics/engine.ts | 111 ------------------ .../src/services/telemetry/metrics/index.ts | 4 - .../services/telemetry/metrics/notifiers.ts | 12 +- .../server/src/services/telemetry/types.ts | 24 +--- 16 files changed, 159 insertions(+), 231 deletions(-) delete mode 100644 packages/server/src/services/telemetry/metrics/engine.ts diff --git a/packages/server/src/agents/local.ts b/packages/server/src/agents/local.ts index a615e41b..a13bab36 100644 --- a/packages/server/src/agents/local.ts +++ b/packages/server/src/agents/local.ts @@ -1,7 +1,9 @@ import { Logger, Services } from '../services/index.js' -import { AgentId } from '../services/monitoring/types.js' +import { AgentId, NotificationListener } from '../services/monitoring/types.js' +import { NotifierHub } from '../services/notification/index.js' +import { NotifierEvents } from '../services/notification/types.js' import { AgentServiceOptions } from '../types.js' -import { Agent, AgentService } from './types.js' +import { Agent, AgentRuntimeContext, AgentService } from './types.js' import { XCMAgent } from './xcm/xcm-agent.js' /** @@ -10,10 +12,27 @@ import { XCMAgent } from './xcm/xcm-agent.js' export class LocalAgentService implements AgentService { readonly #log: Logger readonly #agents: Record + readonly #notifier: NotifierHub constructor(ctx: Services, _options: AgentServiceOptions) { this.#log = ctx.log - this.#agents = this.#loadAgents(ctx) + + // XXX: this is a local in the process memory + // notifier hub + this.#notifier = new NotifierHub(ctx) + + this.#agents = this.#loadAgents({ + ...ctx, + notifier: this.#notifier, + }) + } + + addNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub { + return this.#notifier.on(eventName, listener) + } + + removeNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub { + return this.#notifier.off(eventName, listener) } getAgentIds(): AgentId[] { @@ -46,7 +65,7 @@ export class LocalAgentService implements AgentService { } } - #loadAgents(ctx: Services) { + #loadAgents(ctx: AgentRuntimeContext) { const xcm = new XCMAgent(ctx) return { [xcm.id]: xcm, diff --git a/packages/server/src/agents/types.ts b/packages/server/src/agents/types.ts index 1b9e27b0..418eb0b5 100644 --- a/packages/server/src/agents/types.ts +++ b/packages/server/src/agents/types.ts @@ -1,9 +1,27 @@ import { z } from 'zod' import { Operation } from 'rfc6902' -import { AgentId, Subscription } from '../services/monitoring/types.js' + +import { IngressConsumer } from '../services/ingress/index.js' +import { AgentId, NotificationListener, Subscription } from '../services/monitoring/types.js' +import { NotifierHub } from '../services/notification/hub.js' +import { NotifierEvents } from '../services/notification/types.js' +import { Janitor } from '../services/persistence/janitor.js' +import { SubsStore } from '../services/persistence/subs.js' +import { DB, Logger } from '../services/types.js' + +export type AgentRuntimeContext = { + log: Logger + notifier: NotifierHub + ingressConsumer: IngressConsumer + rootStore: DB + subsStore: SubsStore + janitor: Janitor +} export interface AgentService { + addNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub + removeNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub getAgentById(agentId: AgentId): Agent getAgentInputSchema(agentId: AgentId): z.ZodSchema getAgentIds(): AgentId[] diff --git a/packages/server/src/agents/xcm/matching.ts b/packages/server/src/agents/xcm/matching.ts index 098c7a50..adde1c43 100644 --- a/packages/server/src/agents/xcm/matching.ts +++ b/packages/server/src/agents/xcm/matching.ts @@ -3,7 +3,7 @@ import EventEmitter from 'node:events' import { AbstractSublevel } from 'abstract-level' import { Mutex } from 'async-mutex' -import { DB, Logger, Services, jsonEncoded, prefixes } from '../../services/types.js' +import { DB, Logger, jsonEncoded, prefixes } from '../../services/types.js' import { GenericXcmBridge, GenericXcmHop, @@ -27,7 +27,8 @@ import { import { getRelayId, isOnSameConsensus } from '../../services/config.js' import { Janitor, JanitorTask } from '../../services/persistence/janitor.js' -import { TelemetryEventEmitter } from '../../services/telemetry/types.js' +import { AgentRuntimeContext } from '../types.js' +import { TelemetryXCMEventEmitter } from './telemetry/events.js' export type XcmMatchedReceiver = (message: XcmNotifyMessage) => Promise | void type SubLevel = AbstractSublevel @@ -51,7 +52,7 @@ const DEFAULT_TIMEOUT = 2 * 60000 * - simplify logic to match only by message ID * - check notification storage by message ID and do not store for matching if already matched */ -export class MatchingEngine extends (EventEmitter as new () => TelemetryEventEmitter) { +export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEventEmitter) { readonly #log: Logger readonly #janitor: Janitor @@ -65,7 +66,7 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryEventEmi readonly #mutex: Mutex readonly #xcmMatchedReceiver: XcmMatchedReceiver - constructor({ log, rootStore, janitor }: Services, xcmMatchedReceiver: XcmMatchedReceiver) { + constructor({ log, rootStore, janitor }: AgentRuntimeContext, xcmMatchedReceiver: XcmMatchedReceiver) { super() this.#log = log @@ -535,7 +536,6 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryEventEmi } // TODO: refactor to lower complexity - // eslint-disable-next-line complexity async #storekeysOnOutbound(msg: XcmSent, outboundTTL: number) { const log = this.#log const sublegs = msg.legs.filter((l) => isOnSameConsensus(msg.waypoint.chainId, l.from)) diff --git a/packages/server/src/agents/xcm/types.ts b/packages/server/src/agents/xcm/types.ts index f4d8dc86..42b5519f 100644 --- a/packages/server/src/agents/xcm/types.ts +++ b/packages/server/src/agents/xcm/types.ts @@ -398,6 +398,7 @@ export enum XcmNotificationType { Hop = 'xcm.hop', Bridge = 'xcm.bridge', } + /** * The terminal point of an XCM journey. * @@ -732,12 +733,12 @@ export class GenericXcmBridge implements XcmBridge { this.forwardId = forwardId } } + /** * The XCM event types. * * @public */ - export type XcmNotifyMessage = XcmSent | XcmReceived | XcmRelayed | XcmHop | XcmBridge export function isXcmSent(object: any): object is XcmSent { diff --git a/packages/server/src/agents/xcm/xcm-agent.ts b/packages/server/src/agents/xcm/xcm-agent.ts index 42531fc0..d65e7bc6 100644 --- a/packages/server/src/agents/xcm/xcm-agent.ts +++ b/packages/server/src/agents/xcm/xcm-agent.ts @@ -5,11 +5,12 @@ import { Observable, filter, from, map, share, switchMap } from 'rxjs' import { $Subscription, AgentId, + AnyJson, HexString, RxSubscriptionWithId, Subscription, } from '../../services/monitoring/types.js' -import { Logger, NetworkURN, Services } from '../../services/types.js' +import { Logger, NetworkURN } from '../../services/types.js' import { extractXcmpReceive, extractXcmpSend } from './ops/xcmp.js' import { $XCMSubscriptionArgs, @@ -48,7 +49,8 @@ import { parachainSystemHrmpOutboundMessages, parachainSystemUpwardMessages, } from '../../services/monitoring/storage.js' -import { Agent } from '../types.js' +import { NotifierHub } from '../../services/notification/index.js' +import { Agent, AgentRuntimeContext } from '../types.js' import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './ops/pk-bridge.js' import { getBridgeHubNetworkId } from './ops/util.js' import { @@ -73,17 +75,19 @@ export class XCMAgent implements Agent { readonly #timeouts: NodeJS.Timeout[] = [] readonly #db: SubsStore readonly #ingress: IngressConsumer + readonly #notifier: NotifierHub #shared: { blockEvents: Record> blockExtrinsics: Record> } - constructor(ctx: Services) { - const { log, ingressConsumer, subsStore } = ctx + constructor(ctx: AgentRuntimeContext) { + const { log, ingressConsumer, notifier, subsStore } = ctx this.#log = log this.#ingress = ingressConsumer + this.#notifier = notifier this.#db = subsStore this.#engine = new MatchingEngine(ctx, this.#onXcmWaypointReached.bind(this)) @@ -226,16 +230,22 @@ export class XCMAgent implements Agent { await this.#startNetworkMonitors() } - #onXcmWaypointReached(msg: XcmNotifyMessage) { - const { subscriptionId } = msg + #onXcmWaypointReached(payload: XcmNotifyMessage) { + const { subscriptionId } = payload if (this.#subs[subscriptionId]) { - const { args, sendersControl } = this.#subs[subscriptionId] + const { descriptor, args, sendersControl } = this.#subs[subscriptionId] if ( - (args.events === undefined || args.events === '*' || args.events.includes(msg.type)) && - matchSenders(sendersControl, msg.sender) + (args.events === undefined || args.events === '*' || args.events.includes(payload.type)) && + matchSenders(sendersControl, payload.sender) ) { - // XXX - // this.#notifier.notify(descriptor, msg) + this.#notifier.notify(descriptor, { + metadata: { + type: payload.type, + subscriptionId, + agentId: this.id, + }, + payload: payload as unknown as AnyJson, + }) } } else { // this could happen with closed ephemeral subscriptions diff --git a/packages/server/src/services/monitoring/api/ws/protocol.ts b/packages/server/src/services/monitoring/api/ws/protocol.ts index 46e96db7..e5788e57 100644 --- a/packages/server/src/services/monitoring/api/ws/protocol.ts +++ b/packages/server/src/services/monitoring/api/ws/protocol.ts @@ -5,12 +5,12 @@ import { FastifyRequest } from 'fastify' import { ulid } from 'ulidx' import { z } from 'zod' -import { XcmNotifyMessage } from 'agents/xcm/types.js' import { errorMessage } from '../../../../errors.js' +import { NotifyMessage } from '../../../notification/types.js' import { TelemetryEventEmitter, notifyTelemetryFrom } from '../../../telemetry/types.js' import { Logger } from '../../../types.js' import { Switchboard } from '../../switchboard.js' -import { $Subscription, AgentId, Subscription, XcmEventListener } from '../../types.js' +import { $Subscription, AgentId, NotificationListener, Subscription } from '../../types.js' import { WebsocketProtocolOptions } from './plugin.js' const $EphemeralSubscription = z @@ -50,7 +50,7 @@ function safeWrite(socket: WebSocket, content: NonNullable) { export default class WebsocketProtocol extends (EventEmitter as new () => TelemetryEventEmitter) { readonly #log: Logger readonly #switchboard: Switchboard - readonly #broadcaster: XcmEventListener + readonly #broadcaster: NotificationListener readonly #maxClients: number #connections: Map @@ -65,19 +65,19 @@ export default class WebsocketProtocol extends (EventEmitter as new () => Teleme this.#connections = new Map() this.#maxClients = options.wsMaxClients ?? 10_000 this.#clientsNum = 0 - this.#broadcaster = (sub, xcm) => { + this.#broadcaster = (sub, msg) => { const connections = this.#connections.get(sub.id) if (connections) { for (const connection of connections) { const { socket, ip } = connection try { - safeWrite(socket, xcm) + safeWrite(socket, msg) - this.#telemetryNotify(ip, xcm) + this.#telemetryNotify(ip, msg) } catch (error) { this.#log.error(error) - this.#telemetryNotifyError(ip, xcm, errorMessage(error)) + this.#telemetryNotifyError(ip, msg, errorMessage(error)) } } } @@ -203,11 +203,11 @@ export default class WebsocketProtocol extends (EventEmitter as new () => Teleme }) } - #telemetryNotify(ip: string, xcm: XcmNotifyMessage) { - this.emit('telemetryNotify', notifyTelemetryFrom('websocket', ip, xcm)) + #telemetryNotify(ip: string, msg: NotifyMessage) { + this.emit('telemetryNotify', notifyTelemetryFrom('websocket', ip, msg)) } - #telemetryNotifyError(ip: string, xcm: XcmNotifyMessage, error: string) { - this.emit('telemetryNotifyError', notifyTelemetryFrom('websocket', ip, xcm, error)) + #telemetryNotifyError(ip: string, msg: NotifyMessage, error: string) { + this.emit('telemetryNotifyError', notifyTelemetryFrom('websocket', ip, msg, error)) } } diff --git a/packages/server/src/services/monitoring/switchboard.ts b/packages/server/src/services/monitoring/switchboard.ts index b7857e8c..e1942edc 100644 --- a/packages/server/src/services/monitoring/switchboard.ts +++ b/packages/server/src/services/monitoring/switchboard.ts @@ -1,11 +1,11 @@ import EventEmitter from 'node:events' +import { Operation } from 'rfc6902' + import { Logger, Services } from '../types.js' -import { AgentId, Subscription, SubscriptionStats, XcmEventListener } from './types.js' +import { AgentId, NotificationListener, Subscription, SubscriptionStats } from './types.js' -import { AgentService } from 'agents/types.js' -import { Operation } from 'rfc6902' -import { NotifierHub } from '../notification/hub.js' +import { AgentService } from '../../agents/types.js' import { NotifierEvents } from '../notification/types.js' import { TelemetryCollect, TelemetryEventEmitter } from '../telemetry/types.js' @@ -30,17 +30,12 @@ export type SwitchboardOptions = { } /** - * XCM Subscriptions Switchboard. + * Subscriptions Switchboard. * - * Manages subscriptions and notifications for Cross-Consensus Message Format (XCM) formatted messages. - * Enables subscribing to and unsubscribing from XCM messages of interest, handling 'matched' notifications, - * and managing subscription lifecycles. - * Monitors active subscriptions, processes incoming 'matched' notifications, - * and dynamically updates selection criteria of the subscriptions. + * Manages subscriptions and notifications for the platform agents. */ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitter) { readonly #log: Logger - readonly #notifier: NotifierHub readonly #stats: SubscriptionStats readonly #maxEphemeral: number readonly #maxPersistent: number @@ -52,8 +47,6 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte this.#log = ctx.log this.#agentService = ctx.agentService - // TODO here could be local for websockets over event emitter or remote over redis - this.#notifier = new NotifierHub(ctx) this.#stats = { ephemeral: 0, persistent: 0, @@ -91,8 +84,8 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte * @param eventName The notifier event name. * @param listener The listener function. */ - addNotificationListener(eventName: keyof NotifierEvents, listener: XcmEventListener) { - this.#notifier.on(eventName, listener) + addNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener) { + this.#agentService.addNotificationListener(eventName, listener) } /** @@ -101,8 +94,8 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte * @param eventName The notifier event name. * @param listener The listener function. */ - removeNotificationListener(eventName: keyof NotifierEvents, listener: XcmEventListener) { - this.#notifier.off(eventName, listener) + removeNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener) { + this.#agentService.removeNotificationListener(eventName, listener) } /** @@ -129,9 +122,6 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte // empty } - /** - * Stops the switchboard. - */ async stop() { // empty } @@ -139,10 +129,13 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte /** * Gets a subscription handler by id. */ - findSubscriptionHandler(agentId: AgentId, id: string) { - return this.#agentService.getAgentById(agentId).getSubscriptionHandler(id) + findSubscriptionHandler(agentId: AgentId, subscriptionId: string) { + return this.#agentService.getAgentById(agentId).getSubscriptionHandler(subscriptionId) } + /** + * Gets all the subscriptions for all the known agents. + */ async getAllSubscriptions(): Promise { const subs: Subscription[][] = [] for (const agentId of this.#agentService.getAgentIds()) { @@ -151,19 +144,26 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte return subs.flat() } + /** + * Gets a subscription by identifier. + * + * @param agentId The agent identifier. + * @param subscriptionId The subscription identifier. + */ async getSubscriptionById(agentId: AgentId, subscriptionId: string): Promise { return await this.#agentService.getAgentById(agentId).getSubscriptionById(subscriptionId) } /** + * Updates an existing subscription applying the given JSON patch. * - * @param agentId - * @param id - * @param patch - * @returns + * @param agentId The agent identifier. + * @param subscriptionId The subscription identifier + * @param patch The JSON patch operations. + * @returns the patched subscription object. */ - updateSubscription(agentId: AgentId, id: string, patch: Operation[]) { - return this.#agentService.getAgentById(agentId).update(id, patch) + updateSubscription(agentId: AgentId, subscriptionId: string, patch: Operation[]) { + return this.#agentService.getAgentById(agentId).update(subscriptionId, patch) } /** @@ -173,8 +173,6 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte */ collectTelemetry(collect: TelemetryCollect) { collect(this) - // collect(this.#engine) - collect(this.#notifier) } /** diff --git a/packages/server/src/services/monitoring/types.ts b/packages/server/src/services/monitoring/types.ts index c057b048..95558e0a 100644 --- a/packages/server/src/services/monitoring/types.ts +++ b/packages/server/src/services/monitoring/types.ts @@ -2,7 +2,7 @@ import z from 'zod' import { Subscription as RxSubscription } from 'rxjs' -import { XcmNotifyMessage } from '../../agents/xcm/types.js' +import { NotifyMessage } from '../notification/types.js' /** * Represents a generic JSON object. @@ -132,7 +132,7 @@ export type WebhookNotification = z.infer export type Subscription = z.infer -export type XcmEventListener = (sub: Subscription, xcm: XcmNotifyMessage) => void +export type NotificationListener = (sub: Subscription, msg: NotifyMessage) => void export type RxSubscriptionWithId = { chainId: string diff --git a/packages/server/src/services/notification/hub.ts b/packages/server/src/services/notification/hub.ts index 0f946352..1e62d883 100644 --- a/packages/server/src/services/notification/hub.ts +++ b/packages/server/src/services/notification/hub.ts @@ -1,11 +1,10 @@ import EventEmitter from 'node:events' -import { XcmNotifyMessage } from 'agents/xcm/types.js' import { Subscription } from '../monitoring/types.js' import { TelemetryNotifierEventKeys } from '../telemetry/types.js' import { Services } from '../types.js' import { LogNotifier } from './log.js' -import { Notifier, NotifierEmitter } from './types.js' +import { Notifier, NotifierEmitter, NotifyMessage } from './types.js' import { WebhookNotifier } from './webhook.js' /** @@ -39,12 +38,12 @@ export class NotifierHub extends (EventEmitter as new () => NotifierEmitter) imp } /** - * Notifies an XCM match in the context of a subscription. + * Notifies a message in the context of a subscription. * * @param sub The subscription. * @param msg The message. */ - notify(sub: Subscription, msg: XcmNotifyMessage) { + notify(sub: Subscription, msg: NotifyMessage) { const types: any[] = [] for (const { type } of sub.channels) { if (types.indexOf(type) === -1) { diff --git a/packages/server/src/services/notification/log.ts b/packages/server/src/services/notification/log.ts index bb3ae1b8..c9a6549b 100644 --- a/packages/server/src/services/notification/log.ts +++ b/packages/server/src/services/notification/log.ts @@ -12,7 +12,7 @@ import { import { Logger, Services } from '../../services/types.js' import { Subscription } from '../monitoring/types.js' import { NotifierHub } from './hub.js' -import { Notifier, NotifierEmitter } from './types.js' +import { Notifier, NotifierEmitter, NotifyMessage } from './types.js' export class LogNotifier extends (EventEmitter as new () => NotifierEmitter) implements Notifier { #log: Logger @@ -25,7 +25,15 @@ export class LogNotifier extends (EventEmitter as new () => NotifierEmitter) imp hub.on('log', this.notify.bind(this)) } - notify(sub: Subscription, msg: XcmNotifyMessage) { + notify(sub: Subscription, msg: NotifyMessage) { + this.#log.info( + 'NOTIFICATION %s agent=%s subscription=%s, payload=%j', + msg.metadata.type, + msg.metadata.agentId, + msg.metadata.subscriptionId, + msg.payload + ) + if (isXcmReceived(msg)) { this.#log.info( '[%s ➜ %s] NOTIFICATION %s subscription=%s, messageHash=%s, outcome=%s (o: #%s, d: #%s)', diff --git a/packages/server/src/services/notification/types.ts b/packages/server/src/services/notification/types.ts index 1d043953..02b768d6 100644 --- a/packages/server/src/services/notification/types.ts +++ b/packages/server/src/services/notification/types.ts @@ -1,16 +1,24 @@ -import { XcmNotifyMessage } from 'agents/xcm/types.js' import { TypedEventEmitter } from '../index.js' -import { Subscription } from '../monitoring/types.js' +import { AnyJson, Subscription } from '../monitoring/types.js' import { TelemetryNotifierEvents } from '../telemetry/types.js' +export type NotifyMessage = { + metadata: { + type: string + agentId: string + subscriptionId: string + } + payload: AnyJson +} + export type NotifierEvents = { - log: (sub: Subscription, msg: XcmNotifyMessage) => void - webhook: (sub: Subscription, msg: XcmNotifyMessage) => void - websocket: (sub: Subscription, msg: XcmNotifyMessage) => void + log: (sub: Subscription, msg: NotifyMessage) => void + webhook: (sub: Subscription, msg: NotifyMessage) => void + websocket: (sub: Subscription, msg: NotifyMessage) => void } export type NotifierEmitter = TypedEventEmitter export interface Notifier extends NotifierEmitter { - notify(sub: Subscription, msg: XcmNotifyMessage): void | Promise + notify(sub: Subscription, msg: NotifyMessage): void | Promise } diff --git a/packages/server/src/services/notification/webhook.ts b/packages/server/src/services/notification/webhook.ts index fc6f7d21..ba754a87 100644 --- a/packages/server/src/services/notification/webhook.ts +++ b/packages/server/src/services/notification/webhook.ts @@ -3,7 +3,6 @@ import { EventEmitter } from 'node:events' import got from 'got' import { ulid } from 'ulidx' -import { XcmNotifyMessage } from 'agents/xcm/types.js' import version from '../../version.js' import { Subscription, WebhookNotification } from '../monitoring/types.js' import { Logger, Services } from '../types.js' @@ -12,14 +11,14 @@ import { Scheduled, Scheduler, SubsStore } from '../persistence/index.js' import { notifyTelemetryFrom } from '../telemetry/types.js' import { NotifierHub } from './hub.js' import { TemplateRenderer } from './template.js' -import { Notifier, NotifierEmitter } from './types.js' +import { Notifier, NotifierEmitter, NotifyMessage } from './types.js' const DEFAULT_DELAY = 300000 // 5 minutes type WebhookTask = { id: string subId: string - msg: XcmNotifyMessage + msg: NotifyMessage } const WebhookTaskType = 'task:webhook' @@ -53,7 +52,7 @@ export class WebhookNotifier extends (EventEmitter as new () => NotifierEmitter) hub.on('webhook', this.notify.bind(this)) } - async notify(sub: Subscription, msg: XcmNotifyMessage) { + async notify(sub: Subscription, msg: NotifyMessage) { const { id, channels } = sub for (const chan of channels) { @@ -99,10 +98,10 @@ export class WebhookNotifier extends (EventEmitter as new () => NotifierEmitter) const postUrl = buildPostUrl(url, id) try { - const res = await got.post(postUrl, { + const res = await got.post(postUrl, { body: template === undefined ? JSON.stringify(msg) : this.#renderer.render({ template, data: msg }), headers: { - 'user-agent': 'xcmon/' + version, + 'user-agent': 'ocelloids/' + version, 'content-type': contentType ?? 'application/json', }, retry: { @@ -134,11 +133,12 @@ export class WebhookNotifier extends (EventEmitter as new () => NotifierEmitter) if (res.statusCode >= 200 && res.statusCode < 300) { this.#log.info( - 'NOTIFICATION %s subscription=%s, endpoint=%s, messageHash=%s', - msg.type, - msg.subscriptionId, + 'NOTIFICATION %s agent=%s subscription=%s, endpoint=%s, payload=%j', + msg.metadata.type, + msg.metadata.agentId, + msg.metadata.subscriptionId, postUrl, - msg.waypoint.messageHash + msg.payload ) this.#telemetryNotify(config, msg) } else { @@ -162,11 +162,11 @@ export class WebhookNotifier extends (EventEmitter as new () => NotifierEmitter) } } - #telemetryNotify(config: WebhookNotification, msg: XcmNotifyMessage) { + #telemetryNotify(config: WebhookNotification, msg: NotifyMessage) { this.emit('telemetryNotify', notifyTelemetryFrom(config.type, config.url, msg)) } - #telemetryNotifyError(config: WebhookNotification, msg: XcmNotifyMessage) { + #telemetryNotifyError(config: WebhookNotification, msg: NotifyMessage) { this.emit('telemetryNotifyError', notifyTelemetryFrom(config.type, config.url, msg, 'max_retries')) } } diff --git a/packages/server/src/services/telemetry/metrics/engine.ts b/packages/server/src/services/telemetry/metrics/engine.ts deleted file mode 100644 index eafffe8e..00000000 --- a/packages/server/src/services/telemetry/metrics/engine.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Counter } from 'prom-client' - -import { XcmBridge, XcmHop, XcmInbound, XcmRelayed, XcmSent, XcmTimeout } from 'agents/xcm/types.js' -import { TelemetryEventEmitter } from '../types.js' - -export function engineMetrics(source: TelemetryEventEmitter) { - const inCount = new Counter({ - name: 'oc_engine_in_total', - help: 'Matching engine inbound messages.', - labelNames: ['subscription', 'origin', 'outcome'], - }) - const outCount = new Counter({ - name: 'oc_engine_out_total', - help: 'Matching engine outbound messages.', - labelNames: ['subscription', 'origin', 'destination'], - }) - const matchCount = new Counter({ - name: 'oc_engine_matched_total', - help: 'Matching engine matched messages.', - labelNames: ['subscription', 'origin', 'destination', 'outcome'], - }) - const trapCount = new Counter({ - name: 'oc_engine_trapped_total', - help: 'Matching engine matched messages with trapped assets.', - labelNames: ['subscription', 'origin', 'destination', 'outcome'], - }) - const relayCount = new Counter({ - name: 'oc_engine_relayed_total', - help: 'Matching engine relayed messages.', - labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'outcome'], - }) - const timeoutCount = new Counter({ - name: 'oc_engine_timeout_total', - help: 'Matching engine sent timeout messages.', - labelNames: ['subscription', 'origin', 'destination'], - }) - const hopCount = new Counter({ - name: 'oc_engine_hop_total', - help: 'Matching engine hop messages.', - labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'stop', 'outcome', 'direction'], - }) - const bridgeCount = new Counter({ - name: 'oc_engine_bridge_total', - help: 'Matching engine bridge messages.', - labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'stop', 'outcome', 'direction'], - }) - - source.on('telemetryInbound', (message: XcmInbound) => { - inCount.labels(message.subscriptionId, message.chainId, message.outcome.toString()).inc() - }) - - source.on('telemetryOutbound', (message: XcmSent) => { - outCount.labels(message.subscriptionId, message.origin.chainId, message.destination.chainId).inc() - }) - - source.on('telemetryMatched', (inMsg: XcmInbound, outMsg: XcmSent) => { - matchCount - .labels(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.destination.chainId, inMsg.outcome.toString()) - .inc() - }) - - source.on('telemetryRelayed', (relayMsg: XcmRelayed) => { - relayCount - .labels( - relayMsg.subscriptionId, - relayMsg.origin.chainId, - relayMsg.destination.chainId, - relayMsg.waypoint.legIndex.toString(), - relayMsg.waypoint.outcome.toString() - ) - .inc() - }) - - source.on('telemetryTimeout', (msg: XcmTimeout) => { - timeoutCount.labels(msg.subscriptionId, msg.origin.chainId, msg.destination.chainId).inc() - }) - - source.on('telemetryHop', (msg: XcmHop) => { - hopCount - .labels( - msg.subscriptionId, - msg.origin.chainId, - msg.destination.chainId, - msg.waypoint.legIndex.toString(), - msg.waypoint.chainId, - msg.waypoint.outcome.toString(), - msg.direction - ) - .inc() - }) - - source.on('telemetryBridge', (msg: XcmBridge) => { - bridgeCount - .labels( - msg.subscriptionId, - msg.origin.chainId, - msg.destination.chainId, - msg.waypoint.legIndex.toString(), - msg.waypoint.chainId, - msg.waypoint.outcome.toString(), - msg.bridgeMessageType - ) - .inc() - }) - - source.on('telemetryTrapped', (inMsg: XcmInbound, outMsg: XcmSent) => { - trapCount - .labels(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.destination.chainId, inMsg.outcome.toString()) - .inc() - }) -} diff --git a/packages/server/src/services/telemetry/metrics/index.ts b/packages/server/src/services/telemetry/metrics/index.ts index c16d663c..e1a535ed 100644 --- a/packages/server/src/services/telemetry/metrics/index.ts +++ b/packages/server/src/services/telemetry/metrics/index.ts @@ -1,4 +1,3 @@ -import { MatchingEngine } from '../../../agents/xcm/matching.js' import { IngressConsumer } from '../../ingress/index.js' import IngressProducer from '../../ingress/producer/index.js' import { HeadCatcher } from '../../ingress/watcher/head-catcher.js' @@ -6,7 +5,6 @@ import { Switchboard } from '../../monitoring/switchboard.js' import { NotifierHub } from '../../notification/hub.js' import { TelemetryEventEmitter } from '../types.js' import { catcherMetrics } from './catcher.js' -import { engineMetrics } from './engine.js' import { ingressConsumerMetrics, ingressProducerMetrics } from './ingress.js' import { notifierMetrics } from './notifiers.js' import { switchboardMetrics } from './switchboard.js' @@ -18,8 +16,6 @@ function isIngressConsumer(o: TelemetryEventEmitter): o is IngressConsumer { export function collect(observer: TelemetryEventEmitter) { if (observer instanceof Switchboard) { switchboardMetrics(observer) - } else if (observer instanceof MatchingEngine) { - engineMetrics(observer) } else if (observer instanceof HeadCatcher) { catcherMetrics(observer) } else if (observer instanceof NotifierHub) { diff --git a/packages/server/src/services/telemetry/metrics/notifiers.ts b/packages/server/src/services/telemetry/metrics/notifiers.ts index de44fe8e..e5436518 100644 --- a/packages/server/src/services/telemetry/metrics/notifiers.ts +++ b/packages/server/src/services/telemetry/metrics/notifiers.ts @@ -5,22 +5,18 @@ export function notifierMetrics(source: TelemetryEventEmitter) { const notifyCount = getOrCreateCounter({ name: 'oc_notifier_notification_total', help: 'Notifier notifications.', - labelNames: ['type', 'subscription', 'origin', 'destination', 'outcome', 'channel'], + labelNames: ['type', 'subscription', 'agent', 'channel'], }) const notifyErrorCount = getOrCreateCounter({ name: 'oc_notifier_notification_error_total', help: 'Notifier notification errors.', - labelNames: ['type', 'subscription', 'origin', 'destination', 'outcome', 'channel'], + labelNames: ['type', 'subscription', 'agent', 'channel'], }) source.on('telemetryNotify', (message) => { - notifyCount - .labels(message.type, message.subscription, message.origin, message.destination, message.outcome, message.channel) - .inc() + notifyCount.labels(message.type, message.subscription, message.agent, message.channel).inc() }) source.on('telemetryNotifyError', (message) => { - notifyErrorCount - .labels(message.type, message.subscription, message.origin, message.destination, message.outcome, message.channel) - .inc() + notifyErrorCount.labels(message.type, message.subscription, message.agent, message.channel).inc() }) } diff --git a/packages/server/src/services/telemetry/types.ts b/packages/server/src/services/telemetry/types.ts index caaf8ff8..572c7a9e 100644 --- a/packages/server/src/services/telemetry/types.ts +++ b/packages/server/src/services/telemetry/types.ts @@ -1,16 +1,13 @@ import type { Header } from '@polkadot/types/interfaces' -import { XcmBridge, XcmHop, XcmInbound, XcmNotifyMessage, XcmRelayed, XcmSent, XcmTimeout } from 'agents/xcm/types.js' import { Subscription } from '../monitoring/types.js' +import { NotifyMessage } from '../notification/types.js' import { TypedEventEmitter } from '../types.js' export type NotifyTelemetryMessage = { type: string subscription: string - origin: string - destination: string - waypoint: string - outcome: string + agent: string channel: string error?: string } @@ -18,16 +15,13 @@ export type NotifyTelemetryMessage = { export function notifyTelemetryFrom( type: string, channel: string, - msg: XcmNotifyMessage, + msg: NotifyMessage, error?: string ): NotifyTelemetryMessage { return { type, - subscription: msg.subscriptionId, - origin: msg.origin.chainId, - destination: msg.destination.chainId, - waypoint: msg.waypoint.chainId, - outcome: msg.waypoint.outcome, + subscription: msg.metadata.subscriptionId, + agent: msg.metadata.agentId, channel, error, } @@ -54,14 +48,6 @@ export type TelemetryIngressProducerEvents = { } export type TelemetryEvents = { - telemetryInbound: (message: XcmInbound) => void - telemetryOutbound: (message: XcmSent) => void - telemetryRelayed: (relayMsg: XcmRelayed) => void - telemetryMatched: (inMsg: XcmInbound, outMsg: XcmSent) => void - telemetryTimeout: (message: XcmTimeout) => void - telemetryHop: (message: XcmHop) => void - telemetryBridge: (message: XcmBridge) => void - telemetryTrapped: (inMsg: XcmInbound, outMsg: XcmSent) => void telemetryBlockSeen: (msg: { chainId: string; header: Header }) => void telemetryBlockFinalized: (msg: { chainId: string; header: Header }) => void telemetryBlockCacheHit: (msg: { chainId: string }) => void From a41baa09a3b530195b799e98f0fb9955c8506c45 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 11:14:52 +0200 Subject: [PATCH 07/58] add agent metrics --- packages/server/src/agents/api.ts | 1 + .../server/src/agents/xcm/telemetry/events.ts | 15 +++ .../src/agents/xcm/telemetry/metrics.ts | 111 ++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 packages/server/src/agents/api.ts create mode 100644 packages/server/src/agents/xcm/telemetry/events.ts create mode 100644 packages/server/src/agents/xcm/telemetry/metrics.ts diff --git a/packages/server/src/agents/api.ts b/packages/server/src/agents/api.ts new file mode 100644 index 00000000..c03a6752 --- /dev/null +++ b/packages/server/src/agents/api.ts @@ -0,0 +1 @@ +// TBD agent web api diff --git a/packages/server/src/agents/xcm/telemetry/events.ts b/packages/server/src/agents/xcm/telemetry/events.ts new file mode 100644 index 00000000..6644c5be --- /dev/null +++ b/packages/server/src/agents/xcm/telemetry/events.ts @@ -0,0 +1,15 @@ +import { TypedEventEmitter } from '../../../services/types.js' +import { XcmBridge, XcmHop, XcmInbound, XcmRelayed, XcmSent, XcmTimeout } from '../types.js' + +export type TelemetryEvents = { + telemetryInbound: (message: XcmInbound) => void + telemetryOutbound: (message: XcmSent) => void + telemetryRelayed: (relayMsg: XcmRelayed) => void + telemetryMatched: (inMsg: XcmInbound, outMsg: XcmSent) => void + telemetryTimeout: (message: XcmTimeout) => void + telemetryHop: (message: XcmHop) => void + telemetryBridge: (message: XcmBridge) => void + telemetryTrapped: (inMsg: XcmInbound, outMsg: XcmSent) => void +} + +export type TelemetryXCMEventEmitter = TypedEventEmitter diff --git a/packages/server/src/agents/xcm/telemetry/metrics.ts b/packages/server/src/agents/xcm/telemetry/metrics.ts new file mode 100644 index 00000000..2414b381 --- /dev/null +++ b/packages/server/src/agents/xcm/telemetry/metrics.ts @@ -0,0 +1,111 @@ +import { Counter } from 'prom-client' + +import { XcmBridge, XcmHop, XcmInbound, XcmRelayed, XcmSent, XcmTimeout } from '../types.js' +import { TelemetryXCMEventEmitter } from './events.js' + +export function metrics(source: TelemetryXCMEventEmitter) { + const inCount = new Counter({ + name: 'oc_engine_in_total', + help: 'Matching engine inbound messages.', + labelNames: ['subscription', 'origin', 'outcome'], + }) + const outCount = new Counter({ + name: 'oc_engine_out_total', + help: 'Matching engine outbound messages.', + labelNames: ['subscription', 'origin', 'destination'], + }) + const matchCount = new Counter({ + name: 'oc_engine_matched_total', + help: 'Matching engine matched messages.', + labelNames: ['subscription', 'origin', 'destination', 'outcome'], + }) + const trapCount = new Counter({ + name: 'oc_engine_trapped_total', + help: 'Matching engine matched messages with trapped assets.', + labelNames: ['subscription', 'origin', 'destination', 'outcome'], + }) + const relayCount = new Counter({ + name: 'oc_engine_relayed_total', + help: 'Matching engine relayed messages.', + labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'outcome'], + }) + const timeoutCount = new Counter({ + name: 'oc_engine_timeout_total', + help: 'Matching engine sent timeout messages.', + labelNames: ['subscription', 'origin', 'destination'], + }) + const hopCount = new Counter({ + name: 'oc_engine_hop_total', + help: 'Matching engine hop messages.', + labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'stop', 'outcome', 'direction'], + }) + const bridgeCount = new Counter({ + name: 'oc_engine_bridge_total', + help: 'Matching engine bridge messages.', + labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'stop', 'outcome', 'direction'], + }) + + source.on('telemetryInbound', (message: XcmInbound) => { + inCount.labels(message.subscriptionId, message.chainId, message.outcome.toString()).inc() + }) + + source.on('telemetryOutbound', (message: XcmSent) => { + outCount.labels(message.subscriptionId, message.origin.chainId, message.destination.chainId).inc() + }) + + source.on('telemetryMatched', (inMsg: XcmInbound, outMsg: XcmSent) => { + matchCount + .labels(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.destination.chainId, inMsg.outcome.toString()) + .inc() + }) + + source.on('telemetryRelayed', (relayMsg: XcmRelayed) => { + relayCount + .labels( + relayMsg.subscriptionId, + relayMsg.origin.chainId, + relayMsg.destination.chainId, + relayMsg.waypoint.legIndex.toString(), + relayMsg.waypoint.outcome.toString() + ) + .inc() + }) + + source.on('telemetryTimeout', (msg: XcmTimeout) => { + timeoutCount.labels(msg.subscriptionId, msg.origin.chainId, msg.destination.chainId).inc() + }) + + source.on('telemetryHop', (msg: XcmHop) => { + hopCount + .labels( + msg.subscriptionId, + msg.origin.chainId, + msg.destination.chainId, + msg.waypoint.legIndex.toString(), + msg.waypoint.chainId, + msg.waypoint.outcome.toString(), + msg.direction + ) + .inc() + }) + + source.on('telemetryBridge', (msg: XcmBridge) => { + bridgeCount + .labels( + msg.subscriptionId, + msg.origin.chainId, + msg.destination.chainId, + msg.waypoint.legIndex.toString(), + msg.waypoint.chainId, + msg.waypoint.outcome.toString(), + msg.bridgeMessageType + ) + .inc() + }) + + source.on('telemetryTrapped', (inMsg: XcmInbound, outMsg: XcmSent) => { + trapCount + .labels(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.destination.chainId, inMsg.outcome.toString()) + .inc() + }) +} From 2a5acf615de22714fa7e0cd3149261a38d213781 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 11:17:56 +0200 Subject: [PATCH 08/58] add ws metric --- packages/server/src/agents/types.ts | 2 +- packages/server/src/services/telemetry/metrics/ws.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/agents/types.ts b/packages/server/src/agents/types.ts index 418eb0b5..33ab2e54 100644 --- a/packages/server/src/agents/types.ts +++ b/packages/server/src/agents/types.ts @@ -30,10 +30,10 @@ export interface AgentService { } export interface Agent { + get id(): AgentId getSubscriptionById(subscriptionId: string): Promise getAllSubscriptions(): Promise getInputSchema(): z.ZodSchema - get id(): AgentId getSubscriptionHandler(subscriptionId: string): Subscription subscribe(subscription: Subscription): Promise unsubscribe(subscriptionId: string): Promise diff --git a/packages/server/src/services/telemetry/metrics/ws.ts b/packages/server/src/services/telemetry/metrics/ws.ts index 969175ac..81da0cf2 100644 --- a/packages/server/src/services/telemetry/metrics/ws.ts +++ b/packages/server/src/services/telemetry/metrics/ws.ts @@ -9,15 +9,15 @@ export function wsMetrics(source: TelemetryEventEmitter) { const socketListenerCount = new Gauge({ name: 'oc_socket_listener_count', help: 'Socket listeners.', - labelNames: ['type', 'subscription', 'origin', 'destinations', 'channel'], + labelNames: ['type', 'subscription', 'agent', 'ip'], }) source.on('telemetrySocketListener', (ip, sub, close = false) => { - /*const gauge = socketListenerCount.labels('websocket', sub.id, sub.origin, sub.destinations.join(','), ip) + const gauge = socketListenerCount.labels('websocket', sub.id, sub.agent, ip) if (close) { gauge.dec() } else { gauge.inc() - }*/ + } }) } From ce28bdfaa2947366ff416cd9c9e9cb3cead7f3dd Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 11:21:58 +0200 Subject: [PATCH 09/58] add id params to api --- .../src/services/monitoring/api/routes.ts | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/server/src/services/monitoring/api/routes.ts b/packages/server/src/services/monitoring/api/routes.ts index c07740e9..f7f01d02 100644 --- a/packages/server/src/services/monitoring/api/routes.ts +++ b/packages/server/src/services/monitoring/api/routes.ts @@ -32,20 +32,20 @@ export async function SubscriptionApi(api: FastifyInstance) { ) /** - * GET subs/:agent/:id + * GET subs/:agentId/:subscriptionId */ api.get<{ Params: { - id: string - agent: AgentId + subscriptionId: string + agentId: AgentId } }>( - '/subs/:id', + '/subs/:agentId/:subscriptionId', { schema: { params: { - id: zodToJsonSchema($SafeId), - agent: zodToJsonSchema($AgentId), + subscriptionId: zodToJsonSchema($SafeId), + agentId: zodToJsonSchema($AgentId), }, response: { 200: zodToJsonSchema($Subscription), @@ -54,8 +54,8 @@ export async function SubscriptionApi(api: FastifyInstance) { }, }, async (request, reply) => { - const { agent, id } = request.params - reply.send(await switchboard.getSubscriptionById(agent, id)) + const { agentId, subscriptionId } = request.params + reply.send(await switchboard.getSubscriptionById(agentId, subscriptionId)) } ) @@ -109,21 +109,21 @@ export async function SubscriptionApi(api: FastifyInstance) { ) /** - * PATCH subs/:id + * PATCH subs/:agentId/:subscriptionId */ api.patch<{ Params: { - id: string - agent: AgentId + subscriptionId: string + agentId: AgentId } Body: Operation[] }>( - '/subs/:agent/:id', + '/subs/:agentId/:subscriptionId', { schema: { params: { - id: zodToJsonSchema($SafeId), - agent: zodToJsonSchema($AgentId), + subscriptionId: zodToJsonSchema($SafeId), + agentId: zodToJsonSchema($AgentId), }, body: $JSONPatch, response: { @@ -135,10 +135,10 @@ export async function SubscriptionApi(api: FastifyInstance) { }, async (request, reply) => { const patch = request.body - const { agent, id } = request.params + const { agentId, subscriptionId } = request.params try { - const res = await switchboard.updateSubscription(agent, id, patch) + const res = await switchboard.updateSubscription(agentId, subscriptionId, patch) reply.status(200).send(res) } catch (error) { reply.status(400).send(error) @@ -147,20 +147,20 @@ export async function SubscriptionApi(api: FastifyInstance) { ) /** - * DELETE subs/:id + * DELETE subs/:agentId/:subscriptionId */ api.delete<{ Params: { - agent: AgentId - id: string + agentId: AgentId + subscriptionId: string } }>( - '/subs/:agent/:id', + '/subs/:agentId/:subscriptionId', { schema: { params: { - agent: zodToJsonSchema($AgentId), - id: zodToJsonSchema($SafeId), + agentId: zodToJsonSchema($AgentId), + subscriptionId: zodToJsonSchema($SafeId), }, response: { 200: { @@ -171,8 +171,8 @@ export async function SubscriptionApi(api: FastifyInstance) { }, }, async (request, reply) => { - const { agent, id } = request.params - await switchboard.unsubscribe(agent, id) + const { agentId, subscriptionId } = request.params + await switchboard.unsubscribe(agentId, subscriptionId) reply.send() } From 71755e100836ecb96c43bdbe4b8d7c1be7b1edca Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 11:24:04 +0200 Subject: [PATCH 10/58] add id params to ws --- .../src/services/monitoring/api/ws/plugin.ts | 10 +++++----- .../src/services/monitoring/api/ws/protocol.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/server/src/services/monitoring/api/ws/plugin.ts b/packages/server/src/services/monitoring/api/ws/plugin.ts index ab2f625a..0521083d 100644 --- a/packages/server/src/services/monitoring/api/ws/plugin.ts +++ b/packages/server/src/services/monitoring/api/ws/plugin.ts @@ -34,12 +34,12 @@ const websocketProtocolPlugin: FastifyPluginAsync = as fastify.get<{ Params: { - agent: AgentId - id: string + agentId: AgentId + subscriptionId: string } - }>('/ws/subs/:agent/:id', { websocket: true, schema: { hide: true } }, (socket, request): void => { - const { id, agent } = request.params - setImmediate(() => protocol.handle(socket, request, { agent, id })) + }>('/ws/subs/:agentId/:subscriptionId', { websocket: true, schema: { hide: true } }, (socket, request): void => { + const { agentId, subscriptionId } = request.params + setImmediate(() => protocol.handle(socket, request, { agentId, subscriptionId })) }) fastify.get('/ws/subs', { websocket: true, schema: { hide: true } }, (socket, request): void => { diff --git a/packages/server/src/services/monitoring/api/ws/protocol.ts b/packages/server/src/services/monitoring/api/ws/protocol.ts index e5788e57..dd167e4e 100644 --- a/packages/server/src/services/monitoring/api/ws/protocol.ts +++ b/packages/server/src/services/monitoring/api/ws/protocol.ts @@ -93,14 +93,14 @@ export default class WebsocketProtocol extends (EventEmitter as new () => Teleme * * @param socket The websocket * @param request The Fastify request - * @param subscriptionId The subscription identifier + * @param ids The subscription and agent identifiers */ async handle( socket: WebSocket, request: FastifyRequest, - subscriptionId?: { - id: string - agent: AgentId + ids?: { + subscriptionId: string + agentId: AgentId } ) { if (this.#clientsNum >= this.#maxClients) { @@ -109,7 +109,7 @@ export default class WebsocketProtocol extends (EventEmitter as new () => Teleme } try { - if (subscriptionId === undefined) { + if (ids === undefined) { let resolvedId: { id: string; agent: AgentId } // on-demand ephemeral subscriptions @@ -138,8 +138,8 @@ export default class WebsocketProtocol extends (EventEmitter as new () => Teleme }) } else { // existing subscriptions - const { agent, id } = subscriptionId - const subscription = this.#switchboard.findSubscriptionHandler(agent, id) + const { agentId, subscriptionId } = ids + const subscription = this.#switchboard.findSubscriptionHandler(agentId, subscriptionId) this.#addSubscriber(subscription, socket, request) } } catch (error) { From 38f9691ec31a6ddc2faeb1fcce57108fec2411cc Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 11:49:37 +0200 Subject: [PATCH 11/58] refactor subs store --- packages/server/src/agents/local.ts | 18 ++++++- packages/server/src/agents/xcm/xcm-agent.ts | 6 +-- packages/server/src/server.ts | 4 +- .../server/src/services/monitoring/plugin.ts | 11 ++-- .../server/src/services/monitoring/types.ts | 4 +- .../src/services/notification/webhook.ts | 8 +-- .../server/src/services/persistence/plugin.ts | 4 ++ .../server/src/services/persistence/subs.ts | 52 +++++-------------- 8 files changed, 51 insertions(+), 56 deletions(-) diff --git a/packages/server/src/agents/local.ts b/packages/server/src/agents/local.ts index a13bab36..51dbca2f 100644 --- a/packages/server/src/agents/local.ts +++ b/packages/server/src/agents/local.ts @@ -1,5 +1,5 @@ import { Logger, Services } from '../services/index.js' -import { AgentId, NotificationListener } from '../services/monitoring/types.js' +import { AgentId, NotificationListener, Subscription } from '../services/monitoring/types.js' import { NotifierHub } from '../services/notification/index.js' import { NotifierEvents } from '../services/notification/types.js' import { AgentServiceOptions } from '../types.js' @@ -27,6 +27,22 @@ export class LocalAgentService implements AgentService { }) } + /** + * Retrieves the registered subscriptions in the database + * for all the configured networks. + * + * @returns {Subscription[]} an array with the subscriptions + */ + async getAllSubscriptions() { + let subscriptions: Subscription[] = [] + for (const chainId of this.getAgentIds()) { + const agent = await this.getAgentById(chainId) + subscriptions = subscriptions.concat(await agent.getAllSubscriptions()) + } + + return subscriptions + } + addNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub { return this.#notifier.on(eventName, listener) } diff --git a/packages/server/src/agents/xcm/xcm-agent.ts b/packages/server/src/agents/xcm/xcm-agent.ts index d65e7bc6..71ecbec6 100644 --- a/packages/server/src/agents/xcm/xcm-agent.ts +++ b/packages/server/src/agents/xcm/xcm-agent.ts @@ -98,11 +98,11 @@ export class XCMAgent implements Agent { } async getAllSubscriptions(): Promise { - return await this.#db.getAll() + return await this.#db.getByAgentId(this.id) } async getSubscriptionById(subscriptionId: string): Promise { - return await this.#db.getById(subscriptionId) + return await this.#db.getById(this.id, subscriptionId) } async update(subscriptionId: string, patch: Operation[]): Promise { @@ -197,7 +197,7 @@ export class XCMAgent implements Agent { await this.#engine.clearPendingStates(id) if (!ephemeral) { - await this.#db.remove(id) + await this.#db.remove(this.id, id) } } catch (error) { this.#log.error(error, 'Error unsubscribing %s', id) diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 414372ad..f4cd6292 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -20,9 +20,9 @@ import { Configuration, Connector, Ingress, - Monitoring, Persistence, Root, + Monitoring as Subscriptions, Telemetry, } from './services/index.js' import version from './version.js' @@ -162,7 +162,7 @@ export async function createServer(opts: ServerOptions) { await server.register(Persistence, opts) await server.register(Ingress, opts) await server.register(AgentService, opts) - await server.register(Monitoring, opts) + await server.register(Subscriptions, opts) await server.register(Administration) await server.register(Telemetry, opts) diff --git a/packages/server/src/services/monitoring/plugin.ts b/packages/server/src/services/monitoring/plugin.ts index 752ba0c6..e4faac66 100644 --- a/packages/server/src/services/monitoring/plugin.ts +++ b/packages/server/src/services/monitoring/plugin.ts @@ -1,7 +1,6 @@ import { FastifyPluginAsync } from 'fastify' import fp from 'fastify-plugin' -import { SubsStore } from '../persistence/index.js' import { SubscriptionApi } from './api/index.js' import WebsocketProtocolPlugin, { WebsocketProtocolOptions } from './api/ws/plugin.js' import { Switchboard, SwitchboardOptions } from './switchboard.js' @@ -9,11 +8,10 @@ import { Switchboard, SwitchboardOptions } from './switchboard.js' declare module 'fastify' { interface FastifyInstance { switchboard: Switchboard - subsStore: SubsStore } } -type MonitoringOptions = SwitchboardOptions & WebsocketProtocolOptions +type SubscriptionsOptions = SwitchboardOptions & WebsocketProtocolOptions /** * Monitoring service Fastify plugin. @@ -22,12 +20,9 @@ type MonitoringOptions = SwitchboardOptions & WebsocketProtocolOptions * * @param {FastifyInstance} fastify The Fastify instance. */ -const monitoringPlugin: FastifyPluginAsync = async (fastify, options) => { +const subscriptionsPlugin: FastifyPluginAsync = async (fastify, options) => { const { log } = fastify - const subsStore = new SubsStore(fastify.log, fastify.rootStore, fastify.agentService) - fastify.decorate('subsStore', subsStore) - const switchboard = new Switchboard(fastify, options) fastify.decorate('switchboard', switchboard) @@ -43,4 +38,4 @@ const monitoringPlugin: FastifyPluginAsync = async (fastify, await fastify.register(WebsocketProtocolPlugin, options) } -export default fp(monitoringPlugin, { fastify: '>=4.x', name: 'monitoring' }) +export default fp(subscriptionsPlugin, { fastify: '>=4.x', name: 'subscriptions' }) diff --git a/packages/server/src/services/monitoring/types.ts b/packages/server/src/services/monitoring/types.ts index 95558e0a..f82ecb3c 100644 --- a/packages/server/src/services/monitoring/types.ts +++ b/packages/server/src/services/monitoring/types.ts @@ -107,7 +107,9 @@ export const $AgentArgs = z.record( z.any() ) -export const $AgentId = $SafeId +export const $AgentId = z.string({ + required_error: 'agent id is required' +}) export type AgentId = z.infer diff --git a/packages/server/src/services/notification/webhook.ts b/packages/server/src/services/notification/webhook.ts index ba754a87..dfc2205d 100644 --- a/packages/server/src/services/notification/webhook.ts +++ b/packages/server/src/services/notification/webhook.ts @@ -18,6 +18,7 @@ const DEFAULT_DELAY = 300000 // 5 minutes type WebhookTask = { id: string subId: string + agentId: string msg: NotifyMessage } const WebhookTaskType = 'task:webhook' @@ -53,7 +54,7 @@ export class WebhookNotifier extends (EventEmitter as new () => NotifierEmitter) } async notify(sub: Subscription, msg: NotifyMessage) { - const { id, channels } = sub + const { id, agent, channels } = sub for (const chan of channels) { if (chan.type === 'webhook') { @@ -63,6 +64,7 @@ export class WebhookNotifier extends (EventEmitter as new () => NotifierEmitter) task: { id: taskId, subId: id, + agentId: agent, msg, }, } @@ -73,11 +75,11 @@ export class WebhookNotifier extends (EventEmitter as new () => NotifierEmitter) async #dispatch(scheduled: Scheduled) { const { - task: { subId }, + task: { subId, agentId }, } = scheduled try { - const { channels } = await this.#subs.getById(subId) + const { channels } = await this.#subs.getById(agentId, subId) for (const chan of channels) { if (chan.type === 'webhook') { const config = chan as WebhookNotification diff --git a/packages/server/src/services/persistence/plugin.ts b/packages/server/src/services/persistence/plugin.ts index a078f20e..5858f126 100644 --- a/packages/server/src/services/persistence/plugin.ts +++ b/packages/server/src/services/persistence/plugin.ts @@ -8,10 +8,12 @@ import { RaveLevel } from 'rave-level' import { DB, LevelEngine } from '../types.js' import { Janitor, JanitorOptions } from './janitor.js' import { Scheduler, SchedulerOptions } from './scheduler.js' +import { SubsStore } from './subs.js' declare module 'fastify' { interface FastifyInstance { rootStore: DB + subsStore: SubsStore scheduler: Scheduler janitor: Janitor } @@ -49,10 +51,12 @@ const persistencePlugin: FastifyPluginAsync = async (fastify, options const root = createLevel(fastify, options) const scheduler = new Scheduler(fastify.log, root, options) const janitor = new Janitor(fastify.log, root, scheduler, options) + const subsStore = new SubsStore(fastify.log, root) fastify.decorate('rootStore', root) fastify.decorate('janitor', janitor) fastify.decorate('scheduler', scheduler) + fastify.decorate('subsStore', subsStore) fastify.addHook('onClose', (instance, done) => { scheduler diff --git a/packages/server/src/services/persistence/subs.ts b/packages/server/src/services/persistence/subs.ts index 6dfcae3c..c239a496 100644 --- a/packages/server/src/services/persistence/subs.ts +++ b/packages/server/src/services/persistence/subs.ts @@ -1,7 +1,6 @@ -import { AgentService } from '../../agents/types.js' import { NotFound, ValidationError } from '../../errors.js' import { AgentId, Subscription } from '../monitoring/types.js' -import { DB, Logger, NetworkURN, jsonEncoded, prefixes } from '../types.js' +import { DB, Logger, jsonEncoded, prefixes } from '../types.js' /** * Subscriptions persistence. @@ -11,43 +10,25 @@ import { DB, Logger, NetworkURN, jsonEncoded, prefixes } from '../types.js' export class SubsStore { // readonly #log: Logger; readonly #db: DB - readonly #agentService: AgentService - constructor(_log: Logger, db: DB, agentService: AgentService) { + constructor(_log: Logger, db: DB) { // this.#log = log; this.#db = db - this.#agentService = agentService } /** * Returns true if a subscription for the given id exists, * false otherwise. */ - async exists(id: string): Promise { + async exists(agentId: AgentId, id: string): Promise { try { - await this.getById(id) + await this.getById(agentId, id) return true } catch { return false } } - /** - * Retrieves the registered subscriptions in the database - * for all the configured networks. - * - * @returns {Subscription[]} an array with the subscriptions - */ - async getAll() { - let subscriptions: Subscription[] = [] - for (const chainId of this.#agentService.getAgentIds()) { - const subs = await this.getByAgentId(chainId) - subscriptions = subscriptions.concat(subs) - } - - return subscriptions - } - /** * Retrieves all the subscriptions for a given agent. * @@ -64,17 +45,13 @@ export class SubsStore { * @returns {Subscription} the subscription information * @throws {NotFound} if the subscription does not exist */ - async getById(id: string) { - for (const agentId of this.#agentService.getAgentIds()) { - try { - const subscription = await this.#subsFamily(agentId).get(id) - return subscription - } catch { - continue - } + async getById(agentId: AgentId, id: string) { + try { + const subscription = await this.#subsFamily(agentId).get(id) + return subscription + } catch { + throw new NotFound(`Subscription ${id} not found.`) } - - throw new NotFound(`Subscription ${id} not found.`) } /** @@ -83,8 +60,8 @@ export class SubsStore { * @throws {ValidationError} if there is a validation error. */ async insert(s: Subscription) { - if (await this.exists(s.id)) { - throw new ValidationError(`Subscription with ID ${s.id} already exists`) + if (await this.exists(s.agent, s.id)) { + throw new ValidationError(`Subscription with ID=${s.agent}:${s.id} already exists`) } await this.save(s) } @@ -102,9 +79,8 @@ export class SubsStore { /** * Removes a subscription for the given id. */ - async remove(id: string) { - const s = await this.getById(id) - await this.#subsFamily(s.agent).del(id) + async remove(agentId: AgentId, id: string) { + await this.#subsFamily(agentId).del(id) } #subsFamily(agentId: AgentId) { From 7a91383cf911306aa9bba650f9a2228a66fb6992 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 11:49:51 +0200 Subject: [PATCH 12/58] refactor subs store --- packages/server/src/services/monitoring/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/services/monitoring/types.ts b/packages/server/src/services/monitoring/types.ts index f82ecb3c..a59196f3 100644 --- a/packages/server/src/services/monitoring/types.ts +++ b/packages/server/src/services/monitoring/types.ts @@ -108,7 +108,7 @@ export const $AgentArgs = z.record( ) export const $AgentId = z.string({ - required_error: 'agent id is required' + required_error: 'agent id is required', }) export type AgentId = z.infer From c929b3f3ff086b58da493f7aee7bc35dbdda2f51 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 11:54:08 +0200 Subject: [PATCH 13/58] rename monitoring to subscriptions --- packages/server/src/agents/local.ts | 2 +- packages/server/src/agents/types.ts | 2 +- packages/server/src/agents/xcm/types.ts | 2 +- packages/server/src/agents/xcm/xcm-agent.ts | 4 ++-- packages/server/src/services/notification/hub.ts | 2 +- packages/server/src/services/notification/log.ts | 2 +- packages/server/src/services/notification/webhook.ts | 2 +- packages/server/src/services/persistence/subs.ts | 2 +- .../src/services/{monitoring => subscriptions}/api/index.ts | 0 .../services/{monitoring => subscriptions}/api/json-patch.ts | 0 .../src/services/{monitoring => subscriptions}/api/routes.ts | 0 .../services/{monitoring => subscriptions}/api/ws/plugin.ts | 2 +- .../{monitoring => subscriptions}/api/ws/protocol.spec.ts | 0 .../services/{monitoring => subscriptions}/api/ws/protocol.ts | 0 .../src/services/{monitoring => subscriptions}/plugin.ts | 0 .../src/services/{monitoring => subscriptions}/storage.ts | 0 .../{monitoring => subscriptions}/switchboard.spec.ts | 0 .../src/services/{monitoring => subscriptions}/switchboard.ts | 0 .../src/services/{monitoring => subscriptions}/types.ts | 0 packages/server/src/services/telemetry/metrics/index.ts | 2 +- packages/server/src/services/telemetry/types.ts | 2 +- 21 files changed, 12 insertions(+), 12 deletions(-) rename packages/server/src/services/{monitoring => subscriptions}/api/index.ts (100%) rename packages/server/src/services/{monitoring => subscriptions}/api/json-patch.ts (100%) rename packages/server/src/services/{monitoring => subscriptions}/api/routes.ts (100%) rename packages/server/src/services/{monitoring => subscriptions}/api/ws/plugin.ts (96%) rename packages/server/src/services/{monitoring => subscriptions}/api/ws/protocol.spec.ts (100%) rename packages/server/src/services/{monitoring => subscriptions}/api/ws/protocol.ts (100%) rename packages/server/src/services/{monitoring => subscriptions}/plugin.ts (100%) rename packages/server/src/services/{monitoring => subscriptions}/storage.ts (100%) rename packages/server/src/services/{monitoring => subscriptions}/switchboard.spec.ts (100%) rename packages/server/src/services/{monitoring => subscriptions}/switchboard.ts (100%) rename packages/server/src/services/{monitoring => subscriptions}/types.ts (100%) diff --git a/packages/server/src/agents/local.ts b/packages/server/src/agents/local.ts index 51dbca2f..ad7c4dbc 100644 --- a/packages/server/src/agents/local.ts +++ b/packages/server/src/agents/local.ts @@ -1,5 +1,5 @@ import { Logger, Services } from '../services/index.js' -import { AgentId, NotificationListener, Subscription } from '../services/monitoring/types.js' +import { AgentId, NotificationListener, Subscription } from '../services/subscriptions/types.js' import { NotifierHub } from '../services/notification/index.js' import { NotifierEvents } from '../services/notification/types.js' import { AgentServiceOptions } from '../types.js' diff --git a/packages/server/src/agents/types.ts b/packages/server/src/agents/types.ts index 33ab2e54..de7bc29a 100644 --- a/packages/server/src/agents/types.ts +++ b/packages/server/src/agents/types.ts @@ -3,7 +3,7 @@ import { z } from 'zod' import { Operation } from 'rfc6902' import { IngressConsumer } from '../services/ingress/index.js' -import { AgentId, NotificationListener, Subscription } from '../services/monitoring/types.js' +import { AgentId, NotificationListener, Subscription } from '../services/subscriptions/types.js' import { NotifierHub } from '../services/notification/hub.js' import { NotifierEvents } from '../services/notification/types.js' import { Janitor } from '../services/persistence/janitor.js' diff --git a/packages/server/src/agents/xcm/types.ts b/packages/server/src/agents/xcm/types.ts index 42b5519f..de1c17ab 100644 --- a/packages/server/src/agents/xcm/types.ts +++ b/packages/server/src/agents/xcm/types.ts @@ -14,7 +14,7 @@ import { SignerData, Subscription, toHexString, -} from '../../services/monitoring/types.js' +} from '../../services/subscriptions/types.js' import { NetworkURN } from '../../services/types.js' export type Monitor = { diff --git a/packages/server/src/agents/xcm/xcm-agent.ts b/packages/server/src/agents/xcm/xcm-agent.ts index 71ecbec6..b444b982 100644 --- a/packages/server/src/agents/xcm/xcm-agent.ts +++ b/packages/server/src/agents/xcm/xcm-agent.ts @@ -9,7 +9,7 @@ import { HexString, RxSubscriptionWithId, Subscription, -} from '../../services/monitoring/types.js' +} from '../../services/subscriptions/types.js' import { Logger, NetworkURN } from '../../services/types.js' import { extractXcmpReceive, extractXcmpSend } from './ops/xcmp.js' import { @@ -48,7 +48,7 @@ import { dmpDownwardMessageQueuesKey, parachainSystemHrmpOutboundMessages, parachainSystemUpwardMessages, -} from '../../services/monitoring/storage.js' +} from '../../services/subscriptions/storage.js' import { NotifierHub } from '../../services/notification/index.js' import { Agent, AgentRuntimeContext } from '../types.js' import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './ops/pk-bridge.js' diff --git a/packages/server/src/services/notification/hub.ts b/packages/server/src/services/notification/hub.ts index 1e62d883..63b8e69b 100644 --- a/packages/server/src/services/notification/hub.ts +++ b/packages/server/src/services/notification/hub.ts @@ -1,6 +1,6 @@ import EventEmitter from 'node:events' -import { Subscription } from '../monitoring/types.js' +import { Subscription } from '../subscriptions/types.js' import { TelemetryNotifierEventKeys } from '../telemetry/types.js' import { Services } from '../types.js' import { LogNotifier } from './log.js' diff --git a/packages/server/src/services/notification/log.ts b/packages/server/src/services/notification/log.ts index c9a6549b..014172ea 100644 --- a/packages/server/src/services/notification/log.ts +++ b/packages/server/src/services/notification/log.ts @@ -10,7 +10,7 @@ import { isXcmSent, } from 'agents/xcm/types.js' import { Logger, Services } from '../../services/types.js' -import { Subscription } from '../monitoring/types.js' +import { Subscription } from '../subscriptions/types.js' import { NotifierHub } from './hub.js' import { Notifier, NotifierEmitter, NotifyMessage } from './types.js' diff --git a/packages/server/src/services/notification/webhook.ts b/packages/server/src/services/notification/webhook.ts index dfc2205d..4379f3fa 100644 --- a/packages/server/src/services/notification/webhook.ts +++ b/packages/server/src/services/notification/webhook.ts @@ -4,7 +4,7 @@ import got from 'got' import { ulid } from 'ulidx' import version from '../../version.js' -import { Subscription, WebhookNotification } from '../monitoring/types.js' +import { Subscription, WebhookNotification } from '../subscriptions/types.js' import { Logger, Services } from '../types.js' import { Scheduled, Scheduler, SubsStore } from '../persistence/index.js' diff --git a/packages/server/src/services/persistence/subs.ts b/packages/server/src/services/persistence/subs.ts index c239a496..3de686e4 100644 --- a/packages/server/src/services/persistence/subs.ts +++ b/packages/server/src/services/persistence/subs.ts @@ -1,5 +1,5 @@ import { NotFound, ValidationError } from '../../errors.js' -import { AgentId, Subscription } from '../monitoring/types.js' +import { AgentId, Subscription } from '../subscriptions/types.js' import { DB, Logger, jsonEncoded, prefixes } from '../types.js' /** diff --git a/packages/server/src/services/monitoring/api/index.ts b/packages/server/src/services/subscriptions/api/index.ts similarity index 100% rename from packages/server/src/services/monitoring/api/index.ts rename to packages/server/src/services/subscriptions/api/index.ts diff --git a/packages/server/src/services/monitoring/api/json-patch.ts b/packages/server/src/services/subscriptions/api/json-patch.ts similarity index 100% rename from packages/server/src/services/monitoring/api/json-patch.ts rename to packages/server/src/services/subscriptions/api/json-patch.ts diff --git a/packages/server/src/services/monitoring/api/routes.ts b/packages/server/src/services/subscriptions/api/routes.ts similarity index 100% rename from packages/server/src/services/monitoring/api/routes.ts rename to packages/server/src/services/subscriptions/api/routes.ts diff --git a/packages/server/src/services/monitoring/api/ws/plugin.ts b/packages/server/src/services/subscriptions/api/ws/plugin.ts similarity index 96% rename from packages/server/src/services/monitoring/api/ws/plugin.ts rename to packages/server/src/services/subscriptions/api/ws/plugin.ts index 0521083d..be5aa721 100644 --- a/packages/server/src/services/monitoring/api/ws/plugin.ts +++ b/packages/server/src/services/subscriptions/api/ws/plugin.ts @@ -1,7 +1,7 @@ import { FastifyPluginAsync } from 'fastify' import fp from 'fastify-plugin' -import { AgentId } from 'services/monitoring/types.js' +import { AgentId } from '../../types.js' import WebsocketProtocol from './protocol.js' declare module 'fastify' { diff --git a/packages/server/src/services/monitoring/api/ws/protocol.spec.ts b/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts similarity index 100% rename from packages/server/src/services/monitoring/api/ws/protocol.spec.ts rename to packages/server/src/services/subscriptions/api/ws/protocol.spec.ts diff --git a/packages/server/src/services/monitoring/api/ws/protocol.ts b/packages/server/src/services/subscriptions/api/ws/protocol.ts similarity index 100% rename from packages/server/src/services/monitoring/api/ws/protocol.ts rename to packages/server/src/services/subscriptions/api/ws/protocol.ts diff --git a/packages/server/src/services/monitoring/plugin.ts b/packages/server/src/services/subscriptions/plugin.ts similarity index 100% rename from packages/server/src/services/monitoring/plugin.ts rename to packages/server/src/services/subscriptions/plugin.ts diff --git a/packages/server/src/services/monitoring/storage.ts b/packages/server/src/services/subscriptions/storage.ts similarity index 100% rename from packages/server/src/services/monitoring/storage.ts rename to packages/server/src/services/subscriptions/storage.ts diff --git a/packages/server/src/services/monitoring/switchboard.spec.ts b/packages/server/src/services/subscriptions/switchboard.spec.ts similarity index 100% rename from packages/server/src/services/monitoring/switchboard.spec.ts rename to packages/server/src/services/subscriptions/switchboard.spec.ts diff --git a/packages/server/src/services/monitoring/switchboard.ts b/packages/server/src/services/subscriptions/switchboard.ts similarity index 100% rename from packages/server/src/services/monitoring/switchboard.ts rename to packages/server/src/services/subscriptions/switchboard.ts diff --git a/packages/server/src/services/monitoring/types.ts b/packages/server/src/services/subscriptions/types.ts similarity index 100% rename from packages/server/src/services/monitoring/types.ts rename to packages/server/src/services/subscriptions/types.ts diff --git a/packages/server/src/services/telemetry/metrics/index.ts b/packages/server/src/services/telemetry/metrics/index.ts index e1a535ed..47c15778 100644 --- a/packages/server/src/services/telemetry/metrics/index.ts +++ b/packages/server/src/services/telemetry/metrics/index.ts @@ -1,7 +1,7 @@ import { IngressConsumer } from '../../ingress/index.js' import IngressProducer from '../../ingress/producer/index.js' import { HeadCatcher } from '../../ingress/watcher/head-catcher.js' -import { Switchboard } from '../../monitoring/switchboard.js' +import { Switchboard } from '../../subscriptions/switchboard.js' import { NotifierHub } from '../../notification/hub.js' import { TelemetryEventEmitter } from '../types.js' import { catcherMetrics } from './catcher.js' diff --git a/packages/server/src/services/telemetry/types.ts b/packages/server/src/services/telemetry/types.ts index 572c7a9e..e4e59589 100644 --- a/packages/server/src/services/telemetry/types.ts +++ b/packages/server/src/services/telemetry/types.ts @@ -1,6 +1,6 @@ import type { Header } from '@polkadot/types/interfaces' -import { Subscription } from '../monitoring/types.js' +import { Subscription } from '../subscriptions/types.js' import { NotifyMessage } from '../notification/types.js' import { TypedEventEmitter } from '../types.js' From bb3c87907853fd77930c87d7d469aa8cd94f8b93 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 12:05:39 +0200 Subject: [PATCH 14/58] fix imports --- packages/server/src/agents/api.ts | 1 - packages/server/src/agents/local.ts | 90 -- packages/server/src/agents/plugin.ts | 45 - packages/server/src/agents/types.ts | 43 - .../server/src/agents/xcm/matching.spec.ts | 283 ------ packages/server/src/agents/xcm/matching.ts | 885 ----------------- .../server/src/agents/xcm/ops/bridge.spec.ts | 390 -------- .../server/src/agents/xcm/ops/common.spec.ts | 191 ---- packages/server/src/agents/xcm/ops/common.ts | 191 ---- .../server/src/agents/xcm/ops/criteria.ts | 49 - .../server/src/agents/xcm/ops/dmp.spec.ts | 236 ----- packages/server/src/agents/xcm/ops/dmp.ts | 332 ------- .../server/src/agents/xcm/ops/pk-bridge.ts | 206 ---- .../server/src/agents/xcm/ops/relay.spec.ts | 68 -- packages/server/src/agents/xcm/ops/relay.ts | 52 - .../server/src/agents/xcm/ops/ump.spec.ts | 114 --- packages/server/src/agents/xcm/ops/ump.ts | 119 --- .../server/src/agents/xcm/ops/util.spec.ts | 341 ------- packages/server/src/agents/xcm/ops/util.ts | 417 -------- .../src/agents/xcm/ops/xcm-format.spec.ts | 27 - .../server/src/agents/xcm/ops/xcm-format.ts | 79 -- .../server/src/agents/xcm/ops/xcm-types.ts | 576 ----------- .../server/src/agents/xcm/ops/xcmp.spec.ts | 165 ---- packages/server/src/agents/xcm/ops/xcmp.ts | 131 --- .../server/src/agents/xcm/telemetry/events.ts | 15 - .../src/agents/xcm/telemetry/metrics.ts | 111 --- .../server/src/agents/xcm/types-augmented.ts | 20 - packages/server/src/agents/xcm/types.ts | 797 --------------- packages/server/src/agents/xcm/xcm-agent.ts | 914 ------------------ packages/server/src/lib.ts | 8 +- packages/server/src/server.ts | 6 +- packages/server/src/services/index.ts | 5 +- .../src/services/ingress/consumer/index.ts | 2 +- .../src/services/ingress/producer/index.ts | 2 +- .../src/services/ingress/watcher/codec.ts | 2 +- .../services/ingress/watcher/head-catcher.ts | 2 +- .../services/ingress/watcher/local-cache.ts | 4 +- .../server/src/services/notification/log.ts | 70 +- .../server/src/services/notification/types.ts | 2 +- .../src/services/subscriptions/switchboard.ts | 2 +- .../src/services/telemetry/metrics/index.ts | 2 +- .../services/telemetry/metrics/switchboard.ts | 2 +- .../server/src/services/telemetry/types.ts | 2 +- packages/server/src/services/types.ts | 4 +- 44 files changed, 26 insertions(+), 6977 deletions(-) delete mode 100644 packages/server/src/agents/api.ts delete mode 100644 packages/server/src/agents/local.ts delete mode 100644 packages/server/src/agents/plugin.ts delete mode 100644 packages/server/src/agents/types.ts delete mode 100644 packages/server/src/agents/xcm/matching.spec.ts delete mode 100644 packages/server/src/agents/xcm/matching.ts delete mode 100644 packages/server/src/agents/xcm/ops/bridge.spec.ts delete mode 100644 packages/server/src/agents/xcm/ops/common.spec.ts delete mode 100644 packages/server/src/agents/xcm/ops/common.ts delete mode 100644 packages/server/src/agents/xcm/ops/criteria.ts delete mode 100644 packages/server/src/agents/xcm/ops/dmp.spec.ts delete mode 100644 packages/server/src/agents/xcm/ops/dmp.ts delete mode 100644 packages/server/src/agents/xcm/ops/pk-bridge.ts delete mode 100644 packages/server/src/agents/xcm/ops/relay.spec.ts delete mode 100644 packages/server/src/agents/xcm/ops/relay.ts delete mode 100644 packages/server/src/agents/xcm/ops/ump.spec.ts delete mode 100644 packages/server/src/agents/xcm/ops/ump.ts delete mode 100644 packages/server/src/agents/xcm/ops/util.spec.ts delete mode 100644 packages/server/src/agents/xcm/ops/util.ts delete mode 100644 packages/server/src/agents/xcm/ops/xcm-format.spec.ts delete mode 100644 packages/server/src/agents/xcm/ops/xcm-format.ts delete mode 100644 packages/server/src/agents/xcm/ops/xcm-types.ts delete mode 100644 packages/server/src/agents/xcm/ops/xcmp.spec.ts delete mode 100644 packages/server/src/agents/xcm/ops/xcmp.ts delete mode 100644 packages/server/src/agents/xcm/telemetry/events.ts delete mode 100644 packages/server/src/agents/xcm/telemetry/metrics.ts delete mode 100644 packages/server/src/agents/xcm/types-augmented.ts delete mode 100644 packages/server/src/agents/xcm/types.ts delete mode 100644 packages/server/src/agents/xcm/xcm-agent.ts diff --git a/packages/server/src/agents/api.ts b/packages/server/src/agents/api.ts deleted file mode 100644 index c03a6752..00000000 --- a/packages/server/src/agents/api.ts +++ /dev/null @@ -1 +0,0 @@ -// TBD agent web api diff --git a/packages/server/src/agents/local.ts b/packages/server/src/agents/local.ts deleted file mode 100644 index ad7c4dbc..00000000 --- a/packages/server/src/agents/local.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Logger, Services } from '../services/index.js' -import { AgentId, NotificationListener, Subscription } from '../services/subscriptions/types.js' -import { NotifierHub } from '../services/notification/index.js' -import { NotifierEvents } from '../services/notification/types.js' -import { AgentServiceOptions } from '../types.js' -import { Agent, AgentRuntimeContext, AgentService } from './types.js' -import { XCMAgent } from './xcm/xcm-agent.js' - -/** - * Local agent service. - */ -export class LocalAgentService implements AgentService { - readonly #log: Logger - readonly #agents: Record - readonly #notifier: NotifierHub - - constructor(ctx: Services, _options: AgentServiceOptions) { - this.#log = ctx.log - - // XXX: this is a local in the process memory - // notifier hub - this.#notifier = new NotifierHub(ctx) - - this.#agents = this.#loadAgents({ - ...ctx, - notifier: this.#notifier, - }) - } - - /** - * Retrieves the registered subscriptions in the database - * for all the configured networks. - * - * @returns {Subscription[]} an array with the subscriptions - */ - async getAllSubscriptions() { - let subscriptions: Subscription[] = [] - for (const chainId of this.getAgentIds()) { - const agent = await this.getAgentById(chainId) - subscriptions = subscriptions.concat(await agent.getAllSubscriptions()) - } - - return subscriptions - } - - addNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub { - return this.#notifier.on(eventName, listener) - } - - removeNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub { - return this.#notifier.off(eventName, listener) - } - - getAgentIds(): AgentId[] { - return Object.keys(this.#agents) - } - - getAgentById(agentId: AgentId): Agent { - if (this.#agents[agentId]) { - return this.#agents[agentId] - } - throw new Error(`Agent not found for id=${agentId}`) - } - - getAgentInputSchema(agentId: AgentId) { - const agent = this.getAgentById(agentId) - return agent.getInputSchema() - } - - async start() { - for (const [id, agent] of Object.entries(this.#agents)) { - this.#log.info('[local:agents] Starting agent %s', id) - await agent.start() - } - } - - async stop() { - for (const [id, agent] of Object.entries(this.#agents)) { - this.#log.info('[local:agents] Stopping agent %s', id) - await agent.stop() - } - } - - #loadAgents(ctx: AgentRuntimeContext) { - const xcm = new XCMAgent(ctx) - return { - [xcm.id]: xcm, - } - } -} diff --git a/packages/server/src/agents/plugin.ts b/packages/server/src/agents/plugin.ts deleted file mode 100644 index d15794cf..00000000 --- a/packages/server/src/agents/plugin.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { FastifyPluginAsync } from 'fastify' -import fp from 'fastify-plugin' - -import { AgentServiceMode, AgentServiceOptions } from '../types.js' -import { LocalAgentService } from './local.js' -import { AgentService } from './types.js' - -declare module 'fastify' { - interface FastifyInstance { - agentService: AgentService - } -} - -/** - * Fastify plug-in for instantiating an {@link AgentService} instance. - * - * @param fastify The Fastify instance. - * @param options Options for configuring the Agent Service. - */ -const agentServicePlugin: FastifyPluginAsync = async (fastify, options) => { - if (options.mode !== AgentServiceMode.local) { - throw new Error('Only local agent service is supported') - } - const service: AgentService = new LocalAgentService(fastify, options) - - fastify.addHook('onClose', (server, done) => { - service - .stop() - .then(() => { - server.log.info('Agent service stopped') - }) - .catch((error: any) => { - server.log.error(error, 'Error while stopping agent service') - }) - .finally(() => { - done() - }) - }) - - fastify.decorate('agentService', service) - - await service.start() -} - -export default fp(agentServicePlugin, { fastify: '>=4.x', name: 'agent-service' }) diff --git a/packages/server/src/agents/types.ts b/packages/server/src/agents/types.ts deleted file mode 100644 index de7bc29a..00000000 --- a/packages/server/src/agents/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { z } from 'zod' - -import { Operation } from 'rfc6902' - -import { IngressConsumer } from '../services/ingress/index.js' -import { AgentId, NotificationListener, Subscription } from '../services/subscriptions/types.js' -import { NotifierHub } from '../services/notification/hub.js' -import { NotifierEvents } from '../services/notification/types.js' -import { Janitor } from '../services/persistence/janitor.js' -import { SubsStore } from '../services/persistence/subs.js' -import { DB, Logger } from '../services/types.js' - -export type AgentRuntimeContext = { - log: Logger - notifier: NotifierHub - ingressConsumer: IngressConsumer - rootStore: DB - subsStore: SubsStore - janitor: Janitor -} - -export interface AgentService { - addNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub - removeNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub - getAgentById(agentId: AgentId): Agent - getAgentInputSchema(agentId: AgentId): z.ZodSchema - getAgentIds(): AgentId[] - start(): Promise - stop(): Promise -} - -export interface Agent { - get id(): AgentId - getSubscriptionById(subscriptionId: string): Promise - getAllSubscriptions(): Promise - getInputSchema(): z.ZodSchema - getSubscriptionHandler(subscriptionId: string): Subscription - subscribe(subscription: Subscription): Promise - unsubscribe(subscriptionId: string): Promise - update(subscriptionId: string, patch: Operation[]): Promise - stop(): Promise - start(): Promise -} diff --git a/packages/server/src/agents/xcm/matching.spec.ts b/packages/server/src/agents/xcm/matching.spec.ts deleted file mode 100644 index 304ab2a3..00000000 --- a/packages/server/src/agents/xcm/matching.spec.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { jest } from '@jest/globals' - -import { MemoryLevel as Level } from 'memory-level' - -import { AbstractSublevel } from 'abstract-level' -import { XcmInbound, XcmNotificationType, XcmNotifyMessage, XcmSent } from '../../services/monitoring/types.js' -import { Janitor } from '../../services/persistence/janitor.js' -import { jsonEncoded, prefixes } from '../../services/types.js' -import { matchBridgeMessages } from '../../testing/bridge/matching.js' -import { matchHopMessages, matchMessages, realHopMessages } from '../../testing/matching.js' -import { _services } from '../../testing/services.js' -import { MatchingEngine } from './matching.js' - -describe('message matching engine', () => { - let engine: MatchingEngine - let db: Level - let outbound: AbstractSublevel - const cb = jest.fn((_: XcmNotifyMessage) => { - /* empty */ - }) - const schedule = jest.fn(() => { - /* empty */ - }) - - beforeEach(() => { - cb.mockReset() - schedule.mockReset() - - db = new Level() - engine = new MatchingEngine( - { - ..._services, - rootStore: db, - janitor: { - on: jest.fn(), - schedule, - } as unknown as Janitor, - }, - cb - ) - - outbound = db.sublevel(prefixes.matching.outbound, jsonEncoded) - }) - - it('should match inbound and outbound', async () => { - const { origin, destination, subscriptionId } = matchMessages - const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` - const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` - - await engine.onOutboundMessage(origin) - await engine.onInboundMessage(destination) - - expect(cb).toHaveBeenCalledTimes(2) - await expect(outbound.get(idKey)).rejects.toBeDefined() - await expect(outbound.get(hashKey)).rejects.toBeDefined() - }) - - it('should match outbound and inbound', async () => { - const { origin, destination, subscriptionId } = matchMessages - const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` - const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` - - await engine.onInboundMessage(destination) - await engine.onOutboundMessage(origin) - - expect(cb).toHaveBeenCalledTimes(2) - await expect(outbound.get(idKey)).rejects.toBeDefined() - await expect(outbound.get(hashKey)).rejects.toBeDefined() - }) - - it('should work async concurrently', async () => { - const { origin, destination, subscriptionId } = matchMessages - const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` - const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` - - await Promise.all([engine.onOutboundMessage(origin), engine.onInboundMessage(destination)]) - - expect(cb).toHaveBeenCalledTimes(2) - await expect(outbound.get(idKey)).rejects.toBeDefined() - await expect(outbound.get(hashKey)).rejects.toBeDefined() - }) - - it('should match outbound and relay', async () => { - await engine.onOutboundMessage(matchMessages.origin) - await engine.onRelayedMessage(matchMessages.subscriptionId, matchMessages.relay) - - expect(cb).toHaveBeenCalledTimes(2) - }) - - it('should match relay and outbound', async () => { - await engine.onRelayedMessage(matchMessages.subscriptionId, matchMessages.relay) - await engine.onOutboundMessage(matchMessages.origin) - expect(schedule).toHaveBeenCalledTimes(2) - - expect(cb).toHaveBeenCalledTimes(2) - }) - - it('should match relay and outbound and inbound', async () => { - const { origin, relay, destination, subscriptionId } = matchMessages - const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` - const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` - - await engine.onRelayedMessage(subscriptionId, relay) - await engine.onOutboundMessage(origin) - await engine.onInboundMessage(destination) - - expect(schedule).toHaveBeenCalledTimes(2) - expect(cb).toHaveBeenCalledTimes(3) - await expect(outbound.get(idKey)).rejects.toBeDefined() - await expect(outbound.get(hashKey)).rejects.toBeDefined() - }) - - it('should match outbound and inbound by message hash', async () => { - const { origin, destination, subscriptionId } = matchMessages - const omsg: XcmSent = { - ...origin, - messageId: undefined, - } - const imsg: XcmInbound = { - ...destination, - messageId: destination.messageHash, - } - const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` - const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` - - await engine.onOutboundMessage(omsg) - await engine.onInboundMessage(imsg) - - expect(cb).toHaveBeenCalledTimes(2) - await expect(outbound.get(idKey)).rejects.toBeDefined() - await expect(outbound.get(hashKey)).rejects.toBeDefined() - }) - - it('should match with messageId on outbound and only message hash on inbound', async () => { - const { origin, destination, subscriptionId } = matchMessages - const imsg: XcmInbound = { - ...destination, - messageId: destination.messageHash, - } - const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` - const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` - - await engine.onOutboundMessage(origin) - await engine.onInboundMessage(imsg) - - expect(cb).toHaveBeenCalledTimes(2) - await expect(outbound.get(idKey)).rejects.toBeDefined() - await expect(outbound.get(hashKey)).rejects.toBeDefined() - }) - - it('should match hop messages', async () => { - const { origin, relay0, hopin, hopout, relay2, destination, subscriptionId } = matchHopMessages - const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` - const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` - - await engine.onOutboundMessage(origin) - await engine.onRelayedMessage(subscriptionId, relay0) - - await engine.onInboundMessage(hopin) - await engine.onOutboundMessage(hopout) - await engine.onRelayedMessage(subscriptionId, relay2) - await engine.onInboundMessage(destination) - - expect(cb).toHaveBeenCalledTimes(6) - await expect(outbound.get(idKey)).rejects.toBeDefined() - await expect(outbound.get(hashKey)).rejects.toBeDefined() - }) - - it('should match hop messages', async () => { - const msgTypeCb = jest.fn((_: XcmNotificationType) => { - /* empty */ - }) - cb.mockImplementation((msg) => msgTypeCb(msg.type)) - - const { origin, hopin, hopout } = realHopMessages - - await engine.onOutboundMessage(origin) - - await engine.onInboundMessage(hopin) - await engine.onOutboundMessage(hopout) - - expect(cb).toHaveBeenCalledTimes(3) - expect(msgTypeCb).toHaveBeenNthCalledWith<[XcmNotificationType]>(1, XcmNotificationType.Sent) - expect(msgTypeCb).toHaveBeenNthCalledWith<[XcmNotificationType]>(2, XcmNotificationType.Hop) - expect(msgTypeCb).toHaveBeenNthCalledWith<[XcmNotificationType]>(3, XcmNotificationType.Hop) - }) - - it('should match hop messages with concurrent message on hop stop', async () => { - const { origin, relay0, hopin, hopout, relay2, destination, subscriptionId } = matchHopMessages - const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` - const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` - - await engine.onOutboundMessage(origin) - await engine.onRelayedMessage(subscriptionId, relay0) - await Promise.all([engine.onInboundMessage(hopin), engine.onOutboundMessage(hopout)]) - await engine.onRelayedMessage(subscriptionId, relay2) - await engine.onInboundMessage(destination) - - expect(cb).toHaveBeenCalledTimes(6) - await expect(outbound.get(idKey)).rejects.toBeDefined() - await expect(outbound.get(hashKey)).rejects.toBeDefined() - }) - - it('should match hop messages with concurrent message on hop stop and relay out of order', async () => { - const { origin, relay0, hopin, hopout, relay2, destination, subscriptionId } = matchHopMessages - const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` - const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` - - await engine.onRelayedMessage(subscriptionId, relay0) - await engine.onOutboundMessage(origin) - await engine.onRelayedMessage(subscriptionId, relay2) - - await Promise.all([engine.onInboundMessage(hopin), engine.onOutboundMessage(hopout)]) - - await engine.onInboundMessage(destination) - - expect(cb).toHaveBeenCalledTimes(6) - await expect(outbound.get(idKey)).rejects.toBeDefined() - await expect(outbound.get(hashKey)).rejects.toBeDefined() - }) - - it('should match bridge messages', async () => { - const { - origin, - relay0, - bridgeXcmIn, - bridgeAccepted, - bridgeDelivered, - bridgeIn, - bridgeXcmOut, - relay1, - destination, - subscriptionId, - } = matchBridgeMessages - const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` - const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` - - await engine.onOutboundMessage(origin) - await engine.onRelayedMessage(subscriptionId, relay0) - await engine.onInboundMessage(bridgeXcmIn) - await engine.onBridgeOutboundAccepted(subscriptionId, bridgeAccepted) - await engine.onBridgeOutboundDelivered(subscriptionId, bridgeDelivered) - await engine.onBridgeInbound(subscriptionId, bridgeIn) - await engine.onOutboundMessage(bridgeXcmOut) - await engine.onRelayedMessage(subscriptionId, relay1) - await engine.onInboundMessage(destination) - - expect(cb).toHaveBeenCalledTimes(9) - await expect(outbound.get(idKey)).rejects.toBeDefined() - await expect(outbound.get(hashKey)).rejects.toBeDefined() - }) - - it('should clean up stale data', async () => { - async function count() { - const iterator = db.iterator() - await iterator.all() - return iterator.count - } - - for (let i = 0; i < 100; i++) { - await engine.onInboundMessage({ - ...matchMessages.destination, - subscriptionId: 'z.transfers:' + i, - }) - await engine.onOutboundMessage({ - ...matchMessages.origin, - subscriptionId: 'baba-yaga-1:' + i, - }) - const r = (Math.random() + 1).toString(36).substring(7) - await engine.onOutboundMessage({ - ...matchMessages.origin, - subscriptionId: r + i, - }) - } - expect(await count()).toBe(600) - - for (let i = 0; i < 100; i++) { - await engine.clearPendingStates('z.transfers:' + i) - await engine.clearPendingStates('baba-yaga-1:' + i) - } - expect(await count()).toBe(200) - }) -}) diff --git a/packages/server/src/agents/xcm/matching.ts b/packages/server/src/agents/xcm/matching.ts deleted file mode 100644 index adde1c43..00000000 --- a/packages/server/src/agents/xcm/matching.ts +++ /dev/null @@ -1,885 +0,0 @@ -import EventEmitter from 'node:events' - -import { AbstractSublevel } from 'abstract-level' -import { Mutex } from 'async-mutex' - -import { DB, Logger, jsonEncoded, prefixes } from '../../services/types.js' -import { - GenericXcmBridge, - GenericXcmHop, - GenericXcmReceived, - GenericXcmRelayed, - GenericXcmTimeout, - XcmBridge, - XcmBridgeAcceptedWithContext, - XcmBridgeDeliveredWithContext, - XcmBridgeInboundWithContext, - XcmHop, - XcmInbound, - XcmNotifyMessage, - XcmReceived, - XcmRelayed, - XcmRelayedWithContext, - XcmSent, - XcmTimeout, - XcmWaypointContext, -} from './types.js' - -import { getRelayId, isOnSameConsensus } from '../../services/config.js' -import { Janitor, JanitorTask } from '../../services/persistence/janitor.js' -import { AgentRuntimeContext } from '../types.js' -import { TelemetryXCMEventEmitter } from './telemetry/events.js' - -export type XcmMatchedReceiver = (message: XcmNotifyMessage) => Promise | void -type SubLevel = AbstractSublevel - -export type ChainBlock = { - chainId: string - blockHash: string - blockNumber: string -} - -const DEFAULT_TIMEOUT = 2 * 60000 -/** - * Matches sent XCM messages on the destination. - * It does not assume any ordering. - * - * Current matching logic takes into account that messages at origin and destination - * might or might not have a unique ID set via SetTopic instruction. - * Therefore, it supports matching logic using both message hash and message ID. - * - * When unique message ID is implemented in all XCM events, we can: - * - simplify logic to match only by message ID - * - check notification storage by message ID and do not store for matching if already matched - */ -export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEventEmitter) { - readonly #log: Logger - readonly #janitor: Janitor - - readonly #outbound: SubLevel - readonly #inbound: SubLevel - readonly #relay: SubLevel - readonly #hop: SubLevel - readonly #bridge: SubLevel - readonly #bridgeAccepted: SubLevel - readonly #bridgeInbound: SubLevel - readonly #mutex: Mutex - readonly #xcmMatchedReceiver: XcmMatchedReceiver - - constructor({ log, rootStore, janitor }: AgentRuntimeContext, xcmMatchedReceiver: XcmMatchedReceiver) { - super() - - this.#log = log - this.#janitor = janitor - this.#mutex = new Mutex() - this.#xcmMatchedReceiver = xcmMatchedReceiver - - // Key format: [subscription-id]:[destination-chain-id]:[message-id/hash] - this.#outbound = rootStore.sublevel(prefixes.matching.outbound, jsonEncoded) - // Key format: [subscription-id]:[current-chain-id]:[message-id/hash] - this.#inbound = rootStore.sublevel(prefixes.matching.inbound, jsonEncoded) - // Key format: [subscription-id]:[relay-outbound-chain-id]:[message-id/hash] - this.#relay = rootStore.sublevel(prefixes.matching.relay, jsonEncoded) - // Key format: [subscription-id]:[hop-stop-chain-id]:[message-id/hash] - this.#hop = rootStore.sublevel(prefixes.matching.hop, jsonEncoded) - // Key format: [subscription-id]:[bridge-chain-id]:[message-id] - this.#bridge = rootStore.sublevel(prefixes.matching.bridge, jsonEncoded) - - // Key format: [subscription-id]:[bridge-key] - this.#bridgeAccepted = rootStore.sublevel(prefixes.matching.bridgeAccepted, jsonEncoded) - // Key format: [subscription-id]:[bridge-key] - this.#bridgeInbound = rootStore.sublevel( - prefixes.matching.bridgeIn, - jsonEncoded - ) - - this.#janitor.on('sweep', this.#onXcmSwept.bind(this)) - } - - async onOutboundMessage(outMsg: XcmSent, outboundTTL: number = DEFAULT_TIMEOUT) { - const log = this.#log - - // Confirmation key at destination - await this.#mutex.runExclusive(async () => { - const hashKey = this.#matchingKey(outMsg.subscriptionId, outMsg.destination.chainId, outMsg.waypoint.messageHash) - // try to get any stored relay messages and notify if found. - // do not clean up outbound in case inbound has not arrived yet. - await this.#findRelayInbound(outMsg) - - if (outMsg.forwardId !== undefined) { - // Is bridged message - // Try to match origin message (on other consensus) using the forward ID. - // If found, create outbound message with origin context and current waypoint context - // before trying to match inbound (on same consensus). - // If origin message not found, treat as normal XCM outbound - try { - const { - subscriptionId, - origin: { chainId }, - messageId, - forwardId, - waypoint, - } = outMsg - const forwardIdKey = this.#matchingKey(subscriptionId, chainId, forwardId) - const originMsg = await this.#bridge.get(forwardIdKey) - - const bridgedSent: XcmSent = { - ...originMsg, - waypoint, - messageId, - forwardId, - } - this.#onXcmOutbound(bridgedSent) - await this.#tryMatchOnOutbound(bridgedSent, outboundTTL) - await this.#bridge.del(forwardIdKey) - } catch { - this.#onXcmOutbound(outMsg) - await this.#tryMatchOnOutbound(outMsg, outboundTTL) - } - } else if (outMsg.messageId) { - // Is not bridged message - // First try to match by hop key - // If found, emit hop, and do not store anything - // If no matching hop key, assume is origin outbound message -> try to match inbound - // We assume that the original origin message is ALWAYS received first. - // NOTE: hops can only use idKey since message hash will be different on each hop - try { - const hopKey = this.#matchingKey(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.messageId) - const originMsg = await this.#hop.get(hopKey) - log.info( - '[%s:h] MATCHED HOP OUT origin=%s id=%s (subId=%s, block=%s #%s)', - outMsg.origin.chainId, - originMsg.origin.chainId, - hopKey, - outMsg.subscriptionId, - outMsg.origin.blockHash, - outMsg.origin.blockNumber - ) - // do not delete hop key because maybe hop stop inbound hasn't arrived yet - this.#onXcmHopOut(originMsg, outMsg) - } catch { - this.#onXcmOutbound(outMsg) - // Try to get stored inbound messages and notify if any - // If inbound messages are found, clean up outbound. - // If not found, store outbound message in #outbound to match destination inbound - // and #hop to match hop outbounds and inbounds. - // Note: if relay messages arrive after outbound and inbound, it will not match. - await this.#tryMatchOnOutbound(outMsg, outboundTTL) - } - } else { - this.#onXcmOutbound(outMsg) - // try to get stored inbound messages by message hash and notify if any - try { - const inMsg = await this.#inbound.get(hashKey) - log.info( - '[%s:o] MATCHED hash=%s (subId=%s, block=%s #%s)', - outMsg.origin.chainId, - hashKey, - outMsg.subscriptionId, - outMsg.origin.blockHash, - outMsg.origin.blockNumber - ) - await this.#inbound.del(hashKey) - this.#onXcmMatched(outMsg, inMsg) - } catch { - await this.#storekeysOnOutbound(outMsg, outboundTTL) - } - } - }) - - return outMsg - } - - async onInboundMessage(inMsg: XcmInbound) { - const log = this.#log - - await this.#mutex.runExclusive(async () => { - let hashKey = this.#matchingKey(inMsg.subscriptionId, inMsg.chainId, inMsg.messageHash) - let idKey = this.#matchingKey(inMsg.subscriptionId, inMsg.chainId, inMsg.messageId) - - if (hashKey === idKey) { - // if hash and id are the same, both could be the message hash or both could be the message id - try { - const outMsg = await this.#outbound.get(hashKey) - log.info( - '[%s:i] MATCHED hash=%s (subId=%s, block=%s #%s)', - inMsg.chainId, - hashKey, - inMsg.subscriptionId, - inMsg.blockHash, - inMsg.blockNumber - ) - // if outbound has no messageId, we can safely assume that - // idKey and hashKey are made up of only the message hash. - // if outbound has messageId, we need to reconstruct idKey and hashKey - // using outbound values to ensure that no dangling keys will be left on janitor sweep. - if (outMsg.messageId !== undefined) { - idKey = this.#matchingKey(inMsg.subscriptionId, inMsg.chainId, outMsg.messageId) - hashKey = this.#matchingKey(inMsg.subscriptionId, inMsg.chainId, outMsg.waypoint.messageHash) - } - await this.#outbound.batch().del(idKey).del(hashKey).write() - this.#onXcmMatched(outMsg, inMsg) - } catch { - await this.#tryHopMatchOnInbound(inMsg) - } - } else { - try { - const outMsg = await Promise.any([this.#outbound.get(idKey), this.#outbound.get(hashKey)]) - // Reconstruct hashKey with outbound message hash in case of hopped messages - hashKey = this.#matchingKey(inMsg.subscriptionId, inMsg.chainId, outMsg.waypoint.messageHash) - log.info( - '[%s:i] MATCHED hash=%s id=%s (subId=%s, block=%s #%s)', - inMsg.chainId, - hashKey, - idKey, - inMsg.subscriptionId, - inMsg.blockHash, - inMsg.blockNumber - ) - await this.#outbound.batch().del(idKey).del(hashKey).write() - this.#onXcmMatched(outMsg, inMsg) - } catch { - await this.#tryHopMatchOnInbound(inMsg) - } - } - }) - } - - async onRelayedMessage(subscriptionId: string, relayMsg: XcmRelayedWithContext) { - const log = this.#log - - const relayId = getRelayId(relayMsg.origin) - const idKey = relayMsg.messageId - ? this.#matchingKey(subscriptionId, relayMsg.recipient, relayMsg.messageId) - : this.#matchingKey(subscriptionId, relayMsg.recipient, relayMsg.messageHash) - - await this.#mutex.runExclusive(async () => { - try { - const outMsg = await this.#outbound.get(idKey) - log.info( - '[%s:r] RELAYED origin=%s recipient=%s (subId=%s, block=%s #%s)', - relayId, - relayMsg.origin, - relayMsg.recipient, - subscriptionId, - relayMsg.blockHash, - relayMsg.blockNumber - ) - await this.#relay.del(idKey) - await this.#onXcmRelayed(outMsg, relayMsg) - } catch { - const relayKey = relayMsg.messageId - ? this.#matchingKey(subscriptionId, relayMsg.origin, relayMsg.messageId) - : this.#matchingKey(subscriptionId, relayMsg.origin, relayMsg.messageHash) - log.info( - '[%s:r] STORED relayKey=%s origin=%s recipient=%s (subId=%s, block=%s #%s)', - relayId, - relayKey, - relayMsg.origin, - relayMsg.recipient, - subscriptionId, - relayMsg.blockHash, - relayMsg.blockNumber - ) - await this.#relay.put(relayKey, relayMsg) - await this.#janitor.schedule({ - sublevel: prefixes.matching.relay, - key: relayKey, - }) - } - }) - } - - async onBridgeOutboundAccepted(subscriptionId: string, msg: XcmBridgeAcceptedWithContext) { - const log = this.#log - - await this.#mutex.runExclusive(async () => { - if (msg.forwardId === undefined) { - log.error( - '[%s] forward_id_to not found for bridge accepted message (sub=%s block=%s #%s)', - msg.chainId, - subscriptionId, - msg.blockHash, - msg.blockNumber - ) - return - } - const { chainId, forwardId, bridgeKey } = msg - const idKey = this.#matchingKey(subscriptionId, chainId, forwardId) - - try { - const originMsg = await this.#bridge.get(idKey) - - const { blockHash, blockNumber, event, messageData, instructions, messageHash } = msg - const legIndex = originMsg.legs.findIndex((l) => l.from === chainId && l.type === 'bridge') - const waypointContext: XcmWaypointContext = { - legIndex, - chainId, - blockHash, - blockNumber: blockNumber.toString(), - event, - messageData, - messageHash, - instructions, - outcome: 'Success', // always 'Success' since it's delivered - error: null, - } - const bridgeOutMsg: XcmBridge = new GenericXcmBridge(originMsg, waypointContext, { - bridgeMessageType: 'accepted', - bridgeKey, - forwardId, - }) - const sublevelBridgeKey = `${subscriptionId}:${bridgeKey}` - await this.#bridgeAccepted.put(sublevelBridgeKey, bridgeOutMsg) - await this.#janitor.schedule({ - sublevel: prefixes.matching.bridgeAccepted, - key: sublevelBridgeKey, - }) - log.info( - '[%s:ba] BRIDGE MESSAGE ACCEPTED key=%s (subId=%s, block=%s #%s)', - chainId, - sublevelBridgeKey, - subscriptionId, - msg.blockHash, - msg.blockNumber - ) - this.#onXcmBridgeAccepted(bridgeOutMsg) - } catch { - log.warn( - '[%s:ba] ORIGIN MSG NOT FOUND id=%s (subId=%s, block=%s #%s)', - chainId, - idKey, - subscriptionId, - msg.blockHash, - msg.blockNumber - ) - } - }) - } - - async onBridgeOutboundDelivered(subscriptionId: string, msg: XcmBridgeDeliveredWithContext) { - const log = this.#log - - await this.#mutex.runExclusive(async () => { - const { chainId, bridgeKey } = msg - const sublevelBridgeKey = `${subscriptionId}:${bridgeKey}` - try { - const bridgeOutMsg = await this.#bridgeAccepted.get(sublevelBridgeKey) - try { - const bridgeInMsg = await this.#bridgeInbound.get(sublevelBridgeKey) - log.info( - '[%s:bd] BRIDGE MATCHED key=%s (subId=%s, block=%s #%s)', - chainId, - sublevelBridgeKey, - subscriptionId, - msg.blockHash, - msg.blockNumber - ) - await this.#bridgeInbound.del(sublevelBridgeKey) - await this.#bridgeAccepted.del(sublevelBridgeKey) - this.#onXcmBridgeDelivered({ ...bridgeOutMsg, bridgeMessageType: 'delivered' }) - this.#onXcmBridgeMatched(bridgeOutMsg, bridgeInMsg) - } catch { - this.#log.info( - '[%s:bo] BRIDGE DELIVERED key=%s (subId=%s, block=%s #%s)', - chainId, - sublevelBridgeKey, - subscriptionId, - msg.blockHash, - msg.blockNumber - ) - this.#onXcmBridgeDelivered(bridgeOutMsg) - } - } catch { - log.warn( - '[%s:bd] BRIDGE ACCEPTED MSG NOT FOUND key=%s (subId=%s, block=%s #%s)', - chainId, - sublevelBridgeKey, - subscriptionId, - msg.blockHash, - msg.blockNumber - ) - } - }) - } - - async onBridgeInbound(subscriptionId: string, bridgeInMsg: XcmBridgeInboundWithContext) { - const log = this.#log - - await this.#mutex.runExclusive(async () => { - const { chainId, bridgeKey } = bridgeInMsg - const sublevelBridgeKey = `${subscriptionId}:${bridgeKey}` - - try { - const bridgeOutMsg = await this.#bridgeAccepted.get(sublevelBridgeKey) - log.info( - '[%s:bi] BRIDGE MATCHED key=%s (subId=%s, block=%s #%s)', - chainId, - sublevelBridgeKey, - subscriptionId, - bridgeInMsg.blockHash, - bridgeInMsg.blockNumber - ) - await this.#bridgeAccepted.del(sublevelBridgeKey) - this.#onXcmBridgeMatched(bridgeOutMsg, bridgeInMsg) - } catch { - this.#log.info( - '[%s:bi] BRIDGE IN STORED id=%s (subId=%s, block=%s #%s)', - chainId, - sublevelBridgeKey, - subscriptionId, - bridgeInMsg.blockHash, - bridgeInMsg.blockNumber - ) - this.#bridgeInbound.put(sublevelBridgeKey, bridgeInMsg) - await this.#janitor.schedule({ - sublevel: prefixes.matching.bridgeIn, - key: sublevelBridgeKey, - }) - } - }) - } - - // try to find in DB by hop key - // if found, emit hop, and do not store anything - // if no matching hop key, assume is destination inbound and store. - // We assume that the original origin message is ALWAYS received first. - // NOTE: hops can only use idKey since message hash will be different on each hop - async #tryHopMatchOnInbound(msg: XcmInbound) { - const log = this.#log - try { - const hopKey = this.#matchingKey(msg.subscriptionId, msg.chainId, msg.messageId) - const originMsg = await this.#hop.get(hopKey) - log.info( - '[%s:h] MATCHED HOP IN origin=%s id=%s (subId=%s, block=%s #%s)', - msg.chainId, - originMsg.origin.chainId, - hopKey, - msg.subscriptionId, - msg.blockHash, - msg.blockNumber - ) - // do not delete hop key because maybe hop stop outbound hasn't arrived yet - // TO THINK: store in different keys? - this.#onXcmHopIn(originMsg, msg) - } catch { - const hashKey = this.#matchingKey(msg.subscriptionId, msg.chainId, msg.messageHash) - const idKey = this.#matchingKey(msg.subscriptionId, msg.chainId, msg.messageId) - - if (hashKey === idKey) { - log.info( - '[%s:i] STORED hash=%s (subId=%s, block=%s #%s)', - msg.chainId, - hashKey, - msg.subscriptionId, - msg.blockHash, - msg.blockNumber - ) - await this.#inbound.put(hashKey, msg) - await this.#janitor.schedule({ - sublevel: prefixes.matching.inbound, - key: hashKey, - }) - } else { - log.info( - '[%s:i] STORED hash=%s id=%s (subId=%s, block=%s #%s)', - msg.chainId, - hashKey, - idKey, - msg.subscriptionId, - msg.blockHash, - msg.blockNumber - ) - await this.#inbound.batch().put(idKey, msg).put(hashKey, msg).write() - await this.#janitor.schedule( - { - sublevel: prefixes.matching.inbound, - key: hashKey, - }, - { - sublevel: prefixes.matching.inbound, - key: idKey, - } - ) - } - } - } - - async #tryMatchOnOutbound(msg: XcmSent, outboundTTL: number) { - if (msg.messageId === undefined) { - return - } - - // Still we don't know if the inbound is upgraded, - // i.e. if uses message ids - const idKey = this.#matchingKey(msg.subscriptionId, msg.destination.chainId, msg.messageId) - const hashKey = this.#matchingKey(msg.subscriptionId, msg.destination.chainId, msg.waypoint.messageHash) - - const log = this.#log - try { - const inMsg = await Promise.any([this.#inbound.get(idKey), this.#inbound.get(hashKey)]) - - log.info( - '[%s:o] MATCHED hash=%s id=%s (subId=%s, block=%s #%s)', - msg.origin.chainId, - hashKey, - idKey, - msg.subscriptionId, - msg.origin.blockHash, - msg.origin.blockNumber - ) - await this.#inbound.batch().del(idKey).del(hashKey).write() - this.#onXcmMatched(msg, inMsg) - } catch { - await this.#storekeysOnOutbound(msg, outboundTTL) - } - } - - // TODO: refactor to lower complexity - async #storekeysOnOutbound(msg: XcmSent, outboundTTL: number) { - const log = this.#log - const sublegs = msg.legs.filter((l) => isOnSameConsensus(msg.waypoint.chainId, l.from)) - - for (const [i, leg] of sublegs.entries()) { - const stop = leg.to - const hKey = this.#matchingKey(msg.subscriptionId, stop, msg.waypoint.messageHash) - if (msg.messageId) { - const iKey = this.#matchingKey(msg.subscriptionId, stop, msg.messageId) - if (leg.type === 'bridge') { - const bridgeOut = leg.from - const bridgeIn = leg.to - const bridgeOutIdKey = this.#matchingKey(msg.subscriptionId, bridgeOut, msg.messageId) - const bridgeInIdKey = this.#matchingKey(msg.subscriptionId, bridgeIn, msg.messageId) - log.info( - '[%s:b] STORED out=%s outKey=%s in=%s inKey=%s (subId=%s, block=%s #%s)', - msg.origin.chainId, - bridgeOut, - bridgeOutIdKey, - bridgeIn, - bridgeInIdKey, - msg.subscriptionId, - msg.origin.blockHash, - msg.origin.blockNumber - ) - await this.#bridge.batch().put(bridgeOutIdKey, msg).put(bridgeInIdKey, msg).write() - await this.#janitor.schedule( - { - sublevel: prefixes.matching.bridge, - key: bridgeOutIdKey, - expiry: outboundTTL, - }, - { - sublevel: prefixes.matching.bridge, - key: bridgeInIdKey, - expiry: outboundTTL, - } - ) - } else if (i === sublegs.length - 1 || leg.relay !== undefined) { - log.info( - '[%s:o] STORED dest=%s hash=%s id=%s (subId=%s, block=%s #%s)', - msg.origin.chainId, - stop, - hKey, - iKey, - msg.subscriptionId, - msg.origin.blockHash, - msg.origin.blockNumber - ) - await this.#outbound.batch().put(iKey, msg).put(hKey, msg).write() - await this.#janitor.schedule( - { - sublevel: prefixes.matching.outbound, - key: hKey, - expiry: outboundTTL, - }, - { - sublevel: prefixes.matching.outbound, - key: iKey, - expiry: outboundTTL, - } - ) - } else if (leg.type === 'hop') { - log.info( - '[%s:h] STORED stop=%s hash=%s id=%s (subId=%s, block=%s #%s)', - msg.origin.chainId, - stop, - hKey, - iKey, - msg.subscriptionId, - msg.origin.blockHash, - msg.origin.blockNumber - ) - await this.#hop.batch().put(iKey, msg).put(hKey, msg).write() - await this.#janitor.schedule( - { - sublevel: prefixes.matching.hop, - key: hKey, - expiry: outboundTTL, - }, - { - sublevel: prefixes.matching.hop, - key: iKey, - expiry: outboundTTL, - } - ) - } - } else if (i === sublegs.length - 1 || leg.relay !== undefined) { - log.info( - '[%s:o] STORED dest=%s hash=%s (subId=%s, block=%s #%s)', - msg.origin.chainId, - stop, - hKey, - msg.subscriptionId, - msg.origin.blockHash, - msg.origin.blockNumber - ) - await this.#outbound.put(hKey, msg) - await this.#janitor.schedule({ - sublevel: prefixes.matching.outbound, - key: hKey, - expiry: outboundTTL, - }) - } else if (leg.type === 'hop') { - log.info( - '[%s:h] STORED stop=%s hash=%s(subId=%s, block=%s #%s)', - msg.origin.chainId, - stop, - hKey, - msg.subscriptionId, - msg.origin.blockHash, - msg.origin.blockNumber - ) - await this.#hop.put(hKey, msg) - await this.#janitor.schedule({ - sublevel: prefixes.matching.hop, - key: hKey, - expiry: outboundTTL, - }) - } - } - } - - async #findRelayInbound(outMsg: XcmSent) { - const log = this.#log - const relayKey = outMsg.messageId - ? this.#matchingKey(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.messageId) - : this.#matchingKey(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.waypoint.messageHash) - - try { - const relayMsg = await this.#relay.get(relayKey) - log.info( - '[%s:r] RELAYED key=%s (subId=%s, block=%s #%s)', - outMsg.origin.chainId, - relayKey, - outMsg.subscriptionId, - outMsg.origin.blockHash, - outMsg.origin.blockNumber - ) - await this.#relay.del(relayKey) - await this.#onXcmRelayed(outMsg, relayMsg) - } catch { - // noop, it's possible that there are no relay subscriptions for an origin. - } - } - - async stop() { - await this.#mutex.waitForUnlock() - } - - /** - * Clears the pending states for a subcription. - * - * @param subscriptionId The subscription id. - */ - async clearPendingStates(subscriptionId: string) { - const prefix = subscriptionId + ':' - await this.#clearByPrefix(this.#inbound, prefix) - await this.#clearByPrefix(this.#outbound, prefix) - await this.#clearByPrefix(this.#relay, prefix) - await this.#clearByPrefix(this.#hop, prefix) - } - - async #clearByPrefix(sublevel: SubLevel, prefix: string) { - try { - const batch = sublevel.batch() - for await (const key of sublevel.keys({ gt: prefix })) { - if (key.startsWith(prefix)) { - batch.del(key) - } else { - break - } - } - await batch.write() - } catch (error) { - this.#log.error(error, 'while clearing prefix %s', prefix) - } - } - - #matchingKey(subscriptionId: string, chainId: string, messageId: string) { - // We add the subscription id as a discriminator - // to allow multiple subscriptions to the same messages - return `${subscriptionId}:${messageId}:${chainId}` - } - - #onXcmOutbound(outMsg: XcmSent) { - this.emit('telemetryOutbound', outMsg) - - try { - this.#xcmMatchedReceiver(outMsg) - } catch (e) { - this.#log.error(e, 'Error on notification') - } - } - - #onXcmMatched(outMsg: XcmSent, inMsg: XcmInbound) { - this.emit('telemetryMatched', inMsg, outMsg) - if (inMsg.assetsTrapped !== undefined) { - this.emit('telemetryTrapped', inMsg, outMsg) - } - - try { - const message: XcmReceived = new GenericXcmReceived(outMsg, inMsg) - this.#xcmMatchedReceiver(message) - } catch (e) { - this.#log.error(e, 'Error on notification') - } - } - - #onXcmRelayed(outMsg: XcmSent, relayMsg: XcmRelayedWithContext) { - const message: XcmRelayed = new GenericXcmRelayed(outMsg, relayMsg) - this.emit('telemetryRelayed', message) - - try { - this.#xcmMatchedReceiver(message) - } catch (e) { - this.#log.error(e, 'Error on notification') - } - } - - #onXcmHopOut(originMsg: XcmSent, hopMsg: XcmSent) { - try { - const { chainId, blockHash, blockNumber, event, outcome, error } = hopMsg.origin - const { instructions, messageData, messageHash, assetsTrapped } = hopMsg.waypoint - const currentLeg = hopMsg.legs[0] - const legIndex = originMsg.legs.findIndex((l) => l.from === currentLeg.from && l.to === currentLeg.to) - const waypointContext: XcmWaypointContext = { - legIndex, - chainId, - blockHash, - blockNumber, - event, - outcome, - error, - messageData, - messageHash, - instructions, - assetsTrapped, - } - const message: XcmHop = new GenericXcmHop(originMsg, waypointContext, 'out') - - this.emit('telemetryHop', message) - - this.#xcmMatchedReceiver(message) - } catch (e) { - this.#log.error(e, 'Error on notification') - } - } - - // NOTE: message data, hash and instructions are right for hop messages with 1 intermediate stop - // but will be wrong on the second or later hops for XCM with > 2 intermediate stops - // since we are not storing messages or contexts of intermediate hops - #onXcmHopIn(originMsg: XcmSent, hopMsg: XcmInbound) { - if (hopMsg.assetsTrapped !== undefined) { - this.emit('telemetryTrapped', hopMsg, originMsg) - } - - try { - const { chainId, blockHash, blockNumber, event, outcome, error, assetsTrapped } = hopMsg - const { messageData, messageHash, instructions } = originMsg.waypoint - const legIndex = originMsg.legs.findIndex((l) => l.to === chainId) - const waypointContext: XcmWaypointContext = { - legIndex, - chainId, - blockHash, - blockNumber, - event, - outcome, - error, - messageData, - messageHash, - instructions, - assetsTrapped, - } - const message: XcmHop = new GenericXcmHop(originMsg, waypointContext, 'in') - - this.emit('telemetryHop', message) - - this.#xcmMatchedReceiver(message) - } catch (e) { - this.#log.error(e, 'Error on notification') - } - } - - #onXcmBridgeAccepted(bridgeAcceptedMsg: XcmBridge) { - this.emit('telemetryBridge', bridgeAcceptedMsg) - try { - this.#xcmMatchedReceiver(bridgeAcceptedMsg) - } catch (e) { - this.#log.error(e, 'Error on notification') - } - } - - #onXcmBridgeDelivered(bridgeDeliveredMsg: XcmBridge) { - this.emit('telemetryBridge', bridgeDeliveredMsg) - try { - this.#xcmMatchedReceiver(bridgeDeliveredMsg) - } catch (e) { - this.#log.error(e, 'Error on notification') - } - } - - #onXcmBridgeMatched(bridgeOutMsg: XcmBridge, bridgeInMsg: XcmBridgeInboundWithContext) { - try { - const { chainId, blockHash, blockNumber, event, outcome, error, bridgeKey } = bridgeInMsg - const { messageData, messageHash, instructions } = bridgeOutMsg.waypoint - const legIndex = bridgeOutMsg.legs.findIndex((l) => l.to === chainId && l.type === 'bridge') - const waypointContext: XcmWaypointContext = { - legIndex, - chainId, - blockHash, - blockNumber: blockNumber.toString(), - event, - messageData, - messageHash, - instructions, - outcome, - error, - } - const bridgeMatched: XcmBridge = new GenericXcmBridge(bridgeOutMsg, waypointContext, { - bridgeMessageType: 'received', - bridgeKey, - forwardId: bridgeOutMsg.forwardId, - }) - - this.emit('telemetryBridge', bridgeMatched) - - this.#xcmMatchedReceiver(bridgeMatched) - } catch (e) { - this.#log.error(e, 'Error on notification') - } - } - - #onXcmSwept(task: JanitorTask, msg: string) { - try { - if (task.sublevel === prefixes.matching.outbound) { - const outMsg = JSON.parse(msg) as XcmSent - const message: XcmTimeout = new GenericXcmTimeout(outMsg) - this.#log.debug('TIMEOUT on key %s', task.key) - this.emit('telemetryTimeout', message) - this.#xcmMatchedReceiver(message) - } - } catch (e) { - this.#log.error(e, 'Error on notification') - } - } -} diff --git a/packages/server/src/agents/xcm/ops/bridge.spec.ts b/packages/server/src/agents/xcm/ops/bridge.spec.ts deleted file mode 100644 index b033d9dc..00000000 --- a/packages/server/src/agents/xcm/ops/bridge.spec.ts +++ /dev/null @@ -1,390 +0,0 @@ -import { jest } from '@jest/globals' -import { extractEvents, extractTxWithEvents } from '@sodazone/ocelloids-sdk' - -import { from } from 'rxjs' - -import { - bridgeInPolkadot, - bridgeOutAcceptedKusama, - bridgeOutDeliveredKusama, - registry, - relayHrmpReceiveKusama, - relayHrmpReceivePolkadot, - xcmpReceiveKusamaBridgeHub, - xcmpReceivePolkadotAssetHub, - xcmpSendKusamaAssetHub, - xcmpSendPolkadotBridgeHub, -} from '../../../testing/bridge/blocks.js' - -import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './pk-bridge.js' -import { extractRelayReceive } from './relay.js' -import { extractXcmpReceive, extractXcmpSend } from './xcmp.js' - -import { NetworkURN } from '../../types.js' -import { GenericXcmSentWithContext } from '../types.js' -import { mapXcmSent } from './common.js' -import { getMessageId } from './util.js' -import { fromXcmpFormat } from './xcm-format.js' - -describe('xcmp operator', () => { - describe('extractXcmpSend', () => { - it('should extract XCMP sent message on Kusama', (done) => { - const { origin, blocks, getHrmp } = xcmpSendKusamaAssetHub - - const calls = jest.fn() - - const test$ = extractXcmpSend(origin, getHrmp, registry)(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.instructions).toBeDefined() - expect(msg.messageData).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract XCMP sent message on Polkadot', (done) => { - const { origin, blocks, getHrmp } = xcmpSendPolkadotBridgeHub - - const calls = jest.fn() - - const test$ = extractXcmpSend(origin, getHrmp, registry)(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.instructions).toBeDefined() - expect(msg.messageData).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - }) - - describe('extractXcmpReceive', () => { - it('should extract XCMP receive with outcome success on Kusama', (done) => { - const calls = jest.fn() - - const test$ = extractXcmpReceive()(xcmpReceiveKusamaBridgeHub.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Success') - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract XCMP receive with outcome success on Polkadot', (done) => { - const calls = jest.fn() - - const test$ = extractXcmpReceive()(xcmpReceivePolkadotAssetHub.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Success') - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(2) - done() - }, - }) - }) - }) -}) - -describe('relay operator', () => { - describe('extractRelayReceive', () => { - it('should extract HRMP messages when they arrive on the Kusama relay chain', (done) => { - const { blocks, origin, messageControl, destination } = relayHrmpReceiveKusama - - const calls = jest.fn() - - const test$ = extractRelayReceive( - origin as NetworkURN, - messageControl, - registry - )(blocks.pipe(extractTxWithEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - expect(msg.recipient).toBe(destination) - expect(msg.extrinsicId).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Success') - expect(msg.error).toBeNull() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract HRMP messages when they arrive on the Polkadot relay chain', (done) => { - const { blocks, origin, messageControl, destination } = relayHrmpReceivePolkadot - - const calls = jest.fn() - - const test$ = extractRelayReceive( - origin as NetworkURN, - messageControl, - registry - )(blocks.pipe(extractTxWithEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - expect(msg.recipient).toBe(destination) - expect(msg.extrinsicId).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Success') - expect(msg.error).toBeNull() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(2) - done() - }, - }) - }) - }) -}) - -describe('bridge operator', () => { - describe('extractBridgeMessageAccepted', () => { - it('should extract accepted bridge messages on Bridge Hub', (done) => { - const { origin, destination, blocks, getStorage } = bridgeOutAcceptedKusama - - const calls = jest.fn() - - const test$ = extractBridgeMessageAccepted( - origin as NetworkURN, - registry, - getStorage - )(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - expect(msg.recipient).toBe(destination) - expect(msg.forwardId).toBeDefined() - expect(msg.messageId).toBeDefined() - expect(msg.bridgeKey).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - }) - - describe('extractBridgeMessageDelivered', () => { - it('should extract bridge message delivered event', (done) => { - const { origin, blocks } = bridgeOutDeliveredKusama - - const calls = jest.fn() - - const test$ = extractBridgeMessageDelivered(origin as NetworkURN, registry)(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.chainId).toBeDefined() - expect(msg.chainId).toBe(origin) - expect(msg.bridgeKey).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - }) - - describe('extractBridgeReceive', () => { - it('should extract bridge message receive events when message arrives on receving Bridge Hub', (done) => { - const { origin, blocks } = bridgeInPolkadot - - const calls = jest.fn() - const test$ = extractBridgeReceive(origin as NetworkURN)(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.chainId).toBeDefined() - expect(msg.chainId).toBe(origin) - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Success') - expect(msg.error).toBeNull() - expect(msg.bridgeKey).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - }) -}) - -describe('mapXcmSent', () => { - it('should extract stops for XCM with ExportMessage instruction', (done) => { - const calls = jest.fn() - - const ksmAHBridge = - '0003180004000100000740568a4a5f13000100000740568a4a5f0026020100a10f1401040002010903000700e87648170a130002010903000700e8764817000d010204000101002cb783d5c0ddcccd2608c83d43ee6fc19320408c24764c2f8ac164b27beaee372cf7d2f132944c0c518b4c862d6e68030f0ba49808125a805a11a9ede30d0410ab140d0100010100a10f2c4b422c686214e14cc3034a661b6a01dfd2c9a811ef9ec20ef798d0e687640e6d' - const buf = new Uint8Array(Buffer.from(ksmAHBridge, 'hex')) - - const xcms = fromXcmpFormat(buf, registry) - const test$ = mapXcmSent( - 'test-sub', - registry, - 'urn:ocn:kusama:1000' - )( - from( - xcms.map( - (x) => - new GenericXcmSentWithContext({ - event: {}, - sender: { signer: { id: 'xyz', publicKey: '0x01' }, extraSigners: [] }, - blockHash: '0x01', - blockNumber: '32', - extrinsicId: '32-4', - recipient: 'urn:ocn:kusama:1002', - messageData: buf, - messageHash: x.hash.toHex(), - messageId: getMessageId(x), - instructions: { - bytes: x.toU8a(), - json: x.toHuman(), - }, - }) - ) - ) - ) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.waypoint.chainId).toBe('urn:ocn:kusama:1000') - expect(msg.legs.length).toBe(3) - expect(msg.destination.chainId).toBe('urn:ocn:polkadot:1000') - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract stops for bridged XCM', (done) => { - const calls = jest.fn() - - const dotBridgeHubXcmOut = - '0003200b0104352509030b0100a10f01040002010903000700e87648170a130002010903000700e8764817000d010204000101002cb783d5c0ddcccd2608c83d43ee6fc19320408c24764c2f8ac164b27beaee372cf7d2f132944c0c518b4c862d6e68030f0ba49808125a805a11a9ede30d0410ab' - const buf = new Uint8Array(Buffer.from(dotBridgeHubXcmOut, 'hex')) - - const xcms = fromXcmpFormat(buf, registry) - const test$ = mapXcmSent( - 'test-sub', - registry, - 'urn:ocn:polkadot:1002' - )( - from( - xcms.map( - (x) => - new GenericXcmSentWithContext({ - event: {}, - sender: { signer: { id: 'xyz', publicKey: '0x01' }, extraSigners: [] }, - blockHash: '0x01', - blockNumber: '32', - extrinsicId: '32-4', - recipient: 'urn:ocn:polkadot:1000', - messageData: buf, - messageHash: x.hash.toHex(), - messageId: getMessageId(x), - instructions: { - bytes: x.toU8a(), - json: x.toHuman(), - }, - }) - ) - ) - ) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.waypoint.chainId).toBe('urn:ocn:polkadot:1002') - expect(msg.legs.length).toBe(1) - expect(msg.destination.chainId).toBe('urn:ocn:polkadot:1000') - expect(msg.forwardId).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) -}) diff --git a/packages/server/src/agents/xcm/ops/common.spec.ts b/packages/server/src/agents/xcm/ops/common.spec.ts deleted file mode 100644 index c3d01130..00000000 --- a/packages/server/src/agents/xcm/ops/common.spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { jest } from '@jest/globals' - -import { from, of } from 'rxjs' - -import { registry } from '../../../testing/xcm.js' -import { GenericXcmSentWithContext } from '../types' -import { mapXcmSent } from './common' -import { getMessageId } from './util' -import { asVersionedXcm, fromXcmpFormat } from './xcm-format' - -describe('extract waypoints operator', () => { - describe('mapXcmSent', () => { - it('should extract stops for a V2 XCM message without hops', (done) => { - const calls = jest.fn() - - const moon5531424 = - '0002100004000000001700004b3471bb156b050a13000000001700004b3471bb156b05010300286bee0d010004000101001e08eb75720cb63fbfcbe7237c6d9b7cf6b4953518da6b38731d5bc65b9ffa32021000040000000017206d278c7e297945030a130000000017206d278c7e29794503010300286bee0d010004000101000257fd81d0a71b094c2c8d3e6c93a9b01a31a43d38408bb2c4c2b49a4c58eb01' - const buf = new Uint8Array(Buffer.from(moon5531424, 'hex')) - - const xcms = fromXcmpFormat(buf, registry) - const test$ = mapXcmSent( - 'test-sub', - registry, - 'urn:ocn:local:2004' - )( - from( - xcms.map( - (x) => - new GenericXcmSentWithContext({ - event: {}, - sender: { signer: { id: 'xyz', publicKey: '0x01' }, extraSigners: [] }, - blockHash: '0x01', - blockNumber: '32', - extrinsicId: '32-4', - recipient: 'urn:ocn:local:2104', - messageData: buf, - messageHash: x.hash.toHex(), - messageId: getMessageId(x), - instructions: { - bytes: x.toU8a(), - json: x.toHuman(), - }, - }) - ) - ) - ) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.waypoint.chainId).toBe('urn:ocn:local:2004') - expect(msg.legs.length).toBe(1) - expect(msg.legs[0]).toEqual({ - from: 'urn:ocn:local:2004', - to: 'urn:ocn:local:2104', - relay: 'urn:ocn:local:0', - type: 'hrmp', - }) - expect(msg.destination.chainId).toBe('urn:ocn:local:2104') - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(2) - done() - }, - }) - }) - - it('should extract stops for a XCM message hopping with InitiateReserveWithdraw', (done) => { - const calls = jest.fn() - - const polka19505060 = - '0310000400010300a10f043205011f000700f2052a011300010300a10f043205011f000700f2052a010010010204010100a10f0813000002043205011f0002093d00000d0102040001010081bd2c1d40052682633fb3e67eff151b535284d1d1a9633613af14006656f42b2c8e75728b841da22d8337ff5fadd1264f13addcdee755b01ce1a3afb9ef629b9a' - const buf = new Uint8Array(Buffer.from(polka19505060, 'hex')) - - const xcm = asVersionedXcm(buf, registry) - const test$ = mapXcmSent( - 'test-sub', - registry, - 'urn:ocn:local:0' - )( - of( - new GenericXcmSentWithContext({ - event: {}, - sender: { signer: { id: 'xyz', publicKey: '0x01' }, extraSigners: [] }, - blockHash: '0x01', - blockNumber: '32', - extrinsicId: '32-4', - recipient: 'urn:ocn:local:2034', - messageData: buf, - messageHash: xcm.hash.toHex(), - messageId: getMessageId(xcm), - instructions: { - bytes: xcm.toU8a(), - json: xcm.toHuman(), - }, - }) - ) - ) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.waypoint.chainId).toBe('urn:ocn:local:0') - expect(msg.legs.length).toBe(2) - expect(msg.legs[0]).toEqual({ - from: 'urn:ocn:local:0', - to: 'urn:ocn:local:2034', - type: 'hop', - }) - expect(msg.legs[1]).toEqual({ - from: 'urn:ocn:local:2034', - to: 'urn:ocn:local:1000', - relay: 'urn:ocn:local:0', - type: 'hop', - }) - expect(msg.destination.chainId).toBe('urn:ocn:local:1000') - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract stops for a XCM message hopping with DepositReserveAsset', (done) => { - const calls = jest.fn() - - const heiko5389341 = - '0003100004000000000f251850c822be030a13000000000f120c286411df01000e010204010100411f081300010100511f000f120c286411df01000d01020400010100842745b99b8042d28a7c677d9469332bfc24aa5266c7ec57c43c7af125a0c16c' - const buf = new Uint8Array(Buffer.from(heiko5389341, 'hex')) - - const xcms = fromXcmpFormat(buf, registry) - const test$ = mapXcmSent( - 'test-sub', - registry, - 'urn:ocn:local:2085' - )( - from( - xcms.map( - (x) => - new GenericXcmSentWithContext({ - event: {}, - sender: { signer: { id: 'xyz', publicKey: '0x01' }, extraSigners: [] }, - blockHash: '0x01', - blockNumber: '32', - extrinsicId: '32-4', - recipient: 'urn:ocn:local:2004', - messageData: buf, - messageHash: x.hash.toHex(), - messageId: getMessageId(x), - instructions: { - bytes: x.toU8a(), - json: x.toHuman(), - }, - }) - ) - ) - ) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.waypoint.chainId).toBe('urn:ocn:local:2085') - - expect(msg.legs.length).toBe(2) - expect(msg.legs[0]).toEqual({ - from: 'urn:ocn:local:2085', - to: 'urn:ocn:local:2004', - relay: 'urn:ocn:local:0', - type: 'hop', - }) - expect(msg.legs[1]).toEqual({ - from: 'urn:ocn:local:2004', - to: 'urn:ocn:local:2000', - relay: 'urn:ocn:local:0', - type: 'hop', - }) - - expect(msg.destination.chainId).toBe('urn:ocn:local:2000') - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - }) -}) diff --git a/packages/server/src/agents/xcm/ops/common.ts b/packages/server/src/agents/xcm/ops/common.ts deleted file mode 100644 index 7661f211..00000000 --- a/packages/server/src/agents/xcm/ops/common.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { Observable, map } from 'rxjs' - -import type { XcmV2Xcm, XcmV3Instruction, XcmV3Xcm } from '@polkadot/types/lookup' -import type { Registry } from '@polkadot/types/types' -import { hexToU8a, stringToU8a, u8aConcat } from '@polkadot/util' -import { blake2AsHex } from '@polkadot/util-crypto' - -import { types } from '@sodazone/ocelloids-sdk' -import { createNetworkId, getChainId, getConsensus, isOnSameConsensus } from '../../../services/config.js' -import { AnyJson, HexString } from '../../../services/monitoring/types.js' -import { NetworkURN } from '../../../services/types.js' -import { GenericXcmSent, Leg, XcmSent, XcmSentWithContext } from '../types.js' -import { - getBridgeHubNetworkId, - getParaIdFromJunctions, - getSendersFromEvent, - networkIdFromMultiLocation, -} from './util.js' -import { asVersionedXcm } from './xcm-format.js' -import { XcmV4Instruction, XcmV4Xcm } from './xcm-types.js' - -// eslint-disable-next-line complexity -function recursiveExtractStops(origin: NetworkURN, instructions: XcmV2Xcm | XcmV3Xcm | XcmV4Xcm, stops: NetworkURN[]) { - for (const instruction of instructions) { - let nextStop - let message - - if (instruction.isDepositReserveAsset) { - const { dest, xcm } = instruction.asDepositReserveAsset - nextStop = dest - message = xcm - } else if (instruction.isInitiateReserveWithdraw) { - const { reserve, xcm } = instruction.asInitiateReserveWithdraw - nextStop = reserve - message = xcm - } else if (instruction.isInitiateTeleport) { - const { dest, xcm } = instruction.asInitiateTeleport - nextStop = dest - message = xcm - } else if (instruction.isTransferReserveAsset) { - const { dest, xcm } = instruction.asTransferReserveAsset - nextStop = dest - message = xcm - } else if ((instruction as XcmV3Instruction | XcmV4Instruction).isExportMessage) { - const { network, destination, xcm } = (instruction as XcmV3Instruction | XcmV4Instruction).asExportMessage - const paraId = getParaIdFromJunctions(destination) - if (paraId) { - const consensus = network.toString().toLowerCase() - const networkId = createNetworkId(consensus, paraId) - const bridgeHubNetworkId = getBridgeHubNetworkId(consensus) - // We assume that an ExportMessage will always go through Bridge Hub - if (bridgeHubNetworkId !== undefined && networkId !== bridgeHubNetworkId) { - stops.push(bridgeHubNetworkId) - } - stops.push(networkId) - recursiveExtractStops(networkId, xcm, stops) - } - } - - if (nextStop !== undefined && message !== undefined) { - const networkId = networkIdFromMultiLocation(nextStop, origin) - - if (networkId) { - stops.push(networkId) - recursiveExtractStops(networkId, message, stops) - } - } - } - - return stops -} - -function constructLegs(origin: NetworkURN, stops: NetworkURN[]) { - const legs: Leg[] = [] - const nodes = [origin].concat(stops) - for (let i = 0; i < nodes.length - 1; i++) { - const from = nodes[i] - const to = nodes[i + 1] - const leg = { - from, - to, - type: 'vmp', - } as Leg - - if (getConsensus(from) === getConsensus(to)) { - if (getChainId(from) !== '0' && getChainId(to) !== '0') { - leg.relay = createNetworkId(from, '0') - leg.type = 'hrmp' - } - } else { - leg.type = 'bridge' - } - - legs.push(leg) - } - - if (legs.length === 1) { - return legs - } - - for (let i = 0; i < legs.length - 1; i++) { - const leg1 = legs[i] - const leg2 = legs[i + 1] - if (isOnSameConsensus(leg1.from, leg2.to)) { - leg1.type = 'hop' - leg2.type = 'hop' - } - } - - if (legs.length === 1) { - return legs - } - - for (let i = 0; i < legs.length - 1; i++) { - const leg1 = legs[i] - const leg2 = legs[i + 1] - if (isOnSameConsensus(leg1.from, leg2.to)) { - leg1.type = 'hop' - leg2.type = 'hop' - } - } - - return legs -} - -/** - * Maps a XcmSentWithContext to a XcmSent message. - * Sets the destination as the final stop after recursively extracting all stops from the XCM message, - * constructs the legs for the message and constructs the waypoint context. - * - * @param id subscription ID - * @param registry type registry - * @param origin origin network URN - * @returns Observable - */ -export function mapXcmSent(id: string, registry: Registry, origin: NetworkURN) { - return (source: Observable): Observable => - source.pipe( - map((message) => { - const { instructions, recipient } = message - const stops: NetworkURN[] = [recipient] - const versionedXcm = asVersionedXcm(instructions.bytes, registry) - recursiveExtractStops(origin, versionedXcm[`as${versionedXcm.type}`], stops) - const legs = constructLegs(origin, stops) - - let forwardId: HexString | undefined - // TODO: extract to util? - if (origin === getBridgeHubNetworkId(origin) && message.messageId !== undefined) { - const constant = 'forward_id_for' - const derivedIdBuf = u8aConcat(stringToU8a(constant), hexToU8a(message.messageId)) - forwardId = blake2AsHex(derivedIdBuf) - } - return new GenericXcmSent(id, origin, message, legs, forwardId) - }) - ) -} - -export function blockEventToHuman(event: types.BlockEvent): AnyJson { - return { - extrinsicPosition: event.extrinsicPosition, - extrinsicId: event.extrinsicId, - blockNumber: event.blockNumber.toNumber(), - blockHash: event.blockHash.toHex(), - blockPosition: event.blockPosition, - eventId: event.eventId, - data: event.data.toHuman(), - index: event.index.toHuman(), - meta: event.meta.toHuman(), - method: event.method, - section: event.section, - } as AnyJson -} - -export function xcmMessagesSent() { - return (source: Observable): Observable => { - return source.pipe( - map((event) => { - const xcmMessage = event.data as any - return { - event: blockEventToHuman(event), - sender: getSendersFromEvent(event), - blockHash: event.blockHash.toHex(), - blockNumber: event.blockNumber.toPrimitive(), - extrinsicId: event.extrinsicId, - messageHash: xcmMessage.messageHash?.toHex(), - messageId: xcmMessage.messageId?.toHex(), - } as XcmSentWithContext - }) - ) - } -} diff --git a/packages/server/src/agents/xcm/ops/criteria.ts b/packages/server/src/agents/xcm/ops/criteria.ts deleted file mode 100644 index 9a991bfe..00000000 --- a/packages/server/src/agents/xcm/ops/criteria.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ControlQuery, Criteria } from '@sodazone/ocelloids-sdk' -import { XcmSent } from 'agents/xcm/types.js' -import { SignerData } from '../../../services/monitoring/types.js' -import { NetworkURN } from '../../../services/types.js' - -export function sendersCriteria(senders?: string[] | '*'): Criteria { - if (senders === undefined || senders === '*') { - // match any - return {} - } else { - return { - $or: [ - { 'sender.signer.id': { $in: senders } }, - { 'sender.signer.publicKey': { $in: senders } }, - { 'sender.extraSigners.id': { $in: senders } }, - { 'sender.extraSigners.publicKey': { $in: senders } }, - ], - } - } -} - -// Assuming we are in the same consensus -export function messageCriteria(recipients: NetworkURN[]): Criteria { - return { - recipient: { $in: recipients }, - } -} - -/** - * Matches sender account address and public keys, including extra senders. - */ -export function matchSenders(query: ControlQuery, sender?: SignerData): boolean { - if (sender === undefined) { - return query.value.test({ - sender: undefined, - }) - } - - return query.value.test({ - sender, - }) -} - -/** - * Matches outbound XCM recipients. - */ -export function matchMessage(query: ControlQuery, xcm: XcmSent): boolean { - return query.value.test({ recipient: xcm.destination.chainId }) -} diff --git a/packages/server/src/agents/xcm/ops/dmp.spec.ts b/packages/server/src/agents/xcm/ops/dmp.spec.ts deleted file mode 100644 index 9d7bb522..00000000 --- a/packages/server/src/agents/xcm/ops/dmp.spec.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { jest } from '@jest/globals' - -import { of } from 'rxjs' - -import { extractEvents, extractTxWithEvents } from '@sodazone/ocelloids-sdk' -import { - dmpReceive, - dmpSendMultipleMessagesInQueue, - dmpSendSingleMessageInQueue, - dmpXcmPalletSentEvent, - registry, - xcmHop, - xcmHopOrigin, -} from '../../../testing/xcm.js' -import { extractDmpReceive, extractDmpSend, extractDmpSendByEvent } from './dmp.js' - -const getDmp = () => - of([ - { - msg: new Uint8Array(Buffer.from('0002100004000000001700004b3471bb156b050a1300000000', 'hex')), - toU8a: () => new Uint8Array(Buffer.from('0002100004000000001700004b3471bb156b050a1300000000', 'hex')), - }, - ] as unknown as any) - -describe('dmp operator', () => { - describe('extractDmpSend', () => { - it('should extract DMP sent message', (done) => { - const { origin, blocks } = dmpSendSingleMessageInQueue - - const calls = jest.fn() - - const test$ = extractDmpSend(origin, getDmp, registry)(blocks.pipe(extractTxWithEvents())) - - test$.subscribe({ - next: (msg) => { - calls() - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.instructions).toBeDefined() - expect(msg.messageData).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract DMP sent for multi-leg messages', (done) => { - const { origin, blocks } = xcmHopOrigin - - const calls = jest.fn() - - const test$ = extractDmpSendByEvent(origin, getDmp, registry)(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.instructions).toBeDefined() - expect(msg.messageData).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract DMP sent message with multiple messages in the queue', (done) => { - const { origin, blocks } = dmpSendMultipleMessagesInQueue - - const calls = jest.fn() - - const test$ = extractDmpSend(origin, getDmp, registry)(blocks.pipe(extractTxWithEvents())) - - test$.subscribe({ - next: (msg) => { - calls() - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.instructions).toBeDefined() - expect(msg.messageData).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - }) - - describe('extractDmpSendByEvent', () => { - it('should extract DMP sent message filtered by event', (done) => { - const { origin, blocks } = dmpXcmPalletSentEvent - - const calls = jest.fn() - - const test$ = extractDmpSendByEvent(origin, getDmp, registry)(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - calls() - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.instructions).toBeDefined() - expect(msg.messageData).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - }) - - describe('extractDmpReceive', () => { - it('should extract DMP received message with outcome success', (done) => { - const { successBlocks } = dmpReceive - - const calls = jest.fn() - - const test$ = extractDmpReceive()(successBlocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - calls() - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Success') - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract failed DMP received message with error', (done) => { - const { failBlocks } = dmpReceive - - const calls = jest.fn() - - const test$ = extractDmpReceive()(failBlocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - calls() - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Fail') - expect(msg.error).toBeDefined() - expect(msg.error).toBe('UntrustedReserveLocation') - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract dmp receive with asset trap', (done) => { - const { trappedBlocks } = dmpReceive - - const calls = jest.fn() - - const test$ = extractDmpReceive()(trappedBlocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Fail') - expect(msg.error).toBeDefined() - expect(msg.error).toBe('FailedToTransactAsset') - expect(msg.assetsTrapped).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract DMP received for hop message', (done) => { - const { blocks } = xcmHop - - const calls = jest.fn() - - const test$ = extractDmpReceive()(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Success') - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - }) -}) diff --git a/packages/server/src/agents/xcm/ops/dmp.ts b/packages/server/src/agents/xcm/ops/dmp.ts deleted file mode 100644 index a96d94d2..00000000 --- a/packages/server/src/agents/xcm/ops/dmp.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { Observable, bufferCount, filter, map, mergeMap } from 'rxjs' - -// NOTE: we use Polkadot augmented types -import '@polkadot/api-augment/polkadot' -import type { Compact } from '@polkadot/types' -import type { IU8a } from '@polkadot/types-codec/types' -import type { BlockNumber } from '@polkadot/types/interfaces' -import type { Outcome } from '@polkadot/types/interfaces/xcm' -import type { Registry } from '@polkadot/types/types' - -import { filterNonNull, types } from '@sodazone/ocelloids-sdk' - -import { AnyJson, SignerData } from '../../../services/monitoring/types.js' -import { NetworkURN } from '../../../services/types.js' -import { GetDownwardMessageQueues } from '../types-augmented.js' -import { - GenericXcmInboundWithContext, - GenericXcmSentWithContext, - XcmInboundWithContext, - XcmSentWithContext, -} from '../types.js' -import { blockEventToHuman } from './common.js' -import { - getMessageId, - getSendersFromEvent, - getSendersFromExtrinsic, - mapAssetsTrapped, - matchEvent, - matchExtrinsic, - matchProgramByTopic, - networkIdFromMultiLocation, - networkIdFromVersionedMultiLocation, -} from './util.js' -import { asVersionedXcm } from './xcm-format.js' -import { XcmVersionedAssets, XcmVersionedLocation, XcmVersionedXcm } from './xcm-types.js' - -/* - ================================================================================== - NOTICE - ================================================================================== - - This DMP message matching implementation is provisional and will be replaced - as soon as possible. - - For details see: /~https://github.com/paritytech/polkadot-sdk/issues/1905 -*/ - -type Json = { [property: string]: Json } -type XcmContext = { - recipient: NetworkURN - data: Uint8Array - program: XcmVersionedXcm - blockHash: IU8a - blockNumber: Compact - sender?: SignerData - event?: types.BlockEvent -} - -// eslint-disable-next-line complexity -function matchInstructions( - xcmProgram: XcmVersionedXcm, - assets: XcmVersionedAssets, - beneficiary: XcmVersionedLocation -): boolean { - const program = xcmProgram.value.toHuman() as Json[] - let sameAssetFun = false - let sameBeneficiary = false - - for (const instruction of program) { - const { DepositAsset, ReceiveTeleportedAsset, ReserveAssetDeposited } = instruction - - if (ReceiveTeleportedAsset || ReserveAssetDeposited) { - const fun = ReceiveTeleportedAsset?.[0]?.fun ?? ReserveAssetDeposited[0]?.fun - if (fun) { - const asset = assets.value.toHuman() as Json - sameAssetFun = JSON.stringify(fun) === JSON.stringify(asset[0]?.fun) - } - continue - } - - if (DepositAsset) { - sameBeneficiary = JSON.stringify(DepositAsset.beneficiary) === JSON.stringify(beneficiary.value.toHuman()) - break - } - } - - return sameAssetFun && sameBeneficiary -} - -function createXcmMessageSent({ - recipient, - data, - program, - blockHash, - blockNumber, - sender, - event, -}: XcmContext): GenericXcmSentWithContext { - const messageId = getMessageId(program) - - return new GenericXcmSentWithContext({ - blockHash: blockHash.toHex(), - blockNumber: blockNumber.toPrimitive(), - event: event ? blockEventToHuman(event) : {}, - recipient, - instructions: { - bytes: program.toU8a(), - json: program.toHuman(), - }, - messageData: data, - messageHash: program.hash.toHex(), - messageId, - sender, - }) -} - -// Will be obsolete after DMP refactor: -// /~https://github.com/paritytech/polkadot-sdk/pull/1246 -function findDmpMessagesFromTx(getDmp: GetDownwardMessageQueues, registry: Registry, origin: NetworkURN) { - return (source: Observable): Observable => { - return source.pipe( - map((tx) => { - const dest = tx.extrinsic.args[0] as XcmVersionedLocation - const beneficiary = tx.extrinsic.args[1] as XcmVersionedLocation - const assets = tx.extrinsic.args[2] as XcmVersionedAssets - - const recipient = networkIdFromVersionedMultiLocation(dest, origin) - - if (recipient) { - return { - tx, - recipient, - beneficiary, - assets, - } - } - - return null - }), - filterNonNull(), - mergeMap(({ tx, recipient, beneficiary, assets }) => { - return getDmp(tx.extrinsic.blockHash.toHex(), recipient).pipe( - map((messages) => { - const { blockHash, blockNumber } = tx.extrinsic - if (messages.length === 1) { - const data = messages[0].msg - const program = asVersionedXcm(data, registry) - return createXcmMessageSent({ - blockHash, - blockNumber, - recipient, - data, - program, - sender: getSendersFromExtrinsic(tx.extrinsic), - }) - } else { - // XXX Temporary matching heuristics until DMP message - // sent event is implemented. - // Only matches the first message found. - for (const message of messages) { - const data = message.msg - const program = asVersionedXcm(data, registry) - if (matchInstructions(program, assets, beneficiary)) { - return createXcmMessageSent({ - blockHash, - blockNumber, - recipient, - data, - program, - sender: getSendersFromExtrinsic(tx.extrinsic), - }) - } - } - - return null - } - }), - filterNonNull() - ) - }) - ) - } -} - -function findDmpMessagesFromEvent(origin: NetworkURN, getDmp: GetDownwardMessageQueues, registry: Registry) { - return (source: Observable): Observable => { - return source.pipe( - map((event) => { - if (matchEvent(event, 'xcmPallet', 'Sent')) { - const { destination, messageId } = event.data as any - const recipient = networkIdFromMultiLocation(destination, origin) - - if (recipient) { - return { - recipient, - messageId, - event, - } - } - } - - return null - }), - filterNonNull(), - mergeMap(({ recipient, messageId, event }) => { - return getDmp(event.blockHash.toHex(), recipient as NetworkURN).pipe( - map((messages) => { - const { blockHash, blockNumber } = event - if (messages.length === 1) { - const data = messages[0].msg - const program = asVersionedXcm(data, registry) - return createXcmMessageSent({ - blockHash, - blockNumber, - recipient, - event, - data, - program, - sender: getSendersFromEvent(event), - }) - } else { - // Since we are matching by topic and it is assumed that the TopicId is unique - // we can break out of the loop on first matching message found. - for (const message of messages) { - const data = message.msg - const program = asVersionedXcm(data, registry) - if (matchProgramByTopic(program, messageId)) { - return createXcmMessageSent({ - blockHash, - blockNumber, - recipient, - event, - data, - program, - sender: getSendersFromEvent(event), - }) - } - } - - return null - } - }), - filterNonNull() - ) - }) - ) - } -} - -const METHODS_DMP = ['limitedReserveTransferAssets', 'reserveTransferAssets', 'limitedTeleportAssets', 'teleportAssets'] - -export function extractDmpSend(origin: NetworkURN, getDmp: GetDownwardMessageQueues, registry: Registry) { - return (source: Observable): Observable => { - return source.pipe( - filter((tx) => { - const { extrinsic } = tx - return tx.dispatchError === undefined && matchExtrinsic(extrinsic, 'xcmPallet', METHODS_DMP) - }), - findDmpMessagesFromTx(getDmp, registry, origin) - ) - } -} - -export function extractDmpSendByEvent(origin: NetworkURN, getDmp: GetDownwardMessageQueues, registry: Registry) { - return (source: Observable): Observable => { - return source.pipe(findDmpMessagesFromEvent(origin, getDmp, registry)) - } -} - -function extractXcmError(outcome: Outcome) { - if (outcome.isIncomplete) { - const [_, err] = outcome.asIncomplete - return err.type.toString() - } - if (outcome.isError) { - return outcome.asError.type.toString() - } - return undefined -} - -function createDmpReceivedWithContext(event: types.BlockEvent, assetsTrappedEvent?: types.BlockEvent) { - const xcmMessage = event.data as any - let outcome: 'Success' | 'Fail' = 'Fail' - let error: AnyJson - if (xcmMessage.outcome !== undefined) { - const o = xcmMessage.outcome as Outcome - outcome = o.isComplete ? 'Success' : 'Fail' - error = extractXcmError(o) - } else if (xcmMessage.success !== undefined) { - outcome = xcmMessage.success ? 'Success' : 'Fail' - } - - const messageId = xcmMessage.messageId ? xcmMessage.messageId.toHex() : xcmMessage.id.toHex() - const messageHash = xcmMessage.messageHash?.toHex() ?? messageId - const assetsTrapped = mapAssetsTrapped(assetsTrappedEvent) - - return new GenericXcmInboundWithContext({ - event: blockEventToHuman(event), - blockHash: event.blockHash.toHex(), - blockNumber: event.blockNumber.toPrimitive(), - extrinsicId: event.extrinsicId, - messageHash, - messageId, - outcome, - error, - assetsTrapped, - }) -} - -export function extractDmpReceive() { - return (source: Observable): Observable => { - return source.pipe( - bufferCount(2, 1), - map(([maybeAssetTrapEvent, maybeDmpEvent]) => { - // in reality we expect a continuous stream of events but - // in tests, maybeDmpEvent could be undefined if there are odd number of events - if ( - maybeDmpEvent && - (matchEvent(maybeDmpEvent, 'dmpQueue', 'ExecutedDownward') || - matchEvent(maybeDmpEvent, 'messageQueue', 'Processed')) - ) { - const assetTrapEvent = matchEvent(maybeAssetTrapEvent, 'polkadotXcm', 'AssetsTrapped') - ? maybeAssetTrapEvent - : undefined - return createDmpReceivedWithContext(maybeDmpEvent, assetTrapEvent) - } - return null - }), - filterNonNull() - ) - } -} diff --git a/packages/server/src/agents/xcm/ops/pk-bridge.ts b/packages/server/src/agents/xcm/ops/pk-bridge.ts deleted file mode 100644 index 4eb98154..00000000 --- a/packages/server/src/agents/xcm/ops/pk-bridge.ts +++ /dev/null @@ -1,206 +0,0 @@ -import type { Bytes, Vec } from '@polkadot/types-codec' -import type { Registry } from '@polkadot/types/types' -import { compactFromU8aLim, hexToU8a, stringToU8a, u8aConcat, u8aToHex } from '@polkadot/util' -import { blake2AsU8a } from '@polkadot/util-crypto' -import { blake2AsHex } from '@polkadot/util-crypto' -import { Observable, filter, from, mergeMap } from 'rxjs' - -import { types } from '@sodazone/ocelloids-sdk' - -import { getConsensus } from '../../../services/config.js' -import { bridgeStorageKeys } from '../../../services/monitoring/storage.js' -import { HexString } from '../../../services/monitoring/types.js' -import { NetworkURN } from '../../../services/types.js' -import { GetStorageAt } from '../types-augmented.js' -import { - GenericXcmBridgeAcceptedWithContext, - GenericXcmBridgeDeliveredWithContext, - GenericXcmBridgeInboundWithContext, - XcmBridgeAcceptedWithContext, - XcmBridgeDeliveredWithContext, - XcmBridgeInboundWithContext, -} from '../types.js' -import { blockEventToHuman } from './common.js' -import { getMessageId, getSendersFromEvent, matchEvent, networkIdFromInteriorLocation } from './util.js' -import { - BpMessagesReceivedMessages, - BridgeMessage, - BridgeMessageAccepted, - BridgeMessagesDelivered, -} from './xcm-types.js' - -export function extractBridgeMessageAccepted(origin: NetworkURN, registry: Registry, getStorage: GetStorageAt) { - return (source: Observable): Observable => { - return source.pipe( - filter((event) => - matchEvent( - event, - ['bridgePolkadotMessages', 'bridgeKusamaMessages', 'bridgeRococoMessages', 'bridgeWestendMessages'], - 'MessageAccepted' - ) - ), - mergeMap((blockEvent) => { - const data = blockEvent.data as unknown as BridgeMessageAccepted - const laneId = data.laneId.toU8a() - const nonce = data.nonce.toU8a() - const consensus = getConsensus(origin) - const { messagesOutboundPartial } = bridgeStorageKeys[consensus] - - const value = u8aConcat(laneId, nonce) - - const arg = u8aConcat(blake2AsU8a(value, 128), value) - const key = (messagesOutboundPartial + Buffer.from(arg).toString('hex')) as HexString - - return getStorage(blockEvent.blockHash.toHex(), key).pipe( - mergeMap((buf) => { - // if registry does not have needed types, register them - if (!registry.hasType('BridgeMessage')) { - registry.register({ - VersionedInteriorLocation: { - _enum: { - V0: null, - V1: null, - V2: 'XcmV2MultilocationJunctions', - V3: 'XcmV3Junctions', - }, - }, - BridgeMessage: { - universal_dest: 'VersionedInteriorLocation', - message: 'XcmVersionedXcm', - }, - }) - } - - const msgs: XcmBridgeAcceptedWithContext[] = [] - - // we use the length of the u8 array instead of Option - // since the value is bare. - if (buf.length > 1) { - const bytes = registry.createType('Bytes', buf) as unknown as Bytes - let baseOffset = 0 - - while (baseOffset < bytes.length) { - const [offset, length] = compactFromU8aLim(bytes.slice(baseOffset)) - const increment = offset + length - const msgBuf = bytes.slice(baseOffset + offset, baseOffset + increment) - baseOffset += increment - - const bridgeMessage = registry.createType('BridgeMessage', msgBuf) as unknown as BridgeMessage - const recipient = networkIdFromInteriorLocation(bridgeMessage.universal_dest) - if (recipient === undefined) { - continue - } - const xcmProgram = bridgeMessage.message - const messageId = getMessageId(xcmProgram) - let forwardId: HexString | undefined - if (messageId !== undefined) { - const constant = 'forward_id_for' - const derivedIdBuf = u8aConcat(stringToU8a(constant), hexToU8a(messageId)) - forwardId = blake2AsHex(derivedIdBuf) - } - - const xcmBridgeSent = new GenericXcmBridgeAcceptedWithContext({ - event: blockEvent.toHuman(), - blockHash: blockEvent.blockHash.toHex(), - blockNumber: blockEvent.blockNumber.toPrimitive(), - extrinsicId: blockEvent.extrinsicId, - messageData: xcmProgram.toHex(), - recipient, - messageHash: xcmProgram.hash.toHex(), - instructions: xcmProgram.toHuman(), - messageId, - forwardId, - bridgeKey: u8aToHex(hexToU8a(key).slice(48)), - chainId: origin, - }) - - msgs.push(xcmBridgeSent) - } - } - return from(msgs) - }) - ) - }) - ) - } -} - -export function extractBridgeMessageDelivered(origin: NetworkURN, registry: Registry) { - return (source: Observable): Observable => { - return source.pipe( - filter((event) => - matchEvent( - event, - ['bridgePolkadotMessages', 'bridgeKusamaMessages', 'bridgeRococoMessages', 'bridgeWestendMessages'], - 'MessagesDelivered' - ) - ), - mergeMap((blockEvent) => { - const data = blockEvent.data as unknown as BridgeMessagesDelivered - const begin = data.messages.begin.toNumber() - const end = data.messages.end.toNumber() - const laneId = data.laneId.toU8a() - const msgs: XcmBridgeDeliveredWithContext[] = [] - - for (let i = begin; i <= end; i++) { - const nonce = registry.createType('u64', i) - const value = u8aConcat(laneId, nonce.toU8a()) - - const bridgeKey = u8aToHex(value) - msgs.push( - new GenericXcmBridgeDeliveredWithContext({ - chainId: origin, - bridgeKey, - event: blockEventToHuman(blockEvent), - extrinsicId: blockEvent.extrinsicId, - blockNumber: blockEvent.blockNumber.toPrimitive(), - blockHash: blockEvent.blockHash.toHex(), - sender: getSendersFromEvent(blockEvent), - }) - ) - } - - return from(msgs) - }) - ) - } -} - -export function extractBridgeReceive(origin: NetworkURN) { - return (source: Observable): Observable => { - return source.pipe( - filter((event) => - matchEvent( - event, - ['bridgePolkadotMessages', 'bridgeKusamaMessages', 'bridgeRococoMessages', 'bridgeWestendMessages'], - 'MessagesReceived' - ) - ), - mergeMap((event) => { - // for some reason the Vec is wrapped with another array? - // TODO: investigate for cases of multiple lanes - const receivedMessages = event.data[0] as unknown as Vec - const inboundMsgs: XcmBridgeInboundWithContext[] = [] - for (const message of receivedMessages) { - const laneId = message.lane - for (const [nonce, result] of message.receiveResults) { - const key = u8aConcat(laneId.toU8a(), nonce.toU8a()) - inboundMsgs.push( - new GenericXcmBridgeInboundWithContext({ - chainId: origin, - bridgeKey: u8aToHex(key), - event: blockEventToHuman(event), - extrinsicId: event.extrinsicId, - blockNumber: event.blockNumber.toPrimitive(), - blockHash: event.blockHash.toHex(), - outcome: result.isDispatched ? 'Success' : 'Fail', - error: result.isDispatched ? null : result.type, - }) - ) - } - } - return from(inboundMsgs) - }) - ) - } -} diff --git a/packages/server/src/agents/xcm/ops/relay.spec.ts b/packages/server/src/agents/xcm/ops/relay.spec.ts deleted file mode 100644 index 92467699..00000000 --- a/packages/server/src/agents/xcm/ops/relay.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { jest } from '@jest/globals' - -import { extractTxWithEvents } from '@sodazone/ocelloids-sdk' -import { registry, relayHrmpReceive } from '../../../testing/xcm.js' -import { NetworkURN } from '../../types.js' -import { messageCriteria } from './criteria.js' -import { extractRelayReceive } from './relay.js' - -describe('relay operator', () => { - describe('extractRelayReceive', () => { - it('should extract HRMP messages when they arrive on the relay chain', (done) => { - const { blocks, messageControl, origin, destination } = relayHrmpReceive - - const calls = jest.fn() - - const test$ = extractRelayReceive( - origin as NetworkURN, - messageControl, - registry - )(blocks.pipe(extractTxWithEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - expect(msg.recipient).toBe(destination) - expect(msg.extrinsicId).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Success') - expect(msg.error).toBeNull() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(2) - done() - }, - }) - }) - - it('should pass through if messagae control is updated to remove destination', (done) => { - const { blocks, messageControl, origin } = relayHrmpReceive - - const calls = jest.fn() - - const test$ = extractRelayReceive( - origin as NetworkURN, - messageControl, - registry - )(blocks.pipe(extractTxWithEvents())) - - // remove destination from criteria - messageControl.change(messageCriteria(['urn:ocn:local:2000', 'urn:ocn:local:2006'])) - - test$.subscribe({ - next: (_) => { - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(0) - done() - }, - }) - }) - }) -}) diff --git a/packages/server/src/agents/xcm/ops/relay.ts b/packages/server/src/agents/xcm/ops/relay.ts deleted file mode 100644 index d920bfc6..00000000 --- a/packages/server/src/agents/xcm/ops/relay.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Observable, filter, map, mergeMap } from 'rxjs' - -import type { PolkadotPrimitivesV6InherentData } from '@polkadot/types/lookup' -import type { Registry } from '@polkadot/types/types' - -import { ControlQuery, filterNonNull, types } from '@sodazone/ocelloids-sdk' -import { createNetworkId, getChainId } from '../../../services/config.js' -import { NetworkURN } from '../../../services/types.js' -import { GenericXcmRelayedWithContext, XcmRelayedWithContext } from '../types.js' -import { getMessageId, matchExtrinsic } from './util.js' -import { fromXcmpFormat } from './xcm-format.js' - -export function extractRelayReceive(origin: NetworkURN, messageControl: ControlQuery, registry: Registry) { - return (source: Observable): Observable => { - return source.pipe( - filter(({ extrinsic }) => matchExtrinsic(extrinsic, 'parainherent', 'enter')), - map(({ extrinsic, dispatchError }) => { - const { backedCandidates } = extrinsic.args[0] as unknown as PolkadotPrimitivesV6InherentData - const backed = backedCandidates.find((c) => c.candidate.descriptor.paraId.toString() === getChainId(origin)) - if (backed) { - const { horizontalMessages } = backed.candidate.commitments - const message = horizontalMessages.find(({ recipient }) => { - return messageControl.value.test({ - recipient: createNetworkId(origin, recipient.toString()), - }) - }) - if (message) { - const xcms = fromXcmpFormat(message.data, registry) - const { blockHash, blockNumber, extrinsicId } = extrinsic - return xcms.map( - (xcmProgram) => - new GenericXcmRelayedWithContext({ - blockHash: blockHash.toHex(), - blockNumber: blockNumber.toPrimitive(), - recipient: createNetworkId(origin, message.recipient.toString()), - messageHash: xcmProgram.hash.toHex(), - messageId: getMessageId(xcmProgram), - origin, - extrinsicId, - outcome: dispatchError ? 'Fail' : 'Success', - error: dispatchError ? dispatchError.toHuman() : null, - }) - ) - } - } - return null - }), - filterNonNull(), - mergeMap((x) => x) - ) - } -} diff --git a/packages/server/src/agents/xcm/ops/ump.spec.ts b/packages/server/src/agents/xcm/ops/ump.spec.ts deleted file mode 100644 index ddca4422..00000000 --- a/packages/server/src/agents/xcm/ops/ump.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { jest } from '@jest/globals' -import { extractEvents } from '@sodazone/ocelloids-sdk' - -import { registry, umpReceive, umpSend } from '../../../testing/xcm.js' - -import { extractUmpReceive, extractUmpSend } from './ump.js' - -describe('ump operator', () => { - describe('extractUmpSend', () => { - it('should extract UMP sent message', (done) => { - const { origin, blocks, getUmp } = umpSend - - const calls = jest.fn() - - const test$ = extractUmpSend(origin, getUmp, registry)(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - calls() - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.instructions).toBeDefined() - expect(msg.messageData).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - }) - - describe('extractUmpReceive', () => { - it('should extract failed UMP received message', (done) => { - const { successBlocks } = umpReceive - - const calls = jest.fn() - - const test$ = extractUmpReceive('urn:ocn:local:1000')(successBlocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - calls() - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Success') - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract UMP receive with outcome fail', (done) => { - const { failBlocks } = umpReceive - - const calls = jest.fn() - - const test$ = extractUmpReceive('urn:ocn:local:1000')(failBlocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - calls() - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Fail') - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract ump receive with asset trap', (done) => { - const { trappedBlocks } = umpReceive - - const calls = jest.fn() - - const test$ = extractUmpReceive('urn:ocn:local:2004')(trappedBlocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - calls() - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Success') - expect(msg.error).toBeNull() - expect(msg.assetsTrapped).toBeDefined() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(2) - done() - }, - }) - }) - }) -}) diff --git a/packages/server/src/agents/xcm/ops/ump.ts b/packages/server/src/agents/xcm/ops/ump.ts deleted file mode 100644 index a5e5cf3c..00000000 --- a/packages/server/src/agents/xcm/ops/ump.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Observable, bufferCount, filter, map, mergeMap } from 'rxjs' - -// NOTE: we use Polkadot augmented types -import '@polkadot/api-augment/polkadot' -import type { Registry } from '@polkadot/types/types' - -import { filterNonNull, types } from '@sodazone/ocelloids-sdk' - -import { getChainId, getRelayId } from '../../../services/config.js' -import { NetworkURN } from '../../../services/types.js' -import { GetOutboundUmpMessages } from '../types-augmented.js' -import { - GenericXcmInboundWithContext, - GenericXcmSentWithContext, - XcmInboundWithContext, - XcmSentWithContext, -} from '../types.js' -import { MessageQueueEventContext } from '../types.js' -import { blockEventToHuman, xcmMessagesSent } from './common.js' -import { getMessageId, getParaIdFromOrigin, mapAssetsTrapped, matchEvent } from './util.js' -import { asVersionedXcm } from './xcm-format.js' - -const METHODS_MQ_PROCESSED = ['Processed', 'ProcessingFailed'] - -function createUmpReceivedWithContext( - subOrigin: NetworkURN, - event: types.BlockEvent, - assetsTrappedEvent?: types.BlockEvent -): XcmInboundWithContext | null { - const { id, origin, success, error } = event.data as unknown as MessageQueueEventContext - // Received event only emits field `message_id`, - // which is actually the message hash in the current runtime. - const messageId = id.toHex() - const messageHash = messageId - const messageOrigin = getParaIdFromOrigin(origin) - const assetsTrapped = mapAssetsTrapped(assetsTrappedEvent) - // If we can get message origin, only return message if origin matches with subscription origin - // If no origin, we will return the message without matching with subscription origin - if (messageOrigin === undefined || messageOrigin === getChainId(subOrigin)) { - return new GenericXcmInboundWithContext({ - event: blockEventToHuman(event), - blockHash: event.blockHash.toHex(), - blockNumber: event.blockNumber.toPrimitive(), - messageHash, - messageId, - outcome: success?.isTrue ? 'Success' : 'Fail', - error: error ? error.toHuman() : null, - assetsTrapped, - }) - } - return null -} - -function findOutboundUmpMessage( - origin: NetworkURN, - getOutboundUmpMessages: GetOutboundUmpMessages, - registry: Registry -) { - return (source: Observable): Observable => { - return source.pipe( - mergeMap((sentMsg) => { - const { blockHash, messageHash, messageId } = sentMsg - return getOutboundUmpMessages(blockHash).pipe( - map((messages) => { - return messages - .map((data) => { - const xcmProgram = asVersionedXcm(data, registry) - return new GenericXcmSentWithContext({ - ...sentMsg, - messageData: data.toU8a(), - recipient: getRelayId(origin), // always relay - messageHash: xcmProgram.hash.toHex(), - messageId: getMessageId(xcmProgram), - instructions: { - bytes: xcmProgram.toU8a(), - json: xcmProgram.toHuman(), - }, - }) - }) - .find((msg) => { - return messageId ? msg.messageId === messageId : msg.messageHash === messageHash - }) - }), - filterNonNull() - ) - }) - ) - } -} - -export function extractUmpSend(origin: NetworkURN, getOutboundUmpMessages: GetOutboundUmpMessages, registry: Registry) { - return (source: Observable): Observable => { - return source.pipe( - filter( - (event) => matchEvent(event, 'parachainSystem', 'UpwardMessageSent') || matchEvent(event, 'polkadotXcm', 'Sent') - ), - xcmMessagesSent(), - findOutboundUmpMessage(origin, getOutboundUmpMessages, registry) - ) - } -} - -export function extractUmpReceive(originId: NetworkURN) { - return (source: Observable): Observable => { - return source.pipe( - bufferCount(2, 1), - map(([maybeAssetTrapEvent, maybeUmpEvent]) => { - if (maybeUmpEvent && matchEvent(maybeUmpEvent, 'messageQueue', METHODS_MQ_PROCESSED)) { - const assetTrapEvent = matchEvent(maybeAssetTrapEvent, 'xcmPallet', 'AssetsTrapped') - ? maybeAssetTrapEvent - : undefined - return createUmpReceivedWithContext(originId, maybeUmpEvent, assetTrapEvent) - } - return null - }), - filterNonNull() - ) - } -} diff --git a/packages/server/src/agents/xcm/ops/util.spec.ts b/packages/server/src/agents/xcm/ops/util.spec.ts deleted file mode 100644 index f1bd8659..00000000 --- a/packages/server/src/agents/xcm/ops/util.spec.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { types } from '@sodazone/ocelloids-sdk' - -import { - getMessageId, - getParaIdFromMultiLocation, - getParaIdFromOrigin, - getSendersFromEvent, - getSendersFromExtrinsic, - networkIdFromMultiLocation, -} from './util.js' - -describe('xcm ops utils', () => { - describe('getSendersFromExtrinsic', () => { - it('should extract signers data for signed extrinsic', () => { - const signerData = getSendersFromExtrinsic({ - isSigned: true, - signer: { - toPrimitive: () => '', - toHex: () => '0x0', - }, - extraSigners: [], - } as unknown as types.ExtrinsicWithId) - - expect(signerData).toBeDefined() - }) - - it('should return undefined on unsigned extrinsic', () => { - const signerData = getSendersFromExtrinsic({ - isSigned: false, - extraSigners: [], - } as unknown as types.ExtrinsicWithId) - - expect(signerData).toBeUndefined() - }) - - it('should throw error on malformed extrinsic', () => { - expect(() => - getSendersFromExtrinsic({ - isSigned: true, - } as unknown as types.ExtrinsicWithId) - ).toThrow() - }) - - it('should get extra signers data', () => { - const signerData = getSendersFromExtrinsic({ - isSigned: true, - signer: { - value: { - toPrimitive: () => '', - toHex: () => '0x0', - }, - }, - extraSigners: [ - { - type: 'test', - address: { - value: { - toPrimitive: () => '', - toHex: () => '0x0', - }, - }, - }, - { - type: 'test', - address: { - value: { - toPrimitive: () => '', - toHex: () => '0x0', - }, - }, - }, - ], - } as unknown as types.ExtrinsicWithId) - - expect(signerData).toBeDefined() - expect(signerData?.extraSigners.length).toBe(2) - }) - }) - describe('getSendersFromEvent', () => { - it('should extract signers data for an event with signed extrinsic', () => { - const signerData = getSendersFromEvent({ - extrinsic: { - isSigned: true, - signer: { - toPrimitive: () => '', - toHex: () => '0x0', - }, - extraSigners: [], - }, - } as unknown as types.EventWithId) - - expect(signerData).toBeDefined() - }) - it('should return undefined for an event without extrinsic', () => { - const signerData = getSendersFromEvent({} as unknown as types.EventWithId) - - expect(signerData).toBeUndefined() - }) - }) - describe('getMessageId', () => { - it('should get the message id from setTopic V3 instruction', () => { - const messageId = getMessageId({ - type: 'V3', - asV3: [ - { - isSetTopic: true, - asSetTopic: { - toHex: () => '0x012233', - }, - }, - ], - } as unknown as any) - expect(messageId).toBe('0x012233') - }) - it('should get the message id from setTopic V4 instruction', () => { - const messageId = getMessageId({ - type: 'V4', - asV4: [ - { - isSetTopic: true, - asSetTopic: { - toHex: () => '0x012233', - }, - }, - ], - } as unknown as any) - expect(messageId).toBe('0x012233') - }) - it('should return undefined for V3 without setTopic', () => { - const messageId = getMessageId({ - type: 'V3', - asV3: [ - { - isSetTopic: false, - }, - ], - } as unknown as any) - expect(messageId).toBeUndefined() - }) - it('should return undefined for V2 instruction', () => { - const messageId = getMessageId({ - type: 'V2', - } as unknown as any) - expect(messageId).toBeUndefined() - }) - }) - describe('getParaIdFromOrigin', () => { - it('should get para id from UMP origin', () => { - const paraId = getParaIdFromOrigin({ - isUmp: true, - asUmp: { - isPara: true, - asPara: { - toString: () => '10', - }, - }, - } as unknown as any) - expect(paraId).toBe('10') - }) - it('should return undefined from unknown origin', () => { - expect( - getParaIdFromOrigin({ - isUmp: true, - asUmp: { - isPara: false, - }, - } as unknown as any) - ).toBeUndefined() - expect( - getParaIdFromOrigin({ - isUmp: false, - } as unknown as any) - ).toBeUndefined() - }) - }) - describe('getParaIdFromMultiLocation', () => { - it('should get paraId from local relay multi location', () => { - const paraId = getParaIdFromMultiLocation({ - interior: { - type: 'Here', - }, - parents: { - toNumber: () => 1, - }, - } as unknown as any) - expect(paraId).toBe('0') - }) - - it('should get paraId from V4 multi location', () => { - for (const t of ['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8']) { - const paraId = getParaIdFromMultiLocation({ - interior: { - type: t, - [`as${t}`]: [ - { - isParachain: true, - asParachain: { - toString: () => '10', - }, - }, - ], - }, - } as unknown as any) - expect(paraId).toBe('10') - } - }) - - it('should get paraId from >V4 X1 multi location', () => { - const paraId = getParaIdFromMultiLocation({ - interior: { - type: 'X1', - asX1: { - isParachain: true, - asParachain: { - toString: () => '10', - }, - }, - }, - } as unknown as any) - expect(paraId).toBe('10') - }) - - it('should get paraId from >V4 multi location', () => { - for (const t of ['X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8']) { - const paraId = getParaIdFromMultiLocation({ - interior: { - type: t, - [`as${t}`]: [ - { - isParachain: true, - asParachain: { - toString: () => '10', - }, - }, - ], - }, - } as unknown as any) - expect(paraId).toBe('10') - } - }) - it('should return undefined on unknown multi location', () => { - expect( - getParaIdFromMultiLocation({ - interior: { - type: 'ZZ', - asZZ: [], - }, - } as unknown as any) - ).toBeUndefined() - expect( - getParaIdFromMultiLocation({ - interior: { - type: 'Here', - }, - } as unknown as any) - ).toBeUndefined() - expect( - getParaIdFromMultiLocation({ - interior: { - type: 'Here', - }, - parents: { - toNumber: () => 10, - }, - } as unknown as any) - ).toBeUndefined() - }) - }) - describe('networkIdFromMultiLocation', () => { - it('should get a network id from multi location same consensus', () => { - const networkId = networkIdFromMultiLocation( - { - parents: { - toNumber: () => 1, - }, - interior: { - type: 'X1', - asX1: [ - { - isParachain: true, - asParachain: { - toString: () => '11', - }, - }, - ], - }, - } as unknown as any, - 'urn:ocn:polkadot:10' - ) - expect(networkId).toBe('urn:ocn:polkadot:11') - }) - it('should get a network id from V4 multi location different consensus', () => { - const networkId = networkIdFromMultiLocation( - { - parents: { - toNumber: () => 2, - }, - interior: { - type: 'X2', - asX2: [ - { - isGlobalConsensus: true, - asGlobalConsensus: { - type: 'Espartaco', - }, - }, - { - isParachain: true, - asParachain: { - toString: () => '11', - }, - }, - ], - }, - } as unknown as any, - 'urn:ocn:polkadot:10' - ) - expect(networkId).toBe('urn:ocn:espartaco:11') - }) - it('should get a network id from V3 multi location different consensus', () => { - const networkId = networkIdFromMultiLocation( - { - parents: { - toNumber: () => 2, - }, - interior: { - type: 'X1', - asX1: { - isGlobalConsensus: true, - asGlobalConsensus: { - type: 'Espartaco', - }, - }, - }, - } as unknown as any, - 'urn:ocn:polkadot:10' - ) - expect(networkId).toBe('urn:ocn:espartaco:0') - }) - }) -}) diff --git a/packages/server/src/agents/xcm/ops/util.ts b/packages/server/src/agents/xcm/ops/util.ts deleted file mode 100644 index b379953f..00000000 --- a/packages/server/src/agents/xcm/ops/util.ts +++ /dev/null @@ -1,417 +0,0 @@ -import type { U8aFixed } from '@polkadot/types-codec' -import type { H256 } from '@polkadot/types/interfaces/runtime' -import type { - PolkadotRuntimeParachainsInclusionAggregateMessageOrigin, - StagingXcmV3MultiLocation, - XcmV2MultiLocation, - XcmV2MultiassetMultiAssets, - XcmV2MultilocationJunctions, - XcmV3Junction, - XcmV3Junctions, - XcmV3MultiassetMultiAssets, -} from '@polkadot/types/lookup' - -import { types } from '@sodazone/ocelloids-sdk' - -import { GlobalConsensus, createNetworkId, getConsensus, isGlobalConsensus } from '../../../services/config.js' -import { HexString, SignerData } from '../../../services/monitoring/types.js' -import { NetworkURN } from '../../../services/types.js' -import { AssetsTrapped, TrappedAsset } from '../types.js' -import { - VersionedInteriorLocation, - XcmV4AssetAssets, - XcmV4Junction, - XcmV4Junctions, - XcmV4Location, - XcmVersionedAssets, - XcmVersionedLocation, - XcmVersionedXcm, -} from './xcm-types.js' - -const BRIDGE_HUB_NETWORK_IDS: Record = { - polkadot: 'urn:ocn:polkadot:1002', - kusama: 'urn:ocn:kusama:1002', - rococo: 'urn:ocn:rococo:1013', - westend: 'urn:ocn:westend:1002', - local: 'urn:ocn:local:1002', - wococo: 'urn:ocn:wococo:1002', - ethereum: undefined, - byfork: undefined, - bygenesis: undefined, - bitcoincore: undefined, - bitcoincash: undefined, -} - -export function getBridgeHubNetworkId(consensus: string | NetworkURN): NetworkURN | undefined { - const c = consensus.startsWith('urn:ocn:') ? getConsensus(consensus as NetworkURN) : consensus - if (isGlobalConsensus(c)) { - return BRIDGE_HUB_NETWORK_IDS[c] - } - return undefined -} - -function createSignersData(xt: types.ExtrinsicWithId): SignerData | undefined { - try { - if (xt.isSigned) { - // Signer could be Address or AccountId - const accountId = xt.signer.value ?? xt.signer - return { - signer: { - id: accountId.toPrimitive(), - publicKey: accountId.toHex(), - }, - extraSigners: xt.extraSigners.map((signer) => ({ - type: signer.type, - id: signer.address.value.toPrimitive(), - publicKey: signer.address.value.toHex(), - })), - } - } - } catch (error) { - throw new Error(`creating signers data at ${xt.extrinsicId ?? '-1'}`, { cause: error }) - } - - return undefined -} - -export function getSendersFromExtrinsic(extrinsic: types.ExtrinsicWithId): SignerData | undefined { - return createSignersData(extrinsic) -} - -export function getSendersFromEvent(event: types.BlockEvent): SignerData | undefined { - if (event.extrinsic !== undefined) { - return getSendersFromExtrinsic(event.extrinsic) - } - return undefined -} -/** - * Gets message id from setTopic. - */ -export function getMessageId(program: XcmVersionedXcm): HexString | undefined { - switch (program.type) { - // Only XCM V3+ supports topic ID - case 'V3': - case 'V4': - for (const instruction of program[`as${program.type}`]) { - if (instruction.isSetTopic) { - return instruction.asSetTopic.toHex() - } - } - return undefined - default: - return undefined - } -} - -export function getParaIdFromOrigin( - origin: PolkadotRuntimeParachainsInclusionAggregateMessageOrigin -): string | undefined { - if (origin.isUmp) { - const umpOrigin = origin.asUmp - if (umpOrigin.isPara) { - return umpOrigin.asPara.toString() - } - } - - return undefined -} - -// TODO: revisit Junction guards and conversions -// TODO: extract in multiple files - -function isX1V2Junctions(object: any): object is XcmV2MultilocationJunctions { - return ( - object.asX1 !== undefined && - typeof object.asX1[Symbol.iterator] !== 'function' && - object.asX1.isGlobalConsensus === undefined - ) -} - -const Xn = ['X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8'] -function isXnV2Junctions(object: any): object is XcmV2MultilocationJunctions { - return Xn.every((x) => { - const ax = object[`as${x}`] - return ax === undefined || (Array.isArray(ax) && ax.every((a) => a.isGlobalConsensus === undefined)) - }) -} - -function isX1V4Junctions(object: any): object is XcmV4Junctions { - return object.asX1 !== undefined && typeof object.asX1[Symbol.iterator] === 'function' -} - -function isX1V3Junctions(object: any): object is XcmV3Junctions { - return ( - object.asX1 !== undefined && - typeof object.asX1[Symbol.iterator] !== 'function' && - object.asX1.isGlobalConsensus !== undefined - ) -} - -type NetworkId = { - consensus?: string - chainId?: string -} - -function extractConsensusAndId(j: XcmV3Junction | XcmV4Junction, n: NetworkId) { - const network = j.asGlobalConsensus - if (network.type === 'Ethereum') { - n.consensus = network.type.toLowerCase() - n.chainId = network.asEthereum.chainId.toString() - } else if (network.type !== 'ByFork' && network.type !== 'ByGenesis') { - n.consensus = network.type.toLowerCase() - } -} - -function extractV3X1GlobalConsensus(junctions: XcmV3Junctions, n: NetworkId): NetworkURN | undefined { - if (junctions.asX1.isGlobalConsensus) { - extractConsensusAndId(junctions.asX1, n) - if (n.consensus !== undefined) { - return createNetworkId(n.consensus, n.chainId ?? '0') - } - } - return undefined -} - -function _networkIdFrom(junctions: XcmV3Junctions | XcmV4Junctions, networkId: NetworkId) { - if (junctions.type === 'X1' || junctions.type === 'Here') { - return undefined - } - - for (const j of junctions[`as${junctions.type}`]) { - if (j.isGlobalConsensus) { - extractConsensusAndId(j, networkId) - } - - if (j.isParachain) { - networkId.chainId = j.asParachain.toString() - } - } - - if (networkId.consensus !== undefined) { - return createNetworkId(networkId.consensus, networkId.chainId ?? '0') - } - - return undefined -} - -function networkIdFromV4(junctions: XcmV4Junctions): NetworkURN | undefined { - const networkId: NetworkId = {} - - return _networkIdFrom(junctions, networkId) -} - -function networkIdFromV3(junctions: XcmV3Junctions): NetworkURN | undefined { - if (junctions.type === 'Here') { - return undefined - } - - const networkId: NetworkId = {} - - if (junctions.type === 'X1') { - return extractV3X1GlobalConsensus(junctions, networkId) - } - - return _networkIdFrom(junctions, networkId) -} - -// eslint-disable-next-line complexity -export function getParaIdFromJunctions( - junctions: XcmV2MultilocationJunctions | XcmV3Junctions | XcmV4Junctions -): string | undefined { - if (junctions.type === 'Here') { - return undefined - } - - if (junctions.type === 'X1') { - if (isX1V3Junctions(junctions) || isX1V2Junctions(junctions)) { - return junctions.asX1.isParachain ? junctions.asX1.asParachain.toString() : undefined - } else { - for (const j of junctions[`as${junctions.type}`]) { - if (j.isParachain) { - return j.asParachain.toString() - } - } - } - return undefined - } - - for (const j of junctions[`as${junctions.type}`]) { - if (j.isParachain) { - return j.asParachain.toString() - } - } - return undefined -} - -export function getParaIdFromMultiLocation( - loc: XcmV2MultiLocation | StagingXcmV3MultiLocation | XcmV4Location -): string | undefined { - const junctions = loc.interior - if (junctions.type === 'Here') { - if (loc.parents?.toNumber() === 1) { - return '0' - } - return undefined - } - - return getParaIdFromJunctions(junctions) -} - -export function networkIdFromInteriorLocation(junctions: VersionedInteriorLocation): NetworkURN | undefined { - if (junctions.isV2) { - return undefined - } - - if (junctions.isV3) { - return networkIdFromV3(junctions.asV3) - } - - if (junctions.isV4) { - return networkIdFromV4(junctions.asV4) - } - return undefined -} - -// eslint-disable-next-line complexity -export function networkIdFromMultiLocation( - loc: XcmV2MultiLocation | StagingXcmV3MultiLocation | XcmV4Location, - currentNetworkId: NetworkURN -): NetworkURN | undefined { - const { parents, interior: junctions } = loc - - if (parents.toNumber() <= 1) { - // is within current consensus system - const paraId = getParaIdFromMultiLocation(loc) - - if (paraId !== undefined) { - return createNetworkId(currentNetworkId, paraId) - } - } else if (parents.toNumber() > 1) { - // is in other consensus system - if (junctions.type === 'X1') { - if (isX1V2Junctions(junctions)) { - return undefined - } - - if (isX1V3Junctions(junctions)) { - return networkIdFromV3(junctions) - } - - if (isX1V4Junctions(junctions)) { - return networkIdFromV4(junctions) - } - } else if (!isXnV2Junctions(junctions)) { - return _networkIdFrom(junctions, {}) - } - } - - return undefined -} - -export function networkIdFromVersionedMultiLocation( - loc: XcmVersionedLocation, - currentNetworkId: NetworkURN -): NetworkURN | undefined { - switch (loc.type) { - case 'V2': - case 'V3': - return networkIdFromMultiLocation(loc[`as${loc.type}`], currentNetworkId) - case 'V4': - return networkIdFromMultiLocation(loc.asV4, currentNetworkId) - default: - return undefined - } -} - -export function matchProgramByTopic(message: XcmVersionedXcm, topicId: U8aFixed): boolean { - switch (message.type) { - case 'V2': - throw new Error('Not able to match by topic for XCM V2 program.') - case 'V3': - case 'V4': - for (const instruction of message[`as${message.type}`]) { - if (instruction.isSetTopic) { - return instruction.asSetTopic.eq(topicId) - } - } - return false - default: - throw new Error('XCM version not supported') - } -} - -export function matchEvent(event: types.BlockEvent, section: string | string[], method: string | string[]) { - return ( - (Array.isArray(section) ? section.includes(event.section) : section === event.section) && - (Array.isArray(method) ? method.includes(event.method) : method === event.method) - ) -} - -export function matchExtrinsic(extrinsic: types.ExtrinsicWithId, section: string, method: string | string[]): boolean { - return section === extrinsic.method.section && Array.isArray(method) - ? method.includes(extrinsic.method.method) - : method === extrinsic.method.method -} - -function createTrappedAssetsFromMultiAssets( - version: number, - assets: XcmV2MultiassetMultiAssets | XcmV3MultiassetMultiAssets -): TrappedAsset[] { - return assets.map((a) => ({ - version, - id: { - type: a.id.type, - value: a.id.isConcrete ? a.id.asConcrete.toHuman() : a.id.asAbstract.toHex(), - }, - fungible: a.fun.isFungible, - amount: a.fun.isFungible ? a.fun.asFungible.toPrimitive() : 1, - assetInstance: a.fun.isNonFungible ? a.fun.asNonFungible.toHuman() : undefined, - })) -} - -function createTrappedAssetsFromAssets(version: number, assets: XcmV4AssetAssets): TrappedAsset[] { - return assets.map((a) => ({ - version, - id: { - type: 'Concrete', - value: a.id.toHuman(), - }, - fungible: a.fun.isFungible, - amount: a.fun.isFungible ? a.fun.asFungible.toPrimitive() : 1, - assetInstance: a.fun.isNonFungible ? a.fun.asNonFungible.toHuman() : undefined, - })) -} - -function mapVersionedAssets(assets: XcmVersionedAssets): TrappedAsset[] { - switch (assets.type) { - case 'V2': - case 'V3': - return createTrappedAssetsFromMultiAssets(2, assets[`as${assets.type}`]) - case 'V4': - return createTrappedAssetsFromAssets(4, assets.asV4) - default: - throw new Error('XCM version not supported') - } -} - -export function mapAssetsTrapped(assetsTrappedEvent?: types.BlockEvent): AssetsTrapped | undefined { - if (assetsTrappedEvent === undefined) { - return undefined - } - const [hash_, _, assets] = assetsTrappedEvent.data as unknown as [ - hash_: H256, - _origin: any, - assets: XcmVersionedAssets, - ] - return { - event: { - eventId: assetsTrappedEvent.eventId, - blockNumber: assetsTrappedEvent.blockNumber.toPrimitive(), - blockHash: assetsTrappedEvent.blockHash.toHex(), - section: assetsTrappedEvent.section, - method: assetsTrappedEvent.method, - }, - assets: mapVersionedAssets(assets), - hash: hash_.toHex(), - } -} diff --git a/packages/server/src/agents/xcm/ops/xcm-format.spec.ts b/packages/server/src/agents/xcm/ops/xcm-format.spec.ts deleted file mode 100644 index f93e57e8..00000000 --- a/packages/server/src/agents/xcm/ops/xcm-format.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { registry } from '../../../testing/xcm.js' -import { fromXcmpFormat } from './xcm-format.js' - -describe('xcm formats', () => { - it('should decode xcmp concatenated fragments', () => { - const moon4361335 = - '0002100004000000001700004b3471bb156b050a13000000001700004b3471bb156b05010300286bee0d010004000101001e08eb75720cb63fbfcbe7237c6d9b7cf6b4953518da6b38731d5bc65b9ffa32021000040000000017206d278c7e297945030a130000000017206d278c7e29794503010300286bee0d010004000101000257fd81d0a71b094c2c8d3e6c93a9b01a31a43d38408bb2c4c2b49a4c58eb01' - const buf = new Uint8Array(Buffer.from(moon4361335, 'hex')) - - const xcms = fromXcmpFormat(buf, registry) - - expect(xcms.length).toBe(2) - expect(xcms[0].hash.toHex()).toBe('0x256f9f3e5f89ced85d4253af0bd4fc6a47d069c7cd2c17723b87dda78a2e2b49') - }) - - it('should return an empty array on blobs', () => { - const buf = new Uint8Array(Buffer.from('0100', 'hex')) - - expect(fromXcmpFormat(buf, registry)).toStrictEqual([]) - }) - - it('should fail on unknown format', () => { - const buf = new Uint8Array(Buffer.from('BAD', 'hex')) - - expect(() => fromXcmpFormat(buf, registry)).toThrow(Error) - }) -}) diff --git a/packages/server/src/agents/xcm/ops/xcm-format.ts b/packages/server/src/agents/xcm/ops/xcm-format.ts deleted file mode 100644 index fc13a0e9..00000000 --- a/packages/server/src/agents/xcm/ops/xcm-format.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { Bytes } from '@polkadot/types' - -import type { Registry } from '@polkadot/types/types' -import { XcmVersionedXcm } from './xcm-types.js' - -/** - * Creates a versioned XCM program from bytes. - * - * @param data The data bytes. - * @param registry Optional - The registry to decode types. - * @returns a versioned XCM program - */ -export function asVersionedXcm(data: Bytes | Uint8Array, registry: Registry): XcmVersionedXcm { - if (registry.hasType('XcmVersionedXcm')) { - // TODO patch types because bundled types are wrong - return registry.createType('XcmVersionedXcm', data) as unknown as XcmVersionedXcm - } else if (registry.hasType('StagingXcmVersionedXcm')) { - return registry.createType('StagingXcmVersionedXcm', data) as XcmVersionedXcm - } - - throw new Error('Versioned XCM type not found in chain registry') - - // TODO:does it make sense to default to Polka Reg? - /* - return polkadotRegistry().createType( - 'XcmVersionedXcm', data - ) as XcmVersionedXcm; - */ -} - -function asXcmpVersionedXcms(buffer: Uint8Array, registry: Registry): XcmVersionedXcm[] { - const len = buffer.length - const xcms: XcmVersionedXcm[] = [] - let ptr = 1 - - while (ptr < len) { - try { - const xcm = asVersionedXcm(buffer.slice(ptr), registry) - xcms.push(xcm) - ptr += xcm.encodedLength - } catch (error) { - // TODO use logger - console.error(error) - break - } - } - - return xcms -} - -/** - * Decodes XCMP message formats. - * - * @param buf The data buffer. - * @param registry Optional - The registry to decode types. - * @returns an array of {@link VersionedXcm} programs. - */ -export function fromXcmpFormat(buf: Uint8Array, registry: Registry): XcmVersionedXcm[] { - switch (buf[0]) { - case 0x00: { - // Concatenated XCM fragments - return asXcmpVersionedXcms(buf, registry) - } - case 0x01: { - // XCM blobs - // XCM blobs not supported, ignore - break - } - case 0x02: { - // Signals - // TODO handle signals - break - } - default: { - throw new Error('Unknown XCMP format') - } - } - return [] -} diff --git a/packages/server/src/agents/xcm/ops/xcm-types.ts b/packages/server/src/agents/xcm/ops/xcm-types.ts deleted file mode 100644 index 200a8b85..00000000 --- a/packages/server/src/agents/xcm/ops/xcm-types.ts +++ /dev/null @@ -1,576 +0,0 @@ -/* eslint-disable no-use-before-define */ -import type { - Bytes, - Compact, - Enum, - Option, - Struct, - U8aFixed, - Vec, - bool, - u8, - u32, - u64, - u128, -} from '@polkadot/types-codec' -import type { ITuple } from '@polkadot/types-codec/types' - -import type { - SpWeightsWeightV2Weight, - StagingXcmV3MultiLocation, - XcmDoubleEncoded, - XcmV2MultiLocation, - XcmV2MultiassetMultiAssets, - XcmV2MultilocationJunctions, - XcmV2OriginKind, - XcmV2Xcm, - XcmV3JunctionBodyId, - XcmV3JunctionBodyPart, - XcmV3Junctions, - XcmV3MaybeErrorCode, - XcmV3MultiassetMultiAssets, - XcmV3TraitsError, - XcmV3WeightLimit, - XcmV3Xcm, -} from '@polkadot/types/lookup' - -/** @name XcmVersionedXcm (296) */ -export interface XcmVersionedXcm extends Enum { - readonly isV2: boolean - readonly asV2: XcmV2Xcm - readonly isV3: boolean - readonly asV3: XcmV3Xcm - readonly isV4: boolean - readonly asV4: XcmV4Xcm - readonly type: 'V2' | 'V3' | 'V4' -} - -/** @name XcmVersionedLocation (70) */ -export interface XcmVersionedLocation extends Enum { - readonly isV2: boolean - readonly asV2: XcmV2MultiLocation - readonly isV3: boolean - readonly asV3: StagingXcmV3MultiLocation - readonly isV4: boolean - readonly asV4: XcmV4Location - readonly type: 'V2' | 'V3' | 'V4' -} - -/** @name XcmVersionedAssets (358) */ -export interface XcmVersionedAssets extends Enum { - readonly isV2: boolean - readonly asV2: XcmV2MultiassetMultiAssets - readonly isV3: boolean - readonly asV3: XcmV3MultiassetMultiAssets - readonly isV4: boolean - readonly asV4: XcmV4AssetAssets - readonly type: 'V2' | 'V3' | 'V4' -} - -/** @name XcmV4Xcm (340) */ -export interface XcmV4Xcm extends Vec {} - -/** @name XcmV4Instruction (342) */ -export interface XcmV4Instruction extends Enum { - readonly isWithdrawAsset: boolean - readonly asWithdrawAsset: XcmV4AssetAssets - readonly isReserveAssetDeposited: boolean - readonly asReserveAssetDeposited: XcmV4AssetAssets - readonly isReceiveTeleportedAsset: boolean - readonly asReceiveTeleportedAsset: XcmV4AssetAssets - readonly isQueryResponse: boolean - readonly asQueryResponse: { - readonly queryId: Compact - readonly response: XcmV4Response - readonly maxWeight: SpWeightsWeightV2Weight - readonly querier: Option - } & Struct - readonly isTransferAsset: boolean - readonly asTransferAsset: { - readonly assets: XcmV4AssetAssets - readonly beneficiary: XcmV4Location - } & Struct - readonly isTransferReserveAsset: boolean - readonly asTransferReserveAsset: { - readonly assets: XcmV4AssetAssets - readonly dest: XcmV4Location - readonly xcm: XcmV4Xcm - } & Struct - readonly isTransact: boolean - readonly asTransact: { - readonly originKind: XcmV2OriginKind - readonly requireWeightAtMost: SpWeightsWeightV2Weight - readonly call: XcmDoubleEncoded - } & Struct - readonly isHrmpNewChannelOpenRequest: boolean - readonly asHrmpNewChannelOpenRequest: { - readonly sender: Compact - readonly maxMessageSize: Compact - readonly maxCapacity: Compact - } & Struct - readonly isHrmpChannelAccepted: boolean - readonly asHrmpChannelAccepted: { - readonly recipient: Compact - } & Struct - readonly isHrmpChannelClosing: boolean - readonly asHrmpChannelClosing: { - readonly initiator: Compact - readonly sender: Compact - readonly recipient: Compact - } & Struct - readonly isClearOrigin: boolean - readonly isDescendOrigin: boolean - readonly asDescendOrigin: XcmV4Junctions - readonly isReportError: boolean - readonly asReportError: XcmV4QueryResponseInfo - readonly isDepositAsset: boolean - readonly asDepositAsset: { - readonly assets: XcmV4AssetAssetFilter - readonly beneficiary: XcmV4Location - } & Struct - readonly isDepositReserveAsset: boolean - readonly asDepositReserveAsset: { - readonly assets: XcmV4AssetAssetFilter - readonly dest: XcmV4Location - readonly xcm: XcmV4Xcm - } & Struct - readonly isExchangeAsset: boolean - readonly asExchangeAsset: { - readonly give: XcmV4AssetAssetFilter - readonly want: XcmV4AssetAssets - readonly maximal: bool - } & Struct - readonly isInitiateReserveWithdraw: boolean - readonly asInitiateReserveWithdraw: { - readonly assets: XcmV4AssetAssetFilter - readonly reserve: XcmV4Location - readonly xcm: XcmV4Xcm - } & Struct - readonly isInitiateTeleport: boolean - readonly asInitiateTeleport: { - readonly assets: XcmV4AssetAssetFilter - readonly dest: XcmV4Location - readonly xcm: XcmV4Xcm - } & Struct - readonly isReportHolding: boolean - readonly asReportHolding: { - readonly responseInfo: XcmV4QueryResponseInfo - readonly assets: XcmV4AssetAssetFilter - } & Struct - readonly isBuyExecution: boolean - readonly asBuyExecution: { - readonly fees: XcmV4Asset - readonly weightLimit: XcmV3WeightLimit - } & Struct - readonly isRefundSurplus: boolean - readonly isSetErrorHandler: boolean - readonly asSetErrorHandler: XcmV4Xcm - readonly isSetAppendix: boolean - readonly asSetAppendix: XcmV4Xcm - readonly isClearError: boolean - readonly isClaimAsset: boolean - readonly asClaimAsset: { - readonly assets: XcmV4AssetAssets - readonly ticket: XcmV4Location - } & Struct - readonly isTrap: boolean - readonly asTrap: Compact - readonly isSubscribeVersion: boolean - readonly asSubscribeVersion: { - readonly queryId: Compact - readonly maxResponseWeight: SpWeightsWeightV2Weight - } & Struct - readonly isUnsubscribeVersion: boolean - readonly isBurnAsset: boolean - readonly asBurnAsset: XcmV4AssetAssets - readonly isExpectAsset: boolean - readonly asExpectAsset: XcmV4AssetAssets - readonly isExpectOrigin: boolean - readonly asExpectOrigin: Option - readonly isExpectError: boolean - readonly asExpectError: Option> - readonly isExpectTransactStatus: boolean - readonly asExpectTransactStatus: XcmV3MaybeErrorCode - readonly isQueryPallet: boolean - readonly asQueryPallet: { - readonly moduleName: Bytes - readonly responseInfo: XcmV4QueryResponseInfo - } & Struct - readonly isExpectPallet: boolean - readonly asExpectPallet: { - readonly index: Compact - readonly name: Bytes - readonly moduleName: Bytes - readonly crateMajor: Compact - readonly minCrateMinor: Compact - } & Struct - readonly isReportTransactStatus: boolean - readonly asReportTransactStatus: XcmV4QueryResponseInfo - readonly isClearTransactStatus: boolean - readonly isUniversalOrigin: boolean - readonly asUniversalOrigin: XcmV4Junction - readonly isExportMessage: boolean - readonly asExportMessage: { - readonly network: XcmV4JunctionNetworkId - readonly destination: XcmV4Junctions - readonly xcm: XcmV4Xcm - } & Struct - readonly isLockAsset: boolean - readonly asLockAsset: { - readonly asset: XcmV4Asset - readonly unlocker: XcmV4Location - } & Struct - readonly isUnlockAsset: boolean - readonly asUnlockAsset: { - readonly asset: XcmV4Asset - readonly target: XcmV4Location - } & Struct - readonly isNoteUnlockable: boolean - readonly asNoteUnlockable: { - readonly asset: XcmV4Asset - readonly owner: XcmV4Location - } & Struct - readonly isRequestUnlock: boolean - readonly asRequestUnlock: { - readonly asset: XcmV4Asset - readonly locker: XcmV4Location - } & Struct - readonly isSetFeesMode: boolean - readonly asSetFeesMode: { - readonly jitWithdraw: bool - } & Struct - readonly isSetTopic: boolean - readonly asSetTopic: U8aFixed - readonly isClearTopic: boolean - readonly isAliasOrigin: boolean - readonly asAliasOrigin: XcmV4Location - readonly isUnpaidExecution: boolean - readonly asUnpaidExecution: { - readonly weightLimit: XcmV3WeightLimit - readonly checkOrigin: Option - } & Struct - readonly type: - | 'WithdrawAsset' - | 'ReserveAssetDeposited' - | 'ReceiveTeleportedAsset' - | 'QueryResponse' - | 'TransferAsset' - | 'TransferReserveAsset' - | 'Transact' - | 'HrmpNewChannelOpenRequest' - | 'HrmpChannelAccepted' - | 'HrmpChannelClosing' - | 'ClearOrigin' - | 'DescendOrigin' - | 'ReportError' - | 'DepositAsset' - | 'DepositReserveAsset' - | 'ExchangeAsset' - | 'InitiateReserveWithdraw' - | 'InitiateTeleport' - | 'ReportHolding' - | 'BuyExecution' - | 'RefundSurplus' - | 'SetErrorHandler' - | 'SetAppendix' - | 'ClearError' - | 'ClaimAsset' - | 'Trap' - | 'SubscribeVersion' - | 'UnsubscribeVersion' - | 'BurnAsset' - | 'ExpectAsset' - | 'ExpectOrigin' - | 'ExpectError' - | 'ExpectTransactStatus' - | 'QueryPallet' - | 'ExpectPallet' - | 'ReportTransactStatus' - | 'ClearTransactStatus' - | 'UniversalOrigin' - | 'ExportMessage' - | 'LockAsset' - | 'UnlockAsset' - | 'NoteUnlockable' - | 'RequestUnlock' - | 'SetFeesMode' - | 'SetTopic' - | 'ClearTopic' - | 'AliasOrigin' - | 'UnpaidExecution' -} - -/** @name XcmV4AssetAssets (343) */ -export interface XcmV4AssetAssets extends Vec {} - -/** @name XcmV4Asset (345) */ -interface XcmV4Asset extends Struct { - readonly id: XcmV4AssetAssetId - readonly fun: XcmV4AssetFungibility -} - -/** @name XcmV4AssetFungibility (346) */ -interface XcmV4AssetFungibility extends Enum { - readonly isFungible: boolean - readonly asFungible: Compact - readonly isNonFungible: boolean - readonly asNonFungible: XcmV4AssetAssetInstance - readonly type: 'Fungible' | 'NonFungible' -} - -/** @name XcmV4AssetAssetInstance (347) */ -interface XcmV4AssetAssetInstance extends Enum { - readonly isUndefined: boolean - readonly isIndex: boolean - readonly asIndex: Compact - readonly isArray4: boolean - readonly asArray4: U8aFixed - readonly isArray8: boolean - readonly asArray8: U8aFixed - readonly isArray16: boolean - readonly asArray16: U8aFixed - readonly isArray32: boolean - readonly asArray32: U8aFixed - readonly type: 'Undefined' | 'Index' | 'Array4' | 'Array8' | 'Array16' | 'Array32' -} - -/** @name XcmV4Response (348) */ -interface XcmV4Response extends Enum { - readonly isNull: boolean - readonly isAssets: boolean - readonly asAssets: XcmV4AssetAssets - readonly isExecutionResult: boolean - readonly asExecutionResult: Option> - readonly isVersion: boolean - readonly asVersion: u32 - readonly isPalletsInfo: boolean - readonly asPalletsInfo: Vec - readonly isDispatchResult: boolean - readonly asDispatchResult: XcmV3MaybeErrorCode - readonly type: 'Null' | 'Assets' | 'ExecutionResult' | 'Version' | 'PalletsInfo' | 'DispatchResult' -} - -/** @name XcmV4PalletInfo (350) */ -interface XcmV4PalletInfo extends Struct { - readonly index: Compact - readonly name: Bytes - readonly moduleName: Bytes - readonly major: Compact - readonly minor: Compact - readonly patch: Compact -} - -/** @name XcmV4QueryResponseInfo (354) */ -interface XcmV4QueryResponseInfo extends Struct { - readonly destination: XcmV4Location - readonly queryId: Compact - readonly maxWeight: SpWeightsWeightV2Weight -} - -/** @name XcmV4AssetAssetFilter (355) */ -interface XcmV4AssetAssetFilter extends Enum { - readonly isDefinite: boolean - readonly asDefinite: XcmV4AssetAssets - readonly isWild: boolean - readonly asWild: XcmV4AssetWildAsset - readonly type: 'Definite' | 'Wild' -} - -/** @name XcmV4AssetWildAsset (356) */ -interface XcmV4AssetWildAsset extends Enum { - readonly isAll: boolean - readonly isAllOf: boolean - readonly asAllOf: { - readonly id: XcmV4AssetAssetId - readonly fun: XcmV4AssetWildFungibility - } & Struct - readonly isAllCounted: boolean - readonly asAllCounted: Compact - readonly isAllOfCounted: boolean - readonly asAllOfCounted: { - readonly id: XcmV4AssetAssetId - readonly fun: XcmV4AssetWildFungibility - readonly count: Compact - } & Struct - readonly type: 'All' | 'AllOf' | 'AllCounted' | 'AllOfCounted' -} - -/** @name XcmV4AssetWildFungibility (357) */ -interface XcmV4AssetWildFungibility extends Enum { - readonly isFungible: boolean - readonly isNonFungible: boolean - readonly type: 'Fungible' | 'NonFungible' -} - -/** @name XcmV4Location (56) */ -export interface XcmV4Location extends Struct { - readonly parents: u8 - readonly interior: XcmV4Junctions -} - -/** @name XcmV4Junctions (57) */ -export interface XcmV4Junctions extends Enum { - readonly isHere: boolean - readonly isX1: boolean - readonly asX1: Vec - readonly isX2: boolean - readonly asX2: Vec - readonly isX3: boolean - readonly asX3: Vec - readonly isX4: boolean - readonly asX4: Vec - readonly isX5: boolean - readonly asX5: Vec - readonly isX6: boolean - readonly asX6: Vec - readonly isX7: boolean - readonly asX7: Vec - readonly isX8: boolean - readonly asX8: Vec - readonly type: 'Here' | 'X1' | 'X2' | 'X3' | 'X4' | 'X5' | 'X6' | 'X7' | 'X8' -} - -/** @name XcmV4Junction (59) */ -export interface XcmV4Junction extends Enum { - readonly isParachain: boolean - readonly asParachain: Compact - readonly isAccountId32: boolean - readonly asAccountId32: { - readonly network: Option - readonly id: U8aFixed - } & Struct - readonly isAccountIndex64: boolean - readonly asAccountIndex64: { - readonly network: Option - readonly index: Compact - } & Struct - readonly isAccountKey20: boolean - readonly asAccountKey20: { - readonly network: Option - readonly key: U8aFixed - } & Struct - readonly isPalletInstance: boolean - readonly asPalletInstance: u8 - readonly isGeneralIndex: boolean - readonly asGeneralIndex: Compact - readonly isGeneralKey: boolean - readonly asGeneralKey: { - readonly length: u8 - readonly data: U8aFixed - } & Struct - readonly isOnlyChild: boolean - readonly isPlurality: boolean - readonly asPlurality: { - readonly id: XcmV3JunctionBodyId - readonly part: XcmV3JunctionBodyPart - } & Struct - readonly isGlobalConsensus: boolean - readonly asGlobalConsensus: XcmV4JunctionNetworkId - readonly type: - | 'Parachain' - | 'AccountId32' - | 'AccountIndex64' - | 'AccountKey20' - | 'PalletInstance' - | 'GeneralIndex' - | 'GeneralKey' - | 'OnlyChild' - | 'Plurality' - | 'GlobalConsensus' -} - -/** @name XcmV4JunctionNetworkId (61) */ -interface XcmV4JunctionNetworkId extends Enum { - readonly isByGenesis: boolean - readonly asByGenesis: U8aFixed - readonly isByFork: boolean - readonly asByFork: { - readonly blockNumber: u64 - readonly blockHash: U8aFixed - } & Struct - readonly isPolkadot: boolean - readonly isKusama: boolean - readonly isWestend: boolean - readonly isRococo: boolean - readonly isWococo: boolean - readonly isEthereum: boolean - readonly asEthereum: { - readonly chainId: Compact - } & Struct - readonly isBitcoinCore: boolean - readonly isBitcoinCash: boolean - readonly isPolkadotBulletin: boolean - readonly type: - | 'ByGenesis' - | 'ByFork' - | 'Polkadot' - | 'Kusama' - | 'Westend' - | 'Rococo' - | 'Wococo' - | 'Ethereum' - | 'BitcoinCore' - | 'BitcoinCash' - | 'PolkadotBulletin' -} - -/** @name XcmV4AssetAssetId (69) */ -interface XcmV4AssetAssetId extends XcmV4Location {} - -export interface VersionedInteriorLocation extends Enum { - readonly isV2: boolean - readonly asV2: XcmV2MultilocationJunctions - readonly isV3: boolean - readonly asV3: XcmV3Junctions - readonly isV4: boolean - readonly asV4: XcmV4Junctions - readonly type: 'V2' | 'V3' | 'V4' -} - -export interface BridgeMessage extends Struct { - readonly universal_dest: VersionedInteriorLocation - readonly message: XcmVersionedXcm -} - -export type BridgeMessageAccepted = { - readonly laneId: BpMessagesLaneId - readonly nonce: u64 -} & Struct - -export type BridgeMessagesDelivered = { - readonly laneId: BpMessagesLaneId - readonly messages: BpMessagesDeliveredMessages -} & Struct - -interface BpMessagesLaneId extends U8aFixed {} - -interface BpMessagesDeliveredMessages extends Struct { - readonly begin: u64 - readonly end: u64 -} - -export interface BpMessagesReceivedMessages extends Struct { - readonly lane: BpMessagesLaneId - readonly receiveResults: Vec> -} - -interface BpMessagesReceivalResult extends Enum { - readonly isDispatched: boolean - readonly asDispatched: BpRuntimeMessagesMessageDispatchResult - readonly isInvalidNonce: boolean - readonly isTooManyUnrewardedRelayers: boolean - readonly isTooManyUnconfirmedMessages: boolean - readonly type: 'Dispatched' | 'InvalidNonce' | 'TooManyUnrewardedRelayers' | 'TooManyUnconfirmedMessages' -} - -interface BpRuntimeMessagesMessageDispatchResult extends Struct { - readonly unspentWeight: SpWeightsWeightV2Weight - readonly dispatchLevelResult: BridgeRuntimeCommonMessagesXcmExtensionXcmBlobMessageDispatchResult -} - -interface BridgeRuntimeCommonMessagesXcmExtensionXcmBlobMessageDispatchResult extends Enum { - readonly isInvalidPayload: boolean - readonly isDispatched: boolean - readonly isNotDispatched: boolean - readonly type: 'InvalidPayload' | 'Dispatched' | 'NotDispatched' -} diff --git a/packages/server/src/agents/xcm/ops/xcmp.spec.ts b/packages/server/src/agents/xcm/ops/xcmp.spec.ts deleted file mode 100644 index 6067fcd2..00000000 --- a/packages/server/src/agents/xcm/ops/xcmp.spec.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { jest } from '@jest/globals' -import { extractEvents } from '@sodazone/ocelloids-sdk' - -import { registry, xcmHop, xcmpReceive, xcmpSend } from '../../../testing/xcm.js' - -import { extractXcmpReceive, extractXcmpSend } from './xcmp.js' - -describe('xcmp operator', () => { - describe('extractXcmpSend', () => { - it('should extract XCMP sent message', (done) => { - const { origin, blocks, getHrmp } = xcmpSend - - const calls = jest.fn() - - const test$ = extractXcmpSend(origin, getHrmp, registry)(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.instructions).toBeDefined() - expect(msg.messageData).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract XCMP sent on hops', (done) => { - const { origin, blocks, getHrmp } = xcmHop - - const calls = jest.fn() - - const test$ = extractXcmpSend(origin, getHrmp, registry)(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.instructions).toBeDefined() - expect(msg.messageData).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(2) - done() - }, - }) - }) - }) - - it('should extract XCMP sent message matching by public key', (done) => { - const { origin, blocks, getHrmp } = xcmpSend - - const calls = jest.fn() - - const test$ = extractXcmpSend(origin, getHrmp, registry)(blocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.instructions).toBeDefined() - expect(msg.messageData).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.recipient).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - describe('extractXcmpReceive', () => { - it('should extract XCMP receive with outcome success', (done) => { - const { successBlocks } = xcmpReceive - - const calls = jest.fn() - - const test$ = extractXcmpReceive()(successBlocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Success') - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract failed XCMP received message with error', (done) => { - const { failBlocks } = xcmpReceive - - const calls = jest.fn() - - const test$ = extractXcmpReceive()(failBlocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Fail') - expect(msg.error).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - - it('should extract assets trapped info on XCMP received message', (done) => { - const { trappedBlocks } = xcmpReceive - - const calls = jest.fn() - - const test$ = extractXcmpReceive()(trappedBlocks.pipe(extractEvents())) - - test$.subscribe({ - next: (msg) => { - expect(msg).toBeDefined() - expect(msg.blockNumber).toBeDefined() - expect(msg.blockHash).toBeDefined() - expect(msg.event).toBeDefined() - expect(msg.messageHash).toBeDefined() - expect(msg.outcome).toBeDefined() - expect(msg.outcome).toBe('Fail') - expect(msg.error).toBeDefined() - expect(msg.assetsTrapped).toBeDefined() - calls() - }, - complete: () => { - expect(calls).toHaveBeenCalledTimes(1) - done() - }, - }) - }) - }) -}) diff --git a/packages/server/src/agents/xcm/ops/xcmp.ts b/packages/server/src/agents/xcm/ops/xcmp.ts deleted file mode 100644 index 9a61051c..00000000 --- a/packages/server/src/agents/xcm/ops/xcmp.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { Registry } from '@polkadot/types/types' -import { Observable, bufferCount, filter, map, mergeMap } from 'rxjs' - -import { filterNonNull, types } from '@sodazone/ocelloids-sdk' - -import { createNetworkId } from '../../../services/config.js' -import { NetworkURN } from '../../../services/types.js' -import { GetOutboundHrmpMessages } from '../types-augmented.js' -import { - GenericXcmInboundWithContext, - GenericXcmSentWithContext, - XcmInboundWithContext, - XcmSentWithContext, -} from '../types.js' -import { MessageQueueEventContext } from '../types.js' -import { blockEventToHuman, xcmMessagesSent } from './common.js' -import { getMessageId, mapAssetsTrapped, matchEvent } from './util.js' -import { fromXcmpFormat } from './xcm-format.js' - -const METHODS_XCMP_QUEUE = ['Success', 'Fail'] - -function findOutboundHrmpMessage( - origin: NetworkURN, - getOutboundHrmpMessages: GetOutboundHrmpMessages, - registry: Registry -) { - return (source: Observable): Observable => { - return source.pipe( - mergeMap((sentMsg): Observable => { - const { blockHash, messageHash, messageId } = sentMsg - return getOutboundHrmpMessages(blockHash).pipe( - map((messages) => { - return messages - .flatMap((msg) => { - const { data, recipient } = msg - // TODO: caching strategy - const xcms = fromXcmpFormat(data, registry) - return xcms.map( - (xcmProgram) => - new GenericXcmSentWithContext({ - ...sentMsg, - messageData: xcmProgram.toU8a(), - recipient: createNetworkId(origin, recipient.toNumber().toString()), - messageHash: xcmProgram.hash.toHex(), - instructions: { - bytes: xcmProgram.toU8a(), - json: xcmProgram.toHuman(), - }, - messageId: getMessageId(xcmProgram), - }) - ) - }) - .find((msg) => { - return messageId ? msg.messageId === messageId : msg.messageHash === messageHash - }) - }), - filterNonNull() - ) - }) - ) - } -} - -export function extractXcmpSend( - origin: NetworkURN, - getOutboundHrmpMessages: GetOutboundHrmpMessages, - registry: Registry -) { - return (source: Observable): Observable => { - return source.pipe( - filter((event) => matchEvent(event, 'xcmpQueue', 'XcmpMessageSent') || matchEvent(event, 'polkadotXcm', 'Sent')), - xcmMessagesSent(), - findOutboundHrmpMessage(origin, getOutboundHrmpMessages, registry) - ) - } -} - -export function extractXcmpReceive() { - return (source: Observable): Observable => { - return source.pipe( - bufferCount(2, 1), - // eslint-disable-next-line complexity - map(([maybeAssetTrapEvent, maybeXcmpEvent]) => { - if (maybeXcmpEvent === undefined) { - return null - } - - const assetTrapEvent = matchEvent(maybeAssetTrapEvent, ['xcmPallet', 'polkadotXcm'], 'AssetsTrapped') - ? maybeAssetTrapEvent - : undefined - const assetsTrapped = mapAssetsTrapped(assetTrapEvent) - - if (matchEvent(maybeXcmpEvent, 'xcmpQueue', METHODS_XCMP_QUEUE)) { - const xcmpQueueData = maybeXcmpEvent.data as any - - return new GenericXcmInboundWithContext({ - event: blockEventToHuman(maybeXcmpEvent), - blockHash: maybeXcmpEvent.blockHash.toHex(), - blockNumber: maybeXcmpEvent.blockNumber.toPrimitive(), - extrinsicId: maybeXcmpEvent.extrinsicId, - messageHash: xcmpQueueData.messageHash.toHex(), - messageId: xcmpQueueData.messageId?.toHex(), - outcome: maybeXcmpEvent.method === 'Success' ? 'Success' : 'Fail', - error: xcmpQueueData.error, - assetsTrapped, - }) - } else if (matchEvent(maybeXcmpEvent, 'messageQueue', 'Processed')) { - const { id, success, error } = maybeXcmpEvent.data as unknown as MessageQueueEventContext - // Received event only emits field `message_id`, - // which is actually the message hash in chains that do not yet support Topic ID. - const messageId = id.toHex() - const messageHash = messageId - - return new GenericXcmInboundWithContext({ - event: blockEventToHuman(maybeXcmpEvent), - blockHash: maybeXcmpEvent.blockHash.toHex(), - blockNumber: maybeXcmpEvent.blockNumber.toPrimitive(), - messageHash, - messageId, - outcome: success?.isTrue ? 'Success' : 'Fail', - error: error ? error.toHuman() : null, - assetsTrapped, - }) - } - - return null - }), - filterNonNull() - ) - } -} diff --git a/packages/server/src/agents/xcm/telemetry/events.ts b/packages/server/src/agents/xcm/telemetry/events.ts deleted file mode 100644 index 6644c5be..00000000 --- a/packages/server/src/agents/xcm/telemetry/events.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { TypedEventEmitter } from '../../../services/types.js' -import { XcmBridge, XcmHop, XcmInbound, XcmRelayed, XcmSent, XcmTimeout } from '../types.js' - -export type TelemetryEvents = { - telemetryInbound: (message: XcmInbound) => void - telemetryOutbound: (message: XcmSent) => void - telemetryRelayed: (relayMsg: XcmRelayed) => void - telemetryMatched: (inMsg: XcmInbound, outMsg: XcmSent) => void - telemetryTimeout: (message: XcmTimeout) => void - telemetryHop: (message: XcmHop) => void - telemetryBridge: (message: XcmBridge) => void - telemetryTrapped: (inMsg: XcmInbound, outMsg: XcmSent) => void -} - -export type TelemetryXCMEventEmitter = TypedEventEmitter diff --git a/packages/server/src/agents/xcm/telemetry/metrics.ts b/packages/server/src/agents/xcm/telemetry/metrics.ts deleted file mode 100644 index 2414b381..00000000 --- a/packages/server/src/agents/xcm/telemetry/metrics.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Counter } from 'prom-client' - -import { XcmBridge, XcmHop, XcmInbound, XcmRelayed, XcmSent, XcmTimeout } from '../types.js' -import { TelemetryXCMEventEmitter } from './events.js' - -export function metrics(source: TelemetryXCMEventEmitter) { - const inCount = new Counter({ - name: 'oc_engine_in_total', - help: 'Matching engine inbound messages.', - labelNames: ['subscription', 'origin', 'outcome'], - }) - const outCount = new Counter({ - name: 'oc_engine_out_total', - help: 'Matching engine outbound messages.', - labelNames: ['subscription', 'origin', 'destination'], - }) - const matchCount = new Counter({ - name: 'oc_engine_matched_total', - help: 'Matching engine matched messages.', - labelNames: ['subscription', 'origin', 'destination', 'outcome'], - }) - const trapCount = new Counter({ - name: 'oc_engine_trapped_total', - help: 'Matching engine matched messages with trapped assets.', - labelNames: ['subscription', 'origin', 'destination', 'outcome'], - }) - const relayCount = new Counter({ - name: 'oc_engine_relayed_total', - help: 'Matching engine relayed messages.', - labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'outcome'], - }) - const timeoutCount = new Counter({ - name: 'oc_engine_timeout_total', - help: 'Matching engine sent timeout messages.', - labelNames: ['subscription', 'origin', 'destination'], - }) - const hopCount = new Counter({ - name: 'oc_engine_hop_total', - help: 'Matching engine hop messages.', - labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'stop', 'outcome', 'direction'], - }) - const bridgeCount = new Counter({ - name: 'oc_engine_bridge_total', - help: 'Matching engine bridge messages.', - labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'stop', 'outcome', 'direction'], - }) - - source.on('telemetryInbound', (message: XcmInbound) => { - inCount.labels(message.subscriptionId, message.chainId, message.outcome.toString()).inc() - }) - - source.on('telemetryOutbound', (message: XcmSent) => { - outCount.labels(message.subscriptionId, message.origin.chainId, message.destination.chainId).inc() - }) - - source.on('telemetryMatched', (inMsg: XcmInbound, outMsg: XcmSent) => { - matchCount - .labels(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.destination.chainId, inMsg.outcome.toString()) - .inc() - }) - - source.on('telemetryRelayed', (relayMsg: XcmRelayed) => { - relayCount - .labels( - relayMsg.subscriptionId, - relayMsg.origin.chainId, - relayMsg.destination.chainId, - relayMsg.waypoint.legIndex.toString(), - relayMsg.waypoint.outcome.toString() - ) - .inc() - }) - - source.on('telemetryTimeout', (msg: XcmTimeout) => { - timeoutCount.labels(msg.subscriptionId, msg.origin.chainId, msg.destination.chainId).inc() - }) - - source.on('telemetryHop', (msg: XcmHop) => { - hopCount - .labels( - msg.subscriptionId, - msg.origin.chainId, - msg.destination.chainId, - msg.waypoint.legIndex.toString(), - msg.waypoint.chainId, - msg.waypoint.outcome.toString(), - msg.direction - ) - .inc() - }) - - source.on('telemetryBridge', (msg: XcmBridge) => { - bridgeCount - .labels( - msg.subscriptionId, - msg.origin.chainId, - msg.destination.chainId, - msg.waypoint.legIndex.toString(), - msg.waypoint.chainId, - msg.waypoint.outcome.toString(), - msg.bridgeMessageType - ) - .inc() - }) - - source.on('telemetryTrapped', (inMsg: XcmInbound, outMsg: XcmSent) => { - trapCount - .labels(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.destination.chainId, inMsg.outcome.toString()) - .inc() - }) -} diff --git a/packages/server/src/agents/xcm/types-augmented.ts b/packages/server/src/agents/xcm/types-augmented.ts deleted file mode 100644 index a97d8b8b..00000000 --- a/packages/server/src/agents/xcm/types-augmented.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Observable } from 'rxjs' - -import type { Bytes, Vec } from '@polkadot/types' -import type { - PolkadotCorePrimitivesInboundDownwardMessage, - PolkadotCorePrimitivesOutboundHrmpMessage, -} from '@polkadot/types/lookup' -import { HexString } from '../../services/monitoring/types.js' -import { NetworkURN } from '../../services/types.js' - -export type GetOutboundHrmpMessages = (hash: HexString) => Observable> - -export type GetOutboundUmpMessages = (hash: HexString) => Observable> - -export type GetDownwardMessageQueues = ( - hash: HexString, - networkId: NetworkURN -) => Observable> - -export type GetStorageAt = (hash: HexString, key: HexString) => Observable diff --git a/packages/server/src/agents/xcm/types.ts b/packages/server/src/agents/xcm/types.ts deleted file mode 100644 index de1c17ab..00000000 --- a/packages/server/src/agents/xcm/types.ts +++ /dev/null @@ -1,797 +0,0 @@ -import type { U8aFixed, bool } from '@polkadot/types-codec' -import type { - FrameSupportMessagesProcessMessageError, - PolkadotRuntimeParachainsInclusionAggregateMessageOrigin, -} from '@polkadot/types/lookup' -import { ControlQuery } from '@sodazone/ocelloids-sdk' -import { z } from 'zod' - -import { createNetworkId } from '../../services/config.js' -import { - AnyJson, - HexString, - RxSubscriptionWithId, - SignerData, - Subscription, - toHexString, -} from '../../services/subscriptions/types.js' -import { NetworkURN } from '../../services/types.js' - -export type Monitor = { - subs: RxSubscriptionWithId[] - controls: Record -} - -function distinct(a: Array) { - return Array.from(new Set(a)) -} - -export type XCMSubscriptionHandler = { - originSubs: RxSubscriptionWithId[] - destinationSubs: RxSubscriptionWithId[] - bridgeSubs: BridgeSubscription[] - sendersControl: ControlQuery - messageControl: ControlQuery - descriptor: Subscription - args: XCMSubscriptionArgs - relaySub?: RxSubscriptionWithId -} - -const bridgeTypes = ['pk-bridge', 'snowbridge'] as const - -export type BridgeType = (typeof bridgeTypes)[number] - -export type BridgeSubscription = { type: BridgeType; subs: RxSubscriptionWithId[] } - -export type XcmCriteria = { - sendersControl: ControlQuery - messageControl: ControlQuery -} - -export type XcmWithContext = { - event?: AnyJson - extrinsicId?: string - blockNumber: string | number - blockHash: HexString - messageHash: HexString - messageId?: HexString -} -/** - * Represents the asset that has been trapped. - * - * @public - */ - -export type TrappedAsset = { - version: number - id: { - type: string - value: AnyJson - } - fungible: boolean - amount: string | number - assetInstance?: AnyJson -} -/** - * Event emitted when assets are trapped. - * - * @public - */ - -export type AssetsTrapped = { - assets: TrappedAsset[] - hash: HexString - event: AnyJson -} -/** - * Represents an XCM program bytes and human JSON. - */ - -export type XcmProgram = { - bytes: Uint8Array - json: AnyJson -} - -export interface XcmSentWithContext extends XcmWithContext { - messageData: Uint8Array - recipient: NetworkURN - sender?: SignerData - instructions: XcmProgram -} - -export interface XcmBridgeAcceptedWithContext extends XcmWithContext { - chainId: NetworkURN - bridgeKey: HexString - messageData: HexString - instructions: AnyJson - recipient: NetworkURN - forwardId?: HexString -} - -export interface XcmBridgeDeliveredWithContext { - chainId: NetworkURN - bridgeKey: HexString - event?: AnyJson - extrinsicId?: string - blockNumber: string | number - blockHash: HexString - sender?: SignerData -} - -export interface XcmBridgeAcceptedWithContext extends XcmWithContext { - chainId: NetworkURN - bridgeKey: HexString - messageData: HexString - instructions: AnyJson - recipient: NetworkURN - forwardId?: HexString -} - -export interface XcmBridgeDeliveredWithContext { - chainId: NetworkURN - bridgeKey: HexString - event?: AnyJson - extrinsicId?: string - blockNumber: string | number - blockHash: HexString - sender?: SignerData -} - -export interface XcmBridgeInboundWithContext { - chainId: NetworkURN - bridgeKey: HexString - blockNumber: string | number - blockHash: HexString - outcome: 'Success' | 'Fail' - error: AnyJson - event?: AnyJson - extrinsicId?: string -} - -export interface XcmBridgeInboundWithContext { - chainId: NetworkURN - bridgeKey: HexString - blockNumber: string | number - blockHash: HexString - outcome: 'Success' | 'Fail' - error: AnyJson - event?: AnyJson - extrinsicId?: string -} - -export interface XcmInboundWithContext extends XcmWithContext { - outcome: 'Success' | 'Fail' - error: AnyJson - assetsTrapped?: AssetsTrapped -} - -export interface XcmRelayedWithContext extends XcmInboundWithContext { - recipient: NetworkURN - origin: NetworkURN -} - -export class GenericXcmRelayedWithContext implements XcmRelayedWithContext { - event: AnyJson - extrinsicId?: string - blockNumber: string | number - blockHash: HexString - messageHash: HexString - messageId?: HexString - recipient: NetworkURN - origin: NetworkURN - outcome: 'Success' | 'Fail' - error: AnyJson - - constructor(msg: XcmRelayedWithContext) { - this.event = msg.event - this.messageHash = msg.messageHash - this.messageId = msg.messageId ?? msg.messageHash - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.recipient = msg.recipient - this.origin = msg.origin - this.outcome = msg.outcome - this.error = msg.error - } - - toHuman(_isExpanded?: boolean | undefined): Record { - return { - messageHash: this.messageHash, - messageId: this.messageId, - extrinsicId: this.extrinsicId, - blockHash: this.blockHash, - blockNumber: this.blockNumber, - event: this.event, - recipient: this.recipient, - origin: this.origin, - outcome: this.outcome, - error: this.error, - } - } -} - -export class GenericXcmInboundWithContext implements XcmInboundWithContext { - event: AnyJson - extrinsicId?: string | undefined - blockNumber: string - blockHash: HexString - messageHash: HexString - messageId: HexString - outcome: 'Success' | 'Fail' - error: AnyJson - assetsTrapped?: AssetsTrapped | undefined - - constructor(msg: XcmInboundWithContext) { - this.event = msg.event - this.messageHash = msg.messageHash - this.messageId = msg.messageId ?? msg.messageHash - this.outcome = msg.outcome - this.error = msg.error - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.assetsTrapped = msg.assetsTrapped - } - - toHuman(_isExpanded?: boolean | undefined): Record { - return { - messageHash: this.messageHash, - messageId: this.messageId, - extrinsicId: this.extrinsicId, - blockHash: this.blockHash, - blockNumber: this.blockNumber, - event: this.event, - outcome: this.outcome, - error: this.error, - assetsTrapped: this.assetsTrapped, - } - } -} - -export class XcmInbound { - subscriptionId: string - chainId: NetworkURN - event: AnyJson - messageHash: HexString - messageId: HexString - outcome: 'Success' | 'Fail' - error: AnyJson - blockHash: HexString - blockNumber: string - extrinsicId?: string - assetsTrapped?: AssetsTrapped - - constructor(subscriptionId: string, chainId: NetworkURN, msg: XcmInboundWithContext) { - this.subscriptionId = subscriptionId - this.chainId = chainId - this.event = msg.event - this.messageHash = msg.messageHash - this.messageId = msg.messageId ?? msg.messageHash - this.outcome = msg.outcome - this.error = msg.error - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.assetsTrapped = msg.assetsTrapped - } -} - -export class GenericXcmSentWithContext implements XcmSentWithContext { - messageData: Uint8Array - recipient: NetworkURN - instructions: XcmProgram - messageHash: HexString - event: AnyJson - blockHash: HexString - blockNumber: string - sender?: SignerData - extrinsicId?: string - messageId?: HexString - - constructor(msg: XcmSentWithContext) { - this.event = msg.event - this.messageData = msg.messageData - this.recipient = msg.recipient - this.instructions = msg.instructions - this.messageHash = msg.messageHash - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.messageId = msg.messageId - this.sender = msg.sender - } - - toHuman(_isExpanded?: boolean | undefined): Record { - return { - messageData: toHexString(this.messageData), - recipient: this.recipient, - instructions: this.instructions.json, - messageHash: this.messageHash, - event: this.event, - blockHash: this.blockHash, - blockNumber: this.blockNumber, - extrinsicId: this.extrinsicId, - messageId: this.messageId, - senders: this.sender, - } - } -} - -export class GenericXcmBridgeAcceptedWithContext implements XcmBridgeAcceptedWithContext { - chainId: NetworkURN - bridgeKey: HexString - messageData: HexString - recipient: NetworkURN - instructions: AnyJson - messageHash: HexString - event: AnyJson - blockHash: HexString - blockNumber: string - extrinsicId?: string - messageId?: HexString - forwardId?: HexString - - constructor(msg: XcmBridgeAcceptedWithContext) { - this.chainId = msg.chainId - this.bridgeKey = msg.bridgeKey - this.event = msg.event - this.messageData = msg.messageData - this.recipient = msg.recipient - this.instructions = msg.instructions - this.messageHash = msg.messageHash - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.messageId = msg.messageId - this.forwardId = msg.forwardId - } -} - -export class GenericXcmBridgeDeliveredWithContext implements XcmBridgeDeliveredWithContext { - chainId: NetworkURN - bridgeKey: HexString - event?: AnyJson - extrinsicId?: string - blockNumber: string - blockHash: HexString - sender?: SignerData - - constructor(msg: XcmBridgeDeliveredWithContext) { - this.chainId = msg.chainId - this.bridgeKey = msg.bridgeKey - this.event = msg.event - this.extrinsicId = msg.extrinsicId - this.blockNumber = msg.blockNumber.toString() - this.blockHash = msg.blockHash - this.sender = msg.sender - } -} - -export class GenericXcmBridgeInboundWithContext implements XcmBridgeInboundWithContext { - chainId: NetworkURN - bridgeKey: HexString - event: AnyJson - extrinsicId?: string | undefined - blockNumber: string - blockHash: HexString - outcome: 'Success' | 'Fail' - error: AnyJson - - constructor(msg: XcmBridgeInboundWithContext) { - this.chainId = msg.chainId - this.event = msg.event - this.outcome = msg.outcome - this.error = msg.error - this.blockHash = msg.blockHash - this.blockNumber = msg.blockNumber.toString() - this.extrinsicId = msg.extrinsicId - this.bridgeKey = msg.bridgeKey - } -} - -export enum XcmNotificationType { - Sent = 'xcm.sent', - Received = 'xcm.received', - Relayed = 'xcm.relayed', - Timeout = 'xcm.timeout', - Hop = 'xcm.hop', - Bridge = 'xcm.bridge', -} - -/** - * The terminal point of an XCM journey. - * - * @public - */ - -export type XcmTerminus = { - chainId: NetworkURN -} -/** - * The terminal point of an XCM journey with contextual information. - * - * @public - */ - -export interface XcmTerminusContext extends XcmTerminus { - blockNumber: string - blockHash: HexString - extrinsicId?: string - event: AnyJson - outcome: 'Success' | 'Fail' - error: AnyJson - messageHash: HexString - messageData: string - instructions: AnyJson -} -/** - * The contextual information of an XCM journey waypoint. - * - * @public - */ - -export interface XcmWaypointContext extends XcmTerminusContext { - legIndex: number - assetsTrapped?: AnyJson -} -/** - * Type of an XCM journey leg. - * - * @public - */ - -export const legType = ['bridge', 'hop', 'hrmp', 'vmp'] as const -/** - * A leg of an XCM journey. - * - * @public - */ - -export type Leg = { - from: NetworkURN - to: NetworkURN - relay?: NetworkURN - type: (typeof legType)[number] -} -/** - * Event emitted when an XCM is sent. - * - * @public - */ - -export interface XcmSent { - type: XcmNotificationType - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString -} - -export class GenericXcmSent implements XcmSent { - type: XcmNotificationType = XcmNotificationType.Sent - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor( - subscriptionId: string, - chainId: NetworkURN, - msg: XcmSentWithContext, - legs: Leg[], - forwardId?: HexString - ) { - this.subscriptionId = subscriptionId - this.legs = legs - this.origin = { - chainId, - blockHash: msg.blockHash, - blockNumber: msg.blockNumber.toString(), - extrinsicId: msg.extrinsicId, - event: msg.event, - outcome: 'Success', - error: null, - messageData: toHexString(msg.messageData), - instructions: msg.instructions.json, - messageHash: msg.messageHash, - } - this.destination = { - chainId: legs[legs.length - 1].to, // last stop is the destination - } - this.waypoint = { - ...this.origin, - legIndex: 0, - messageData: toHexString(msg.messageData), - instructions: msg.instructions.json, - messageHash: msg.messageHash, - } - - this.messageId = msg.messageId - this.forwardId = forwardId - this.sender = msg.sender - } -} -/** - * Event emitted when an XCM is received. - * - * @public - */ - -export interface XcmReceived { - type: XcmNotificationType - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminusContext - sender?: SignerData - messageId?: HexString - forwardId?: HexString -} -/** - * Event emitted when an XCM is not received within a specified timeframe. - * - * @public - */ - -export type XcmTimeout = XcmSent - -export class GenericXcmTimeout implements XcmTimeout { - type: XcmNotificationType = XcmNotificationType.Timeout - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor(msg: XcmSent) { - this.subscriptionId = msg.subscriptionId - this.legs = msg.legs - this.origin = msg.origin - this.destination = msg.destination - this.waypoint = msg.waypoint - this.messageId = msg.messageId - this.sender = msg.sender - this.forwardId = msg.forwardId - } -} - -export class GenericXcmReceived implements XcmReceived { - type: XcmNotificationType = XcmNotificationType.Received - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminusContext - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor(outMsg: XcmSent, inMsg: XcmInbound) { - this.subscriptionId = outMsg.subscriptionId - this.legs = outMsg.legs - this.destination = { - chainId: inMsg.chainId, - blockNumber: inMsg.blockNumber, - blockHash: inMsg.blockHash, - extrinsicId: inMsg.extrinsicId, - event: inMsg.event, - outcome: inMsg.outcome, - error: inMsg.error, - instructions: outMsg.waypoint.instructions, - messageData: outMsg.waypoint.messageData, - messageHash: outMsg.waypoint.messageHash, - } - this.origin = outMsg.origin - this.waypoint = { - ...this.destination, - legIndex: this.legs.findIndex((l) => l.to === inMsg.chainId && l.type !== 'bridge'), - instructions: outMsg.waypoint.instructions, - messageData: outMsg.waypoint.messageData, - messageHash: outMsg.waypoint.messageHash, - assetsTrapped: inMsg.assetsTrapped, - } - this.sender = outMsg.sender - this.messageId = outMsg.messageId - this.forwardId = outMsg.forwardId - } -} -/** - * Event emitted when an XCM is received on the relay chain - * for an HRMP message. - * - * @public - */ - -export type XcmRelayed = XcmSent - -export class GenericXcmRelayed implements XcmRelayed { - type: XcmNotificationType = XcmNotificationType.Relayed - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor(outMsg: XcmSent, relayMsg: XcmRelayedWithContext) { - this.subscriptionId = outMsg.subscriptionId - this.legs = outMsg.legs - this.destination = outMsg.destination - this.origin = outMsg.origin - this.waypoint = { - legIndex: outMsg.legs.findIndex((l) => l.from === relayMsg.origin && l.relay !== undefined), - chainId: createNetworkId(relayMsg.origin, '0'), // relay waypoint always at relay chain - blockNumber: relayMsg.blockNumber.toString(), - blockHash: relayMsg.blockHash, - extrinsicId: relayMsg.extrinsicId, - event: relayMsg.event, - outcome: relayMsg.outcome, - error: relayMsg.error, - instructions: outMsg.waypoint.instructions, - messageData: outMsg.waypoint.messageData, - messageHash: outMsg.waypoint.messageHash, - } - this.sender = outMsg.sender - this.messageId = outMsg.messageId - this.forwardId = outMsg.forwardId - } -} -/** - * Event emitted when an XCM is sent or received on an intermediate stop. - * - * @public - */ - -export interface XcmHop extends XcmSent { - direction: 'out' | 'in' -} - -export class GenericXcmHop implements XcmHop { - type: XcmNotificationType = XcmNotificationType.Hop - direction: 'out' | 'in' - subscriptionId: string - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor(originMsg: XcmSent, hopWaypoint: XcmWaypointContext, direction: 'out' | 'in') { - this.subscriptionId = originMsg.subscriptionId - this.legs = originMsg.legs - this.origin = originMsg.origin - this.destination = originMsg.destination - this.waypoint = hopWaypoint - this.messageId = originMsg.messageId - this.sender = originMsg.sender - this.direction = direction - this.forwardId = originMsg.forwardId - } -} - -export type BridgeMessageType = 'accepted' | 'delivered' | 'received' -/** - * Event emitted when an XCM is sent or received on an intermediate stop. - * - * @public - */ - -export interface XcmBridge extends XcmSent { - bridgeKey: HexString - bridgeMessageType: BridgeMessageType -} -type XcmBridgeContext = { - bridgeMessageType: BridgeMessageType - bridgeKey: HexString - forwardId?: HexString -} - -export class GenericXcmBridge implements XcmBridge { - type: XcmNotificationType = XcmNotificationType.Bridge - bridgeMessageType: BridgeMessageType - subscriptionId: string - bridgeKey: HexString - legs: Leg[] - waypoint: XcmWaypointContext - origin: XcmTerminusContext - destination: XcmTerminus - sender?: SignerData - messageId?: HexString - forwardId?: HexString - - constructor( - originMsg: XcmSent, - waypoint: XcmWaypointContext, - { bridgeKey, bridgeMessageType, forwardId }: XcmBridgeContext - ) { - this.subscriptionId = originMsg.subscriptionId - this.bridgeMessageType = bridgeMessageType - this.legs = originMsg.legs - this.origin = originMsg.origin - this.destination = originMsg.destination - this.waypoint = waypoint - this.messageId = originMsg.messageId - this.sender = originMsg.sender - this.bridgeKey = bridgeKey - this.forwardId = forwardId - } -} - -/** - * The XCM event types. - * - * @public - */ -export type XcmNotifyMessage = XcmSent | XcmReceived | XcmRelayed | XcmHop | XcmBridge - -export function isXcmSent(object: any): object is XcmSent { - return object.type !== undefined && object.type === XcmNotificationType.Sent -} - -export function isXcmReceived(object: any): object is XcmReceived { - return object.type !== undefined && object.type === XcmNotificationType.Received -} - -export function isXcmHop(object: any): object is XcmHop { - return object.type !== undefined && object.type === XcmNotificationType.Hop -} - -export function isXcmRelayed(object: any): object is XcmRelayed { - return object.type !== undefined && object.type === XcmNotificationType.Relayed -} - -const XCM_NOTIFICATION_TYPE_ERROR = `at least 1 event type is required [${Object.values(XcmNotificationType).join( - ',' -)}]` - -const XCM_OUTBOUND_TTL_TYPE_ERROR = 'XCM outbound message TTL should be at least 6 seconds' - -export const $XCMSubscriptionArgs = z.object({ - origin: z - .string({ - required_error: 'origin id is required', - }) - .min(1), - senders: z.optional( - z.literal('*').or(z.array(z.string()).min(1, 'at least 1 sender address is required').transform(distinct)) - ), - destinations: z - .array( - z - .string({ - required_error: 'destination id is required', - }) - .min(1) - ) - .transform(distinct), - bridges: z.optional(z.array(z.enum(bridgeTypes)).min(1, 'Please specify at least one bridge.')), - // prevent using $refs - events: z.optional(z.literal('*').or(z.array(z.nativeEnum(XcmNotificationType)).min(1, XCM_NOTIFICATION_TYPE_ERROR))), - outboundTTL: z.optional(z.number().min(6000, XCM_OUTBOUND_TTL_TYPE_ERROR).max(Number.MAX_SAFE_INTEGER)), -}) - -export type XCMSubscriptionArgs = z.infer - -export type MessageQueueEventContext = { - id: U8aFixed - origin: PolkadotRuntimeParachainsInclusionAggregateMessageOrigin - success?: bool - error?: FrameSupportMessagesProcessMessageError -} diff --git a/packages/server/src/agents/xcm/xcm-agent.ts b/packages/server/src/agents/xcm/xcm-agent.ts deleted file mode 100644 index b444b982..00000000 --- a/packages/server/src/agents/xcm/xcm-agent.ts +++ /dev/null @@ -1,914 +0,0 @@ -import { Registry } from '@polkadot/types-codec/types' -import { ControlQuery, extractEvents, extractTxWithEvents, flattenCalls, types } from '@sodazone/ocelloids-sdk' -import { Observable, filter, from, map, share, switchMap } from 'rxjs' - -import { - $Subscription, - AgentId, - AnyJson, - HexString, - RxSubscriptionWithId, - Subscription, -} from '../../services/subscriptions/types.js' -import { Logger, NetworkURN } from '../../services/types.js' -import { extractXcmpReceive, extractXcmpSend } from './ops/xcmp.js' -import { - $XCMSubscriptionArgs, - BridgeSubscription, - BridgeType, - Monitor, - XCMSubscriptionArgs, - XCMSubscriptionHandler, - XcmBridgeAcceptedWithContext, - XcmBridgeDeliveredWithContext, - XcmBridgeInboundWithContext, - XcmInbound, - XcmInboundWithContext, - XcmNotificationType, - XcmNotifyMessage, - XcmRelayedWithContext, - XcmSentWithContext, -} from './types.js' - -import { SubsStore } from '../../services/persistence/subs.js' -import { MatchingEngine } from './matching.js' - -import { ValidationError, errorMessage } from '../../errors.js' -import { IngressConsumer } from '../../services/ingress/index.js' -import { mapXcmSent } from './ops/common.js' -import { matchMessage, matchSenders, messageCriteria, sendersCriteria } from './ops/criteria.js' -import { extractDmpReceive, extractDmpSend, extractDmpSendByEvent } from './ops/dmp.js' -import { extractRelayReceive } from './ops/relay.js' -import { extractUmpReceive, extractUmpSend } from './ops/ump.js' - -import { Operation, applyPatch } from 'rfc6902' -import { z } from 'zod' -import { getChainId, getConsensus } from '../../services/config.js' -import { - dmpDownwardMessageQueuesKey, - parachainSystemHrmpOutboundMessages, - parachainSystemUpwardMessages, -} from '../../services/subscriptions/storage.js' -import { NotifierHub } from '../../services/notification/index.js' -import { Agent, AgentRuntimeContext } from '../types.js' -import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './ops/pk-bridge.js' -import { getBridgeHubNetworkId } from './ops/util.js' -import { - GetDownwardMessageQueues, - GetOutboundHrmpMessages, - GetOutboundUmpMessages, - GetStorageAt, -} from './types-augmented.js' - -const SUB_ERROR_RETRY_MS = 5000 - -const allowedPaths = ['/senders', '/destinations', '/channels', '/events'] - -function hasOp(patch: Operation[], path: string) { - return patch.some((op) => op.path.startsWith(path)) -} - -export class XCMAgent implements Agent { - readonly #subs: Record = {} - readonly #log: Logger - readonly #engine: MatchingEngine - readonly #timeouts: NodeJS.Timeout[] = [] - readonly #db: SubsStore - readonly #ingress: IngressConsumer - readonly #notifier: NotifierHub - - #shared: { - blockEvents: Record> - blockExtrinsics: Record> - } - - constructor(ctx: AgentRuntimeContext) { - const { log, ingressConsumer, notifier, subsStore } = ctx - - this.#log = log - this.#ingress = ingressConsumer - this.#notifier = notifier - this.#db = subsStore - this.#engine = new MatchingEngine(ctx, this.#onXcmWaypointReached.bind(this)) - - this.#shared = { - blockEvents: {}, - blockExtrinsics: {}, - } - } - - async getAllSubscriptions(): Promise { - return await this.#db.getByAgentId(this.id) - } - - async getSubscriptionById(subscriptionId: string): Promise { - return await this.#db.getById(this.id, subscriptionId) - } - - async update(subscriptionId: string, patch: Operation[]): Promise { - const sub = this.#subs[subscriptionId] - const descriptor = sub.descriptor - - // Check allowed patch ops - const allowedOps = patch.every((op) => allowedPaths.some((s) => op.path.startsWith(s))) - - if (allowedOps) { - applyPatch(descriptor, patch) - $Subscription.parse(descriptor) - const args = $XCMSubscriptionArgs.parse(descriptor.args) - - await this.#db.save(descriptor) - - sub.args = args - sub.descriptor = descriptor - - if (hasOp(patch, '/senders')) { - this.#updateSenders(subscriptionId) - } - - if (hasOp(patch, '/destinations')) { - this.#updateDestinations(subscriptionId) - } - - if (hasOp(patch, '/events')) { - this.#updateEvents(subscriptionId) - } - - return descriptor - } else { - throw Error('Only operations on these paths are allowed: ' + allowedPaths.join(',')) - } - } - - getInputSchema(): z.ZodSchema { - return $XCMSubscriptionArgs - } - - get id(): AgentId { - return 'xcm' - } - - getSubscriptionHandler(id: string): Subscription { - if (this.#subs[id]) { - return this.#subs[id].descriptor - } else { - throw Error('subscription handler not found') - } - } - - async subscribe(s: Subscription): Promise { - const args = $XCMSubscriptionArgs.parse(s.args) - - const origin = args.origin as NetworkURN - const dests = args.destinations as NetworkURN[] - this.#validateChainIds([origin, ...dests]) - - if (!s.ephemeral) { - await this.#db.insert(s) - } - - this.#monitor(s, args) - } - - async unsubscribe(id: string): Promise { - if (this.#subs[id] === undefined) { - this.#log.warn('unsubscribe from a non-existent subscription %s', id) - return - } - - try { - const { - descriptor: { ephemeral }, - args: { origin }, - originSubs, - destinationSubs, - relaySub, - } = this.#subs[id] - - this.#log.info('[%s] unsubscribe %s', origin, id) - - originSubs.forEach(({ sub }) => sub.unsubscribe()) - destinationSubs.forEach(({ sub }) => sub.unsubscribe()) - if (relaySub) { - relaySub.sub.unsubscribe() - } - delete this.#subs[id] - - await this.#engine.clearPendingStates(id) - - if (!ephemeral) { - await this.#db.remove(this.id, id) - } - } catch (error) { - this.#log.error(error, 'Error unsubscribing %s', id) - } - } - async stop(): Promise { - for (const { - descriptor: { id }, - originSubs, - destinationSubs, - relaySub, - } of Object.values(this.#subs)) { - this.#log.info('Unsubscribe %s', id) - - originSubs.forEach(({ sub }) => sub.unsubscribe()) - destinationSubs.forEach(({ sub }) => sub.unsubscribe()) - if (relaySub) { - relaySub.sub.unsubscribe() - } - } - - for (const t of this.#timeouts) { - t.unref() - } - - await this.#engine.stop() - } - - async start(): Promise { - await this.#startNetworkMonitors() - } - - #onXcmWaypointReached(payload: XcmNotifyMessage) { - const { subscriptionId } = payload - if (this.#subs[subscriptionId]) { - const { descriptor, args, sendersControl } = this.#subs[subscriptionId] - if ( - (args.events === undefined || args.events === '*' || args.events.includes(payload.type)) && - matchSenders(sendersControl, payload.sender) - ) { - this.#notifier.notify(descriptor, { - metadata: { - type: payload.type, - subscriptionId, - agentId: this.id, - }, - payload: payload as unknown as AnyJson, - }) - } - } else { - // this could happen with closed ephemeral subscriptions - this.#log.warn('Unable to find descriptor for subscription %s', subscriptionId) - } - } - - /** - * Main monitoring logic. - * - * This method sets up and manages subscriptions for XCM messages based on the provided - * subscription information. It creates subscriptions for both the origin and destination - * networks, monitors XCM message transfers, and emits events accordingly. - * - * @param {Subscription} descriptor - The subscription descriptor. - * @param {XCMSubscriptionArgs} args - The coerced subscription arguments. - * @throws {Error} If there is an error during the subscription setup process. - * @private - */ - #monitor(descriptor: Subscription, args: XCMSubscriptionArgs) { - const { id } = descriptor - - let origMonitor: Monitor = { subs: [], controls: {} } - let destMonitor: Monitor = { subs: [], controls: {} } - const bridgeSubs: BridgeSubscription[] = [] - let relaySub: RxSubscriptionWithId | undefined - - try { - origMonitor = this.#monitorOrigins(descriptor, args) - destMonitor = this.#monitorDestinations(descriptor, args) - } catch (error) { - // Clean up origin subscriptions. - origMonitor.subs.forEach(({ sub }) => { - sub.unsubscribe() - }) - throw error - } - - // Only subscribe to relay events if required by subscription. - // Contained in its own try-catch so it doesn't prevent origin-destination subs in case of error. - if (this.#shouldMonitorRelay(args)) { - try { - relaySub = this.#monitorRelay(descriptor, args) - } catch (error) { - // log instead of throw to not block OD subscriptions - this.#log.error(error, 'Error on relay subscription (%s)', id) - } - } - - if (args.bridges !== undefined) { - if (args.bridges.includes('pk-bridge')) { - try { - bridgeSubs.push(this.#monitorPkBridge(descriptor, args)) - } catch (error) { - // log instead of throw to not block OD subscriptions - this.#log.error(error, 'Error on bridge subscription (%s)', id) - } - } - } - - const { sendersControl, messageControl } = origMonitor.controls - - this.#subs[id] = { - descriptor, - args, - sendersControl, - messageControl, - originSubs: origMonitor.subs, - destinationSubs: destMonitor.subs, - bridgeSubs, - relaySub, - } - } - - /** - * Set up inbound monitors for XCM protocols. - * - * @private - */ - #monitorDestinations({ id }: Subscription, { origin, destinations }: XCMSubscriptionArgs): Monitor { - const subs: RxSubscriptionWithId[] = [] - const originId = origin as NetworkURN - try { - for (const dest of destinations as NetworkURN[]) { - const chainId = dest - if (this.#subs[id]?.destinationSubs.find((s) => s.chainId === chainId)) { - // Skip existing subscriptions - // for the same destination chain - continue - } - - const inboundObserver = { - error: (error: any) => { - this.#log.error(error, '[%s] error on destination subscription %s', chainId, id) - - /*this.emit('telemetrySubscriptionError', { - subscriptionId: id, - chainId, - direction: 'in', - })*/ - - // try recover inbound subscription - if (this.#subs[id]) { - const { destinationSubs } = this.#subs[id] - const index = destinationSubs.findIndex((s) => s.chainId === chainId) - if (index > -1) { - destinationSubs.splice(index, 1) - this.#timeouts.push( - setTimeout(() => { - this.#log.info( - '[%s] UPDATE destination subscription %s due error %s', - chainId, - id, - errorMessage(error) - ) - const updated = this.#updateDestinationSubscriptions(id) - this.#subs[id].destinationSubs = updated - }, SUB_ERROR_RETRY_MS) - ) - } - } - }, - } - - if (this.#ingress.isRelay(dest)) { - // VMP UMP - this.#log.info('[%s] subscribe inbound UMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#sharedBlockEvents(chainId) - .pipe(extractUmpReceive(originId), this.#emitInbound(id, chainId)) - .subscribe(inboundObserver), - }) - } else if (this.#ingress.isRelay(originId)) { - // VMP DMP - this.#log.info('[%s] subscribe inbound DMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#sharedBlockEvents(chainId) - .pipe(extractDmpReceive(), this.#emitInbound(id, chainId)) - .subscribe(inboundObserver), - }) - } else { - // Inbound HRMP / XCMP transport - this.#log.info('[%s] subscribe inbound HRMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#sharedBlockEvents(chainId) - .pipe(extractXcmpReceive(), this.#emitInbound(id, chainId)) - .subscribe(inboundObserver), - }) - } - } - } catch (error) { - // Clean up subscriptions. - subs.forEach(({ sub }) => { - sub.unsubscribe() - }) - throw error - } - - return { subs, controls: {} } - } - - /** - * Set up outbound monitors for XCM protocols. - * - * @private - */ - #monitorOrigins({ id }: Subscription, { origin, senders, destinations }: XCMSubscriptionArgs): Monitor { - const subs: RxSubscriptionWithId[] = [] - const chainId = origin as NetworkURN - - if (this.#subs[id]?.originSubs.find((s) => s.chainId === chainId)) { - throw new Error(`Fatal: duplicated origin monitor ${id} for chain ${chainId}`) - } - - const sendersControl = ControlQuery.from(sendersCriteria(senders)) - const messageControl = ControlQuery.from(messageCriteria(destinations as NetworkURN[])) - - const outboundObserver = { - error: (error: any) => { - this.#log.error(error, '[%s] error on origin subscription %s', chainId, id) - /* - this.emit('telemetrySubscriptionError', { - subscriptionId: id, - chainId, - direction: 'out', - })*/ - - // try recover outbound subscription - // note: there is a single origin per outbound - if (this.#subs[id]) { - const { originSubs, descriptor, args } = this.#subs[id] - const index = originSubs.findIndex((s) => s.chainId === chainId) - if (index > -1) { - this.#subs[id].originSubs = [] - this.#timeouts.push( - setTimeout(() => { - if (this.#subs[id]) { - this.#log.info('[%s] UPDATE origin subscription %s due error %s', chainId, id, errorMessage(error)) - const { subs: updated, controls } = this.#monitorOrigins(descriptor, args) - this.#subs[id].sendersControl = controls.sendersControl - this.#subs[id].messageControl = controls.messageControl - this.#subs[id].originSubs = updated - } - }, SUB_ERROR_RETRY_MS) - ) - } - } - }, - } - - try { - if (this.#ingress.isRelay(chainId)) { - // VMP DMP - this.#log.info('[%s] subscribe outbound DMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#ingress - .getRegistry(chainId) - .pipe( - switchMap((registry) => - this.#sharedBlockExtrinsics(chainId).pipe( - extractDmpSend(chainId, this.#getDmp(chainId, registry), registry), - this.#emitOutbound(id, chainId, registry, messageControl) - ) - ) - ) - .subscribe(outboundObserver), - }) - - // VMP DMP - this.#log.info('[%s] subscribe outbound DMP - by event (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#ingress - .getRegistry(chainId) - .pipe( - switchMap((registry) => - this.#sharedBlockEvents(chainId).pipe( - extractDmpSendByEvent(chainId, this.#getDmp(chainId, registry), registry), - this.#emitOutbound(id, chainId, registry, messageControl) - ) - ) - ) - .subscribe(outboundObserver), - }) - } else { - // Outbound HRMP / XCMP transport - this.#log.info('[%s] subscribe outbound HRMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#ingress - .getRegistry(chainId) - .pipe( - switchMap((registry) => - this.#sharedBlockEvents(chainId).pipe( - extractXcmpSend(chainId, this.#getHrmp(chainId, registry), registry), - this.#emitOutbound(id, chainId, registry, messageControl) - ) - ) - ) - .subscribe(outboundObserver), - }) - - // VMP UMP - this.#log.info('[%s] subscribe outbound UMP (%s)', chainId, id) - - subs.push({ - chainId, - sub: this.#ingress - .getRegistry(chainId) - .pipe( - switchMap((registry) => - this.#sharedBlockEvents(chainId).pipe( - extractUmpSend(chainId, this.#getUmp(chainId, registry), registry), - this.#emitOutbound(id, chainId, registry, messageControl) - ) - ) - ) - .subscribe(outboundObserver), - }) - } - } catch (error) { - // Clean up subscriptions. - subs.forEach(({ sub }) => { - sub.unsubscribe() - }) - throw error - } - - return { - subs, - controls: { - sendersControl, - messageControl, - }, - } - } - - #monitorRelay({ id }: Subscription, { origin, destinations }: XCMSubscriptionArgs) { - const chainId = origin as NetworkURN - if (this.#subs[id]?.relaySub) { - this.#log.debug('Relay subscription already exists.') - } - const messageControl = ControlQuery.from(messageCriteria(destinations as NetworkURN[])) - - const emitRelayInbound = () => (source: Observable) => - source.pipe(switchMap((message) => from(this.#engine.onRelayedMessage(id, message)))) - - const relayObserver = { - error: (error: any) => { - this.#log.error(error, '[%s] error on relay subscription s', chainId, id) - /* - this.emit('telemetrySubscriptionError', { - subscriptionId: id, - chainId, - direction: 'relay', - })*/ - - // try recover relay subscription - // there is only one subscription per subscription ID for relay - if (this.#subs[id]) { - const sub = this.#subs[id] - this.#timeouts.push( - setTimeout(async () => { - this.#log.info('[%s] UPDATE relay subscription %s due error %s', chainId, id, errorMessage(error)) - const updatedSub = await this.#monitorRelay(sub.descriptor, sub.args) - sub.relaySub = updatedSub - }, SUB_ERROR_RETRY_MS) - ) - } - }, - } - - // TODO: should resolve relay id for consensus in context - const relayIds = this.#ingress.getRelayIds() - const relayId = relayIds.find((r) => getConsensus(r) === getConsensus(chainId)) - - if (relayId === undefined) { - throw new Error(`No relay ID found for chain ${chainId}`) - } - this.#log.info('[%s] subscribe relay %s xcm events (%s)', chainId, relayId, id) - return { - chainId, - sub: this.#ingress - .getRegistry(relayId) - .pipe( - switchMap((registry) => - this.#sharedBlockExtrinsics(relayId).pipe( - extractRelayReceive(chainId, messageControl, registry), - emitRelayInbound() - ) - ) - ) - .subscribe(relayObserver), - } - } - - // Assumes only 1 pair of bridge hub origin-destination is possible - // TODO: handle possible multiple different consensus utilizing PK bridge e.g. solochains? - #monitorPkBridge({ id }: Subscription, { origin, destinations }: XCMSubscriptionArgs) { - const originBridgeHub = getBridgeHubNetworkId(origin as NetworkURN) - const dest = (destinations as NetworkURN[]).find((d) => getConsensus(d) !== getConsensus(origin as NetworkURN)) - - if (dest === undefined) { - throw new Error(`No destination on different consensus found for bridging (sub=${id})`) - } - - const destBridgeHub = getBridgeHubNetworkId(dest) - - if (originBridgeHub === undefined || destBridgeHub === undefined) { - throw new Error( - `Unable to subscribe to PK bridge due to missing bridge hub network URNs for origin=${origin} and destinations=${destinations}. (sub=${id})` - ) - } - - if (this.#subs[id]?.bridgeSubs.find((s) => s.type === 'pk-bridge')) { - throw new Error(`Fatal: duplicated PK bridge monitor ${id}`) - } - - const type: BridgeType = 'pk-bridge' - - const emitBridgeOutboundAccepted = () => (source: Observable) => - source.pipe(switchMap((message) => from(this.#engine.onBridgeOutboundAccepted(id, message)))) - - const emitBridgeOutboundDelivered = () => (source: Observable) => - source.pipe(switchMap((message) => from(this.#engine.onBridgeOutboundDelivered(id, message)))) - - const emitBridgeInbound = () => (source: Observable) => - source.pipe(switchMap((message) => from(this.#engine.onBridgeInbound(id, message)))) - - const pkBridgeObserver = { - error: (error: any) => { - this.#log.error(error, '[%s] error on PK bridge subscription s', originBridgeHub, id) - // this.emit('telemetrySubscriptionError', { - // subscriptionId: id, - // chainId: originBridgeHub, - // direction: 'bridge', - // }); - - // try recover pk bridge subscription - if (this.#subs[id]) { - const sub = this.#subs[id] - const { bridgeSubs } = sub - const index = bridgeSubs.findIndex((s) => s.type === 'pk-bridge') - if (index > -1) { - bridgeSubs.splice(index, 1) - this.#timeouts.push( - setTimeout(() => { - this.#log.info( - '[%s] UPDATE destination subscription %s due error %s', - originBridgeHub, - id, - errorMessage(error) - ) - bridgeSubs.push(this.#monitorPkBridge(sub.descriptor, sub.args)) - sub.bridgeSubs = bridgeSubs - }, SUB_ERROR_RETRY_MS) - ) - } - } - }, - } - - this.#log.info( - '[%s] subscribe PK bridge outbound accepted events on bridge hub %s (%s)', - origin, - originBridgeHub, - id - ) - const outboundAccepted: RxSubscriptionWithId = { - chainId: originBridgeHub, - sub: this.#ingress - .getRegistry(originBridgeHub) - .pipe( - switchMap((registry) => - this.#sharedBlockEvents(originBridgeHub).pipe( - extractBridgeMessageAccepted(originBridgeHub, registry, this.#getStorageAt(originBridgeHub)), - emitBridgeOutboundAccepted() - ) - ) - ) - .subscribe(pkBridgeObserver), - } - - this.#log.info( - '[%s] subscribe PK bridge outbound delivered events on bridge hub %s (%s)', - origin, - originBridgeHub, - id - ) - const outboundDelivered: RxSubscriptionWithId = { - chainId: originBridgeHub, - sub: this.#ingress - .getRegistry(originBridgeHub) - .pipe( - switchMap((registry) => - this.#sharedBlockEvents(originBridgeHub).pipe( - extractBridgeMessageDelivered(originBridgeHub, registry), - emitBridgeOutboundDelivered() - ) - ) - ) - .subscribe(pkBridgeObserver), - } - - this.#log.info('[%s] subscribe PK bridge inbound events on bridge hub %s (%s)', origin, destBridgeHub, id) - const inbound: RxSubscriptionWithId = { - chainId: destBridgeHub, - sub: this.#sharedBlockEvents(destBridgeHub) - .pipe(extractBridgeReceive(destBridgeHub), emitBridgeInbound()) - .subscribe(pkBridgeObserver), - } - - return { - type, - subs: [outboundAccepted, outboundDelivered, inbound], - } - } - - #updateDestinationSubscriptions(id: string) { - const { descriptor, args, destinationSubs } = this.#subs[id] - // Subscribe to new destinations, if any - const { subs } = this.#monitorDestinations(descriptor, args) - const updatedSubs = destinationSubs.concat(subs) - // Unsubscribe removed destinations, if any - const removed = updatedSubs.filter((s) => !args.destinations.includes(s.chainId)) - removed.forEach(({ sub }) => sub.unsubscribe()) - // Return list of updated subscriptions - return updatedSubs.filter((s) => !removed.includes(s)) - } - - /** - * Starts collecting XCM messages. - * - * Monitors all the active subscriptions. - * - * @private - */ - async #startNetworkMonitors() { - const subs = await this.#db.getByAgentId(this.id) - - this.#log.info('[%s] #subscriptions %d', this.id, subs.length) - - for (const sub of subs) { - try { - this.#monitor(sub, $XCMSubscriptionArgs.parse(sub.args)) - } catch (err) { - this.#log.error(err, 'Unable to create subscription: %j', sub) - } - } - } - - #sharedBlockEvents(chainId: NetworkURN): Observable { - if (!this.#shared.blockEvents[chainId]) { - this.#shared.blockEvents[chainId] = this.#ingress.finalizedBlocks(chainId).pipe(extractEvents(), share()) - } - return this.#shared.blockEvents[chainId] - } - - #sharedBlockExtrinsics(chainId: NetworkURN): Observable { - if (!this.#shared.blockExtrinsics[chainId]) { - this.#shared.blockExtrinsics[chainId] = this.#ingress - .finalizedBlocks(chainId) - .pipe(extractTxWithEvents(), flattenCalls(), share()) - } - return this.#shared.blockExtrinsics[chainId] - } - - /** - * Checks if relayed HRMP messages should be monitored. - * - * All of the following conditions needs to be met: - * 1. `xcm.relayed` notification event is requested in the subscription - * 2. Origin chain is not a relay chain - * 3. At least one destination chain is a parachain - * - * @param Subscription - * @returns boolean - */ - #shouldMonitorRelay({ origin, destinations, events }: XCMSubscriptionArgs) { - return ( - (events === undefined || events === '*' || events.includes(XcmNotificationType.Relayed)) && - !this.#ingress.isRelay(origin as NetworkURN) && - destinations.some((d) => !this.#ingress.isRelay(d as NetworkURN)) - ) - } - - #emitInbound(id: string, chainId: NetworkURN) { - return (source: Observable) => - source.pipe(switchMap((msg) => from(this.#engine.onInboundMessage(new XcmInbound(id, chainId, msg))))) - } - - #emitOutbound(id: string, origin: NetworkURN, registry: Registry, messageControl: ControlQuery) { - const { - args: { outboundTTL }, - } = this.#subs[id] - - return (source: Observable) => - source.pipe( - mapXcmSent(id, registry, origin), - filter((msg) => matchMessage(messageControl, msg)), - switchMap((outbound) => from(this.#engine.onOutboundMessage(outbound, outboundTTL))) - ) - } - - #getDmp(chainId: NetworkURN, registry: Registry): GetDownwardMessageQueues { - return (blockHash: HexString, networkId: NetworkURN) => { - const paraId = getChainId(networkId) - return from(this.#ingress.getStorage(chainId, dmpDownwardMessageQueuesKey(registry, paraId), blockHash)).pipe( - map((buffer) => { - return registry.createType('Vec', buffer) - }) - ) - } - } - - #getUmp(chainId: NetworkURN, registry: Registry): GetOutboundUmpMessages { - return (blockHash: HexString) => { - return from(this.#ingress.getStorage(chainId, parachainSystemUpwardMessages, blockHash)).pipe( - map((buffer) => { - return registry.createType('Vec', buffer) - }) - ) - } - } - - #getHrmp(chainId: NetworkURN, registry: Registry): GetOutboundHrmpMessages { - return (blockHash: HexString) => { - return from(this.#ingress.getStorage(chainId, parachainSystemHrmpOutboundMessages, blockHash)).pipe( - map((buffer) => { - return registry.createType('Vec', buffer) - }) - ) - } - } - - #getStorageAt(chainId: NetworkURN): GetStorageAt { - return (blockHash: HexString, key: HexString) => { - return from(this.#ingress.getStorage(chainId, key, blockHash)) - } - } - - /** - * Updates the senders control handler. - * - * Applies to the outbound extrinsic signers. - */ - #updateSenders(id: string) { - const { - args: { senders }, - sendersControl, - } = this.#subs[id] - - sendersControl.change(sendersCriteria(senders)) - } - - /** - * Updates the message control handler. - * - * Updates the destination subscriptions. - */ - #updateDestinations(id: string) { - const { args, messageControl } = this.#subs[id] - - messageControl.change(messageCriteria(args.destinations as NetworkURN[])) - - const updatedSubs = this.#updateDestinationSubscriptions(id) - this.#subs[id].destinationSubs = updatedSubs - } - - /** - * Updates the subscription to relayed HRMP messages in the relay chain. - */ - #updateEvents(id: string) { - const { descriptor, args, relaySub } = this.#subs[id] - - if (this.#shouldMonitorRelay(args) && relaySub === undefined) { - try { - this.#subs[id].relaySub = this.#monitorRelay(descriptor, args) - } catch (error) { - // log instead of throw to not block OD subscriptions - this.#log.error(error, 'Error on relay subscription (%s)', id) - } - } else if (!this.#shouldMonitorRelay(args) && relaySub !== undefined) { - relaySub.sub.unsubscribe() - delete this.#subs[id].relaySub - } - } - - #validateChainIds(chainIds: NetworkURN[]) { - chainIds.forEach((chainId) => { - if (!this.#ingress.isNetworkDefined(chainId)) { - throw new ValidationError('Invalid chain id:' + chainId) - } - }) - } -} diff --git a/packages/server/src/lib.ts b/packages/server/src/lib.ts index 82ab3005..cfaa9c56 100644 --- a/packages/server/src/lib.ts +++ b/packages/server/src/lib.ts @@ -6,8 +6,12 @@ export type { AnyJson, HexString, SignerData, -} from './services/monitoring/types.js' +} from './services/subscriptions/types.js' +/** + * XCM agent types + * TODO: should be moved + */ export type { XcmReceived, XcmRelayed, @@ -23,4 +27,4 @@ export type { XcmTerminus, XcmTerminusContext, XcmWaypointContext, -} from './agents/xcm/types.js' +} from './services/agents/xcm/types.js' diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index f4cd6292..2dbd2cb1 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -11,18 +11,18 @@ import FastifySwaggerUI from '@fastify/swagger-ui' import FastifyWebsocket from '@fastify/websocket' import FastifyHealthcheck from 'fastify-healthcheck' -import AgentService from './agents/plugin.js' import { logger } from './environment.js' import { errorHandler } from './errors.js' import { Administration, + Agents, Auth, Configuration, Connector, Ingress, Persistence, Root, - Monitoring as Subscriptions, + Subscriptions, Telemetry, } from './services/index.js' import version from './version.js' @@ -161,7 +161,7 @@ export async function createServer(opts: ServerOptions) { await server.register(Persistence, opts) await server.register(Ingress, opts) - await server.register(AgentService, opts) + await server.register(Agents, opts) await server.register(Subscriptions, opts) await server.register(Administration) await server.register(Telemetry, opts) diff --git a/packages/server/src/services/index.ts b/packages/server/src/services/index.ts index f8c7c4a3..01927b8e 100644 --- a/packages/server/src/services/index.ts +++ b/packages/server/src/services/index.ts @@ -1,13 +1,14 @@ import Administration from './admin/routes.js' +import Agents from './agents/plugin.js' import Auth from './auth.js' import Configuration from './config.js' import Ingress from './ingress/consumer/plugin.js' -import Monitoring from './monitoring/plugin.js' import Connector from './networking/plugin.js' import Persistence from './persistence/plugin.js' import Root from './root.js' +import Subscriptions from './subscriptions/plugin.js' import Telemetry from './telemetry/plugin.js' export * from './types.js' -export { Root, Auth, Administration, Persistence, Connector, Configuration, Monitoring, Telemetry, Ingress } +export { Root, Auth, Administration, Persistence, Connector, Configuration, Agents, Subscriptions, Telemetry, Ingress } diff --git a/packages/server/src/services/ingress/consumer/index.ts b/packages/server/src/services/ingress/consumer/index.ts index c026e3bd..a9aa561a 100644 --- a/packages/server/src/services/ingress/consumer/index.ts +++ b/packages/server/src/services/ingress/consumer/index.ts @@ -21,7 +21,7 @@ import { } from '../distributor.js' import { ServiceConfiguration, isNetworkDefined, isRelay } from '../../config.js' -import { HexString } from '../../monitoring/types.js' +import { HexString } from '../../subscriptions/types.js' import { TelemetryCollect, TelemetryEventEmitter } from '../../telemetry/types.js' import { decodeSignedBlockExtended } from '../watcher/codec.js' import { HeadCatcher } from '../watcher/head-catcher.js' diff --git a/packages/server/src/services/ingress/producer/index.ts b/packages/server/src/services/ingress/producer/index.ts index defc89b9..908e021d 100644 --- a/packages/server/src/services/ingress/producer/index.ts +++ b/packages/server/src/services/ingress/producer/index.ts @@ -5,7 +5,7 @@ import { Subscription as RxSubscription } from 'rxjs' import { IngressOptions } from '../../../types.js' import { ServiceConfiguration } from '../../config.js' -import { HexString } from '../../monitoring/types.js' +import { HexString } from '../../subscriptions/types.js' import { Logger, NetworkURN, Services } from '../../types.js' import { NetworkEntry, diff --git a/packages/server/src/services/ingress/watcher/codec.ts b/packages/server/src/services/ingress/watcher/codec.ts index 73526d31..7e6ad0a6 100644 --- a/packages/server/src/services/ingress/watcher/codec.ts +++ b/packages/server/src/services/ingress/watcher/codec.ts @@ -6,7 +6,7 @@ import type { AccountId, EventRecord } from '@polkadot/types/interfaces' import { createSignedBlockExtended } from '@polkadot/api-derive' -import { BinBlock } from '../../monitoring/types.js' +import { BinBlock } from '../../subscriptions/types.js' export function decodeSignedBlockExtended(registry: Registry, buffer: Buffer | Uint8Array) { const binBlock: BinBlock = decode(buffer) diff --git a/packages/server/src/services/ingress/watcher/head-catcher.ts b/packages/server/src/services/ingress/watcher/head-catcher.ts index 57c75e54..da8591ac 100644 --- a/packages/server/src/services/ingress/watcher/head-catcher.ts +++ b/packages/server/src/services/ingress/watcher/head-catcher.ts @@ -26,7 +26,7 @@ import type { Header } from '@polkadot/types/interfaces' import { SubstrateApis, blockFromHeader, finalizedHeads, retryWithTruncatedExpBackoff } from '@sodazone/ocelloids-sdk' import { ServiceConfiguration } from '../../config.js' -import { BlockNumberRange, ChainHead as ChainTip, HexString } from '../../monitoring/types.js' +import { BlockNumberRange, ChainHead as ChainTip, HexString } from '../../subscriptions/types.js' import { TelemetryEventEmitter } from '../../telemetry/types.js' import { DB, Logger, NetworkURN, Services, jsonEncoded, prefixes } from '../../types.js' diff --git a/packages/server/src/services/ingress/watcher/local-cache.ts b/packages/server/src/services/ingress/watcher/local-cache.ts index e9d29735..56e50e20 100644 --- a/packages/server/src/services/ingress/watcher/local-cache.ts +++ b/packages/server/src/services/ingress/watcher/local-cache.ts @@ -10,9 +10,9 @@ import type { Raw } from '@polkadot/types' import type { Hash } from '@polkadot/types/interfaces' import { NetworkConfiguration } from '../../config.js' -import { parachainSystemHrmpOutboundMessages, parachainSystemUpwardMessages } from '../../monitoring/storage.js' -import { HexString } from '../../monitoring/types.js' import { Janitor } from '../../persistence/janitor.js' +import { parachainSystemHrmpOutboundMessages, parachainSystemUpwardMessages } from '../../subscriptions/storage.js' +import { HexString } from '../../subscriptions/types.js' import { TelemetryEventEmitter } from '../../telemetry/types.js' import { DB, Logger, NetworkURN, Services, prefixes } from '../../types.js' diff --git a/packages/server/src/services/notification/log.ts b/packages/server/src/services/notification/log.ts index 014172ea..51ea842c 100644 --- a/packages/server/src/services/notification/log.ts +++ b/packages/server/src/services/notification/log.ts @@ -1,14 +1,5 @@ import EventEmitter from 'node:events' -import { - XcmHop, - XcmNotificationType, - XcmNotifyMessage, - isXcmHop, - isXcmReceived, - isXcmRelayed, - isXcmSent, -} from 'agents/xcm/types.js' import { Logger, Services } from '../../services/types.js' import { Subscription } from '../subscriptions/types.js' import { NotifierHub } from './hub.js' @@ -25,7 +16,7 @@ export class LogNotifier extends (EventEmitter as new () => NotifierEmitter) imp hub.on('log', this.notify.bind(this)) } - notify(sub: Subscription, msg: NotifyMessage) { + notify(_sub: Subscription, msg: NotifyMessage) { this.#log.info( 'NOTIFICATION %s agent=%s subscription=%s, payload=%j', msg.metadata.type, @@ -33,64 +24,5 @@ export class LogNotifier extends (EventEmitter as new () => NotifierEmitter) imp msg.metadata.subscriptionId, msg.payload ) - - if (isXcmReceived(msg)) { - this.#log.info( - '[%s ➜ %s] NOTIFICATION %s subscription=%s, messageHash=%s, outcome=%s (o: #%s, d: #%s)', - msg.origin.chainId, - msg.destination.chainId, - msg.type, - sub.id, - msg.waypoint.messageHash, - msg.waypoint.outcome, - msg.origin.blockNumber, - msg.destination.blockNumber - ) - } else if (isXcmHop(msg)) { - this.#notifyHop(sub, msg) - } else if (isXcmRelayed(msg) && msg.type === XcmNotificationType.Relayed) { - this.#log.info( - '[%s ↠ %s] NOTIFICATION %s subscription=%s, messageHash=%s, block=%s', - msg.origin.chainId, - msg.destination.chainId, - msg.type, - sub.id, - msg.waypoint.messageHash, - msg.waypoint.blockNumber - ) - } else if (isXcmSent(msg)) { - this.#log.info( - '[%s ➜] NOTIFICATION %s subscription=%s, messageHash=%s, block=%s', - msg.origin.chainId, - msg.type, - sub.id, - msg.waypoint.messageHash, - msg.origin.blockNumber - ) - } - } - - #notifyHop(sub: Subscription, msg: XcmHop) { - if (msg.direction === 'out') { - this.#log.info( - '[%s ↷] NOTIFICATION %s-%s subscription=%s, messageHash=%s, block=%s', - msg.waypoint.chainId, - msg.type, - msg.direction, - sub.id, - msg.waypoint.messageHash, - msg.waypoint.blockNumber - ) - } else if (msg.direction === 'in') { - this.#log.info( - '[↷ %s] NOTIFICATION %s-%s subscription=%s, messageHash=%s, block=%s', - msg.waypoint.chainId, - msg.type, - msg.direction, - sub.id, - msg.waypoint.messageHash, - msg.waypoint.blockNumber - ) - } } } diff --git a/packages/server/src/services/notification/types.ts b/packages/server/src/services/notification/types.ts index 02b768d6..0a551aa6 100644 --- a/packages/server/src/services/notification/types.ts +++ b/packages/server/src/services/notification/types.ts @@ -1,5 +1,5 @@ import { TypedEventEmitter } from '../index.js' -import { AnyJson, Subscription } from '../monitoring/types.js' +import { AnyJson, Subscription } from '../subscriptions/types.js' import { TelemetryNotifierEvents } from '../telemetry/types.js' export type NotifyMessage = { diff --git a/packages/server/src/services/subscriptions/switchboard.ts b/packages/server/src/services/subscriptions/switchboard.ts index e1942edc..b4a812a8 100644 --- a/packages/server/src/services/subscriptions/switchboard.ts +++ b/packages/server/src/services/subscriptions/switchboard.ts @@ -5,7 +5,7 @@ import { Operation } from 'rfc6902' import { Logger, Services } from '../types.js' import { AgentId, NotificationListener, Subscription, SubscriptionStats } from './types.js' -import { AgentService } from '../../agents/types.js' +import { AgentService } from '../agents/types.js' import { NotifierEvents } from '../notification/types.js' import { TelemetryCollect, TelemetryEventEmitter } from '../telemetry/types.js' diff --git a/packages/server/src/services/telemetry/metrics/index.ts b/packages/server/src/services/telemetry/metrics/index.ts index 47c15778..d4115e52 100644 --- a/packages/server/src/services/telemetry/metrics/index.ts +++ b/packages/server/src/services/telemetry/metrics/index.ts @@ -1,8 +1,8 @@ import { IngressConsumer } from '../../ingress/index.js' import IngressProducer from '../../ingress/producer/index.js' import { HeadCatcher } from '../../ingress/watcher/head-catcher.js' -import { Switchboard } from '../../subscriptions/switchboard.js' import { NotifierHub } from '../../notification/hub.js' +import { Switchboard } from '../../subscriptions/switchboard.js' import { TelemetryEventEmitter } from '../types.js' import { catcherMetrics } from './catcher.js' import { ingressConsumerMetrics, ingressProducerMetrics } from './ingress.js' diff --git a/packages/server/src/services/telemetry/metrics/switchboard.ts b/packages/server/src/services/telemetry/metrics/switchboard.ts index 281bc113..154954e9 100644 --- a/packages/server/src/services/telemetry/metrics/switchboard.ts +++ b/packages/server/src/services/telemetry/metrics/switchboard.ts @@ -1,6 +1,6 @@ import { Counter, Gauge } from 'prom-client' -import { Switchboard } from '../../monitoring/switchboard.js' +import { Switchboard } from '../../subscriptions/switchboard.js' export function switchboardMetrics(switchboard: Switchboard) { const subsErrors = new Counter({ diff --git a/packages/server/src/services/telemetry/types.ts b/packages/server/src/services/telemetry/types.ts index e4e59589..2f9c145a 100644 --- a/packages/server/src/services/telemetry/types.ts +++ b/packages/server/src/services/telemetry/types.ts @@ -1,7 +1,7 @@ import type { Header } from '@polkadot/types/interfaces' -import { Subscription } from '../subscriptions/types.js' import { NotifyMessage } from '../notification/types.js' +import { Subscription } from '../subscriptions/types.js' import { TypedEventEmitter } from '../types.js' export type NotifyTelemetryMessage = { diff --git a/packages/server/src/services/types.ts b/packages/server/src/services/types.ts index cd8f0861..f6a925fc 100644 --- a/packages/server/src/services/types.ts +++ b/packages/server/src/services/types.ts @@ -1,14 +1,14 @@ import { AbstractBatchOperation, AbstractLevel, AbstractSublevel } from 'abstract-level' -import { AgentService } from 'agents/types.js' import { FastifyBaseLogger } from 'fastify' +import { AgentService } from './agents/types.js' import { ServiceConfiguration } from './config.js' import { IngressConsumer } from './ingress/consumer/index.js' -import { AgentId, BlockNumberRange, HexString } from './monitoring/types.js' import Connector from './networking/connector.js' import { Janitor } from './persistence/janitor.js' import { Scheduler } from './persistence/scheduler.js' import { SubsStore } from './persistence/subs.js' +import { AgentId, BlockNumberRange, HexString } from './subscriptions/types.js' export type NetworkURN = `urn:ocn:${string}` From b3d2c8a40859685e0183b97711674c1b82ce49ad Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 12:05:48 +0200 Subject: [PATCH 15/58] fix imports --- packages/server/src/services/agents/api.ts | 62 ++ packages/server/src/services/agents/local.ts | 90 ++ packages/server/src/services/agents/plugin.ts | 45 + packages/server/src/services/agents/types.ts | 43 + .../src/services/agents/xcm/matching.spec.ts | 283 ++++++ .../src/services/agents/xcm/matching.ts | 885 +++++++++++++++++ .../services/agents/xcm/ops/bridge.spec.ts | 390 ++++++++ .../services/agents/xcm/ops/common.spec.ts | 191 ++++ .../src/services/agents/xcm/ops/common.ts | 191 ++++ .../src/services/agents/xcm/ops/criteria.ts | 49 + .../src/services/agents/xcm/ops/dmp.spec.ts | 236 +++++ .../server/src/services/agents/xcm/ops/dmp.ts | 332 +++++++ .../src/services/agents/xcm/ops/pk-bridge.ts | 206 ++++ .../src/services/agents/xcm/ops/relay.spec.ts | 68 ++ .../src/services/agents/xcm/ops/relay.ts | 52 + .../src/services/agents/xcm/ops/ump.spec.ts | 114 +++ .../server/src/services/agents/xcm/ops/ump.ts | 119 +++ .../src/services/agents/xcm/ops/util.spec.ts | 341 +++++++ .../src/services/agents/xcm/ops/util.ts | 417 ++++++++ .../agents/xcm/ops/xcm-format.spec.ts | 27 + .../src/services/agents/xcm/ops/xcm-format.ts | 79 ++ .../src/services/agents/xcm/ops/xcm-types.ts | 576 +++++++++++ .../src/services/agents/xcm/ops/xcmp.spec.ts | 165 ++++ .../src/services/agents/xcm/ops/xcmp.ts | 131 +++ .../services/agents/xcm/telemetry/events.ts | 15 + .../services/agents/xcm/telemetry/metrics.ts | 111 +++ .../services/agents/xcm/types-augmented.ts | 20 + .../server/src/services/agents/xcm/types.ts | 797 +++++++++++++++ .../src/services/agents/xcm/xcm-agent.ts | 914 ++++++++++++++++++ 29 files changed, 6949 insertions(+) create mode 100644 packages/server/src/services/agents/api.ts create mode 100644 packages/server/src/services/agents/local.ts create mode 100644 packages/server/src/services/agents/plugin.ts create mode 100644 packages/server/src/services/agents/types.ts create mode 100644 packages/server/src/services/agents/xcm/matching.spec.ts create mode 100644 packages/server/src/services/agents/xcm/matching.ts create mode 100644 packages/server/src/services/agents/xcm/ops/bridge.spec.ts create mode 100644 packages/server/src/services/agents/xcm/ops/common.spec.ts create mode 100644 packages/server/src/services/agents/xcm/ops/common.ts create mode 100644 packages/server/src/services/agents/xcm/ops/criteria.ts create mode 100644 packages/server/src/services/agents/xcm/ops/dmp.spec.ts create mode 100644 packages/server/src/services/agents/xcm/ops/dmp.ts create mode 100644 packages/server/src/services/agents/xcm/ops/pk-bridge.ts create mode 100644 packages/server/src/services/agents/xcm/ops/relay.spec.ts create mode 100644 packages/server/src/services/agents/xcm/ops/relay.ts create mode 100644 packages/server/src/services/agents/xcm/ops/ump.spec.ts create mode 100644 packages/server/src/services/agents/xcm/ops/ump.ts create mode 100644 packages/server/src/services/agents/xcm/ops/util.spec.ts create mode 100644 packages/server/src/services/agents/xcm/ops/util.ts create mode 100644 packages/server/src/services/agents/xcm/ops/xcm-format.spec.ts create mode 100644 packages/server/src/services/agents/xcm/ops/xcm-format.ts create mode 100644 packages/server/src/services/agents/xcm/ops/xcm-types.ts create mode 100644 packages/server/src/services/agents/xcm/ops/xcmp.spec.ts create mode 100644 packages/server/src/services/agents/xcm/ops/xcmp.ts create mode 100644 packages/server/src/services/agents/xcm/telemetry/events.ts create mode 100644 packages/server/src/services/agents/xcm/telemetry/metrics.ts create mode 100644 packages/server/src/services/agents/xcm/types-augmented.ts create mode 100644 packages/server/src/services/agents/xcm/types.ts create mode 100644 packages/server/src/services/agents/xcm/xcm-agent.ts diff --git a/packages/server/src/services/agents/api.ts b/packages/server/src/services/agents/api.ts new file mode 100644 index 00000000..603b6261 --- /dev/null +++ b/packages/server/src/services/agents/api.ts @@ -0,0 +1,62 @@ +// TBD agent web api +/* +// TO MOVE OUT to the XCM agent + if (isXcmReceived(msg)) { + this.#log.info( + '[%s ➜ %s] NOTIFICATION %s subscription=%s, messageHash=%s, outcome=%s (o: #%s, d: #%s)', + msg.origin.chainId, + msg.destination.chainId, + msg.type, + sub.id, + msg.waypoint.messageHash, + msg.waypoint.outcome, + msg.origin.blockNumber, + msg.destination.blockNumber + ) + } else if (isXcmHop(msg)) { + this.#notifyHop(sub, msg) + } else if (isXcmRelayed(msg) && msg.type === XcmNotificationType.Relayed) { + this.#log.info( + '[%s ↠ %s] NOTIFICATION %s subscription=%s, messageHash=%s, block=%s', + msg.origin.chainId, + msg.destination.chainId, + msg.type, + sub.id, + msg.waypoint.messageHash, + msg.waypoint.blockNumber + ) + } else if (isXcmSent(msg)) { + this.#log.info( + '[%s ➜] NOTIFICATION %s subscription=%s, messageHash=%s, block=%s', + msg.origin.chainId, + msg.type, + sub.id, + msg.waypoint.messageHash, + msg.origin.blockNumber + ) + } + } + + #notifyHop(sub: Subscription, msg: XcmHop) { + if (msg.direction === 'out') { + this.#log.info( + '[%s ↷] NOTIFICATION %s-%s subscription=%s, messageHash=%s, block=%s', + msg.waypoint.chainId, + msg.type, + msg.direction, + sub.id, + msg.waypoint.messageHash, + msg.waypoint.blockNumber + ) + } else if (msg.direction === 'in') { + this.#log.info( + '[↷ %s] NOTIFICATION %s-%s subscription=%s, messageHash=%s, block=%s', + msg.waypoint.chainId, + msg.type, + msg.direction, + sub.id, + msg.waypoint.messageHash, + msg.waypoint.blockNumber + ) + } +*/ diff --git a/packages/server/src/services/agents/local.ts b/packages/server/src/services/agents/local.ts new file mode 100644 index 00000000..0d05fbd1 --- /dev/null +++ b/packages/server/src/services/agents/local.ts @@ -0,0 +1,90 @@ +import { AgentServiceOptions } from '../../types.js' +import { Logger, Services } from '../index.js' +import { NotifierHub } from '../notification/index.js' +import { NotifierEvents } from '../notification/types.js' +import { AgentId, NotificationListener, Subscription } from '../subscriptions/types.js' +import { Agent, AgentRuntimeContext, AgentService } from './types.js' +import { XCMAgent } from './xcm/xcm-agent.js' + +/** + * Local agent service. + */ +export class LocalAgentService implements AgentService { + readonly #log: Logger + readonly #agents: Record + readonly #notifier: NotifierHub + + constructor(ctx: Services, _options: AgentServiceOptions) { + this.#log = ctx.log + + // XXX: this is a local in the process memory + // notifier hub + this.#notifier = new NotifierHub(ctx) + + this.#agents = this.#loadAgents({ + ...ctx, + notifier: this.#notifier, + }) + } + + /** + * Retrieves the registered subscriptions in the database + * for all the configured networks. + * + * @returns {Subscription[]} an array with the subscriptions + */ + async getAllSubscriptions() { + let subscriptions: Subscription[] = [] + for (const chainId of this.getAgentIds()) { + const agent = await this.getAgentById(chainId) + subscriptions = subscriptions.concat(await agent.getAllSubscriptions()) + } + + return subscriptions + } + + addNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub { + return this.#notifier.on(eventName, listener) + } + + removeNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub { + return this.#notifier.off(eventName, listener) + } + + getAgentIds(): AgentId[] { + return Object.keys(this.#agents) + } + + getAgentById(agentId: AgentId): Agent { + if (this.#agents[agentId]) { + return this.#agents[agentId] + } + throw new Error(`Agent not found for id=${agentId}`) + } + + getAgentInputSchema(agentId: AgentId) { + const agent = this.getAgentById(agentId) + return agent.getInputSchema() + } + + async start() { + for (const [id, agent] of Object.entries(this.#agents)) { + this.#log.info('[local:agents] Starting agent %s', id) + await agent.start() + } + } + + async stop() { + for (const [id, agent] of Object.entries(this.#agents)) { + this.#log.info('[local:agents] Stopping agent %s', id) + await agent.stop() + } + } + + #loadAgents(ctx: AgentRuntimeContext) { + const xcm = new XCMAgent(ctx) + return { + [xcm.id]: xcm, + } + } +} diff --git a/packages/server/src/services/agents/plugin.ts b/packages/server/src/services/agents/plugin.ts new file mode 100644 index 00000000..3bcf4ec9 --- /dev/null +++ b/packages/server/src/services/agents/plugin.ts @@ -0,0 +1,45 @@ +import { FastifyPluginAsync } from 'fastify' +import fp from 'fastify-plugin' + +import { AgentServiceMode, AgentServiceOptions } from '../../types.js' +import { LocalAgentService } from './local.js' +import { AgentService } from './types.js' + +declare module 'fastify' { + interface FastifyInstance { + agentService: AgentService + } +} + +/** + * Fastify plug-in for instantiating an {@link AgentService} instance. + * + * @param fastify The Fastify instance. + * @param options Options for configuring the Agent Service. + */ +const agentServicePlugin: FastifyPluginAsync = async (fastify, options) => { + if (options.mode !== AgentServiceMode.local) { + throw new Error('Only local agent service is supported') + } + const service: AgentService = new LocalAgentService(fastify, options) + + fastify.addHook('onClose', (server, done) => { + service + .stop() + .then(() => { + server.log.info('Agent service stopped') + }) + .catch((error: any) => { + server.log.error(error, 'Error while stopping agent service') + }) + .finally(() => { + done() + }) + }) + + fastify.decorate('agentService', service) + + await service.start() +} + +export default fp(agentServicePlugin, { fastify: '>=4.x', name: 'agent-service' }) diff --git a/packages/server/src/services/agents/types.ts b/packages/server/src/services/agents/types.ts new file mode 100644 index 00000000..0073bc96 --- /dev/null +++ b/packages/server/src/services/agents/types.ts @@ -0,0 +1,43 @@ +import { z } from 'zod' + +import { Operation } from 'rfc6902' + +import { IngressConsumer } from '../ingress/index.js' +import { NotifierHub } from '../notification/hub.js' +import { NotifierEvents } from '../notification/types.js' +import { Janitor } from '../persistence/janitor.js' +import { SubsStore } from '../persistence/subs.js' +import { AgentId, NotificationListener, Subscription } from '../subscriptions/types.js' +import { DB, Logger } from '../types.js' + +export type AgentRuntimeContext = { + log: Logger + notifier: NotifierHub + ingressConsumer: IngressConsumer + rootStore: DB + subsStore: SubsStore + janitor: Janitor +} + +export interface AgentService { + addNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub + removeNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener): NotifierHub + getAgentById(agentId: AgentId): Agent + getAgentInputSchema(agentId: AgentId): z.ZodSchema + getAgentIds(): AgentId[] + start(): Promise + stop(): Promise +} + +export interface Agent { + get id(): AgentId + getSubscriptionById(subscriptionId: string): Promise + getAllSubscriptions(): Promise + getInputSchema(): z.ZodSchema + getSubscriptionHandler(subscriptionId: string): Subscription + subscribe(subscription: Subscription): Promise + unsubscribe(subscriptionId: string): Promise + update(subscriptionId: string, patch: Operation[]): Promise + stop(): Promise + start(): Promise +} diff --git a/packages/server/src/services/agents/xcm/matching.spec.ts b/packages/server/src/services/agents/xcm/matching.spec.ts new file mode 100644 index 00000000..304ab2a3 --- /dev/null +++ b/packages/server/src/services/agents/xcm/matching.spec.ts @@ -0,0 +1,283 @@ +import { jest } from '@jest/globals' + +import { MemoryLevel as Level } from 'memory-level' + +import { AbstractSublevel } from 'abstract-level' +import { XcmInbound, XcmNotificationType, XcmNotifyMessage, XcmSent } from '../../services/monitoring/types.js' +import { Janitor } from '../../services/persistence/janitor.js' +import { jsonEncoded, prefixes } from '../../services/types.js' +import { matchBridgeMessages } from '../../testing/bridge/matching.js' +import { matchHopMessages, matchMessages, realHopMessages } from '../../testing/matching.js' +import { _services } from '../../testing/services.js' +import { MatchingEngine } from './matching.js' + +describe('message matching engine', () => { + let engine: MatchingEngine + let db: Level + let outbound: AbstractSublevel + const cb = jest.fn((_: XcmNotifyMessage) => { + /* empty */ + }) + const schedule = jest.fn(() => { + /* empty */ + }) + + beforeEach(() => { + cb.mockReset() + schedule.mockReset() + + db = new Level() + engine = new MatchingEngine( + { + ..._services, + rootStore: db, + janitor: { + on: jest.fn(), + schedule, + } as unknown as Janitor, + }, + cb + ) + + outbound = db.sublevel(prefixes.matching.outbound, jsonEncoded) + }) + + it('should match inbound and outbound', async () => { + const { origin, destination, subscriptionId } = matchMessages + const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` + const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` + + await engine.onOutboundMessage(origin) + await engine.onInboundMessage(destination) + + expect(cb).toHaveBeenCalledTimes(2) + await expect(outbound.get(idKey)).rejects.toBeDefined() + await expect(outbound.get(hashKey)).rejects.toBeDefined() + }) + + it('should match outbound and inbound', async () => { + const { origin, destination, subscriptionId } = matchMessages + const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` + const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` + + await engine.onInboundMessage(destination) + await engine.onOutboundMessage(origin) + + expect(cb).toHaveBeenCalledTimes(2) + await expect(outbound.get(idKey)).rejects.toBeDefined() + await expect(outbound.get(hashKey)).rejects.toBeDefined() + }) + + it('should work async concurrently', async () => { + const { origin, destination, subscriptionId } = matchMessages + const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` + const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` + + await Promise.all([engine.onOutboundMessage(origin), engine.onInboundMessage(destination)]) + + expect(cb).toHaveBeenCalledTimes(2) + await expect(outbound.get(idKey)).rejects.toBeDefined() + await expect(outbound.get(hashKey)).rejects.toBeDefined() + }) + + it('should match outbound and relay', async () => { + await engine.onOutboundMessage(matchMessages.origin) + await engine.onRelayedMessage(matchMessages.subscriptionId, matchMessages.relay) + + expect(cb).toHaveBeenCalledTimes(2) + }) + + it('should match relay and outbound', async () => { + await engine.onRelayedMessage(matchMessages.subscriptionId, matchMessages.relay) + await engine.onOutboundMessage(matchMessages.origin) + expect(schedule).toHaveBeenCalledTimes(2) + + expect(cb).toHaveBeenCalledTimes(2) + }) + + it('should match relay and outbound and inbound', async () => { + const { origin, relay, destination, subscriptionId } = matchMessages + const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` + const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` + + await engine.onRelayedMessage(subscriptionId, relay) + await engine.onOutboundMessage(origin) + await engine.onInboundMessage(destination) + + expect(schedule).toHaveBeenCalledTimes(2) + expect(cb).toHaveBeenCalledTimes(3) + await expect(outbound.get(idKey)).rejects.toBeDefined() + await expect(outbound.get(hashKey)).rejects.toBeDefined() + }) + + it('should match outbound and inbound by message hash', async () => { + const { origin, destination, subscriptionId } = matchMessages + const omsg: XcmSent = { + ...origin, + messageId: undefined, + } + const imsg: XcmInbound = { + ...destination, + messageId: destination.messageHash, + } + const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` + const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` + + await engine.onOutboundMessage(omsg) + await engine.onInboundMessage(imsg) + + expect(cb).toHaveBeenCalledTimes(2) + await expect(outbound.get(idKey)).rejects.toBeDefined() + await expect(outbound.get(hashKey)).rejects.toBeDefined() + }) + + it('should match with messageId on outbound and only message hash on inbound', async () => { + const { origin, destination, subscriptionId } = matchMessages + const imsg: XcmInbound = { + ...destination, + messageId: destination.messageHash, + } + const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` + const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` + + await engine.onOutboundMessage(origin) + await engine.onInboundMessage(imsg) + + expect(cb).toHaveBeenCalledTimes(2) + await expect(outbound.get(idKey)).rejects.toBeDefined() + await expect(outbound.get(hashKey)).rejects.toBeDefined() + }) + + it('should match hop messages', async () => { + const { origin, relay0, hopin, hopout, relay2, destination, subscriptionId } = matchHopMessages + const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` + const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` + + await engine.onOutboundMessage(origin) + await engine.onRelayedMessage(subscriptionId, relay0) + + await engine.onInboundMessage(hopin) + await engine.onOutboundMessage(hopout) + await engine.onRelayedMessage(subscriptionId, relay2) + await engine.onInboundMessage(destination) + + expect(cb).toHaveBeenCalledTimes(6) + await expect(outbound.get(idKey)).rejects.toBeDefined() + await expect(outbound.get(hashKey)).rejects.toBeDefined() + }) + + it('should match hop messages', async () => { + const msgTypeCb = jest.fn((_: XcmNotificationType) => { + /* empty */ + }) + cb.mockImplementation((msg) => msgTypeCb(msg.type)) + + const { origin, hopin, hopout } = realHopMessages + + await engine.onOutboundMessage(origin) + + await engine.onInboundMessage(hopin) + await engine.onOutboundMessage(hopout) + + expect(cb).toHaveBeenCalledTimes(3) + expect(msgTypeCb).toHaveBeenNthCalledWith<[XcmNotificationType]>(1, XcmNotificationType.Sent) + expect(msgTypeCb).toHaveBeenNthCalledWith<[XcmNotificationType]>(2, XcmNotificationType.Hop) + expect(msgTypeCb).toHaveBeenNthCalledWith<[XcmNotificationType]>(3, XcmNotificationType.Hop) + }) + + it('should match hop messages with concurrent message on hop stop', async () => { + const { origin, relay0, hopin, hopout, relay2, destination, subscriptionId } = matchHopMessages + const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` + const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` + + await engine.onOutboundMessage(origin) + await engine.onRelayedMessage(subscriptionId, relay0) + await Promise.all([engine.onInboundMessage(hopin), engine.onOutboundMessage(hopout)]) + await engine.onRelayedMessage(subscriptionId, relay2) + await engine.onInboundMessage(destination) + + expect(cb).toHaveBeenCalledTimes(6) + await expect(outbound.get(idKey)).rejects.toBeDefined() + await expect(outbound.get(hashKey)).rejects.toBeDefined() + }) + + it('should match hop messages with concurrent message on hop stop and relay out of order', async () => { + const { origin, relay0, hopin, hopout, relay2, destination, subscriptionId } = matchHopMessages + const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` + const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` + + await engine.onRelayedMessage(subscriptionId, relay0) + await engine.onOutboundMessage(origin) + await engine.onRelayedMessage(subscriptionId, relay2) + + await Promise.all([engine.onInboundMessage(hopin), engine.onOutboundMessage(hopout)]) + + await engine.onInboundMessage(destination) + + expect(cb).toHaveBeenCalledTimes(6) + await expect(outbound.get(idKey)).rejects.toBeDefined() + await expect(outbound.get(hashKey)).rejects.toBeDefined() + }) + + it('should match bridge messages', async () => { + const { + origin, + relay0, + bridgeXcmIn, + bridgeAccepted, + bridgeDelivered, + bridgeIn, + bridgeXcmOut, + relay1, + destination, + subscriptionId, + } = matchBridgeMessages + const idKey = `${subscriptionId}:${origin.messageId}:${destination.chainId}` + const hashKey = `${subscriptionId}:${origin.waypoint.messageHash}:${destination.chainId}` + + await engine.onOutboundMessage(origin) + await engine.onRelayedMessage(subscriptionId, relay0) + await engine.onInboundMessage(bridgeXcmIn) + await engine.onBridgeOutboundAccepted(subscriptionId, bridgeAccepted) + await engine.onBridgeOutboundDelivered(subscriptionId, bridgeDelivered) + await engine.onBridgeInbound(subscriptionId, bridgeIn) + await engine.onOutboundMessage(bridgeXcmOut) + await engine.onRelayedMessage(subscriptionId, relay1) + await engine.onInboundMessage(destination) + + expect(cb).toHaveBeenCalledTimes(9) + await expect(outbound.get(idKey)).rejects.toBeDefined() + await expect(outbound.get(hashKey)).rejects.toBeDefined() + }) + + it('should clean up stale data', async () => { + async function count() { + const iterator = db.iterator() + await iterator.all() + return iterator.count + } + + for (let i = 0; i < 100; i++) { + await engine.onInboundMessage({ + ...matchMessages.destination, + subscriptionId: 'z.transfers:' + i, + }) + await engine.onOutboundMessage({ + ...matchMessages.origin, + subscriptionId: 'baba-yaga-1:' + i, + }) + const r = (Math.random() + 1).toString(36).substring(7) + await engine.onOutboundMessage({ + ...matchMessages.origin, + subscriptionId: r + i, + }) + } + expect(await count()).toBe(600) + + for (let i = 0; i < 100; i++) { + await engine.clearPendingStates('z.transfers:' + i) + await engine.clearPendingStates('baba-yaga-1:' + i) + } + expect(await count()).toBe(200) + }) +}) diff --git a/packages/server/src/services/agents/xcm/matching.ts b/packages/server/src/services/agents/xcm/matching.ts new file mode 100644 index 00000000..65b025f1 --- /dev/null +++ b/packages/server/src/services/agents/xcm/matching.ts @@ -0,0 +1,885 @@ +import EventEmitter from 'node:events' + +import { AbstractSublevel } from 'abstract-level' +import { Mutex } from 'async-mutex' + +import { DB, Logger, jsonEncoded, prefixes } from '../../types.js' +import { + GenericXcmBridge, + GenericXcmHop, + GenericXcmReceived, + GenericXcmRelayed, + GenericXcmTimeout, + XcmBridge, + XcmBridgeAcceptedWithContext, + XcmBridgeDeliveredWithContext, + XcmBridgeInboundWithContext, + XcmHop, + XcmInbound, + XcmNotifyMessage, + XcmReceived, + XcmRelayed, + XcmRelayedWithContext, + XcmSent, + XcmTimeout, + XcmWaypointContext, +} from './types.js' + +import { getRelayId, isOnSameConsensus } from '../../config.js' +import { Janitor, JanitorTask } from '../../persistence/janitor.js' +import { AgentRuntimeContext } from '../types.js' +import { TelemetryXCMEventEmitter } from './telemetry/events.js' + +export type XcmMatchedReceiver = (message: XcmNotifyMessage) => Promise | void +type SubLevel = AbstractSublevel + +export type ChainBlock = { + chainId: string + blockHash: string + blockNumber: string +} + +const DEFAULT_TIMEOUT = 2 * 60000 +/** + * Matches sent XCM messages on the destination. + * It does not assume any ordering. + * + * Current matching logic takes into account that messages at origin and destination + * might or might not have a unique ID set via SetTopic instruction. + * Therefore, it supports matching logic using both message hash and message ID. + * + * When unique message ID is implemented in all XCM events, we can: + * - simplify logic to match only by message ID + * - check notification storage by message ID and do not store for matching if already matched + */ +export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEventEmitter) { + readonly #log: Logger + readonly #janitor: Janitor + + readonly #outbound: SubLevel + readonly #inbound: SubLevel + readonly #relay: SubLevel + readonly #hop: SubLevel + readonly #bridge: SubLevel + readonly #bridgeAccepted: SubLevel + readonly #bridgeInbound: SubLevel + readonly #mutex: Mutex + readonly #xcmMatchedReceiver: XcmMatchedReceiver + + constructor({ log, rootStore, janitor }: AgentRuntimeContext, xcmMatchedReceiver: XcmMatchedReceiver) { + super() + + this.#log = log + this.#janitor = janitor + this.#mutex = new Mutex() + this.#xcmMatchedReceiver = xcmMatchedReceiver + + // Key format: [subscription-id]:[destination-chain-id]:[message-id/hash] + this.#outbound = rootStore.sublevel(prefixes.matching.outbound, jsonEncoded) + // Key format: [subscription-id]:[current-chain-id]:[message-id/hash] + this.#inbound = rootStore.sublevel(prefixes.matching.inbound, jsonEncoded) + // Key format: [subscription-id]:[relay-outbound-chain-id]:[message-id/hash] + this.#relay = rootStore.sublevel(prefixes.matching.relay, jsonEncoded) + // Key format: [subscription-id]:[hop-stop-chain-id]:[message-id/hash] + this.#hop = rootStore.sublevel(prefixes.matching.hop, jsonEncoded) + // Key format: [subscription-id]:[bridge-chain-id]:[message-id] + this.#bridge = rootStore.sublevel(prefixes.matching.bridge, jsonEncoded) + + // Key format: [subscription-id]:[bridge-key] + this.#bridgeAccepted = rootStore.sublevel(prefixes.matching.bridgeAccepted, jsonEncoded) + // Key format: [subscription-id]:[bridge-key] + this.#bridgeInbound = rootStore.sublevel( + prefixes.matching.bridgeIn, + jsonEncoded + ) + + this.#janitor.on('sweep', this.#onXcmSwept.bind(this)) + } + + async onOutboundMessage(outMsg: XcmSent, outboundTTL: number = DEFAULT_TIMEOUT) { + const log = this.#log + + // Confirmation key at destination + await this.#mutex.runExclusive(async () => { + const hashKey = this.#matchingKey(outMsg.subscriptionId, outMsg.destination.chainId, outMsg.waypoint.messageHash) + // try to get any stored relay messages and notify if found. + // do not clean up outbound in case inbound has not arrived yet. + await this.#findRelayInbound(outMsg) + + if (outMsg.forwardId !== undefined) { + // Is bridged message + // Try to match origin message (on other consensus) using the forward ID. + // If found, create outbound message with origin context and current waypoint context + // before trying to match inbound (on same consensus). + // If origin message not found, treat as normal XCM outbound + try { + const { + subscriptionId, + origin: { chainId }, + messageId, + forwardId, + waypoint, + } = outMsg + const forwardIdKey = this.#matchingKey(subscriptionId, chainId, forwardId) + const originMsg = await this.#bridge.get(forwardIdKey) + + const bridgedSent: XcmSent = { + ...originMsg, + waypoint, + messageId, + forwardId, + } + this.#onXcmOutbound(bridgedSent) + await this.#tryMatchOnOutbound(bridgedSent, outboundTTL) + await this.#bridge.del(forwardIdKey) + } catch { + this.#onXcmOutbound(outMsg) + await this.#tryMatchOnOutbound(outMsg, outboundTTL) + } + } else if (outMsg.messageId) { + // Is not bridged message + // First try to match by hop key + // If found, emit hop, and do not store anything + // If no matching hop key, assume is origin outbound message -> try to match inbound + // We assume that the original origin message is ALWAYS received first. + // NOTE: hops can only use idKey since message hash will be different on each hop + try { + const hopKey = this.#matchingKey(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.messageId) + const originMsg = await this.#hop.get(hopKey) + log.info( + '[%s:h] MATCHED HOP OUT origin=%s id=%s (subId=%s, block=%s #%s)', + outMsg.origin.chainId, + originMsg.origin.chainId, + hopKey, + outMsg.subscriptionId, + outMsg.origin.blockHash, + outMsg.origin.blockNumber + ) + // do not delete hop key because maybe hop stop inbound hasn't arrived yet + this.#onXcmHopOut(originMsg, outMsg) + } catch { + this.#onXcmOutbound(outMsg) + // Try to get stored inbound messages and notify if any + // If inbound messages are found, clean up outbound. + // If not found, store outbound message in #outbound to match destination inbound + // and #hop to match hop outbounds and inbounds. + // Note: if relay messages arrive after outbound and inbound, it will not match. + await this.#tryMatchOnOutbound(outMsg, outboundTTL) + } + } else { + this.#onXcmOutbound(outMsg) + // try to get stored inbound messages by message hash and notify if any + try { + const inMsg = await this.#inbound.get(hashKey) + log.info( + '[%s:o] MATCHED hash=%s (subId=%s, block=%s #%s)', + outMsg.origin.chainId, + hashKey, + outMsg.subscriptionId, + outMsg.origin.blockHash, + outMsg.origin.blockNumber + ) + await this.#inbound.del(hashKey) + this.#onXcmMatched(outMsg, inMsg) + } catch { + await this.#storekeysOnOutbound(outMsg, outboundTTL) + } + } + }) + + return outMsg + } + + async onInboundMessage(inMsg: XcmInbound) { + const log = this.#log + + await this.#mutex.runExclusive(async () => { + let hashKey = this.#matchingKey(inMsg.subscriptionId, inMsg.chainId, inMsg.messageHash) + let idKey = this.#matchingKey(inMsg.subscriptionId, inMsg.chainId, inMsg.messageId) + + if (hashKey === idKey) { + // if hash and id are the same, both could be the message hash or both could be the message id + try { + const outMsg = await this.#outbound.get(hashKey) + log.info( + '[%s:i] MATCHED hash=%s (subId=%s, block=%s #%s)', + inMsg.chainId, + hashKey, + inMsg.subscriptionId, + inMsg.blockHash, + inMsg.blockNumber + ) + // if outbound has no messageId, we can safely assume that + // idKey and hashKey are made up of only the message hash. + // if outbound has messageId, we need to reconstruct idKey and hashKey + // using outbound values to ensure that no dangling keys will be left on janitor sweep. + if (outMsg.messageId !== undefined) { + idKey = this.#matchingKey(inMsg.subscriptionId, inMsg.chainId, outMsg.messageId) + hashKey = this.#matchingKey(inMsg.subscriptionId, inMsg.chainId, outMsg.waypoint.messageHash) + } + await this.#outbound.batch().del(idKey).del(hashKey).write() + this.#onXcmMatched(outMsg, inMsg) + } catch { + await this.#tryHopMatchOnInbound(inMsg) + } + } else { + try { + const outMsg = await Promise.any([this.#outbound.get(idKey), this.#outbound.get(hashKey)]) + // Reconstruct hashKey with outbound message hash in case of hopped messages + hashKey = this.#matchingKey(inMsg.subscriptionId, inMsg.chainId, outMsg.waypoint.messageHash) + log.info( + '[%s:i] MATCHED hash=%s id=%s (subId=%s, block=%s #%s)', + inMsg.chainId, + hashKey, + idKey, + inMsg.subscriptionId, + inMsg.blockHash, + inMsg.blockNumber + ) + await this.#outbound.batch().del(idKey).del(hashKey).write() + this.#onXcmMatched(outMsg, inMsg) + } catch { + await this.#tryHopMatchOnInbound(inMsg) + } + } + }) + } + + async onRelayedMessage(subscriptionId: string, relayMsg: XcmRelayedWithContext) { + const log = this.#log + + const relayId = getRelayId(relayMsg.origin) + const idKey = relayMsg.messageId + ? this.#matchingKey(subscriptionId, relayMsg.recipient, relayMsg.messageId) + : this.#matchingKey(subscriptionId, relayMsg.recipient, relayMsg.messageHash) + + await this.#mutex.runExclusive(async () => { + try { + const outMsg = await this.#outbound.get(idKey) + log.info( + '[%s:r] RELAYED origin=%s recipient=%s (subId=%s, block=%s #%s)', + relayId, + relayMsg.origin, + relayMsg.recipient, + subscriptionId, + relayMsg.blockHash, + relayMsg.blockNumber + ) + await this.#relay.del(idKey) + await this.#onXcmRelayed(outMsg, relayMsg) + } catch { + const relayKey = relayMsg.messageId + ? this.#matchingKey(subscriptionId, relayMsg.origin, relayMsg.messageId) + : this.#matchingKey(subscriptionId, relayMsg.origin, relayMsg.messageHash) + log.info( + '[%s:r] STORED relayKey=%s origin=%s recipient=%s (subId=%s, block=%s #%s)', + relayId, + relayKey, + relayMsg.origin, + relayMsg.recipient, + subscriptionId, + relayMsg.blockHash, + relayMsg.blockNumber + ) + await this.#relay.put(relayKey, relayMsg) + await this.#janitor.schedule({ + sublevel: prefixes.matching.relay, + key: relayKey, + }) + } + }) + } + + async onBridgeOutboundAccepted(subscriptionId: string, msg: XcmBridgeAcceptedWithContext) { + const log = this.#log + + await this.#mutex.runExclusive(async () => { + if (msg.forwardId === undefined) { + log.error( + '[%s] forward_id_to not found for bridge accepted message (sub=%s block=%s #%s)', + msg.chainId, + subscriptionId, + msg.blockHash, + msg.blockNumber + ) + return + } + const { chainId, forwardId, bridgeKey } = msg + const idKey = this.#matchingKey(subscriptionId, chainId, forwardId) + + try { + const originMsg = await this.#bridge.get(idKey) + + const { blockHash, blockNumber, event, messageData, instructions, messageHash } = msg + const legIndex = originMsg.legs.findIndex((l) => l.from === chainId && l.type === 'bridge') + const waypointContext: XcmWaypointContext = { + legIndex, + chainId, + blockHash, + blockNumber: blockNumber.toString(), + event, + messageData, + messageHash, + instructions, + outcome: 'Success', // always 'Success' since it's delivered + error: null, + } + const bridgeOutMsg: XcmBridge = new GenericXcmBridge(originMsg, waypointContext, { + bridgeMessageType: 'accepted', + bridgeKey, + forwardId, + }) + const sublevelBridgeKey = `${subscriptionId}:${bridgeKey}` + await this.#bridgeAccepted.put(sublevelBridgeKey, bridgeOutMsg) + await this.#janitor.schedule({ + sublevel: prefixes.matching.bridgeAccepted, + key: sublevelBridgeKey, + }) + log.info( + '[%s:ba] BRIDGE MESSAGE ACCEPTED key=%s (subId=%s, block=%s #%s)', + chainId, + sublevelBridgeKey, + subscriptionId, + msg.blockHash, + msg.blockNumber + ) + this.#onXcmBridgeAccepted(bridgeOutMsg) + } catch { + log.warn( + '[%s:ba] ORIGIN MSG NOT FOUND id=%s (subId=%s, block=%s #%s)', + chainId, + idKey, + subscriptionId, + msg.blockHash, + msg.blockNumber + ) + } + }) + } + + async onBridgeOutboundDelivered(subscriptionId: string, msg: XcmBridgeDeliveredWithContext) { + const log = this.#log + + await this.#mutex.runExclusive(async () => { + const { chainId, bridgeKey } = msg + const sublevelBridgeKey = `${subscriptionId}:${bridgeKey}` + try { + const bridgeOutMsg = await this.#bridgeAccepted.get(sublevelBridgeKey) + try { + const bridgeInMsg = await this.#bridgeInbound.get(sublevelBridgeKey) + log.info( + '[%s:bd] BRIDGE MATCHED key=%s (subId=%s, block=%s #%s)', + chainId, + sublevelBridgeKey, + subscriptionId, + msg.blockHash, + msg.blockNumber + ) + await this.#bridgeInbound.del(sublevelBridgeKey) + await this.#bridgeAccepted.del(sublevelBridgeKey) + this.#onXcmBridgeDelivered({ ...bridgeOutMsg, bridgeMessageType: 'delivered' }) + this.#onXcmBridgeMatched(bridgeOutMsg, bridgeInMsg) + } catch { + this.#log.info( + '[%s:bo] BRIDGE DELIVERED key=%s (subId=%s, block=%s #%s)', + chainId, + sublevelBridgeKey, + subscriptionId, + msg.blockHash, + msg.blockNumber + ) + this.#onXcmBridgeDelivered(bridgeOutMsg) + } + } catch { + log.warn( + '[%s:bd] BRIDGE ACCEPTED MSG NOT FOUND key=%s (subId=%s, block=%s #%s)', + chainId, + sublevelBridgeKey, + subscriptionId, + msg.blockHash, + msg.blockNumber + ) + } + }) + } + + async onBridgeInbound(subscriptionId: string, bridgeInMsg: XcmBridgeInboundWithContext) { + const log = this.#log + + await this.#mutex.runExclusive(async () => { + const { chainId, bridgeKey } = bridgeInMsg + const sublevelBridgeKey = `${subscriptionId}:${bridgeKey}` + + try { + const bridgeOutMsg = await this.#bridgeAccepted.get(sublevelBridgeKey) + log.info( + '[%s:bi] BRIDGE MATCHED key=%s (subId=%s, block=%s #%s)', + chainId, + sublevelBridgeKey, + subscriptionId, + bridgeInMsg.blockHash, + bridgeInMsg.blockNumber + ) + await this.#bridgeAccepted.del(sublevelBridgeKey) + this.#onXcmBridgeMatched(bridgeOutMsg, bridgeInMsg) + } catch { + this.#log.info( + '[%s:bi] BRIDGE IN STORED id=%s (subId=%s, block=%s #%s)', + chainId, + sublevelBridgeKey, + subscriptionId, + bridgeInMsg.blockHash, + bridgeInMsg.blockNumber + ) + this.#bridgeInbound.put(sublevelBridgeKey, bridgeInMsg) + await this.#janitor.schedule({ + sublevel: prefixes.matching.bridgeIn, + key: sublevelBridgeKey, + }) + } + }) + } + + // try to find in DB by hop key + // if found, emit hop, and do not store anything + // if no matching hop key, assume is destination inbound and store. + // We assume that the original origin message is ALWAYS received first. + // NOTE: hops can only use idKey since message hash will be different on each hop + async #tryHopMatchOnInbound(msg: XcmInbound) { + const log = this.#log + try { + const hopKey = this.#matchingKey(msg.subscriptionId, msg.chainId, msg.messageId) + const originMsg = await this.#hop.get(hopKey) + log.info( + '[%s:h] MATCHED HOP IN origin=%s id=%s (subId=%s, block=%s #%s)', + msg.chainId, + originMsg.origin.chainId, + hopKey, + msg.subscriptionId, + msg.blockHash, + msg.blockNumber + ) + // do not delete hop key because maybe hop stop outbound hasn't arrived yet + // TO THINK: store in different keys? + this.#onXcmHopIn(originMsg, msg) + } catch { + const hashKey = this.#matchingKey(msg.subscriptionId, msg.chainId, msg.messageHash) + const idKey = this.#matchingKey(msg.subscriptionId, msg.chainId, msg.messageId) + + if (hashKey === idKey) { + log.info( + '[%s:i] STORED hash=%s (subId=%s, block=%s #%s)', + msg.chainId, + hashKey, + msg.subscriptionId, + msg.blockHash, + msg.blockNumber + ) + await this.#inbound.put(hashKey, msg) + await this.#janitor.schedule({ + sublevel: prefixes.matching.inbound, + key: hashKey, + }) + } else { + log.info( + '[%s:i] STORED hash=%s id=%s (subId=%s, block=%s #%s)', + msg.chainId, + hashKey, + idKey, + msg.subscriptionId, + msg.blockHash, + msg.blockNumber + ) + await this.#inbound.batch().put(idKey, msg).put(hashKey, msg).write() + await this.#janitor.schedule( + { + sublevel: prefixes.matching.inbound, + key: hashKey, + }, + { + sublevel: prefixes.matching.inbound, + key: idKey, + } + ) + } + } + } + + async #tryMatchOnOutbound(msg: XcmSent, outboundTTL: number) { + if (msg.messageId === undefined) { + return + } + + // Still we don't know if the inbound is upgraded, + // i.e. if uses message ids + const idKey = this.#matchingKey(msg.subscriptionId, msg.destination.chainId, msg.messageId) + const hashKey = this.#matchingKey(msg.subscriptionId, msg.destination.chainId, msg.waypoint.messageHash) + + const log = this.#log + try { + const inMsg = await Promise.any([this.#inbound.get(idKey), this.#inbound.get(hashKey)]) + + log.info( + '[%s:o] MATCHED hash=%s id=%s (subId=%s, block=%s #%s)', + msg.origin.chainId, + hashKey, + idKey, + msg.subscriptionId, + msg.origin.blockHash, + msg.origin.blockNumber + ) + await this.#inbound.batch().del(idKey).del(hashKey).write() + this.#onXcmMatched(msg, inMsg) + } catch { + await this.#storekeysOnOutbound(msg, outboundTTL) + } + } + + // TODO: refactor to lower complexity + async #storekeysOnOutbound(msg: XcmSent, outboundTTL: number) { + const log = this.#log + const sublegs = msg.legs.filter((l) => isOnSameConsensus(msg.waypoint.chainId, l.from)) + + for (const [i, leg] of sublegs.entries()) { + const stop = leg.to + const hKey = this.#matchingKey(msg.subscriptionId, stop, msg.waypoint.messageHash) + if (msg.messageId) { + const iKey = this.#matchingKey(msg.subscriptionId, stop, msg.messageId) + if (leg.type === 'bridge') { + const bridgeOut = leg.from + const bridgeIn = leg.to + const bridgeOutIdKey = this.#matchingKey(msg.subscriptionId, bridgeOut, msg.messageId) + const bridgeInIdKey = this.#matchingKey(msg.subscriptionId, bridgeIn, msg.messageId) + log.info( + '[%s:b] STORED out=%s outKey=%s in=%s inKey=%s (subId=%s, block=%s #%s)', + msg.origin.chainId, + bridgeOut, + bridgeOutIdKey, + bridgeIn, + bridgeInIdKey, + msg.subscriptionId, + msg.origin.blockHash, + msg.origin.blockNumber + ) + await this.#bridge.batch().put(bridgeOutIdKey, msg).put(bridgeInIdKey, msg).write() + await this.#janitor.schedule( + { + sublevel: prefixes.matching.bridge, + key: bridgeOutIdKey, + expiry: outboundTTL, + }, + { + sublevel: prefixes.matching.bridge, + key: bridgeInIdKey, + expiry: outboundTTL, + } + ) + } else if (i === sublegs.length - 1 || leg.relay !== undefined) { + log.info( + '[%s:o] STORED dest=%s hash=%s id=%s (subId=%s, block=%s #%s)', + msg.origin.chainId, + stop, + hKey, + iKey, + msg.subscriptionId, + msg.origin.blockHash, + msg.origin.blockNumber + ) + await this.#outbound.batch().put(iKey, msg).put(hKey, msg).write() + await this.#janitor.schedule( + { + sublevel: prefixes.matching.outbound, + key: hKey, + expiry: outboundTTL, + }, + { + sublevel: prefixes.matching.outbound, + key: iKey, + expiry: outboundTTL, + } + ) + } else if (leg.type === 'hop') { + log.info( + '[%s:h] STORED stop=%s hash=%s id=%s (subId=%s, block=%s #%s)', + msg.origin.chainId, + stop, + hKey, + iKey, + msg.subscriptionId, + msg.origin.blockHash, + msg.origin.blockNumber + ) + await this.#hop.batch().put(iKey, msg).put(hKey, msg).write() + await this.#janitor.schedule( + { + sublevel: prefixes.matching.hop, + key: hKey, + expiry: outboundTTL, + }, + { + sublevel: prefixes.matching.hop, + key: iKey, + expiry: outboundTTL, + } + ) + } + } else if (i === sublegs.length - 1 || leg.relay !== undefined) { + log.info( + '[%s:o] STORED dest=%s hash=%s (subId=%s, block=%s #%s)', + msg.origin.chainId, + stop, + hKey, + msg.subscriptionId, + msg.origin.blockHash, + msg.origin.blockNumber + ) + await this.#outbound.put(hKey, msg) + await this.#janitor.schedule({ + sublevel: prefixes.matching.outbound, + key: hKey, + expiry: outboundTTL, + }) + } else if (leg.type === 'hop') { + log.info( + '[%s:h] STORED stop=%s hash=%s(subId=%s, block=%s #%s)', + msg.origin.chainId, + stop, + hKey, + msg.subscriptionId, + msg.origin.blockHash, + msg.origin.blockNumber + ) + await this.#hop.put(hKey, msg) + await this.#janitor.schedule({ + sublevel: prefixes.matching.hop, + key: hKey, + expiry: outboundTTL, + }) + } + } + } + + async #findRelayInbound(outMsg: XcmSent) { + const log = this.#log + const relayKey = outMsg.messageId + ? this.#matchingKey(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.messageId) + : this.#matchingKey(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.waypoint.messageHash) + + try { + const relayMsg = await this.#relay.get(relayKey) + log.info( + '[%s:r] RELAYED key=%s (subId=%s, block=%s #%s)', + outMsg.origin.chainId, + relayKey, + outMsg.subscriptionId, + outMsg.origin.blockHash, + outMsg.origin.blockNumber + ) + await this.#relay.del(relayKey) + await this.#onXcmRelayed(outMsg, relayMsg) + } catch { + // noop, it's possible that there are no relay subscriptions for an origin. + } + } + + async stop() { + await this.#mutex.waitForUnlock() + } + + /** + * Clears the pending states for a subcription. + * + * @param subscriptionId The subscription id. + */ + async clearPendingStates(subscriptionId: string) { + const prefix = subscriptionId + ':' + await this.#clearByPrefix(this.#inbound, prefix) + await this.#clearByPrefix(this.#outbound, prefix) + await this.#clearByPrefix(this.#relay, prefix) + await this.#clearByPrefix(this.#hop, prefix) + } + + async #clearByPrefix(sublevel: SubLevel, prefix: string) { + try { + const batch = sublevel.batch() + for await (const key of sublevel.keys({ gt: prefix })) { + if (key.startsWith(prefix)) { + batch.del(key) + } else { + break + } + } + await batch.write() + } catch (error) { + this.#log.error(error, 'while clearing prefix %s', prefix) + } + } + + #matchingKey(subscriptionId: string, chainId: string, messageId: string) { + // We add the subscription id as a discriminator + // to allow multiple subscriptions to the same messages + return `${subscriptionId}:${messageId}:${chainId}` + } + + #onXcmOutbound(outMsg: XcmSent) { + this.emit('telemetryOutbound', outMsg) + + try { + this.#xcmMatchedReceiver(outMsg) + } catch (e) { + this.#log.error(e, 'Error on notification') + } + } + + #onXcmMatched(outMsg: XcmSent, inMsg: XcmInbound) { + this.emit('telemetryMatched', inMsg, outMsg) + if (inMsg.assetsTrapped !== undefined) { + this.emit('telemetryTrapped', inMsg, outMsg) + } + + try { + const message: XcmReceived = new GenericXcmReceived(outMsg, inMsg) + this.#xcmMatchedReceiver(message) + } catch (e) { + this.#log.error(e, 'Error on notification') + } + } + + #onXcmRelayed(outMsg: XcmSent, relayMsg: XcmRelayedWithContext) { + const message: XcmRelayed = new GenericXcmRelayed(outMsg, relayMsg) + this.emit('telemetryRelayed', message) + + try { + this.#xcmMatchedReceiver(message) + } catch (e) { + this.#log.error(e, 'Error on notification') + } + } + + #onXcmHopOut(originMsg: XcmSent, hopMsg: XcmSent) { + try { + const { chainId, blockHash, blockNumber, event, outcome, error } = hopMsg.origin + const { instructions, messageData, messageHash, assetsTrapped } = hopMsg.waypoint + const currentLeg = hopMsg.legs[0] + const legIndex = originMsg.legs.findIndex((l) => l.from === currentLeg.from && l.to === currentLeg.to) + const waypointContext: XcmWaypointContext = { + legIndex, + chainId, + blockHash, + blockNumber, + event, + outcome, + error, + messageData, + messageHash, + instructions, + assetsTrapped, + } + const message: XcmHop = new GenericXcmHop(originMsg, waypointContext, 'out') + + this.emit('telemetryHop', message) + + this.#xcmMatchedReceiver(message) + } catch (e) { + this.#log.error(e, 'Error on notification') + } + } + + // NOTE: message data, hash and instructions are right for hop messages with 1 intermediate stop + // but will be wrong on the second or later hops for XCM with > 2 intermediate stops + // since we are not storing messages or contexts of intermediate hops + #onXcmHopIn(originMsg: XcmSent, hopMsg: XcmInbound) { + if (hopMsg.assetsTrapped !== undefined) { + this.emit('telemetryTrapped', hopMsg, originMsg) + } + + try { + const { chainId, blockHash, blockNumber, event, outcome, error, assetsTrapped } = hopMsg + const { messageData, messageHash, instructions } = originMsg.waypoint + const legIndex = originMsg.legs.findIndex((l) => l.to === chainId) + const waypointContext: XcmWaypointContext = { + legIndex, + chainId, + blockHash, + blockNumber, + event, + outcome, + error, + messageData, + messageHash, + instructions, + assetsTrapped, + } + const message: XcmHop = new GenericXcmHop(originMsg, waypointContext, 'in') + + this.emit('telemetryHop', message) + + this.#xcmMatchedReceiver(message) + } catch (e) { + this.#log.error(e, 'Error on notification') + } + } + + #onXcmBridgeAccepted(bridgeAcceptedMsg: XcmBridge) { + this.emit('telemetryBridge', bridgeAcceptedMsg) + try { + this.#xcmMatchedReceiver(bridgeAcceptedMsg) + } catch (e) { + this.#log.error(e, 'Error on notification') + } + } + + #onXcmBridgeDelivered(bridgeDeliveredMsg: XcmBridge) { + this.emit('telemetryBridge', bridgeDeliveredMsg) + try { + this.#xcmMatchedReceiver(bridgeDeliveredMsg) + } catch (e) { + this.#log.error(e, 'Error on notification') + } + } + + #onXcmBridgeMatched(bridgeOutMsg: XcmBridge, bridgeInMsg: XcmBridgeInboundWithContext) { + try { + const { chainId, blockHash, blockNumber, event, outcome, error, bridgeKey } = bridgeInMsg + const { messageData, messageHash, instructions } = bridgeOutMsg.waypoint + const legIndex = bridgeOutMsg.legs.findIndex((l) => l.to === chainId && l.type === 'bridge') + const waypointContext: XcmWaypointContext = { + legIndex, + chainId, + blockHash, + blockNumber: blockNumber.toString(), + event, + messageData, + messageHash, + instructions, + outcome, + error, + } + const bridgeMatched: XcmBridge = new GenericXcmBridge(bridgeOutMsg, waypointContext, { + bridgeMessageType: 'received', + bridgeKey, + forwardId: bridgeOutMsg.forwardId, + }) + + this.emit('telemetryBridge', bridgeMatched) + + this.#xcmMatchedReceiver(bridgeMatched) + } catch (e) { + this.#log.error(e, 'Error on notification') + } + } + + #onXcmSwept(task: JanitorTask, msg: string) { + try { + if (task.sublevel === prefixes.matching.outbound) { + const outMsg = JSON.parse(msg) as XcmSent + const message: XcmTimeout = new GenericXcmTimeout(outMsg) + this.#log.debug('TIMEOUT on key %s', task.key) + this.emit('telemetryTimeout', message) + this.#xcmMatchedReceiver(message) + } + } catch (e) { + this.#log.error(e, 'Error on notification') + } + } +} diff --git a/packages/server/src/services/agents/xcm/ops/bridge.spec.ts b/packages/server/src/services/agents/xcm/ops/bridge.spec.ts new file mode 100644 index 00000000..b033d9dc --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/bridge.spec.ts @@ -0,0 +1,390 @@ +import { jest } from '@jest/globals' +import { extractEvents, extractTxWithEvents } from '@sodazone/ocelloids-sdk' + +import { from } from 'rxjs' + +import { + bridgeInPolkadot, + bridgeOutAcceptedKusama, + bridgeOutDeliveredKusama, + registry, + relayHrmpReceiveKusama, + relayHrmpReceivePolkadot, + xcmpReceiveKusamaBridgeHub, + xcmpReceivePolkadotAssetHub, + xcmpSendKusamaAssetHub, + xcmpSendPolkadotBridgeHub, +} from '../../../testing/bridge/blocks.js' + +import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './pk-bridge.js' +import { extractRelayReceive } from './relay.js' +import { extractXcmpReceive, extractXcmpSend } from './xcmp.js' + +import { NetworkURN } from '../../types.js' +import { GenericXcmSentWithContext } from '../types.js' +import { mapXcmSent } from './common.js' +import { getMessageId } from './util.js' +import { fromXcmpFormat } from './xcm-format.js' + +describe('xcmp operator', () => { + describe('extractXcmpSend', () => { + it('should extract XCMP sent message on Kusama', (done) => { + const { origin, blocks, getHrmp } = xcmpSendKusamaAssetHub + + const calls = jest.fn() + + const test$ = extractXcmpSend(origin, getHrmp, registry)(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.instructions).toBeDefined() + expect(msg.messageData).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract XCMP sent message on Polkadot', (done) => { + const { origin, blocks, getHrmp } = xcmpSendPolkadotBridgeHub + + const calls = jest.fn() + + const test$ = extractXcmpSend(origin, getHrmp, registry)(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.instructions).toBeDefined() + expect(msg.messageData).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + }) + + describe('extractXcmpReceive', () => { + it('should extract XCMP receive with outcome success on Kusama', (done) => { + const calls = jest.fn() + + const test$ = extractXcmpReceive()(xcmpReceiveKusamaBridgeHub.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Success') + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract XCMP receive with outcome success on Polkadot', (done) => { + const calls = jest.fn() + + const test$ = extractXcmpReceive()(xcmpReceivePolkadotAssetHub.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Success') + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(2) + done() + }, + }) + }) + }) +}) + +describe('relay operator', () => { + describe('extractRelayReceive', () => { + it('should extract HRMP messages when they arrive on the Kusama relay chain', (done) => { + const { blocks, origin, messageControl, destination } = relayHrmpReceiveKusama + + const calls = jest.fn() + + const test$ = extractRelayReceive( + origin as NetworkURN, + messageControl, + registry + )(blocks.pipe(extractTxWithEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + expect(msg.recipient).toBe(destination) + expect(msg.extrinsicId).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Success') + expect(msg.error).toBeNull() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract HRMP messages when they arrive on the Polkadot relay chain', (done) => { + const { blocks, origin, messageControl, destination } = relayHrmpReceivePolkadot + + const calls = jest.fn() + + const test$ = extractRelayReceive( + origin as NetworkURN, + messageControl, + registry + )(blocks.pipe(extractTxWithEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + expect(msg.recipient).toBe(destination) + expect(msg.extrinsicId).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Success') + expect(msg.error).toBeNull() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(2) + done() + }, + }) + }) + }) +}) + +describe('bridge operator', () => { + describe('extractBridgeMessageAccepted', () => { + it('should extract accepted bridge messages on Bridge Hub', (done) => { + const { origin, destination, blocks, getStorage } = bridgeOutAcceptedKusama + + const calls = jest.fn() + + const test$ = extractBridgeMessageAccepted( + origin as NetworkURN, + registry, + getStorage + )(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + expect(msg.recipient).toBe(destination) + expect(msg.forwardId).toBeDefined() + expect(msg.messageId).toBeDefined() + expect(msg.bridgeKey).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + }) + + describe('extractBridgeMessageDelivered', () => { + it('should extract bridge message delivered event', (done) => { + const { origin, blocks } = bridgeOutDeliveredKusama + + const calls = jest.fn() + + const test$ = extractBridgeMessageDelivered(origin as NetworkURN, registry)(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.chainId).toBeDefined() + expect(msg.chainId).toBe(origin) + expect(msg.bridgeKey).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + }) + + describe('extractBridgeReceive', () => { + it('should extract bridge message receive events when message arrives on receving Bridge Hub', (done) => { + const { origin, blocks } = bridgeInPolkadot + + const calls = jest.fn() + const test$ = extractBridgeReceive(origin as NetworkURN)(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.chainId).toBeDefined() + expect(msg.chainId).toBe(origin) + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Success') + expect(msg.error).toBeNull() + expect(msg.bridgeKey).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + }) +}) + +describe('mapXcmSent', () => { + it('should extract stops for XCM with ExportMessage instruction', (done) => { + const calls = jest.fn() + + const ksmAHBridge = + '0003180004000100000740568a4a5f13000100000740568a4a5f0026020100a10f1401040002010903000700e87648170a130002010903000700e8764817000d010204000101002cb783d5c0ddcccd2608c83d43ee6fc19320408c24764c2f8ac164b27beaee372cf7d2f132944c0c518b4c862d6e68030f0ba49808125a805a11a9ede30d0410ab140d0100010100a10f2c4b422c686214e14cc3034a661b6a01dfd2c9a811ef9ec20ef798d0e687640e6d' + const buf = new Uint8Array(Buffer.from(ksmAHBridge, 'hex')) + + const xcms = fromXcmpFormat(buf, registry) + const test$ = mapXcmSent( + 'test-sub', + registry, + 'urn:ocn:kusama:1000' + )( + from( + xcms.map( + (x) => + new GenericXcmSentWithContext({ + event: {}, + sender: { signer: { id: 'xyz', publicKey: '0x01' }, extraSigners: [] }, + blockHash: '0x01', + blockNumber: '32', + extrinsicId: '32-4', + recipient: 'urn:ocn:kusama:1002', + messageData: buf, + messageHash: x.hash.toHex(), + messageId: getMessageId(x), + instructions: { + bytes: x.toU8a(), + json: x.toHuman(), + }, + }) + ) + ) + ) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.waypoint.chainId).toBe('urn:ocn:kusama:1000') + expect(msg.legs.length).toBe(3) + expect(msg.destination.chainId).toBe('urn:ocn:polkadot:1000') + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract stops for bridged XCM', (done) => { + const calls = jest.fn() + + const dotBridgeHubXcmOut = + '0003200b0104352509030b0100a10f01040002010903000700e87648170a130002010903000700e8764817000d010204000101002cb783d5c0ddcccd2608c83d43ee6fc19320408c24764c2f8ac164b27beaee372cf7d2f132944c0c518b4c862d6e68030f0ba49808125a805a11a9ede30d0410ab' + const buf = new Uint8Array(Buffer.from(dotBridgeHubXcmOut, 'hex')) + + const xcms = fromXcmpFormat(buf, registry) + const test$ = mapXcmSent( + 'test-sub', + registry, + 'urn:ocn:polkadot:1002' + )( + from( + xcms.map( + (x) => + new GenericXcmSentWithContext({ + event: {}, + sender: { signer: { id: 'xyz', publicKey: '0x01' }, extraSigners: [] }, + blockHash: '0x01', + blockNumber: '32', + extrinsicId: '32-4', + recipient: 'urn:ocn:polkadot:1000', + messageData: buf, + messageHash: x.hash.toHex(), + messageId: getMessageId(x), + instructions: { + bytes: x.toU8a(), + json: x.toHuman(), + }, + }) + ) + ) + ) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.waypoint.chainId).toBe('urn:ocn:polkadot:1002') + expect(msg.legs.length).toBe(1) + expect(msg.destination.chainId).toBe('urn:ocn:polkadot:1000') + expect(msg.forwardId).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) +}) diff --git a/packages/server/src/services/agents/xcm/ops/common.spec.ts b/packages/server/src/services/agents/xcm/ops/common.spec.ts new file mode 100644 index 00000000..c3d01130 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/common.spec.ts @@ -0,0 +1,191 @@ +import { jest } from '@jest/globals' + +import { from, of } from 'rxjs' + +import { registry } from '../../../testing/xcm.js' +import { GenericXcmSentWithContext } from '../types' +import { mapXcmSent } from './common' +import { getMessageId } from './util' +import { asVersionedXcm, fromXcmpFormat } from './xcm-format' + +describe('extract waypoints operator', () => { + describe('mapXcmSent', () => { + it('should extract stops for a V2 XCM message without hops', (done) => { + const calls = jest.fn() + + const moon5531424 = + '0002100004000000001700004b3471bb156b050a13000000001700004b3471bb156b05010300286bee0d010004000101001e08eb75720cb63fbfcbe7237c6d9b7cf6b4953518da6b38731d5bc65b9ffa32021000040000000017206d278c7e297945030a130000000017206d278c7e29794503010300286bee0d010004000101000257fd81d0a71b094c2c8d3e6c93a9b01a31a43d38408bb2c4c2b49a4c58eb01' + const buf = new Uint8Array(Buffer.from(moon5531424, 'hex')) + + const xcms = fromXcmpFormat(buf, registry) + const test$ = mapXcmSent( + 'test-sub', + registry, + 'urn:ocn:local:2004' + )( + from( + xcms.map( + (x) => + new GenericXcmSentWithContext({ + event: {}, + sender: { signer: { id: 'xyz', publicKey: '0x01' }, extraSigners: [] }, + blockHash: '0x01', + blockNumber: '32', + extrinsicId: '32-4', + recipient: 'urn:ocn:local:2104', + messageData: buf, + messageHash: x.hash.toHex(), + messageId: getMessageId(x), + instructions: { + bytes: x.toU8a(), + json: x.toHuman(), + }, + }) + ) + ) + ) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.waypoint.chainId).toBe('urn:ocn:local:2004') + expect(msg.legs.length).toBe(1) + expect(msg.legs[0]).toEqual({ + from: 'urn:ocn:local:2004', + to: 'urn:ocn:local:2104', + relay: 'urn:ocn:local:0', + type: 'hrmp', + }) + expect(msg.destination.chainId).toBe('urn:ocn:local:2104') + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(2) + done() + }, + }) + }) + + it('should extract stops for a XCM message hopping with InitiateReserveWithdraw', (done) => { + const calls = jest.fn() + + const polka19505060 = + '0310000400010300a10f043205011f000700f2052a011300010300a10f043205011f000700f2052a010010010204010100a10f0813000002043205011f0002093d00000d0102040001010081bd2c1d40052682633fb3e67eff151b535284d1d1a9633613af14006656f42b2c8e75728b841da22d8337ff5fadd1264f13addcdee755b01ce1a3afb9ef629b9a' + const buf = new Uint8Array(Buffer.from(polka19505060, 'hex')) + + const xcm = asVersionedXcm(buf, registry) + const test$ = mapXcmSent( + 'test-sub', + registry, + 'urn:ocn:local:0' + )( + of( + new GenericXcmSentWithContext({ + event: {}, + sender: { signer: { id: 'xyz', publicKey: '0x01' }, extraSigners: [] }, + blockHash: '0x01', + blockNumber: '32', + extrinsicId: '32-4', + recipient: 'urn:ocn:local:2034', + messageData: buf, + messageHash: xcm.hash.toHex(), + messageId: getMessageId(xcm), + instructions: { + bytes: xcm.toU8a(), + json: xcm.toHuman(), + }, + }) + ) + ) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.waypoint.chainId).toBe('urn:ocn:local:0') + expect(msg.legs.length).toBe(2) + expect(msg.legs[0]).toEqual({ + from: 'urn:ocn:local:0', + to: 'urn:ocn:local:2034', + type: 'hop', + }) + expect(msg.legs[1]).toEqual({ + from: 'urn:ocn:local:2034', + to: 'urn:ocn:local:1000', + relay: 'urn:ocn:local:0', + type: 'hop', + }) + expect(msg.destination.chainId).toBe('urn:ocn:local:1000') + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract stops for a XCM message hopping with DepositReserveAsset', (done) => { + const calls = jest.fn() + + const heiko5389341 = + '0003100004000000000f251850c822be030a13000000000f120c286411df01000e010204010100411f081300010100511f000f120c286411df01000d01020400010100842745b99b8042d28a7c677d9469332bfc24aa5266c7ec57c43c7af125a0c16c' + const buf = new Uint8Array(Buffer.from(heiko5389341, 'hex')) + + const xcms = fromXcmpFormat(buf, registry) + const test$ = mapXcmSent( + 'test-sub', + registry, + 'urn:ocn:local:2085' + )( + from( + xcms.map( + (x) => + new GenericXcmSentWithContext({ + event: {}, + sender: { signer: { id: 'xyz', publicKey: '0x01' }, extraSigners: [] }, + blockHash: '0x01', + blockNumber: '32', + extrinsicId: '32-4', + recipient: 'urn:ocn:local:2004', + messageData: buf, + messageHash: x.hash.toHex(), + messageId: getMessageId(x), + instructions: { + bytes: x.toU8a(), + json: x.toHuman(), + }, + }) + ) + ) + ) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.waypoint.chainId).toBe('urn:ocn:local:2085') + + expect(msg.legs.length).toBe(2) + expect(msg.legs[0]).toEqual({ + from: 'urn:ocn:local:2085', + to: 'urn:ocn:local:2004', + relay: 'urn:ocn:local:0', + type: 'hop', + }) + expect(msg.legs[1]).toEqual({ + from: 'urn:ocn:local:2004', + to: 'urn:ocn:local:2000', + relay: 'urn:ocn:local:0', + type: 'hop', + }) + + expect(msg.destination.chainId).toBe('urn:ocn:local:2000') + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + }) +}) diff --git a/packages/server/src/services/agents/xcm/ops/common.ts b/packages/server/src/services/agents/xcm/ops/common.ts new file mode 100644 index 00000000..25aff2b0 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/common.ts @@ -0,0 +1,191 @@ +import { Observable, map } from 'rxjs' + +import type { XcmV2Xcm, XcmV3Instruction, XcmV3Xcm } from '@polkadot/types/lookup' +import type { Registry } from '@polkadot/types/types' +import { hexToU8a, stringToU8a, u8aConcat } from '@polkadot/util' +import { blake2AsHex } from '@polkadot/util-crypto' + +import { types } from '@sodazone/ocelloids-sdk' +import { createNetworkId, getChainId, getConsensus, isOnSameConsensus } from '../../../config.js' +import { AnyJson, HexString } from '../../../subscriptions/types.js' +import { NetworkURN } from '../../../types.js' +import { GenericXcmSent, Leg, XcmSent, XcmSentWithContext } from '../types.js' +import { + getBridgeHubNetworkId, + getParaIdFromJunctions, + getSendersFromEvent, + networkIdFromMultiLocation, +} from './util.js' +import { asVersionedXcm } from './xcm-format.js' +import { XcmV4Instruction, XcmV4Xcm } from './xcm-types.js' + +// eslint-disable-next-line complexity +function recursiveExtractStops(origin: NetworkURN, instructions: XcmV2Xcm | XcmV3Xcm | XcmV4Xcm, stops: NetworkURN[]) { + for (const instruction of instructions) { + let nextStop + let message + + if (instruction.isDepositReserveAsset) { + const { dest, xcm } = instruction.asDepositReserveAsset + nextStop = dest + message = xcm + } else if (instruction.isInitiateReserveWithdraw) { + const { reserve, xcm } = instruction.asInitiateReserveWithdraw + nextStop = reserve + message = xcm + } else if (instruction.isInitiateTeleport) { + const { dest, xcm } = instruction.asInitiateTeleport + nextStop = dest + message = xcm + } else if (instruction.isTransferReserveAsset) { + const { dest, xcm } = instruction.asTransferReserveAsset + nextStop = dest + message = xcm + } else if ((instruction as XcmV3Instruction | XcmV4Instruction).isExportMessage) { + const { network, destination, xcm } = (instruction as XcmV3Instruction | XcmV4Instruction).asExportMessage + const paraId = getParaIdFromJunctions(destination) + if (paraId) { + const consensus = network.toString().toLowerCase() + const networkId = createNetworkId(consensus, paraId) + const bridgeHubNetworkId = getBridgeHubNetworkId(consensus) + // We assume that an ExportMessage will always go through Bridge Hub + if (bridgeHubNetworkId !== undefined && networkId !== bridgeHubNetworkId) { + stops.push(bridgeHubNetworkId) + } + stops.push(networkId) + recursiveExtractStops(networkId, xcm, stops) + } + } + + if (nextStop !== undefined && message !== undefined) { + const networkId = networkIdFromMultiLocation(nextStop, origin) + + if (networkId) { + stops.push(networkId) + recursiveExtractStops(networkId, message, stops) + } + } + } + + return stops +} + +function constructLegs(origin: NetworkURN, stops: NetworkURN[]) { + const legs: Leg[] = [] + const nodes = [origin].concat(stops) + for (let i = 0; i < nodes.length - 1; i++) { + const from = nodes[i] + const to = nodes[i + 1] + const leg = { + from, + to, + type: 'vmp', + } as Leg + + if (getConsensus(from) === getConsensus(to)) { + if (getChainId(from) !== '0' && getChainId(to) !== '0') { + leg.relay = createNetworkId(from, '0') + leg.type = 'hrmp' + } + } else { + leg.type = 'bridge' + } + + legs.push(leg) + } + + if (legs.length === 1) { + return legs + } + + for (let i = 0; i < legs.length - 1; i++) { + const leg1 = legs[i] + const leg2 = legs[i + 1] + if (isOnSameConsensus(leg1.from, leg2.to)) { + leg1.type = 'hop' + leg2.type = 'hop' + } + } + + if (legs.length === 1) { + return legs + } + + for (let i = 0; i < legs.length - 1; i++) { + const leg1 = legs[i] + const leg2 = legs[i + 1] + if (isOnSameConsensus(leg1.from, leg2.to)) { + leg1.type = 'hop' + leg2.type = 'hop' + } + } + + return legs +} + +/** + * Maps a XcmSentWithContext to a XcmSent message. + * Sets the destination as the final stop after recursively extracting all stops from the XCM message, + * constructs the legs for the message and constructs the waypoint context. + * + * @param id subscription ID + * @param registry type registry + * @param origin origin network URN + * @returns Observable + */ +export function mapXcmSent(id: string, registry: Registry, origin: NetworkURN) { + return (source: Observable): Observable => + source.pipe( + map((message) => { + const { instructions, recipient } = message + const stops: NetworkURN[] = [recipient] + const versionedXcm = asVersionedXcm(instructions.bytes, registry) + recursiveExtractStops(origin, versionedXcm[`as${versionedXcm.type}`], stops) + const legs = constructLegs(origin, stops) + + let forwardId: HexString | undefined + // TODO: extract to util? + if (origin === getBridgeHubNetworkId(origin) && message.messageId !== undefined) { + const constant = 'forward_id_for' + const derivedIdBuf = u8aConcat(stringToU8a(constant), hexToU8a(message.messageId)) + forwardId = blake2AsHex(derivedIdBuf) + } + return new GenericXcmSent(id, origin, message, legs, forwardId) + }) + ) +} + +export function blockEventToHuman(event: types.BlockEvent): AnyJson { + return { + extrinsicPosition: event.extrinsicPosition, + extrinsicId: event.extrinsicId, + blockNumber: event.blockNumber.toNumber(), + blockHash: event.blockHash.toHex(), + blockPosition: event.blockPosition, + eventId: event.eventId, + data: event.data.toHuman(), + index: event.index.toHuman(), + meta: event.meta.toHuman(), + method: event.method, + section: event.section, + } as AnyJson +} + +export function xcmMessagesSent() { + return (source: Observable): Observable => { + return source.pipe( + map((event) => { + const xcmMessage = event.data as any + return { + event: blockEventToHuman(event), + sender: getSendersFromEvent(event), + blockHash: event.blockHash.toHex(), + blockNumber: event.blockNumber.toPrimitive(), + extrinsicId: event.extrinsicId, + messageHash: xcmMessage.messageHash?.toHex(), + messageId: xcmMessage.messageId?.toHex(), + } as XcmSentWithContext + }) + ) + } +} diff --git a/packages/server/src/services/agents/xcm/ops/criteria.ts b/packages/server/src/services/agents/xcm/ops/criteria.ts new file mode 100644 index 00000000..acc56a8f --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/criteria.ts @@ -0,0 +1,49 @@ +import { ControlQuery, Criteria } from '@sodazone/ocelloids-sdk' +import { SignerData } from '../../../subscriptions/types.js' +import { NetworkURN } from '../../../types.js' +import { XcmSent } from '../types.js' + +export function sendersCriteria(senders?: string[] | '*'): Criteria { + if (senders === undefined || senders === '*') { + // match any + return {} + } else { + return { + $or: [ + { 'sender.signer.id': { $in: senders } }, + { 'sender.signer.publicKey': { $in: senders } }, + { 'sender.extraSigners.id': { $in: senders } }, + { 'sender.extraSigners.publicKey': { $in: senders } }, + ], + } + } +} + +// Assuming we are in the same consensus +export function messageCriteria(recipients: NetworkURN[]): Criteria { + return { + recipient: { $in: recipients }, + } +} + +/** + * Matches sender account address and public keys, including extra senders. + */ +export function matchSenders(query: ControlQuery, sender?: SignerData): boolean { + if (sender === undefined) { + return query.value.test({ + sender: undefined, + }) + } + + return query.value.test({ + sender, + }) +} + +/** + * Matches outbound XCM recipients. + */ +export function matchMessage(query: ControlQuery, xcm: XcmSent): boolean { + return query.value.test({ recipient: xcm.destination.chainId }) +} diff --git a/packages/server/src/services/agents/xcm/ops/dmp.spec.ts b/packages/server/src/services/agents/xcm/ops/dmp.spec.ts new file mode 100644 index 00000000..9d7bb522 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/dmp.spec.ts @@ -0,0 +1,236 @@ +import { jest } from '@jest/globals' + +import { of } from 'rxjs' + +import { extractEvents, extractTxWithEvents } from '@sodazone/ocelloids-sdk' +import { + dmpReceive, + dmpSendMultipleMessagesInQueue, + dmpSendSingleMessageInQueue, + dmpXcmPalletSentEvent, + registry, + xcmHop, + xcmHopOrigin, +} from '../../../testing/xcm.js' +import { extractDmpReceive, extractDmpSend, extractDmpSendByEvent } from './dmp.js' + +const getDmp = () => + of([ + { + msg: new Uint8Array(Buffer.from('0002100004000000001700004b3471bb156b050a1300000000', 'hex')), + toU8a: () => new Uint8Array(Buffer.from('0002100004000000001700004b3471bb156b050a1300000000', 'hex')), + }, + ] as unknown as any) + +describe('dmp operator', () => { + describe('extractDmpSend', () => { + it('should extract DMP sent message', (done) => { + const { origin, blocks } = dmpSendSingleMessageInQueue + + const calls = jest.fn() + + const test$ = extractDmpSend(origin, getDmp, registry)(blocks.pipe(extractTxWithEvents())) + + test$.subscribe({ + next: (msg) => { + calls() + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.instructions).toBeDefined() + expect(msg.messageData).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract DMP sent for multi-leg messages', (done) => { + const { origin, blocks } = xcmHopOrigin + + const calls = jest.fn() + + const test$ = extractDmpSendByEvent(origin, getDmp, registry)(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.instructions).toBeDefined() + expect(msg.messageData).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract DMP sent message with multiple messages in the queue', (done) => { + const { origin, blocks } = dmpSendMultipleMessagesInQueue + + const calls = jest.fn() + + const test$ = extractDmpSend(origin, getDmp, registry)(blocks.pipe(extractTxWithEvents())) + + test$.subscribe({ + next: (msg) => { + calls() + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.instructions).toBeDefined() + expect(msg.messageData).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + }) + + describe('extractDmpSendByEvent', () => { + it('should extract DMP sent message filtered by event', (done) => { + const { origin, blocks } = dmpXcmPalletSentEvent + + const calls = jest.fn() + + const test$ = extractDmpSendByEvent(origin, getDmp, registry)(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + calls() + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.instructions).toBeDefined() + expect(msg.messageData).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + }) + + describe('extractDmpReceive', () => { + it('should extract DMP received message with outcome success', (done) => { + const { successBlocks } = dmpReceive + + const calls = jest.fn() + + const test$ = extractDmpReceive()(successBlocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + calls() + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Success') + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract failed DMP received message with error', (done) => { + const { failBlocks } = dmpReceive + + const calls = jest.fn() + + const test$ = extractDmpReceive()(failBlocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + calls() + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Fail') + expect(msg.error).toBeDefined() + expect(msg.error).toBe('UntrustedReserveLocation') + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract dmp receive with asset trap', (done) => { + const { trappedBlocks } = dmpReceive + + const calls = jest.fn() + + const test$ = extractDmpReceive()(trappedBlocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Fail') + expect(msg.error).toBeDefined() + expect(msg.error).toBe('FailedToTransactAsset') + expect(msg.assetsTrapped).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract DMP received for hop message', (done) => { + const { blocks } = xcmHop + + const calls = jest.fn() + + const test$ = extractDmpReceive()(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Success') + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + }) +}) diff --git a/packages/server/src/services/agents/xcm/ops/dmp.ts b/packages/server/src/services/agents/xcm/ops/dmp.ts new file mode 100644 index 00000000..593528d2 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/dmp.ts @@ -0,0 +1,332 @@ +import { Observable, bufferCount, filter, map, mergeMap } from 'rxjs' + +// NOTE: we use Polkadot augmented types +import '@polkadot/api-augment/polkadot' +import type { Compact } from '@polkadot/types' +import type { IU8a } from '@polkadot/types-codec/types' +import type { BlockNumber } from '@polkadot/types/interfaces' +import type { Outcome } from '@polkadot/types/interfaces/xcm' +import type { Registry } from '@polkadot/types/types' + +import { filterNonNull, types } from '@sodazone/ocelloids-sdk' + +import { AnyJson, SignerData } from '../../../subscriptions/types.js' +import { NetworkURN } from '../../../types.js' +import { GetDownwardMessageQueues } from '../types-augmented.js' +import { + GenericXcmInboundWithContext, + GenericXcmSentWithContext, + XcmInboundWithContext, + XcmSentWithContext, +} from '../types.js' +import { blockEventToHuman } from './common.js' +import { + getMessageId, + getSendersFromEvent, + getSendersFromExtrinsic, + mapAssetsTrapped, + matchEvent, + matchExtrinsic, + matchProgramByTopic, + networkIdFromMultiLocation, + networkIdFromVersionedMultiLocation, +} from './util.js' +import { asVersionedXcm } from './xcm-format.js' +import { XcmVersionedAssets, XcmVersionedLocation, XcmVersionedXcm } from './xcm-types.js' + +/* + ================================================================================== + NOTICE + ================================================================================== + + This DMP message matching implementation is provisional and will be replaced + as soon as possible. + + For details see: /~https://github.com/paritytech/polkadot-sdk/issues/1905 +*/ + +type Json = { [property: string]: Json } +type XcmContext = { + recipient: NetworkURN + data: Uint8Array + program: XcmVersionedXcm + blockHash: IU8a + blockNumber: Compact + sender?: SignerData + event?: types.BlockEvent +} + +// eslint-disable-next-line complexity +function matchInstructions( + xcmProgram: XcmVersionedXcm, + assets: XcmVersionedAssets, + beneficiary: XcmVersionedLocation +): boolean { + const program = xcmProgram.value.toHuman() as Json[] + let sameAssetFun = false + let sameBeneficiary = false + + for (const instruction of program) { + const { DepositAsset, ReceiveTeleportedAsset, ReserveAssetDeposited } = instruction + + if (ReceiveTeleportedAsset || ReserveAssetDeposited) { + const fun = ReceiveTeleportedAsset?.[0]?.fun ?? ReserveAssetDeposited[0]?.fun + if (fun) { + const asset = assets.value.toHuman() as Json + sameAssetFun = JSON.stringify(fun) === JSON.stringify(asset[0]?.fun) + } + continue + } + + if (DepositAsset) { + sameBeneficiary = JSON.stringify(DepositAsset.beneficiary) === JSON.stringify(beneficiary.value.toHuman()) + break + } + } + + return sameAssetFun && sameBeneficiary +} + +function createXcmMessageSent({ + recipient, + data, + program, + blockHash, + blockNumber, + sender, + event, +}: XcmContext): GenericXcmSentWithContext { + const messageId = getMessageId(program) + + return new GenericXcmSentWithContext({ + blockHash: blockHash.toHex(), + blockNumber: blockNumber.toPrimitive(), + event: event ? blockEventToHuman(event) : {}, + recipient, + instructions: { + bytes: program.toU8a(), + json: program.toHuman(), + }, + messageData: data, + messageHash: program.hash.toHex(), + messageId, + sender, + }) +} + +// Will be obsolete after DMP refactor: +// /~https://github.com/paritytech/polkadot-sdk/pull/1246 +function findDmpMessagesFromTx(getDmp: GetDownwardMessageQueues, registry: Registry, origin: NetworkURN) { + return (source: Observable): Observable => { + return source.pipe( + map((tx) => { + const dest = tx.extrinsic.args[0] as XcmVersionedLocation + const beneficiary = tx.extrinsic.args[1] as XcmVersionedLocation + const assets = tx.extrinsic.args[2] as XcmVersionedAssets + + const recipient = networkIdFromVersionedMultiLocation(dest, origin) + + if (recipient) { + return { + tx, + recipient, + beneficiary, + assets, + } + } + + return null + }), + filterNonNull(), + mergeMap(({ tx, recipient, beneficiary, assets }) => { + return getDmp(tx.extrinsic.blockHash.toHex(), recipient).pipe( + map((messages) => { + const { blockHash, blockNumber } = tx.extrinsic + if (messages.length === 1) { + const data = messages[0].msg + const program = asVersionedXcm(data, registry) + return createXcmMessageSent({ + blockHash, + blockNumber, + recipient, + data, + program, + sender: getSendersFromExtrinsic(tx.extrinsic), + }) + } else { + // XXX Temporary matching heuristics until DMP message + // sent event is implemented. + // Only matches the first message found. + for (const message of messages) { + const data = message.msg + const program = asVersionedXcm(data, registry) + if (matchInstructions(program, assets, beneficiary)) { + return createXcmMessageSent({ + blockHash, + blockNumber, + recipient, + data, + program, + sender: getSendersFromExtrinsic(tx.extrinsic), + }) + } + } + + return null + } + }), + filterNonNull() + ) + }) + ) + } +} + +function findDmpMessagesFromEvent(origin: NetworkURN, getDmp: GetDownwardMessageQueues, registry: Registry) { + return (source: Observable): Observable => { + return source.pipe( + map((event) => { + if (matchEvent(event, 'xcmPallet', 'Sent')) { + const { destination, messageId } = event.data as any + const recipient = networkIdFromMultiLocation(destination, origin) + + if (recipient) { + return { + recipient, + messageId, + event, + } + } + } + + return null + }), + filterNonNull(), + mergeMap(({ recipient, messageId, event }) => { + return getDmp(event.blockHash.toHex(), recipient as NetworkURN).pipe( + map((messages) => { + const { blockHash, blockNumber } = event + if (messages.length === 1) { + const data = messages[0].msg + const program = asVersionedXcm(data, registry) + return createXcmMessageSent({ + blockHash, + blockNumber, + recipient, + event, + data, + program, + sender: getSendersFromEvent(event), + }) + } else { + // Since we are matching by topic and it is assumed that the TopicId is unique + // we can break out of the loop on first matching message found. + for (const message of messages) { + const data = message.msg + const program = asVersionedXcm(data, registry) + if (matchProgramByTopic(program, messageId)) { + return createXcmMessageSent({ + blockHash, + blockNumber, + recipient, + event, + data, + program, + sender: getSendersFromEvent(event), + }) + } + } + + return null + } + }), + filterNonNull() + ) + }) + ) + } +} + +const METHODS_DMP = ['limitedReserveTransferAssets', 'reserveTransferAssets', 'limitedTeleportAssets', 'teleportAssets'] + +export function extractDmpSend(origin: NetworkURN, getDmp: GetDownwardMessageQueues, registry: Registry) { + return (source: Observable): Observable => { + return source.pipe( + filter((tx) => { + const { extrinsic } = tx + return tx.dispatchError === undefined && matchExtrinsic(extrinsic, 'xcmPallet', METHODS_DMP) + }), + findDmpMessagesFromTx(getDmp, registry, origin) + ) + } +} + +export function extractDmpSendByEvent(origin: NetworkURN, getDmp: GetDownwardMessageQueues, registry: Registry) { + return (source: Observable): Observable => { + return source.pipe(findDmpMessagesFromEvent(origin, getDmp, registry)) + } +} + +function extractXcmError(outcome: Outcome) { + if (outcome.isIncomplete) { + const [_, err] = outcome.asIncomplete + return err.type.toString() + } + if (outcome.isError) { + return outcome.asError.type.toString() + } + return undefined +} + +function createDmpReceivedWithContext(event: types.BlockEvent, assetsTrappedEvent?: types.BlockEvent) { + const xcmMessage = event.data as any + let outcome: 'Success' | 'Fail' = 'Fail' + let error: AnyJson + if (xcmMessage.outcome !== undefined) { + const o = xcmMessage.outcome as Outcome + outcome = o.isComplete ? 'Success' : 'Fail' + error = extractXcmError(o) + } else if (xcmMessage.success !== undefined) { + outcome = xcmMessage.success ? 'Success' : 'Fail' + } + + const messageId = xcmMessage.messageId ? xcmMessage.messageId.toHex() : xcmMessage.id.toHex() + const messageHash = xcmMessage.messageHash?.toHex() ?? messageId + const assetsTrapped = mapAssetsTrapped(assetsTrappedEvent) + + return new GenericXcmInboundWithContext({ + event: blockEventToHuman(event), + blockHash: event.blockHash.toHex(), + blockNumber: event.blockNumber.toPrimitive(), + extrinsicId: event.extrinsicId, + messageHash, + messageId, + outcome, + error, + assetsTrapped, + }) +} + +export function extractDmpReceive() { + return (source: Observable): Observable => { + return source.pipe( + bufferCount(2, 1), + map(([maybeAssetTrapEvent, maybeDmpEvent]) => { + // in reality we expect a continuous stream of events but + // in tests, maybeDmpEvent could be undefined if there are odd number of events + if ( + maybeDmpEvent && + (matchEvent(maybeDmpEvent, 'dmpQueue', 'ExecutedDownward') || + matchEvent(maybeDmpEvent, 'messageQueue', 'Processed')) + ) { + const assetTrapEvent = matchEvent(maybeAssetTrapEvent, 'polkadotXcm', 'AssetsTrapped') + ? maybeAssetTrapEvent + : undefined + return createDmpReceivedWithContext(maybeDmpEvent, assetTrapEvent) + } + return null + }), + filterNonNull() + ) + } +} diff --git a/packages/server/src/services/agents/xcm/ops/pk-bridge.ts b/packages/server/src/services/agents/xcm/ops/pk-bridge.ts new file mode 100644 index 00000000..2109a5cc --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/pk-bridge.ts @@ -0,0 +1,206 @@ +import type { Bytes, Vec } from '@polkadot/types-codec' +import type { Registry } from '@polkadot/types/types' +import { compactFromU8aLim, hexToU8a, stringToU8a, u8aConcat, u8aToHex } from '@polkadot/util' +import { blake2AsU8a } from '@polkadot/util-crypto' +import { blake2AsHex } from '@polkadot/util-crypto' +import { Observable, filter, from, mergeMap } from 'rxjs' + +import { types } from '@sodazone/ocelloids-sdk' + +import { getConsensus } from '../../../config.js' +import { bridgeStorageKeys } from '../../../subscriptions/storage.js' +import { HexString } from '../../../subscriptions/types.js' +import { NetworkURN } from '../../../types.js' +import { GetStorageAt } from '../types-augmented.js' +import { + GenericXcmBridgeAcceptedWithContext, + GenericXcmBridgeDeliveredWithContext, + GenericXcmBridgeInboundWithContext, + XcmBridgeAcceptedWithContext, + XcmBridgeDeliveredWithContext, + XcmBridgeInboundWithContext, +} from '../types.js' +import { blockEventToHuman } from './common.js' +import { getMessageId, getSendersFromEvent, matchEvent, networkIdFromInteriorLocation } from './util.js' +import { + BpMessagesReceivedMessages, + BridgeMessage, + BridgeMessageAccepted, + BridgeMessagesDelivered, +} from './xcm-types.js' + +export function extractBridgeMessageAccepted(origin: NetworkURN, registry: Registry, getStorage: GetStorageAt) { + return (source: Observable): Observable => { + return source.pipe( + filter((event) => + matchEvent( + event, + ['bridgePolkadotMessages', 'bridgeKusamaMessages', 'bridgeRococoMessages', 'bridgeWestendMessages'], + 'MessageAccepted' + ) + ), + mergeMap((blockEvent) => { + const data = blockEvent.data as unknown as BridgeMessageAccepted + const laneId = data.laneId.toU8a() + const nonce = data.nonce.toU8a() + const consensus = getConsensus(origin) + const { messagesOutboundPartial } = bridgeStorageKeys[consensus] + + const value = u8aConcat(laneId, nonce) + + const arg = u8aConcat(blake2AsU8a(value, 128), value) + const key = (messagesOutboundPartial + Buffer.from(arg).toString('hex')) as HexString + + return getStorage(blockEvent.blockHash.toHex(), key).pipe( + mergeMap((buf) => { + // if registry does not have needed types, register them + if (!registry.hasType('BridgeMessage')) { + registry.register({ + VersionedInteriorLocation: { + _enum: { + V0: null, + V1: null, + V2: 'XcmV2MultilocationJunctions', + V3: 'XcmV3Junctions', + }, + }, + BridgeMessage: { + universal_dest: 'VersionedInteriorLocation', + message: 'XcmVersionedXcm', + }, + }) + } + + const msgs: XcmBridgeAcceptedWithContext[] = [] + + // we use the length of the u8 array instead of Option + // since the value is bare. + if (buf.length > 1) { + const bytes = registry.createType('Bytes', buf) as unknown as Bytes + let baseOffset = 0 + + while (baseOffset < bytes.length) { + const [offset, length] = compactFromU8aLim(bytes.slice(baseOffset)) + const increment = offset + length + const msgBuf = bytes.slice(baseOffset + offset, baseOffset + increment) + baseOffset += increment + + const bridgeMessage = registry.createType('BridgeMessage', msgBuf) as unknown as BridgeMessage + const recipient = networkIdFromInteriorLocation(bridgeMessage.universal_dest) + if (recipient === undefined) { + continue + } + const xcmProgram = bridgeMessage.message + const messageId = getMessageId(xcmProgram) + let forwardId: HexString | undefined + if (messageId !== undefined) { + const constant = 'forward_id_for' + const derivedIdBuf = u8aConcat(stringToU8a(constant), hexToU8a(messageId)) + forwardId = blake2AsHex(derivedIdBuf) + } + + const xcmBridgeSent = new GenericXcmBridgeAcceptedWithContext({ + event: blockEvent.toHuman(), + blockHash: blockEvent.blockHash.toHex(), + blockNumber: blockEvent.blockNumber.toPrimitive(), + extrinsicId: blockEvent.extrinsicId, + messageData: xcmProgram.toHex(), + recipient, + messageHash: xcmProgram.hash.toHex(), + instructions: xcmProgram.toHuman(), + messageId, + forwardId, + bridgeKey: u8aToHex(hexToU8a(key).slice(48)), + chainId: origin, + }) + + msgs.push(xcmBridgeSent) + } + } + return from(msgs) + }) + ) + }) + ) + } +} + +export function extractBridgeMessageDelivered(origin: NetworkURN, registry: Registry) { + return (source: Observable): Observable => { + return source.pipe( + filter((event) => + matchEvent( + event, + ['bridgePolkadotMessages', 'bridgeKusamaMessages', 'bridgeRococoMessages', 'bridgeWestendMessages'], + 'MessagesDelivered' + ) + ), + mergeMap((blockEvent) => { + const data = blockEvent.data as unknown as BridgeMessagesDelivered + const begin = data.messages.begin.toNumber() + const end = data.messages.end.toNumber() + const laneId = data.laneId.toU8a() + const msgs: XcmBridgeDeliveredWithContext[] = [] + + for (let i = begin; i <= end; i++) { + const nonce = registry.createType('u64', i) + const value = u8aConcat(laneId, nonce.toU8a()) + + const bridgeKey = u8aToHex(value) + msgs.push( + new GenericXcmBridgeDeliveredWithContext({ + chainId: origin, + bridgeKey, + event: blockEventToHuman(blockEvent), + extrinsicId: blockEvent.extrinsicId, + blockNumber: blockEvent.blockNumber.toPrimitive(), + blockHash: blockEvent.blockHash.toHex(), + sender: getSendersFromEvent(blockEvent), + }) + ) + } + + return from(msgs) + }) + ) + } +} + +export function extractBridgeReceive(origin: NetworkURN) { + return (source: Observable): Observable => { + return source.pipe( + filter((event) => + matchEvent( + event, + ['bridgePolkadotMessages', 'bridgeKusamaMessages', 'bridgeRococoMessages', 'bridgeWestendMessages'], + 'MessagesReceived' + ) + ), + mergeMap((event) => { + // for some reason the Vec is wrapped with another array? + // TODO: investigate for cases of multiple lanes + const receivedMessages = event.data[0] as unknown as Vec + const inboundMsgs: XcmBridgeInboundWithContext[] = [] + for (const message of receivedMessages) { + const laneId = message.lane + for (const [nonce, result] of message.receiveResults) { + const key = u8aConcat(laneId.toU8a(), nonce.toU8a()) + inboundMsgs.push( + new GenericXcmBridgeInboundWithContext({ + chainId: origin, + bridgeKey: u8aToHex(key), + event: blockEventToHuman(event), + extrinsicId: event.extrinsicId, + blockNumber: event.blockNumber.toPrimitive(), + blockHash: event.blockHash.toHex(), + outcome: result.isDispatched ? 'Success' : 'Fail', + error: result.isDispatched ? null : result.type, + }) + ) + } + } + return from(inboundMsgs) + }) + ) + } +} diff --git a/packages/server/src/services/agents/xcm/ops/relay.spec.ts b/packages/server/src/services/agents/xcm/ops/relay.spec.ts new file mode 100644 index 00000000..92467699 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/relay.spec.ts @@ -0,0 +1,68 @@ +import { jest } from '@jest/globals' + +import { extractTxWithEvents } from '@sodazone/ocelloids-sdk' +import { registry, relayHrmpReceive } from '../../../testing/xcm.js' +import { NetworkURN } from '../../types.js' +import { messageCriteria } from './criteria.js' +import { extractRelayReceive } from './relay.js' + +describe('relay operator', () => { + describe('extractRelayReceive', () => { + it('should extract HRMP messages when they arrive on the relay chain', (done) => { + const { blocks, messageControl, origin, destination } = relayHrmpReceive + + const calls = jest.fn() + + const test$ = extractRelayReceive( + origin as NetworkURN, + messageControl, + registry + )(blocks.pipe(extractTxWithEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + expect(msg.recipient).toBe(destination) + expect(msg.extrinsicId).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Success') + expect(msg.error).toBeNull() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(2) + done() + }, + }) + }) + + it('should pass through if messagae control is updated to remove destination', (done) => { + const { blocks, messageControl, origin } = relayHrmpReceive + + const calls = jest.fn() + + const test$ = extractRelayReceive( + origin as NetworkURN, + messageControl, + registry + )(blocks.pipe(extractTxWithEvents())) + + // remove destination from criteria + messageControl.change(messageCriteria(['urn:ocn:local:2000', 'urn:ocn:local:2006'])) + + test$.subscribe({ + next: (_) => { + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(0) + done() + }, + }) + }) + }) +}) diff --git a/packages/server/src/services/agents/xcm/ops/relay.ts b/packages/server/src/services/agents/xcm/ops/relay.ts new file mode 100644 index 00000000..b6119d0d --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/relay.ts @@ -0,0 +1,52 @@ +import { Observable, filter, map, mergeMap } from 'rxjs' + +import type { PolkadotPrimitivesV6InherentData } from '@polkadot/types/lookup' +import type { Registry } from '@polkadot/types/types' + +import { ControlQuery, filterNonNull, types } from '@sodazone/ocelloids-sdk' +import { createNetworkId, getChainId } from '../../../config.js' +import { NetworkURN } from '../../../types.js' +import { GenericXcmRelayedWithContext, XcmRelayedWithContext } from '../types.js' +import { getMessageId, matchExtrinsic } from './util.js' +import { fromXcmpFormat } from './xcm-format.js' + +export function extractRelayReceive(origin: NetworkURN, messageControl: ControlQuery, registry: Registry) { + return (source: Observable): Observable => { + return source.pipe( + filter(({ extrinsic }) => matchExtrinsic(extrinsic, 'parainherent', 'enter')), + map(({ extrinsic, dispatchError }) => { + const { backedCandidates } = extrinsic.args[0] as unknown as PolkadotPrimitivesV6InherentData + const backed = backedCandidates.find((c) => c.candidate.descriptor.paraId.toString() === getChainId(origin)) + if (backed) { + const { horizontalMessages } = backed.candidate.commitments + const message = horizontalMessages.find(({ recipient }) => { + return messageControl.value.test({ + recipient: createNetworkId(origin, recipient.toString()), + }) + }) + if (message) { + const xcms = fromXcmpFormat(message.data, registry) + const { blockHash, blockNumber, extrinsicId } = extrinsic + return xcms.map( + (xcmProgram) => + new GenericXcmRelayedWithContext({ + blockHash: blockHash.toHex(), + blockNumber: blockNumber.toPrimitive(), + recipient: createNetworkId(origin, message.recipient.toString()), + messageHash: xcmProgram.hash.toHex(), + messageId: getMessageId(xcmProgram), + origin, + extrinsicId, + outcome: dispatchError ? 'Fail' : 'Success', + error: dispatchError ? dispatchError.toHuman() : null, + }) + ) + } + } + return null + }), + filterNonNull(), + mergeMap((x) => x) + ) + } +} diff --git a/packages/server/src/services/agents/xcm/ops/ump.spec.ts b/packages/server/src/services/agents/xcm/ops/ump.spec.ts new file mode 100644 index 00000000..ddca4422 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/ump.spec.ts @@ -0,0 +1,114 @@ +import { jest } from '@jest/globals' +import { extractEvents } from '@sodazone/ocelloids-sdk' + +import { registry, umpReceive, umpSend } from '../../../testing/xcm.js' + +import { extractUmpReceive, extractUmpSend } from './ump.js' + +describe('ump operator', () => { + describe('extractUmpSend', () => { + it('should extract UMP sent message', (done) => { + const { origin, blocks, getUmp } = umpSend + + const calls = jest.fn() + + const test$ = extractUmpSend(origin, getUmp, registry)(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + calls() + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.instructions).toBeDefined() + expect(msg.messageData).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + }) + + describe('extractUmpReceive', () => { + it('should extract failed UMP received message', (done) => { + const { successBlocks } = umpReceive + + const calls = jest.fn() + + const test$ = extractUmpReceive('urn:ocn:local:1000')(successBlocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + calls() + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Success') + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract UMP receive with outcome fail', (done) => { + const { failBlocks } = umpReceive + + const calls = jest.fn() + + const test$ = extractUmpReceive('urn:ocn:local:1000')(failBlocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + calls() + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Fail') + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract ump receive with asset trap', (done) => { + const { trappedBlocks } = umpReceive + + const calls = jest.fn() + + const test$ = extractUmpReceive('urn:ocn:local:2004')(trappedBlocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + calls() + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Success') + expect(msg.error).toBeNull() + expect(msg.assetsTrapped).toBeDefined() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(2) + done() + }, + }) + }) + }) +}) diff --git a/packages/server/src/services/agents/xcm/ops/ump.ts b/packages/server/src/services/agents/xcm/ops/ump.ts new file mode 100644 index 00000000..ecbe73ac --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/ump.ts @@ -0,0 +1,119 @@ +import { Observable, bufferCount, filter, map, mergeMap } from 'rxjs' + +// NOTE: we use Polkadot augmented types +import '@polkadot/api-augment/polkadot' +import type { Registry } from '@polkadot/types/types' + +import { filterNonNull, types } from '@sodazone/ocelloids-sdk' + +import { getChainId, getRelayId } from '../../../config.js' +import { NetworkURN } from '../../../types.js' +import { GetOutboundUmpMessages } from '../types-augmented.js' +import { + GenericXcmInboundWithContext, + GenericXcmSentWithContext, + XcmInboundWithContext, + XcmSentWithContext, +} from '../types.js' +import { MessageQueueEventContext } from '../types.js' +import { blockEventToHuman, xcmMessagesSent } from './common.js' +import { getMessageId, getParaIdFromOrigin, mapAssetsTrapped, matchEvent } from './util.js' +import { asVersionedXcm } from './xcm-format.js' + +const METHODS_MQ_PROCESSED = ['Processed', 'ProcessingFailed'] + +function createUmpReceivedWithContext( + subOrigin: NetworkURN, + event: types.BlockEvent, + assetsTrappedEvent?: types.BlockEvent +): XcmInboundWithContext | null { + const { id, origin, success, error } = event.data as unknown as MessageQueueEventContext + // Received event only emits field `message_id`, + // which is actually the message hash in the current runtime. + const messageId = id.toHex() + const messageHash = messageId + const messageOrigin = getParaIdFromOrigin(origin) + const assetsTrapped = mapAssetsTrapped(assetsTrappedEvent) + // If we can get message origin, only return message if origin matches with subscription origin + // If no origin, we will return the message without matching with subscription origin + if (messageOrigin === undefined || messageOrigin === getChainId(subOrigin)) { + return new GenericXcmInboundWithContext({ + event: blockEventToHuman(event), + blockHash: event.blockHash.toHex(), + blockNumber: event.blockNumber.toPrimitive(), + messageHash, + messageId, + outcome: success?.isTrue ? 'Success' : 'Fail', + error: error ? error.toHuman() : null, + assetsTrapped, + }) + } + return null +} + +function findOutboundUmpMessage( + origin: NetworkURN, + getOutboundUmpMessages: GetOutboundUmpMessages, + registry: Registry +) { + return (source: Observable): Observable => { + return source.pipe( + mergeMap((sentMsg) => { + const { blockHash, messageHash, messageId } = sentMsg + return getOutboundUmpMessages(blockHash).pipe( + map((messages) => { + return messages + .map((data) => { + const xcmProgram = asVersionedXcm(data, registry) + return new GenericXcmSentWithContext({ + ...sentMsg, + messageData: data.toU8a(), + recipient: getRelayId(origin), // always relay + messageHash: xcmProgram.hash.toHex(), + messageId: getMessageId(xcmProgram), + instructions: { + bytes: xcmProgram.toU8a(), + json: xcmProgram.toHuman(), + }, + }) + }) + .find((msg) => { + return messageId ? msg.messageId === messageId : msg.messageHash === messageHash + }) + }), + filterNonNull() + ) + }) + ) + } +} + +export function extractUmpSend(origin: NetworkURN, getOutboundUmpMessages: GetOutboundUmpMessages, registry: Registry) { + return (source: Observable): Observable => { + return source.pipe( + filter( + (event) => matchEvent(event, 'parachainSystem', 'UpwardMessageSent') || matchEvent(event, 'polkadotXcm', 'Sent') + ), + xcmMessagesSent(), + findOutboundUmpMessage(origin, getOutboundUmpMessages, registry) + ) + } +} + +export function extractUmpReceive(originId: NetworkURN) { + return (source: Observable): Observable => { + return source.pipe( + bufferCount(2, 1), + map(([maybeAssetTrapEvent, maybeUmpEvent]) => { + if (maybeUmpEvent && matchEvent(maybeUmpEvent, 'messageQueue', METHODS_MQ_PROCESSED)) { + const assetTrapEvent = matchEvent(maybeAssetTrapEvent, 'xcmPallet', 'AssetsTrapped') + ? maybeAssetTrapEvent + : undefined + return createUmpReceivedWithContext(originId, maybeUmpEvent, assetTrapEvent) + } + return null + }), + filterNonNull() + ) + } +} diff --git a/packages/server/src/services/agents/xcm/ops/util.spec.ts b/packages/server/src/services/agents/xcm/ops/util.spec.ts new file mode 100644 index 00000000..f1bd8659 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/util.spec.ts @@ -0,0 +1,341 @@ +import { types } from '@sodazone/ocelloids-sdk' + +import { + getMessageId, + getParaIdFromMultiLocation, + getParaIdFromOrigin, + getSendersFromEvent, + getSendersFromExtrinsic, + networkIdFromMultiLocation, +} from './util.js' + +describe('xcm ops utils', () => { + describe('getSendersFromExtrinsic', () => { + it('should extract signers data for signed extrinsic', () => { + const signerData = getSendersFromExtrinsic({ + isSigned: true, + signer: { + toPrimitive: () => '', + toHex: () => '0x0', + }, + extraSigners: [], + } as unknown as types.ExtrinsicWithId) + + expect(signerData).toBeDefined() + }) + + it('should return undefined on unsigned extrinsic', () => { + const signerData = getSendersFromExtrinsic({ + isSigned: false, + extraSigners: [], + } as unknown as types.ExtrinsicWithId) + + expect(signerData).toBeUndefined() + }) + + it('should throw error on malformed extrinsic', () => { + expect(() => + getSendersFromExtrinsic({ + isSigned: true, + } as unknown as types.ExtrinsicWithId) + ).toThrow() + }) + + it('should get extra signers data', () => { + const signerData = getSendersFromExtrinsic({ + isSigned: true, + signer: { + value: { + toPrimitive: () => '', + toHex: () => '0x0', + }, + }, + extraSigners: [ + { + type: 'test', + address: { + value: { + toPrimitive: () => '', + toHex: () => '0x0', + }, + }, + }, + { + type: 'test', + address: { + value: { + toPrimitive: () => '', + toHex: () => '0x0', + }, + }, + }, + ], + } as unknown as types.ExtrinsicWithId) + + expect(signerData).toBeDefined() + expect(signerData?.extraSigners.length).toBe(2) + }) + }) + describe('getSendersFromEvent', () => { + it('should extract signers data for an event with signed extrinsic', () => { + const signerData = getSendersFromEvent({ + extrinsic: { + isSigned: true, + signer: { + toPrimitive: () => '', + toHex: () => '0x0', + }, + extraSigners: [], + }, + } as unknown as types.EventWithId) + + expect(signerData).toBeDefined() + }) + it('should return undefined for an event without extrinsic', () => { + const signerData = getSendersFromEvent({} as unknown as types.EventWithId) + + expect(signerData).toBeUndefined() + }) + }) + describe('getMessageId', () => { + it('should get the message id from setTopic V3 instruction', () => { + const messageId = getMessageId({ + type: 'V3', + asV3: [ + { + isSetTopic: true, + asSetTopic: { + toHex: () => '0x012233', + }, + }, + ], + } as unknown as any) + expect(messageId).toBe('0x012233') + }) + it('should get the message id from setTopic V4 instruction', () => { + const messageId = getMessageId({ + type: 'V4', + asV4: [ + { + isSetTopic: true, + asSetTopic: { + toHex: () => '0x012233', + }, + }, + ], + } as unknown as any) + expect(messageId).toBe('0x012233') + }) + it('should return undefined for V3 without setTopic', () => { + const messageId = getMessageId({ + type: 'V3', + asV3: [ + { + isSetTopic: false, + }, + ], + } as unknown as any) + expect(messageId).toBeUndefined() + }) + it('should return undefined for V2 instruction', () => { + const messageId = getMessageId({ + type: 'V2', + } as unknown as any) + expect(messageId).toBeUndefined() + }) + }) + describe('getParaIdFromOrigin', () => { + it('should get para id from UMP origin', () => { + const paraId = getParaIdFromOrigin({ + isUmp: true, + asUmp: { + isPara: true, + asPara: { + toString: () => '10', + }, + }, + } as unknown as any) + expect(paraId).toBe('10') + }) + it('should return undefined from unknown origin', () => { + expect( + getParaIdFromOrigin({ + isUmp: true, + asUmp: { + isPara: false, + }, + } as unknown as any) + ).toBeUndefined() + expect( + getParaIdFromOrigin({ + isUmp: false, + } as unknown as any) + ).toBeUndefined() + }) + }) + describe('getParaIdFromMultiLocation', () => { + it('should get paraId from local relay multi location', () => { + const paraId = getParaIdFromMultiLocation({ + interior: { + type: 'Here', + }, + parents: { + toNumber: () => 1, + }, + } as unknown as any) + expect(paraId).toBe('0') + }) + + it('should get paraId from V4 multi location', () => { + for (const t of ['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8']) { + const paraId = getParaIdFromMultiLocation({ + interior: { + type: t, + [`as${t}`]: [ + { + isParachain: true, + asParachain: { + toString: () => '10', + }, + }, + ], + }, + } as unknown as any) + expect(paraId).toBe('10') + } + }) + + it('should get paraId from >V4 X1 multi location', () => { + const paraId = getParaIdFromMultiLocation({ + interior: { + type: 'X1', + asX1: { + isParachain: true, + asParachain: { + toString: () => '10', + }, + }, + }, + } as unknown as any) + expect(paraId).toBe('10') + }) + + it('should get paraId from >V4 multi location', () => { + for (const t of ['X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8']) { + const paraId = getParaIdFromMultiLocation({ + interior: { + type: t, + [`as${t}`]: [ + { + isParachain: true, + asParachain: { + toString: () => '10', + }, + }, + ], + }, + } as unknown as any) + expect(paraId).toBe('10') + } + }) + it('should return undefined on unknown multi location', () => { + expect( + getParaIdFromMultiLocation({ + interior: { + type: 'ZZ', + asZZ: [], + }, + } as unknown as any) + ).toBeUndefined() + expect( + getParaIdFromMultiLocation({ + interior: { + type: 'Here', + }, + } as unknown as any) + ).toBeUndefined() + expect( + getParaIdFromMultiLocation({ + interior: { + type: 'Here', + }, + parents: { + toNumber: () => 10, + }, + } as unknown as any) + ).toBeUndefined() + }) + }) + describe('networkIdFromMultiLocation', () => { + it('should get a network id from multi location same consensus', () => { + const networkId = networkIdFromMultiLocation( + { + parents: { + toNumber: () => 1, + }, + interior: { + type: 'X1', + asX1: [ + { + isParachain: true, + asParachain: { + toString: () => '11', + }, + }, + ], + }, + } as unknown as any, + 'urn:ocn:polkadot:10' + ) + expect(networkId).toBe('urn:ocn:polkadot:11') + }) + it('should get a network id from V4 multi location different consensus', () => { + const networkId = networkIdFromMultiLocation( + { + parents: { + toNumber: () => 2, + }, + interior: { + type: 'X2', + asX2: [ + { + isGlobalConsensus: true, + asGlobalConsensus: { + type: 'Espartaco', + }, + }, + { + isParachain: true, + asParachain: { + toString: () => '11', + }, + }, + ], + }, + } as unknown as any, + 'urn:ocn:polkadot:10' + ) + expect(networkId).toBe('urn:ocn:espartaco:11') + }) + it('should get a network id from V3 multi location different consensus', () => { + const networkId = networkIdFromMultiLocation( + { + parents: { + toNumber: () => 2, + }, + interior: { + type: 'X1', + asX1: { + isGlobalConsensus: true, + asGlobalConsensus: { + type: 'Espartaco', + }, + }, + }, + } as unknown as any, + 'urn:ocn:polkadot:10' + ) + expect(networkId).toBe('urn:ocn:espartaco:0') + }) + }) +}) diff --git a/packages/server/src/services/agents/xcm/ops/util.ts b/packages/server/src/services/agents/xcm/ops/util.ts new file mode 100644 index 00000000..0f488d16 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/util.ts @@ -0,0 +1,417 @@ +import type { U8aFixed } from '@polkadot/types-codec' +import type { H256 } from '@polkadot/types/interfaces/runtime' +import type { + PolkadotRuntimeParachainsInclusionAggregateMessageOrigin, + StagingXcmV3MultiLocation, + XcmV2MultiLocation, + XcmV2MultiassetMultiAssets, + XcmV2MultilocationJunctions, + XcmV3Junction, + XcmV3Junctions, + XcmV3MultiassetMultiAssets, +} from '@polkadot/types/lookup' + +import { types } from '@sodazone/ocelloids-sdk' + +import { GlobalConsensus, createNetworkId, getConsensus, isGlobalConsensus } from '../../../config.js' +import { HexString, SignerData } from '../../../subscriptions/types.js' +import { NetworkURN } from '../../../types.js' +import { AssetsTrapped, TrappedAsset } from '../types.js' +import { + VersionedInteriorLocation, + XcmV4AssetAssets, + XcmV4Junction, + XcmV4Junctions, + XcmV4Location, + XcmVersionedAssets, + XcmVersionedLocation, + XcmVersionedXcm, +} from './xcm-types.js' + +const BRIDGE_HUB_NETWORK_IDS: Record = { + polkadot: 'urn:ocn:polkadot:1002', + kusama: 'urn:ocn:kusama:1002', + rococo: 'urn:ocn:rococo:1013', + westend: 'urn:ocn:westend:1002', + local: 'urn:ocn:local:1002', + wococo: 'urn:ocn:wococo:1002', + ethereum: undefined, + byfork: undefined, + bygenesis: undefined, + bitcoincore: undefined, + bitcoincash: undefined, +} + +export function getBridgeHubNetworkId(consensus: string | NetworkURN): NetworkURN | undefined { + const c = consensus.startsWith('urn:ocn:') ? getConsensus(consensus as NetworkURN) : consensus + if (isGlobalConsensus(c)) { + return BRIDGE_HUB_NETWORK_IDS[c] + } + return undefined +} + +function createSignersData(xt: types.ExtrinsicWithId): SignerData | undefined { + try { + if (xt.isSigned) { + // Signer could be Address or AccountId + const accountId = xt.signer.value ?? xt.signer + return { + signer: { + id: accountId.toPrimitive(), + publicKey: accountId.toHex(), + }, + extraSigners: xt.extraSigners.map((signer) => ({ + type: signer.type, + id: signer.address.value.toPrimitive(), + publicKey: signer.address.value.toHex(), + })), + } + } + } catch (error) { + throw new Error(`creating signers data at ${xt.extrinsicId ?? '-1'}`, { cause: error }) + } + + return undefined +} + +export function getSendersFromExtrinsic(extrinsic: types.ExtrinsicWithId): SignerData | undefined { + return createSignersData(extrinsic) +} + +export function getSendersFromEvent(event: types.BlockEvent): SignerData | undefined { + if (event.extrinsic !== undefined) { + return getSendersFromExtrinsic(event.extrinsic) + } + return undefined +} +/** + * Gets message id from setTopic. + */ +export function getMessageId(program: XcmVersionedXcm): HexString | undefined { + switch (program.type) { + // Only XCM V3+ supports topic ID + case 'V3': + case 'V4': + for (const instruction of program[`as${program.type}`]) { + if (instruction.isSetTopic) { + return instruction.asSetTopic.toHex() + } + } + return undefined + default: + return undefined + } +} + +export function getParaIdFromOrigin( + origin: PolkadotRuntimeParachainsInclusionAggregateMessageOrigin +): string | undefined { + if (origin.isUmp) { + const umpOrigin = origin.asUmp + if (umpOrigin.isPara) { + return umpOrigin.asPara.toString() + } + } + + return undefined +} + +// TODO: revisit Junction guards and conversions +// TODO: extract in multiple files + +function isX1V2Junctions(object: any): object is XcmV2MultilocationJunctions { + return ( + object.asX1 !== undefined && + typeof object.asX1[Symbol.iterator] !== 'function' && + object.asX1.isGlobalConsensus === undefined + ) +} + +const Xn = ['X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8'] +function isXnV2Junctions(object: any): object is XcmV2MultilocationJunctions { + return Xn.every((x) => { + const ax = object[`as${x}`] + return ax === undefined || (Array.isArray(ax) && ax.every((a) => a.isGlobalConsensus === undefined)) + }) +} + +function isX1V4Junctions(object: any): object is XcmV4Junctions { + return object.asX1 !== undefined && typeof object.asX1[Symbol.iterator] === 'function' +} + +function isX1V3Junctions(object: any): object is XcmV3Junctions { + return ( + object.asX1 !== undefined && + typeof object.asX1[Symbol.iterator] !== 'function' && + object.asX1.isGlobalConsensus !== undefined + ) +} + +type NetworkId = { + consensus?: string + chainId?: string +} + +function extractConsensusAndId(j: XcmV3Junction | XcmV4Junction, n: NetworkId) { + const network = j.asGlobalConsensus + if (network.type === 'Ethereum') { + n.consensus = network.type.toLowerCase() + n.chainId = network.asEthereum.chainId.toString() + } else if (network.type !== 'ByFork' && network.type !== 'ByGenesis') { + n.consensus = network.type.toLowerCase() + } +} + +function extractV3X1GlobalConsensus(junctions: XcmV3Junctions, n: NetworkId): NetworkURN | undefined { + if (junctions.asX1.isGlobalConsensus) { + extractConsensusAndId(junctions.asX1, n) + if (n.consensus !== undefined) { + return createNetworkId(n.consensus, n.chainId ?? '0') + } + } + return undefined +} + +function _networkIdFrom(junctions: XcmV3Junctions | XcmV4Junctions, networkId: NetworkId) { + if (junctions.type === 'X1' || junctions.type === 'Here') { + return undefined + } + + for (const j of junctions[`as${junctions.type}`]) { + if (j.isGlobalConsensus) { + extractConsensusAndId(j, networkId) + } + + if (j.isParachain) { + networkId.chainId = j.asParachain.toString() + } + } + + if (networkId.consensus !== undefined) { + return createNetworkId(networkId.consensus, networkId.chainId ?? '0') + } + + return undefined +} + +function networkIdFromV4(junctions: XcmV4Junctions): NetworkURN | undefined { + const networkId: NetworkId = {} + + return _networkIdFrom(junctions, networkId) +} + +function networkIdFromV3(junctions: XcmV3Junctions): NetworkURN | undefined { + if (junctions.type === 'Here') { + return undefined + } + + const networkId: NetworkId = {} + + if (junctions.type === 'X1') { + return extractV3X1GlobalConsensus(junctions, networkId) + } + + return _networkIdFrom(junctions, networkId) +} + +// eslint-disable-next-line complexity +export function getParaIdFromJunctions( + junctions: XcmV2MultilocationJunctions | XcmV3Junctions | XcmV4Junctions +): string | undefined { + if (junctions.type === 'Here') { + return undefined + } + + if (junctions.type === 'X1') { + if (isX1V3Junctions(junctions) || isX1V2Junctions(junctions)) { + return junctions.asX1.isParachain ? junctions.asX1.asParachain.toString() : undefined + } else { + for (const j of junctions[`as${junctions.type}`]) { + if (j.isParachain) { + return j.asParachain.toString() + } + } + } + return undefined + } + + for (const j of junctions[`as${junctions.type}`]) { + if (j.isParachain) { + return j.asParachain.toString() + } + } + return undefined +} + +export function getParaIdFromMultiLocation( + loc: XcmV2MultiLocation | StagingXcmV3MultiLocation | XcmV4Location +): string | undefined { + const junctions = loc.interior + if (junctions.type === 'Here') { + if (loc.parents?.toNumber() === 1) { + return '0' + } + return undefined + } + + return getParaIdFromJunctions(junctions) +} + +export function networkIdFromInteriorLocation(junctions: VersionedInteriorLocation): NetworkURN | undefined { + if (junctions.isV2) { + return undefined + } + + if (junctions.isV3) { + return networkIdFromV3(junctions.asV3) + } + + if (junctions.isV4) { + return networkIdFromV4(junctions.asV4) + } + return undefined +} + +// eslint-disable-next-line complexity +export function networkIdFromMultiLocation( + loc: XcmV2MultiLocation | StagingXcmV3MultiLocation | XcmV4Location, + currentNetworkId: NetworkURN +): NetworkURN | undefined { + const { parents, interior: junctions } = loc + + if (parents.toNumber() <= 1) { + // is within current consensus system + const paraId = getParaIdFromMultiLocation(loc) + + if (paraId !== undefined) { + return createNetworkId(currentNetworkId, paraId) + } + } else if (parents.toNumber() > 1) { + // is in other consensus system + if (junctions.type === 'X1') { + if (isX1V2Junctions(junctions)) { + return undefined + } + + if (isX1V3Junctions(junctions)) { + return networkIdFromV3(junctions) + } + + if (isX1V4Junctions(junctions)) { + return networkIdFromV4(junctions) + } + } else if (!isXnV2Junctions(junctions)) { + return _networkIdFrom(junctions, {}) + } + } + + return undefined +} + +export function networkIdFromVersionedMultiLocation( + loc: XcmVersionedLocation, + currentNetworkId: NetworkURN +): NetworkURN | undefined { + switch (loc.type) { + case 'V2': + case 'V3': + return networkIdFromMultiLocation(loc[`as${loc.type}`], currentNetworkId) + case 'V4': + return networkIdFromMultiLocation(loc.asV4, currentNetworkId) + default: + return undefined + } +} + +export function matchProgramByTopic(message: XcmVersionedXcm, topicId: U8aFixed): boolean { + switch (message.type) { + case 'V2': + throw new Error('Not able to match by topic for XCM V2 program.') + case 'V3': + case 'V4': + for (const instruction of message[`as${message.type}`]) { + if (instruction.isSetTopic) { + return instruction.asSetTopic.eq(topicId) + } + } + return false + default: + throw new Error('XCM version not supported') + } +} + +export function matchEvent(event: types.BlockEvent, section: string | string[], method: string | string[]) { + return ( + (Array.isArray(section) ? section.includes(event.section) : section === event.section) && + (Array.isArray(method) ? method.includes(event.method) : method === event.method) + ) +} + +export function matchExtrinsic(extrinsic: types.ExtrinsicWithId, section: string, method: string | string[]): boolean { + return section === extrinsic.method.section && Array.isArray(method) + ? method.includes(extrinsic.method.method) + : method === extrinsic.method.method +} + +function createTrappedAssetsFromMultiAssets( + version: number, + assets: XcmV2MultiassetMultiAssets | XcmV3MultiassetMultiAssets +): TrappedAsset[] { + return assets.map((a) => ({ + version, + id: { + type: a.id.type, + value: a.id.isConcrete ? a.id.asConcrete.toHuman() : a.id.asAbstract.toHex(), + }, + fungible: a.fun.isFungible, + amount: a.fun.isFungible ? a.fun.asFungible.toPrimitive() : 1, + assetInstance: a.fun.isNonFungible ? a.fun.asNonFungible.toHuman() : undefined, + })) +} + +function createTrappedAssetsFromAssets(version: number, assets: XcmV4AssetAssets): TrappedAsset[] { + return assets.map((a) => ({ + version, + id: { + type: 'Concrete', + value: a.id.toHuman(), + }, + fungible: a.fun.isFungible, + amount: a.fun.isFungible ? a.fun.asFungible.toPrimitive() : 1, + assetInstance: a.fun.isNonFungible ? a.fun.asNonFungible.toHuman() : undefined, + })) +} + +function mapVersionedAssets(assets: XcmVersionedAssets): TrappedAsset[] { + switch (assets.type) { + case 'V2': + case 'V3': + return createTrappedAssetsFromMultiAssets(2, assets[`as${assets.type}`]) + case 'V4': + return createTrappedAssetsFromAssets(4, assets.asV4) + default: + throw new Error('XCM version not supported') + } +} + +export function mapAssetsTrapped(assetsTrappedEvent?: types.BlockEvent): AssetsTrapped | undefined { + if (assetsTrappedEvent === undefined) { + return undefined + } + const [hash_, _, assets] = assetsTrappedEvent.data as unknown as [ + hash_: H256, + _origin: any, + assets: XcmVersionedAssets, + ] + return { + event: { + eventId: assetsTrappedEvent.eventId, + blockNumber: assetsTrappedEvent.blockNumber.toPrimitive(), + blockHash: assetsTrappedEvent.blockHash.toHex(), + section: assetsTrappedEvent.section, + method: assetsTrappedEvent.method, + }, + assets: mapVersionedAssets(assets), + hash: hash_.toHex(), + } +} diff --git a/packages/server/src/services/agents/xcm/ops/xcm-format.spec.ts b/packages/server/src/services/agents/xcm/ops/xcm-format.spec.ts new file mode 100644 index 00000000..f93e57e8 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/xcm-format.spec.ts @@ -0,0 +1,27 @@ +import { registry } from '../../../testing/xcm.js' +import { fromXcmpFormat } from './xcm-format.js' + +describe('xcm formats', () => { + it('should decode xcmp concatenated fragments', () => { + const moon4361335 = + '0002100004000000001700004b3471bb156b050a13000000001700004b3471bb156b05010300286bee0d010004000101001e08eb75720cb63fbfcbe7237c6d9b7cf6b4953518da6b38731d5bc65b9ffa32021000040000000017206d278c7e297945030a130000000017206d278c7e29794503010300286bee0d010004000101000257fd81d0a71b094c2c8d3e6c93a9b01a31a43d38408bb2c4c2b49a4c58eb01' + const buf = new Uint8Array(Buffer.from(moon4361335, 'hex')) + + const xcms = fromXcmpFormat(buf, registry) + + expect(xcms.length).toBe(2) + expect(xcms[0].hash.toHex()).toBe('0x256f9f3e5f89ced85d4253af0bd4fc6a47d069c7cd2c17723b87dda78a2e2b49') + }) + + it('should return an empty array on blobs', () => { + const buf = new Uint8Array(Buffer.from('0100', 'hex')) + + expect(fromXcmpFormat(buf, registry)).toStrictEqual([]) + }) + + it('should fail on unknown format', () => { + const buf = new Uint8Array(Buffer.from('BAD', 'hex')) + + expect(() => fromXcmpFormat(buf, registry)).toThrow(Error) + }) +}) diff --git a/packages/server/src/services/agents/xcm/ops/xcm-format.ts b/packages/server/src/services/agents/xcm/ops/xcm-format.ts new file mode 100644 index 00000000..fc13a0e9 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/xcm-format.ts @@ -0,0 +1,79 @@ +import type { Bytes } from '@polkadot/types' + +import type { Registry } from '@polkadot/types/types' +import { XcmVersionedXcm } from './xcm-types.js' + +/** + * Creates a versioned XCM program from bytes. + * + * @param data The data bytes. + * @param registry Optional - The registry to decode types. + * @returns a versioned XCM program + */ +export function asVersionedXcm(data: Bytes | Uint8Array, registry: Registry): XcmVersionedXcm { + if (registry.hasType('XcmVersionedXcm')) { + // TODO patch types because bundled types are wrong + return registry.createType('XcmVersionedXcm', data) as unknown as XcmVersionedXcm + } else if (registry.hasType('StagingXcmVersionedXcm')) { + return registry.createType('StagingXcmVersionedXcm', data) as XcmVersionedXcm + } + + throw new Error('Versioned XCM type not found in chain registry') + + // TODO:does it make sense to default to Polka Reg? + /* + return polkadotRegistry().createType( + 'XcmVersionedXcm', data + ) as XcmVersionedXcm; + */ +} + +function asXcmpVersionedXcms(buffer: Uint8Array, registry: Registry): XcmVersionedXcm[] { + const len = buffer.length + const xcms: XcmVersionedXcm[] = [] + let ptr = 1 + + while (ptr < len) { + try { + const xcm = asVersionedXcm(buffer.slice(ptr), registry) + xcms.push(xcm) + ptr += xcm.encodedLength + } catch (error) { + // TODO use logger + console.error(error) + break + } + } + + return xcms +} + +/** + * Decodes XCMP message formats. + * + * @param buf The data buffer. + * @param registry Optional - The registry to decode types. + * @returns an array of {@link VersionedXcm} programs. + */ +export function fromXcmpFormat(buf: Uint8Array, registry: Registry): XcmVersionedXcm[] { + switch (buf[0]) { + case 0x00: { + // Concatenated XCM fragments + return asXcmpVersionedXcms(buf, registry) + } + case 0x01: { + // XCM blobs + // XCM blobs not supported, ignore + break + } + case 0x02: { + // Signals + // TODO handle signals + break + } + default: { + throw new Error('Unknown XCMP format') + } + } + return [] +} diff --git a/packages/server/src/services/agents/xcm/ops/xcm-types.ts b/packages/server/src/services/agents/xcm/ops/xcm-types.ts new file mode 100644 index 00000000..200a8b85 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/xcm-types.ts @@ -0,0 +1,576 @@ +/* eslint-disable no-use-before-define */ +import type { + Bytes, + Compact, + Enum, + Option, + Struct, + U8aFixed, + Vec, + bool, + u8, + u32, + u64, + u128, +} from '@polkadot/types-codec' +import type { ITuple } from '@polkadot/types-codec/types' + +import type { + SpWeightsWeightV2Weight, + StagingXcmV3MultiLocation, + XcmDoubleEncoded, + XcmV2MultiLocation, + XcmV2MultiassetMultiAssets, + XcmV2MultilocationJunctions, + XcmV2OriginKind, + XcmV2Xcm, + XcmV3JunctionBodyId, + XcmV3JunctionBodyPart, + XcmV3Junctions, + XcmV3MaybeErrorCode, + XcmV3MultiassetMultiAssets, + XcmV3TraitsError, + XcmV3WeightLimit, + XcmV3Xcm, +} from '@polkadot/types/lookup' + +/** @name XcmVersionedXcm (296) */ +export interface XcmVersionedXcm extends Enum { + readonly isV2: boolean + readonly asV2: XcmV2Xcm + readonly isV3: boolean + readonly asV3: XcmV3Xcm + readonly isV4: boolean + readonly asV4: XcmV4Xcm + readonly type: 'V2' | 'V3' | 'V4' +} + +/** @name XcmVersionedLocation (70) */ +export interface XcmVersionedLocation extends Enum { + readonly isV2: boolean + readonly asV2: XcmV2MultiLocation + readonly isV3: boolean + readonly asV3: StagingXcmV3MultiLocation + readonly isV4: boolean + readonly asV4: XcmV4Location + readonly type: 'V2' | 'V3' | 'V4' +} + +/** @name XcmVersionedAssets (358) */ +export interface XcmVersionedAssets extends Enum { + readonly isV2: boolean + readonly asV2: XcmV2MultiassetMultiAssets + readonly isV3: boolean + readonly asV3: XcmV3MultiassetMultiAssets + readonly isV4: boolean + readonly asV4: XcmV4AssetAssets + readonly type: 'V2' | 'V3' | 'V4' +} + +/** @name XcmV4Xcm (340) */ +export interface XcmV4Xcm extends Vec {} + +/** @name XcmV4Instruction (342) */ +export interface XcmV4Instruction extends Enum { + readonly isWithdrawAsset: boolean + readonly asWithdrawAsset: XcmV4AssetAssets + readonly isReserveAssetDeposited: boolean + readonly asReserveAssetDeposited: XcmV4AssetAssets + readonly isReceiveTeleportedAsset: boolean + readonly asReceiveTeleportedAsset: XcmV4AssetAssets + readonly isQueryResponse: boolean + readonly asQueryResponse: { + readonly queryId: Compact + readonly response: XcmV4Response + readonly maxWeight: SpWeightsWeightV2Weight + readonly querier: Option + } & Struct + readonly isTransferAsset: boolean + readonly asTransferAsset: { + readonly assets: XcmV4AssetAssets + readonly beneficiary: XcmV4Location + } & Struct + readonly isTransferReserveAsset: boolean + readonly asTransferReserveAsset: { + readonly assets: XcmV4AssetAssets + readonly dest: XcmV4Location + readonly xcm: XcmV4Xcm + } & Struct + readonly isTransact: boolean + readonly asTransact: { + readonly originKind: XcmV2OriginKind + readonly requireWeightAtMost: SpWeightsWeightV2Weight + readonly call: XcmDoubleEncoded + } & Struct + readonly isHrmpNewChannelOpenRequest: boolean + readonly asHrmpNewChannelOpenRequest: { + readonly sender: Compact + readonly maxMessageSize: Compact + readonly maxCapacity: Compact + } & Struct + readonly isHrmpChannelAccepted: boolean + readonly asHrmpChannelAccepted: { + readonly recipient: Compact + } & Struct + readonly isHrmpChannelClosing: boolean + readonly asHrmpChannelClosing: { + readonly initiator: Compact + readonly sender: Compact + readonly recipient: Compact + } & Struct + readonly isClearOrigin: boolean + readonly isDescendOrigin: boolean + readonly asDescendOrigin: XcmV4Junctions + readonly isReportError: boolean + readonly asReportError: XcmV4QueryResponseInfo + readonly isDepositAsset: boolean + readonly asDepositAsset: { + readonly assets: XcmV4AssetAssetFilter + readonly beneficiary: XcmV4Location + } & Struct + readonly isDepositReserveAsset: boolean + readonly asDepositReserveAsset: { + readonly assets: XcmV4AssetAssetFilter + readonly dest: XcmV4Location + readonly xcm: XcmV4Xcm + } & Struct + readonly isExchangeAsset: boolean + readonly asExchangeAsset: { + readonly give: XcmV4AssetAssetFilter + readonly want: XcmV4AssetAssets + readonly maximal: bool + } & Struct + readonly isInitiateReserveWithdraw: boolean + readonly asInitiateReserveWithdraw: { + readonly assets: XcmV4AssetAssetFilter + readonly reserve: XcmV4Location + readonly xcm: XcmV4Xcm + } & Struct + readonly isInitiateTeleport: boolean + readonly asInitiateTeleport: { + readonly assets: XcmV4AssetAssetFilter + readonly dest: XcmV4Location + readonly xcm: XcmV4Xcm + } & Struct + readonly isReportHolding: boolean + readonly asReportHolding: { + readonly responseInfo: XcmV4QueryResponseInfo + readonly assets: XcmV4AssetAssetFilter + } & Struct + readonly isBuyExecution: boolean + readonly asBuyExecution: { + readonly fees: XcmV4Asset + readonly weightLimit: XcmV3WeightLimit + } & Struct + readonly isRefundSurplus: boolean + readonly isSetErrorHandler: boolean + readonly asSetErrorHandler: XcmV4Xcm + readonly isSetAppendix: boolean + readonly asSetAppendix: XcmV4Xcm + readonly isClearError: boolean + readonly isClaimAsset: boolean + readonly asClaimAsset: { + readonly assets: XcmV4AssetAssets + readonly ticket: XcmV4Location + } & Struct + readonly isTrap: boolean + readonly asTrap: Compact + readonly isSubscribeVersion: boolean + readonly asSubscribeVersion: { + readonly queryId: Compact + readonly maxResponseWeight: SpWeightsWeightV2Weight + } & Struct + readonly isUnsubscribeVersion: boolean + readonly isBurnAsset: boolean + readonly asBurnAsset: XcmV4AssetAssets + readonly isExpectAsset: boolean + readonly asExpectAsset: XcmV4AssetAssets + readonly isExpectOrigin: boolean + readonly asExpectOrigin: Option + readonly isExpectError: boolean + readonly asExpectError: Option> + readonly isExpectTransactStatus: boolean + readonly asExpectTransactStatus: XcmV3MaybeErrorCode + readonly isQueryPallet: boolean + readonly asQueryPallet: { + readonly moduleName: Bytes + readonly responseInfo: XcmV4QueryResponseInfo + } & Struct + readonly isExpectPallet: boolean + readonly asExpectPallet: { + readonly index: Compact + readonly name: Bytes + readonly moduleName: Bytes + readonly crateMajor: Compact + readonly minCrateMinor: Compact + } & Struct + readonly isReportTransactStatus: boolean + readonly asReportTransactStatus: XcmV4QueryResponseInfo + readonly isClearTransactStatus: boolean + readonly isUniversalOrigin: boolean + readonly asUniversalOrigin: XcmV4Junction + readonly isExportMessage: boolean + readonly asExportMessage: { + readonly network: XcmV4JunctionNetworkId + readonly destination: XcmV4Junctions + readonly xcm: XcmV4Xcm + } & Struct + readonly isLockAsset: boolean + readonly asLockAsset: { + readonly asset: XcmV4Asset + readonly unlocker: XcmV4Location + } & Struct + readonly isUnlockAsset: boolean + readonly asUnlockAsset: { + readonly asset: XcmV4Asset + readonly target: XcmV4Location + } & Struct + readonly isNoteUnlockable: boolean + readonly asNoteUnlockable: { + readonly asset: XcmV4Asset + readonly owner: XcmV4Location + } & Struct + readonly isRequestUnlock: boolean + readonly asRequestUnlock: { + readonly asset: XcmV4Asset + readonly locker: XcmV4Location + } & Struct + readonly isSetFeesMode: boolean + readonly asSetFeesMode: { + readonly jitWithdraw: bool + } & Struct + readonly isSetTopic: boolean + readonly asSetTopic: U8aFixed + readonly isClearTopic: boolean + readonly isAliasOrigin: boolean + readonly asAliasOrigin: XcmV4Location + readonly isUnpaidExecution: boolean + readonly asUnpaidExecution: { + readonly weightLimit: XcmV3WeightLimit + readonly checkOrigin: Option + } & Struct + readonly type: + | 'WithdrawAsset' + | 'ReserveAssetDeposited' + | 'ReceiveTeleportedAsset' + | 'QueryResponse' + | 'TransferAsset' + | 'TransferReserveAsset' + | 'Transact' + | 'HrmpNewChannelOpenRequest' + | 'HrmpChannelAccepted' + | 'HrmpChannelClosing' + | 'ClearOrigin' + | 'DescendOrigin' + | 'ReportError' + | 'DepositAsset' + | 'DepositReserveAsset' + | 'ExchangeAsset' + | 'InitiateReserveWithdraw' + | 'InitiateTeleport' + | 'ReportHolding' + | 'BuyExecution' + | 'RefundSurplus' + | 'SetErrorHandler' + | 'SetAppendix' + | 'ClearError' + | 'ClaimAsset' + | 'Trap' + | 'SubscribeVersion' + | 'UnsubscribeVersion' + | 'BurnAsset' + | 'ExpectAsset' + | 'ExpectOrigin' + | 'ExpectError' + | 'ExpectTransactStatus' + | 'QueryPallet' + | 'ExpectPallet' + | 'ReportTransactStatus' + | 'ClearTransactStatus' + | 'UniversalOrigin' + | 'ExportMessage' + | 'LockAsset' + | 'UnlockAsset' + | 'NoteUnlockable' + | 'RequestUnlock' + | 'SetFeesMode' + | 'SetTopic' + | 'ClearTopic' + | 'AliasOrigin' + | 'UnpaidExecution' +} + +/** @name XcmV4AssetAssets (343) */ +export interface XcmV4AssetAssets extends Vec {} + +/** @name XcmV4Asset (345) */ +interface XcmV4Asset extends Struct { + readonly id: XcmV4AssetAssetId + readonly fun: XcmV4AssetFungibility +} + +/** @name XcmV4AssetFungibility (346) */ +interface XcmV4AssetFungibility extends Enum { + readonly isFungible: boolean + readonly asFungible: Compact + readonly isNonFungible: boolean + readonly asNonFungible: XcmV4AssetAssetInstance + readonly type: 'Fungible' | 'NonFungible' +} + +/** @name XcmV4AssetAssetInstance (347) */ +interface XcmV4AssetAssetInstance extends Enum { + readonly isUndefined: boolean + readonly isIndex: boolean + readonly asIndex: Compact + readonly isArray4: boolean + readonly asArray4: U8aFixed + readonly isArray8: boolean + readonly asArray8: U8aFixed + readonly isArray16: boolean + readonly asArray16: U8aFixed + readonly isArray32: boolean + readonly asArray32: U8aFixed + readonly type: 'Undefined' | 'Index' | 'Array4' | 'Array8' | 'Array16' | 'Array32' +} + +/** @name XcmV4Response (348) */ +interface XcmV4Response extends Enum { + readonly isNull: boolean + readonly isAssets: boolean + readonly asAssets: XcmV4AssetAssets + readonly isExecutionResult: boolean + readonly asExecutionResult: Option> + readonly isVersion: boolean + readonly asVersion: u32 + readonly isPalletsInfo: boolean + readonly asPalletsInfo: Vec + readonly isDispatchResult: boolean + readonly asDispatchResult: XcmV3MaybeErrorCode + readonly type: 'Null' | 'Assets' | 'ExecutionResult' | 'Version' | 'PalletsInfo' | 'DispatchResult' +} + +/** @name XcmV4PalletInfo (350) */ +interface XcmV4PalletInfo extends Struct { + readonly index: Compact + readonly name: Bytes + readonly moduleName: Bytes + readonly major: Compact + readonly minor: Compact + readonly patch: Compact +} + +/** @name XcmV4QueryResponseInfo (354) */ +interface XcmV4QueryResponseInfo extends Struct { + readonly destination: XcmV4Location + readonly queryId: Compact + readonly maxWeight: SpWeightsWeightV2Weight +} + +/** @name XcmV4AssetAssetFilter (355) */ +interface XcmV4AssetAssetFilter extends Enum { + readonly isDefinite: boolean + readonly asDefinite: XcmV4AssetAssets + readonly isWild: boolean + readonly asWild: XcmV4AssetWildAsset + readonly type: 'Definite' | 'Wild' +} + +/** @name XcmV4AssetWildAsset (356) */ +interface XcmV4AssetWildAsset extends Enum { + readonly isAll: boolean + readonly isAllOf: boolean + readonly asAllOf: { + readonly id: XcmV4AssetAssetId + readonly fun: XcmV4AssetWildFungibility + } & Struct + readonly isAllCounted: boolean + readonly asAllCounted: Compact + readonly isAllOfCounted: boolean + readonly asAllOfCounted: { + readonly id: XcmV4AssetAssetId + readonly fun: XcmV4AssetWildFungibility + readonly count: Compact + } & Struct + readonly type: 'All' | 'AllOf' | 'AllCounted' | 'AllOfCounted' +} + +/** @name XcmV4AssetWildFungibility (357) */ +interface XcmV4AssetWildFungibility extends Enum { + readonly isFungible: boolean + readonly isNonFungible: boolean + readonly type: 'Fungible' | 'NonFungible' +} + +/** @name XcmV4Location (56) */ +export interface XcmV4Location extends Struct { + readonly parents: u8 + readonly interior: XcmV4Junctions +} + +/** @name XcmV4Junctions (57) */ +export interface XcmV4Junctions extends Enum { + readonly isHere: boolean + readonly isX1: boolean + readonly asX1: Vec + readonly isX2: boolean + readonly asX2: Vec + readonly isX3: boolean + readonly asX3: Vec + readonly isX4: boolean + readonly asX4: Vec + readonly isX5: boolean + readonly asX5: Vec + readonly isX6: boolean + readonly asX6: Vec + readonly isX7: boolean + readonly asX7: Vec + readonly isX8: boolean + readonly asX8: Vec + readonly type: 'Here' | 'X1' | 'X2' | 'X3' | 'X4' | 'X5' | 'X6' | 'X7' | 'X8' +} + +/** @name XcmV4Junction (59) */ +export interface XcmV4Junction extends Enum { + readonly isParachain: boolean + readonly asParachain: Compact + readonly isAccountId32: boolean + readonly asAccountId32: { + readonly network: Option + readonly id: U8aFixed + } & Struct + readonly isAccountIndex64: boolean + readonly asAccountIndex64: { + readonly network: Option + readonly index: Compact + } & Struct + readonly isAccountKey20: boolean + readonly asAccountKey20: { + readonly network: Option + readonly key: U8aFixed + } & Struct + readonly isPalletInstance: boolean + readonly asPalletInstance: u8 + readonly isGeneralIndex: boolean + readonly asGeneralIndex: Compact + readonly isGeneralKey: boolean + readonly asGeneralKey: { + readonly length: u8 + readonly data: U8aFixed + } & Struct + readonly isOnlyChild: boolean + readonly isPlurality: boolean + readonly asPlurality: { + readonly id: XcmV3JunctionBodyId + readonly part: XcmV3JunctionBodyPart + } & Struct + readonly isGlobalConsensus: boolean + readonly asGlobalConsensus: XcmV4JunctionNetworkId + readonly type: + | 'Parachain' + | 'AccountId32' + | 'AccountIndex64' + | 'AccountKey20' + | 'PalletInstance' + | 'GeneralIndex' + | 'GeneralKey' + | 'OnlyChild' + | 'Plurality' + | 'GlobalConsensus' +} + +/** @name XcmV4JunctionNetworkId (61) */ +interface XcmV4JunctionNetworkId extends Enum { + readonly isByGenesis: boolean + readonly asByGenesis: U8aFixed + readonly isByFork: boolean + readonly asByFork: { + readonly blockNumber: u64 + readonly blockHash: U8aFixed + } & Struct + readonly isPolkadot: boolean + readonly isKusama: boolean + readonly isWestend: boolean + readonly isRococo: boolean + readonly isWococo: boolean + readonly isEthereum: boolean + readonly asEthereum: { + readonly chainId: Compact + } & Struct + readonly isBitcoinCore: boolean + readonly isBitcoinCash: boolean + readonly isPolkadotBulletin: boolean + readonly type: + | 'ByGenesis' + | 'ByFork' + | 'Polkadot' + | 'Kusama' + | 'Westend' + | 'Rococo' + | 'Wococo' + | 'Ethereum' + | 'BitcoinCore' + | 'BitcoinCash' + | 'PolkadotBulletin' +} + +/** @name XcmV4AssetAssetId (69) */ +interface XcmV4AssetAssetId extends XcmV4Location {} + +export interface VersionedInteriorLocation extends Enum { + readonly isV2: boolean + readonly asV2: XcmV2MultilocationJunctions + readonly isV3: boolean + readonly asV3: XcmV3Junctions + readonly isV4: boolean + readonly asV4: XcmV4Junctions + readonly type: 'V2' | 'V3' | 'V4' +} + +export interface BridgeMessage extends Struct { + readonly universal_dest: VersionedInteriorLocation + readonly message: XcmVersionedXcm +} + +export type BridgeMessageAccepted = { + readonly laneId: BpMessagesLaneId + readonly nonce: u64 +} & Struct + +export type BridgeMessagesDelivered = { + readonly laneId: BpMessagesLaneId + readonly messages: BpMessagesDeliveredMessages +} & Struct + +interface BpMessagesLaneId extends U8aFixed {} + +interface BpMessagesDeliveredMessages extends Struct { + readonly begin: u64 + readonly end: u64 +} + +export interface BpMessagesReceivedMessages extends Struct { + readonly lane: BpMessagesLaneId + readonly receiveResults: Vec> +} + +interface BpMessagesReceivalResult extends Enum { + readonly isDispatched: boolean + readonly asDispatched: BpRuntimeMessagesMessageDispatchResult + readonly isInvalidNonce: boolean + readonly isTooManyUnrewardedRelayers: boolean + readonly isTooManyUnconfirmedMessages: boolean + readonly type: 'Dispatched' | 'InvalidNonce' | 'TooManyUnrewardedRelayers' | 'TooManyUnconfirmedMessages' +} + +interface BpRuntimeMessagesMessageDispatchResult extends Struct { + readonly unspentWeight: SpWeightsWeightV2Weight + readonly dispatchLevelResult: BridgeRuntimeCommonMessagesXcmExtensionXcmBlobMessageDispatchResult +} + +interface BridgeRuntimeCommonMessagesXcmExtensionXcmBlobMessageDispatchResult extends Enum { + readonly isInvalidPayload: boolean + readonly isDispatched: boolean + readonly isNotDispatched: boolean + readonly type: 'InvalidPayload' | 'Dispatched' | 'NotDispatched' +} diff --git a/packages/server/src/services/agents/xcm/ops/xcmp.spec.ts b/packages/server/src/services/agents/xcm/ops/xcmp.spec.ts new file mode 100644 index 00000000..6067fcd2 --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/xcmp.spec.ts @@ -0,0 +1,165 @@ +import { jest } from '@jest/globals' +import { extractEvents } from '@sodazone/ocelloids-sdk' + +import { registry, xcmHop, xcmpReceive, xcmpSend } from '../../../testing/xcm.js' + +import { extractXcmpReceive, extractXcmpSend } from './xcmp.js' + +describe('xcmp operator', () => { + describe('extractXcmpSend', () => { + it('should extract XCMP sent message', (done) => { + const { origin, blocks, getHrmp } = xcmpSend + + const calls = jest.fn() + + const test$ = extractXcmpSend(origin, getHrmp, registry)(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.instructions).toBeDefined() + expect(msg.messageData).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract XCMP sent on hops', (done) => { + const { origin, blocks, getHrmp } = xcmHop + + const calls = jest.fn() + + const test$ = extractXcmpSend(origin, getHrmp, registry)(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.instructions).toBeDefined() + expect(msg.messageData).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(2) + done() + }, + }) + }) + }) + + it('should extract XCMP sent message matching by public key', (done) => { + const { origin, blocks, getHrmp } = xcmpSend + + const calls = jest.fn() + + const test$ = extractXcmpSend(origin, getHrmp, registry)(blocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.instructions).toBeDefined() + expect(msg.messageData).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.recipient).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + describe('extractXcmpReceive', () => { + it('should extract XCMP receive with outcome success', (done) => { + const { successBlocks } = xcmpReceive + + const calls = jest.fn() + + const test$ = extractXcmpReceive()(successBlocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Success') + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract failed XCMP received message with error', (done) => { + const { failBlocks } = xcmpReceive + + const calls = jest.fn() + + const test$ = extractXcmpReceive()(failBlocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Fail') + expect(msg.error).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + + it('should extract assets trapped info on XCMP received message', (done) => { + const { trappedBlocks } = xcmpReceive + + const calls = jest.fn() + + const test$ = extractXcmpReceive()(trappedBlocks.pipe(extractEvents())) + + test$.subscribe({ + next: (msg) => { + expect(msg).toBeDefined() + expect(msg.blockNumber).toBeDefined() + expect(msg.blockHash).toBeDefined() + expect(msg.event).toBeDefined() + expect(msg.messageHash).toBeDefined() + expect(msg.outcome).toBeDefined() + expect(msg.outcome).toBe('Fail') + expect(msg.error).toBeDefined() + expect(msg.assetsTrapped).toBeDefined() + calls() + }, + complete: () => { + expect(calls).toHaveBeenCalledTimes(1) + done() + }, + }) + }) + }) +}) diff --git a/packages/server/src/services/agents/xcm/ops/xcmp.ts b/packages/server/src/services/agents/xcm/ops/xcmp.ts new file mode 100644 index 00000000..a19fc2fa --- /dev/null +++ b/packages/server/src/services/agents/xcm/ops/xcmp.ts @@ -0,0 +1,131 @@ +import type { Registry } from '@polkadot/types/types' +import { Observable, bufferCount, filter, map, mergeMap } from 'rxjs' + +import { filterNonNull, types } from '@sodazone/ocelloids-sdk' + +import { createNetworkId } from '../../../config.js' +import { NetworkURN } from '../../../types.js' +import { GetOutboundHrmpMessages } from '../types-augmented.js' +import { + GenericXcmInboundWithContext, + GenericXcmSentWithContext, + XcmInboundWithContext, + XcmSentWithContext, +} from '../types.js' +import { MessageQueueEventContext } from '../types.js' +import { blockEventToHuman, xcmMessagesSent } from './common.js' +import { getMessageId, mapAssetsTrapped, matchEvent } from './util.js' +import { fromXcmpFormat } from './xcm-format.js' + +const METHODS_XCMP_QUEUE = ['Success', 'Fail'] + +function findOutboundHrmpMessage( + origin: NetworkURN, + getOutboundHrmpMessages: GetOutboundHrmpMessages, + registry: Registry +) { + return (source: Observable): Observable => { + return source.pipe( + mergeMap((sentMsg): Observable => { + const { blockHash, messageHash, messageId } = sentMsg + return getOutboundHrmpMessages(blockHash).pipe( + map((messages) => { + return messages + .flatMap((msg) => { + const { data, recipient } = msg + // TODO: caching strategy + const xcms = fromXcmpFormat(data, registry) + return xcms.map( + (xcmProgram) => + new GenericXcmSentWithContext({ + ...sentMsg, + messageData: xcmProgram.toU8a(), + recipient: createNetworkId(origin, recipient.toNumber().toString()), + messageHash: xcmProgram.hash.toHex(), + instructions: { + bytes: xcmProgram.toU8a(), + json: xcmProgram.toHuman(), + }, + messageId: getMessageId(xcmProgram), + }) + ) + }) + .find((msg) => { + return messageId ? msg.messageId === messageId : msg.messageHash === messageHash + }) + }), + filterNonNull() + ) + }) + ) + } +} + +export function extractXcmpSend( + origin: NetworkURN, + getOutboundHrmpMessages: GetOutboundHrmpMessages, + registry: Registry +) { + return (source: Observable): Observable => { + return source.pipe( + filter((event) => matchEvent(event, 'xcmpQueue', 'XcmpMessageSent') || matchEvent(event, 'polkadotXcm', 'Sent')), + xcmMessagesSent(), + findOutboundHrmpMessage(origin, getOutboundHrmpMessages, registry) + ) + } +} + +export function extractXcmpReceive() { + return (source: Observable): Observable => { + return source.pipe( + bufferCount(2, 1), + // eslint-disable-next-line complexity + map(([maybeAssetTrapEvent, maybeXcmpEvent]) => { + if (maybeXcmpEvent === undefined) { + return null + } + + const assetTrapEvent = matchEvent(maybeAssetTrapEvent, ['xcmPallet', 'polkadotXcm'], 'AssetsTrapped') + ? maybeAssetTrapEvent + : undefined + const assetsTrapped = mapAssetsTrapped(assetTrapEvent) + + if (matchEvent(maybeXcmpEvent, 'xcmpQueue', METHODS_XCMP_QUEUE)) { + const xcmpQueueData = maybeXcmpEvent.data as any + + return new GenericXcmInboundWithContext({ + event: blockEventToHuman(maybeXcmpEvent), + blockHash: maybeXcmpEvent.blockHash.toHex(), + blockNumber: maybeXcmpEvent.blockNumber.toPrimitive(), + extrinsicId: maybeXcmpEvent.extrinsicId, + messageHash: xcmpQueueData.messageHash.toHex(), + messageId: xcmpQueueData.messageId?.toHex(), + outcome: maybeXcmpEvent.method === 'Success' ? 'Success' : 'Fail', + error: xcmpQueueData.error, + assetsTrapped, + }) + } else if (matchEvent(maybeXcmpEvent, 'messageQueue', 'Processed')) { + const { id, success, error } = maybeXcmpEvent.data as unknown as MessageQueueEventContext + // Received event only emits field `message_id`, + // which is actually the message hash in chains that do not yet support Topic ID. + const messageId = id.toHex() + const messageHash = messageId + + return new GenericXcmInboundWithContext({ + event: blockEventToHuman(maybeXcmpEvent), + blockHash: maybeXcmpEvent.blockHash.toHex(), + blockNumber: maybeXcmpEvent.blockNumber.toPrimitive(), + messageHash, + messageId, + outcome: success?.isTrue ? 'Success' : 'Fail', + error: error ? error.toHuman() : null, + assetsTrapped, + }) + } + + return null + }), + filterNonNull() + ) + } +} diff --git a/packages/server/src/services/agents/xcm/telemetry/events.ts b/packages/server/src/services/agents/xcm/telemetry/events.ts new file mode 100644 index 00000000..37ae2ec4 --- /dev/null +++ b/packages/server/src/services/agents/xcm/telemetry/events.ts @@ -0,0 +1,15 @@ +import { TypedEventEmitter } from '../../../types.js' +import { XcmBridge, XcmHop, XcmInbound, XcmRelayed, XcmSent, XcmTimeout } from '../types.js' + +export type TelemetryEvents = { + telemetryInbound: (message: XcmInbound) => void + telemetryOutbound: (message: XcmSent) => void + telemetryRelayed: (relayMsg: XcmRelayed) => void + telemetryMatched: (inMsg: XcmInbound, outMsg: XcmSent) => void + telemetryTimeout: (message: XcmTimeout) => void + telemetryHop: (message: XcmHop) => void + telemetryBridge: (message: XcmBridge) => void + telemetryTrapped: (inMsg: XcmInbound, outMsg: XcmSent) => void +} + +export type TelemetryXCMEventEmitter = TypedEventEmitter diff --git a/packages/server/src/services/agents/xcm/telemetry/metrics.ts b/packages/server/src/services/agents/xcm/telemetry/metrics.ts new file mode 100644 index 00000000..2414b381 --- /dev/null +++ b/packages/server/src/services/agents/xcm/telemetry/metrics.ts @@ -0,0 +1,111 @@ +import { Counter } from 'prom-client' + +import { XcmBridge, XcmHop, XcmInbound, XcmRelayed, XcmSent, XcmTimeout } from '../types.js' +import { TelemetryXCMEventEmitter } from './events.js' + +export function metrics(source: TelemetryXCMEventEmitter) { + const inCount = new Counter({ + name: 'oc_engine_in_total', + help: 'Matching engine inbound messages.', + labelNames: ['subscription', 'origin', 'outcome'], + }) + const outCount = new Counter({ + name: 'oc_engine_out_total', + help: 'Matching engine outbound messages.', + labelNames: ['subscription', 'origin', 'destination'], + }) + const matchCount = new Counter({ + name: 'oc_engine_matched_total', + help: 'Matching engine matched messages.', + labelNames: ['subscription', 'origin', 'destination', 'outcome'], + }) + const trapCount = new Counter({ + name: 'oc_engine_trapped_total', + help: 'Matching engine matched messages with trapped assets.', + labelNames: ['subscription', 'origin', 'destination', 'outcome'], + }) + const relayCount = new Counter({ + name: 'oc_engine_relayed_total', + help: 'Matching engine relayed messages.', + labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'outcome'], + }) + const timeoutCount = new Counter({ + name: 'oc_engine_timeout_total', + help: 'Matching engine sent timeout messages.', + labelNames: ['subscription', 'origin', 'destination'], + }) + const hopCount = new Counter({ + name: 'oc_engine_hop_total', + help: 'Matching engine hop messages.', + labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'stop', 'outcome', 'direction'], + }) + const bridgeCount = new Counter({ + name: 'oc_engine_bridge_total', + help: 'Matching engine bridge messages.', + labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'stop', 'outcome', 'direction'], + }) + + source.on('telemetryInbound', (message: XcmInbound) => { + inCount.labels(message.subscriptionId, message.chainId, message.outcome.toString()).inc() + }) + + source.on('telemetryOutbound', (message: XcmSent) => { + outCount.labels(message.subscriptionId, message.origin.chainId, message.destination.chainId).inc() + }) + + source.on('telemetryMatched', (inMsg: XcmInbound, outMsg: XcmSent) => { + matchCount + .labels(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.destination.chainId, inMsg.outcome.toString()) + .inc() + }) + + source.on('telemetryRelayed', (relayMsg: XcmRelayed) => { + relayCount + .labels( + relayMsg.subscriptionId, + relayMsg.origin.chainId, + relayMsg.destination.chainId, + relayMsg.waypoint.legIndex.toString(), + relayMsg.waypoint.outcome.toString() + ) + .inc() + }) + + source.on('telemetryTimeout', (msg: XcmTimeout) => { + timeoutCount.labels(msg.subscriptionId, msg.origin.chainId, msg.destination.chainId).inc() + }) + + source.on('telemetryHop', (msg: XcmHop) => { + hopCount + .labels( + msg.subscriptionId, + msg.origin.chainId, + msg.destination.chainId, + msg.waypoint.legIndex.toString(), + msg.waypoint.chainId, + msg.waypoint.outcome.toString(), + msg.direction + ) + .inc() + }) + + source.on('telemetryBridge', (msg: XcmBridge) => { + bridgeCount + .labels( + msg.subscriptionId, + msg.origin.chainId, + msg.destination.chainId, + msg.waypoint.legIndex.toString(), + msg.waypoint.chainId, + msg.waypoint.outcome.toString(), + msg.bridgeMessageType + ) + .inc() + }) + + source.on('telemetryTrapped', (inMsg: XcmInbound, outMsg: XcmSent) => { + trapCount + .labels(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.destination.chainId, inMsg.outcome.toString()) + .inc() + }) +} diff --git a/packages/server/src/services/agents/xcm/types-augmented.ts b/packages/server/src/services/agents/xcm/types-augmented.ts new file mode 100644 index 00000000..e5800623 --- /dev/null +++ b/packages/server/src/services/agents/xcm/types-augmented.ts @@ -0,0 +1,20 @@ +import { Observable } from 'rxjs' + +import type { Bytes, Vec } from '@polkadot/types' +import type { + PolkadotCorePrimitivesInboundDownwardMessage, + PolkadotCorePrimitivesOutboundHrmpMessage, +} from '@polkadot/types/lookup' +import { HexString } from '../../subscriptions/types.js' +import { NetworkURN } from '../../types.js' + +export type GetOutboundHrmpMessages = (hash: HexString) => Observable> + +export type GetOutboundUmpMessages = (hash: HexString) => Observable> + +export type GetDownwardMessageQueues = ( + hash: HexString, + networkId: NetworkURN +) => Observable> + +export type GetStorageAt = (hash: HexString, key: HexString) => Observable diff --git a/packages/server/src/services/agents/xcm/types.ts b/packages/server/src/services/agents/xcm/types.ts new file mode 100644 index 00000000..c4cacd21 --- /dev/null +++ b/packages/server/src/services/agents/xcm/types.ts @@ -0,0 +1,797 @@ +import type { U8aFixed, bool } from '@polkadot/types-codec' +import type { + FrameSupportMessagesProcessMessageError, + PolkadotRuntimeParachainsInclusionAggregateMessageOrigin, +} from '@polkadot/types/lookup' +import { ControlQuery } from '@sodazone/ocelloids-sdk' +import { z } from 'zod' + +import { createNetworkId } from '../../config.js' +import { + AnyJson, + HexString, + RxSubscriptionWithId, + SignerData, + Subscription, + toHexString, +} from '../../subscriptions/types.js' +import { NetworkURN } from '../../types.js' + +export type Monitor = { + subs: RxSubscriptionWithId[] + controls: Record +} + +function distinct(a: Array) { + return Array.from(new Set(a)) +} + +export type XCMSubscriptionHandler = { + originSubs: RxSubscriptionWithId[] + destinationSubs: RxSubscriptionWithId[] + bridgeSubs: BridgeSubscription[] + sendersControl: ControlQuery + messageControl: ControlQuery + descriptor: Subscription + args: XCMSubscriptionArgs + relaySub?: RxSubscriptionWithId +} + +const bridgeTypes = ['pk-bridge', 'snowbridge'] as const + +export type BridgeType = (typeof bridgeTypes)[number] + +export type BridgeSubscription = { type: BridgeType; subs: RxSubscriptionWithId[] } + +export type XcmCriteria = { + sendersControl: ControlQuery + messageControl: ControlQuery +} + +export type XcmWithContext = { + event?: AnyJson + extrinsicId?: string + blockNumber: string | number + blockHash: HexString + messageHash: HexString + messageId?: HexString +} +/** + * Represents the asset that has been trapped. + * + * @public + */ + +export type TrappedAsset = { + version: number + id: { + type: string + value: AnyJson + } + fungible: boolean + amount: string | number + assetInstance?: AnyJson +} +/** + * Event emitted when assets are trapped. + * + * @public + */ + +export type AssetsTrapped = { + assets: TrappedAsset[] + hash: HexString + event: AnyJson +} +/** + * Represents an XCM program bytes and human JSON. + */ + +export type XcmProgram = { + bytes: Uint8Array + json: AnyJson +} + +export interface XcmSentWithContext extends XcmWithContext { + messageData: Uint8Array + recipient: NetworkURN + sender?: SignerData + instructions: XcmProgram +} + +export interface XcmBridgeAcceptedWithContext extends XcmWithContext { + chainId: NetworkURN + bridgeKey: HexString + messageData: HexString + instructions: AnyJson + recipient: NetworkURN + forwardId?: HexString +} + +export interface XcmBridgeDeliveredWithContext { + chainId: NetworkURN + bridgeKey: HexString + event?: AnyJson + extrinsicId?: string + blockNumber: string | number + blockHash: HexString + sender?: SignerData +} + +export interface XcmBridgeAcceptedWithContext extends XcmWithContext { + chainId: NetworkURN + bridgeKey: HexString + messageData: HexString + instructions: AnyJson + recipient: NetworkURN + forwardId?: HexString +} + +export interface XcmBridgeDeliveredWithContext { + chainId: NetworkURN + bridgeKey: HexString + event?: AnyJson + extrinsicId?: string + blockNumber: string | number + blockHash: HexString + sender?: SignerData +} + +export interface XcmBridgeInboundWithContext { + chainId: NetworkURN + bridgeKey: HexString + blockNumber: string | number + blockHash: HexString + outcome: 'Success' | 'Fail' + error: AnyJson + event?: AnyJson + extrinsicId?: string +} + +export interface XcmBridgeInboundWithContext { + chainId: NetworkURN + bridgeKey: HexString + blockNumber: string | number + blockHash: HexString + outcome: 'Success' | 'Fail' + error: AnyJson + event?: AnyJson + extrinsicId?: string +} + +export interface XcmInboundWithContext extends XcmWithContext { + outcome: 'Success' | 'Fail' + error: AnyJson + assetsTrapped?: AssetsTrapped +} + +export interface XcmRelayedWithContext extends XcmInboundWithContext { + recipient: NetworkURN + origin: NetworkURN +} + +export class GenericXcmRelayedWithContext implements XcmRelayedWithContext { + event: AnyJson + extrinsicId?: string + blockNumber: string | number + blockHash: HexString + messageHash: HexString + messageId?: HexString + recipient: NetworkURN + origin: NetworkURN + outcome: 'Success' | 'Fail' + error: AnyJson + + constructor(msg: XcmRelayedWithContext) { + this.event = msg.event + this.messageHash = msg.messageHash + this.messageId = msg.messageId ?? msg.messageHash + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.recipient = msg.recipient + this.origin = msg.origin + this.outcome = msg.outcome + this.error = msg.error + } + + toHuman(_isExpanded?: boolean | undefined): Record { + return { + messageHash: this.messageHash, + messageId: this.messageId, + extrinsicId: this.extrinsicId, + blockHash: this.blockHash, + blockNumber: this.blockNumber, + event: this.event, + recipient: this.recipient, + origin: this.origin, + outcome: this.outcome, + error: this.error, + } + } +} + +export class GenericXcmInboundWithContext implements XcmInboundWithContext { + event: AnyJson + extrinsicId?: string | undefined + blockNumber: string + blockHash: HexString + messageHash: HexString + messageId: HexString + outcome: 'Success' | 'Fail' + error: AnyJson + assetsTrapped?: AssetsTrapped | undefined + + constructor(msg: XcmInboundWithContext) { + this.event = msg.event + this.messageHash = msg.messageHash + this.messageId = msg.messageId ?? msg.messageHash + this.outcome = msg.outcome + this.error = msg.error + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.assetsTrapped = msg.assetsTrapped + } + + toHuman(_isExpanded?: boolean | undefined): Record { + return { + messageHash: this.messageHash, + messageId: this.messageId, + extrinsicId: this.extrinsicId, + blockHash: this.blockHash, + blockNumber: this.blockNumber, + event: this.event, + outcome: this.outcome, + error: this.error, + assetsTrapped: this.assetsTrapped, + } + } +} + +export class XcmInbound { + subscriptionId: string + chainId: NetworkURN + event: AnyJson + messageHash: HexString + messageId: HexString + outcome: 'Success' | 'Fail' + error: AnyJson + blockHash: HexString + blockNumber: string + extrinsicId?: string + assetsTrapped?: AssetsTrapped + + constructor(subscriptionId: string, chainId: NetworkURN, msg: XcmInboundWithContext) { + this.subscriptionId = subscriptionId + this.chainId = chainId + this.event = msg.event + this.messageHash = msg.messageHash + this.messageId = msg.messageId ?? msg.messageHash + this.outcome = msg.outcome + this.error = msg.error + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.assetsTrapped = msg.assetsTrapped + } +} + +export class GenericXcmSentWithContext implements XcmSentWithContext { + messageData: Uint8Array + recipient: NetworkURN + instructions: XcmProgram + messageHash: HexString + event: AnyJson + blockHash: HexString + blockNumber: string + sender?: SignerData + extrinsicId?: string + messageId?: HexString + + constructor(msg: XcmSentWithContext) { + this.event = msg.event + this.messageData = msg.messageData + this.recipient = msg.recipient + this.instructions = msg.instructions + this.messageHash = msg.messageHash + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.messageId = msg.messageId + this.sender = msg.sender + } + + toHuman(_isExpanded?: boolean | undefined): Record { + return { + messageData: toHexString(this.messageData), + recipient: this.recipient, + instructions: this.instructions.json, + messageHash: this.messageHash, + event: this.event, + blockHash: this.blockHash, + blockNumber: this.blockNumber, + extrinsicId: this.extrinsicId, + messageId: this.messageId, + senders: this.sender, + } + } +} + +export class GenericXcmBridgeAcceptedWithContext implements XcmBridgeAcceptedWithContext { + chainId: NetworkURN + bridgeKey: HexString + messageData: HexString + recipient: NetworkURN + instructions: AnyJson + messageHash: HexString + event: AnyJson + blockHash: HexString + blockNumber: string + extrinsicId?: string + messageId?: HexString + forwardId?: HexString + + constructor(msg: XcmBridgeAcceptedWithContext) { + this.chainId = msg.chainId + this.bridgeKey = msg.bridgeKey + this.event = msg.event + this.messageData = msg.messageData + this.recipient = msg.recipient + this.instructions = msg.instructions + this.messageHash = msg.messageHash + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.messageId = msg.messageId + this.forwardId = msg.forwardId + } +} + +export class GenericXcmBridgeDeliveredWithContext implements XcmBridgeDeliveredWithContext { + chainId: NetworkURN + bridgeKey: HexString + event?: AnyJson + extrinsicId?: string + blockNumber: string + blockHash: HexString + sender?: SignerData + + constructor(msg: XcmBridgeDeliveredWithContext) { + this.chainId = msg.chainId + this.bridgeKey = msg.bridgeKey + this.event = msg.event + this.extrinsicId = msg.extrinsicId + this.blockNumber = msg.blockNumber.toString() + this.blockHash = msg.blockHash + this.sender = msg.sender + } +} + +export class GenericXcmBridgeInboundWithContext implements XcmBridgeInboundWithContext { + chainId: NetworkURN + bridgeKey: HexString + event: AnyJson + extrinsicId?: string | undefined + blockNumber: string + blockHash: HexString + outcome: 'Success' | 'Fail' + error: AnyJson + + constructor(msg: XcmBridgeInboundWithContext) { + this.chainId = msg.chainId + this.event = msg.event + this.outcome = msg.outcome + this.error = msg.error + this.blockHash = msg.blockHash + this.blockNumber = msg.blockNumber.toString() + this.extrinsicId = msg.extrinsicId + this.bridgeKey = msg.bridgeKey + } +} + +export enum XcmNotificationType { + Sent = 'xcm.sent', + Received = 'xcm.received', + Relayed = 'xcm.relayed', + Timeout = 'xcm.timeout', + Hop = 'xcm.hop', + Bridge = 'xcm.bridge', +} + +/** + * The terminal point of an XCM journey. + * + * @public + */ + +export type XcmTerminus = { + chainId: NetworkURN +} +/** + * The terminal point of an XCM journey with contextual information. + * + * @public + */ + +export interface XcmTerminusContext extends XcmTerminus { + blockNumber: string + blockHash: HexString + extrinsicId?: string + event: AnyJson + outcome: 'Success' | 'Fail' + error: AnyJson + messageHash: HexString + messageData: string + instructions: AnyJson +} +/** + * The contextual information of an XCM journey waypoint. + * + * @public + */ + +export interface XcmWaypointContext extends XcmTerminusContext { + legIndex: number + assetsTrapped?: AnyJson +} +/** + * Type of an XCM journey leg. + * + * @public + */ + +export const legType = ['bridge', 'hop', 'hrmp', 'vmp'] as const +/** + * A leg of an XCM journey. + * + * @public + */ + +export type Leg = { + from: NetworkURN + to: NetworkURN + relay?: NetworkURN + type: (typeof legType)[number] +} +/** + * Event emitted when an XCM is sent. + * + * @public + */ + +export interface XcmSent { + type: XcmNotificationType + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString +} + +export class GenericXcmSent implements XcmSent { + type: XcmNotificationType = XcmNotificationType.Sent + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor( + subscriptionId: string, + chainId: NetworkURN, + msg: XcmSentWithContext, + legs: Leg[], + forwardId?: HexString + ) { + this.subscriptionId = subscriptionId + this.legs = legs + this.origin = { + chainId, + blockHash: msg.blockHash, + blockNumber: msg.blockNumber.toString(), + extrinsicId: msg.extrinsicId, + event: msg.event, + outcome: 'Success', + error: null, + messageData: toHexString(msg.messageData), + instructions: msg.instructions.json, + messageHash: msg.messageHash, + } + this.destination = { + chainId: legs[legs.length - 1].to, // last stop is the destination + } + this.waypoint = { + ...this.origin, + legIndex: 0, + messageData: toHexString(msg.messageData), + instructions: msg.instructions.json, + messageHash: msg.messageHash, + } + + this.messageId = msg.messageId + this.forwardId = forwardId + this.sender = msg.sender + } +} +/** + * Event emitted when an XCM is received. + * + * @public + */ + +export interface XcmReceived { + type: XcmNotificationType + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminusContext + sender?: SignerData + messageId?: HexString + forwardId?: HexString +} +/** + * Event emitted when an XCM is not received within a specified timeframe. + * + * @public + */ + +export type XcmTimeout = XcmSent + +export class GenericXcmTimeout implements XcmTimeout { + type: XcmNotificationType = XcmNotificationType.Timeout + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor(msg: XcmSent) { + this.subscriptionId = msg.subscriptionId + this.legs = msg.legs + this.origin = msg.origin + this.destination = msg.destination + this.waypoint = msg.waypoint + this.messageId = msg.messageId + this.sender = msg.sender + this.forwardId = msg.forwardId + } +} + +export class GenericXcmReceived implements XcmReceived { + type: XcmNotificationType = XcmNotificationType.Received + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminusContext + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor(outMsg: XcmSent, inMsg: XcmInbound) { + this.subscriptionId = outMsg.subscriptionId + this.legs = outMsg.legs + this.destination = { + chainId: inMsg.chainId, + blockNumber: inMsg.blockNumber, + blockHash: inMsg.blockHash, + extrinsicId: inMsg.extrinsicId, + event: inMsg.event, + outcome: inMsg.outcome, + error: inMsg.error, + instructions: outMsg.waypoint.instructions, + messageData: outMsg.waypoint.messageData, + messageHash: outMsg.waypoint.messageHash, + } + this.origin = outMsg.origin + this.waypoint = { + ...this.destination, + legIndex: this.legs.findIndex((l) => l.to === inMsg.chainId && l.type !== 'bridge'), + instructions: outMsg.waypoint.instructions, + messageData: outMsg.waypoint.messageData, + messageHash: outMsg.waypoint.messageHash, + assetsTrapped: inMsg.assetsTrapped, + } + this.sender = outMsg.sender + this.messageId = outMsg.messageId + this.forwardId = outMsg.forwardId + } +} +/** + * Event emitted when an XCM is received on the relay chain + * for an HRMP message. + * + * @public + */ + +export type XcmRelayed = XcmSent + +export class GenericXcmRelayed implements XcmRelayed { + type: XcmNotificationType = XcmNotificationType.Relayed + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor(outMsg: XcmSent, relayMsg: XcmRelayedWithContext) { + this.subscriptionId = outMsg.subscriptionId + this.legs = outMsg.legs + this.destination = outMsg.destination + this.origin = outMsg.origin + this.waypoint = { + legIndex: outMsg.legs.findIndex((l) => l.from === relayMsg.origin && l.relay !== undefined), + chainId: createNetworkId(relayMsg.origin, '0'), // relay waypoint always at relay chain + blockNumber: relayMsg.blockNumber.toString(), + blockHash: relayMsg.blockHash, + extrinsicId: relayMsg.extrinsicId, + event: relayMsg.event, + outcome: relayMsg.outcome, + error: relayMsg.error, + instructions: outMsg.waypoint.instructions, + messageData: outMsg.waypoint.messageData, + messageHash: outMsg.waypoint.messageHash, + } + this.sender = outMsg.sender + this.messageId = outMsg.messageId + this.forwardId = outMsg.forwardId + } +} +/** + * Event emitted when an XCM is sent or received on an intermediate stop. + * + * @public + */ + +export interface XcmHop extends XcmSent { + direction: 'out' | 'in' +} + +export class GenericXcmHop implements XcmHop { + type: XcmNotificationType = XcmNotificationType.Hop + direction: 'out' | 'in' + subscriptionId: string + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor(originMsg: XcmSent, hopWaypoint: XcmWaypointContext, direction: 'out' | 'in') { + this.subscriptionId = originMsg.subscriptionId + this.legs = originMsg.legs + this.origin = originMsg.origin + this.destination = originMsg.destination + this.waypoint = hopWaypoint + this.messageId = originMsg.messageId + this.sender = originMsg.sender + this.direction = direction + this.forwardId = originMsg.forwardId + } +} + +export type BridgeMessageType = 'accepted' | 'delivered' | 'received' +/** + * Event emitted when an XCM is sent or received on an intermediate stop. + * + * @public + */ + +export interface XcmBridge extends XcmSent { + bridgeKey: HexString + bridgeMessageType: BridgeMessageType +} +type XcmBridgeContext = { + bridgeMessageType: BridgeMessageType + bridgeKey: HexString + forwardId?: HexString +} + +export class GenericXcmBridge implements XcmBridge { + type: XcmNotificationType = XcmNotificationType.Bridge + bridgeMessageType: BridgeMessageType + subscriptionId: string + bridgeKey: HexString + legs: Leg[] + waypoint: XcmWaypointContext + origin: XcmTerminusContext + destination: XcmTerminus + sender?: SignerData + messageId?: HexString + forwardId?: HexString + + constructor( + originMsg: XcmSent, + waypoint: XcmWaypointContext, + { bridgeKey, bridgeMessageType, forwardId }: XcmBridgeContext + ) { + this.subscriptionId = originMsg.subscriptionId + this.bridgeMessageType = bridgeMessageType + this.legs = originMsg.legs + this.origin = originMsg.origin + this.destination = originMsg.destination + this.waypoint = waypoint + this.messageId = originMsg.messageId + this.sender = originMsg.sender + this.bridgeKey = bridgeKey + this.forwardId = forwardId + } +} + +/** + * The XCM event types. + * + * @public + */ +export type XcmNotifyMessage = XcmSent | XcmReceived | XcmRelayed | XcmHop | XcmBridge + +export function isXcmSent(object: any): object is XcmSent { + return object.type !== undefined && object.type === XcmNotificationType.Sent +} + +export function isXcmReceived(object: any): object is XcmReceived { + return object.type !== undefined && object.type === XcmNotificationType.Received +} + +export function isXcmHop(object: any): object is XcmHop { + return object.type !== undefined && object.type === XcmNotificationType.Hop +} + +export function isXcmRelayed(object: any): object is XcmRelayed { + return object.type !== undefined && object.type === XcmNotificationType.Relayed +} + +const XCM_NOTIFICATION_TYPE_ERROR = `at least 1 event type is required [${Object.values(XcmNotificationType).join( + ',' +)}]` + +const XCM_OUTBOUND_TTL_TYPE_ERROR = 'XCM outbound message TTL should be at least 6 seconds' + +export const $XCMSubscriptionArgs = z.object({ + origin: z + .string({ + required_error: 'origin id is required', + }) + .min(1), + senders: z.optional( + z.literal('*').or(z.array(z.string()).min(1, 'at least 1 sender address is required').transform(distinct)) + ), + destinations: z + .array( + z + .string({ + required_error: 'destination id is required', + }) + .min(1) + ) + .transform(distinct), + bridges: z.optional(z.array(z.enum(bridgeTypes)).min(1, 'Please specify at least one bridge.')), + // prevent using $refs + events: z.optional(z.literal('*').or(z.array(z.nativeEnum(XcmNotificationType)).min(1, XCM_NOTIFICATION_TYPE_ERROR))), + outboundTTL: z.optional(z.number().min(6000, XCM_OUTBOUND_TTL_TYPE_ERROR).max(Number.MAX_SAFE_INTEGER)), +}) + +export type XCMSubscriptionArgs = z.infer + +export type MessageQueueEventContext = { + id: U8aFixed + origin: PolkadotRuntimeParachainsInclusionAggregateMessageOrigin + success?: bool + error?: FrameSupportMessagesProcessMessageError +} diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts new file mode 100644 index 00000000..e8e7bff1 --- /dev/null +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -0,0 +1,914 @@ +import { Registry } from '@polkadot/types-codec/types' +import { ControlQuery, extractEvents, extractTxWithEvents, flattenCalls, types } from '@sodazone/ocelloids-sdk' +import { Observable, filter, from, map, share, switchMap } from 'rxjs' + +import { + $Subscription, + AgentId, + AnyJson, + HexString, + RxSubscriptionWithId, + Subscription, +} from '../../subscriptions/types.js' +import { Logger, NetworkURN } from '../../types.js' +import { extractXcmpReceive, extractXcmpSend } from './ops/xcmp.js' +import { + $XCMSubscriptionArgs, + BridgeSubscription, + BridgeType, + Monitor, + XCMSubscriptionArgs, + XCMSubscriptionHandler, + XcmBridgeAcceptedWithContext, + XcmBridgeDeliveredWithContext, + XcmBridgeInboundWithContext, + XcmInbound, + XcmInboundWithContext, + XcmNotificationType, + XcmNotifyMessage, + XcmRelayedWithContext, + XcmSentWithContext, +} from './types.js' + +import { SubsStore } from '../../persistence/subs.js' +import { MatchingEngine } from './matching.js' + +import { ValidationError, errorMessage } from '../../../errors.js' +import { IngressConsumer } from '../../ingress/index.js' +import { mapXcmSent } from './ops/common.js' +import { matchMessage, matchSenders, messageCriteria, sendersCriteria } from './ops/criteria.js' +import { extractDmpReceive, extractDmpSend, extractDmpSendByEvent } from './ops/dmp.js' +import { extractRelayReceive } from './ops/relay.js' +import { extractUmpReceive, extractUmpSend } from './ops/ump.js' + +import { Operation, applyPatch } from 'rfc6902' +import { z } from 'zod' +import { getChainId, getConsensus } from '../../config.js' +import { NotifierHub } from '../../notification/index.js' +import { + dmpDownwardMessageQueuesKey, + parachainSystemHrmpOutboundMessages, + parachainSystemUpwardMessages, +} from '../../subscriptions/storage.js' +import { Agent, AgentRuntimeContext } from '../types.js' +import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './ops/pk-bridge.js' +import { getBridgeHubNetworkId } from './ops/util.js' +import { + GetDownwardMessageQueues, + GetOutboundHrmpMessages, + GetOutboundUmpMessages, + GetStorageAt, +} from './types-augmented.js' + +const SUB_ERROR_RETRY_MS = 5000 + +const allowedPaths = ['/senders', '/destinations', '/channels', '/events'] + +function hasOp(patch: Operation[], path: string) { + return patch.some((op) => op.path.startsWith(path)) +} + +export class XCMAgent implements Agent { + readonly #subs: Record = {} + readonly #log: Logger + readonly #engine: MatchingEngine + readonly #timeouts: NodeJS.Timeout[] = [] + readonly #db: SubsStore + readonly #ingress: IngressConsumer + readonly #notifier: NotifierHub + + #shared: { + blockEvents: Record> + blockExtrinsics: Record> + } + + constructor(ctx: AgentRuntimeContext) { + const { log, ingressConsumer, notifier, subsStore } = ctx + + this.#log = log + this.#ingress = ingressConsumer + this.#notifier = notifier + this.#db = subsStore + this.#engine = new MatchingEngine(ctx, this.#onXcmWaypointReached.bind(this)) + + this.#shared = { + blockEvents: {}, + blockExtrinsics: {}, + } + } + + async getAllSubscriptions(): Promise { + return await this.#db.getByAgentId(this.id) + } + + async getSubscriptionById(subscriptionId: string): Promise { + return await this.#db.getById(this.id, subscriptionId) + } + + async update(subscriptionId: string, patch: Operation[]): Promise { + const sub = this.#subs[subscriptionId] + const descriptor = sub.descriptor + + // Check allowed patch ops + const allowedOps = patch.every((op) => allowedPaths.some((s) => op.path.startsWith(s))) + + if (allowedOps) { + applyPatch(descriptor, patch) + $Subscription.parse(descriptor) + const args = $XCMSubscriptionArgs.parse(descriptor.args) + + await this.#db.save(descriptor) + + sub.args = args + sub.descriptor = descriptor + + if (hasOp(patch, '/senders')) { + this.#updateSenders(subscriptionId) + } + + if (hasOp(patch, '/destinations')) { + this.#updateDestinations(subscriptionId) + } + + if (hasOp(patch, '/events')) { + this.#updateEvents(subscriptionId) + } + + return descriptor + } else { + throw Error('Only operations on these paths are allowed: ' + allowedPaths.join(',')) + } + } + + getInputSchema(): z.ZodSchema { + return $XCMSubscriptionArgs + } + + get id(): AgentId { + return 'xcm' + } + + getSubscriptionHandler(id: string): Subscription { + if (this.#subs[id]) { + return this.#subs[id].descriptor + } else { + throw Error('subscription handler not found') + } + } + + async subscribe(s: Subscription): Promise { + const args = $XCMSubscriptionArgs.parse(s.args) + + const origin = args.origin as NetworkURN + const dests = args.destinations as NetworkURN[] + this.#validateChainIds([origin, ...dests]) + + if (!s.ephemeral) { + await this.#db.insert(s) + } + + this.#monitor(s, args) + } + + async unsubscribe(id: string): Promise { + if (this.#subs[id] === undefined) { + this.#log.warn('unsubscribe from a non-existent subscription %s', id) + return + } + + try { + const { + descriptor: { ephemeral }, + args: { origin }, + originSubs, + destinationSubs, + relaySub, + } = this.#subs[id] + + this.#log.info('[%s] unsubscribe %s', origin, id) + + originSubs.forEach(({ sub }) => sub.unsubscribe()) + destinationSubs.forEach(({ sub }) => sub.unsubscribe()) + if (relaySub) { + relaySub.sub.unsubscribe() + } + delete this.#subs[id] + + await this.#engine.clearPendingStates(id) + + if (!ephemeral) { + await this.#db.remove(this.id, id) + } + } catch (error) { + this.#log.error(error, 'Error unsubscribing %s', id) + } + } + async stop(): Promise { + for (const { + descriptor: { id }, + originSubs, + destinationSubs, + relaySub, + } of Object.values(this.#subs)) { + this.#log.info('Unsubscribe %s', id) + + originSubs.forEach(({ sub }) => sub.unsubscribe()) + destinationSubs.forEach(({ sub }) => sub.unsubscribe()) + if (relaySub) { + relaySub.sub.unsubscribe() + } + } + + for (const t of this.#timeouts) { + t.unref() + } + + await this.#engine.stop() + } + + async start(): Promise { + await this.#startNetworkMonitors() + } + + #onXcmWaypointReached(payload: XcmNotifyMessage) { + const { subscriptionId } = payload + if (this.#subs[subscriptionId]) { + const { descriptor, args, sendersControl } = this.#subs[subscriptionId] + if ( + (args.events === undefined || args.events === '*' || args.events.includes(payload.type)) && + matchSenders(sendersControl, payload.sender) + ) { + this.#notifier.notify(descriptor, { + metadata: { + type: payload.type, + subscriptionId, + agentId: this.id, + }, + payload: payload as unknown as AnyJson, + }) + } + } else { + // this could happen with closed ephemeral subscriptions + this.#log.warn('Unable to find descriptor for subscription %s', subscriptionId) + } + } + + /** + * Main monitoring logic. + * + * This method sets up and manages subscriptions for XCM messages based on the provided + * subscription information. It creates subscriptions for both the origin and destination + * networks, monitors XCM message transfers, and emits events accordingly. + * + * @param {Subscription} descriptor - The subscription descriptor. + * @param {XCMSubscriptionArgs} args - The coerced subscription arguments. + * @throws {Error} If there is an error during the subscription setup process. + * @private + */ + #monitor(descriptor: Subscription, args: XCMSubscriptionArgs) { + const { id } = descriptor + + let origMonitor: Monitor = { subs: [], controls: {} } + let destMonitor: Monitor = { subs: [], controls: {} } + const bridgeSubs: BridgeSubscription[] = [] + let relaySub: RxSubscriptionWithId | undefined + + try { + origMonitor = this.#monitorOrigins(descriptor, args) + destMonitor = this.#monitorDestinations(descriptor, args) + } catch (error) { + // Clean up origin subscriptions. + origMonitor.subs.forEach(({ sub }) => { + sub.unsubscribe() + }) + throw error + } + + // Only subscribe to relay events if required by subscription. + // Contained in its own try-catch so it doesn't prevent origin-destination subs in case of error. + if (this.#shouldMonitorRelay(args)) { + try { + relaySub = this.#monitorRelay(descriptor, args) + } catch (error) { + // log instead of throw to not block OD subscriptions + this.#log.error(error, 'Error on relay subscription (%s)', id) + } + } + + if (args.bridges !== undefined) { + if (args.bridges.includes('pk-bridge')) { + try { + bridgeSubs.push(this.#monitorPkBridge(descriptor, args)) + } catch (error) { + // log instead of throw to not block OD subscriptions + this.#log.error(error, 'Error on bridge subscription (%s)', id) + } + } + } + + const { sendersControl, messageControl } = origMonitor.controls + + this.#subs[id] = { + descriptor, + args, + sendersControl, + messageControl, + originSubs: origMonitor.subs, + destinationSubs: destMonitor.subs, + bridgeSubs, + relaySub, + } + } + + /** + * Set up inbound monitors for XCM protocols. + * + * @private + */ + #monitorDestinations({ id }: Subscription, { origin, destinations }: XCMSubscriptionArgs): Monitor { + const subs: RxSubscriptionWithId[] = [] + const originId = origin as NetworkURN + try { + for (const dest of destinations as NetworkURN[]) { + const chainId = dest + if (this.#subs[id]?.destinationSubs.find((s) => s.chainId === chainId)) { + // Skip existing subscriptions + // for the same destination chain + continue + } + + const inboundObserver = { + error: (error: any) => { + this.#log.error(error, '[%s] error on destination subscription %s', chainId, id) + + /*this.emit('telemetrySubscriptionError', { + subscriptionId: id, + chainId, + direction: 'in', + })*/ + + // try recover inbound subscription + if (this.#subs[id]) { + const { destinationSubs } = this.#subs[id] + const index = destinationSubs.findIndex((s) => s.chainId === chainId) + if (index > -1) { + destinationSubs.splice(index, 1) + this.#timeouts.push( + setTimeout(() => { + this.#log.info( + '[%s] UPDATE destination subscription %s due error %s', + chainId, + id, + errorMessage(error) + ) + const updated = this.#updateDestinationSubscriptions(id) + this.#subs[id].destinationSubs = updated + }, SUB_ERROR_RETRY_MS) + ) + } + } + }, + } + + if (this.#ingress.isRelay(dest)) { + // VMP UMP + this.#log.info('[%s] subscribe inbound UMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#sharedBlockEvents(chainId) + .pipe(extractUmpReceive(originId), this.#emitInbound(id, chainId)) + .subscribe(inboundObserver), + }) + } else if (this.#ingress.isRelay(originId)) { + // VMP DMP + this.#log.info('[%s] subscribe inbound DMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#sharedBlockEvents(chainId) + .pipe(extractDmpReceive(), this.#emitInbound(id, chainId)) + .subscribe(inboundObserver), + }) + } else { + // Inbound HRMP / XCMP transport + this.#log.info('[%s] subscribe inbound HRMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#sharedBlockEvents(chainId) + .pipe(extractXcmpReceive(), this.#emitInbound(id, chainId)) + .subscribe(inboundObserver), + }) + } + } + } catch (error) { + // Clean up subscriptions. + subs.forEach(({ sub }) => { + sub.unsubscribe() + }) + throw error + } + + return { subs, controls: {} } + } + + /** + * Set up outbound monitors for XCM protocols. + * + * @private + */ + #monitorOrigins({ id }: Subscription, { origin, senders, destinations }: XCMSubscriptionArgs): Monitor { + const subs: RxSubscriptionWithId[] = [] + const chainId = origin as NetworkURN + + if (this.#subs[id]?.originSubs.find((s) => s.chainId === chainId)) { + throw new Error(`Fatal: duplicated origin monitor ${id} for chain ${chainId}`) + } + + const sendersControl = ControlQuery.from(sendersCriteria(senders)) + const messageControl = ControlQuery.from(messageCriteria(destinations as NetworkURN[])) + + const outboundObserver = { + error: (error: any) => { + this.#log.error(error, '[%s] error on origin subscription %s', chainId, id) + /* + this.emit('telemetrySubscriptionError', { + subscriptionId: id, + chainId, + direction: 'out', + })*/ + + // try recover outbound subscription + // note: there is a single origin per outbound + if (this.#subs[id]) { + const { originSubs, descriptor, args } = this.#subs[id] + const index = originSubs.findIndex((s) => s.chainId === chainId) + if (index > -1) { + this.#subs[id].originSubs = [] + this.#timeouts.push( + setTimeout(() => { + if (this.#subs[id]) { + this.#log.info('[%s] UPDATE origin subscription %s due error %s', chainId, id, errorMessage(error)) + const { subs: updated, controls } = this.#monitorOrigins(descriptor, args) + this.#subs[id].sendersControl = controls.sendersControl + this.#subs[id].messageControl = controls.messageControl + this.#subs[id].originSubs = updated + } + }, SUB_ERROR_RETRY_MS) + ) + } + } + }, + } + + try { + if (this.#ingress.isRelay(chainId)) { + // VMP DMP + this.#log.info('[%s] subscribe outbound DMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#ingress + .getRegistry(chainId) + .pipe( + switchMap((registry) => + this.#sharedBlockExtrinsics(chainId).pipe( + extractDmpSend(chainId, this.#getDmp(chainId, registry), registry), + this.#emitOutbound(id, chainId, registry, messageControl) + ) + ) + ) + .subscribe(outboundObserver), + }) + + // VMP DMP + this.#log.info('[%s] subscribe outbound DMP - by event (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#ingress + .getRegistry(chainId) + .pipe( + switchMap((registry) => + this.#sharedBlockEvents(chainId).pipe( + extractDmpSendByEvent(chainId, this.#getDmp(chainId, registry), registry), + this.#emitOutbound(id, chainId, registry, messageControl) + ) + ) + ) + .subscribe(outboundObserver), + }) + } else { + // Outbound HRMP / XCMP transport + this.#log.info('[%s] subscribe outbound HRMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#ingress + .getRegistry(chainId) + .pipe( + switchMap((registry) => + this.#sharedBlockEvents(chainId).pipe( + extractXcmpSend(chainId, this.#getHrmp(chainId, registry), registry), + this.#emitOutbound(id, chainId, registry, messageControl) + ) + ) + ) + .subscribe(outboundObserver), + }) + + // VMP UMP + this.#log.info('[%s] subscribe outbound UMP (%s)', chainId, id) + + subs.push({ + chainId, + sub: this.#ingress + .getRegistry(chainId) + .pipe( + switchMap((registry) => + this.#sharedBlockEvents(chainId).pipe( + extractUmpSend(chainId, this.#getUmp(chainId, registry), registry), + this.#emitOutbound(id, chainId, registry, messageControl) + ) + ) + ) + .subscribe(outboundObserver), + }) + } + } catch (error) { + // Clean up subscriptions. + subs.forEach(({ sub }) => { + sub.unsubscribe() + }) + throw error + } + + return { + subs, + controls: { + sendersControl, + messageControl, + }, + } + } + + #monitorRelay({ id }: Subscription, { origin, destinations }: XCMSubscriptionArgs) { + const chainId = origin as NetworkURN + if (this.#subs[id]?.relaySub) { + this.#log.debug('Relay subscription already exists.') + } + const messageControl = ControlQuery.from(messageCriteria(destinations as NetworkURN[])) + + const emitRelayInbound = () => (source: Observable) => + source.pipe(switchMap((message) => from(this.#engine.onRelayedMessage(id, message)))) + + const relayObserver = { + error: (error: any) => { + this.#log.error(error, '[%s] error on relay subscription s', chainId, id) + /* + this.emit('telemetrySubscriptionError', { + subscriptionId: id, + chainId, + direction: 'relay', + })*/ + + // try recover relay subscription + // there is only one subscription per subscription ID for relay + if (this.#subs[id]) { + const sub = this.#subs[id] + this.#timeouts.push( + setTimeout(async () => { + this.#log.info('[%s] UPDATE relay subscription %s due error %s', chainId, id, errorMessage(error)) + const updatedSub = await this.#monitorRelay(sub.descriptor, sub.args) + sub.relaySub = updatedSub + }, SUB_ERROR_RETRY_MS) + ) + } + }, + } + + // TODO: should resolve relay id for consensus in context + const relayIds = this.#ingress.getRelayIds() + const relayId = relayIds.find((r) => getConsensus(r) === getConsensus(chainId)) + + if (relayId === undefined) { + throw new Error(`No relay ID found for chain ${chainId}`) + } + this.#log.info('[%s] subscribe relay %s xcm events (%s)', chainId, relayId, id) + return { + chainId, + sub: this.#ingress + .getRegistry(relayId) + .pipe( + switchMap((registry) => + this.#sharedBlockExtrinsics(relayId).pipe( + extractRelayReceive(chainId, messageControl, registry), + emitRelayInbound() + ) + ) + ) + .subscribe(relayObserver), + } + } + + // Assumes only 1 pair of bridge hub origin-destination is possible + // TODO: handle possible multiple different consensus utilizing PK bridge e.g. solochains? + #monitorPkBridge({ id }: Subscription, { origin, destinations }: XCMSubscriptionArgs) { + const originBridgeHub = getBridgeHubNetworkId(origin as NetworkURN) + const dest = (destinations as NetworkURN[]).find((d) => getConsensus(d) !== getConsensus(origin as NetworkURN)) + + if (dest === undefined) { + throw new Error(`No destination on different consensus found for bridging (sub=${id})`) + } + + const destBridgeHub = getBridgeHubNetworkId(dest) + + if (originBridgeHub === undefined || destBridgeHub === undefined) { + throw new Error( + `Unable to subscribe to PK bridge due to missing bridge hub network URNs for origin=${origin} and destinations=${destinations}. (sub=${id})` + ) + } + + if (this.#subs[id]?.bridgeSubs.find((s) => s.type === 'pk-bridge')) { + throw new Error(`Fatal: duplicated PK bridge monitor ${id}`) + } + + const type: BridgeType = 'pk-bridge' + + const emitBridgeOutboundAccepted = () => (source: Observable) => + source.pipe(switchMap((message) => from(this.#engine.onBridgeOutboundAccepted(id, message)))) + + const emitBridgeOutboundDelivered = () => (source: Observable) => + source.pipe(switchMap((message) => from(this.#engine.onBridgeOutboundDelivered(id, message)))) + + const emitBridgeInbound = () => (source: Observable) => + source.pipe(switchMap((message) => from(this.#engine.onBridgeInbound(id, message)))) + + const pkBridgeObserver = { + error: (error: any) => { + this.#log.error(error, '[%s] error on PK bridge subscription s', originBridgeHub, id) + // this.emit('telemetrySubscriptionError', { + // subscriptionId: id, + // chainId: originBridgeHub, + // direction: 'bridge', + // }); + + // try recover pk bridge subscription + if (this.#subs[id]) { + const sub = this.#subs[id] + const { bridgeSubs } = sub + const index = bridgeSubs.findIndex((s) => s.type === 'pk-bridge') + if (index > -1) { + bridgeSubs.splice(index, 1) + this.#timeouts.push( + setTimeout(() => { + this.#log.info( + '[%s] UPDATE destination subscription %s due error %s', + originBridgeHub, + id, + errorMessage(error) + ) + bridgeSubs.push(this.#monitorPkBridge(sub.descriptor, sub.args)) + sub.bridgeSubs = bridgeSubs + }, SUB_ERROR_RETRY_MS) + ) + } + } + }, + } + + this.#log.info( + '[%s] subscribe PK bridge outbound accepted events on bridge hub %s (%s)', + origin, + originBridgeHub, + id + ) + const outboundAccepted: RxSubscriptionWithId = { + chainId: originBridgeHub, + sub: this.#ingress + .getRegistry(originBridgeHub) + .pipe( + switchMap((registry) => + this.#sharedBlockEvents(originBridgeHub).pipe( + extractBridgeMessageAccepted(originBridgeHub, registry, this.#getStorageAt(originBridgeHub)), + emitBridgeOutboundAccepted() + ) + ) + ) + .subscribe(pkBridgeObserver), + } + + this.#log.info( + '[%s] subscribe PK bridge outbound delivered events on bridge hub %s (%s)', + origin, + originBridgeHub, + id + ) + const outboundDelivered: RxSubscriptionWithId = { + chainId: originBridgeHub, + sub: this.#ingress + .getRegistry(originBridgeHub) + .pipe( + switchMap((registry) => + this.#sharedBlockEvents(originBridgeHub).pipe( + extractBridgeMessageDelivered(originBridgeHub, registry), + emitBridgeOutboundDelivered() + ) + ) + ) + .subscribe(pkBridgeObserver), + } + + this.#log.info('[%s] subscribe PK bridge inbound events on bridge hub %s (%s)', origin, destBridgeHub, id) + const inbound: RxSubscriptionWithId = { + chainId: destBridgeHub, + sub: this.#sharedBlockEvents(destBridgeHub) + .pipe(extractBridgeReceive(destBridgeHub), emitBridgeInbound()) + .subscribe(pkBridgeObserver), + } + + return { + type, + subs: [outboundAccepted, outboundDelivered, inbound], + } + } + + #updateDestinationSubscriptions(id: string) { + const { descriptor, args, destinationSubs } = this.#subs[id] + // Subscribe to new destinations, if any + const { subs } = this.#monitorDestinations(descriptor, args) + const updatedSubs = destinationSubs.concat(subs) + // Unsubscribe removed destinations, if any + const removed = updatedSubs.filter((s) => !args.destinations.includes(s.chainId)) + removed.forEach(({ sub }) => sub.unsubscribe()) + // Return list of updated subscriptions + return updatedSubs.filter((s) => !removed.includes(s)) + } + + /** + * Starts collecting XCM messages. + * + * Monitors all the active subscriptions. + * + * @private + */ + async #startNetworkMonitors() { + const subs = await this.#db.getByAgentId(this.id) + + this.#log.info('[%s] #subscriptions %d', this.id, subs.length) + + for (const sub of subs) { + try { + this.#monitor(sub, $XCMSubscriptionArgs.parse(sub.args)) + } catch (err) { + this.#log.error(err, 'Unable to create subscription: %j', sub) + } + } + } + + #sharedBlockEvents(chainId: NetworkURN): Observable { + if (!this.#shared.blockEvents[chainId]) { + this.#shared.blockEvents[chainId] = this.#ingress.finalizedBlocks(chainId).pipe(extractEvents(), share()) + } + return this.#shared.blockEvents[chainId] + } + + #sharedBlockExtrinsics(chainId: NetworkURN): Observable { + if (!this.#shared.blockExtrinsics[chainId]) { + this.#shared.blockExtrinsics[chainId] = this.#ingress + .finalizedBlocks(chainId) + .pipe(extractTxWithEvents(), flattenCalls(), share()) + } + return this.#shared.blockExtrinsics[chainId] + } + + /** + * Checks if relayed HRMP messages should be monitored. + * + * All of the following conditions needs to be met: + * 1. `xcm.relayed` notification event is requested in the subscription + * 2. Origin chain is not a relay chain + * 3. At least one destination chain is a parachain + * + * @param Subscription + * @returns boolean + */ + #shouldMonitorRelay({ origin, destinations, events }: XCMSubscriptionArgs) { + return ( + (events === undefined || events === '*' || events.includes(XcmNotificationType.Relayed)) && + !this.#ingress.isRelay(origin as NetworkURN) && + destinations.some((d) => !this.#ingress.isRelay(d as NetworkURN)) + ) + } + + #emitInbound(id: string, chainId: NetworkURN) { + return (source: Observable) => + source.pipe(switchMap((msg) => from(this.#engine.onInboundMessage(new XcmInbound(id, chainId, msg))))) + } + + #emitOutbound(id: string, origin: NetworkURN, registry: Registry, messageControl: ControlQuery) { + const { + args: { outboundTTL }, + } = this.#subs[id] + + return (source: Observable) => + source.pipe( + mapXcmSent(id, registry, origin), + filter((msg) => matchMessage(messageControl, msg)), + switchMap((outbound) => from(this.#engine.onOutboundMessage(outbound, outboundTTL))) + ) + } + + #getDmp(chainId: NetworkURN, registry: Registry): GetDownwardMessageQueues { + return (blockHash: HexString, networkId: NetworkURN) => { + const paraId = getChainId(networkId) + return from(this.#ingress.getStorage(chainId, dmpDownwardMessageQueuesKey(registry, paraId), blockHash)).pipe( + map((buffer) => { + return registry.createType('Vec', buffer) + }) + ) + } + } + + #getUmp(chainId: NetworkURN, registry: Registry): GetOutboundUmpMessages { + return (blockHash: HexString) => { + return from(this.#ingress.getStorage(chainId, parachainSystemUpwardMessages, blockHash)).pipe( + map((buffer) => { + return registry.createType('Vec', buffer) + }) + ) + } + } + + #getHrmp(chainId: NetworkURN, registry: Registry): GetOutboundHrmpMessages { + return (blockHash: HexString) => { + return from(this.#ingress.getStorage(chainId, parachainSystemHrmpOutboundMessages, blockHash)).pipe( + map((buffer) => { + return registry.createType('Vec', buffer) + }) + ) + } + } + + #getStorageAt(chainId: NetworkURN): GetStorageAt { + return (blockHash: HexString, key: HexString) => { + return from(this.#ingress.getStorage(chainId, key, blockHash)) + } + } + + /** + * Updates the senders control handler. + * + * Applies to the outbound extrinsic signers. + */ + #updateSenders(id: string) { + const { + args: { senders }, + sendersControl, + } = this.#subs[id] + + sendersControl.change(sendersCriteria(senders)) + } + + /** + * Updates the message control handler. + * + * Updates the destination subscriptions. + */ + #updateDestinations(id: string) { + const { args, messageControl } = this.#subs[id] + + messageControl.change(messageCriteria(args.destinations as NetworkURN[])) + + const updatedSubs = this.#updateDestinationSubscriptions(id) + this.#subs[id].destinationSubs = updatedSubs + } + + /** + * Updates the subscription to relayed HRMP messages in the relay chain. + */ + #updateEvents(id: string) { + const { descriptor, args, relaySub } = this.#subs[id] + + if (this.#shouldMonitorRelay(args) && relaySub === undefined) { + try { + this.#subs[id].relaySub = this.#monitorRelay(descriptor, args) + } catch (error) { + // log instead of throw to not block OD subscriptions + this.#log.error(error, 'Error on relay subscription (%s)', id) + } + } else if (!this.#shouldMonitorRelay(args) && relaySub !== undefined) { + relaySub.sub.unsubscribe() + delete this.#subs[id].relaySub + } + } + + #validateChainIds(chainIds: NetworkURN[]) { + chainIds.forEach((chainId) => { + if (!this.#ingress.isNetworkDefined(chainId)) { + throw new ValidationError('Invalid chain id:' + chainId) + } + }) + } +} From 1366640827e5ce97e8068180e74d5deac1526cd5 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 12:55:36 +0200 Subject: [PATCH 16/58] implement agents HTTP API --- packages/server/src/server.ts | 2 +- packages/server/src/services/agents/api.ts | 113 +++++++++--------- packages/server/src/services/agents/local.ts | 7 +- packages/server/src/services/agents/plugin.ts | 7 +- .../src/services/subscriptions/api/routes.ts | 29 +++++ .../src/services/subscriptions/switchboard.ts | 55 ++++++--- 6 files changed, 132 insertions(+), 81 deletions(-) diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 2dbd2cb1..34666189 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -111,7 +111,7 @@ export async function createServer(opts: ServerOptions) { await server.register(FastifySwagger, { openapi: { info: { - title: 'Ocelloids Execution Service', + title: 'Ocelloids Execution Node', version, }, }, diff --git a/packages/server/src/services/agents/api.ts b/packages/server/src/services/agents/api.ts index 603b6261..9794bc02 100644 --- a/packages/server/src/services/agents/api.ts +++ b/packages/server/src/services/agents/api.ts @@ -1,62 +1,57 @@ -// TBD agent web api -/* -// TO MOVE OUT to the XCM agent - if (isXcmReceived(msg)) { - this.#log.info( - '[%s ➜ %s] NOTIFICATION %s subscription=%s, messageHash=%s, outcome=%s (o: #%s, d: #%s)', - msg.origin.chainId, - msg.destination.chainId, - msg.type, - sub.id, - msg.waypoint.messageHash, - msg.waypoint.outcome, - msg.origin.blockNumber, - msg.destination.blockNumber - ) - } else if (isXcmHop(msg)) { - this.#notifyHop(sub, msg) - } else if (isXcmRelayed(msg) && msg.type === XcmNotificationType.Relayed) { - this.#log.info( - '[%s ↠ %s] NOTIFICATION %s subscription=%s, messageHash=%s, block=%s', - msg.origin.chainId, - msg.destination.chainId, - msg.type, - sub.id, - msg.waypoint.messageHash, - msg.waypoint.blockNumber - ) - } else if (isXcmSent(msg)) { - this.#log.info( - '[%s ➜] NOTIFICATION %s subscription=%s, messageHash=%s, block=%s', - msg.origin.chainId, - msg.type, - sub.id, - msg.waypoint.messageHash, - msg.origin.blockNumber - ) +import { FastifyInstance } from 'fastify' +import { zodToJsonSchema } from 'zod-to-json-schema' + +import { $AgentId, AgentId } from '../subscriptions/types.js' + +/** + * Agents HTTP API + */ +export async function AgentsApi(api: FastifyInstance) { + const { agentService } = api + + /** + * GET /agents + */ + api.get( + '/agents', + { + schema: { + response: { + 200: { + type: 'array', + items: { type: 'string' }, + }, + }, + }, + }, + async (_, reply) => { + reply.send(await agentService.getAgentIds()) } - } + ) - #notifyHop(sub: Subscription, msg: XcmHop) { - if (msg.direction === 'out') { - this.#log.info( - '[%s ↷] NOTIFICATION %s-%s subscription=%s, messageHash=%s, block=%s', - msg.waypoint.chainId, - msg.type, - msg.direction, - sub.id, - msg.waypoint.messageHash, - msg.waypoint.blockNumber - ) - } else if (msg.direction === 'in') { - this.#log.info( - '[↷ %s] NOTIFICATION %s-%s subscription=%s, messageHash=%s, block=%s', - msg.waypoint.chainId, - msg.type, - msg.direction, - sub.id, - msg.waypoint.messageHash, - msg.waypoint.blockNumber - ) + /** + * GET /agents/:agentId/inputs + */ + api.get<{ + Params: { + agentId: AgentId + } + }>( + '/agents/:agentId/inputs', + { + schema: { + params: { + agentId: zodToJsonSchema($AgentId), + }, + response: { + 200: { type: 'object', additionalProperties: true }, + 404: { type: 'string' }, + }, + }, + }, + async (request, reply) => { + const { agentId } = request.params + reply.send(zodToJsonSchema(await agentService.getAgentInputSchema(agentId))) } -*/ + ) +} diff --git a/packages/server/src/services/agents/local.ts b/packages/server/src/services/agents/local.ts index 0d05fbd1..339a73f0 100644 --- a/packages/server/src/services/agents/local.ts +++ b/packages/server/src/services/agents/local.ts @@ -1,3 +1,4 @@ +import { NotFound } from '../../errors.js' import { AgentServiceOptions } from '../../types.js' import { Logger, Services } from '../index.js' import { NotifierHub } from '../notification/index.js' @@ -16,11 +17,7 @@ export class LocalAgentService implements AgentService { constructor(ctx: Services, _options: AgentServiceOptions) { this.#log = ctx.log - - // XXX: this is a local in the process memory - // notifier hub this.#notifier = new NotifierHub(ctx) - this.#agents = this.#loadAgents({ ...ctx, notifier: this.#notifier, @@ -59,7 +56,7 @@ export class LocalAgentService implements AgentService { if (this.#agents[agentId]) { return this.#agents[agentId] } - throw new Error(`Agent not found for id=${agentId}`) + throw new NotFound(`Agent not found for id=${agentId}`) } getAgentInputSchema(agentId: AgentId) { diff --git a/packages/server/src/services/agents/plugin.ts b/packages/server/src/services/agents/plugin.ts index 3bcf4ec9..42cc08ed 100644 --- a/packages/server/src/services/agents/plugin.ts +++ b/packages/server/src/services/agents/plugin.ts @@ -2,6 +2,7 @@ import { FastifyPluginAsync } from 'fastify' import fp from 'fastify-plugin' import { AgentServiceMode, AgentServiceOptions } from '../../types.js' +import { AgentsApi } from './api.js' import { LocalAgentService } from './local.js' import { AgentService } from './types.js' @@ -23,6 +24,10 @@ const agentServicePlugin: FastifyPluginAsync = async (fasti } const service: AgentService = new LocalAgentService(fastify, options) + fastify.decorate('agentService', service) + + await AgentsApi(fastify) + fastify.addHook('onClose', (server, done) => { service .stop() @@ -37,8 +42,6 @@ const agentServicePlugin: FastifyPluginAsync = async (fasti }) }) - fastify.decorate('agentService', service) - await service.start() } diff --git a/packages/server/src/services/subscriptions/api/routes.ts b/packages/server/src/services/subscriptions/api/routes.ts index f7f01d02..ef40e19b 100644 --- a/packages/server/src/services/subscriptions/api/routes.ts +++ b/packages/server/src/services/subscriptions/api/routes.ts @@ -31,6 +31,35 @@ export async function SubscriptionApi(api: FastifyInstance) { } ) + /** + * GET subs/:agentId/:subscriptionId + */ + api.get<{ + Params: { + agentId: AgentId + } + }>( + '/subs/:agentId', + { + schema: { + params: { + agentId: zodToJsonSchema($AgentId), + }, + response: { + 200: { + type: 'array', + items: zodToJsonSchema($Subscription), + }, + 404: { type: 'string' }, + }, + }, + }, + async (request, reply) => { + const { agentId } = request.params + reply.send(await switchboard.getSubscriptionsByAgentId(agentId)) + } + ) + /** * GET subs/:agentId/:subscriptionId */ diff --git a/packages/server/src/services/subscriptions/switchboard.ts b/packages/server/src/services/subscriptions/switchboard.ts index b4a812a8..f246c6a8 100644 --- a/packages/server/src/services/subscriptions/switchboard.ts +++ b/packages/server/src/services/subscriptions/switchboard.ts @@ -13,6 +13,9 @@ export enum SubscribeErrorCodes { TOO_MANY_SUBSCRIBERS, } +/** + * Custom error class for subscription-related errors. + */ export class SubscribeError extends Error { code: SubscribeErrorCodes @@ -81,8 +84,11 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte /** * Adds a listener function to the underlying notifier. * + * This method is used by the web socket broadcaster. + * * @param eventName The notifier event name. * @param listener The listener function. + * @see {@link WebsocketProtocol} */ addNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener) { this.#agentService.addNotificationListener(eventName, listener) @@ -93,6 +99,7 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte * * @param eventName The notifier event name. * @param listener The listener function. + * @see {@link WebsocketProtocol} */ removeNotificationListener(eventName: keyof NotifierEvents, listener: NotificationListener) { this.#agentService.removeNotificationListener(eventName, listener) @@ -104,30 +111,37 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte * If the subscription does not exists just ignores it. * * @param {AgentId} agentId The agent identifier. - * @param {string} id The subscription identifier. + * @param {string} subscriptionId The subscription identifier. */ - async unsubscribe(agentId: AgentId, id: string) { - const agent = this.#agentService.getAgentById(agentId) - const { ephemeral } = agent.getSubscriptionHandler(id) - await agent.unsubscribe(id) - - if (ephemeral) { - this.#stats.ephemeral-- - } else { - this.#stats.persistent-- + async unsubscribe(agentId: AgentId, subscriptionId: string) { + try { + const agent = this.#agentService.getAgentById(agentId) + const { ephemeral } = agent.getSubscriptionHandler(subscriptionId) + await agent.unsubscribe(subscriptionId) + + if (ephemeral) { + this.#stats.ephemeral-- + } else { + this.#stats.persistent-- + } + } catch { + // ignore } } async start() { - // empty + // This method can be used for initialization if needed. } async stop() { - // empty + // This method can be used for cleanup if needed. } /** * Gets a subscription handler by id. + * + * @param {AgentId} agentId The agent identifier. + * @param {string} subscriptionId The subscription identifier. */ findSubscriptionHandler(agentId: AgentId, subscriptionId: string) { return this.#agentService.getAgentById(agentId).getSubscriptionHandler(subscriptionId) @@ -135,6 +149,8 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte /** * Gets all the subscriptions for all the known agents. + * + * @returns {Promise} All subscriptions. */ async getAllSubscriptions(): Promise { const subs: Subscription[][] = [] @@ -145,10 +161,21 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte } /** - * Gets a subscription by identifier. + * Gets all the subscriptions under an agent. + * + * @param agentId The agent identifier. + * @returns {Promise} All subscriptions under the specified agent. + */ + async getSubscriptionsByAgentId(agentId: string): Promise { + return await this.#agentService.getAgentById(agentId).getAllSubscriptions() + } + + /** + * Gets a subscription by subscription identifier under an agent. * * @param agentId The agent identifier. * @param subscriptionId The subscription identifier. + * @returns {Promise} The subscription with the specified identifier. */ async getSubscriptionById(agentId: AgentId, subscriptionId: string): Promise { return await this.#agentService.getAgentById(agentId).getSubscriptionById(subscriptionId) @@ -160,7 +187,7 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte * @param agentId The agent identifier. * @param subscriptionId The subscription identifier * @param patch The JSON patch operations. - * @returns the patched subscription object. + * @returns {Promise} The updated subscription object. */ updateSubscription(agentId: AgentId, subscriptionId: string, patch: Operation[]) { return this.#agentService.getAgentById(agentId).update(subscriptionId, patch) From e94584bc250100be8dc3dadc3e04d2461e09e0df Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Wed, 29 May 2024 13:20:37 +0200 Subject: [PATCH 17/58] extract base agent --- .../src/services/agents/base/base-agent.ts | 93 ++++++ packages/server/src/services/agents/types.ts | 7 + .../src/services/agents/xcm/xcm-agent.ts | 300 +++++++----------- 3 files changed, 219 insertions(+), 181 deletions(-) create mode 100644 packages/server/src/services/agents/base/base-agent.ts diff --git a/packages/server/src/services/agents/base/base-agent.ts b/packages/server/src/services/agents/base/base-agent.ts new file mode 100644 index 00000000..45586a16 --- /dev/null +++ b/packages/server/src/services/agents/base/base-agent.ts @@ -0,0 +1,93 @@ +import { z } from 'zod' +import { Operation } from 'rfc6902' +import { Observable, from, share } from 'rxjs' +import { extractEvents, extractTxWithEvents, flattenCalls, types } from '@sodazone/ocelloids-sdk' + +import { Logger, NetworkURN } from '../../types.js' +import { IngressConsumer } from '../../ingress/index.js' +import { NotifierHub } from '../../notification/hub.js' +import { SubsStore } from '../../persistence/subs.js' +import { AgentId, HexString, Subscription } from '../../subscriptions/types.js' +import { Agent, AgentMetadata, AgentRuntimeContext } from '../types.js' +import { GetStorageAt } from '../xcm/types-augmented.js' + +type SubscriptionHandler = { + descriptor: Subscription +} + +export abstract class BaseAgent implements Agent { + protected readonly subs: Record = {} + protected readonly log: Logger + protected readonly timeouts: NodeJS.Timeout[] = [] + protected readonly db: SubsStore + protected readonly ingress: IngressConsumer + protected readonly notifier: NotifierHub + + protected shared: { + blockEvents: Record> + blockExtrinsics: Record> + } + + constructor(ctx: AgentRuntimeContext) { + const { log, ingressConsumer, notifier, subsStore } = ctx + + this.log = log + this.ingress = ingressConsumer + this.notifier = notifier + this.db = subsStore + + this.shared = { + blockEvents: {}, + blockExtrinsics: {}, + } + } + abstract get metadata(): AgentMetadata + + get id(): AgentId { + return this.metadata.id + } + + async getSubscriptionById(subscriptionId: string): Promise { + return await this.db.getById(this.id, subscriptionId) + } + async getAllSubscriptions(): Promise { + return await this.db.getByAgentId(this.id) + } + + getSubscriptionHandler(subscriptionId: string): Subscription { + if (this.subs[subscriptionId]) { + return this.subs[subscriptionId].descriptor + } else { + throw Error('subscription handler not found') + } + } + + abstract getInputSchema(): z.ZodSchema + abstract subscribe(subscription: Subscription): Promise + abstract unsubscribe(subscriptionId: string): Promise + abstract update(subscriptionId: string, patch: Operation[]): Promise + abstract stop(): Promise + abstract start(): Promise + + protected sharedBlockEvents(chainId: NetworkURN): Observable { + if (!this.shared.blockEvents[chainId]) { + this.shared.blockEvents[chainId] = this.ingress.finalizedBlocks(chainId).pipe(extractEvents(), share()) + } + return this.shared.blockEvents[chainId] + } + + protected sharedBlockExtrinsics(chainId: NetworkURN): Observable { + if (!this.shared.blockExtrinsics[chainId]) { + this.shared.blockExtrinsics[chainId] = this.ingress + .finalizedBlocks(chainId) + .pipe(extractTxWithEvents(), flattenCalls(), share()) + } + return this.shared.blockExtrinsics[chainId] + } + + protected getStorageAt(chainId: NetworkURN): GetStorageAt { + return (blockHash: HexString, key: HexString) => { + return from(this.ingress.getStorage(chainId, key, blockHash)) + } + } +} diff --git a/packages/server/src/services/agents/types.ts b/packages/server/src/services/agents/types.ts index 0073bc96..9c617afa 100644 --- a/packages/server/src/services/agents/types.ts +++ b/packages/server/src/services/agents/types.ts @@ -29,8 +29,15 @@ export interface AgentService { stop(): Promise } +export type AgentMetadata = { + id: AgentId + name?: string + description?: string +} + export interface Agent { get id(): AgentId + get metadata(): AgentMetadata getSubscriptionById(subscriptionId: string): Promise getAllSubscriptions(): Promise getInputSchema(): z.ZodSchema diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts index e8e7bff1..eb6d014a 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -1,6 +1,8 @@ import { Registry } from '@polkadot/types-codec/types' -import { ControlQuery, extractEvents, extractTxWithEvents, flattenCalls, types } from '@sodazone/ocelloids-sdk' -import { Observable, filter, from, map, share, switchMap } from 'rxjs' +import { ControlQuery } from '@sodazone/ocelloids-sdk' +import { Operation, applyPatch } from 'rfc6902' +import { Observable, filter, from, map, switchMap } from 'rxjs' +import { z } from 'zod' import { $Subscription, @@ -10,7 +12,7 @@ import { RxSubscriptionWithId, Subscription, } from '../../subscriptions/types.js' -import { Logger, NetworkURN } from '../../types.js' +import { NetworkURN } from '../../types.js' import { extractXcmpReceive, extractXcmpSend } from './ops/xcmp.js' import { $XCMSubscriptionArgs, @@ -30,35 +32,26 @@ import { XcmSentWithContext, } from './types.js' -import { SubsStore } from '../../persistence/subs.js' import { MatchingEngine } from './matching.js' import { ValidationError, errorMessage } from '../../../errors.js' -import { IngressConsumer } from '../../ingress/index.js' import { mapXcmSent } from './ops/common.js' import { matchMessage, matchSenders, messageCriteria, sendersCriteria } from './ops/criteria.js' import { extractDmpReceive, extractDmpSend, extractDmpSendByEvent } from './ops/dmp.js' import { extractRelayReceive } from './ops/relay.js' import { extractUmpReceive, extractUmpSend } from './ops/ump.js' -import { Operation, applyPatch } from 'rfc6902' -import { z } from 'zod' import { getChainId, getConsensus } from '../../config.js' -import { NotifierHub } from '../../notification/index.js' import { dmpDownwardMessageQueuesKey, parachainSystemHrmpOutboundMessages, parachainSystemUpwardMessages, } from '../../subscriptions/storage.js' -import { Agent, AgentRuntimeContext } from '../types.js' +import { BaseAgent } from '../base/base-agent.js' +import { AgentMetadata, AgentRuntimeContext } from '../types.js' import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './ops/pk-bridge.js' import { getBridgeHubNetworkId } from './ops/util.js' -import { - GetDownwardMessageQueues, - GetOutboundHrmpMessages, - GetOutboundUmpMessages, - GetStorageAt, -} from './types-augmented.js' +import { GetDownwardMessageQueues, GetOutboundHrmpMessages, GetOutboundUmpMessages } from './types-augmented.js' const SUB_ERROR_RETRY_MS = 5000 @@ -68,45 +61,17 @@ function hasOp(patch: Operation[], path: string) { return patch.some((op) => op.path.startsWith(path)) } -export class XCMAgent implements Agent { - readonly #subs: Record = {} - readonly #log: Logger +export class XCMAgent extends BaseAgent { + protected readonly subs: Record = {} readonly #engine: MatchingEngine - readonly #timeouts: NodeJS.Timeout[] = [] - readonly #db: SubsStore - readonly #ingress: IngressConsumer - readonly #notifier: NotifierHub - - #shared: { - blockEvents: Record> - blockExtrinsics: Record> - } constructor(ctx: AgentRuntimeContext) { - const { log, ingressConsumer, notifier, subsStore } = ctx - - this.#log = log - this.#ingress = ingressConsumer - this.#notifier = notifier - this.#db = subsStore - this.#engine = new MatchingEngine(ctx, this.#onXcmWaypointReached.bind(this)) - - this.#shared = { - blockEvents: {}, - blockExtrinsics: {}, - } - } - - async getAllSubscriptions(): Promise { - return await this.#db.getByAgentId(this.id) - } - - async getSubscriptionById(subscriptionId: string): Promise { - return await this.#db.getById(this.id, subscriptionId) + super(ctx) + this.#engine = new MatchingEngine(ctx, this.#onXcmWaypointReached) } async update(subscriptionId: string, patch: Operation[]): Promise { - const sub = this.#subs[subscriptionId] + const sub = this.subs[subscriptionId] const descriptor = sub.descriptor // Check allowed patch ops @@ -117,7 +82,7 @@ export class XCMAgent implements Agent { $Subscription.parse(descriptor) const args = $XCMSubscriptionArgs.parse(descriptor.args) - await this.#db.save(descriptor) + await this.db.save(descriptor) sub.args = args sub.descriptor = descriptor @@ -144,15 +109,10 @@ export class XCMAgent implements Agent { return $XCMSubscriptionArgs } - get id(): AgentId { - return 'xcm' - } - - getSubscriptionHandler(id: string): Subscription { - if (this.#subs[id]) { - return this.#subs[id].descriptor - } else { - throw Error('subscription handler not found') + get metadata(): AgentMetadata { + return { + id: 'xcm', + name: 'XCM Agent' } } @@ -164,15 +124,15 @@ export class XCMAgent implements Agent { this.#validateChainIds([origin, ...dests]) if (!s.ephemeral) { - await this.#db.insert(s) + await this.db.insert(s) } this.#monitor(s, args) } async unsubscribe(id: string): Promise { - if (this.#subs[id] === undefined) { - this.#log.warn('unsubscribe from a non-existent subscription %s', id) + if (this.subs[id] === undefined) { + this.log.warn('unsubscribe from a non-existent subscription %s', id) return } @@ -183,24 +143,24 @@ export class XCMAgent implements Agent { originSubs, destinationSubs, relaySub, - } = this.#subs[id] + } = this.subs[id] - this.#log.info('[%s] unsubscribe %s', origin, id) + this.log.info('[%s] unsubscribe %s', origin, id) originSubs.forEach(({ sub }) => sub.unsubscribe()) destinationSubs.forEach(({ sub }) => sub.unsubscribe()) if (relaySub) { relaySub.sub.unsubscribe() } - delete this.#subs[id] + delete this.subs[id] await this.#engine.clearPendingStates(id) if (!ephemeral) { - await this.#db.remove(this.id, id) + await this.db.remove(this.id, id) } } catch (error) { - this.#log.error(error, 'Error unsubscribing %s', id) + this.log.error(error, 'Error unsubscribing %s', id) } } async stop(): Promise { @@ -209,8 +169,8 @@ export class XCMAgent implements Agent { originSubs, destinationSubs, relaySub, - } of Object.values(this.#subs)) { - this.#log.info('Unsubscribe %s', id) + } of Object.values(this.subs)) { + this.log.info('Unsubscribe %s', id) originSubs.forEach(({ sub }) => sub.unsubscribe()) destinationSubs.forEach(({ sub }) => sub.unsubscribe()) @@ -219,7 +179,7 @@ export class XCMAgent implements Agent { } } - for (const t of this.#timeouts) { + for (const t of this.timeouts) { t.unref() } @@ -232,13 +192,13 @@ export class XCMAgent implements Agent { #onXcmWaypointReached(payload: XcmNotifyMessage) { const { subscriptionId } = payload - if (this.#subs[subscriptionId]) { - const { descriptor, args, sendersControl } = this.#subs[subscriptionId] + if (this.subs[subscriptionId]) { + const { descriptor, args, sendersControl } = this.subs[subscriptionId] if ( (args.events === undefined || args.events === '*' || args.events.includes(payload.type)) && matchSenders(sendersControl, payload.sender) ) { - this.#notifier.notify(descriptor, { + this.notifier.notify(descriptor, { metadata: { type: payload.type, subscriptionId, @@ -249,7 +209,7 @@ export class XCMAgent implements Agent { } } else { // this could happen with closed ephemeral subscriptions - this.#log.warn('Unable to find descriptor for subscription %s', subscriptionId) + this.log.warn('Unable to find descriptor for subscription %s', subscriptionId) } } @@ -291,7 +251,7 @@ export class XCMAgent implements Agent { relaySub = this.#monitorRelay(descriptor, args) } catch (error) { // log instead of throw to not block OD subscriptions - this.#log.error(error, 'Error on relay subscription (%s)', id) + this.log.error(error, 'Error on relay subscription (%s)', id) } } @@ -301,14 +261,14 @@ export class XCMAgent implements Agent { bridgeSubs.push(this.#monitorPkBridge(descriptor, args)) } catch (error) { // log instead of throw to not block OD subscriptions - this.#log.error(error, 'Error on bridge subscription (%s)', id) + this.log.error(error, 'Error on bridge subscription (%s)', id) } } } const { sendersControl, messageControl } = origMonitor.controls - this.#subs[id] = { + this.subs[id] = { descriptor, args, sendersControl, @@ -331,7 +291,7 @@ export class XCMAgent implements Agent { try { for (const dest of destinations as NetworkURN[]) { const chainId = dest - if (this.#subs[id]?.destinationSubs.find((s) => s.chainId === chainId)) { + if (this.subs[id]?.destinationSubs.find((s) => s.chainId === chainId)) { // Skip existing subscriptions // for the same destination chain continue @@ -339,7 +299,7 @@ export class XCMAgent implements Agent { const inboundObserver = { error: (error: any) => { - this.#log.error(error, '[%s] error on destination subscription %s', chainId, id) + this.log.error(error, '[%s] error on destination subscription %s', chainId, id) /*this.emit('telemetrySubscriptionError', { subscriptionId: id, @@ -348,21 +308,21 @@ export class XCMAgent implements Agent { })*/ // try recover inbound subscription - if (this.#subs[id]) { - const { destinationSubs } = this.#subs[id] + if (this.subs[id]) { + const { destinationSubs } = this.subs[id] const index = destinationSubs.findIndex((s) => s.chainId === chainId) if (index > -1) { destinationSubs.splice(index, 1) - this.#timeouts.push( + this.timeouts.push( setTimeout(() => { - this.#log.info( + this.log.info( '[%s] UPDATE destination subscription %s due error %s', chainId, id, errorMessage(error) ) const updated = this.#updateDestinationSubscriptions(id) - this.#subs[id].destinationSubs = updated + this.subs[id].destinationSubs = updated }, SUB_ERROR_RETRY_MS) ) } @@ -370,33 +330,33 @@ export class XCMAgent implements Agent { }, } - if (this.#ingress.isRelay(dest)) { + if (this.ingress.isRelay(dest)) { // VMP UMP - this.#log.info('[%s] subscribe inbound UMP (%s)', chainId, id) + this.log.info('[%s] subscribe inbound UMP (%s)', chainId, id) subs.push({ chainId, - sub: this.#sharedBlockEvents(chainId) + sub: this.sharedBlockEvents(chainId) .pipe(extractUmpReceive(originId), this.#emitInbound(id, chainId)) .subscribe(inboundObserver), }) - } else if (this.#ingress.isRelay(originId)) { + } else if (this.ingress.isRelay(originId)) { // VMP DMP - this.#log.info('[%s] subscribe inbound DMP (%s)', chainId, id) + this.log.info('[%s] subscribe inbound DMP (%s)', chainId, id) subs.push({ chainId, - sub: this.#sharedBlockEvents(chainId) + sub: this.sharedBlockEvents(chainId) .pipe(extractDmpReceive(), this.#emitInbound(id, chainId)) .subscribe(inboundObserver), }) } else { // Inbound HRMP / XCMP transport - this.#log.info('[%s] subscribe inbound HRMP (%s)', chainId, id) + this.log.info('[%s] subscribe inbound HRMP (%s)', chainId, id) subs.push({ chainId, - sub: this.#sharedBlockEvents(chainId) + sub: this.sharedBlockEvents(chainId) .pipe(extractXcmpReceive(), this.#emitInbound(id, chainId)) .subscribe(inboundObserver), }) @@ -422,7 +382,7 @@ export class XCMAgent implements Agent { const subs: RxSubscriptionWithId[] = [] const chainId = origin as NetworkURN - if (this.#subs[id]?.originSubs.find((s) => s.chainId === chainId)) { + if (this.subs[id]?.originSubs.find((s) => s.chainId === chainId)) { throw new Error(`Fatal: duplicated origin monitor ${id} for chain ${chainId}`) } @@ -431,7 +391,7 @@ export class XCMAgent implements Agent { const outboundObserver = { error: (error: any) => { - this.#log.error(error, '[%s] error on origin subscription %s', chainId, id) + this.log.error(error, '[%s] error on origin subscription %s', chainId, id) /* this.emit('telemetrySubscriptionError', { subscriptionId: id, @@ -441,19 +401,19 @@ export class XCMAgent implements Agent { // try recover outbound subscription // note: there is a single origin per outbound - if (this.#subs[id]) { - const { originSubs, descriptor, args } = this.#subs[id] + if (this.subs[id]) { + const { originSubs, descriptor, args } = this.subs[id] const index = originSubs.findIndex((s) => s.chainId === chainId) if (index > -1) { - this.#subs[id].originSubs = [] - this.#timeouts.push( + this.subs[id].originSubs = [] + this.timeouts.push( setTimeout(() => { - if (this.#subs[id]) { - this.#log.info('[%s] UPDATE origin subscription %s due error %s', chainId, id, errorMessage(error)) + if (this.subs[id]) { + this.log.info('[%s] UPDATE origin subscription %s due error %s', chainId, id, errorMessage(error)) const { subs: updated, controls } = this.#monitorOrigins(descriptor, args) - this.#subs[id].sendersControl = controls.sendersControl - this.#subs[id].messageControl = controls.messageControl - this.#subs[id].originSubs = updated + this.subs[id].sendersControl = controls.sendersControl + this.subs[id].messageControl = controls.messageControl + this.subs[id].originSubs = updated } }, SUB_ERROR_RETRY_MS) ) @@ -463,17 +423,17 @@ export class XCMAgent implements Agent { } try { - if (this.#ingress.isRelay(chainId)) { + if (this.ingress.isRelay(chainId)) { // VMP DMP - this.#log.info('[%s] subscribe outbound DMP (%s)', chainId, id) + this.log.info('[%s] subscribe outbound DMP (%s)', chainId, id) subs.push({ chainId, - sub: this.#ingress + sub: this.ingress .getRegistry(chainId) .pipe( switchMap((registry) => - this.#sharedBlockExtrinsics(chainId).pipe( + this.sharedBlockExtrinsics(chainId).pipe( extractDmpSend(chainId, this.#getDmp(chainId, registry), registry), this.#emitOutbound(id, chainId, registry, messageControl) ) @@ -483,15 +443,15 @@ export class XCMAgent implements Agent { }) // VMP DMP - this.#log.info('[%s] subscribe outbound DMP - by event (%s)', chainId, id) + this.log.info('[%s] subscribe outbound DMP - by event (%s)', chainId, id) subs.push({ chainId, - sub: this.#ingress + sub: this.ingress .getRegistry(chainId) .pipe( switchMap((registry) => - this.#sharedBlockEvents(chainId).pipe( + this.sharedBlockEvents(chainId).pipe( extractDmpSendByEvent(chainId, this.#getDmp(chainId, registry), registry), this.#emitOutbound(id, chainId, registry, messageControl) ) @@ -501,15 +461,15 @@ export class XCMAgent implements Agent { }) } else { // Outbound HRMP / XCMP transport - this.#log.info('[%s] subscribe outbound HRMP (%s)', chainId, id) + this.log.info('[%s] subscribe outbound HRMP (%s)', chainId, id) subs.push({ chainId, - sub: this.#ingress + sub: this.ingress .getRegistry(chainId) .pipe( switchMap((registry) => - this.#sharedBlockEvents(chainId).pipe( + this.sharedBlockEvents(chainId).pipe( extractXcmpSend(chainId, this.#getHrmp(chainId, registry), registry), this.#emitOutbound(id, chainId, registry, messageControl) ) @@ -519,15 +479,15 @@ export class XCMAgent implements Agent { }) // VMP UMP - this.#log.info('[%s] subscribe outbound UMP (%s)', chainId, id) + this.log.info('[%s] subscribe outbound UMP (%s)', chainId, id) subs.push({ chainId, - sub: this.#ingress + sub: this.ingress .getRegistry(chainId) .pipe( switchMap((registry) => - this.#sharedBlockEvents(chainId).pipe( + this.sharedBlockEvents(chainId).pipe( extractUmpSend(chainId, this.#getUmp(chainId, registry), registry), this.#emitOutbound(id, chainId, registry, messageControl) ) @@ -555,8 +515,8 @@ export class XCMAgent implements Agent { #monitorRelay({ id }: Subscription, { origin, destinations }: XCMSubscriptionArgs) { const chainId = origin as NetworkURN - if (this.#subs[id]?.relaySub) { - this.#log.debug('Relay subscription already exists.') + if (this.subs[id]?.relaySub) { + this.log.debug('Relay subscription already exists.') } const messageControl = ControlQuery.from(messageCriteria(destinations as NetworkURN[])) @@ -565,7 +525,7 @@ export class XCMAgent implements Agent { const relayObserver = { error: (error: any) => { - this.#log.error(error, '[%s] error on relay subscription s', chainId, id) + this.log.error(error, '[%s] error on relay subscription s', chainId, id) /* this.emit('telemetrySubscriptionError', { subscriptionId: id, @@ -575,11 +535,11 @@ export class XCMAgent implements Agent { // try recover relay subscription // there is only one subscription per subscription ID for relay - if (this.#subs[id]) { - const sub = this.#subs[id] - this.#timeouts.push( + if (this.subs[id]) { + const sub = this.subs[id] + this.timeouts.push( setTimeout(async () => { - this.#log.info('[%s] UPDATE relay subscription %s due error %s', chainId, id, errorMessage(error)) + this.log.info('[%s] UPDATE relay subscription %s due error %s', chainId, id, errorMessage(error)) const updatedSub = await this.#monitorRelay(sub.descriptor, sub.args) sub.relaySub = updatedSub }, SUB_ERROR_RETRY_MS) @@ -589,20 +549,20 @@ export class XCMAgent implements Agent { } // TODO: should resolve relay id for consensus in context - const relayIds = this.#ingress.getRelayIds() + const relayIds = this.ingress.getRelayIds() const relayId = relayIds.find((r) => getConsensus(r) === getConsensus(chainId)) if (relayId === undefined) { throw new Error(`No relay ID found for chain ${chainId}`) } - this.#log.info('[%s] subscribe relay %s xcm events (%s)', chainId, relayId, id) + this.log.info('[%s] subscribe relay %s xcm events (%s)', chainId, relayId, id) return { chainId, - sub: this.#ingress + sub: this.ingress .getRegistry(relayId) .pipe( switchMap((registry) => - this.#sharedBlockExtrinsics(relayId).pipe( + this.sharedBlockExtrinsics(relayId).pipe( extractRelayReceive(chainId, messageControl, registry), emitRelayInbound() ) @@ -630,7 +590,7 @@ export class XCMAgent implements Agent { ) } - if (this.#subs[id]?.bridgeSubs.find((s) => s.type === 'pk-bridge')) { + if (this.subs[id]?.bridgeSubs.find((s) => s.type === 'pk-bridge')) { throw new Error(`Fatal: duplicated PK bridge monitor ${id}`) } @@ -647,7 +607,7 @@ export class XCMAgent implements Agent { const pkBridgeObserver = { error: (error: any) => { - this.#log.error(error, '[%s] error on PK bridge subscription s', originBridgeHub, id) + this.log.error(error, '[%s] error on PK bridge subscription s', originBridgeHub, id) // this.emit('telemetrySubscriptionError', { // subscriptionId: id, // chainId: originBridgeHub, @@ -655,15 +615,15 @@ export class XCMAgent implements Agent { // }); // try recover pk bridge subscription - if (this.#subs[id]) { - const sub = this.#subs[id] + if (this.subs[id]) { + const sub = this.subs[id] const { bridgeSubs } = sub const index = bridgeSubs.findIndex((s) => s.type === 'pk-bridge') if (index > -1) { bridgeSubs.splice(index, 1) - this.#timeouts.push( + this.timeouts.push( setTimeout(() => { - this.#log.info( + this.log.info( '[%s] UPDATE destination subscription %s due error %s', originBridgeHub, id, @@ -678,7 +638,7 @@ export class XCMAgent implements Agent { }, } - this.#log.info( + this.log.info( '[%s] subscribe PK bridge outbound accepted events on bridge hub %s (%s)', origin, originBridgeHub, @@ -686,12 +646,12 @@ export class XCMAgent implements Agent { ) const outboundAccepted: RxSubscriptionWithId = { chainId: originBridgeHub, - sub: this.#ingress + sub: this.ingress .getRegistry(originBridgeHub) .pipe( switchMap((registry) => - this.#sharedBlockEvents(originBridgeHub).pipe( - extractBridgeMessageAccepted(originBridgeHub, registry, this.#getStorageAt(originBridgeHub)), + this.sharedBlockEvents(originBridgeHub).pipe( + extractBridgeMessageAccepted(originBridgeHub, registry, this.getStorageAt(originBridgeHub)), emitBridgeOutboundAccepted() ) ) @@ -699,7 +659,7 @@ export class XCMAgent implements Agent { .subscribe(pkBridgeObserver), } - this.#log.info( + this.log.info( '[%s] subscribe PK bridge outbound delivered events on bridge hub %s (%s)', origin, originBridgeHub, @@ -707,11 +667,11 @@ export class XCMAgent implements Agent { ) const outboundDelivered: RxSubscriptionWithId = { chainId: originBridgeHub, - sub: this.#ingress + sub: this.ingress .getRegistry(originBridgeHub) .pipe( switchMap((registry) => - this.#sharedBlockEvents(originBridgeHub).pipe( + this.sharedBlockEvents(originBridgeHub).pipe( extractBridgeMessageDelivered(originBridgeHub, registry), emitBridgeOutboundDelivered() ) @@ -720,10 +680,10 @@ export class XCMAgent implements Agent { .subscribe(pkBridgeObserver), } - this.#log.info('[%s] subscribe PK bridge inbound events on bridge hub %s (%s)', origin, destBridgeHub, id) + this.log.info('[%s] subscribe PK bridge inbound events on bridge hub %s (%s)', origin, destBridgeHub, id) const inbound: RxSubscriptionWithId = { chainId: destBridgeHub, - sub: this.#sharedBlockEvents(destBridgeHub) + sub: this.sharedBlockEvents(destBridgeHub) .pipe(extractBridgeReceive(destBridgeHub), emitBridgeInbound()) .subscribe(pkBridgeObserver), } @@ -735,7 +695,7 @@ export class XCMAgent implements Agent { } #updateDestinationSubscriptions(id: string) { - const { descriptor, args, destinationSubs } = this.#subs[id] + const { descriptor, args, destinationSubs } = this.subs[id] // Subscribe to new destinations, if any const { subs } = this.#monitorDestinations(descriptor, args) const updatedSubs = destinationSubs.concat(subs) @@ -754,35 +714,19 @@ export class XCMAgent implements Agent { * @private */ async #startNetworkMonitors() { - const subs = await this.#db.getByAgentId(this.id) + const subs = await this.db.getByAgentId(this.id) - this.#log.info('[%s] #subscriptions %d', this.id, subs.length) + this.log.info('[%s] subscriptions %d', this.id, subs.length) for (const sub of subs) { try { this.#monitor(sub, $XCMSubscriptionArgs.parse(sub.args)) } catch (err) { - this.#log.error(err, 'Unable to create subscription: %j', sub) + this.log.error(err, 'Unable to create subscription: %j', sub) } } } - #sharedBlockEvents(chainId: NetworkURN): Observable { - if (!this.#shared.blockEvents[chainId]) { - this.#shared.blockEvents[chainId] = this.#ingress.finalizedBlocks(chainId).pipe(extractEvents(), share()) - } - return this.#shared.blockEvents[chainId] - } - - #sharedBlockExtrinsics(chainId: NetworkURN): Observable { - if (!this.#shared.blockExtrinsics[chainId]) { - this.#shared.blockExtrinsics[chainId] = this.#ingress - .finalizedBlocks(chainId) - .pipe(extractTxWithEvents(), flattenCalls(), share()) - } - return this.#shared.blockExtrinsics[chainId] - } - /** * Checks if relayed HRMP messages should be monitored. * @@ -797,8 +741,8 @@ export class XCMAgent implements Agent { #shouldMonitorRelay({ origin, destinations, events }: XCMSubscriptionArgs) { return ( (events === undefined || events === '*' || events.includes(XcmNotificationType.Relayed)) && - !this.#ingress.isRelay(origin as NetworkURN) && - destinations.some((d) => !this.#ingress.isRelay(d as NetworkURN)) + !this.ingress.isRelay(origin as NetworkURN) && + destinations.some((d) => !this.ingress.isRelay(d as NetworkURN)) ) } @@ -810,7 +754,7 @@ export class XCMAgent implements Agent { #emitOutbound(id: string, origin: NetworkURN, registry: Registry, messageControl: ControlQuery) { const { args: { outboundTTL }, - } = this.#subs[id] + } = this.subs[id] return (source: Observable) => source.pipe( @@ -823,7 +767,7 @@ export class XCMAgent implements Agent { #getDmp(chainId: NetworkURN, registry: Registry): GetDownwardMessageQueues { return (blockHash: HexString, networkId: NetworkURN) => { const paraId = getChainId(networkId) - return from(this.#ingress.getStorage(chainId, dmpDownwardMessageQueuesKey(registry, paraId), blockHash)).pipe( + return from(this.ingress.getStorage(chainId, dmpDownwardMessageQueuesKey(registry, paraId), blockHash)).pipe( map((buffer) => { return registry.createType('Vec', buffer) }) @@ -833,7 +777,7 @@ export class XCMAgent implements Agent { #getUmp(chainId: NetworkURN, registry: Registry): GetOutboundUmpMessages { return (blockHash: HexString) => { - return from(this.#ingress.getStorage(chainId, parachainSystemUpwardMessages, blockHash)).pipe( + return from(this.ingress.getStorage(chainId, parachainSystemUpwardMessages, blockHash)).pipe( map((buffer) => { return registry.createType('Vec', buffer) }) @@ -843,7 +787,7 @@ export class XCMAgent implements Agent { #getHrmp(chainId: NetworkURN, registry: Registry): GetOutboundHrmpMessages { return (blockHash: HexString) => { - return from(this.#ingress.getStorage(chainId, parachainSystemHrmpOutboundMessages, blockHash)).pipe( + return from(this.ingress.getStorage(chainId, parachainSystemHrmpOutboundMessages, blockHash)).pipe( map((buffer) => { return registry.createType('Vec', buffer) }) @@ -851,12 +795,6 @@ export class XCMAgent implements Agent { } } - #getStorageAt(chainId: NetworkURN): GetStorageAt { - return (blockHash: HexString, key: HexString) => { - return from(this.#ingress.getStorage(chainId, key, blockHash)) - } - } - /** * Updates the senders control handler. * @@ -866,7 +804,7 @@ export class XCMAgent implements Agent { const { args: { senders }, sendersControl, - } = this.#subs[id] + } = this.subs[id] sendersControl.change(sendersCriteria(senders)) } @@ -877,36 +815,36 @@ export class XCMAgent implements Agent { * Updates the destination subscriptions. */ #updateDestinations(id: string) { - const { args, messageControl } = this.#subs[id] + const { args, messageControl } = this.subs[id] messageControl.change(messageCriteria(args.destinations as NetworkURN[])) const updatedSubs = this.#updateDestinationSubscriptions(id) - this.#subs[id].destinationSubs = updatedSubs + this.subs[id].destinationSubs = updatedSubs } /** * Updates the subscription to relayed HRMP messages in the relay chain. */ #updateEvents(id: string) { - const { descriptor, args, relaySub } = this.#subs[id] + const { descriptor, args, relaySub } = this.subs[id] if (this.#shouldMonitorRelay(args) && relaySub === undefined) { try { - this.#subs[id].relaySub = this.#monitorRelay(descriptor, args) + this.subs[id].relaySub = this.#monitorRelay(descriptor, args) } catch (error) { // log instead of throw to not block OD subscriptions - this.#log.error(error, 'Error on relay subscription (%s)', id) + this.log.error(error, 'Error on relay subscription (%s)', id) } } else if (!this.#shouldMonitorRelay(args) && relaySub !== undefined) { relaySub.sub.unsubscribe() - delete this.#subs[id].relaySub + delete this.subs[id].relaySub } } #validateChainIds(chainIds: NetworkURN[]) { chainIds.forEach((chainId) => { - if (!this.#ingress.isNetworkDefined(chainId)) { + if (!this.ingress.isNetworkDefined(chainId)) { throw new ValidationError('Invalid chain id:' + chainId) } }) From 34a7727ad1be039f4c9dc4b65992a666e77f409d Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Wed, 29 May 2024 13:25:19 +0200 Subject: [PATCH 18/58] move AgentId type --- packages/server/src/services/agents/api.ts | 2 +- .../server/src/services/agents/base/base-agent.ts | 10 +++++----- packages/server/src/services/agents/local.ts | 4 ++-- packages/server/src/services/agents/types.ts | 8 +++++++- packages/server/src/services/agents/xcm/xcm-agent.ts | 11 ++--------- packages/server/src/services/persistence/subs.ts | 3 ++- .../server/src/services/subscriptions/api/routes.ts | 3 ++- .../src/services/subscriptions/api/ws/plugin.ts | 2 +- .../src/services/subscriptions/api/ws/protocol.ts | 3 ++- .../server/src/services/subscriptions/switchboard.ts | 4 ++-- packages/server/src/services/subscriptions/types.ts | 7 +------ packages/server/src/services/types.ts | 4 ++-- 12 files changed, 29 insertions(+), 32 deletions(-) diff --git a/packages/server/src/services/agents/api.ts b/packages/server/src/services/agents/api.ts index 9794bc02..02f33cd3 100644 --- a/packages/server/src/services/agents/api.ts +++ b/packages/server/src/services/agents/api.ts @@ -1,7 +1,7 @@ import { FastifyInstance } from 'fastify' import { zodToJsonSchema } from 'zod-to-json-schema' -import { $AgentId, AgentId } from '../subscriptions/types.js' +import { $AgentId, AgentId } from './types.js' /** * Agents HTTP API diff --git a/packages/server/src/services/agents/base/base-agent.ts b/packages/server/src/services/agents/base/base-agent.ts index 45586a16..732f466e 100644 --- a/packages/server/src/services/agents/base/base-agent.ts +++ b/packages/server/src/services/agents/base/base-agent.ts @@ -1,14 +1,14 @@ -import { z } from 'zod' +import { extractEvents, extractTxWithEvents, flattenCalls, types } from '@sodazone/ocelloids-sdk' import { Operation } from 'rfc6902' import { Observable, from, share } from 'rxjs' -import { extractEvents, extractTxWithEvents, flattenCalls, types } from '@sodazone/ocelloids-sdk' +import { z } from 'zod' -import { Logger, NetworkURN } from '../../types.js' import { IngressConsumer } from '../../ingress/index.js' import { NotifierHub } from '../../notification/hub.js' import { SubsStore } from '../../persistence/subs.js' -import { AgentId, HexString, Subscription } from '../../subscriptions/types.js' -import { Agent, AgentMetadata, AgentRuntimeContext } from '../types.js' +import { HexString, Subscription } from '../../subscriptions/types.js' +import { Logger, NetworkURN } from '../../types.js' +import { Agent, AgentId, AgentMetadata, AgentRuntimeContext } from '../types.js' import { GetStorageAt } from '../xcm/types-augmented.js' type SubscriptionHandler = { diff --git a/packages/server/src/services/agents/local.ts b/packages/server/src/services/agents/local.ts index 339a73f0..7a0fda82 100644 --- a/packages/server/src/services/agents/local.ts +++ b/packages/server/src/services/agents/local.ts @@ -3,8 +3,8 @@ import { AgentServiceOptions } from '../../types.js' import { Logger, Services } from '../index.js' import { NotifierHub } from '../notification/index.js' import { NotifierEvents } from '../notification/types.js' -import { AgentId, NotificationListener, Subscription } from '../subscriptions/types.js' -import { Agent, AgentRuntimeContext, AgentService } from './types.js' +import { NotificationListener, Subscription } from '../subscriptions/types.js' +import { Agent, AgentId, AgentRuntimeContext, AgentService } from './types.js' import { XCMAgent } from './xcm/xcm-agent.js' /** diff --git a/packages/server/src/services/agents/types.ts b/packages/server/src/services/agents/types.ts index 9c617afa..03edd31d 100644 --- a/packages/server/src/services/agents/types.ts +++ b/packages/server/src/services/agents/types.ts @@ -7,9 +7,15 @@ import { NotifierHub } from '../notification/hub.js' import { NotifierEvents } from '../notification/types.js' import { Janitor } from '../persistence/janitor.js' import { SubsStore } from '../persistence/subs.js' -import { AgentId, NotificationListener, Subscription } from '../subscriptions/types.js' +import { NotificationListener, Subscription } from '../subscriptions/types.js' import { DB, Logger } from '../types.js' +export const $AgentId = z.string({ + required_error: 'agent id is required', +}) + +export type AgentId = z.infer + export type AgentRuntimeContext = { log: Logger notifier: NotifierHub diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts index eb6d014a..e00c1367 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -4,14 +4,7 @@ import { Operation, applyPatch } from 'rfc6902' import { Observable, filter, from, map, switchMap } from 'rxjs' import { z } from 'zod' -import { - $Subscription, - AgentId, - AnyJson, - HexString, - RxSubscriptionWithId, - Subscription, -} from '../../subscriptions/types.js' +import { $Subscription, AnyJson, HexString, RxSubscriptionWithId, Subscription } from '../../subscriptions/types.js' import { NetworkURN } from '../../types.js' import { extractXcmpReceive, extractXcmpSend } from './ops/xcmp.js' import { @@ -112,7 +105,7 @@ export class XCMAgent extends BaseAgent { get metadata(): AgentMetadata { return { id: 'xcm', - name: 'XCM Agent' + name: 'XCM Agent', } } diff --git a/packages/server/src/services/persistence/subs.ts b/packages/server/src/services/persistence/subs.ts index 3de686e4..f02a52ba 100644 --- a/packages/server/src/services/persistence/subs.ts +++ b/packages/server/src/services/persistence/subs.ts @@ -1,5 +1,6 @@ import { NotFound, ValidationError } from '../../errors.js' -import { AgentId, Subscription } from '../subscriptions/types.js' +import { AgentId } from '../agents/types.js' +import { Subscription } from '../subscriptions/types.js' import { DB, Logger, jsonEncoded, prefixes } from '../types.js' /** diff --git a/packages/server/src/services/subscriptions/api/routes.ts b/packages/server/src/services/subscriptions/api/routes.ts index ef40e19b..5ca3f3e6 100644 --- a/packages/server/src/services/subscriptions/api/routes.ts +++ b/packages/server/src/services/subscriptions/api/routes.ts @@ -2,7 +2,8 @@ import { FastifyInstance } from 'fastify' import { Operation } from 'rfc6902' import { zodToJsonSchema } from 'zod-to-json-schema' -import { $AgentId, $SafeId, $Subscription, AgentId, Subscription } from '../types.js' +import { $AgentId, AgentId } from '../../agents/types.js' +import { $SafeId, $Subscription, Subscription } from '../types.js' import $JSONPatch from './json-patch.js' /** diff --git a/packages/server/src/services/subscriptions/api/ws/plugin.ts b/packages/server/src/services/subscriptions/api/ws/plugin.ts index be5aa721..c76badc6 100644 --- a/packages/server/src/services/subscriptions/api/ws/plugin.ts +++ b/packages/server/src/services/subscriptions/api/ws/plugin.ts @@ -1,7 +1,7 @@ import { FastifyPluginAsync } from 'fastify' import fp from 'fastify-plugin' -import { AgentId } from '../../types.js' +import { AgentId } from '../../../agents/types.js' import WebsocketProtocol from './protocol.js' declare module 'fastify' { diff --git a/packages/server/src/services/subscriptions/api/ws/protocol.ts b/packages/server/src/services/subscriptions/api/ws/protocol.ts index dd167e4e..5496bc8f 100644 --- a/packages/server/src/services/subscriptions/api/ws/protocol.ts +++ b/packages/server/src/services/subscriptions/api/ws/protocol.ts @@ -6,11 +6,12 @@ import { ulid } from 'ulidx' import { z } from 'zod' import { errorMessage } from '../../../../errors.js' +import { AgentId } from '../../../agents/types.js' import { NotifyMessage } from '../../../notification/types.js' import { TelemetryEventEmitter, notifyTelemetryFrom } from '../../../telemetry/types.js' import { Logger } from '../../../types.js' import { Switchboard } from '../../switchboard.js' -import { $Subscription, AgentId, NotificationListener, Subscription } from '../../types.js' +import { $Subscription, NotificationListener, Subscription } from '../../types.js' import { WebsocketProtocolOptions } from './plugin.js' const $EphemeralSubscription = z diff --git a/packages/server/src/services/subscriptions/switchboard.ts b/packages/server/src/services/subscriptions/switchboard.ts index f246c6a8..0be56a4a 100644 --- a/packages/server/src/services/subscriptions/switchboard.ts +++ b/packages/server/src/services/subscriptions/switchboard.ts @@ -3,9 +3,9 @@ import EventEmitter from 'node:events' import { Operation } from 'rfc6902' import { Logger, Services } from '../types.js' -import { AgentId, NotificationListener, Subscription, SubscriptionStats } from './types.js' +import { NotificationListener, Subscription, SubscriptionStats } from './types.js' -import { AgentService } from '../agents/types.js' +import { AgentId, AgentService } from '../agents/types.js' import { NotifierEvents } from '../notification/types.js' import { TelemetryCollect, TelemetryEventEmitter } from '../telemetry/types.js' diff --git a/packages/server/src/services/subscriptions/types.ts b/packages/server/src/services/subscriptions/types.ts index a59196f3..342d4956 100644 --- a/packages/server/src/services/subscriptions/types.ts +++ b/packages/server/src/services/subscriptions/types.ts @@ -2,6 +2,7 @@ import z from 'zod' import { Subscription as RxSubscription } from 'rxjs' +import { $AgentId } from '../agents/types.js' import { NotifyMessage } from '../notification/types.js' /** @@ -107,12 +108,6 @@ export const $AgentArgs = z.record( z.any() ) -export const $AgentId = z.string({ - required_error: 'agent id is required', -}) - -export type AgentId = z.infer - export const $Subscription = z .object({ id: $SafeId, diff --git a/packages/server/src/services/types.ts b/packages/server/src/services/types.ts index f6a925fc..bcde7819 100644 --- a/packages/server/src/services/types.ts +++ b/packages/server/src/services/types.ts @@ -1,14 +1,14 @@ import { AbstractBatchOperation, AbstractLevel, AbstractSublevel } from 'abstract-level' import { FastifyBaseLogger } from 'fastify' -import { AgentService } from './agents/types.js' +import { AgentId, AgentService } from './agents/types.js' import { ServiceConfiguration } from './config.js' import { IngressConsumer } from './ingress/consumer/index.js' import Connector from './networking/connector.js' import { Janitor } from './persistence/janitor.js' import { Scheduler } from './persistence/scheduler.js' import { SubsStore } from './persistence/subs.js' -import { AgentId, BlockNumberRange, HexString } from './subscriptions/types.js' +import { BlockNumberRange, HexString } from './subscriptions/types.js' export type NetworkURN = `urn:ocn:${string}` From df2944177f090105456c49cc0c9af42947abf86c Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Wed, 29 May 2024 14:27:57 +0200 Subject: [PATCH 19/58] agent telemetry --- .../src/services/agents/base/base-agent.ts | 5 ++ packages/server/src/services/agents/local.ts | 13 ++++++ packages/server/src/services/agents/types.ts | 5 +- .../src/services/agents/xcm/matching.ts | 22 ++++----- .../services/agents/xcm/telemetry/events.ts | 21 +++++---- .../services/agents/xcm/telemetry/metrics.ts | 46 ++++++++++++------- .../src/services/agents/xcm/xcm-agent.ts | 35 ++++++++------ .../src/services/telemetry/metrics/index.ts | 6 +-- .../services/telemetry/metrics/switchboard.ts | 14 +----- .../server/src/services/telemetry/plugin.ts | 7 ++- .../server/src/services/telemetry/types.ts | 6 +-- 11 files changed, 105 insertions(+), 75 deletions(-) diff --git a/packages/server/src/services/agents/base/base-agent.ts b/packages/server/src/services/agents/base/base-agent.ts index 732f466e..c252e92b 100644 --- a/packages/server/src/services/agents/base/base-agent.ts +++ b/packages/server/src/services/agents/base/base-agent.ts @@ -41,6 +41,7 @@ export abstract class BaseAgent implements Agent blockExtrinsics: {}, } } + abstract get metadata(): AgentMetadata get id(): AgentId { @@ -69,6 +70,10 @@ export abstract class BaseAgent implements Agent abstract stop(): Promise abstract start(): Promise + collectTelemetry() { + /* no telemetry */ + } + protected sharedBlockEvents(chainId: NetworkURN): Observable { if (!this.shared.blockEvents[chainId]) { this.shared.blockEvents[chainId] = this.ingress.finalizedBlocks(chainId).pipe(extractEvents(), share()) diff --git a/packages/server/src/services/agents/local.ts b/packages/server/src/services/agents/local.ts index 7a0fda82..1884800f 100644 --- a/packages/server/src/services/agents/local.ts +++ b/packages/server/src/services/agents/local.ts @@ -4,6 +4,7 @@ import { Logger, Services } from '../index.js' import { NotifierHub } from '../notification/index.js' import { NotifierEvents } from '../notification/types.js' import { NotificationListener, Subscription } from '../subscriptions/types.js' +import { TelemetryCollect } from '../telemetry/types.js' import { Agent, AgentId, AgentRuntimeContext, AgentService } from './types.js' import { XCMAgent } from './xcm/xcm-agent.js' @@ -78,6 +79,18 @@ export class LocalAgentService implements AgentService { } } + /** + * Calls the given collect function for each private observable component. + * + * @param collect The collect callback function. + */ + collectTelemetry() { + for (const [id, agent] of Object.entries(this.#agents)) { + this.#log.info('[local:agents] collect telemetry from agent %s', id) + agent.collectTelemetry() + } + } + #loadAgents(ctx: AgentRuntimeContext) { const xcm = new XCMAgent(ctx) return { diff --git a/packages/server/src/services/agents/types.ts b/packages/server/src/services/agents/types.ts index 03edd31d..169b48e0 100644 --- a/packages/server/src/services/agents/types.ts +++ b/packages/server/src/services/agents/types.ts @@ -1,6 +1,5 @@ -import { z } from 'zod' - import { Operation } from 'rfc6902' +import { z } from 'zod' import { IngressConsumer } from '../ingress/index.js' import { NotifierHub } from '../notification/hub.js' @@ -33,6 +32,7 @@ export interface AgentService { getAgentIds(): AgentId[] start(): Promise stop(): Promise + collectTelemetry(): void } export type AgentMetadata = { @@ -42,6 +42,7 @@ export type AgentMetadata = { } export interface Agent { + collectTelemetry(): void get id(): AgentId get metadata(): AgentMetadata getSubscriptionById(subscriptionId: string): Promise diff --git a/packages/server/src/services/agents/xcm/matching.ts b/packages/server/src/services/agents/xcm/matching.ts index 65b025f1..23cdcdc0 100644 --- a/packages/server/src/services/agents/xcm/matching.ts +++ b/packages/server/src/services/agents/xcm/matching.ts @@ -722,7 +722,7 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEvent } #onXcmOutbound(outMsg: XcmSent) { - this.emit('telemetryOutbound', outMsg) + this.emit('telemetryXcmOutbound', outMsg) try { this.#xcmMatchedReceiver(outMsg) @@ -732,9 +732,9 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEvent } #onXcmMatched(outMsg: XcmSent, inMsg: XcmInbound) { - this.emit('telemetryMatched', inMsg, outMsg) + this.emit('telemetryXcmMatched', inMsg, outMsg) if (inMsg.assetsTrapped !== undefined) { - this.emit('telemetryTrapped', inMsg, outMsg) + this.emit('telemetryXcmTrapped', inMsg, outMsg) } try { @@ -747,7 +747,7 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEvent #onXcmRelayed(outMsg: XcmSent, relayMsg: XcmRelayedWithContext) { const message: XcmRelayed = new GenericXcmRelayed(outMsg, relayMsg) - this.emit('telemetryRelayed', message) + this.emit('telemetryXcmRelayed', message) try { this.#xcmMatchedReceiver(message) @@ -777,7 +777,7 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEvent } const message: XcmHop = new GenericXcmHop(originMsg, waypointContext, 'out') - this.emit('telemetryHop', message) + this.emit('telemetryXcmHop', message) this.#xcmMatchedReceiver(message) } catch (e) { @@ -790,7 +790,7 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEvent // since we are not storing messages or contexts of intermediate hops #onXcmHopIn(originMsg: XcmSent, hopMsg: XcmInbound) { if (hopMsg.assetsTrapped !== undefined) { - this.emit('telemetryTrapped', hopMsg, originMsg) + this.emit('telemetryXcmTrapped', hopMsg, originMsg) } try { @@ -812,7 +812,7 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEvent } const message: XcmHop = new GenericXcmHop(originMsg, waypointContext, 'in') - this.emit('telemetryHop', message) + this.emit('telemetryXcmHop', message) this.#xcmMatchedReceiver(message) } catch (e) { @@ -821,7 +821,7 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEvent } #onXcmBridgeAccepted(bridgeAcceptedMsg: XcmBridge) { - this.emit('telemetryBridge', bridgeAcceptedMsg) + this.emit('telemetryXcmBridge', bridgeAcceptedMsg) try { this.#xcmMatchedReceiver(bridgeAcceptedMsg) } catch (e) { @@ -830,7 +830,7 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEvent } #onXcmBridgeDelivered(bridgeDeliveredMsg: XcmBridge) { - this.emit('telemetryBridge', bridgeDeliveredMsg) + this.emit('telemetryXcmBridge', bridgeDeliveredMsg) try { this.#xcmMatchedReceiver(bridgeDeliveredMsg) } catch (e) { @@ -861,7 +861,7 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEvent forwardId: bridgeOutMsg.forwardId, }) - this.emit('telemetryBridge', bridgeMatched) + this.emit('telemetryXcmBridge', bridgeMatched) this.#xcmMatchedReceiver(bridgeMatched) } catch (e) { @@ -875,7 +875,7 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEvent const outMsg = JSON.parse(msg) as XcmSent const message: XcmTimeout = new GenericXcmTimeout(outMsg) this.#log.debug('TIMEOUT on key %s', task.key) - this.emit('telemetryTimeout', message) + this.emit('telemetryXcmTimeout', message) this.#xcmMatchedReceiver(message) } } catch (e) { diff --git a/packages/server/src/services/agents/xcm/telemetry/events.ts b/packages/server/src/services/agents/xcm/telemetry/events.ts index 37ae2ec4..4b91b1d6 100644 --- a/packages/server/src/services/agents/xcm/telemetry/events.ts +++ b/packages/server/src/services/agents/xcm/telemetry/events.ts @@ -2,14 +2,19 @@ import { TypedEventEmitter } from '../../../types.js' import { XcmBridge, XcmHop, XcmInbound, XcmRelayed, XcmSent, XcmTimeout } from '../types.js' export type TelemetryEvents = { - telemetryInbound: (message: XcmInbound) => void - telemetryOutbound: (message: XcmSent) => void - telemetryRelayed: (relayMsg: XcmRelayed) => void - telemetryMatched: (inMsg: XcmInbound, outMsg: XcmSent) => void - telemetryTimeout: (message: XcmTimeout) => void - telemetryHop: (message: XcmHop) => void - telemetryBridge: (message: XcmBridge) => void - telemetryTrapped: (inMsg: XcmInbound, outMsg: XcmSent) => void + telemetryXcmInbound: (message: XcmInbound) => void + telemetryXcmOutbound: (message: XcmSent) => void + telemetryXcmRelayed: (relayMsg: XcmRelayed) => void + telemetryXcmMatched: (inMsg: XcmInbound, outMsg: XcmSent) => void + telemetryXcmTimeout: (message: XcmTimeout) => void + telemetryXcmHop: (message: XcmHop) => void + telemetryXcmBridge: (message: XcmBridge) => void + telemetryXcmTrapped: (inMsg: XcmInbound, outMsg: XcmSent) => void + telemetryXcmSubscriptionError: (msg: { + subscriptionId: string + chainId: string + direction: 'in' | 'out' | 'relay' | 'bridge' + }) => void } export type TelemetryXCMEventEmitter = TypedEventEmitter diff --git a/packages/server/src/services/agents/xcm/telemetry/metrics.ts b/packages/server/src/services/agents/xcm/telemetry/metrics.ts index 2414b381..e9d3d25e 100644 --- a/packages/server/src/services/agents/xcm/telemetry/metrics.ts +++ b/packages/server/src/services/agents/xcm/telemetry/metrics.ts @@ -3,63 +3,75 @@ import { Counter } from 'prom-client' import { XcmBridge, XcmHop, XcmInbound, XcmRelayed, XcmSent, XcmTimeout } from '../types.js' import { TelemetryXCMEventEmitter } from './events.js' -export function metrics(source: TelemetryXCMEventEmitter) { +export function xcmAgentMetrics(source: TelemetryXCMEventEmitter) { + const subsErrors = new Counter({ + name: 'oc_xcm_subscription_errors_count', + help: 'Subscription errors', + labelNames: ['id', 'chainId', 'direction'], + }) + + source.on('telemetryXcmSubscriptionError', (msg) => { + subsErrors.labels(msg.subscriptionId, msg.chainId, msg.direction).inc() + }) +} + +export function xcmAgentEngineMetrics(source: TelemetryXCMEventEmitter) { const inCount = new Counter({ - name: 'oc_engine_in_total', + name: 'oc_xcm_engine_in_total', help: 'Matching engine inbound messages.', labelNames: ['subscription', 'origin', 'outcome'], }) const outCount = new Counter({ - name: 'oc_engine_out_total', + name: 'oc_xcm_engine_out_total', help: 'Matching engine outbound messages.', labelNames: ['subscription', 'origin', 'destination'], }) const matchCount = new Counter({ - name: 'oc_engine_matched_total', + name: 'oc_xcm_engine_matched_total', help: 'Matching engine matched messages.', labelNames: ['subscription', 'origin', 'destination', 'outcome'], }) const trapCount = new Counter({ - name: 'oc_engine_trapped_total', + name: 'oc_xcm_engine_trapped_total', help: 'Matching engine matched messages with trapped assets.', labelNames: ['subscription', 'origin', 'destination', 'outcome'], }) const relayCount = new Counter({ - name: 'oc_engine_relayed_total', + name: 'oc_xcm_engine_relayed_total', help: 'Matching engine relayed messages.', labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'outcome'], }) const timeoutCount = new Counter({ - name: 'oc_engine_timeout_total', + name: 'oc_xcm_engine_timeout_total', help: 'Matching engine sent timeout messages.', labelNames: ['subscription', 'origin', 'destination'], }) const hopCount = new Counter({ - name: 'oc_engine_hop_total', + name: 'oc_xcm_engine_hop_total', help: 'Matching engine hop messages.', labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'stop', 'outcome', 'direction'], }) const bridgeCount = new Counter({ - name: 'oc_engine_bridge_total', + name: 'oc_xcm_engine_bridge_total', help: 'Matching engine bridge messages.', labelNames: ['subscription', 'origin', 'destination', 'legIndex', 'stop', 'outcome', 'direction'], }) - source.on('telemetryInbound', (message: XcmInbound) => { + source.on('telemetryXcmInbound', (message: XcmInbound) => { inCount.labels(message.subscriptionId, message.chainId, message.outcome.toString()).inc() }) - source.on('telemetryOutbound', (message: XcmSent) => { + source.on('telemetryXcmOutbound', (message: XcmSent) => { outCount.labels(message.subscriptionId, message.origin.chainId, message.destination.chainId).inc() }) - source.on('telemetryMatched', (inMsg: XcmInbound, outMsg: XcmSent) => { + source.on('telemetryXcmMatched', (inMsg: XcmInbound, outMsg: XcmSent) => { matchCount .labels(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.destination.chainId, inMsg.outcome.toString()) .inc() }) - source.on('telemetryRelayed', (relayMsg: XcmRelayed) => { + source.on('telemetryXcmRelayed', (relayMsg: XcmRelayed) => { relayCount .labels( relayMsg.subscriptionId, @@ -71,11 +83,11 @@ export function metrics(source: TelemetryXCMEventEmitter) { .inc() }) - source.on('telemetryTimeout', (msg: XcmTimeout) => { + source.on('telemetryXcmTimeout', (msg: XcmTimeout) => { timeoutCount.labels(msg.subscriptionId, msg.origin.chainId, msg.destination.chainId).inc() }) - source.on('telemetryHop', (msg: XcmHop) => { + source.on('telemetryXcmHop', (msg: XcmHop) => { hopCount .labels( msg.subscriptionId, @@ -89,7 +101,7 @@ export function metrics(source: TelemetryXCMEventEmitter) { .inc() }) - source.on('telemetryBridge', (msg: XcmBridge) => { + source.on('telemetryXcmBridge', (msg: XcmBridge) => { bridgeCount .labels( msg.subscriptionId, @@ -103,7 +115,7 @@ export function metrics(source: TelemetryXCMEventEmitter) { .inc() }) - source.on('telemetryTrapped', (inMsg: XcmInbound, outMsg: XcmSent) => { + source.on('telemetryXcmTrapped', (inMsg: XcmInbound, outMsg: XcmSent) => { trapCount .labels(outMsg.subscriptionId, outMsg.origin.chainId, outMsg.destination.chainId, inMsg.outcome.toString()) .inc() diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts index e00c1367..de96abd1 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -34,6 +34,7 @@ import { extractDmpReceive, extractDmpSend, extractDmpSendByEvent } from './ops/ import { extractRelayReceive } from './ops/relay.js' import { extractUmpReceive, extractUmpSend } from './ops/ump.js' +import { EventEmitter } from 'node:events' import { getChainId, getConsensus } from '../../config.js' import { dmpDownwardMessageQueuesKey, @@ -44,6 +45,8 @@ import { BaseAgent } from '../base/base-agent.js' import { AgentMetadata, AgentRuntimeContext } from '../types.js' import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './ops/pk-bridge.js' import { getBridgeHubNetworkId } from './ops/util.js' +import { TelemetryXCMEventEmitter } from './telemetry/events.js' +import { xcmAgentMetrics, xcmAgentEngineMetrics as xcmMatchingEngineMetrics } from './telemetry/metrics.js' import { GetDownwardMessageQueues, GetOutboundHrmpMessages, GetOutboundUmpMessages } from './types-augmented.js' const SUB_ERROR_RETRY_MS = 5000 @@ -57,10 +60,13 @@ function hasOp(patch: Operation[], path: string) { export class XCMAgent extends BaseAgent { protected readonly subs: Record = {} readonly #engine: MatchingEngine + readonly #telemetry: TelemetryXCMEventEmitter constructor(ctx: AgentRuntimeContext) { super(ctx) + this.#engine = new MatchingEngine(ctx, this.#onXcmWaypointReached) + this.#telemetry = new (EventEmitter as new () => TelemetryXCMEventEmitter)() } async update(subscriptionId: string, patch: Operation[]): Promise { @@ -109,6 +115,11 @@ export class XCMAgent extends BaseAgent { } } + override collectTelemetry(): void { + xcmAgentMetrics(this.#telemetry) + xcmMatchingEngineMetrics(this.#engine) + } + async subscribe(s: Subscription): Promise { const args = $XCMSubscriptionArgs.parse(s.args) @@ -294,11 +305,11 @@ export class XCMAgent extends BaseAgent { error: (error: any) => { this.log.error(error, '[%s] error on destination subscription %s', chainId, id) - /*this.emit('telemetrySubscriptionError', { + this.#telemetry.emit('telemetryXcmSubscriptionError', { subscriptionId: id, chainId, direction: 'in', - })*/ + }) // try recover inbound subscription if (this.subs[id]) { @@ -385,12 +396,11 @@ export class XCMAgent extends BaseAgent { const outboundObserver = { error: (error: any) => { this.log.error(error, '[%s] error on origin subscription %s', chainId, id) - /* - this.emit('telemetrySubscriptionError', { + this.#telemetry.emit('telemetryXcmSubscriptionError', { subscriptionId: id, chainId, direction: 'out', - })*/ + }) // try recover outbound subscription // note: there is a single origin per outbound @@ -519,12 +529,11 @@ export class XCMAgent extends BaseAgent { const relayObserver = { error: (error: any) => { this.log.error(error, '[%s] error on relay subscription s', chainId, id) - /* - this.emit('telemetrySubscriptionError', { + this.#telemetry.emit('telemetryXcmSubscriptionError', { subscriptionId: id, chainId, direction: 'relay', - })*/ + }) // try recover relay subscription // there is only one subscription per subscription ID for relay @@ -601,11 +610,11 @@ export class XCMAgent extends BaseAgent { const pkBridgeObserver = { error: (error: any) => { this.log.error(error, '[%s] error on PK bridge subscription s', originBridgeHub, id) - // this.emit('telemetrySubscriptionError', { - // subscriptionId: id, - // chainId: originBridgeHub, - // direction: 'bridge', - // }); + this.#telemetry.emit('telemetryXcmSubscriptionError', { + subscriptionId: id, + chainId: originBridgeHub, + direction: 'bridge', + }) // try recover pk bridge subscription if (this.subs[id]) { diff --git a/packages/server/src/services/telemetry/metrics/index.ts b/packages/server/src/services/telemetry/metrics/index.ts index d4115e52..e793cc63 100644 --- a/packages/server/src/services/telemetry/metrics/index.ts +++ b/packages/server/src/services/telemetry/metrics/index.ts @@ -2,21 +2,17 @@ import { IngressConsumer } from '../../ingress/index.js' import IngressProducer from '../../ingress/producer/index.js' import { HeadCatcher } from '../../ingress/watcher/head-catcher.js' import { NotifierHub } from '../../notification/hub.js' -import { Switchboard } from '../../subscriptions/switchboard.js' import { TelemetryEventEmitter } from '../types.js' import { catcherMetrics } from './catcher.js' import { ingressConsumerMetrics, ingressProducerMetrics } from './ingress.js' import { notifierMetrics } from './notifiers.js' -import { switchboardMetrics } from './switchboard.js' function isIngressConsumer(o: TelemetryEventEmitter): o is IngressConsumer { return 'finalizedBlocks' in o && 'getRegistry' in o } export function collect(observer: TelemetryEventEmitter) { - if (observer instanceof Switchboard) { - switchboardMetrics(observer) - } else if (observer instanceof HeadCatcher) { + if (observer instanceof HeadCatcher) { catcherMetrics(observer) } else if (observer instanceof NotifierHub) { notifierMetrics(observer) diff --git a/packages/server/src/services/telemetry/metrics/switchboard.ts b/packages/server/src/services/telemetry/metrics/switchboard.ts index 154954e9..3fef53f1 100644 --- a/packages/server/src/services/telemetry/metrics/switchboard.ts +++ b/packages/server/src/services/telemetry/metrics/switchboard.ts @@ -1,19 +1,7 @@ -import { Counter, Gauge } from 'prom-client' +import { Gauge } from 'prom-client' import { Switchboard } from '../../subscriptions/switchboard.js' -export function switchboardMetrics(switchboard: Switchboard) { - const subsErrors = new Counter({ - name: 'oc_subscription_errors_count', - help: 'Subscription errors', - labelNames: ['id', 'chainId', 'direction'], - }) - - switchboard.on('telemetrySubscriptionError', (msg) => { - subsErrors.labels(msg.subscriptionId, msg.chainId, msg.direction).inc() - }) -} - export function collectSwitchboardStats(switchboard: Switchboard) { const subsGauge = new Gauge({ name: 'oc_active_subscriptions_count', diff --git a/packages/server/src/services/telemetry/plugin.ts b/packages/server/src/services/telemetry/plugin.ts index 46c3906a..2e879c4f 100644 --- a/packages/server/src/services/telemetry/plugin.ts +++ b/packages/server/src/services/telemetry/plugin.ts @@ -28,7 +28,7 @@ type PullCollect = () => Promise * @param options - The telemetry options */ const telemetryPlugin: FastifyPluginAsync = async (fastify, options) => { - const { log, switchboard, wsProtocol, rootStore, ingressConsumer, ingressProducer } = fastify + const { log, switchboard, wsProtocol, rootStore, ingressConsumer, ingressProducer, agentService } = fastify if (options.telemetry) { log.info('Enable default metrics') @@ -62,6 +62,11 @@ const telemetryPlugin: FastifyPluginAsync = async (fastify, op ingressProducer.collectTelemetry(collect) } + if (agentService) { + log.info('Enable agent metrics') + agentService.collectTelemetry() + } + fastify.addHook('onResponse', createReplyHook()) fastify.get( diff --git a/packages/server/src/services/telemetry/types.ts b/packages/server/src/services/telemetry/types.ts index 2f9c145a..451c7485 100644 --- a/packages/server/src/services/telemetry/types.ts +++ b/packages/server/src/services/telemetry/types.ts @@ -1,5 +1,6 @@ import type { Header } from '@polkadot/types/interfaces' +import EventEmitter from 'events' import { NotifyMessage } from '../notification/types.js' import { Subscription } from '../subscriptions/types.js' import { TypedEventEmitter } from '../types.js' @@ -52,11 +53,6 @@ export type TelemetryEvents = { telemetryBlockFinalized: (msg: { chainId: string; header: Header }) => void telemetryBlockCacheHit: (msg: { chainId: string }) => void telemetrySocketListener: (ip: string, sub: Subscription, close?: boolean) => void - telemetrySubscriptionError: (msg: { - subscriptionId: string - chainId: string - direction: 'in' | 'out' | 'relay' - }) => void telemetryHeadCatcherError: (msg: { chainId: string; method: string }) => void telemetryBlockCacheError: (msg: { chainId: string; method: string }) => void } & TelemetryNotifierEvents & From 112b2e9662521bdf8d6743f22a7028b8f76f434c Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Wed, 29 May 2024 15:31:22 +0200 Subject: [PATCH 20/58] unnessesary redeclare --- packages/server/src/services/agents/xcm/xcm-agent.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts index de96abd1..615b122e 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -58,7 +58,6 @@ function hasOp(patch: Operation[], path: string) { } export class XCMAgent extends BaseAgent { - protected readonly subs: Record = {} readonly #engine: MatchingEngine readonly #telemetry: TelemetryXCMEventEmitter From 2ecb6da1b545ea7b49c721fb545996975ea8219b Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 17:19:29 +0200 Subject: [PATCH 21/58] client lib support #86 --- packages/client/deno-build.mjs | 2 +- packages/client/src/client.spec.ts | 242 ++++++++++-------- packages/client/src/client.ts | 79 +++--- packages/client/src/lib.ts | 5 + packages/client/src/server-types.ts | 15 +- packages/client/src/types.ts | 110 +++----- packages/client/src/xcm/types.ts | 70 +++++ packages/client/test/bun/run.ts | 25 +- packages/client/test/deno/run.ts | 29 ++- packages/server/src/lib.ts | 28 +- .../server/src/services/agents/xcm/lib.ts | 16 ++ .../server/src/services/notification/types.ts | 5 + .../services/subscriptions/api/ws/protocol.ts | 4 +- 13 files changed, 368 insertions(+), 262 deletions(-) create mode 100644 packages/client/src/xcm/types.ts create mode 100644 packages/server/src/services/agents/xcm/lib.ts diff --git a/packages/client/deno-build.mjs b/packages/client/deno-build.mjs index d6070ca3..5b8000a2 100644 --- a/packages/client/deno-build.mjs +++ b/packages/client/deno-build.mjs @@ -31,7 +31,7 @@ const denoLibRoot = join(distRoot, "deno"); const replacements = { "isows": "// native", - "xcmon-server": "export type * from './ocelloids-client.d.ts';\n" + "@sodazone/ocelloids-service-node": "export type * from './ocelloids-client.d.ts';\n" }; const walkAndBuild = (/** @type string */ dir) => { diff --git a/packages/client/src/client.spec.ts b/packages/client/src/client.spec.ts index 1e39e4b8..ac740418 100644 --- a/packages/client/src/client.spec.ts +++ b/packages/client/src/client.spec.ts @@ -15,7 +15,8 @@ jest.unstable_mockModule('isows', () => { //[!] important: requires dynamic imports const { OcelloidsClient } = await import('./client') -const { isXcmRelayed, isSubscriptionError, isXcmSent, isXcmReceived } = await import('./lib') +const { isSubscriptionError } = await import('./lib') +const { isXcmReceived, isXcmSent, isXcmRelayed } = await import('./xcm/types') describe('OcelloidsClient', () => { it('should create a client instance', () => { @@ -35,7 +36,7 @@ describe('OcelloidsClient', () => { }) it('should connect to a subscription', (done) => { - const wsUrl = 'ws://mock/ws/subs/subid' + const wsUrl = 'ws://mock/ws/subs/agentid/subid' mockServer = new Server(wsUrl, { mock: false }) const samplesNum = samples.length @@ -51,27 +52,33 @@ describe('OcelloidsClient', () => { }) let called = 0 - const ws = client.subscribe('subid', { - onMessage: (msg) => { - expect(ws.readyState).toBe(1) - expect(msg).toBeDefined() - - switch (called) { - case 1: - expect(isXcmSent(msg)).toBeTruthy() - break - case 2: - expect(isXcmReceived(msg)).toBeTruthy() - break - default: - // - } - - if (++called === samplesNum) { - done() - } + const ws = client.subscribe( + { + subscriptionId: 'subid', + agentId: 'agentid', }, - }) + { + onMessage: (msg) => { + expect(ws.readyState).toBe(1) + expect(msg).toBeDefined() + + switch (called) { + case 1: + expect(isXcmSent(msg)).toBeTruthy() + break + case 2: + expect(isXcmReceived(msg)).toBeTruthy() + break + default: + // + } + + if (++called === samplesNum) { + done() + } + }, + } + ) }) it('should create on-demand subscription', (done) => { @@ -92,16 +99,19 @@ describe('OcelloidsClient', () => { const ws = client.subscribe( { - origin: 'urn:ocn:local:2004', - senders: '*', - events: '*', - destinations: [ - 'urn:ocn:local:0', - 'urn:ocn:local:1000', - 'urn:ocn:local:2000', - 'urn:ocn:local:2034', - 'urn:ocn:local:2104', - ], + agent: 'xcm', + args: { + origin: 'urn:ocn:local:2004', + senders: '*', + events: '*', + destinations: [ + 'urn:ocn:local:0', + 'urn:ocn:local:1000', + 'urn:ocn:local:2000', + 'urn:ocn:local:2034', + 'urn:ocn:local:2104', + ], + }, }, { onMessage: (msg) => { @@ -113,7 +123,7 @@ describe('OcelloidsClient', () => { }, { onSubscriptionCreated: (sub) => { - expect(sub.origin).toBe('urn:ocn:local:2004') + expect(sub.agent).toBe('xcm') }, } ) @@ -149,16 +159,19 @@ describe('OcelloidsClient', () => { const ws = client.subscribe( { - origin: 'urn:ocn:local:2004', - senders: '*', - events: '*', - destinations: [ - 'urn:ocn:local:0', - 'urn:ocn:local:1000', - 'urn:ocn:local:2000', - 'urn:ocn:local:2034', - 'urn:ocn:local:2104', - ], + agent: 'xcm', + args: { + origin: 'urn:ocn:local:2004', + senders: '*', + events: '*', + destinations: [ + 'urn:ocn:local:0', + 'urn:ocn:local:1000', + 'urn:ocn:local:2000', + 'urn:ocn:local:2034', + 'urn:ocn:local:2104', + ], + }, }, { onMessage: (_) => { @@ -177,7 +190,7 @@ describe('OcelloidsClient', () => { }) it('should handle socket closed', (done) => { - const wsUrl = 'ws://mock/ws/subs/subid' + const wsUrl = 'ws://mock/ws/subs/agentid/subid' mockServer = new Server(wsUrl, { mock: false }) mockServer.on('connection', (socket) => { @@ -193,22 +206,28 @@ describe('OcelloidsClient', () => { httpUrl: 'https://rpc.abc', }) - client.subscribe('subid', { - onMessage: (_) => { - fail('should not receive messages') - }, - onError: (_) => { - fail('should not receive error') - }, - onClose: (e) => { - expect(e).toBeDefined() - done() + client.subscribe( + { + agentId: 'agentid', + subscriptionId: 'subid', }, - }) + { + onMessage: (_) => { + fail('should not receive messages') + }, + onError: (_) => { + fail('should not receive error') + }, + onClose: (e) => { + expect(e).toBeDefined() + done() + }, + } + ) }) it('should authentitcate', (done) => { - const wsUrl = 'ws://mock/ws/subs/subid' + const wsUrl = 'ws://mock/ws/subs/agentid/subid' mockServer = new Server(wsUrl, { mock: false }) mockServer.on('connection', (socket) => { @@ -225,21 +244,27 @@ describe('OcelloidsClient', () => { httpUrl: 'https://rpc.abc', }) - client.subscribe('subid', { - onMessage: (_) => { - done() - }, - onError: (_) => { - fail('should not receive error') - }, - onClose: (_) => { - fail('should not receive close') + client.subscribe( + { + agentId: 'agentid', + subscriptionId: 'subid', }, - }) + { + onMessage: (_) => { + done() + }, + onError: (_) => { + fail('should not receive error') + }, + onClose: (_) => { + fail('should not receive close') + }, + } + ) }) it('should handle auth error', (done) => { - const wsUrl = 'ws://mock/ws/subs/subid' + const wsUrl = 'ws://mock/ws/subs/agentid/subid' mockServer = new Server(wsUrl, { mock: false }) mockServer.on('connection', (socket) => { @@ -254,25 +279,31 @@ describe('OcelloidsClient', () => { httpUrl: 'https://rpc.abc', }) - client.subscribe('subid', { - onMessage: (_) => { - fail('should not receive message') - }, - onAuthError: (r) => { - expect(r.error).toBeTruthy() - done() - }, - onError: (_) => { - fail('should not receive error') - }, - onClose: (_) => { - fail('should not receive close') + client.subscribe( + { + agentId: 'agentid', + subscriptionId: 'subid', }, - }) + { + onMessage: (_) => { + fail('should not receive message') + }, + onAuthError: (r) => { + expect(r.error).toBeTruthy() + done() + }, + onError: (_) => { + fail('should not receive error') + }, + onClose: (_) => { + fail('should not receive close') + }, + } + ) }) it('should throw auth error event', (done) => { - const wsUrl = 'ws://mock/ws/subs/subid' + const wsUrl = 'ws://mock/ws/subs/agentid/subid' mockServer = new Server(wsUrl, { mock: false }) mockServer.on('connection', (socket) => { @@ -287,16 +318,22 @@ describe('OcelloidsClient', () => { httpUrl: 'https://rpc.abc', }) - client.subscribe('subid', { - onMessage: (_) => { - fail('should not receive message') - }, - onError: (error) => { - const authError = error as WsAuthErrorEvent - expect(authError.name).toBe('WsAuthError') - done() + client.subscribe( + { + agentId: 'agentid', + subscriptionId: 'subid', }, - }) + { + onMessage: (_) => { + fail('should not receive message') + }, + onError: (error) => { + const authError = error as WsAuthErrorEvent + expect(authError.name).toBe('WsAuthError') + done() + }, + } + ) }) }) @@ -308,16 +345,19 @@ describe('OcelloidsClient', () => { it('should create a subscription', async () => { const sub = { id: 'my-subscription', - origin: 'urn:ocn:local:2004', - senders: '*', - events: '*', - destinations: [ - 'urn:ocn:local:0', - 'urn:ocn:local:1000', - 'urn:ocn:local:2000', - 'urn:ocn:local:2034', - 'urn:ocn:local:2104', - ], + agent: 'xcm', + args: { + origin: 'urn:ocn:local:2004', + senders: '*', + events: '*', + destinations: [ + 'urn:ocn:local:0', + 'urn:ocn:local:1000', + 'urn:ocn:local:2000', + 'urn:ocn:local:2034', + 'urn:ocn:local:2104', + ], + }, channels: [ { type: 'webhook', diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 4d0478be..9672d90b 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -1,6 +1,6 @@ import { type MessageEvent, WebSocket } from 'isows' -import type { XcmNotifyMessage } from './server-types' +import type { NotifyMessage } from './server-types' import { type AuthReply, type MessageHandler, @@ -8,10 +8,12 @@ import { type OnDemandSubscriptionHandlers, type Subscription, type SubscriptionError, + type SubscriptionIds, type WebSocketHandlers, WsAuthErrorEvent, isSubscription, isSubscriptionError, + isSubscriptionIds, } from './types' /** @@ -44,14 +46,14 @@ function isBlob(value: any): value is Blob { */ class Protocol { readonly #queue: MessageHandler[] = [] - readonly #stream: MessageHandler + readonly #stream: MessageHandler #isStreaming: boolean /** * Constructs a Protocol instance. * @param stream - The message handler for streaming state. */ - constructor(stream: MessageHandler) { + constructor(stream: MessageHandler) { this.#stream = stream this.#isStreaming = false } @@ -70,13 +72,16 @@ class Protocol { * @param event - The message event to handle. */ handle(event: MessageEvent) { + console.log('jkkjkjkjkjkjkjkjkk', event) const ws = event.target as WebSocket let current: MessageHandler if (this.#isStreaming) { + console.log('SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS') current = this.#stream } else { const next = this.#queue.pop() + console.log(next) if (next) { current = next } else { @@ -85,6 +90,7 @@ class Protocol { } } + console.log('MMMMMMMMMMMMM', event.data) if (isBlob(event.data)) { ;(event.data as Blob).text().then((blob) => current(JSON.parse(blob), ws, event)) } else { @@ -112,23 +118,28 @@ class Protocol { * // create a 'long-lived' subscription * const reply = await client.create({ * id: "my-subscription", - * origin: "urn:ocn:polkadot:2004", - * senders: "*", - * events: "*", - * destinations: [ - * "urn:ocn:polkadot:0", - * "urn:ocn:polkadot:1000", - * "urn:ocn:polkadot:2000", - * "urn:ocn:polkadot:2034", - * "urn:ocn:polkadot:2104" - * ], - * channels: [{ - * type: "webhook", - * url: "https://some.webhook" + * agent: "xcm", + * args: { + * origin: "urn:ocn:polkadot:2004", + * senders: "*", + * events: "*", + * destinations: [ + * "urn:ocn:polkadot:0", + * "urn:ocn:polkadot:1000", + * "urn:ocn:polkadot:2000", + * "urn:ocn:polkadot:2034", + * "urn:ocn:polkadot:2104" + * ], * }, - * { - * type: "websocket" - * }] + * channels: [ + * { + * type: "webhook", + * url: "https://some.webhook" + * }, + * { + * type: "websocket" + * } + * ] * }); * * // subscribe to the previously created subscription @@ -151,16 +162,19 @@ class Protocol { * ```typescript * // subscribe on-demand * const ws = client.subscribe({ - * origin: "urn:ocn:polkadot:2004", - * senders: "*", - * events: "*", - * destinations: [ - * "urn:ocn:polkadot:0", - * "urn:ocn:polkadot:1000", - * "urn:ocn:polkadot:2000", - * "urn:ocn:polkadot:2034", - * "urn:ocn:polkadot:2104" - * ] + * agent: "xcm", + * args: { + * origin: "urn:ocn:polkadot:2004", + * senders: "*", + * events: "*", + * destinations: [ + * "urn:ocn:polkadot:0", + * "urn:ocn:polkadot:1000", + * "urn:ocn:polkadot:2000", + * "urn:ocn:polkadot:2034", + * "urn:ocn:polkadot:2104" + * ] + * } * }, { * onMessage: msg => { * if(isXcmReceived(msg)) { @@ -254,14 +268,14 @@ export class OcelloidsClient { * @returns A promise that resolves with the WebSocket instance. */ subscribe( - subscription: string | OnDemandSubscription, + subscription: SubscriptionIds | OnDemandSubscription, handlers: WebSocketHandlers, onDemandHandlers?: OnDemandSubscriptionHandlers ): WebSocket { const url = this.#config.wsUrl + '/ws/subs' - return typeof subscription === 'string' - ? this.#openWebSocket(`${url}/${subscription}`, handlers) + return isSubscriptionIds(subscription) + ? this.#openWebSocket(`${url}/${subscription.agentId}/${subscription.subscriptionId}`, handlers) : this.#openWebSocket(url, handlers, { sub: subscription, onDemandHandlers, @@ -327,6 +341,7 @@ export class OcelloidsClient { const { sub, onDemandHandlers } = onDemandSub ws.send(JSON.stringify(sub)) + protocol.next((msg) => { if (onDemandHandlers?.onSubscriptionCreated && isSubscription(msg)) { onDemandHandlers.onSubscriptionCreated(msg) diff --git a/packages/client/src/lib.ts b/packages/client/src/lib.ts index e1aae539..81a4e988 100644 --- a/packages/client/src/lib.ts +++ b/packages/client/src/lib.ts @@ -9,3 +9,8 @@ export * from './client' export * from './types' export * from './server-types' + +// The "export * as ___" syntax is not supported yet; as a workaround, +// use "import * as ___" with a separate "export { ___ }" declaration +import * as xcm from './xcm/types' +export { xcm } diff --git a/packages/client/src/server-types.ts b/packages/client/src/server-types.ts index 428961d1..ae227eda 100644 --- a/packages/client/src/server-types.ts +++ b/packages/client/src/server-types.ts @@ -1,19 +1,6 @@ export type { AnyJson, HexString, - XcmNotifyMessage, - XcmSent, - XcmReceived, - XcmRelayed, - XcmTimeout, - XcmHop, - XcmTerminus, - XcmTerminusContext, - XcmWaypointContext, - AssetsTrapped, - TrappedAsset, - Leg, + NotifyMessage, SignerData, - legType, - XcmBridge, } from '@sodazone/ocelloids-service-node' diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 56542799..3849b53f 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -1,4 +1,4 @@ -import { XcmNotifyMessage, XcmReceived, XcmRelayed, XcmSent } from './lib' +import { NotifyMessage } from './lib' /** * Represents a {@link Subscription} delivery channel. @@ -22,6 +22,13 @@ export type DeliveryChannel = type: 'websocket' } +/** + * Generic subscription inputs placeholder type. + * + * @public + */ +export type SubscriptionInputs = Record + /** * Represents a persistent subscription. * @@ -29,13 +36,16 @@ export type DeliveryChannel = * ```typescript * { * id: "polkadot-transfers", - * origin: "0", - * senders: "*", - * destinations: [ - * "2000", - * "1000" - * ], - * events: "*", + * agent: "xcm", + * args: { + * origin: "0", + * senders: "*", + * destinations: [ + * "2000", + * "1000" + * ], + * events: "*", + * }, * channels: [ * { * type: "webhook", @@ -58,19 +68,14 @@ export type Subscription = { id: string /** - * The origin chain id. + * The agent id. */ - origin: string + agent: string /** - * An array of sender addresses or '*' for all. + * The specific agent inputs. */ - senders?: '*' | string[] - - /** - * An array of destination chain ids. - */ - destinations: string[] + args: SubscriptionInputs /** * Indicates the persistence preference. @@ -81,13 +86,6 @@ export type Subscription = { * An array of delivery channels. */ channels: DeliveryChannel[] - - /** - * An optional array with the events to deliver. - * Use '*' for all. - * @see {@link XcmNotificationType} for supported event names. - */ - events?: '*' | string[] } /** @@ -106,24 +104,30 @@ export type SubscriptionError = { } /** - * The XCM event types. + * Represents an on-demand subscription. + * + * @public + */ +export type OnDemandSubscription = Omit + +/** + * Subscription identifiers. * * @public */ -export enum XcmNotificationType { - Sent = 'xcm.sent', - Received = 'xcm.received', - Relayed = 'xcm.relayed', - Timeout = 'xcm.timeout', - Hop = 'xcm.hop', +export type SubscriptionIds = { + subscriptionId: string + agentId: string } /** - * Represents an on-demand subscription. + * Guard condition for {@link SubscriptionIds}. * * @public */ -export type OnDemandSubscription = Omit +export function isSubscriptionIds(object: any): object is SubscriptionIds { + return object.subscriptionId !== undefined && object.agentId !== undefined +} /** * Authentication reply. @@ -181,10 +185,10 @@ export type ErrorHandler = (error: Event) => void */ export type WebSocketHandlers = { /** - * Called on every {@link XcmNotifyMessage}. + * Called on every {@link NotifyMessage}. * This is the main message handling callback. */ - onMessage: MessageHandler + onMessage: MessageHandler /** * Called if the authentication fails. @@ -218,14 +222,9 @@ export type OnDemandSubscriptionHandlers = { * * @public */ -export function isSubscription(obj: Subscription | SubscriptionError | XcmNotifyMessage): obj is Subscription { +export function isSubscription(obj: Subscription | SubscriptionError | NotifyMessage): obj is Subscription { const maybeSub = obj as Subscription - return ( - maybeSub.origin !== undefined && - maybeSub.destinations !== undefined && - maybeSub.id !== undefined && - maybeSub.channels !== undefined - ) + return maybeSub.id !== undefined && maybeSub.agent !== undefined && maybeSub.channels !== undefined } /** @@ -237,30 +236,3 @@ export function isSubscriptionError(obj: Subscription | SubscriptionError): obj const maybeError = obj as SubscriptionError return maybeError.issues !== undefined && maybeError.name !== undefined } - -/** - * Guard condition for {@link XcmSent}. - * - * @public - */ -export function isXcmSent(object: any): object is XcmSent { - return object.type !== undefined && object.type === XcmNotificationType.Sent -} - -/** - * Guard condition for {@link XcmReceived}. - * - * @public - */ -export function isXcmReceived(object: any): object is XcmReceived { - return object.type !== undefined && object.type === XcmNotificationType.Received -} - -/** - * Guard condition for {@link XcmRelayed}. - * - * @public - */ -export function isXcmRelayed(object: any): object is XcmRelayed { - return object.type !== undefined && object.type === XcmNotificationType.Relayed -} diff --git a/packages/client/src/xcm/types.ts b/packages/client/src/xcm/types.ts new file mode 100644 index 00000000..7fd4bc36 --- /dev/null +++ b/packages/client/src/xcm/types.ts @@ -0,0 +1,70 @@ +import type { xcm } from '@sodazone/ocelloids-service-node' + +/** + * XCM Agent subscription inputs. + * + * @public + */ +export type XcmSubscriptionInputs = { + /** + * The origin chain id. + */ + origin: string + + /** + * An array of sender addresses or '*' for all. + */ + senders?: '*' | string[] + + /** + * An array of destination chain ids. + */ + destinations: string[] + + /** + * An optional array with the events to deliver. + * Use '*' for all. + * @see {@link xcm.XcmNotificationType} for supported event names. + */ + events?: '*' | string[] +} + +/** + * The XCM event types. + * + * @public + */ +export enum XcmNotificationType { + Sent = 'xcm.sent', + Received = 'xcm.received', + Relayed = 'xcm.relayed', + Timeout = 'xcm.timeout', + Hop = 'xcm.hop', +} + +/** + * Guard condition for {@link xcm.XcmSent}. + * + * @public + */ +export function isXcmSent(object: any): object is xcm.XcmSent { + return object.type !== undefined && object.type === XcmNotificationType.Sent +} + +/** + * Guard condition for {@link xcm.XcmReceived}. + * + * @public + */ +export function isXcmReceived(object: any): object is xcm.XcmReceived { + return object.type !== undefined && object.type === XcmNotificationType.Received +} + +/** + * Guard condition for {@link xcm.XcmRelayed}. + * + * @public + */ +export function isXcmRelayed(object: any): object is xcm.XcmRelayed { + return object.type !== undefined && object.type === XcmNotificationType.Relayed +} diff --git a/packages/client/test/bun/run.ts b/packages/client/test/bun/run.ts index 5a2a4a60..b20b1261 100644 --- a/packages/client/test/bun/run.ts +++ b/packages/client/test/bun/run.ts @@ -1,4 +1,4 @@ -import { OcelloidsClient, isXcmReceived, isXcmSent } from '../..' +import { OcelloidsClient, xcm } from '../..' const client = new OcelloidsClient({ httpUrl: 'http://127.0.0.1:3000', @@ -9,22 +9,21 @@ client.health().then(console.log).catch(console.error) client.subscribe( { - origin: 'urn:ocn:polkadot:2004', - senders: '*', - events: '*', - destinations: [ - 'urn:ocn:polkadot:0', - 'urn:ocn:polkadot:1000', - 'urn:ocn:polkadot:2000', - 'urn:ocn:polkadot:2034', - 'urn:ocn:polkadot:2104', - ], + agent: 'xcm', + args: { + origin: 'urn:ocn:polkadot:0', + senders: '*', + events: '*', + destinations: [ + 'urn:ocn:polkadot:1000' + ], + } }, { onMessage: (msg, ws) => { - if (isXcmReceived(msg)) { + if (xcm.isXcmReceived(msg)) { console.log('RECV', msg.subscriptionId) - } else if (isXcmSent(msg)) { + } else if (xcm.isXcmSent(msg)) { console.log('SENT', msg.subscriptionId) } console.log(msg) diff --git a/packages/client/test/deno/run.ts b/packages/client/test/deno/run.ts index 7a294ddf..5aa765e1 100644 --- a/packages/client/test/deno/run.ts +++ b/packages/client/test/deno/run.ts @@ -1,4 +1,4 @@ -import { OcelloidsClient, isXcmReceived, isXcmSent } from '../../dist/deno/mod.ts' +import { OcelloidsClient, xcm } from '../../dist/deno/mod.ts' const client = new OcelloidsClient({ httpUrl: 'http://127.0.0.1:3000', @@ -9,22 +9,25 @@ client.health().then(console.log).catch(console.error) client.subscribe( { - origin: 'urn:ocn:polkadot:2004', - senders: '*', - events: '*', - destinations: [ - 'urn:ocn:polkadot:0', - 'urn:ocn:polkadot:1000', - 'urn:ocn:polkadot:2000', - 'urn:ocn:polkadot:2034', - 'urn:ocn:polkadot:2104', - ], + agent: 'xcm', + args: { + origin: 'urn:ocn:polkadot:2004', + senders: '*', + events: '*', + destinations: [ + 'urn:ocn:polkadot:0', + 'urn:ocn:polkadot:1000', + 'urn:ocn:polkadot:2000', + 'urn:ocn:polkadot:2034', + 'urn:ocn:polkadot:2104', + ], + } }, { onMessage: (msg, ws) => { - if (isXcmReceived(msg)) { + if (xcm.isXcmReceived(msg)) { console.log('RECV', msg.subscriptionId) - } else if (isXcmSent(msg)) { + } else if (xcm.isXcmSent(msg)) { console.log('SENT', msg.subscriptionId) } console.log(msg) diff --git a/packages/server/src/lib.ts b/packages/server/src/lib.ts index cfaa9c56..bafb7b0e 100644 --- a/packages/server/src/lib.ts +++ b/packages/server/src/lib.ts @@ -8,23 +8,17 @@ export type { SignerData, } from './services/subscriptions/types.js' +export type { NotifyMessage } from './services/notification/types.js' + +// ==================================================================== +// Agent-specific support types +// NOTE: this will be extracted +// ==================================================================== + /** * XCM agent types - * TODO: should be moved */ -export type { - XcmReceived, - XcmRelayed, - XcmSent, - XcmTimeout, - XcmHop, - XcmBridge, - AssetsTrapped, - TrappedAsset, - XcmNotifyMessage, - Leg, - legType, - XcmTerminus, - XcmTerminusContext, - XcmWaypointContext, -} from './services/agents/xcm/types.js' +// The "export * as ___" syntax is not supported yet; as a workaround, +// use "import * as ___" with a separate "export { ___ }" declaration +import * as xcm from './services/agents/xcm/lib.js' +export { xcm } diff --git a/packages/server/src/services/agents/xcm/lib.ts b/packages/server/src/services/agents/xcm/lib.ts new file mode 100644 index 00000000..88231f09 --- /dev/null +++ b/packages/server/src/services/agents/xcm/lib.ts @@ -0,0 +1,16 @@ +export type { + XcmReceived, + XcmRelayed, + XcmSent, + XcmTimeout, + XcmHop, + XcmBridge, + AssetsTrapped, + TrappedAsset, + XcmNotifyMessage, + Leg, + legType, + XcmTerminus, + XcmTerminusContext, + XcmWaypointContext, +} from './types.js' diff --git a/packages/server/src/services/notification/types.ts b/packages/server/src/services/notification/types.ts index 0a551aa6..42ed3046 100644 --- a/packages/server/src/services/notification/types.ts +++ b/packages/server/src/services/notification/types.ts @@ -2,6 +2,11 @@ import { TypedEventEmitter } from '../index.js' import { AnyJson, Subscription } from '../subscriptions/types.js' import { TelemetryNotifierEvents } from '../telemetry/types.js' +/** + * The generic message. + * + * @public + */ export type NotifyMessage = { metadata: { type: string diff --git a/packages/server/src/services/subscriptions/api/ws/protocol.ts b/packages/server/src/services/subscriptions/api/ws/protocol.ts index 5496bc8f..8391ab03 100644 --- a/packages/server/src/services/subscriptions/api/ws/protocol.ts +++ b/packages/server/src/services/subscriptions/api/ws/protocol.ts @@ -111,10 +111,10 @@ export default class WebsocketProtocol extends (EventEmitter as new () => Teleme try { if (ids === undefined) { - let resolvedId: { id: string; agent: AgentId } + let resolvedId: { id: string; agent: AgentId } | undefined = undefined // on-demand ephemeral subscriptions - socket.on('data', (data: Buffer) => { + socket.on('message', (data: Buffer) => { setImmediate(async () => { if (resolvedId) { safeWrite(socket, resolvedId) From 6261cae75bd2db46062e0244509293d9311e0ef2 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 17:22:13 +0200 Subject: [PATCH 22/58] fix print log :trollface: --- packages/client/src/client.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 9672d90b..42522bec 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -72,16 +72,13 @@ class Protocol { * @param event - The message event to handle. */ handle(event: MessageEvent) { - console.log('jkkjkjkjkjkjkjkjkk', event) const ws = event.target as WebSocket let current: MessageHandler if (this.#isStreaming) { - console.log('SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS') current = this.#stream } else { const next = this.#queue.pop() - console.log(next) if (next) { current = next } else { @@ -90,7 +87,6 @@ class Protocol { } } - console.log('MMMMMMMMMMMMM', event.data) if (isBlob(event.data)) { ;(event.data as Blob).text().then((blob) => current(JSON.parse(blob), ws, event)) } else { From a2dbd0bddc9c52ed82280e728e58eff86887934f Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 17:24:57 +0200 Subject: [PATCH 23/58] print subscription --- packages/client/test/bun/run.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/client/test/bun/run.ts b/packages/client/test/bun/run.ts index b20b1261..4a38d371 100644 --- a/packages/client/test/bun/run.ts +++ b/packages/client/test/bun/run.ts @@ -31,5 +31,10 @@ client.subscribe( }, onError: (error) => console.log(error), onClose: (event) => console.log(event.reason), + }, + { + onSubscriptionCreated: (sub) => { + console.log('SUB', sub) + } } ) From f5b32cc3a8e1cdcc871ec3d0b6872250b2b021cb Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 17:26:49 +0200 Subject: [PATCH 24/58] update subscribe example --- packages/client/README.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/client/README.md b/packages/client/README.md index 0a8a8b68..85a61e7d 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -30,16 +30,19 @@ const client = new OcelloidsClient({ // subscribe on-demand const ws = client.subscribe({ - origin: "urn:ocn:polkadot:2004", - senders: "*", - events: "*", - destinations: [ - "urn:ocn:polkadot:0", - "urn:ocn:polkadot:1000", - "urn:ocn:polkadot:2000", - "urn:ocn:polkadot:2034", - "urn:ocn:polkadot:2104" - ] + agent: "xcm", + args: { + origin: "urn:ocn:polkadot:2004", + senders: "*", + events: "*", + destinations: [ + "urn:ocn:polkadot:0", + "urn:ocn:polkadot:1000", + "urn:ocn:polkadot:2000", + "urn:ocn:polkadot:2034", + "urn:ocn:polkadot:2104" + ] + } }, { onMessage: msg => { if(isXcmReceived(msg)) { From 208fecd7f917a05686f9268dace69fe0de87471a Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 17:27:32 +0200 Subject: [PATCH 25/58] update subscribe example --- packages/client/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/README.md b/packages/client/README.md index 85a61e7d..3c97fc36 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -43,7 +43,7 @@ const ws = client.subscribe({ "urn:ocn:polkadot:2104" ] } -}, { + }, { onMessage: msg => { if(isXcmReceived(msg)) { console.log("RECV", msg.subscriptionId); From 13e4920134d98f439648ca04c2c84c3efcaed89c Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 17:28:27 +0200 Subject: [PATCH 26/58] update subscribe example --- packages/client/README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/client/README.md b/packages/client/README.md index 3c97fc36..bfb76c2c 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -44,16 +44,16 @@ const ws = client.subscribe({ ] } }, { - onMessage: msg => { - if(isXcmReceived(msg)) { - console.log("RECV", msg.subscriptionId); - } else if(isXcmSent(msg)) { - console.log("SENT", msg.subscriptionId) - } - console.log(msg); - }, - onError: error => console.log(error), - onClose: event => console.log(event.reason) + onMessage: msg => { + if(isXcmReceived(msg)) { + console.log("RECV", msg.subscriptionId); + } else if(isXcmSent(msg)) { + console.log("SENT", msg.subscriptionId) + } + console.log(msg); + }, + onError: error => console.log(error), + onClose: event => console.log(event.reason) }); ``` From f4322470a3b597887d50052dc506470d33dc2b8f Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 18:03:47 +0200 Subject: [PATCH 27/58] typed subscription inputs --- packages/client/src/client.spec.ts | 3 ++- packages/client/src/client.ts | 13 +++++++------ packages/client/src/types.ts | 8 ++++---- packages/client/src/xcm/types.ts | 30 +++++++++++++++--------------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/client/src/client.spec.ts b/packages/client/src/client.spec.ts index ac740418..2fc20584 100644 --- a/packages/client/src/client.spec.ts +++ b/packages/client/src/client.spec.ts @@ -5,6 +5,7 @@ import nock from 'nock' import samples from '../test/.data/samples.json' import type { Subscription, WsAuthErrorEvent } from './lib' +import { XcmSubscriptionInputs } from './xcm/types' jest.unstable_mockModule('isows', () => { return { @@ -97,7 +98,7 @@ describe('OcelloidsClient', () => { httpUrl: 'https://rpc.abc', }) - const ws = client.subscribe( + const ws = client.subscribe( { agent: 'xcm', args: { diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 42522bec..21d7c91a 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -2,6 +2,7 @@ import { type MessageEvent, WebSocket } from 'isows' import type { NotifyMessage } from './server-types' import { + AnySubscriptionInputs, type AuthReply, type MessageHandler, type OnDemandSubscription, @@ -263,16 +264,16 @@ export class OcelloidsClient { * @param onDemandHandlers - The on-demand subscription handlers. * @returns A promise that resolves with the WebSocket instance. */ - subscribe( - subscription: SubscriptionIds | OnDemandSubscription, + subscribe( + subscription: SubscriptionIds | OnDemandSubscription, handlers: WebSocketHandlers, onDemandHandlers?: OnDemandSubscriptionHandlers ): WebSocket { const url = this.#config.wsUrl + '/ws/subs' return isSubscriptionIds(subscription) - ? this.#openWebSocket(`${url}/${subscription.agentId}/${subscription.subscriptionId}`, handlers) - : this.#openWebSocket(url, handlers, { + ? this.#openWebSocket(`${url}/${subscription.agentId}/${subscription.subscriptionId}`, handlers) + : this.#openWebSocket(url, handlers, { sub: subscription, onDemandHandlers, }) @@ -309,11 +310,11 @@ export class OcelloidsClient { }) } - #openWebSocket( + #openWebSocket( url: string, { onMessage, onAuthError, onError, onClose }: WebSocketHandlers, onDemandSub?: { - sub: OnDemandSubscription + sub: OnDemandSubscription onDemandHandlers?: OnDemandSubscriptionHandlers } ) { diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 3849b53f..de0a1092 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -27,7 +27,7 @@ export type DeliveryChannel = * * @public */ -export type SubscriptionInputs = Record +export type AnySubscriptionInputs = Record /** * Represents a persistent subscription. @@ -60,7 +60,7 @@ export type SubscriptionInputs = Record * * @public */ -export type Subscription = { +export type Subscription = { /** * The subscription id. * Must be unique. @@ -75,7 +75,7 @@ export type Subscription = { /** * The specific agent inputs. */ - args: SubscriptionInputs + args: T /** * Indicates the persistence preference. @@ -108,7 +108,7 @@ export type SubscriptionError = { * * @public */ -export type OnDemandSubscription = Omit +export type OnDemandSubscription = Omit, 'id' | 'channels'> /** * Subscription identifiers. diff --git a/packages/client/src/xcm/types.ts b/packages/client/src/xcm/types.ts index 7fd4bc36..a50b6f98 100644 --- a/packages/client/src/xcm/types.ts +++ b/packages/client/src/xcm/types.ts @@ -1,5 +1,18 @@ import type { xcm } from '@sodazone/ocelloids-service-node' +/** + * The XCM event types. + * + * @public + */ +export enum XcmNotificationType { + Sent = 'xcm.sent', + Received = 'xcm.received', + Relayed = 'xcm.relayed', + Timeout = 'xcm.timeout', + Hop = 'xcm.hop', +} + /** * XCM Agent subscription inputs. * @@ -24,22 +37,9 @@ export type XcmSubscriptionInputs = { /** * An optional array with the events to deliver. * Use '*' for all. - * @see {@link xcm.XcmNotificationType} for supported event names. + * @see {@link XcmNotificationType} for supported event names. */ - events?: '*' | string[] -} - -/** - * The XCM event types. - * - * @public - */ -export enum XcmNotificationType { - Sent = 'xcm.sent', - Received = 'xcm.received', - Relayed = 'xcm.relayed', - Timeout = 'xcm.timeout', - Hop = 'xcm.hop', + events?: '*' | XcmNotificationType[] } /** From 2e5af30b1f927c84a4b9b038c8fff0deee67d74f Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 18:11:51 +0200 Subject: [PATCH 28/58] typed subscription inputs --- packages/client/README.md | 10 +-- packages/client/deno-build.mjs | 111 ------------------------- packages/client/package.json | 3 +- packages/client/src/client.spec.ts | 2 +- packages/client/src/client.ts | 21 +++-- packages/client/test/bun/run.ts | 2 +- packages/client/test/deno/package.json | 7 -- packages/client/test/deno/run.ts | 39 --------- packages/client/test/deno/yarn.lock | 12 --- 9 files changed, 20 insertions(+), 187 deletions(-) delete mode 100644 packages/client/deno-build.mjs delete mode 100644 packages/client/test/deno/package.json delete mode 100644 packages/client/test/deno/run.ts delete mode 100644 packages/client/test/deno/yarn.lock diff --git a/packages/client/README.md b/packages/client/README.md index bfb76c2c..10c86db6 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -21,7 +21,7 @@ yarn add @sodazone/ocelloids-client ## Usage ```typescript -import { OcelloidsClient, isXcmReceived, isXcmSent } from "@sodazone/ocelloids-client"; +import { OcelloidsClient, xcm } from "@sodazone/ocelloids-client"; const client = new OcelloidsClient({ httpUrl: "http://127.0.0.1:3000", @@ -29,7 +29,7 @@ const client = new OcelloidsClient({ }); // subscribe on-demand -const ws = client.subscribe({ +const ws = client.subscribe({ agent: "xcm", args: { origin: "urn:ocn:polkadot:2004", @@ -45,9 +45,9 @@ const ws = client.subscribe({ } }, { onMessage: msg => { - if(isXcmReceived(msg)) { + if(xcm.isXcmReceived(msg)) { console.log("RECV", msg.subscriptionId); - } else if(isXcmSent(msg)) { + } else if(xcm.isXcmSent(msg)) { console.log("SENT", msg.subscriptionId) } console.log(msg); @@ -83,4 +83,4 @@ yarn test ## Compatibility -Compatible with [browser environments, Node, Bun and Deno](/~https://github.com/sodazone/ocelloids-services/blob/main/packages/client/test). +Compatible with [browser environments, Node and Bun](/~https://github.com/sodazone/ocelloids-services/blob/main/packages/client/test). diff --git a/packages/client/deno-build.mjs b/packages/client/deno-build.mjs deleted file mode 100644 index 5b8000a2..00000000 --- a/packages/client/deno-build.mjs +++ /dev/null @@ -1,111 +0,0 @@ -// Based on: -// MIT License -// Copyright (c) 2020 Colin McDonnell -// /~https://github.com/colinhacks/zod/blob/master/deno-build.mjs -// This script expects to be run via `yarn build:deno`. -// -// Although this script generates code for use in Deno, this script itself is -// written for Node so that contributors do not need to install Deno to build. -// -// @ts-check - -import { - mkdirSync, - readdirSync, - readFileSync, - statSync, - writeFileSync, - cpSync -} from "fs"; -import { dirname } from "path"; - -// Node's path.join() normalize explicitly-relative paths like "./index.ts" to -// paths like "index.ts" which don't work as relative ES imports, so we do this. -const join = (/** @type string[] */ ...parts) => - parts.join("/").replace(/\/\//g, "/"); - -const projectRoot = process.cwd(); -const nodeSrcRoot = join(projectRoot, "src"); -const distRoot = join(projectRoot, "dist"); -const denoLibRoot = join(distRoot, "deno"); - -const replacements = { - "isows": "// native", - "@sodazone/ocelloids-service-node": "export type * from './ocelloids-client.d.ts';\n" -}; - -const walkAndBuild = (/** @type string */ dir) => { - for (const entry of readdirSync(join(nodeSrcRoot, dir), { - withFileTypes: true, - encoding: "utf-8", - })) { - if (entry.isDirectory()) { - walkAndBuild(join(dir, entry.name)); - } else if (entry.isFile() && entry.name.endsWith(".ts")) { - const nodePath = join(nodeSrcRoot, dir, entry.name); - const denoPath = join(denoLibRoot, dir, entry.name); - - if (nodePath.match(/.*\/.*\.spec\..*/)) { - continue; - } - - const nodeSource = readFileSync(nodePath, { encoding: "utf-8" }); - - const denoSource = nodeSource.replace( - /^(?:import|export|export type)[\s\S]*?from\s*['"]([^'"]*)['"];$/gm, - (line, target) => { - if (replacements[target]) { - return replacements[target]; - } - - const targetNodePath = join(dirname(nodePath), target); - const targetNodePathIfFile = targetNodePath + ".ts"; - const targetNodePathIfDir = join(targetNodePath, "index.ts"); - - try { - if (statSync(targetNodePathIfFile)?.isFile()) { - return line.replace(target, target + ".ts"); - } - } catch (error) { - if (error?.code !== "ENOENT") { - throw error; - } - } - - try { - if (statSync(targetNodePathIfDir)?.isFile()) { - return line.replace(target, join(target, "index.ts")); - } - } catch (error) { - if (error?.code !== "ENOENT") { - throw error; - } - } - - // console.warn(`Skipping non-resolvable import:\n ${line}`); - return line; - } - ); - - mkdirSync(dirname(denoPath), { recursive: true }); - writeFileSync(denoPath, denoSource, { encoding: "utf-8" }); - } - } -}; - -console.log("Deno build..."); - -walkAndBuild(""); - -const xcmonTypes = join(denoLibRoot, "ocelloids-client.d.ts"); -cpSync(join(distRoot, 'ocelloids-client.d.ts'), xcmonTypes); - -console.log("Done."); - -writeFileSync( - join(denoLibRoot, "mod.ts"), - "export * from './lib.ts';\n", - { - encoding: "utf-8", - } -); diff --git a/packages/client/package.json b/packages/client/package.json index 80dab37f..ec276a8c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -32,10 +32,9 @@ "clean": true }, "scripts": { - "build": "yarn build:ts && yarn build:api && yarn build:deno", + "build": "yarn build:ts && yarn build:api", "build:ts": "tsup", "build:api": "tsc && api-extractor run --local --verbose", - "build:deno": "node deno-build.mjs", "test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" jest", "docs": "typedoc", "lint": "biome check --apply src/**/*.ts" diff --git a/packages/client/src/client.spec.ts b/packages/client/src/client.spec.ts index 2fc20584..49ed2923 100644 --- a/packages/client/src/client.spec.ts +++ b/packages/client/src/client.spec.ts @@ -368,7 +368,7 @@ describe('OcelloidsClient', () => { type: 'websocket', }, ], - } as Subscription + } as Subscription const scope = nock('http://mock') .matchHeader('content-type', 'application/json') diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 21d7c91a..fcfc2f3e 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -102,7 +102,7 @@ class Protocol { * @example Create a client instance * * ```typescript - * import { OcelloidsClient, isXcmReceived, isXcmSent } from "@sodazone/ocelloids-client"; + * import { OcelloidsClient, xcm } from "@sodazone/ocelloids-client"; * * const client = new OcelloidsClient({ * httpUrl: "http://127.0.0.1:3000", @@ -113,7 +113,7 @@ class Protocol { * * ```typescript * // create a 'long-lived' subscription - * const reply = await client.create({ + * const reply = await client.create({ * id: "my-subscription", * agent: "xcm", * args: { @@ -142,9 +142,9 @@ class Protocol { * // subscribe to the previously created subscription * const ws = client.subscribe("my-subscription", { * onMessage: msg => { - * if(isXcmReceived(msg)) { + * if(xcm.isXcmReceived(msg)) { * console.log("RECV", msg.subscriptionId); - * } else if(isXcmSent(msg)) { + * } else if(xcm.isXcmSent(msg)) { * console.log("SENT", msg.subscriptionId) * } * console.log(msg); @@ -158,7 +158,7 @@ class Protocol { * * ```typescript * // subscribe on-demand - * const ws = client.subscribe({ + * const ws = client.subscribe({ * agent: "xcm", * args: { * origin: "urn:ocn:polkadot:2004", @@ -174,9 +174,9 @@ class Protocol { * } * }, { * onMessage: msg => { - * if(isXcmReceived(msg)) { + * if(xcm.isXcmReceived(msg)) { * console.log("RECV", msg.subscriptionId); - * } else if(isXcmSent(msg)) { + * } else if(xcm.isXcmSent(msg)) { * console.log("SENT", msg.subscriptionId) * } * console.log(msg); @@ -217,7 +217,7 @@ export class OcelloidsClient { * @param init - The fetch request init. * @returns A promise that resolves when the subscription is created. */ - async create(subscription: Subscription, init: RequestInit = {}) { + async create(subscription: Subscription, init: RequestInit = {}) { return this.#fetch(this.#config.httpUrl + '/subs', { ...init, method: 'POST', @@ -232,7 +232,10 @@ export class OcelloidsClient { * @param init - The fetch request init. * @returns A promise that resolves with the subscription or rejects if not found. */ - async getSubscription(subscriptionId: string, init?: RequestInit): Promise { + async getSubscription( + subscriptionId: string, + init?: RequestInit + ): Promise> { return this.#fetch(this.#config.httpUrl + '/subs/' + subscriptionId, init) } diff --git a/packages/client/test/bun/run.ts b/packages/client/test/bun/run.ts index 4a38d371..3e27cbe2 100644 --- a/packages/client/test/bun/run.ts +++ b/packages/client/test/bun/run.ts @@ -7,7 +7,7 @@ const client = new OcelloidsClient({ client.health().then(console.log).catch(console.error) -client.subscribe( +client.subscribe( { agent: 'xcm', args: { diff --git a/packages/client/test/deno/package.json b/packages/client/test/deno/package.json deleted file mode 100644 index eb47a344..00000000 --- a/packages/client/test/deno/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "test-deno", - "private": true, - "scripts": { - "test": "deno run --allow-net run.ts" - } -} diff --git a/packages/client/test/deno/run.ts b/packages/client/test/deno/run.ts deleted file mode 100644 index 5aa765e1..00000000 --- a/packages/client/test/deno/run.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { OcelloidsClient, xcm } from '../../dist/deno/mod.ts' - -const client = new OcelloidsClient({ - httpUrl: 'http://127.0.0.1:3000', - wsUrl: 'ws://127.0.0.1:3000', -}) - -client.health().then(console.log).catch(console.error) - -client.subscribe( - { - agent: 'xcm', - args: { - origin: 'urn:ocn:polkadot:2004', - senders: '*', - events: '*', - destinations: [ - 'urn:ocn:polkadot:0', - 'urn:ocn:polkadot:1000', - 'urn:ocn:polkadot:2000', - 'urn:ocn:polkadot:2034', - 'urn:ocn:polkadot:2104', - ], - } - }, - { - onMessage: (msg, ws) => { - if (xcm.isXcmReceived(msg)) { - console.log('RECV', msg.subscriptionId) - } else if (xcm.isXcmSent(msg)) { - console.log('SENT', msg.subscriptionId) - } - console.log(msg) - ws.close(1000, 'bye!') - }, - onError: (error) => console.log(error), - onClose: (event) => console.log(event.reason), - } -) diff --git a/packages/client/test/deno/yarn.lock b/packages/client/test/deno/yarn.lock deleted file mode 100644 index dca67bef..00000000 --- a/packages/client/test/deno/yarn.lock +++ /dev/null @@ -1,12 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 8 - cacheKey: 10c0 - -"test-deno@workspace:.": - version: 0.0.0-use.local - resolution: "test-deno@workspace:." - languageName: unknown - linkType: soft From de9cf82326f33f564c3d2216f8530fc55ec7cced Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 18:15:01 +0200 Subject: [PATCH 29/58] shorten xcm inputs name --- packages/client/README.md | 2 +- packages/client/src/client.spec.ts | 6 +++--- packages/client/src/client.ts | 4 ++-- packages/client/src/xcm/types.ts | 2 +- packages/client/test/bun/run.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/client/README.md b/packages/client/README.md index 10c86db6..b399854a 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -29,7 +29,7 @@ const client = new OcelloidsClient({ }); // subscribe on-demand -const ws = client.subscribe({ +const ws = client.subscribe({ agent: "xcm", args: { origin: "urn:ocn:polkadot:2004", diff --git a/packages/client/src/client.spec.ts b/packages/client/src/client.spec.ts index 49ed2923..1ea4ffd5 100644 --- a/packages/client/src/client.spec.ts +++ b/packages/client/src/client.spec.ts @@ -5,7 +5,7 @@ import nock from 'nock' import samples from '../test/.data/samples.json' import type { Subscription, WsAuthErrorEvent } from './lib' -import { XcmSubscriptionInputs } from './xcm/types' +import { XcmInputs } from './xcm/types' jest.unstable_mockModule('isows', () => { return { @@ -98,7 +98,7 @@ describe('OcelloidsClient', () => { httpUrl: 'https://rpc.abc', }) - const ws = client.subscribe( + const ws = client.subscribe( { agent: 'xcm', args: { @@ -368,7 +368,7 @@ describe('OcelloidsClient', () => { type: 'websocket', }, ], - } as Subscription + } as Subscription const scope = nock('http://mock') .matchHeader('content-type', 'application/json') diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index fcfc2f3e..29980edf 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -113,7 +113,7 @@ class Protocol { * * ```typescript * // create a 'long-lived' subscription - * const reply = await client.create({ + * const reply = await client.create({ * id: "my-subscription", * agent: "xcm", * args: { @@ -158,7 +158,7 @@ class Protocol { * * ```typescript * // subscribe on-demand - * const ws = client.subscribe({ + * const ws = client.subscribe({ * agent: "xcm", * args: { * origin: "urn:ocn:polkadot:2004", diff --git a/packages/client/src/xcm/types.ts b/packages/client/src/xcm/types.ts index a50b6f98..5f3b2680 100644 --- a/packages/client/src/xcm/types.ts +++ b/packages/client/src/xcm/types.ts @@ -18,7 +18,7 @@ export enum XcmNotificationType { * * @public */ -export type XcmSubscriptionInputs = { +export type XcmInputs = { /** * The origin chain id. */ diff --git a/packages/client/test/bun/run.ts b/packages/client/test/bun/run.ts index 3e27cbe2..c5525b37 100644 --- a/packages/client/test/bun/run.ts +++ b/packages/client/test/bun/run.ts @@ -7,7 +7,7 @@ const client = new OcelloidsClient({ client.health().then(console.log).catch(console.error) -client.subscribe( +client.subscribe( { agent: 'xcm', args: { From 5d779f7cf92bbc003906a4427d292839528c1f0d Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 18:21:10 +0200 Subject: [PATCH 30/58] change order of guard --- packages/client/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index de0a1092..2c51769e 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -224,7 +224,7 @@ export type OnDemandSubscriptionHandlers = { */ export function isSubscription(obj: Subscription | SubscriptionError | NotifyMessage): obj is Subscription { const maybeSub = obj as Subscription - return maybeSub.id !== undefined && maybeSub.agent !== undefined && maybeSub.channels !== undefined + return maybeSub.channels !== undefined && maybeSub.agent !== undefined && maybeSub.id !== undefined } /** From 0796b5d1407f811a23f03f5cfb105ddb578ac36d Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 18:36:29 +0200 Subject: [PATCH 31/58] fix source docs --- packages/server/src/services/agents/local.ts | 14 +------------- packages/server/src/services/notification/types.ts | 2 +- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/server/src/services/agents/local.ts b/packages/server/src/services/agents/local.ts index 1884800f..b85a660e 100644 --- a/packages/server/src/services/agents/local.ts +++ b/packages/server/src/services/agents/local.ts @@ -4,7 +4,6 @@ import { Logger, Services } from '../index.js' import { NotifierHub } from '../notification/index.js' import { NotifierEvents } from '../notification/types.js' import { NotificationListener, Subscription } from '../subscriptions/types.js' -import { TelemetryCollect } from '../telemetry/types.js' import { Agent, AgentId, AgentRuntimeContext, AgentService } from './types.js' import { XCMAgent } from './xcm/xcm-agent.js' @@ -25,12 +24,6 @@ export class LocalAgentService implements AgentService { }) } - /** - * Retrieves the registered subscriptions in the database - * for all the configured networks. - * - * @returns {Subscription[]} an array with the subscriptions - */ async getAllSubscriptions() { let subscriptions: Subscription[] = [] for (const chainId of this.getAgentIds()) { @@ -79,11 +72,6 @@ export class LocalAgentService implements AgentService { } } - /** - * Calls the given collect function for each private observable component. - * - * @param collect The collect callback function. - */ collectTelemetry() { for (const [id, agent] of Object.entries(this.#agents)) { this.#log.info('[local:agents] collect telemetry from agent %s', id) @@ -95,6 +83,6 @@ export class LocalAgentService implements AgentService { const xcm = new XCMAgent(ctx) return { [xcm.id]: xcm, - } + } as unknown as Record } } diff --git a/packages/server/src/services/notification/types.ts b/packages/server/src/services/notification/types.ts index 42ed3046..ce44b2db 100644 --- a/packages/server/src/services/notification/types.ts +++ b/packages/server/src/services/notification/types.ts @@ -4,7 +4,7 @@ import { TelemetryNotifierEvents } from '../telemetry/types.js' /** * The generic message. - * + * * @public */ export type NotifyMessage = { From 205bbeec13fa5d35df5dfb41d22adfe733341ea4 Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Wed, 29 May 2024 16:51:50 +0200 Subject: [PATCH 32/58] fix test imports --- packages/server/src/server.spec.ts | 68 +++++++++++-------- .../src/services/agents/xcm/matching.spec.ts | 12 ++-- .../src/services/agents/xcm/matching.ts | 5 +- .../services/agents/xcm/ops/bridge.spec.ts | 4 +- .../services/agents/xcm/ops/common.spec.ts | 10 +-- .../src/services/agents/xcm/ops/dmp.spec.ts | 2 +- .../src/services/agents/xcm/ops/relay.spec.ts | 4 +- .../src/services/agents/xcm/ops/ump.spec.ts | 2 +- .../agents/xcm/ops/xcm-format.spec.ts | 2 +- .../src/services/agents/xcm/ops/xcmp.spec.ts | 2 +- .../src/services/agents/xcm/xcm-agent.ts | 2 +- .../src/services/persistence/subs.spec.ts | 19 +++--- .../src/services/subscriptions/api/routes.ts | 2 +- packages/server/src/testing/data.ts | 48 ++++++++----- 14 files changed, 106 insertions(+), 76 deletions(-) diff --git a/packages/server/src/server.spec.ts b/packages/server/src/server.spec.ts index a676fb7a..1f20b390 100644 --- a/packages/server/src/server.spec.ts +++ b/packages/server/src/server.spec.ts @@ -1,21 +1,25 @@ -import { Subscription } from './services/monitoring/types.js' -import { jsonEncoded, prefixes } from './services/types.js' +import { Subscription } from './services/subscriptions/types.js' +import { LevelEngine, jsonEncoded, prefixes } from './services/types.js' import './testing/network.js' import { FastifyInstance, InjectOptions } from 'fastify' +import { AgentServiceMode } from './types.js' const testSubContent = { id: 'macatron', - origin: 'urn:ocn:local:1000', - senders: ['ALICE'], - destinations: ['urn:ocn:local:2000'], + agent: 'xcm', + args: { + origin: 'urn:ocn:local:1000', + senders: ['ALICE'], + events: '*', + destinations: ['urn:ocn:local:2000'], + }, channels: [ { type: 'log', }, ], - events: '*', } as Subscription const { createServer } = await import('./server.js') @@ -41,6 +45,8 @@ describe('monitoring server API', () => { corsCredentials: false, corsOrigin: true, distributed: false, + levelEngine: LevelEngine.mem, + mode: AgentServiceMode.local, }) return server.ready() @@ -87,15 +93,18 @@ describe('monitoring server API', () => { url: '/subs', body: { id: 'wild', - origin: 'urn:ocn:local:1000', - senders: '*', - destinations: ['urn:ocn:local:2000'], + agent: 'xcm', + args: { + origin: 'urn:ocn:local:1000', + senders: '*', + events: '*', + destinations: ['urn:ocn:local:2000'], + }, channels: [ { type: 'log', }, ], - events: '*', } as Subscription, }, (_err, response) => { @@ -114,12 +123,10 @@ describe('monitoring server API', () => { { ...testSubContent, id: 'm1', - senders: ['M1'], }, { ...testSubContent, id: 'm2', - senders: ['M2'], }, ], }, @@ -155,12 +162,10 @@ describe('monitoring server API', () => { { ...testSubContent, id: 'm3', - senders: ['M3'], }, { ...testSubContent, id: 'm1', - senders: ['M1'], }, ], }, @@ -185,7 +190,7 @@ describe('monitoring server API', () => { server.inject( { method: 'DELETE', - url: '/subs/m2', + url: '/subs/xcm/m2', }, (_err, response) => { done() @@ -198,7 +203,7 @@ describe('monitoring server API', () => { server.inject( { method: 'GET', - url: '/subs/macatron', + url: '/subs/xcm/macatron', }, (_err, response) => { done() @@ -212,12 +217,12 @@ describe('monitoring server API', () => { server.inject( { method: 'GET', - url: '/subs/wild', + url: '/subs/xcm/wild', }, (_err, response) => { done() expect(response.statusCode).toStrictEqual(200) - expect(JSON.parse(response.body).senders).toEqual('*') + expect(JSON.parse(response.body).args.senders).toEqual('*') } ) }) @@ -226,7 +231,7 @@ describe('monitoring server API', () => { server.inject( { method: 'GET', - url: '/subs/non-existent', + url: '/subs/xcm/non-existent', }, (_err, response) => { done() @@ -239,7 +244,7 @@ describe('monitoring server API', () => { server.inject( { method: 'PATCH', - url: '/subs/macatron', + url: '/subs/xcm/macatron', body: [ { op: 'replace', @@ -248,7 +253,7 @@ describe('monitoring server API', () => { }, { op: 'add', - path: '/senders/-', + path: '/args/senders/-', value: 'BOB', }, ], @@ -264,11 +269,11 @@ describe('monitoring server API', () => { server.inject( { method: 'PATCH', - url: '/subs/macatron', + url: '/subs/xcm/macatron', body: [ { op: 'add', - path: '/senders/-', + path: '/args/senders/-', value: 'BOB', }, ], @@ -276,7 +281,8 @@ describe('monitoring server API', () => { (_err, response) => { done() expect(response.statusCode).toStrictEqual(200) - expect(JSON.parse(response.body).senders).toEqual(['ALICE', 'BOB']) + + expect(JSON.parse(response.body).args.senders).toEqual(['ALICE', 'BOB']) } ) }) @@ -285,11 +291,11 @@ describe('monitoring server API', () => { server.inject( { method: 'PATCH', - url: '/subs/macatron', + url: '/subs/xcm/macatron', body: [ { op: 'remove', - path: '/senders', + path: '/args/senders', }, ], }, @@ -304,11 +310,11 @@ describe('monitoring server API', () => { server.inject( { method: 'PATCH', - url: '/subs/macatron', + url: '/subs/xcm/macatron', body: [ { op: 'add', - path: '/destinations/-', + path: '/args/destinations/-', value: 'urn:ocn:local:3000', }, ], @@ -316,7 +322,7 @@ describe('monitoring server API', () => { (_err, response) => { done() expect(response.statusCode).toStrictEqual(200) - expect(JSON.parse(response.body).destinations).toEqual(['urn:ocn:local:2000', 'urn:ocn:local:3000']) + expect(JSON.parse(response.body).args.destinations).toEqual(['urn:ocn:local:2000', 'urn:ocn:local:3000']) } ) }) @@ -325,7 +331,7 @@ describe('monitoring server API', () => { server.inject( { method: 'PATCH', - url: '/subs/macatron', + url: '/subs/xcm/macatron', body: [ { op: 'replace', @@ -420,12 +426,14 @@ describe('monitoring server API', () => { }) }) + /* TODO: move to XCM agent??? it('should get pending messages', (done) => { server.inject(adminRq('/admin/xcm'), (_err, response) => { done() expect(response.statusCode).toStrictEqual(200) }) }) + */ it('should get scheduled tasks', (done) => { server.inject(adminRq('/admin/sched'), (_err, response) => { diff --git a/packages/server/src/services/agents/xcm/matching.spec.ts b/packages/server/src/services/agents/xcm/matching.spec.ts index 304ab2a3..0d9ee044 100644 --- a/packages/server/src/services/agents/xcm/matching.spec.ts +++ b/packages/server/src/services/agents/xcm/matching.spec.ts @@ -3,13 +3,13 @@ import { jest } from '@jest/globals' import { MemoryLevel as Level } from 'memory-level' import { AbstractSublevel } from 'abstract-level' -import { XcmInbound, XcmNotificationType, XcmNotifyMessage, XcmSent } from '../../services/monitoring/types.js' -import { Janitor } from '../../services/persistence/janitor.js' -import { jsonEncoded, prefixes } from '../../services/types.js' -import { matchBridgeMessages } from '../../testing/bridge/matching.js' -import { matchHopMessages, matchMessages, realHopMessages } from '../../testing/matching.js' -import { _services } from '../../testing/services.js' +import { matchBridgeMessages } from '../../../testing/bridge/matching.js' +import { matchHopMessages, matchMessages, realHopMessages } from '../../../testing/matching.js' +import { _services } from '../../../testing/services.js' +import { Janitor } from '../../persistence/janitor.js' +import { jsonEncoded, prefixes } from '../../types.js' import { MatchingEngine } from './matching.js' +import { XcmInbound, XcmNotificationType, XcmNotifyMessage, XcmSent } from './types.js' describe('message matching engine', () => { let engine: MatchingEngine diff --git a/packages/server/src/services/agents/xcm/matching.ts b/packages/server/src/services/agents/xcm/matching.ts index 23cdcdc0..f3f8a544 100644 --- a/packages/server/src/services/agents/xcm/matching.ts +++ b/packages/server/src/services/agents/xcm/matching.ts @@ -66,7 +66,10 @@ export class MatchingEngine extends (EventEmitter as new () => TelemetryXCMEvent readonly #mutex: Mutex readonly #xcmMatchedReceiver: XcmMatchedReceiver - constructor({ log, rootStore, janitor }: AgentRuntimeContext, xcmMatchedReceiver: XcmMatchedReceiver) { + constructor( + { log, rootStore, janitor }: Omit, + xcmMatchedReceiver: XcmMatchedReceiver + ) { super() this.#log = log diff --git a/packages/server/src/services/agents/xcm/ops/bridge.spec.ts b/packages/server/src/services/agents/xcm/ops/bridge.spec.ts index b033d9dc..f53bfc95 100644 --- a/packages/server/src/services/agents/xcm/ops/bridge.spec.ts +++ b/packages/server/src/services/agents/xcm/ops/bridge.spec.ts @@ -14,13 +14,13 @@ import { xcmpReceivePolkadotAssetHub, xcmpSendKusamaAssetHub, xcmpSendPolkadotBridgeHub, -} from '../../../testing/bridge/blocks.js' +} from '../../../../testing/bridge/blocks.js' import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './pk-bridge.js' import { extractRelayReceive } from './relay.js' import { extractXcmpReceive, extractXcmpSend } from './xcmp.js' -import { NetworkURN } from '../../types.js' +import { NetworkURN } from '../../../types.js' import { GenericXcmSentWithContext } from '../types.js' import { mapXcmSent } from './common.js' import { getMessageId } from './util.js' diff --git a/packages/server/src/services/agents/xcm/ops/common.spec.ts b/packages/server/src/services/agents/xcm/ops/common.spec.ts index c3d01130..218e246b 100644 --- a/packages/server/src/services/agents/xcm/ops/common.spec.ts +++ b/packages/server/src/services/agents/xcm/ops/common.spec.ts @@ -2,11 +2,11 @@ import { jest } from '@jest/globals' import { from, of } from 'rxjs' -import { registry } from '../../../testing/xcm.js' -import { GenericXcmSentWithContext } from '../types' -import { mapXcmSent } from './common' -import { getMessageId } from './util' -import { asVersionedXcm, fromXcmpFormat } from './xcm-format' +import { registry } from '../../../../testing/xcm.js' +import { GenericXcmSentWithContext } from '../types.js' +import { mapXcmSent } from './common.js' +import { getMessageId } from './util.js' +import { asVersionedXcm, fromXcmpFormat } from './xcm-format.js' describe('extract waypoints operator', () => { describe('mapXcmSent', () => { diff --git a/packages/server/src/services/agents/xcm/ops/dmp.spec.ts b/packages/server/src/services/agents/xcm/ops/dmp.spec.ts index 9d7bb522..9ff3296d 100644 --- a/packages/server/src/services/agents/xcm/ops/dmp.spec.ts +++ b/packages/server/src/services/agents/xcm/ops/dmp.spec.ts @@ -11,7 +11,7 @@ import { registry, xcmHop, xcmHopOrigin, -} from '../../../testing/xcm.js' +} from '../../../../testing/xcm.js' import { extractDmpReceive, extractDmpSend, extractDmpSendByEvent } from './dmp.js' const getDmp = () => diff --git a/packages/server/src/services/agents/xcm/ops/relay.spec.ts b/packages/server/src/services/agents/xcm/ops/relay.spec.ts index 92467699..530f1ce5 100644 --- a/packages/server/src/services/agents/xcm/ops/relay.spec.ts +++ b/packages/server/src/services/agents/xcm/ops/relay.spec.ts @@ -1,8 +1,8 @@ import { jest } from '@jest/globals' import { extractTxWithEvents } from '@sodazone/ocelloids-sdk' -import { registry, relayHrmpReceive } from '../../../testing/xcm.js' -import { NetworkURN } from '../../types.js' +import { registry, relayHrmpReceive } from '../../../../testing/xcm.js' +import { NetworkURN } from '../../../types.js' import { messageCriteria } from './criteria.js' import { extractRelayReceive } from './relay.js' diff --git a/packages/server/src/services/agents/xcm/ops/ump.spec.ts b/packages/server/src/services/agents/xcm/ops/ump.spec.ts index ddca4422..ac64badb 100644 --- a/packages/server/src/services/agents/xcm/ops/ump.spec.ts +++ b/packages/server/src/services/agents/xcm/ops/ump.spec.ts @@ -1,7 +1,7 @@ import { jest } from '@jest/globals' import { extractEvents } from '@sodazone/ocelloids-sdk' -import { registry, umpReceive, umpSend } from '../../../testing/xcm.js' +import { registry, umpReceive, umpSend } from '../../../../testing/xcm.js' import { extractUmpReceive, extractUmpSend } from './ump.js' diff --git a/packages/server/src/services/agents/xcm/ops/xcm-format.spec.ts b/packages/server/src/services/agents/xcm/ops/xcm-format.spec.ts index f93e57e8..613ec5dc 100644 --- a/packages/server/src/services/agents/xcm/ops/xcm-format.spec.ts +++ b/packages/server/src/services/agents/xcm/ops/xcm-format.spec.ts @@ -1,4 +1,4 @@ -import { registry } from '../../../testing/xcm.js' +import { registry } from '../../../../testing/xcm.js' import { fromXcmpFormat } from './xcm-format.js' describe('xcm formats', () => { diff --git a/packages/server/src/services/agents/xcm/ops/xcmp.spec.ts b/packages/server/src/services/agents/xcm/ops/xcmp.spec.ts index 6067fcd2..5f20b7a8 100644 --- a/packages/server/src/services/agents/xcm/ops/xcmp.spec.ts +++ b/packages/server/src/services/agents/xcm/ops/xcmp.spec.ts @@ -1,7 +1,7 @@ import { jest } from '@jest/globals' import { extractEvents } from '@sodazone/ocelloids-sdk' -import { registry, xcmHop, xcmpReceive, xcmpSend } from '../../../testing/xcm.js' +import { registry, xcmHop, xcmpReceive, xcmpSend } from '../../../../testing/xcm.js' import { extractXcmpReceive, extractXcmpSend } from './xcmp.js' diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts index 615b122e..58720400 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -51,7 +51,7 @@ import { GetDownwardMessageQueues, GetOutboundHrmpMessages, GetOutboundUmpMessag const SUB_ERROR_RETRY_MS = 5000 -const allowedPaths = ['/senders', '/destinations', '/channels', '/events'] +const allowedPaths = ['/args/senders', '/args/destinations', '/channels', '/args/events'] function hasOp(patch: Operation[], path: string) { return patch.some((op) => op.path.startsWith(path)) diff --git a/packages/server/src/services/persistence/subs.spec.ts b/packages/server/src/services/persistence/subs.spec.ts index ca0bcecf..649d0a73 100644 --- a/packages/server/src/services/persistence/subs.spec.ts +++ b/packages/server/src/services/persistence/subs.spec.ts @@ -1,6 +1,6 @@ import { MemoryLevel as Level } from 'memory-level' -import { _subsFix } from '../../testing/data' +import { _subsFix, _testAgentId } from '../../testing/data' import { _ingress, _log } from '../../testing/services' import { SubsStore } from './subs' @@ -9,31 +9,32 @@ describe('subscriptions persistence', () => { beforeAll(() => { const mem = new Level() - db = new SubsStore(_log, mem, _ingress) + db = new SubsStore(_log, mem) }) describe('prepare data', () => { - it('should insert subscriptions fix', async () => { + it('should insert subscriptions', async () => { for (const sub of _subsFix) { await db.insert(sub) } - expect((await db.getAll()).length).toBe(5) + expect((await db.getByAgentId(_testAgentId)).length).toBe(5) }) }) describe('modify subscriptions', () => { - it('should prevent duplicate ids', async () => { + it('should prevent duplicate subscription ids under the same agent', async () => { await expect(async () => { await db.insert(_subsFix[0]) }).rejects.toThrow() }) it('should remove subsciption by id', async () => { - const subs = await db.getAll() - await db.remove(subs[subs.length - 1].id) - expect((await db.getAll()).length).toBe(subs.length - 1) + const subs = await db.getByAgentId(_testAgentId) + await db.remove(_testAgentId, subs[subs.length - 1].id) + expect((await db.getByAgentId(_testAgentId)).length).toBe(subs.length - 1) }) + /* TODO: move to agents?? it('should prevent unconfigured chain ids', async () => { await expect(async () => { await db.save({ @@ -48,6 +49,7 @@ describe('subscriptions persistence', () => { }) }).rejects.toThrow() }) + it('should allow multiple subscription for the same conditions', async () => { const len = (await db.getAll()).length @@ -60,5 +62,6 @@ describe('subscriptions persistence', () => { }) expect((await db.getAll()).length).toBe(len + 1) }) + */ }) }) diff --git a/packages/server/src/services/subscriptions/api/routes.ts b/packages/server/src/services/subscriptions/api/routes.ts index 5ca3f3e6..04a02424 100644 --- a/packages/server/src/services/subscriptions/api/routes.ts +++ b/packages/server/src/services/subscriptions/api/routes.ts @@ -171,7 +171,7 @@ export async function SubscriptionApi(api: FastifyInstance) { const res = await switchboard.updateSubscription(agentId, subscriptionId, patch) reply.status(200).send(res) } catch (error) { - reply.status(400).send(error) + reply.code(400).send((error as Error).message) } } ) diff --git a/packages/server/src/testing/data.ts b/packages/server/src/testing/data.ts index fd53bbf8..bd01454c 100644 --- a/packages/server/src/testing/data.ts +++ b/packages/server/src/testing/data.ts @@ -1,11 +1,15 @@ -import { Subscription } from '../services/monitoring/types.js' +import { Subscription } from '../services/subscriptions/types.js' +export const _testAgentId = 'agent-mcmuffin' export const _subsFix: Subscription[] = [ { id: '0:1000:1', - origin: 'urn:ocn:local:0', - destinations: ['urn:ocn:local:1000'], - senders: ['a', 'b', 'c'], + agent: _testAgentId, + args: { + origin: 'urn:ocn:local:0', + destinations: ['urn:ocn:local:1000'], + senders: ['a', 'b', 'c'], + }, channels: [ { type: 'log', @@ -14,9 +18,12 @@ export const _subsFix: Subscription[] = [ }, { id: '0:1000:2', - origin: 'urn:ocn:local:0', - destinations: ['urn:ocn:local:1000'], - senders: ['d', 'e', 'f'], + agent: _testAgentId, + args: { + origin: 'urn:ocn:local:0', + destinations: ['urn:ocn:local:1000'], + senders: ['d', 'e', 'f'], + }, channels: [ { type: 'log', @@ -25,9 +32,12 @@ export const _subsFix: Subscription[] = [ }, { id: '0:2000:1', - origin: 'urn:ocn:local:0', - destinations: ['urn:ocn:local:2000'], - senders: ['a', 'b', 'c'], + agent: _testAgentId, + args: { + origin: 'urn:ocn:local:0', + destinations: ['urn:ocn:local:2000'], + senders: ['a', 'b', 'c'], + }, channels: [ { type: 'log', @@ -36,9 +46,12 @@ export const _subsFix: Subscription[] = [ }, { id: '100:0-2000:1', - origin: 'urn:ocn:local:1000', - destinations: ['urn:ocn:local:0', 'urn:ocn:local:2000'], - senders: ['a', 'b', 'c'], + agent: _testAgentId, + args: { + origin: 'urn:ocn:local:1000', + destinations: ['urn:ocn:local:0', 'urn:ocn:local:2000'], + senders: ['a', 'b', 'c'], + }, channels: [ { type: 'log', @@ -47,9 +60,12 @@ export const _subsFix: Subscription[] = [ }, { id: '100:0-2000:2', - origin: 'urn:ocn:local:1000', - destinations: ['urn:ocn:local:0', 'urn:ocn:local:2000'], - senders: ['d', 'e', 'f'], + agent: _testAgentId, + args: { + origin: 'urn:ocn:local:1000', + destinations: ['urn:ocn:local:0', 'urn:ocn:local:2000'], + senders: ['d', 'e', 'f'], + }, channels: [ { type: 'log', From e5c8cbef5b01733a8c6d1770a8eeda63bc29c72e Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Wed, 29 May 2024 18:14:40 +0200 Subject: [PATCH 33/58] fix switchboard tests --- packages/server/src/server.spec.ts | 2 +- .../src/services/agents/xcm/xcm-agent.ts | 1 + .../subscriptions/switchboard.spec.ts | 82 ++++++++++++------- packages/server/src/testing/services.ts | 15 +++- 4 files changed, 69 insertions(+), 31 deletions(-) diff --git a/packages/server/src/server.spec.ts b/packages/server/src/server.spec.ts index 1f20b390..95ca3832 100644 --- a/packages/server/src/server.spec.ts +++ b/packages/server/src/server.spec.ts @@ -281,7 +281,7 @@ describe('monitoring server API', () => { (_err, response) => { done() expect(response.statusCode).toStrictEqual(200) - + expect(JSON.parse(response.body).args.senders).toEqual(['ALICE', 'BOB']) } ) diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts index 58720400..8bb977c8 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -382,6 +382,7 @@ export class XCMAgent extends BaseAgent { * @private */ #monitorOrigins({ id }: Subscription, { origin, senders, destinations }: XCMSubscriptionArgs): Monitor { + console.log(this.subs) const subs: RxSubscriptionWithId[] = [] const chainId = origin as NetworkURN diff --git a/packages/server/src/services/subscriptions/switchboard.spec.ts b/packages/server/src/services/subscriptions/switchboard.spec.ts index 141141d9..4db30128 100644 --- a/packages/server/src/services/subscriptions/switchboard.spec.ts +++ b/packages/server/src/services/subscriptions/switchboard.spec.ts @@ -7,8 +7,13 @@ import { of, throwError } from 'rxjs' import { _services } from '../../testing/services.js' import { SubsStore } from '../persistence/subs' import type { Switchboard } from './switchboard.js' -import { Subscription, XcmInboundWithContext, XcmNotificationType, XcmSentWithContext } from './types' +import { Subscription } from './types' +import { AgentService } from '../agents/types.js' +import { LocalAgentService } from '../agents/local.js' +import { Services } from '../types.js' +import { AgentServiceMode } from '../../types.js' +/* TODO: move to xcm agent tests jest.unstable_mockModule('./ops/xcmp.js', () => { return { extractXcmpSend: jest.fn(), @@ -22,29 +27,33 @@ jest.unstable_mockModule('./ops/ump.js', () => { extractUmpSend: jest.fn(), } }) +*/ const SwitchboardImpl = (await import('./switchboard.js')).Switchboard -const { extractXcmpReceive, extractXcmpSend } = await import('./ops/xcmp.js') -const { extractUmpReceive, extractUmpSend } = await import('./ops/ump.js') const testSub: Subscription = { id: '1000:2000:0', - origin: 'urn:ocn:local:1000', - senders: ['14DqgdKU6Zfh1UjdU4PYwpoHi2QTp37R6djehfbhXe9zoyQT'], - destinations: ['urn:ocn:local:2000'], + agent: 'xcm', + args: { + origin: 'urn:ocn:local:1000', + senders: ['14DqgdKU6Zfh1UjdU4PYwpoHi2QTp37R6djehfbhXe9zoyQT'], + events: '*', + destinations: ['urn:ocn:local:2000'], + }, channels: [ { type: 'log', }, ], - events: '*', } describe('switchboard service', () => { let switchboard: Switchboard let subs: SubsStore + let agentService: AgentService - beforeEach(() => { + beforeAll(async () => { + /* ;(extractXcmpSend as jest.Mock).mockImplementation(() => { return () => { return of({ @@ -96,49 +105,52 @@ describe('switchboard service', () => { outcome: 'Success', } as unknown as XcmInboundWithContext) }) + */ + + subs = new SubsStore(_services.log, _services.rootStore) + agentService = new LocalAgentService( + { + ..._services, + subsStore: subs, + } as Services, + { mode: AgentServiceMode.local } + ) - subs = _services.subsStore switchboard = new SwitchboardImpl(_services, { subscriptionMaxEphemeral: 10_00, subscriptionMaxPersistent: 10_000, }) + agentService.start() }) - afterEach(async () => { + afterAll(async () => { await _services.rootStore.clear() - return switchboard.stop() + return agentService.stop() }) - it('should unsubscribe', async () => { - await switchboard.start() - + it('should add a subscription by agent', async () => { await switchboard.subscribe(testSub) - expect(switchboard.findSubscriptionHandler(testSub.id)).toBeDefined() - expect(await subs.getById(testSub.id)).toBeDefined() - - await switchboard.unsubscribe(testSub.id) - - expect(switchboard.findSubscriptionHandler(testSub.id)).not.toBeDefined() + expect(switchboard.findSubscriptionHandler('xcm', testSub.id)).toBeDefined() + expect(await subs.getById('xcm', testSub.id)).toBeDefined() }) - it('should notify on matched HRMP', async () => { - await switchboard.start() - - await switchboard.subscribe(testSub) + it('should remove subscription by agent', async () => { + expect(switchboard.findSubscriptionHandler('xcm', testSub.id)).toBeDefined() + expect(await subs.getById('xcm', testSub.id)).toBeDefined() - await switchboard.stop() + await switchboard.unsubscribe('xcm', testSub.id) - // we can extract the NotifierHub as a service - // to test the matched, but not really worth right now + expect(() => {switchboard.findSubscriptionHandler('xcm', testSub.id)}).toThrow('subscription handler not found') }) + /* TODO: move to agent service test it('should subscribe to persisted subscriptions on start', async () => { await subs.insert(testSub) - await switchboard.start() + await agentService.start() - expect(switchboard.findSubscriptionHandler(testSub.id)).toBeDefined() + expect(switchboard.findSubscriptionHandler('xcm', testSub.id)).toBeDefined() }) it('should handle relay subscriptions', async () => { @@ -296,4 +308,16 @@ describe('switchboard service', () => { const { relaySub: newRelaySub } = switchboard.findSubscriptionHandler(testSub.id) expect(newRelaySub).not.toBeDefined() }) + + it('should notify on matched HRMP', async () => { + await switchboard.start() + + await switchboard.subscribe(testSub) + + await switchboard.stop() + + // we can extract the NotifierHub as a service + // to test the matched, but not really worth right now + }) + */ }) diff --git a/packages/server/src/testing/services.ts b/packages/server/src/testing/services.ts index b570db81..41b2264b 100644 --- a/packages/server/src/testing/services.ts +++ b/packages/server/src/testing/services.ts @@ -4,6 +4,8 @@ import { pino } from 'pino' import { of } from 'rxjs' import toml from 'toml' +import { LocalAgentService } from '../services/agents/local.js' +import { AgentService } from '../services/agents/types.js' import { $ServiceConfiguration } from '../services/config.js' import { IngressConsumer, LocalIngressConsumer } from '../services/ingress/consumer/index.js' import Connector from '../services/networking/connector.js' @@ -11,6 +13,7 @@ import { Janitor } from '../services/persistence/janitor.js' import { Scheduler } from '../services/persistence/scheduler.js' import { SubsStore } from '../services/persistence/subs.js' import { Services } from '../services/types.js' +import { AgentServiceMode } from '../types.js' import { _configToml } from './data.js' export const _log = pino({ @@ -175,6 +178,7 @@ const __services = { rootStore: _rootDB, subsStore: {} as unknown as SubsStore, ingressConsumer: {} as unknown as IngressConsumer, + agentService: {} as unknown as AgentService, scheduler: { on: () => { /* empty */ @@ -191,10 +195,19 @@ const __services = { } export const _ingress = new LocalIngressConsumer(__services) -export const _subsDB = new SubsStore(_log, _rootDB, _ingress) +export const _subsDB = new SubsStore(_log, _rootDB) +export const _agentService = new LocalAgentService( + { + ...__services, + ingressConsumer: _ingress, + subsStore: _subsDB, + } as Services, + { mode: AgentServiceMode.local } +) export const _services = { ...__services, ingressConsumer: _ingress, subsStore: _subsDB, + agentService: _agentService, } as Services From e6ef6fdb26f9014c92a8e3c8823f749634e42752 Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Wed, 29 May 2024 18:53:09 +0200 Subject: [PATCH 34/58] fix more tests --- .../src/services/agents/xcm/xcm-agent.ts | 1 - .../ingress/watcher/head-catcher.spec.ts | 4 +- .../src/services/notification/webhook.spec.ts | 94 +++++++++---------- .../subscriptions/switchboard.spec.ts | 12 ++- packages/server/src/testing/blocks.ts | 2 +- packages/server/src/testing/bridge/blocks.ts | 2 +- .../server/src/testing/bridge/matching.ts | 2 +- packages/server/src/testing/matching.ts | 2 +- packages/server/src/testing/xcm.ts | 2 +- 9 files changed, 58 insertions(+), 63 deletions(-) diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts index 8bb977c8..58720400 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -382,7 +382,6 @@ export class XCMAgent extends BaseAgent { * @private */ #monitorOrigins({ id }: Subscription, { origin, senders, destinations }: XCMSubscriptionArgs): Monitor { - console.log(this.subs) const subs: RxSubscriptionWithId[] = [] const chainId = origin as NetworkURN diff --git a/packages/server/src/services/ingress/watcher/head-catcher.spec.ts b/packages/server/src/services/ingress/watcher/head-catcher.spec.ts index 6ac79091..78f87c5c 100644 --- a/packages/server/src/services/ingress/watcher/head-catcher.spec.ts +++ b/packages/server/src/services/ingress/watcher/head-catcher.spec.ts @@ -7,10 +7,10 @@ import { from, of } from 'rxjs' import { interlayBlocks, polkadotBlocks, testBlocksFrom } from '../../../testing/blocks.js' import { mockConfigMixed, mockConfigWS } from '../../../testing/configs.js' import { _services } from '../../../testing/services.js' -import { parachainSystemHrmpOutboundMessages, parachainSystemUpwardMessages } from '../../monitoring/storage.js' -import { BlockNumberRange, ChainHead } from '../../monitoring/types.js' import Connector from '../../networking/connector.js' import { Janitor } from '../../persistence/janitor.js' +import { parachainSystemHrmpOutboundMessages, parachainSystemUpwardMessages } from '../../subscriptions/storage.js' +import { BlockNumberRange, ChainHead } from '../../subscriptions/types.js' import { DB, NetworkURN, jsonEncoded, prefixes } from '../../types.js' const HeadCatcher = (await import('./head-catcher.js')).HeadCatcher diff --git a/packages/server/src/services/notification/webhook.spec.ts b/packages/server/src/services/notification/webhook.spec.ts index 1eb19fc0..6603a107 100644 --- a/packages/server/src/services/notification/webhook.spec.ts +++ b/packages/server/src/services/notification/webhook.spec.ts @@ -5,57 +5,38 @@ import nock from 'nock' import { _log, _services } from '../../testing/services.js' -import { Subscription, XcmNotificationType, XcmNotifyMessage, XcmTerminusContext } from '../monitoring/types.js' import { Scheduler } from '../persistence/scheduler.js' +import { Subscription } from '../subscriptions/types.js' import { NotifierHub } from './hub.js' +import { NotifyMessage } from './types.js' import { WebhookNotifier } from './webhook.js' -const destinationContext: XcmTerminusContext = { - blockHash: '0xBEEF', - blockNumber: '2', - chainId: 'urn:ocn:local:1', - event: {}, - outcome: 'Success', - error: null, - messageHash: '0xCAFE', - instructions: '0x', - messageData: '0x', -} -const notification: XcmNotifyMessage = { - type: XcmNotificationType.Received, - subscriptionId: 'ok', - legs: [{ type: 'hrmp', from: 'urn:ocn:local:0', to: 'urn:ocn:local:1' }], - waypoint: { - ...destinationContext, - legIndex: 0, +const notification: NotifyMessage = { + metadata: { + type: 'xcm.ok', + agentId: 'xcm', + subscriptionId: 'ok', }, - destination: destinationContext, - origin: { - blockHash: '0xBEEF', - blockNumber: '2', - chainId: 'urn:ocn:local:0', - event: {}, - outcome: 'Success', - error: null, - messageHash: '0xCAFE', - instructions: '0x', - messageData: '0x', + payload: { + foo: 'bar', }, - sender: { signer: { id: 'w123', publicKey: '0x0' }, extraSigners: [] }, } const subOk = { - destinations: ['urn:ocn:local:1000'], id: 'ok', + agent: 'xcm', + args: { + destinations: ['urn:ocn:local:1000'], + origin: 'urn:ocn:local:0', + senders: '*', + events: '*', + }, channels: [ { type: 'webhook', url: 'http://localhost/ok', }, ], - origin: 'urn:ocn:local:0', - senders: '*', - events: '*', } as Subscription const xmlTemplate = ` @@ -75,8 +56,8 @@ const xmlTemplate = ` ` const subOkXml = { - destinations: ['urn:ocn:local:0'], id: 'ok:xml', + agent: 'xcm', channels: [ { type: 'webhook', @@ -85,30 +66,36 @@ const subOkXml = { template: xmlTemplate, }, ], - origin: 'urn:ocn:local:1000', - senders: '*', - events: '*', + args: { + origin: 'urn:ocn:local:1000', + destinations: ['urn:ocn:local:0'], + senders: '*', + events: '*', + }, } as Subscription const subFail = { - destinations: ['urn:ocn:local:2000'], id: 'fail', + agent: 'xcm', channels: [ { type: 'webhook', url: 'http://localhost/not-found', }, ], - origin: 'urn:ocn:local:0', - senders: '*', - events: '*', + args: { + origin: 'urn:ocn:local:0', + destinations: ['urn:ocn:local:2000'], + senders: '*', + events: '*', + }, } as Subscription const authToken = 'secret' const subOkAuth = { - destinations: ['urn:ocn:local:3000'], id: 'ok:auth', + agent: 'xcm', channels: [ { type: 'webhook', @@ -116,9 +103,12 @@ const subOkAuth = { bearer: authToken, }, ], - origin: 'urn:ocn:local:0', - senders: '*', - events: '*', + args: { + origin: 'urn:ocn:local:0', + destinations: ['urn:ocn:local:3000'], + senders: '*', + events: '*', + }, } as Subscription describe('webhook notifier', () => { @@ -175,10 +165,14 @@ describe('webhook notifier', () => { const ok = jest.fn() notifier.on('telemetryNotify', ok) - await notifier.notify(subOkXml, { + const xmlNotifyMsg = { ...notification, - subscriptionId: 'ok:xml', - }) + metadata: { + ...notification.metadata, + subscriptionId: 'ok:xml', + }, + } + await notifier.notify(subOkXml, xmlNotifyMsg) expect(ok).toHaveBeenCalled() scope.done() diff --git a/packages/server/src/services/subscriptions/switchboard.spec.ts b/packages/server/src/services/subscriptions/switchboard.spec.ts index 4db30128..79985116 100644 --- a/packages/server/src/services/subscriptions/switchboard.spec.ts +++ b/packages/server/src/services/subscriptions/switchboard.spec.ts @@ -5,13 +5,13 @@ import '../../testing/network.js' import { of, throwError } from 'rxjs' import { _services } from '../../testing/services.js' +import { AgentServiceMode } from '../../types.js' +import { LocalAgentService } from '../agents/local.js' +import { AgentService } from '../agents/types.js' import { SubsStore } from '../persistence/subs' +import { Services } from '../types.js' import type { Switchboard } from './switchboard.js' import { Subscription } from './types' -import { AgentService } from '../agents/types.js' -import { LocalAgentService } from '../agents/local.js' -import { Services } from '../types.js' -import { AgentServiceMode } from '../../types.js' /* TODO: move to xcm agent tests jest.unstable_mockModule('./ops/xcmp.js', () => { @@ -141,7 +141,9 @@ describe('switchboard service', () => { await switchboard.unsubscribe('xcm', testSub.id) - expect(() => {switchboard.findSubscriptionHandler('xcm', testSub.id)}).toThrow('subscription handler not found') + expect(() => { + switchboard.findSubscriptionHandler('xcm', testSub.id) + }).toThrow('subscription handler not found') }) /* TODO: move to agent service test diff --git a/packages/server/src/testing/blocks.ts b/packages/server/src/testing/blocks.ts index f5079630..8e5bab83 100644 --- a/packages/server/src/testing/blocks.ts +++ b/packages/server/src/testing/blocks.ts @@ -7,7 +7,7 @@ import { Metadata, TypeRegistry } from '@polkadot/types' import type { AccountId, EventRecord, SignedBlock } from '@polkadot/types/interfaces' import { decode } from 'cbor-x' -import { HexString } from '../services/monitoring/types.js' +import { HexString } from '../services/subscriptions/types.js' const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) diff --git a/packages/server/src/testing/bridge/blocks.ts b/packages/server/src/testing/bridge/blocks.ts index 0b3cb3d3..a41fe9fc 100644 --- a/packages/server/src/testing/bridge/blocks.ts +++ b/packages/server/src/testing/bridge/blocks.ts @@ -10,7 +10,7 @@ import { hexToU8a } from '@polkadot/util' import { ControlQuery } from '@sodazone/ocelloids-sdk' -import { messageCriteria } from '../../services/monitoring/ops/criteria.js' +import { messageCriteria } from '../../services/agents/xcm/ops/criteria.js' import { NetworkURN } from '../../services/types.js' import { testBlocksFrom } from '../blocks.js' diff --git a/packages/server/src/testing/bridge/matching.ts b/packages/server/src/testing/bridge/matching.ts index 1efd02c3..334f722a 100644 --- a/packages/server/src/testing/bridge/matching.ts +++ b/packages/server/src/testing/bridge/matching.ts @@ -6,7 +6,7 @@ import { XcmNotificationType, XcmRelayedWithContext, XcmSent, -} from '../../services/monitoring/types' +} from '../../services/agents/xcm/types.js' type MatchBridgeMessages = { subscriptionId: string diff --git a/packages/server/src/testing/matching.ts b/packages/server/src/testing/matching.ts index 34318753..0bc40c12 100644 --- a/packages/server/src/testing/matching.ts +++ b/packages/server/src/testing/matching.ts @@ -4,7 +4,7 @@ import { XcmRelayedWithContext, XcmSent, XcmTerminusContext, -} from '../services/monitoring/types' +} from '../services/agents/xcm/types.js' const subscriptionId = 'manamana-1' diff --git a/packages/server/src/testing/xcm.ts b/packages/server/src/testing/xcm.ts index 62cbcbb6..e3c37834 100644 --- a/packages/server/src/testing/xcm.ts +++ b/packages/server/src/testing/xcm.ts @@ -14,7 +14,7 @@ import type { import { ControlQuery } from '@sodazone/ocelloids-sdk' import { from } from 'rxjs' -import { messageCriteria, sendersCriteria } from '../services/monitoring/ops/criteria.js' +import { messageCriteria, sendersCriteria } from '../services/agents/xcm/ops/criteria.js' import { NetworkURN } from '../services/types.js' import { testBlocksFrom } from './blocks.js' From 798952723fc9cb834209611068ac1ee1c3c3140b Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Wed, 29 May 2024 18:38:30 +0200 Subject: [PATCH 35/58] import as type --- packages/client/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 29980edf..5c00f7da 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -2,7 +2,7 @@ import { type MessageEvent, WebSocket } from 'isows' import type { NotifyMessage } from './server-types' import { - AnySubscriptionInputs, + type AnySubscriptionInputs, type AuthReply, type MessageHandler, type OnDemandSubscription, From 52bf9de53c2586a52e44d8e987a6b78f988834d2 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 10:00:03 +0200 Subject: [PATCH 36/58] re-export xcm type --- packages/client/src/xcm/types.ts | 92 +++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/packages/client/src/xcm/types.ts b/packages/client/src/xcm/types.ts index 5f3b2680..dc202d99 100644 --- a/packages/client/src/xcm/types.ts +++ b/packages/client/src/xcm/types.ts @@ -13,6 +13,85 @@ export enum XcmNotificationType { Hop = 'xcm.hop', } +/** + * XCM sent event. + * + * @public + */ +export type XcmSent = xcm.XcmSent +/** + * XCM received event. + * + * @public + */ +export type XcmReceived = xcm.XcmReceived +/** + * XCM relayed event. + * + * @public + */ +export type XcmRelayed = xcm.XcmRelayed +/** + * XCM hop event. + * + * @public + */ +export type XcmHop = xcm.XcmHop +/** + * XCM bridge event. + * + * @public + */ +export type XcmBridge = xcm.XcmBridge +/** + * XCM timeout event. + * + * @public + */ +export type XcmTimeout = xcm.XcmTimeout +/** + * The XCM notification payload. + * + * @public + */ +export type XcmNotifyMessage = xcm.XcmNotifyMessage +/** + * XCM assets trapped event. + * + * @public + */ +export type XcmAssetsTrapped = xcm.AssetsTrapped +/** + * XCM trapped asset data. + * + * @public + */ +export type XcmTrappedAsset = xcm.TrappedAsset +/** + * The leg of an XCM journey. + * + * @public + */ +export type XcmLeg = xcm.Leg +/** + * The XcmTerminus contextual information. + * + * @public + */ +export type XcmTerminusContext = xcm.XcmTerminusContext +/** + * Terminal point of an XCM journey. + * + * @public + */ +export type XcmTerminus = xcm.XcmTerminus +/** + * The XCM waypoint contextual information. + * + * @public + */ +export type XcmWaypointContext = xcm.XcmWaypointContext + /** * XCM Agent subscription inputs. * @@ -37,34 +116,33 @@ export type XcmInputs = { /** * An optional array with the events to deliver. * Use '*' for all. - * @see {@link XcmNotificationType} for supported event names. */ events?: '*' | XcmNotificationType[] } /** - * Guard condition for {@link xcm.XcmSent}. + * Guard condition for XcmSent. * * @public */ -export function isXcmSent(object: any): object is xcm.XcmSent { +export function isXcmSent(object: any): object is XcmSent { return object.type !== undefined && object.type === XcmNotificationType.Sent } /** - * Guard condition for {@link xcm.XcmReceived}. + * Guard condition for XcmReceived. * * @public */ -export function isXcmReceived(object: any): object is xcm.XcmReceived { +export function isXcmReceived(object: any): object is XcmReceived { return object.type !== undefined && object.type === XcmNotificationType.Received } /** - * Guard condition for {@link xcm.XcmRelayed}. + * Guard condition for XcmRelayed. * * @public */ -export function isXcmRelayed(object: any): object is xcm.XcmRelayed { +export function isXcmRelayed(object: any): object is XcmRelayed { return object.type !== undefined && object.type === XcmNotificationType.Relayed } From b9bf32b6dc5780a694142eee6db4c6c3e79ad5d6 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 10:00:39 +0200 Subject: [PATCH 37/58] fix lib tests --- packages/client/test/app-svelte/src/store.ts | 19 +++++++------ packages/client/test/app-vanilla/subscribe.js | 15 ++++++----- packages/client/test/node/run.js | 13 +++++---- packages/client/test/node/run.mjs | 13 +++++---- packages/client/test/node/run.ts | 27 +++++++++---------- 5 files changed, 47 insertions(+), 40 deletions(-) diff --git a/packages/client/test/app-svelte/src/store.ts b/packages/client/test/app-svelte/src/store.ts index b8ac1807..00d11143 100644 --- a/packages/client/test/app-svelte/src/store.ts +++ b/packages/client/test/app-svelte/src/store.ts @@ -1,5 +1,5 @@ import { writable } from 'svelte/store' -import { OcelloidsClient, type XcmSent, isXcmSent } from '../../../dist/lib' +import { OcelloidsClient, xcm } from '../../../dist/lib' export const createSubscriptionStore = async () => { const { subscribe, set, update } = writable([]) @@ -9,23 +9,22 @@ export const createSubscriptionStore = async () => { httpUrl: 'http://localhost:3000', }) - const ws = client.subscribe( + const ws = client.subscribe({ + agent: 'xcm', + args: { - origin: 'urn:ocn:polkadot:2004', + origin: 'urn:ocn:polkadot:1000', senders: '*', events: '*', destinations: [ 'urn:ocn:polkadot:0', - 'urn:ocn:polkadot:1000', - 'urn:ocn:polkadot:2000', - 'urn:ocn:polkadot:2034', - 'urn:ocn:polkadot:2104', ], - }, + } + }, { onMessage: (msg) => { - if (isXcmSent(msg)) { - const sent = msg as XcmSent + if (xcm.isXcmSent(msg)) { + const sent = msg as xcm.XcmSent console.log(sent.type, sent.subscriptionId) } update((messages) => [JSON.stringify(msg)].concat(messages)) diff --git a/packages/client/test/app-vanilla/subscribe.js b/packages/client/test/app-vanilla/subscribe.js index 64e43ede..03cf2cc0 100644 --- a/packages/client/test/app-vanilla/subscribe.js +++ b/packages/client/test/app-vanilla/subscribe.js @@ -1,4 +1,4 @@ -import { OcelloidsClient, isXcmSent } from "../../"; +import { OcelloidsClient, xcm } from "../../"; export function setup() { const messages = document.querySelector('#messages') @@ -10,13 +10,16 @@ export function setup() { }); const ws = client.subscribe({ - origin: "urn:ocn:polkadot:2004", - senders: "*", - events: "*", - destinations: ["urn:ocn:polkadot:0", "urn:ocn:polkadot:1000", "urn:ocn:polkadot:2000", "urn:ocn:polkadot:2034", "urn:ocn:polkadot:2104"] + agent: "xcm", + args: { + origin: "urn:ocn:polkadot:1000", + senders: "*", + events: "*", + destinations: ["urn:ocn:polkadot:0"] + } }, { onMessage: msg => { - if(isXcmSent(msg)) { + if (xcm.isXcmSent(msg)) { console.log('SENT', msg.subscriptionId); } const pre = document.createElement('pre') diff --git a/packages/client/test/node/run.js b/packages/client/test/node/run.js index 968856d9..b8121c97 100644 --- a/packages/client/test/node/run.js +++ b/packages/client/test/node/run.js @@ -8,11 +8,14 @@ const client = new OcelloidsClient({ client.health().then(console.log).catch(console.error) client.subscribe( - { - origin: "urn:ocn:polkadot:2004", - senders: "*", - events: "*", - destinations: [ "urn:ocn:polkadot:0","urn:ocn:polkadot:1000", "urn:ocn:polkadot:2000", "urn:ocn:polkadot:2034", "urn:ocn:polkadot:2104" ] + { + agent: "xcm", + args: { + origin: "urn:ocn:polkadot:0", + senders: "*", + events: "*", + destinations: ["urn:ocn:polkadot:1000"] + } }, { onMessage: (msg, ws) => { console.log(msg); diff --git a/packages/client/test/node/run.mjs b/packages/client/test/node/run.mjs index 97aca59b..c9b1c6c7 100644 --- a/packages/client/test/node/run.mjs +++ b/packages/client/test/node/run.mjs @@ -8,11 +8,14 @@ const client = new OcelloidsClient({ client.health().then(console.log).catch(console.error) client.subscribe( - { - origin: "urn:ocn:polkadot:2004", - senders: "*", - events: "*", - destinations: [ "urn:ocn:polkadot:0","urn:ocn:polkadot:1000", "urn:ocn:polkadot:2000", "urn:ocn:polkadot:2034", "urn:ocn:polkadot:2104" ] + { + agent: "xcm", + args: { + origin: "urn:ocn:polkadot:0", + senders: "*", + events: "*", + destinations: ["urn:ocn:polkadot:1000"] + } }, { onMessage: (msg, ws) => { console.log(msg); diff --git a/packages/client/test/node/run.ts b/packages/client/test/node/run.ts index 5a2a4a60..9c41cdb8 100644 --- a/packages/client/test/node/run.ts +++ b/packages/client/test/node/run.ts @@ -1,4 +1,4 @@ -import { OcelloidsClient, isXcmReceived, isXcmSent } from '../..' +import { OcelloidsClient, xcm } from '../..' const client = new OcelloidsClient({ httpUrl: 'http://127.0.0.1:3000', @@ -7,24 +7,23 @@ const client = new OcelloidsClient({ client.health().then(console.log).catch(console.error) -client.subscribe( +client.subscribe( { - origin: 'urn:ocn:polkadot:2004', - senders: '*', - events: '*', - destinations: [ - 'urn:ocn:polkadot:0', - 'urn:ocn:polkadot:1000', - 'urn:ocn:polkadot:2000', - 'urn:ocn:polkadot:2034', - 'urn:ocn:polkadot:2104', - ], + agent: "xcm", + args: { + origin: 'urn:ocn:polkadot:0', + senders: '*', + events: '*', + destinations: [ + 'urn:ocn:polkadot:1000' + ], + } }, { onMessage: (msg, ws) => { - if (isXcmReceived(msg)) { + if (xcm.isXcmReceived(msg)) { console.log('RECV', msg.subscriptionId) - } else if (isXcmSent(msg)) { + } else if (xcm.isXcmSent(msg)) { console.log('SENT', msg.subscriptionId) } console.log(msg) From e40a6c1b6ee3628aa82f55c35175c82a92f2e851 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 10:01:44 +0200 Subject: [PATCH 38/58] rm deno support --- packages/client/package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/client/package.json b/packages/client/package.json index ec276a8c..e17ea95c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -43,12 +43,8 @@ "preset": "ts-jest", "testEnvironment": "node", "collectCoverage": true, - "testPathIgnorePatterns": [ - "./deno_dist" - ], "coveragePathIgnorePatterns": [ - ".*/dist", - ".*/deno_dist" + ".*/dist" ], "extensionsToTreatAsEsm": [ ".ts" From 49a68241b4e91faa6396a8f39a62da5304d4e381 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 10:02:16 +0200 Subject: [PATCH 39/58] rm jest preset --- packages/client/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/client/package.json b/packages/client/package.json index e17ea95c..0b8b886d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -40,7 +40,6 @@ "lint": "biome check --apply src/**/*.ts" }, "jest": { - "preset": "ts-jest", "testEnvironment": "node", "collectCoverage": true, "coveragePathIgnorePatterns": [ From 8ca7e2fea095fedaa8a8ea36e649316f48b0fc16 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 10:05:35 +0200 Subject: [PATCH 40/58] update yarn.lock --- packages/client/test/node/yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/test/node/yarn.lock b/packages/client/test/node/yarn.lock index e1d75ca4..eca8c784 100644 --- a/packages/client/test/node/yarn.lock +++ b/packages/client/test/node/yarn.lock @@ -1035,7 +1035,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.2.1": +"tar@npm:^6.1.11, tar@npm:^6.1.2": version: 6.2.1 resolution: "tar@npm:6.2.1" dependencies: From 832fedf77a0fa01fb9ea19f0cabc7aa4575fdf71 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 10:28:44 +0200 Subject: [PATCH 41/58] fix subscription fetch --- packages/client/src/client.spec.ts | 8 ++++---- packages/client/src/client.ts | 16 +++++++--------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/client/src/client.spec.ts b/packages/client/src/client.spec.ts index 1ea4ffd5..31897e50 100644 --- a/packages/client/src/client.spec.ts +++ b/packages/client/src/client.spec.ts @@ -389,9 +389,9 @@ describe('OcelloidsClient', () => { const scope = nock('http://mock') .get('/health') .reply(200, '{}') - .get('/subs') + .get('/subs/xcm') .reply(200, '[]') - .get('/subs/id') + .get('/subs/xcm/id') .reply(200, '{}') const client = new OcelloidsClient({ @@ -400,8 +400,8 @@ describe('OcelloidsClient', () => { }) await client.health() - await client.allSubscriptions() - await client.getSubscription('id') + await client.allSubscriptions('xcm') + await client.getSubscription({ agentId: 'xcm', subscriptionId: 'id' }) scope.done() }) diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 5c00f7da..08229c6a 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -228,25 +228,23 @@ export class OcelloidsClient { /** * Gets a subscription by its identifier. * - * @param subscriptionId - The subscription identifier. + * @param ids - The agent and subscription identifiers. * @param init - The fetch request init. * @returns A promise that resolves with the subscription or rejects if not found. */ - async getSubscription( - subscriptionId: string, - init?: RequestInit - ): Promise> { - return this.#fetch(this.#config.httpUrl + '/subs/' + subscriptionId, init) + async getSubscription(ids: SubscriptionIds, init?: RequestInit): Promise> { + return this.#fetch(`${this.#config.httpUrl}/subs/${ids.agentId}/${ids.subscriptionId}`, init) } /** - * Lists all subscriptions. + * Lists all subscriptions for a given agent. * + * @param agentId - The agent identifier. * @param init - The fetch request init. * @returns A promise that resolves with an array of subscriptions. */ - async allSubscriptions(init?: RequestInit): Promise { - return this.#fetch(this.#config.httpUrl + '/subs', init) + async allSubscriptions(agentId: string, init?: RequestInit): Promise { + return this.#fetch(this.#config.httpUrl + '/subs/' + agentId, init) } /** From c14edb0357946fc1fd0a6ddf4a07c48a1471b39e Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 10:30:33 +0200 Subject: [PATCH 42/58] add input type --- packages/client/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 08229c6a..3394ebea 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -243,7 +243,7 @@ export class OcelloidsClient { * @param init - The fetch request init. * @returns A promise that resolves with an array of subscriptions. */ - async allSubscriptions(agentId: string, init?: RequestInit): Promise { + async allSubscriptions(agentId: string, init?: RequestInit): Promise[]> { return this.#fetch(this.#config.httpUrl + '/subs/' + agentId, init) } From 2846db22f031b2bb39907fcf6c1817fb1dc214bd Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 11:24:48 +0200 Subject: [PATCH 43/58] types message payload --- packages/client/src/xcm/types.ts | 2 +- .../guides/hurl/scenarios/transfers/dev.json | 48 +++--- .../hurl/scenarios/transfers/polkadot.json | 138 ++++++++++-------- .../src/services/agents/base/base-agent.ts | 6 +- .../server/src/services/agents/xcm/lib.ts | 2 +- .../src/services/agents/xcm/matching.ts | 4 +- .../server/src/services/agents/xcm/types.ts | 4 +- .../src/services/agents/xcm/xcm-agent.ts | 6 +- .../server/src/services/notification/types.ts | 4 +- .../src/services/notification/webhook.ts | 5 +- 10 files changed, 125 insertions(+), 94 deletions(-) diff --git a/packages/client/src/xcm/types.ts b/packages/client/src/xcm/types.ts index dc202d99..de972d5e 100644 --- a/packages/client/src/xcm/types.ts +++ b/packages/client/src/xcm/types.ts @@ -54,7 +54,7 @@ export type XcmTimeout = xcm.XcmTimeout * * @public */ -export type XcmNotifyMessage = xcm.XcmNotifyMessage +export type XcmMessagePayload = xcm.XcmMessagePayload /** * XCM assets trapped event. * diff --git a/packages/server/guides/hurl/scenarios/transfers/dev.json b/packages/server/guides/hurl/scenarios/transfers/dev.json index 2fd150fa..38bb37c0 100644 --- a/packages/server/guides/hurl/scenarios/transfers/dev.json +++ b/packages/server/guides/hurl/scenarios/transfers/dev.json @@ -1,12 +1,16 @@ [ { "id": "relay-transfers", - "origin": "urn:ocn:local:0", - "senders": "*", - "destinations": [ - "urn:ocn:local:1000", "urn:ocn:local:2000" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:local:0", + "senders": "*", + "destinations": [ + "urn:ocn:local:1000", + "urn:ocn:local:2000" + ], + "events": "*" + }, "channels": [ { "type": "log" @@ -18,12 +22,16 @@ }, { "id": "asset-hub-transfers", - "origin": "urn:ocn:local:1000", - "senders": "*", - "destinations": [ - "urn:ocn:local:0", "urn:ocn:local:2000" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:local:1000", + "senders": "*", + "destinations": [ + "urn:ocn:local:0", + "urn:ocn:local:2000" + ], + "events": "*" + }, "channels": [ { "type": "log" @@ -35,12 +43,16 @@ }, { "id": "parachain-transfers", - "origin": "urn:ocn:local:2000", - "senders": "*", - "destinations": [ - "urn:ocn:local:0", "urn:ocn:local:1000" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:local:2000", + "senders": "*", + "destinations": [ + "urn:ocn:local:0", + "urn:ocn:local:1000" + ], + "events": "*" + }, "channels": [ { "type": "log" diff --git a/packages/server/guides/hurl/scenarios/transfers/polkadot.json b/packages/server/guides/hurl/scenarios/transfers/polkadot.json index d13b99cc..4a433e1d 100644 --- a/packages/server/guides/hurl/scenarios/transfers/polkadot.json +++ b/packages/server/guides/hurl/scenarios/transfers/polkadot.json @@ -1,16 +1,19 @@ [ { "id": "polkadot-transfers", - "origin": "urn:ocn:polkadot:0", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:1000", - "urn:ocn:polkadot:2000", - "urn:ocn:polkadot:2004", - "urn:ocn:polkadot:2006", - "urn:ocn:polkadot:2034" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:0", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:1000", + "urn:ocn:polkadot:2000", + "urn:ocn:polkadot:2004", + "urn:ocn:polkadot:2006", + "urn:ocn:polkadot:2034" + ], + "events": "*" + }, "channels": [ { "type": "webhook", @@ -23,16 +26,19 @@ }, { "id": "asset-hub-transfers", - "origin": "urn:ocn:polkadot:1000", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:0", - "urn:ocn:polkadot:2000", - "urn:ocn:polkadot:2004", - "urn:ocn:polkadot:2006", - "urn:ocn:polkadot:2034" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:1000", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:0", + "urn:ocn:polkadot:2000", + "urn:ocn:polkadot:2004", + "urn:ocn:polkadot:2006", + "urn:ocn:polkadot:2034" + ], + "events": "*" + }, "channels": [ { "type": "webhook", @@ -45,16 +51,19 @@ }, { "id": "acala-transfers", - "origin": "urn:ocn:polkadot:2000", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:1000", - "urn:ocn:polkadot:0", - "urn:ocn:polkadot:2004", - "urn:ocn:polkadot:2006", - "urn:ocn:polkadot:2034" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:2000", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:1000", + "urn:ocn:polkadot:0", + "urn:ocn:polkadot:2004", + "urn:ocn:polkadot:2006", + "urn:ocn:polkadot:2034" + ], + "events": "*" + }, "channels": [ { "type": "webhook", @@ -67,16 +76,19 @@ }, { "id": "moonbeam-transfers", - "origin": "urn:ocn:polkadot:2004", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:1000", - "urn:ocn:polkadot:2000", - "urn:ocn:polkadot:0", - "urn:ocn:polkadot:2006", - "urn:ocn:polkadot:2034" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:2004", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:1000", + "urn:ocn:polkadot:2000", + "urn:ocn:polkadot:0", + "urn:ocn:polkadot:2006", + "urn:ocn:polkadot:2034" + ], + "events": "*" + }, "channels": [ { "type": "webhook", @@ -89,16 +101,19 @@ }, { "id": "astar-transfers", - "origin": "urn:ocn:polkadot:2006", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:1000", - "urn:ocn:polkadot:2000", - "urn:ocn:polkadot:2004", - "urn:ocn:polkadot:0", - "urn:ocn:polkadot:2034" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:2006", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:1000", + "urn:ocn:polkadot:2000", + "urn:ocn:polkadot:2004", + "urn:ocn:polkadot:0", + "urn:ocn:polkadot:2034" + ], + "events": "*" + }, "channels": [ { "type": "webhook", @@ -111,16 +126,19 @@ }, { "id": "hydra-transfers", - "origin": "urn:ocn:polkadot:2034", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:1000", - "urn:ocn:polkadot:2000", - "urn:ocn:polkadot:2004", - "urn:ocn:polkadot:2006", - "urn:ocn:polkadot:0" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:2034", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:1000", + "urn:ocn:polkadot:2000", + "urn:ocn:polkadot:2004", + "urn:ocn:polkadot:2006", + "urn:ocn:polkadot:0" + ], + "events": "*" + }, "channels": [ { "type": "webhook", diff --git a/packages/server/src/services/agents/base/base-agent.ts b/packages/server/src/services/agents/base/base-agent.ts index c252e92b..6321c721 100644 --- a/packages/server/src/services/agents/base/base-agent.ts +++ b/packages/server/src/services/agents/base/base-agent.ts @@ -16,9 +16,9 @@ type SubscriptionHandler = { } export abstract class BaseAgent implements Agent { - protected readonly subs: Record = {} + protected readonly subs: Record protected readonly log: Logger - protected readonly timeouts: NodeJS.Timeout[] = [] + protected readonly timeouts: NodeJS.Timeout[] protected readonly db: SubsStore protected readonly ingress: IngressConsumer protected readonly notifier: NotifierHub @@ -35,6 +35,8 @@ export abstract class BaseAgent implements Agent this.ingress = ingressConsumer this.notifier = notifier this.db = subsStore + this.subs = {} + this.timeouts = [] this.shared = { blockEvents: {}, diff --git a/packages/server/src/services/agents/xcm/lib.ts b/packages/server/src/services/agents/xcm/lib.ts index 88231f09..25cd8930 100644 --- a/packages/server/src/services/agents/xcm/lib.ts +++ b/packages/server/src/services/agents/xcm/lib.ts @@ -7,7 +7,7 @@ export type { XcmBridge, AssetsTrapped, TrappedAsset, - XcmNotifyMessage, + XcmMessagePayload, Leg, legType, XcmTerminus, diff --git a/packages/server/src/services/agents/xcm/matching.ts b/packages/server/src/services/agents/xcm/matching.ts index f3f8a544..b4f034f5 100644 --- a/packages/server/src/services/agents/xcm/matching.ts +++ b/packages/server/src/services/agents/xcm/matching.ts @@ -16,7 +16,7 @@ import { XcmBridgeInboundWithContext, XcmHop, XcmInbound, - XcmNotifyMessage, + XcmMessagePayload, XcmReceived, XcmRelayed, XcmRelayedWithContext, @@ -30,7 +30,7 @@ import { Janitor, JanitorTask } from '../../persistence/janitor.js' import { AgentRuntimeContext } from '../types.js' import { TelemetryXCMEventEmitter } from './telemetry/events.js' -export type XcmMatchedReceiver = (message: XcmNotifyMessage) => Promise | void +export type XcmMatchedReceiver = (payload: XcmMessagePayload) => Promise | void type SubLevel = AbstractSublevel export type ChainBlock = { diff --git a/packages/server/src/services/agents/xcm/types.ts b/packages/server/src/services/agents/xcm/types.ts index c4cacd21..2b4a3769 100644 --- a/packages/server/src/services/agents/xcm/types.ts +++ b/packages/server/src/services/agents/xcm/types.ts @@ -735,11 +735,11 @@ export class GenericXcmBridge implements XcmBridge { } /** - * The XCM event types. + * The XCM payloads. * * @public */ -export type XcmNotifyMessage = XcmSent | XcmReceived | XcmRelayed | XcmHop | XcmBridge +export type XcmMessagePayload = XcmSent | XcmReceived | XcmRelayed | XcmHop | XcmBridge export function isXcmSent(object: any): object is XcmSent { return object.type !== undefined && object.type === XcmNotificationType.Sent diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts index 58720400..3109e5da 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -19,8 +19,8 @@ import { XcmBridgeInboundWithContext, XcmInbound, XcmInboundWithContext, + XcmMessagePayload, XcmNotificationType, - XcmNotifyMessage, XcmRelayedWithContext, XcmSentWithContext, } from './types.js' @@ -64,7 +64,7 @@ export class XCMAgent extends BaseAgent { constructor(ctx: AgentRuntimeContext) { super(ctx) - this.#engine = new MatchingEngine(ctx, this.#onXcmWaypointReached) + this.#engine = new MatchingEngine(ctx, this.#onXcmWaypointReached.bind(this)) this.#telemetry = new (EventEmitter as new () => TelemetryXCMEventEmitter)() } @@ -193,7 +193,7 @@ export class XCMAgent extends BaseAgent { await this.#startNetworkMonitors() } - #onXcmWaypointReached(payload: XcmNotifyMessage) { + #onXcmWaypointReached(payload: XcmMessagePayload) { const { subscriptionId } = payload if (this.subs[subscriptionId]) { const { descriptor, args, sendersControl } = this.subs[subscriptionId] diff --git a/packages/server/src/services/notification/types.ts b/packages/server/src/services/notification/types.ts index ce44b2db..165cd7db 100644 --- a/packages/server/src/services/notification/types.ts +++ b/packages/server/src/services/notification/types.ts @@ -7,13 +7,13 @@ import { TelemetryNotifierEvents } from '../telemetry/types.js' * * @public */ -export type NotifyMessage = { +export type NotifyMessage = { metadata: { type: string agentId: string subscriptionId: string } - payload: AnyJson + payload: T } export type NotifierEvents = { diff --git a/packages/server/src/services/notification/webhook.ts b/packages/server/src/services/notification/webhook.ts index 4379f3fa..c4e4f10a 100644 --- a/packages/server/src/services/notification/webhook.ts +++ b/packages/server/src/services/notification/webhook.ts @@ -135,12 +135,11 @@ export class WebhookNotifier extends (EventEmitter as new () => NotifierEmitter) if (res.statusCode >= 200 && res.statusCode < 300) { this.#log.info( - 'NOTIFICATION %s agent=%s subscription=%s, endpoint=%s, payload=%j', + 'NOTIFICATION %s agent=%s subscription=%s, endpoint=%s', msg.metadata.type, msg.metadata.agentId, msg.metadata.subscriptionId, - postUrl, - msg.payload + postUrl ) this.#telemetryNotify(config, msg) } else { From 7c99850864472ba42a2d430a5a58180d831ed6cd Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Thu, 30 May 2024 10:45:58 +0200 Subject: [PATCH 44/58] =?UTF-8?q?all=20tests=20passing=20=D9=A9(=E0=B9=91?= =?UTF-8?q?=E2=9D=9B=E1=B4=97=E2=9D=9B=E0=B9=91)=DB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/agents/base/base-agent.ts | 2 +- .../src/services/notification/webhook.spec.ts | 45 ++++++++++++++++--- .../subscriptions/api/ws/protocol.spec.ts | 12 ++--- .../subscriptions/switchboard.spec.ts | 2 +- 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/packages/server/src/services/agents/base/base-agent.ts b/packages/server/src/services/agents/base/base-agent.ts index 6321c721..f590849f 100644 --- a/packages/server/src/services/agents/base/base-agent.ts +++ b/packages/server/src/services/agents/base/base-agent.ts @@ -61,7 +61,7 @@ export abstract class BaseAgent implements Agent if (this.subs[subscriptionId]) { return this.subs[subscriptionId].descriptor } else { - throw Error('subscription handler not found') + throw Error('subscription not found') } } diff --git a/packages/server/src/services/notification/webhook.spec.ts b/packages/server/src/services/notification/webhook.spec.ts index 6603a107..fb0ca546 100644 --- a/packages/server/src/services/notification/webhook.spec.ts +++ b/packages/server/src/services/notification/webhook.spec.ts @@ -11,6 +11,18 @@ import { NotifierHub } from './hub.js' import { NotifyMessage } from './types.js' import { WebhookNotifier } from './webhook.js' +const destinationContext = { + blockHash: '0xBEEF', + blockNumber: '2', + chainId: 'urn:ocn:local:1', + event: {}, + outcome: 'Success', + error: null, + messageHash: '0xCAFE', + instructions: '0x', + messageData: '0x', +} + const notification: NotifyMessage = { metadata: { type: 'xcm.ok', @@ -18,7 +30,26 @@ const notification: NotifyMessage = { subscriptionId: 'ok', }, payload: { - foo: 'bar', + type: 'xcm.ok', + subscriptionId: 'ok', + legs: [{ type: 'hrmp', from: 'urn:ocn:local:0', to: 'urn:ocn:local:1' }], + waypoint: { + ...destinationContext, + legIndex: 0, + }, + destination: destinationContext, + origin: { + blockHash: '0xBEEF', + blockNumber: '2', + chainId: 'urn:ocn:local:0', + event: {}, + outcome: 'Success', + error: null, + messageHash: '0xCAFE', + instructions: '0x', + messageData: '0x', + }, + sender: { signer: { id: 'w123', publicKey: '0x0' }, extraSigners: [] }, }, } @@ -44,12 +75,12 @@ const xmlTemplate = ` "http://dtd.worldpay.com/paymentService_v1.dtd"> - - {{origin.chainId}} - {{destination.chainId}} - {{sender.signer.id}} - {{#if waypoint.error}} - {{waypoint.error}} + + {{payload.origin.chainId}} + {{payload.destination.chainId}} + {{payload.sender.signer.id}} + {{#if payload.waypoint.error}} + {{payload.waypoint.error}} {{/if}} diff --git a/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts b/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts index 70d13c09..d2488f88 100644 --- a/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts +++ b/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts @@ -9,15 +9,16 @@ const flushPromises = () => new Promise((resolve) => jest.requireActual('ti const testSub: Subscription = { id: 'test-subscription', + agent: 'xcm', + args: { origin: '1000', senders: ['14DqgdKU6Zfh1UjdU4PYwpoHi2QTp37R6djehfbhXe9zoyQT'], - destinations: ['2000'], + destinations: ['2000']}, channels: [ { type: 'websocket', }, ], - events: '*', } describe('WebsocketProtocol', () => { @@ -127,11 +128,10 @@ describe('WebsocketProtocol', () => { ip: 'mockRequestIp', } as FastifyRequest mockSwitchboard.findSubscriptionHandler.mockImplementationOnce(() => ({ - descriptor: { ...testSub, channels: [{ type: 'log' }], - }, - })) + } + )) await websocketProtocol.handle(mockStream, mockRequest, 'test-subscription') await flushPromises() @@ -168,7 +168,7 @@ describe('WebsocketProtocol', () => { it('should close connection with error code if an error occurs', async () => { const mockStream = { close: jest.fn() } mockSwitchboard.findSubscriptionHandler.mockImplementationOnce(() => { - return undefined + throw new Error('subscription not found') }) await websocketProtocol.handle(mockStream, {} as FastifyRequest, 'testId') expect(mockStream.close).toHaveBeenCalledWith(1007, 'subscription not found') diff --git a/packages/server/src/services/subscriptions/switchboard.spec.ts b/packages/server/src/services/subscriptions/switchboard.spec.ts index 79985116..56634b51 100644 --- a/packages/server/src/services/subscriptions/switchboard.spec.ts +++ b/packages/server/src/services/subscriptions/switchboard.spec.ts @@ -143,7 +143,7 @@ describe('switchboard service', () => { expect(() => { switchboard.findSubscriptionHandler('xcm', testSub.id) - }).toThrow('subscription handler not found') + }).toThrow('subscription not found') }) /* TODO: move to agent service test From 92bdd1165c44375a922e4ce1abd7295f8c65de75 Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Thu, 30 May 2024 11:44:20 +0200 Subject: [PATCH 45/58] fix lint --- .../src/services/notification/webhook.spec.ts | 34 +++++++++---------- .../subscriptions/api/ws/protocol.spec.ts | 14 ++++---- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/server/src/services/notification/webhook.spec.ts b/packages/server/src/services/notification/webhook.spec.ts index fb0ca546..62fd9ffb 100644 --- a/packages/server/src/services/notification/webhook.spec.ts +++ b/packages/server/src/services/notification/webhook.spec.ts @@ -33,23 +33,23 @@ const notification: NotifyMessage = { type: 'xcm.ok', subscriptionId: 'ok', legs: [{ type: 'hrmp', from: 'urn:ocn:local:0', to: 'urn:ocn:local:1' }], - waypoint: { - ...destinationContext, - legIndex: 0, - }, - destination: destinationContext, - origin: { - blockHash: '0xBEEF', - blockNumber: '2', - chainId: 'urn:ocn:local:0', - event: {}, - outcome: 'Success', - error: null, - messageHash: '0xCAFE', - instructions: '0x', - messageData: '0x', - }, - sender: { signer: { id: 'w123', publicKey: '0x0' }, extraSigners: [] }, + waypoint: { + ...destinationContext, + legIndex: 0, + }, + destination: destinationContext, + origin: { + blockHash: '0xBEEF', + blockNumber: '2', + chainId: 'urn:ocn:local:0', + event: {}, + outcome: 'Success', + error: null, + messageHash: '0xCAFE', + instructions: '0x', + messageData: '0x', + }, + sender: { signer: { id: 'w123', publicKey: '0x0' }, extraSigners: [] }, }, } diff --git a/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts b/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts index d2488f88..b079ae3d 100644 --- a/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts +++ b/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts @@ -11,9 +11,10 @@ const testSub: Subscription = { id: 'test-subscription', agent: 'xcm', args: { - origin: '1000', - senders: ['14DqgdKU6Zfh1UjdU4PYwpoHi2QTp37R6djehfbhXe9zoyQT'], - destinations: ['2000']}, + origin: '1000', + senders: ['14DqgdKU6Zfh1UjdU4PYwpoHi2QTp37R6djehfbhXe9zoyQT'], + destinations: ['2000'], + }, channels: [ { type: 'websocket', @@ -128,10 +129,9 @@ describe('WebsocketProtocol', () => { ip: 'mockRequestIp', } as FastifyRequest mockSwitchboard.findSubscriptionHandler.mockImplementationOnce(() => ({ - ...testSub, - channels: [{ type: 'log' }], - } - )) + ...testSub, + channels: [{ type: 'log' }], + })) await websocketProtocol.handle(mockStream, mockRequest, 'test-subscription') await flushPromises() From 4891a86f3d17d12d3e60f4f6c00295aa57a0fc59 Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Thu, 30 May 2024 11:47:41 +0200 Subject: [PATCH 46/58] remove agent tests from subscriptions --- .../subscriptions/switchboard.spec.ts | 251 ------------------ 1 file changed, 251 deletions(-) diff --git a/packages/server/src/services/subscriptions/switchboard.spec.ts b/packages/server/src/services/subscriptions/switchboard.spec.ts index 56634b51..329ca4bc 100644 --- a/packages/server/src/services/subscriptions/switchboard.spec.ts +++ b/packages/server/src/services/subscriptions/switchboard.spec.ts @@ -1,9 +1,5 @@ -import { jest } from '@jest/globals' - import '../../testing/network.js' -import { of, throwError } from 'rxjs' - import { _services } from '../../testing/services.js' import { AgentServiceMode } from '../../types.js' import { LocalAgentService } from '../agents/local.js' @@ -13,22 +9,6 @@ import { Services } from '../types.js' import type { Switchboard } from './switchboard.js' import { Subscription } from './types' -/* TODO: move to xcm agent tests -jest.unstable_mockModule('./ops/xcmp.js', () => { - return { - extractXcmpSend: jest.fn(), - extractXcmpReceive: jest.fn(), - } -}) - -jest.unstable_mockModule('./ops/ump.js', () => { - return { - extractUmpReceive: jest.fn(), - extractUmpSend: jest.fn(), - } -}) -*/ - const SwitchboardImpl = (await import('./switchboard.js')).Switchboard const testSub: Subscription = { @@ -53,60 +33,6 @@ describe('switchboard service', () => { let agentService: AgentService beforeAll(async () => { - /* - ;(extractXcmpSend as jest.Mock).mockImplementation(() => { - return () => { - return of({ - recipient: 'urn:ocn:local:2000', - blockNumber: 1, - blockHash: '0x0', - messageHash: '0x0', - messageData: new Uint8Array([0x00]), - instructions: { - bytes: '0x0300', - }, - } as unknown as XcmSentWithContext) - } - }) - ;(extractXcmpReceive as jest.Mock).mockImplementation(() => { - return () => { - return of({ - blockNumber: { - toString: () => 1, - }, - blockHash: '0x0', - messageHash: '0x0', - outcome: 'Success', - } as unknown as XcmInboundWithContext) - } - }) - ;(extractUmpSend as jest.Mock).mockImplementation(() => { - return () => - of({ - recipient: 'urn:ocn:local:0', - blockNumber: 1, - blockHash: '0x0', - messageHash: '0x0', - messageData: new Uint8Array([0x00]), - instructions: { - bytes: '0x0300', - }, - } as unknown as XcmSentWithContext) - }) - ;(extractUmpReceive as jest.Mock).mockImplementation(() => { - return () => - of({ - recipient: 'urn:ocn:local:0', - blockNumber: { - toString: () => 1, - }, - blockHash: '0x0', - messageHash: '0x0', - outcome: 'Success', - } as unknown as XcmInboundWithContext) - }) - */ - subs = new SubsStore(_services.log, _services.rootStore) agentService = new LocalAgentService( { @@ -145,181 +71,4 @@ describe('switchboard service', () => { switchboard.findSubscriptionHandler('xcm', testSub.id) }).toThrow('subscription not found') }) - - /* TODO: move to agent service test - it('should subscribe to persisted subscriptions on start', async () => { - await subs.insert(testSub) - - await agentService.start() - - expect(switchboard.findSubscriptionHandler('xcm', testSub.id)).toBeDefined() - }) - - it('should handle relay subscriptions', async () => { - await switchboard.start() - - await switchboard.subscribe({ - ...testSub, - origin: 'urn:ocn:local:0', - }) - - expect(switchboard.findSubscriptionHandler(testSub.id)).toBeDefined() - }) - - it('should handle pipe errors', async () => { - ;(extractUmpSend as jest.Mock).mockImplementation(() => () => { - return throwError(() => new Error('errored')) - }) - ;(extractUmpReceive as jest.Mock).mockImplementation(() => () => { - return throwError(() => new Error('errored')) - }) - ;(extractXcmpSend as jest.Mock).mockImplementation(() => () => { - return throwError(() => new Error('errored')) - }) - ;(extractXcmpReceive as jest.Mock).mockImplementation(() => () => { - return throwError(() => new Error('errored')) - }) - - await switchboard.start() - - await switchboard.subscribe(testSub) - - expect(switchboard.findSubscriptionHandler(testSub.id)).toBeDefined() - - await switchboard.stop() - }) - - it('should update destination subscriptions on destinations change', async () => { - await switchboard.start() - - await switchboard.subscribe({ - ...testSub, - destinations: ['urn:ocn:local:0', 'urn:ocn:local:2000'], - }) - - const { destinationSubs } = switchboard.findSubscriptionHandler(testSub.id) - expect(destinationSubs.length).toBe(2) - expect(destinationSubs.filter((s) => s.chainId === 'urn:ocn:local:0').length).toBe(1) - expect(destinationSubs.filter((s) => s.chainId === 'urn:ocn:local:2000').length).toBe(1) - - // Remove 2000 and add 3000 to destinations - const newSub = { - ...testSub, - destinations: ['urn:ocn:local:0', 'urn:ocn:local:3000'], - } - await subs.save(newSub) - - switchboard.updateSubscription(newSub) - switchboard.updateDestinations(newSub.id) - const { destinationSubs: newDestinationSubs } = switchboard.findSubscriptionHandler(testSub.id) - expect(newDestinationSubs.length).toBe(2) - expect(newDestinationSubs.filter((s) => s.chainId === 'urn:ocn:local:0').length).toBe(1) - expect(newDestinationSubs.filter((s) => s.chainId === 'urn:ocn:local:3000').length).toBe(1) - expect(newDestinationSubs.filter((s) => s.chainId === 'urn:ocn:local:2000').length).toBe(0) - }) - - it('should create relay hrmp subscription when there is at least one HRMP pair in subscription', async () => { - await switchboard.start() - - await switchboard.subscribe(testSub) // origin: '1000', destinations: ['2000'] - - const { relaySub } = switchboard.findSubscriptionHandler(testSub.id) - expect(relaySub).toBeDefined() - }) - - it('should not create relay hrmp subscription when the origin is a relay chain', async () => { - await switchboard.start() - - await switchboard.subscribe({ - ...testSub, - origin: 'urn:ocn:local:0', // origin: '0', destinations: ['2000'] - }) - - const { relaySub } = switchboard.findSubscriptionHandler(testSub.id) - expect(relaySub).not.toBeDefined() - }) - - it('should not create relay hrmp subscription when there are no HRMP pairs in the subscription', async () => { - await switchboard.start() - - await switchboard.subscribe({ - ...testSub, - destinations: ['urn:ocn:local:0'], // origin: '1000', destinations: ['0'] - }) - - const { relaySub } = switchboard.findSubscriptionHandler(testSub.id) - expect(relaySub).not.toBeDefined() - }) - - it('should not create relay hrmp subscription when relayed events are not requested', async () => { - await switchboard.start() - - await switchboard.subscribe({ - ...testSub, - events: [XcmNotificationType.Received], - }) - - const { relaySub } = switchboard.findSubscriptionHandler(testSub.id) - expect(relaySub).not.toBeDefined() - }) - - it('should create relay hrmp subscription if relayed event is added', async () => { - await switchboard.start() - - await switchboard.subscribe({ - ...testSub, - events: [XcmNotificationType.Received], - }) - - const { relaySub } = switchboard.findSubscriptionHandler(testSub.id) - expect(relaySub).not.toBeDefined() - - // add relayed event to subscription - const newSub = { - ...testSub, - events: [XcmNotificationType.Received, XcmNotificationType.Relayed], - } - await subs.save(newSub) - - switchboard.updateSubscription(newSub) - switchboard.updateEvents(newSub.id) - const { relaySub: newRelaySub } = switchboard.findSubscriptionHandler(testSub.id) - expect(newRelaySub).toBeDefined() - }) - - it('should remove relay hrmp subscription if relayed event is removed', async () => { - await switchboard.start() - - await switchboard.subscribe({ - ...testSub, - events: '*', - }) - - const { relaySub } = switchboard.findSubscriptionHandler(testSub.id) - expect(relaySub).toBeDefined() - - // remove relayed event - const newSub = { - ...testSub, - events: [XcmNotificationType.Received, XcmNotificationType.Sent], - } - await subs.save(newSub) - - switchboard.updateSubscription(newSub) - switchboard.updateEvents(newSub.id) - const { relaySub: newRelaySub } = switchboard.findSubscriptionHandler(testSub.id) - expect(newRelaySub).not.toBeDefined() - }) - - it('should notify on matched HRMP', async () => { - await switchboard.start() - - await switchboard.subscribe(testSub) - - await switchboard.stop() - - // we can extract the NotifierHub as a service - // to test the matched, but not really worth right now - }) - */ }) From 235b3d677fd25f41047fb5d944a894e4ba1ecfb8 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 11:50:22 +0200 Subject: [PATCH 47/58] bumped to 2.0.0 :japanese_ogre: --- packages/client/package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/client/package.json b/packages/client/package.json index 0b8b886d..dec28b40 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@sodazone/ocelloids-client", - "version": "1.0.3-dev.0", + "version": "2.0.0", "type": "module", "description": "Ocelloids client library", "author": "SO/DA ", @@ -77,6 +77,5 @@ }, "dependencies": { "isows": "^1.0.4" - }, - "stableVersion": "1.0.2" + } } From 45776400fb84505342bd5a8faf824f0cf0c20a00 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 11:54:28 +0200 Subject: [PATCH 48/58] bumped to 2.0.0 :japanese_ogre: --- packages/server/package.json | 2 +- packages/server/src/version.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 10d59cda..e10927ea 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@sodazone/ocelloids-service-node", - "version": "1.0.0", + "version": "2.0.0", "type": "module", "description": "Ocelloids Service Node", "author": "SO/DA ", diff --git a/packages/server/src/version.ts b/packages/server/src/version.ts index 5976b8b3..f939ac55 100644 --- a/packages/server/src/version.ts +++ b/packages/server/src/version.ts @@ -1 +1 @@ -export default '1.0.0' +export default '2.0.0' From 9c79cf72584198f4361f97b397cc102985ac1abb Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 12:01:57 +0200 Subject: [PATCH 49/58] fix logs --- packages/server/src/services/agents/local.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/src/services/agents/local.ts b/packages/server/src/services/agents/local.ts index b85a660e..183d6e68 100644 --- a/packages/server/src/services/agents/local.ts +++ b/packages/server/src/services/agents/local.ts @@ -60,21 +60,21 @@ export class LocalAgentService implements AgentService { async start() { for (const [id, agent] of Object.entries(this.#agents)) { - this.#log.info('[local:agents] Starting agent %s', id) + this.#log.info('[LOCAL] Starting agent %s (%s)', id, agent.metadata.name ?? 'unnamed') await agent.start() } } async stop() { for (const [id, agent] of Object.entries(this.#agents)) { - this.#log.info('[local:agents] Stopping agent %s', id) + this.#log.info('[LOCAL] Stopping agent %s', id) await agent.stop() } } collectTelemetry() { for (const [id, agent] of Object.entries(this.#agents)) { - this.#log.info('[local:agents] collect telemetry from agent %s', id) + this.#log.info('[LOCAL] collect telemetry from agent %s', id) agent.collectTelemetry() } } From 58ff12661c48ebcf55966845fc6f40f79c90808e Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 12:02:19 +0200 Subject: [PATCH 50/58] fix logs --- packages/server/src/services/agents/local.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/services/agents/local.ts b/packages/server/src/services/agents/local.ts index 183d6e68..eed38acf 100644 --- a/packages/server/src/services/agents/local.ts +++ b/packages/server/src/services/agents/local.ts @@ -60,14 +60,14 @@ export class LocalAgentService implements AgentService { async start() { for (const [id, agent] of Object.entries(this.#agents)) { - this.#log.info('[LOCAL] Starting agent %s (%s)', id, agent.metadata.name ?? 'unnamed') + this.#log.info('[LOCAL] starting agent %s (%s)', id, agent.metadata.name ?? 'unnamed') await agent.start() } } async stop() { for (const [id, agent] of Object.entries(this.#agents)) { - this.#log.info('[LOCAL] Stopping agent %s', id) + this.#log.info('[LOCAL] stopping agent %s', id) await agent.stop() } } From 7577105ce449158a776afac12c750cc048e2553a Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 12:26:17 +0200 Subject: [PATCH 51/58] update guides payloads --- .../guides/hurl/scenarios/templates/data.json | 212 +++++++----------- .../hurl/scenarios/transfers/rw-bridge.json | 60 ++--- .../hurl/tests/subscriptions/0_create.hurl | 11 +- .../hurl/tests/subscriptions/1_list.hurl | 2 +- .../hurl/tests/subscriptions/2_get.hurl | 2 +- .../hurl/tests/subscriptions/3_update.hurl | 14 +- .../hurl/tests/subscriptions/4_delete.hurl | 2 +- 7 files changed, 137 insertions(+), 166 deletions(-) diff --git a/packages/server/guides/hurl/scenarios/templates/data.json b/packages/server/guides/hurl/scenarios/templates/data.json index d8e60070..bfac1a9f 100644 --- a/packages/server/guides/hurl/scenarios/templates/data.json +++ b/packages/server/guides/hurl/scenarios/templates/data.json @@ -1,18 +1,21 @@ [ { "id": "acala-transfers", - "origin": "urn:ocn:polkadot:2000", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:0", - "urn:ocn:polkadot:1000", - "urn:ocn:polkadot:2004", - "urn:ocn:polkadot:2034", - "urn:ocn:polkadot:2104" - ], - "events": [ - "xcm.received" - ], + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:2000", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:0", + "urn:ocn:polkadot:1000", + "urn:ocn:polkadot:2004", + "urn:ocn:polkadot:2034", + "urn:ocn:polkadot:2104" + ], + "events": [ + "xcm.received" + ] + }, "channels": [ { "type": "webhook", @@ -22,33 +25,24 @@ }, { "id": "moonbeam-transfers", - "origin": "urn:ocn:polkadot:2004", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:0", - "urn:ocn:polkadot:1000", - "urn:ocn:polkadot:2000", - "urn:ocn:polkadot:2034", - "urn:ocn:polkadot:2104" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:2004", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:0", + "urn:ocn:polkadot:1000", + "urn:ocn:polkadot:2000", + "urn:ocn:polkadot:2034", + "urn:ocn:polkadot:2104" + ], + "events": "*" + }, "channels": [ { "type": "webhook", "contentType": "text/plain", - "events": [ - "xcm.sent" - ], - "template": "SENT id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", - "url": "https://en785006d7bvj.x.pipedream.net" - }, - { - "type": "webhook", - "contentType": "text/plain", - "events": [ - "xcm.received" - ], - "template": "RECEIVED id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", + "template": "TEMPLATE id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", "url": "https://en785006d7bvj.x.pipedream.net" }, { @@ -58,33 +52,24 @@ }, { "id": "polkadot-transfers", - "origin": "urn:ocn:polkadot:0", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:2000", - "urn:ocn:polkadot:1000", - "urn:ocn:polkadot:2004", - "urn:ocn:polkadot:2034", - "urn:ocn:polkadot:2104" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:0", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:2000", + "urn:ocn:polkadot:1000", + "urn:ocn:polkadot:2004", + "urn:ocn:polkadot:2034", + "urn:ocn:polkadot:2104" + ], + "events": "*" + }, "channels": [ { "type": "webhook", "contentType": "text/plain", - "events": [ - "xcm.sent" - ], - "template": "SENT id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", - "url": "https://en785006d7bvj.x.pipedream.net" - }, - { - "type": "webhook", - "contentType": "text/plain", - "events": [ - "xcm.received" - ], - "template": "RECEIVED id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", + "template": "TEMPLATE id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", "url": "https://en785006d7bvj.x.pipedream.net" }, { @@ -94,33 +79,24 @@ }, { "id": "asset-hub-transfers", - "origin": "urn:ocn:polkadot:1000", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:2000", - "urn:ocn:polkadot:0", - "urn:ocn:polkadot:2004", - "urn:ocn:polkadot:2034", - "urn:ocn:polkadot:2104" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:1000", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:2000", + "urn:ocn:polkadot:0", + "urn:ocn:polkadot:2004", + "urn:ocn:polkadot:2034", + "urn:ocn:polkadot:2104" + ], + "events": "*" + }, "channels": [ { "type": "webhook", "contentType": "text/plain", - "events": [ - "xcm.sent" - ], - "template": "SENT id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", - "url": "https://en785006d7bvj.x.pipedream.net" - }, - { - "type": "webhook", - "contentType": "text/plain", - "events": [ - "xcm.received" - ], - "template": "RECEIVED id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", + "template": "TEMPLATE id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", "url": "https://en785006d7bvj.x.pipedream.net" }, { @@ -130,33 +106,24 @@ }, { "id": "hydra-transfers", - "origin": "urn:ocn:polkadot:2034", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:2000", - "urn:ocn:polkadot:1000", - "urn:ocn:polkadot:2004", - "urn:ocn:polkadot:0", - "urn:ocn:polkadot:2104" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:2034", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:2000", + "urn:ocn:polkadot:1000", + "urn:ocn:polkadot:2004", + "urn:ocn:polkadot:0", + "urn:ocn:polkadot:2104" + ], + "events": "*" + }, "channels": [ { "type": "webhook", "contentType": "text/plain", - "events": [ - "xcm.sent" - ], - "template": "SENT id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", - "url": "https://en785006d7bvj.x.pipedream.net" - }, - { - "type": "webhook", - "contentType": "text/plain", - "events": [ - "xcm.received" - ], - "template": "RECEIVED id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", + "template": "TEMPLATE id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", "url": "https://en785006d7bvj.x.pipedream.net" }, { @@ -166,33 +133,24 @@ }, { "id": "manta-transfers", - "origin": "urn:ocn:polkadot:2104", - "senders": "*", - "destinations": [ - "urn:ocn:polkadot:2000", - "urn:ocn:polkadot:1000", - "urn:ocn:polkadot:2004", - "urn:ocn:polkadot:2034", - "urn:ocn:polkadot:0" - ], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:2104", + "senders": "*", + "destinations": [ + "urn:ocn:polkadot:2000", + "urn:ocn:polkadot:1000", + "urn:ocn:polkadot:2004", + "urn:ocn:polkadot:2034", + "urn:ocn:polkadot:0" + ], + "events": "*" + }, "channels": [ { "type": "webhook", "contentType": "text/plain", - "events": [ - "xcm.sent" - ], - "template": "SENT id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", - "url": "https://en785006d7bvj.x.pipedream.net" - }, - { - "type": "webhook", - "contentType": "text/plain", - "events": [ - "xcm.received" - ], - "template": "RECEIVED id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", + "template": "TEMPLATE id={{subscriptionId}} leg={{waypoint.legIndex}} legs=[{{#each legs}}(from={{from}}, to={{to}}){{/each}}]", "url": "https://en785006d7bvj.x.pipedream.net" }, { diff --git a/packages/server/guides/hurl/scenarios/transfers/rw-bridge.json b/packages/server/guides/hurl/scenarios/transfers/rw-bridge.json index 90fa6999..e1dee3a8 100644 --- a/packages/server/guides/hurl/scenarios/transfers/rw-bridge.json +++ b/packages/server/guides/hurl/scenarios/transfers/rw-bridge.json @@ -1,20 +1,25 @@ [ { "id": "rococo-asset-hub-transfers", - "origin": "urn:ocn:rococo:1000", - "senders": "*", - "destinations": [ - "urn:ocn:rococo:0", - "urn:ocn:rococo:1013", - "urn:ocn:westend:1000", - "urn:ocn:westend:1002", - "urn:ocn:westend:0" - ], - "bridges": [{ - "type": "pk-bridge", - "subscription": "westend-bridge-hub-transfers" - }], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:rococo:1000", + "senders": "*", + "destinations": [ + "urn:ocn:rococo:0", + "urn:ocn:rococo:1013", + "urn:ocn:westend:1000", + "urn:ocn:westend:1002", + "urn:ocn:westend:0" + ], + "bridges": [ + { + "type": "pk-bridge", + "subscription": "westend-bridge-hub-transfers" + } + ], + "events": "*" + }, "channels": [ { "type": "webhook", @@ -27,17 +32,22 @@ }, { "id": "westend-bridge-hub-transfers", - "origin": "urn:ocn:westend:1002", - "senders": "*", - "destinations": [ - "urn:ocn:westend:1000", - "urn:ocn:westend:0" - ], - "bridges": [{ - "type": "pk-bridge", - "subscription": "rococo-asset-hub-transfers" - }], - "events": "*", + "agent": "xcm", + "args": { + "origin": "urn:ocn:westend:1002", + "senders": "*", + "destinations": [ + "urn:ocn:westend:1000", + "urn:ocn:westend:0" + ], + "bridges": [ + { + "type": "pk-bridge", + "subscription": "rococo-asset-hub-transfers" + } + ], + "events": "*" + }, "channels": [ { "type": "webhook", diff --git a/packages/server/guides/hurl/tests/subscriptions/0_create.hurl b/packages/server/guides/hurl/tests/subscriptions/0_create.hurl index d4e99521..6266ec0f 100644 --- a/packages/server/guides/hurl/tests/subscriptions/0_create.hurl +++ b/packages/server/guides/hurl/tests/subscriptions/0_create.hurl @@ -3,10 +3,13 @@ POST {{base-url}}/subs ```json { "id": "test-1", - "origin": "urn:ocn:polkadot:0", - "senders": ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"], - "destinations": ["urn:ocn:polkadot:1000"], - "events": ["xcm.received"], + "agent": "xcm", + "args": { + "origin": "urn:ocn:polkadot:0", + "senders": ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"], + "destinations": ["urn:ocn:polkadot:1000"], + "events": ["xcm.received"] + }, "channels": [{ "type": "webhook", "url": "https://webhook.site/faf64821-cb4d-41ad-bb81-fd119e80ad02" diff --git a/packages/server/guides/hurl/tests/subscriptions/1_list.hurl b/packages/server/guides/hurl/tests/subscriptions/1_list.hurl index 57fffff6..1af967e2 100644 --- a/packages/server/guides/hurl/tests/subscriptions/1_list.hurl +++ b/packages/server/guides/hurl/tests/subscriptions/1_list.hurl @@ -1,3 +1,3 @@ # List Subscriptions -GET {{base-url}}/subs +GET {{base-url}}/subs/xcm HTTP 200 \ No newline at end of file diff --git a/packages/server/guides/hurl/tests/subscriptions/2_get.hurl b/packages/server/guides/hurl/tests/subscriptions/2_get.hurl index d0f007cb..0203b954 100644 --- a/packages/server/guides/hurl/tests/subscriptions/2_get.hurl +++ b/packages/server/guides/hurl/tests/subscriptions/2_get.hurl @@ -1,3 +1,3 @@ # Get Subscription -GET {{base-url}}/subs/test-1 +GET {{base-url}}/subs/xcm/test-1 HTTP 200 \ No newline at end of file diff --git a/packages/server/guides/hurl/tests/subscriptions/3_update.hurl b/packages/server/guides/hurl/tests/subscriptions/3_update.hurl index baf35c94..5159070e 100644 --- a/packages/server/guides/hurl/tests/subscriptions/3_update.hurl +++ b/packages/server/guides/hurl/tests/subscriptions/3_update.hurl @@ -1,5 +1,5 @@ # Update Senders -PATCH {{base-url}}/subs/test-1 +PATCH {{base-url}}/subs/xcm/test-1 ```json [ { "op": "add", "path": "/senders/-", "value": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y" }, @@ -11,7 +11,7 @@ PATCH {{base-url}}/subs/test-1 HTTP 200 # Update Destinations -PATCH {{base-url}}/subs/test-1 +PATCH {{base-url}}/subs/xcm/test-1 ```json [ { "op": "add", "path": "/destinations/-", "value": "urn:ocn:polkadot:2000" }, @@ -21,7 +21,7 @@ PATCH {{base-url}}/subs/test-1 HTTP 200 # Update Notify -PATCH {{base-url}}/subs/test-1 +PATCH {{base-url}}/subs/xcm/test-1 ```json [ { @@ -35,9 +35,9 @@ PATCH {{base-url}}/subs/test-1 ``` # Check Updates -GET {{base-url}}/subs/test-1 +GET {{base-url}}/subs/xcm/test-1 HTTP 200 [Asserts] -jsonpath "$.destinations" count == 2 -jsonpath "$.senders" count == 3 -jsonpath "$.channels[0].type" == "log" +jsonpath "$.args.destinations" count == 2 +jsonpath "$.args.senders" count == 3 +jsonpath "$.args.channels[0].type" == "log" diff --git a/packages/server/guides/hurl/tests/subscriptions/4_delete.hurl b/packages/server/guides/hurl/tests/subscriptions/4_delete.hurl index 99566636..67144dc0 100644 --- a/packages/server/guides/hurl/tests/subscriptions/4_delete.hurl +++ b/packages/server/guides/hurl/tests/subscriptions/4_delete.hurl @@ -1,3 +1,3 @@ # Delete Subscription -DELETE {{base-url}}/subs/test-1 +DELETE {{base-url}}/subs/xcm/test-1 HTTP 200 From f7abb7ca5f5782be471d67823d221dbe3ab331d7 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 12:28:01 +0200 Subject: [PATCH 52/58] update guides payloads --- packages/server/guides/artillery/on_demand_subscriptions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/guides/artillery/on_demand_subscriptions.yaml b/packages/server/guides/artillery/on_demand_subscriptions.yaml index 4f340603..899a99f9 100644 --- a/packages/server/guides/artillery/on_demand_subscriptions.yaml +++ b/packages/server/guides/artillery/on_demand_subscriptions.yaml @@ -13,5 +13,5 @@ scenarios: flow: - connect: "{{ target }}/ws/subs" - send: > - { "origin": "urn:ocn:polkadot:2004", "senders": "*", "destinations": [ "urn:ocn:polkadot:0", "urn:ocn:polkadot:1000", "urn:ocn:polkadot:2000", "urn:ocn:polkadot:2034", "urn:ocn:polkadot:2104" ] } + { "agent": "xcm", "args": { "origin": "urn:ocn:polkadot:2004", "senders": "*", "destinations": [ "urn:ocn:polkadot:0", "urn:ocn:polkadot:1000", "urn:ocn:polkadot:2000", "urn:ocn:polkadot:2034", "urn:ocn:polkadot:2104" ] } } - think: 600 \ No newline at end of file From 299ed6a333cbed548e46f210e66b9d23daad3a0f Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 12:30:32 +0200 Subject: [PATCH 53/58] update safe id checks --- packages/server/src/services/agents/types.ts | 10 +++++++--- packages/server/src/services/subscriptions/types.ts | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/server/src/services/agents/types.ts b/packages/server/src/services/agents/types.ts index 169b48e0..d42bb1cc 100644 --- a/packages/server/src/services/agents/types.ts +++ b/packages/server/src/services/agents/types.ts @@ -9,9 +9,13 @@ import { SubsStore } from '../persistence/subs.js' import { NotificationListener, Subscription } from '../subscriptions/types.js' import { DB, Logger } from '../types.js' -export const $AgentId = z.string({ - required_error: 'agent id is required', -}) +export const $AgentId = z + .string({ + required_error: 'agent id is required', + }) + .min(1) + .max(100) + .regex(/[A-Za-z0-9.\-_]+/) export type AgentId = z.infer diff --git a/packages/server/src/services/subscriptions/types.ts b/packages/server/src/services/subscriptions/types.ts index 342d4956..f4924178 100644 --- a/packages/server/src/services/subscriptions/types.ts +++ b/packages/server/src/services/subscriptions/types.ts @@ -43,7 +43,7 @@ export const $SafeId = z }) .min(1) .max(100) - .regex(/[A-Za-z0-9:.\-_]+/) + .regex(/[A-Za-z0-9.\-_]+/) /** * A hex string starting with '0x'. From 995f6c171d6eecaef7075259d57f80a94dcd6c38 Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 12:41:57 +0200 Subject: [PATCH 54/58] update patch paths --- packages/server/src/services/agents/xcm/xcm-agent.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts index 3109e5da..e352a204 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -85,15 +85,15 @@ export class XCMAgent extends BaseAgent { sub.args = args sub.descriptor = descriptor - if (hasOp(patch, '/senders')) { + if (hasOp(patch, '/args/senders')) { this.#updateSenders(subscriptionId) } - if (hasOp(patch, '/destinations')) { + if (hasOp(patch, '/args/destinations')) { this.#updateDestinations(subscriptionId) } - if (hasOp(patch, '/events')) { + if (hasOp(patch, '/args/events')) { this.#updateEvents(subscriptionId) } From de2ff5438ec289abf64e31380981af1d39f22ebf Mon Sep 17 00:00:00 2001 From: Marc Fornos Date: Thu, 30 May 2024 12:43:09 +0200 Subject: [PATCH 55/58] update patch paths --- .../guides/hurl/tests/subscriptions/3_update.hurl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/server/guides/hurl/tests/subscriptions/3_update.hurl b/packages/server/guides/hurl/tests/subscriptions/3_update.hurl index 5159070e..a585ff2d 100644 --- a/packages/server/guides/hurl/tests/subscriptions/3_update.hurl +++ b/packages/server/guides/hurl/tests/subscriptions/3_update.hurl @@ -2,10 +2,10 @@ PATCH {{base-url}}/subs/xcm/test-1 ```json [ - { "op": "add", "path": "/senders/-", "value": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y" }, - { "op": "add", "path": "/senders/-", "value": "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" }, - { "op": "add", "path": "/senders/-", "value": "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" }, - { "op": "remove", "path": "/senders/0" } + { "op": "add", "path": "/args/senders/-", "value": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y" }, + { "op": "add", "path": "/args/senders/-", "value": "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" }, + { "op": "add", "path": "/args/senders/-", "value": "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" }, + { "op": "remove", "path": "/args/senders/0" } ] ``` HTTP 200 @@ -14,8 +14,8 @@ HTTP 200 PATCH {{base-url}}/subs/xcm/test-1 ```json [ - { "op": "add", "path": "/destinations/-", "value": "urn:ocn:polkadot:2000" }, - { "op": "replace", "path": "/destinations/0", "value": "urn:ocn:polkadot:2004" } + { "op": "add", "path": "/args/destinations/-", "value": "urn:ocn:polkadot:2000" }, + { "op": "replace", "path": "/args/destinations/0", "value": "urn:ocn:polkadot:2004" } ] ``` HTTP 200 @@ -40,4 +40,4 @@ HTTP 200 [Asserts] jsonpath "$.args.destinations" count == 2 jsonpath "$.args.senders" count == 3 -jsonpath "$.args.channels[0].type" == "log" +jsonpath "$.channels[0].type" == "log" From b588e8321bf09a9c6b11ce31c8a29b797241b0fd Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Thu, 30 May 2024 12:50:06 +0200 Subject: [PATCH 56/58] decouple agent get descriptor and get handler --- .../src/services/agents/base/base-agent.ts | 18 +++++++++++------- packages/server/src/services/agents/types.ts | 9 +++++++-- .../src/services/agents/xcm/xcm-agent.ts | 10 ++++++++++ .../services/subscriptions/api/ws/protocol.ts | 2 +- .../services/subscriptions/switchboard.spec.ts | 6 +++--- .../src/services/subscriptions/switchboard.ts | 8 ++++---- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/packages/server/src/services/agents/base/base-agent.ts b/packages/server/src/services/agents/base/base-agent.ts index f590849f..c5794f57 100644 --- a/packages/server/src/services/agents/base/base-agent.ts +++ b/packages/server/src/services/agents/base/base-agent.ts @@ -8,14 +8,10 @@ import { NotifierHub } from '../../notification/hub.js' import { SubsStore } from '../../persistence/subs.js' import { HexString, Subscription } from '../../subscriptions/types.js' import { Logger, NetworkURN } from '../../types.js' -import { Agent, AgentId, AgentMetadata, AgentRuntimeContext } from '../types.js' +import { Agent, AgentId, AgentMetadata, AgentRuntimeContext, SubscriptionHandler } from '../types.js' import { GetStorageAt } from '../xcm/types-augmented.js' -type SubscriptionHandler = { - descriptor: Subscription -} - -export abstract class BaseAgent implements Agent { +export abstract class BaseAgent implements Agent { protected readonly subs: Record protected readonly log: Logger protected readonly timeouts: NodeJS.Timeout[] @@ -57,7 +53,7 @@ export abstract class BaseAgent implements Agent return await this.db.getByAgentId(this.id) } - getSubscriptionHandler(subscriptionId: string): Subscription { + getSubscriptionDescriptor(subscriptionId: string): Subscription { if (this.subs[subscriptionId]) { return this.subs[subscriptionId].descriptor } else { @@ -65,6 +61,14 @@ export abstract class BaseAgent implements Agent } } + getSubscriptionHandler(subscriptionId: string): T { + if (this.subs[subscriptionId]) { + return this.subs[subscriptionId] + } else { + throw Error('subscription handler not found') + } + } + abstract getInputSchema(): z.ZodSchema abstract subscribe(subscription: Subscription): Promise abstract unsubscribe(subscriptionId: string): Promise diff --git a/packages/server/src/services/agents/types.ts b/packages/server/src/services/agents/types.ts index d42bb1cc..8d028e89 100644 --- a/packages/server/src/services/agents/types.ts +++ b/packages/server/src/services/agents/types.ts @@ -45,14 +45,19 @@ export type AgentMetadata = { description?: string } -export interface Agent { +export type SubscriptionHandler = { + descriptor: Subscription +} + +export interface Agent { collectTelemetry(): void get id(): AgentId get metadata(): AgentMetadata getSubscriptionById(subscriptionId: string): Promise getAllSubscriptions(): Promise getInputSchema(): z.ZodSchema - getSubscriptionHandler(subscriptionId: string): Subscription + getSubscriptionDescriptor(subscriptionId: string): Subscription + getSubscriptionHandler(subscriptionId: string): T subscribe(subscription: Subscription): Promise unsubscribe(subscriptionId: string): Promise update(subscriptionId: string, patch: Operation[]): Promise diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts index e352a204..ed43b014 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -97,6 +97,8 @@ export class XCMAgent extends BaseAgent { this.#updateEvents(subscriptionId) } + this.#updateDescriptor(descriptor) + return descriptor } else { throw Error('Only operations on these paths are allowed: ' + allowedPaths.join(',')) @@ -843,6 +845,14 @@ export class XCMAgent extends BaseAgent { } } + #updateDescriptor(sub: Subscription) { + if (this.subs[sub.id]) { + this.subs[sub.id].descriptor = sub + } else { + this.log.warn('trying to update an unknown subscription %s', sub.id) + } + } + #validateChainIds(chainIds: NetworkURN[]) { chainIds.forEach((chainId) => { if (!this.ingress.isNetworkDefined(chainId)) { diff --git a/packages/server/src/services/subscriptions/api/ws/protocol.ts b/packages/server/src/services/subscriptions/api/ws/protocol.ts index 8391ab03..a30278ff 100644 --- a/packages/server/src/services/subscriptions/api/ws/protocol.ts +++ b/packages/server/src/services/subscriptions/api/ws/protocol.ts @@ -140,7 +140,7 @@ export default class WebsocketProtocol extends (EventEmitter as new () => Teleme } else { // existing subscriptions const { agentId, subscriptionId } = ids - const subscription = this.#switchboard.findSubscriptionHandler(agentId, subscriptionId) + const subscription = this.#switchboard.findSubscription(agentId, subscriptionId) this.#addSubscriber(subscription, socket, request) } } catch (error) { diff --git a/packages/server/src/services/subscriptions/switchboard.spec.ts b/packages/server/src/services/subscriptions/switchboard.spec.ts index 329ca4bc..9feecbe4 100644 --- a/packages/server/src/services/subscriptions/switchboard.spec.ts +++ b/packages/server/src/services/subscriptions/switchboard.spec.ts @@ -57,18 +57,18 @@ describe('switchboard service', () => { it('should add a subscription by agent', async () => { await switchboard.subscribe(testSub) - expect(switchboard.findSubscriptionHandler('xcm', testSub.id)).toBeDefined() + expect(switchboard.findSubscription('xcm', testSub.id)).toBeDefined() expect(await subs.getById('xcm', testSub.id)).toBeDefined() }) it('should remove subscription by agent', async () => { - expect(switchboard.findSubscriptionHandler('xcm', testSub.id)).toBeDefined() + expect(switchboard.findSubscription('xcm', testSub.id)).toBeDefined() expect(await subs.getById('xcm', testSub.id)).toBeDefined() await switchboard.unsubscribe('xcm', testSub.id) expect(() => { - switchboard.findSubscriptionHandler('xcm', testSub.id) + switchboard.findSubscription('xcm', testSub.id) }).toThrow('subscription not found') }) }) diff --git a/packages/server/src/services/subscriptions/switchboard.ts b/packages/server/src/services/subscriptions/switchboard.ts index 0be56a4a..023aee4f 100644 --- a/packages/server/src/services/subscriptions/switchboard.ts +++ b/packages/server/src/services/subscriptions/switchboard.ts @@ -116,7 +116,7 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte async unsubscribe(agentId: AgentId, subscriptionId: string) { try { const agent = this.#agentService.getAgentById(agentId) - const { ephemeral } = agent.getSubscriptionHandler(subscriptionId) + const { ephemeral } = agent.getSubscriptionDescriptor(subscriptionId) await agent.unsubscribe(subscriptionId) if (ephemeral) { @@ -138,13 +138,13 @@ export class Switchboard extends (EventEmitter as new () => TelemetryEventEmitte } /** - * Gets a subscription handler by id. + * Gets a subscription of an agent by agent id and subscription id. * * @param {AgentId} agentId The agent identifier. * @param {string} subscriptionId The subscription identifier. */ - findSubscriptionHandler(agentId: AgentId, subscriptionId: string) { - return this.#agentService.getAgentById(agentId).getSubscriptionHandler(subscriptionId) + findSubscription(agentId: AgentId, subscriptionId: string): Subscription { + return this.#agentService.getAgentById(agentId).getSubscriptionDescriptor(subscriptionId) } /** From 238bbbb4a9dd1d16633434cc54bcc51242731399 Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Thu, 30 May 2024 13:06:39 +0200 Subject: [PATCH 57/58] add xcm agent tests --- .../src/services/agents/xcm/xcm-agent.spec.ts | 358 ++++++++++++++++++ .../subscriptions/api/ws/protocol.spec.ts | 6 +- 2 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 packages/server/src/services/agents/xcm/xcm-agent.spec.ts diff --git a/packages/server/src/services/agents/xcm/xcm-agent.spec.ts b/packages/server/src/services/agents/xcm/xcm-agent.spec.ts new file mode 100644 index 00000000..9d5b81aa --- /dev/null +++ b/packages/server/src/services/agents/xcm/xcm-agent.spec.ts @@ -0,0 +1,358 @@ +import { jest } from '@jest/globals' + +import { of, throwError } from 'rxjs' + +import '../../../testing/network.js' + +import { Subscription } from '../../subscriptions/types.js' +import { extractUmpReceive, extractUmpSend } from './ops/ump.js' +import * as XcmpOps from './ops/xcmp.js' +import { XcmInboundWithContext, XcmNotificationType, XcmSentWithContext, XCMSubscriptionHandler } from './types.js' +import { _services } from '../../../testing/services.js' +import { AgentServiceMode } from '../../../types.js' +import { Services } from '../../index.js' +import { SubsStore } from '../../persistence/subs.js' +import { LocalAgentService } from '../local.js' +import { AgentService } from '../types.js' +import { XCMAgent } from './xcm-agent.js' + +const mockExtractXcmpReceive = jest.fn() +const mockExtractXcmpSend = jest.fn() +jest.unstable_mockModule('./ops/xcmp.js', () => { + return { + __esModule: true, + ...XcmpOps, + extractXcmpReceive: mockExtractXcmpReceive, + extractXcmpSend: mockExtractXcmpSend, + } +}) + +const mockExtractUmpReceive = jest.fn() +const mockExtractUmpSend = jest.fn() +jest.unstable_mockModule('./ops/ump.js', () => { + return { + extractUmpReceive: mockExtractUmpReceive, + extractUmpSend: mockExtractUmpSend, + } +}) + +const testSub: Subscription = { + id: '1000:2000:0', + agent: 'xcm', + args: { + origin: 'urn:ocn:local:1000', + senders: ['14DqgdKU6Zfh1UjdU4PYwpoHi2QTp37R6djehfbhXe9zoyQT'], + events: '*', + destinations: ['urn:ocn:local:2000'], + }, + channels: [ + { + type: 'log', + }, + ], +} + +describe('switchboard service', () => { + let subs: SubsStore + let agentService: AgentService + let xcmAgent: XCMAgent + + beforeEach(async () => { + mockExtractXcmpSend.mockImplementation(() => { + return () => { + return of({ + recipient: 'urn:ocn:local:2000', + blockNumber: 1, + blockHash: '0x0', + messageHash: '0x0', + messageData: new Uint8Array([0x00]), + instructions: { + bytes: '0x0300', + }, + } as unknown as XcmSentWithContext) + } + }) + + mockExtractXcmpReceive.mockImplementation(() => { + return () => { + return of({ + blockNumber: { + toString: () => 1, + }, + blockHash: '0x0', + messageHash: '0x0', + outcome: 'Success', + } as unknown as XcmInboundWithContext) + } + }) + mockExtractUmpSend.mockImplementation(() => { + return () => + of({ + recipient: 'urn:ocn:local:0', + blockNumber: 1, + blockHash: '0x0', + messageHash: '0x0', + messageData: new Uint8Array([0x00]), + instructions: { + bytes: '0x0300', + }, + } as unknown as XcmSentWithContext) + }) + mockExtractUmpReceive.mockImplementation(() => { + return () => + of({ + recipient: 'urn:ocn:local:0', + blockNumber: { + toString: () => 1, + }, + blockHash: '0x0', + messageHash: '0x0', + outcome: 'Success', + } as unknown as XcmInboundWithContext) + }) + + subs = new SubsStore(_services.log, _services.rootStore) + agentService = new LocalAgentService( + { + ..._services, + subsStore: subs, + } as Services, + { mode: AgentServiceMode.local } + ) + + + }) + + afterEach(async () => { + await _services.rootStore.clear() + return agentService.stop() + }) + + it('should subscribe to persisted subscriptions on start', async () => { + await subs.insert(testSub) + + await agentService.start() + + expect(agentService.getAgentById('xcm').getSubscriptionDescriptor(testSub.id)).toBeDefined() + }) + + it('should handle relay subscriptions', async () => { + await agentService.start() + + xcmAgent = agentService.getAgentById('xcm') as XCMAgent + + await xcmAgent.subscribe({ + ...testSub, + args: { + ...testSub.args, + origin: 'urn:ocn:local:0', + } + }) + + expect(xcmAgent.getSubscriptionDescriptor(testSub.id)).toBeDefined() + }) + + it('should handle pipe errors', async () => { + mockExtractUmpSend.mockImplementationOnce(() => () => { + return throwError(() => new Error('errored')) + }) + mockExtractUmpReceive.mockImplementationOnce(() => () => { + return throwError(() => new Error('errored')) + }) + mockExtractXcmpSend.mockImplementationOnce(() => () => { + return throwError(() => new Error('errored')) + }) + mockExtractXcmpReceive.mockImplementationOnce(() => () => { + return throwError(() => new Error('errored')) + }) + + await agentService.start() + + xcmAgent = agentService.getAgentById('xcm') as XCMAgent + await xcmAgent.subscribe(testSub) + + expect(xcmAgent.getSubscriptionDescriptor(testSub.id)).toBeDefined() + + }) + + it('should update destination subscriptions on destinations change', async () => { + await agentService.start() + + xcmAgent = agentService.getAgentById('xcm') as XCMAgent + + await xcmAgent.subscribe({ + ...testSub, + args: { + ...testSub.args, + destinations: ['urn:ocn:local:0', 'urn:ocn:local:2000'], + } + + }) + + const { destinationSubs } = xcmAgent.getSubscriptionHandler(testSub.id) + expect(destinationSubs.length).toBe(2) + expect(destinationSubs.filter((s) => s.chainId === 'urn:ocn:local:0').length).toBe(1) + expect(destinationSubs.filter((s) => s.chainId === 'urn:ocn:local:2000').length).toBe(1) + + // Remove 2000 and add 3000 to destinations + const newSub = { + ...testSub, + args: { + ...testSub.args, + destinations: ['urn:ocn:local:0', 'urn:ocn:local:3000'], + } + + } + + await xcmAgent.update(newSub.id, [{ + op: 'remove', + path: '/args/destinations/1' + }, + { + op: 'add', + path: '/args/destinations/-', + value: 'urn:ocn:local:3000' + }]) + const { destinationSubs: newDestinationSubs, descriptor } = agentService.getAgentById('xcm').getSubscriptionHandler(testSub.id) as XCMSubscriptionHandler + + expect(newDestinationSubs.length).toBe(2) + expect(newDestinationSubs.filter((s) => s.chainId === 'urn:ocn:local:0').length).toBe(1) + expect(newDestinationSubs.filter((s) => s.chainId === 'urn:ocn:local:3000').length).toBe(1) + expect(newDestinationSubs.filter((s) => s.chainId === 'urn:ocn:local:2000').length).toBe(0) + expect(descriptor).toEqual(newSub) + }) + + it('should create relay hrmp subscription when there is at least one HRMP pair in subscription', async () => { + await agentService.start() + + xcmAgent = agentService.getAgentById('xcm') as XCMAgent + + await xcmAgent.subscribe(testSub) // origin: '1000', destinations: ['2000'] + + const { relaySub } = xcmAgent.getSubscriptionHandler(testSub.id) as XCMSubscriptionHandler + expect(relaySub).toBeDefined() + }) + + it('should not create relay hrmp subscription when the origin is a relay chain', async () => { + await agentService.start() + + xcmAgent = agentService.getAgentById('xcm') as XCMAgent + + await xcmAgent.subscribe({ + ...testSub, + args: { + ...testSub.args, + origin: 'urn:ocn:local:0' // origin: '0', destinations: ['2000'] + } + }) + + const { relaySub } = xcmAgent.getSubscriptionHandler(testSub.id) as XCMSubscriptionHandler + expect(relaySub).not.toBeDefined() + }) + + it('should not create relay hrmp subscription when there are no HRMP pairs in the subscription', async () => { + await agentService.start() + + xcmAgent = agentService.getAgentById('xcm') as XCMAgent + + await xcmAgent.subscribe({ + ...testSub, + args: { + ...testSub.args, + destinations: ['urn:ocn:local:0'], // origin: '1000', destinations: ['0'] + } + }) + + const { relaySub } = xcmAgent.getSubscriptionHandler(testSub.id) + expect(relaySub).not.toBeDefined() + }) + + it('should not create relay hrmp subscription when relayed events are not requested', async () => { + await agentService.start() + + xcmAgent = agentService.getAgentById('xcm') as XCMAgent + + await xcmAgent.subscribe({ + ...testSub, + args: { + ...testSub.args, + events: [XcmNotificationType.Received] + } + }) + + const { relaySub } = xcmAgent.getSubscriptionHandler(testSub.id) + expect(relaySub).not.toBeDefined() + }) + + it('should create relay hrmp subscription if relayed event is added', async () => { + await agentService.start() + + xcmAgent = agentService.getAgentById('xcm') as XCMAgent + + await xcmAgent.subscribe({ + ...testSub, + args: { + ...testSub.args, + events: [XcmNotificationType.Received] + } + }) + + const { relaySub } = xcmAgent.getSubscriptionHandler(testSub.id) + expect(relaySub).not.toBeDefined() + + // add relayed event to subscription + const newSub = { + ...testSub, + args: { + ...testSub.args, + events: [XcmNotificationType.Received, XcmNotificationType.Relayed]} + } + + await xcmAgent.update(newSub.id, [{ + op: 'add', + path: '/args/events/-', + value: XcmNotificationType.Relayed + }]) + const { relaySub: newRelaySub, descriptor } = xcmAgent.getSubscriptionHandler(testSub.id) + expect(newRelaySub).toBeDefined() + expect(descriptor).toEqual(newSub) + }) + + it('should remove relay hrmp subscription if relayed event is removed', async () => { + await agentService.start() + + xcmAgent = agentService.getAgentById('xcm') as XCMAgent + + await xcmAgent.subscribe({ + ...testSub, + args: { + ...testSub.args, + events: [XcmNotificationType.Received, XcmNotificationType.Sent, XcmNotificationType.Relayed], + } + + }) + + const { relaySub } = xcmAgent.getSubscriptionHandler(testSub.id) + expect(relaySub).toBeDefined() + + // remove relayed event + const newSub = { + ...testSub, + args: { + ...testSub.args, + events: [XcmNotificationType.Received, XcmNotificationType.Sent], + } + + } + + await xcmAgent.update(newSub.id, [ + { + op: 'remove', + path: '/args/events/2' + } + ]) + const { relaySub: newRelaySub, descriptor } = xcmAgent.getSubscriptionHandler(testSub.id) + expect(newRelaySub).not.toBeDefined() + expect(descriptor).toEqual(newSub) + }) +}) \ No newline at end of file diff --git a/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts b/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts index b079ae3d..96e5f829 100644 --- a/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts +++ b/packages/server/src/services/subscriptions/api/ws/protocol.spec.ts @@ -37,7 +37,7 @@ describe('WebsocketProtocol', () => { removeNotificationListener: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn(), - findSubscriptionHandler: jest.fn(), + findSubscription: jest.fn(), } mockOptions = { wsMaxClients: 2, @@ -128,7 +128,7 @@ describe('WebsocketProtocol', () => { id: 'mockRequestId', ip: 'mockRequestIp', } as FastifyRequest - mockSwitchboard.findSubscriptionHandler.mockImplementationOnce(() => ({ + mockSwitchboard.findSubscription.mockImplementationOnce(() => ({ ...testSub, channels: [{ type: 'log' }], })) @@ -167,7 +167,7 @@ describe('WebsocketProtocol', () => { it('should close connection with error code if an error occurs', async () => { const mockStream = { close: jest.fn() } - mockSwitchboard.findSubscriptionHandler.mockImplementationOnce(() => { + mockSwitchboard.findSubscription.mockImplementationOnce(() => { throw new Error('subscription not found') }) await websocketProtocol.handle(mockStream, {} as FastifyRequest, 'testId') From 4a099d6eab92562f719755408dc3131ca4a42b27 Mon Sep 17 00:00:00 2001 From: Xueying Wang Date: Thu, 30 May 2024 13:13:31 +0200 Subject: [PATCH 58/58] move storage keys to xcm agent --- .../src/services/agents/xcm/ops/pk-bridge.ts | 2 +- .../{subscriptions => agents/xcm}/storage.ts | 2 +- .../src/services/agents/xcm/xcm-agent.spec.ts | 96 +++++++++---------- .../src/services/agents/xcm/xcm-agent.ts | 10 +- .../ingress/watcher/head-catcher.spec.ts | 2 +- .../services/ingress/watcher/local-cache.ts | 2 +- 6 files changed, 57 insertions(+), 57 deletions(-) rename packages/server/src/services/{subscriptions => agents/xcm}/storage.ts (96%) diff --git a/packages/server/src/services/agents/xcm/ops/pk-bridge.ts b/packages/server/src/services/agents/xcm/ops/pk-bridge.ts index 2109a5cc..49315b67 100644 --- a/packages/server/src/services/agents/xcm/ops/pk-bridge.ts +++ b/packages/server/src/services/agents/xcm/ops/pk-bridge.ts @@ -8,9 +8,9 @@ import { Observable, filter, from, mergeMap } from 'rxjs' import { types } from '@sodazone/ocelloids-sdk' import { getConsensus } from '../../../config.js' -import { bridgeStorageKeys } from '../../../subscriptions/storage.js' import { HexString } from '../../../subscriptions/types.js' import { NetworkURN } from '../../../types.js' +import { bridgeStorageKeys } from '../storage.js' import { GetStorageAt } from '../types-augmented.js' import { GenericXcmBridgeAcceptedWithContext, diff --git a/packages/server/src/services/subscriptions/storage.ts b/packages/server/src/services/agents/xcm/storage.ts similarity index 96% rename from packages/server/src/services/subscriptions/storage.ts rename to packages/server/src/services/agents/xcm/storage.ts index 36325554..db18fd37 100644 --- a/packages/server/src/services/subscriptions/storage.ts +++ b/packages/server/src/services/agents/xcm/storage.ts @@ -1,7 +1,7 @@ import { Registry } from '@polkadot/types-codec/types' import { u8aConcat, u8aToU8a } from '@polkadot/util' import { xxhashAsU8a } from '@polkadot/util-crypto' -import { HexString } from './types.js' +import { HexString } from '../../subscriptions/types.js' // Storage Keys Constants export const parachainSystemUpwardMessages = '0x45323df7cc47150b3930e2666b0aa313549294c71991aee810463ccf34a0f1d1' diff --git a/packages/server/src/services/agents/xcm/xcm-agent.spec.ts b/packages/server/src/services/agents/xcm/xcm-agent.spec.ts index 9d5b81aa..387363a7 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.spec.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.spec.ts @@ -4,16 +4,16 @@ import { of, throwError } from 'rxjs' import '../../../testing/network.js' -import { Subscription } from '../../subscriptions/types.js' -import { extractUmpReceive, extractUmpSend } from './ops/ump.js' -import * as XcmpOps from './ops/xcmp.js' -import { XcmInboundWithContext, XcmNotificationType, XcmSentWithContext, XCMSubscriptionHandler } from './types.js' import { _services } from '../../../testing/services.js' import { AgentServiceMode } from '../../../types.js' import { Services } from '../../index.js' import { SubsStore } from '../../persistence/subs.js' +import { Subscription } from '../../subscriptions/types.js' import { LocalAgentService } from '../local.js' import { AgentService } from '../types.js' +import { extractUmpReceive, extractUmpSend } from './ops/ump.js' +import * as XcmpOps from './ops/xcmp.js' +import { XCMSubscriptionHandler, XcmInboundWithContext, XcmNotificationType, XcmSentWithContext } from './types.js' import { XCMAgent } from './xcm-agent.js' const mockExtractXcmpReceive = jest.fn() @@ -57,7 +57,7 @@ describe('switchboard service', () => { let agentService: AgentService let xcmAgent: XCMAgent - beforeEach(async () => { + beforeEach(async () => { mockExtractXcmpSend.mockImplementation(() => { return () => { return of({ @@ -72,7 +72,7 @@ describe('switchboard service', () => { } as unknown as XcmSentWithContext) } }) - + mockExtractXcmpReceive.mockImplementation(() => { return () => { return of({ @@ -119,8 +119,6 @@ describe('switchboard service', () => { } as Services, { mode: AgentServiceMode.local } ) - - }) afterEach(async () => { @@ -145,13 +143,13 @@ describe('switchboard service', () => { ...testSub, args: { ...testSub.args, - origin: 'urn:ocn:local:0', - } + origin: 'urn:ocn:local:0', + }, }) expect(xcmAgent.getSubscriptionDescriptor(testSub.id)).toBeDefined() }) - + it('should handle pipe errors', async () => { mockExtractUmpSend.mockImplementationOnce(() => () => { return throwError(() => new Error('errored')) @@ -172,9 +170,8 @@ describe('switchboard service', () => { await xcmAgent.subscribe(testSub) expect(xcmAgent.getSubscriptionDescriptor(testSub.id)).toBeDefined() - }) - + it('should update destination subscriptions on destinations change', async () => { await agentService.start() @@ -185,8 +182,7 @@ describe('switchboard service', () => { args: { ...testSub.args, destinations: ['urn:ocn:local:0', 'urn:ocn:local:2000'], - } - + }, }) const { destinationSubs } = xcmAgent.getSubscriptionHandler(testSub.id) @@ -200,20 +196,23 @@ describe('switchboard service', () => { args: { ...testSub.args, destinations: ['urn:ocn:local:0', 'urn:ocn:local:3000'], - } - + }, } - await xcmAgent.update(newSub.id, [{ - op: 'remove', - path: '/args/destinations/1' - }, - { - op: 'add', - path: '/args/destinations/-', - value: 'urn:ocn:local:3000' - }]) - const { destinationSubs: newDestinationSubs, descriptor } = agentService.getAgentById('xcm').getSubscriptionHandler(testSub.id) as XCMSubscriptionHandler + await xcmAgent.update(newSub.id, [ + { + op: 'remove', + path: '/args/destinations/1', + }, + { + op: 'add', + path: '/args/destinations/-', + value: 'urn:ocn:local:3000', + }, + ]) + const { destinationSubs: newDestinationSubs, descriptor } = agentService + .getAgentById('xcm') + .getSubscriptionHandler(testSub.id) as XCMSubscriptionHandler expect(newDestinationSubs.length).toBe(2) expect(newDestinationSubs.filter((s) => s.chainId === 'urn:ocn:local:0').length).toBe(1) @@ -242,8 +241,8 @@ describe('switchboard service', () => { ...testSub, args: { ...testSub.args, - origin: 'urn:ocn:local:0' // origin: '0', destinations: ['2000'] - } + origin: 'urn:ocn:local:0', // origin: '0', destinations: ['2000'] + }, }) const { relaySub } = xcmAgent.getSubscriptionHandler(testSub.id) as XCMSubscriptionHandler @@ -259,8 +258,8 @@ describe('switchboard service', () => { ...testSub, args: { ...testSub.args, - destinations: ['urn:ocn:local:0'], // origin: '1000', destinations: ['0'] - } + destinations: ['urn:ocn:local:0'], // origin: '1000', destinations: ['0'] + }, }) const { relaySub } = xcmAgent.getSubscriptionHandler(testSub.id) @@ -276,8 +275,8 @@ describe('switchboard service', () => { ...testSub, args: { ...testSub.args, - events: [XcmNotificationType.Received] - } + events: [XcmNotificationType.Received], + }, }) const { relaySub } = xcmAgent.getSubscriptionHandler(testSub.id) @@ -293,8 +292,8 @@ describe('switchboard service', () => { ...testSub, args: { ...testSub.args, - events: [XcmNotificationType.Received] - } + events: [XcmNotificationType.Received], + }, }) const { relaySub } = xcmAgent.getSubscriptionHandler(testSub.id) @@ -305,14 +304,17 @@ describe('switchboard service', () => { ...testSub, args: { ...testSub.args, - events: [XcmNotificationType.Received, XcmNotificationType.Relayed]} + events: [XcmNotificationType.Received, XcmNotificationType.Relayed], + }, } - await xcmAgent.update(newSub.id, [{ - op: 'add', - path: '/args/events/-', - value: XcmNotificationType.Relayed - }]) + await xcmAgent.update(newSub.id, [ + { + op: 'add', + path: '/args/events/-', + value: XcmNotificationType.Relayed, + }, + ]) const { relaySub: newRelaySub, descriptor } = xcmAgent.getSubscriptionHandler(testSub.id) expect(newRelaySub).toBeDefined() expect(descriptor).toEqual(newSub) @@ -328,8 +330,7 @@ describe('switchboard service', () => { args: { ...testSub.args, events: [XcmNotificationType.Received, XcmNotificationType.Sent, XcmNotificationType.Relayed], - } - + }, }) const { relaySub } = xcmAgent.getSubscriptionHandler(testSub.id) @@ -341,18 +342,17 @@ describe('switchboard service', () => { args: { ...testSub.args, events: [XcmNotificationType.Received, XcmNotificationType.Sent], - } - + }, } await xcmAgent.update(newSub.id, [ { op: 'remove', - path: '/args/events/2' - } + path: '/args/events/2', + }, ]) const { relaySub: newRelaySub, descriptor } = xcmAgent.getSubscriptionHandler(testSub.id) expect(newRelaySub).not.toBeDefined() expect(descriptor).toEqual(newSub) }) -}) \ No newline at end of file +}) diff --git a/packages/server/src/services/agents/xcm/xcm-agent.ts b/packages/server/src/services/agents/xcm/xcm-agent.ts index ed43b014..9f9146fa 100644 --- a/packages/server/src/services/agents/xcm/xcm-agent.ts +++ b/packages/server/src/services/agents/xcm/xcm-agent.ts @@ -36,15 +36,15 @@ import { extractUmpReceive, extractUmpSend } from './ops/ump.js' import { EventEmitter } from 'node:events' import { getChainId, getConsensus } from '../../config.js' -import { - dmpDownwardMessageQueuesKey, - parachainSystemHrmpOutboundMessages, - parachainSystemUpwardMessages, -} from '../../subscriptions/storage.js' import { BaseAgent } from '../base/base-agent.js' import { AgentMetadata, AgentRuntimeContext } from '../types.js' import { extractBridgeMessageAccepted, extractBridgeMessageDelivered, extractBridgeReceive } from './ops/pk-bridge.js' import { getBridgeHubNetworkId } from './ops/util.js' +import { + dmpDownwardMessageQueuesKey, + parachainSystemHrmpOutboundMessages, + parachainSystemUpwardMessages, +} from './storage.js' import { TelemetryXCMEventEmitter } from './telemetry/events.js' import { xcmAgentMetrics, xcmAgentEngineMetrics as xcmMatchingEngineMetrics } from './telemetry/metrics.js' import { GetDownwardMessageQueues, GetOutboundHrmpMessages, GetOutboundUmpMessages } from './types-augmented.js' diff --git a/packages/server/src/services/ingress/watcher/head-catcher.spec.ts b/packages/server/src/services/ingress/watcher/head-catcher.spec.ts index 78f87c5c..bfa1eeff 100644 --- a/packages/server/src/services/ingress/watcher/head-catcher.spec.ts +++ b/packages/server/src/services/ingress/watcher/head-catcher.spec.ts @@ -7,9 +7,9 @@ import { from, of } from 'rxjs' import { interlayBlocks, polkadotBlocks, testBlocksFrom } from '../../../testing/blocks.js' import { mockConfigMixed, mockConfigWS } from '../../../testing/configs.js' import { _services } from '../../../testing/services.js' +import { parachainSystemHrmpOutboundMessages, parachainSystemUpwardMessages } from '../../agents/xcm/storage.js' import Connector from '../../networking/connector.js' import { Janitor } from '../../persistence/janitor.js' -import { parachainSystemHrmpOutboundMessages, parachainSystemUpwardMessages } from '../../subscriptions/storage.js' import { BlockNumberRange, ChainHead } from '../../subscriptions/types.js' import { DB, NetworkURN, jsonEncoded, prefixes } from '../../types.js' diff --git a/packages/server/src/services/ingress/watcher/local-cache.ts b/packages/server/src/services/ingress/watcher/local-cache.ts index 56e50e20..4b2852ae 100644 --- a/packages/server/src/services/ingress/watcher/local-cache.ts +++ b/packages/server/src/services/ingress/watcher/local-cache.ts @@ -9,9 +9,9 @@ import type { SignedBlockExtended } from '@polkadot/api-derive/types' import type { Raw } from '@polkadot/types' import type { Hash } from '@polkadot/types/interfaces' +import { parachainSystemHrmpOutboundMessages, parachainSystemUpwardMessages } from '../../agents/xcm/storage.js' import { NetworkConfiguration } from '../../config.js' import { Janitor } from '../../persistence/janitor.js' -import { parachainSystemHrmpOutboundMessages, parachainSystemUpwardMessages } from '../../subscriptions/storage.js' import { HexString } from '../../subscriptions/types.js' import { TelemetryEventEmitter } from '../../telemetry/types.js' import { DB, Logger, NetworkURN, Services, prefixes } from '../../types.js'