diff --git a/packages/azimuth-watcher/MOCK_EVENTS.md b/packages/azimuth-watcher/MOCK_EVENTS.md new file mode 100644 index 0000000..2e0c62c --- /dev/null +++ b/packages/azimuth-watcher/MOCK_EVENTS.md @@ -0,0 +1,131 @@ +# Mock Sponsorship Events + +This document describes how to inject mock sponsorship change events into the azimuth-watcher for testing purposes. + +## Overview + +The azimuth-watcher supports injecting mock events for sponsorship changes (EscapeRequested, EscapeAccepted, EscapeCanceled, LostSponsor) via a GraphQL mutation. + +## Usage + +### Inject Mock Events + +Open the GraphQL playground and run the `injectMockEvents` mutation: + +```graphql +mutation { + injectMockEvents( + events: [ + { + type: ESCAPE_REQUESTED + point: 256 + sponsor: 1 + blockNumber: 1000 + } + { + type: ESCAPE_ACCEPTED + point: 256 + sponsor: 1 + blockNumber: 1001 + } + ] + ) { + success + eventsInjected + } +} +``` + +## Event Types + +The following event types are supported: + +### Sponsorship Events +- `ESCAPE_REQUESTED` - Star requests to change sponsor +- `ESCAPE_ACCEPTED` - New galaxy accepts the star +- `ESCAPE_CANCELED` - Escape request is canceled (by star or rejected by galaxy) +- `LOST_SPONSOR` - Current galaxy stops sponsoring the star + +### Ownership Events +- `OWNER_CHANGED` - Ownership of a point changes + +## Example Scenarios + +### Successful Sponsor Change + +```graphql +mutation { + injectMockEvents( + events: [ + { type: ESCAPE_REQUESTED, point: 256, sponsor: 1, blockNumber: 1000 } + { type: ESCAPE_ACCEPTED, point: 256, sponsor: 1, blockNumber: 1001 } + ] + ) { + success + eventsInjected + } +} +``` + +### Galaxy Detaches Star + +```graphql +mutation { + injectMockEvents( + events: [ + { type: LOST_SPONSOR, point: 512, sponsor: 0, blockNumber: 2000 } + ] + ) { + success + eventsInjected + } +} +``` + +### Canceled Escape + +```graphql +mutation { + injectMockEvents( + events: [ + { type: ESCAPE_REQUESTED, point: 768, sponsor: 2, blockNumber: 3000 } + { type: ESCAPE_CANCELED, point: 768, sponsor: 2, blockNumber: 3001 } + ] + ) { + success + eventsInjected + } +} +``` + +### Ownership Change + +```graphql +mutation { + injectMockEvents( + events: [ + { type: OWNER_CHANGED, point: 256, owner: "0x1234567890123456789012345678901234567890", blockNumber: 4000 } + ] + ) { + success + eventsInjected + } +} +``` + +## Testing from zenithd + +1. Set the azimuth-watcher endpoint: + ```bash + export AZIMUTH_WATCHER_ENDPOINT=http://localhost:3001/graphql + ``` + +2. Inject mock events via GraphQL playground (step 2 above) + +3. Run zenithd tests - it will receive the mock events through normal GraphQL queries + +## Files Modified + +- `src/mock-event-store.ts` - In-memory event storage +- `src/schema.gql` - Added mutation and types +- `src/resolvers.ts` - Added mutation resolver and modified `eventsInRange` query diff --git a/packages/azimuth-watcher/src/mock-event-store.ts b/packages/azimuth-watcher/src/mock-event-store.ts new file mode 100644 index 0000000..81f16c2 --- /dev/null +++ b/packages/azimuth-watcher/src/mock-event-store.ts @@ -0,0 +1,105 @@ +// +// Copyright 2025 DeepStack +// + +import { ResultEvent } from '@cerc-io/util'; + +const AZIMUTH_CONTRACT = '0x223c067F8CF28ae173EE5CafEa60cA44C335fecB'; +const MOCK_TX_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000'; +const MOCK_FROM_ADDRESS = '0x0000000000000000000000000000000000000000'; + +export type MockEventType = 'ESCAPE_REQUESTED' | 'ESCAPE_ACCEPTED' | 'ESCAPE_CANCELED' | 'LOST_SPONSOR' | 'OWNER_CHANGED'; + +export interface MockEventInput { + type: MockEventType; + point: number; + sponsor?: number; + owner?: string; + blockNumber: number; +} + +class MockEventStore { + private events: ResultEvent[] = []; + + addEvents (inputs: MockEventInput[]): number { + const newEvents = inputs.map((input, index) => this.createMockEvent(input, index)); + this.events.push(...newEvents); + return newEvents.length; + } + + getEventsInRange (fromBlock: number, toBlock: number, eventName?: string): ResultEvent[] { + return this.events.filter(event => { + const blockNumber = event.block.number; + const matchesRange = blockNumber >= fromBlock && blockNumber <= toBlock; + const matchesName = !eventName || event.event.__typename === `${eventName}Event`; + return matchesRange && matchesName; + }); + } + + private createMockEvent (input: MockEventInput, eventIndex: number): ResultEvent { + const { type, point, sponsor, owner, blockNumber } = input; + + const blockHash = `0x${blockNumber.toString(16).padStart(64, '0')}`; + const parentHash = `0x${(blockNumber - 1).toString(16).padStart(64, '0')}`; + + return { + block: { + cid: null, + hash: blockHash, + number: blockNumber, + timestamp: Math.floor(Date.now() / 1000), + parentHash + }, + tx: { + hash: MOCK_TX_HASH, + index: 0, + from: MOCK_FROM_ADDRESS, + to: AZIMUTH_CONTRACT + }, + contract: AZIMUTH_CONTRACT, + eventIndex, + eventSignature: '', + event: this.createEventData(type, point, sponsor, owner), + proof: '' + }; + } + + private createEventData (type: MockEventType, point: number, sponsor?: number, owner?: string): any { + switch (type) { + case 'ESCAPE_REQUESTED': + return { + __typename: 'EscapeRequestedEvent', + point: BigInt(point), + sponsor: BigInt(sponsor ?? 0) + }; + case 'ESCAPE_ACCEPTED': + return { + __typename: 'EscapeAcceptedEvent', + point: BigInt(point), + sponsor: BigInt(sponsor ?? 0) + }; + case 'ESCAPE_CANCELED': + return { + __typename: 'EscapeCanceledEvent', + point: BigInt(point), + sponsor: BigInt(sponsor ?? 0) + }; + case 'LOST_SPONSOR': + return { + __typename: 'LostSponsorEvent', + point: BigInt(point), + sponsor: BigInt(sponsor ?? 0) + }; + case 'OWNER_CHANGED': + return { + __typename: 'OwnerChangedEvent', + point: BigInt(point), + owner: owner ?? '' + }; + default: + throw new Error(`Unknown mock event type: ${type}`); + } + } +} + +export const mockEventStore = new MockEventStore(); diff --git a/packages/azimuth-watcher/src/resolvers.ts b/packages/azimuth-watcher/src/resolvers.ts index 2a85232..004f72d 100644 --- a/packages/azimuth-watcher/src/resolvers.ts +++ b/packages/azimuth-watcher/src/resolvers.ts @@ -23,6 +23,7 @@ import { } from '@cerc-io/util'; import { Indexer } from './indexer'; +import { mockEventStore, MockEventInput } from './mock-event-store'; const log = debug('vulcanize:resolver'); @@ -105,6 +106,12 @@ export const createResolvers = async ( await indexer.watchContract(address, kind, checkpoint, startingBlock); return true; + }, + + injectMockEvents: async (_: any, { events }: { events: MockEventInput[] }): Promise<{ success: boolean, eventsInjected: number }> => { + log('injectMockEvents', events.length); + const eventsInjected = mockEventStore.addEvents(events); + return { success: true, eventsInjected }; } }, @@ -1125,7 +1132,8 @@ export const createResolvers = async ( } const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber, name); - return events.map(event => indexer.getResultEvent(event)); + const mockEvents = mockEventStore.getEventsInRange(fromBlockNumber, toBlockNumber, name); + return [...events.map(event => indexer.getResultEvent(event)), ...mockEvents]; } ); }, diff --git a/packages/azimuth-watcher/src/schema.gql b/packages/azimuth-watcher/src/schema.gql index bcee8a2..682ca2c 100644 --- a/packages/azimuth-watcher/src/schema.gql +++ b/packages/azimuth-watcher/src/schema.gql @@ -174,6 +174,27 @@ type SyncStatus { latestProcessedBlockNumber: Int! } +enum MockEventType { + ESCAPE_REQUESTED + ESCAPE_ACCEPTED + ESCAPE_CANCELED + LOST_SPONSOR + OWNER_CHANGED +} + +input MockEventInput { + type: MockEventType! + point: Int! + sponsor: Int + owner: String + blockNumber: Int! +} + +type MockInjectionResult { + success: Boolean! + eventsInjected: Int! +} + type Query { events(blockHash: String!, contractAddress: String!, name: String): [ResultEvent!] eventsInRange(fromBlockNumber: Int!, toBlockNumber: Int!, name: String): [ResultEvent!] @@ -228,6 +249,7 @@ type Query { type Mutation { watchContract(address: String!, kind: String!, checkpoint: Boolean!, startingBlock: Int): Boolean! + injectMockEvents(events: [MockEventInput!]!): MockInjectionResult! } type Subscription {