UNPKG

@glowlabs-org/events-sdk

Version:

Typed event SDK for Glow, powered by RabbitMQ and Zod.

685 lines (535 loc) โ€ข 21.8 kB
# Glow Events SDK A TypeScript-first SDK for consuming and emitting typed events on the Glow platform, powered by RabbitMQ (topic exchange). Provides runtime validation and type inference using Zod schemas. --- ## ๐Ÿš€ Quick Start ### 1. Install ```bash pnpm install @glowlabs-org/events-sdk ``` --- ## ๐Ÿ“š Zones The following zones are currently available: | Zone ID | Zone Name | | ------- | ------------------ | | 0 | All Zones | | 1 | Clean Grid Project | | 2 | Rising Utah | | 3 | Shining Missouri | | 4 | Golden Colorado | | 5 | Rajasthan | | 6 | Florida | | 7 | Lebanon | | 8 | Noble Oklahoma | | 9 | Idaho | | 10 | Virgin Islands | - Use `zoneId: 0` (with `zoneName: "All Zones"`) to listen to all zones. **Emitters must be constructed with a specific zoneId (not 0).** - The SDK types and runtime validation now officially support `zoneId: 0` and `zoneName: "All Zones"` everywhere for listeners, but not for emitters. - **Use the `getZoneId` utility to get the correct zoneId for a given zone name:** ```ts import { getZoneId } from "@glowlabs-org/events-sdk"; const zoneId = getZoneId("CleanGridProject"); ``` --- ## ๐Ÿ› ๏ธ Options for Listeners & Emitters Both `createGlowEventListener` and `createGlowEventEmitter` accept the following options: | Option | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------------------------------------------ | | `username` | string | Yes | RabbitMQ username | | `password` | string | Yes | RabbitMQ password | | `zoneId` | number | Yes | Zone ID (use `getZoneId("ZoneName")`) | | `queueName` | string | No | (Listener only) Pre-created queue name | | `exchangePrefix` | string | No | Exchange prefix (default: `glow.zone-`) | | `host` | string | No | **RabbitMQ host/port** (default: `turntable.proxy.rlwy.net:50784`) | --- ## ๐Ÿ“ฆ Event Types & Versions Currently supported event types and versions: | Event Name | Version | Payload Type | Description | | ------------------------- | ---------- | ------------------------------------- | --------------------------------------------------- | | `audit.pushed` | "v1" | `AuditPushedV1Payload` | Emitted when an audit is pushed | | `audit.slashed` | "v1" | `AuditSlashedV1Payload` | Emitted when a farm is slashed | | `audit.pfees.paid` | "v1" | `AuditPfeesPaidV1Payload` | Paid (by applicationId) | | `audit.pfees.paid` | "v2" | `AuditPfeesPaidV2Payload` | Paid (by farmId) | | `audit.pfees.paid` | "v2-alpha" | `AuditPfeesPaidV2AlphaPayload` | Paid (by applicationId) + currency metadata | | `application.created` | "v1" | `ApplicationCreatedV1Payload` | Emitted when an application is created | | `application.created` | "v2-alpha" | `ApplicationCreatedV2AlphaPayload` | V2-alpha: adds protocol fee price, omits installer | | `audit.pushed` | "v2" | `AuditPushedV2Payload` | Emitted when an audit is pushed (farmId is bytes16) | | `audit.pushed` | "v2-alpha" | `AuditPushedV2AlphaPayload` | Same as v1 but without protocol fee | | `application.price.quote` | "v2-alpha" | `ApplicationPriceQuoteV2AlphaPayload` | Price quote for application | | `gctl.minted` | "v2-alpha" | `GctlMintedV2AlphaPayload` | GCTL minted amount and metadata | | `auditor.fees.paid` | "v2-alpha" | `AuditorFeesPaidV2AlphaPayload` | Auditor fee paid (by applicationId) | | `fraction.created` | "v2-alpha" | `FractionCreatedV2AlphaPayload` | Fraction created with token and ownership details | | `fraction.sold` | "v2-alpha" | `FractionSoldV2AlphaPayload` | Fraction sold between creator and buyer | | `fraction.closed` | "v2-alpha" | `FractionClosedV2AlphaPayload` | Fraction closed/cancelled | | `fraction.refunded` | "v2-alpha" | `FractionRefundedV2AlphaPayload` | Fraction refunded to user | > **Environment:** All events include an `environment` field that defaults to `"production"`. You can set it to `"staging"` when creating an emitter. --- ### Event Types Enum The SDK provides a TypeScript constant for all supported event types to ensure type safety and avoid typos: ```ts import { eventTypes, EventType } from "./src/event-types"; // Usage example: const myEventType: EventType = eventTypes.auditPushed; ``` - `eventTypes` is a readonly object containing all event type strings. - `EventType` is a TypeScript type representing any valid event type string. Use these in your code to avoid hardcoding event names and to benefit from autocompletion and type checking. --- ## ๐Ÿ“ Event Payload Schemas ### `audit.pushed` v1 ```ts export interface AuditPushedV1Payload { farmId: string; // UUID string protocolFeeUSDPrice_6Decimals: string; // uint256 (decimal) โˆ’ 12 implied decimals expectedProduction_12Decimals: string; // uint256 (decimal) โˆ’ 12 implied decimals txHash: string; // bytes32 hex string (0x...) } ``` **Validation:** - `farmId` must be a valid UUID string (e.g., `afbc56b6-0b16-4119-b144-025728067ba6`). - `protocolFeeUSDPrice_6Decimals` and `expectedProduction_12Decimals` must be decimal strings representing unsigned big integers. - `txHash` must be a 32-byte hex string (e.g., `0x...`). ### `audit.slashed` v1 ```ts export interface AuditSlashedV1Payload { farmId: string; // bytes16 hex string (0x...) slasher: string; // Ethereum address (0x...) txHash: string; // bytes32 hex string (0x...) } ``` **Validation:** - `farmId` must be a 16-byte hex string (e.g., `0x...`). - `slasher` must be a valid Ethereum address (0x...40 hex chars). - `txHash` must be a 32-byte hex string (e.g., `0x...`). ### `audit.pfees.paid` v1 (by applicationId) ```ts export interface AuditPfeesPaidV1Payload { applicationId: string; // UUID string payer: string; // Ethereum address (0x...) amount_6Decimals: string; // uint256 (decimal) โˆ’ 6 implied decimals txHash: string; // bytes32 hex string (0x...) } ``` **Validation:** - `applicationId` must be a valid UUID string (e.g., `3ed964b1-4f02-475a-9789-fb74b3466c70`). - `payer` must be a valid Ethereum address (0x...40 hex chars). - `amount_6Decimals` must be a decimal string representing an unsigned big integer (6 implied decimals). - `txHash` must be a 32-byte hex string (e.g., `0x...`). ### `audit.pfees.paid` v2 (by farmId) ```ts export interface AuditPfeesPaidV2Payload { farmId: string; // bytes16 hex string (0x...) payer: string; // Ethereum address (0x...) amount_12Decimals: string; // uint256 (decimal) โˆ’ 12 implied decimals txHash: string; // bytes32 hex string (0x...) } ``` **Validation:** - `farmId` must be a 16-byte hex string (e.g., `0x...`). - `payer` must be a valid Ethereum address (0x...40 hex chars). - `amount_12Decimals` must be a decimal string representing an unsigned big integer (12 implied decimals). - `txHash` must be a 32-byte hex string (e.g., `0x...`). ### `audit.pushed` v2 ````ts export interface AuditPushedV2Payload { farmId: string; // bytes16 hex string (0x...) protocolFeeUSDPrice_12Decimals: string; // uint256 (decimal) โˆ’ 12 implied decimals expectedProduction_12Decimals: string; // uint256 (decimal) โˆ’ 12 implied decimals txHash: string; // bytes32 hex string (0x...) } ### `audit.pushed` v2-alpha ```ts export interface AuditPushedV2AlphaPayload { farmId: string; // UUID string expectedProduction_12Decimals: string; // uint256 (decimal) โˆ’ 12 implied decimals txHash: string; // bytes32 hex string (0x...) } ```` ### `audit.pfees.paid` v2-alpha ```ts export type PaymentCurrency = "GCTL" | "USDC" | "USDG" | "GLW" | "SGCTL"; export interface AuditPfeesPaidV2AlphaPayload { applicationId: string; // UUID string payer: string; // Ethereum address (0x...) amount_6Decimals: string; // uint256 (decimal) โˆ’ 6 implied decimals txHash: string; // bytes32 hex string (0x...) paymentCurrency: PaymentCurrency; paymentEventType: string; isSponsored: boolean; } ``` ### `application.price.quote` v2-alpha ```ts export interface ApplicationPriceQuoteV2AlphaPayload { applicationId: string; // UUID string createdAt: string; // ISO-8601 datetime string prices: { GCTL: string; GLW: string; USDC: string; USDG: string }; // uint256 decimal strings signature: string; // bytes32 hex string (0x...) gcaAddress: string; // Ethereum address (0x...) } ``` ### `gctl.minted` v2-alpha ```ts export interface GctlMintedV2AlphaPayload { gctlAmount_6Decimals: string; // uint256 (decimal) โˆ’ 6 implied decimals timestamp: string; // ISO-8601 datetime string minter: string; // Ethereum address (0x...) txHash: string; // bytes32 hex string (0x...) } ``` ### `auditor.fees.paid` v2-alpha ```ts export interface AuditorFeesPaidV2AlphaPayload { applicationId: string; // UUID string payer: string; // Ethereum address (0x...) amount_6Decimals: string; // uint256 (decimal) โˆ’ 6 implied decimals txHash: string; // bytes32 hex string (0x...) } ``` ### `fraction.created` v2-alpha ```ts export interface FractionCreatedV2AlphaPayload { fractionId: string; transactionHash: string; // bytes32 hex string (0x...) blockNumber: string; // uint256 (decimal) logIndex: number; token: string; // Ethereum address (0x...) owner: string; // Ethereum address (0x...) step: string; // uint256 (decimal) totalSteps: string; // uint256 (decimal) } ``` ### `fraction.sold` v2-alpha ```ts export interface FractionSoldV2AlphaPayload { fractionId: string; transactionHash: string; // bytes32 hex string (0x...) blockNumber: string; // uint256 (decimal) logIndex: number; creator: string; // Ethereum address (0x...) buyer: string; // Ethereum address (0x...) step: string; // uint256 (decimal) amount: string; // uint256 (decimal) timestamp: number; } ``` ### `fraction.closed` v2-alpha ```ts export interface FractionClosedV2AlphaPayload { fractionId: string; transactionHash: string; // bytes32 hex string (0x...) blockNumber: string; // uint256 (decimal) logIndex: number; token: string; // Ethereum address (0x...) owner: string; // Ethereum address (0x...) timestamp: number; } ``` **Validation:** - `fractionId` is a string identifier. - `transactionHash` must be a 32-byte hex string. - `blockNumber`, `step`, `totalSteps`, and `amount` must be decimal strings representing unsigned big integers. - `token`, `owner`, `creator`, and `buyer` must be valid Ethereum addresses. --- ```ts export interface FractionSoldV2AlphaPayload { fractionId: string; transactionHash: string; // bytes32 hex string (0x...) blockNumber: string; // uint256 (decimal) logIndex: number; creator: string; // Ethereum address (0x...) buyer: string; // Ethereum address (0x...) step: string; // uint256 (decimal) amount: string; // uint256 (decimal) timestamp: number; } ``` ### `fraction.closed` v2-alpha ```ts export interface FractionClosedV2AlphaPayload { fractionId: string; transactionHash: string; // bytes32 hex string (0x...) blockNumber: string; // uint256 (decimal) logIndex: number; token: string; // Ethereum address (0x...) owner: string; // Ethereum address (0x...) timestamp: number; } ``` ### `fraction.refunded` v2-alpha ```ts export interface FractionRefundedV2AlphaPayload { fractionId: string; // hex string creator: string; // Ethereum address (0x...) user: string; // Ethereum address (0x...) refundTo: string; // Ethereum address (0x...) amount: string; // uint256 (decimal) timestamp: number; blockNumber: string; // uint256 (decimal) transactionHash: string; // bytes32 hex string (0x...) logIndex: number; } ``` --- ### `application.created` v2-alpha ```ts export interface ApplicationCreatedV2AlphaPayload { gcaAddress: string; // Ethereum address (0x...) lat: number; lng: number; estimatedCostOfPowerPerKWh: number; estimatedKWhGeneratedPerYear: number; estimatedProtocolFeeUSDPrice_6Decimals: string; // uint256 (decimal) โˆ’ 6 implied decimals } ``` **Validation:** - `gcaAddress` must be a valid Ethereum address (0x...40 hex chars). - `lat` and `lng` are numbers (coordinates). - `estimatedCostOfPowerPerKWh` and `estimatedKWhGeneratedPerYear` are numbers. - `installerCompanyName` is a string. --- ## โœจ Usage Example ### Listen to Specific Event Types/Versions ```ts import { createGlowEventListener, getZoneId } from "@glowlabs-org/events-sdk"; const listener = createGlowEventListener({ username: "listener", password: "your-password-here", zoneId: getZoneId("CleanGridProject"), queueName: "my.precreated.queue", host: "my.rabbitmq.host:5672", // Optional: override the default host }); listener.onEvent("audit.pushed", "v1", (event) => { // event: GlowEvent<AuditPushedV1Payload> console.log( "Received audit.pushed v1:", event.payload.farmId, event.zoneId, event.zoneName ); }); listener.onEvent("audit.slashed", "v1", (event) => { // event: GlowEvent<AuditSlashedV1Payload> console.log( "Received audit.slashed v1:", event.payload.farmId, event.payload.slasher ); }); listener.onEvent("audit.pfees.paid", "v1", (event) => { // event: GlowEvent<AuditPfeesPaidV1Payload> console.log( "Received audit.pfees.paid v1:", event.payload.applicationId, event.payload.payer, event.payload.amount_6Decimals ); }); listener.onEvent("audit.pfees.paid", "v2", (event) => { // event: GlowEvent<AuditPfeesPaidV2Payload> console.log( "Received audit.pfees.paid v2:", event.payload.farmId, event.payload.payer, event.payload.amount_12Decimals ); }); listener.onEvent("application.created", "v1", (event) => { // event: GlowEvent<ApplicationCreatedV1Payload> console.log( "Received application.created v1:", event.payload.gcaAddress, event.payload.lat, event.payload.lng, event.payload.estimatedCostOfPowerPerKWh, event.payload.estimatedKWhGeneratedPerYear, event.payload.installerCompanyName ); }); await listener.start(); // To stop listening: // await listener.stop(); ``` ### Emit Events (Admin Only) ```ts import { createGlowEventEmitter, getZoneId } from "@glowlabs-org/events-sdk"; // You must construct the emitter with a specific zoneId (not 0) const emitter = createGlowEventEmitter({ username: "admin", password: "your-password-here", zoneId: getZoneId("CleanGridProject"), // must be a specific zone host: "my.rabbitmq.host:5672", // Optional: override the default host environment: "staging", // Optional: defaults to "production" }); await emitter.emit({ eventType: "audit.pushed", schemaVersion: "v1", payload: { farmId: "afbc56b6-0b16-4119-b144-025728067ba6", // UUID string protocolFeeUSDPrice_6Decimals: "...", expectedProduction_12Decimals: "...", txHash: "0x...", }, }); await emitter.emit({ eventType: "audit.slashed", schemaVersion: "v1", payload: { farmId: "0x...", slasher: "0x...", txHash: "0x...", }, }); await emitter.emit({ eventType: "audit.pfees.paid", schemaVersion: "v1", payload: { applicationId: "3ed964b1-4f02-475a-9789-fb74b3466c70", // UUID string payer: "0x...", amount_6Decimals: "1000000000000", txHash: "0x...", }, }); await emitter.emit({ eventType: "audit.pfees.paid", schemaVersion: "v2", payload: { farmId: "0x...", payer: "0x...", amount_12Decimals: "1000000000000", txHash: "0x...", }, }); await emitter.emit({ eventType: "application.created", schemaVersion: "v1", payload: { gcaAddress: "0x...", lat: 45.5017, lng: -73.5673, estimatedCostOfPowerPerKWh: 0.12, estimatedKWhGeneratedPerYear: 10000, installerCompanyName: "SolarCo", }, }); await emitter.disconnect(); ``` > **Note:** > > - The emitter will automatically publish each event to both the global (zone 0) and the specific zone exchange. > - You cannot construct an emitter for zoneId: 0, and you cannot specify zoneId per emit call. > - `schemaVersion` is always a string (e.g., "v1", "v2", "v2-alpha"). > - **You can override the RabbitMQ host using the `host` option. Default is `turntable.proxy.rlwy.net:50784`.** ### ๐ŸŒ Listening to All Zones You can listen to **all zones at once** by passing `zoneId: 0` and `zoneName: "All Zones"` to the listener. **Emitters must always use a specific zone.** #### Listen to All Zones ```ts import { createGlowEventListener } from "@glowlabs-org/events-sdk"; const listener = createGlowEventListener({ username: "listener", password: "your-password-here", zoneId: 0, // special value for all zones queueName: "my.precreated.queue", }); listener.onEvent("audit.pushed", "v1", (event) => { console.log( "Received audit.pushed v1 from any zone:", event.payload.farmId, event.zoneId, event.zoneName ); }); // ... other event handlers ... await listener.start(); // To stop listening: // await listener.stop(); ``` --- ## ๐Ÿงช Validation & Error Handling - All events are validated at runtime using Zod schemas. - If you emit or process an event with a `zoneName` that does not match the `zoneId`, an error is thrown. `zoneId: 0` and `zoneName: "All Zones"` are a valid pairing. - If you emit or process an event with a `schemaVersion` for which no schema exists (e.g., `audit.pushed` v2), an error is thrown. - If the payload does not match the schema, an error is thrown. --- ## ๐Ÿ” Permissions & Credentials - **Listener credentials:** Can only subscribe to events. Cannot emit events or create new queues. - **Admin credentials:** Can subscribe, emit events, and create/bind new queues and exchanges. If you try to emit with listener credentials, the SDK will throw an error. --- ## ๐Ÿ› ๏ธ Admin & Queue Management The SDK exposes helpers for programmatically creating, binding, and deleting exchanges and queues (admin credentials required). Use these for pre-creating queues for listeners, bootstrapping environments, or advanced queue management. ### `createExchange(options)` Creates a topic exchange (default: `exchangeType = "topic"`). ### `bindQueueToExchange(options)` Binds a queue to a topic exchange. You can specify a `routingKey` for fine-grained event filtering: - `routingKey = "#"` (default): all events - `routingKey = "audit.pushed.v1"`: only audit.pushed v1 events - `routingKey = "audit.pushed.*"`: all versions of audit.pushed #### Example ```ts import { createExchange, bindQueueToExchange, deleteExchange, deleteQueue, } from "@glowlabs-org/events-sdk"; await createExchange({ username: "admin", password: "your-password-here", exchange: "glow.zone-1.events", }); await bindQueueToExchange({ username: "admin", password: "your-password-here", exchange: "glow.zone-1.events", queue: "glow-listener-queue", routingKey: "audit.pushed.v1", // only audit.pushed v1 events }); ``` --- ## ๐Ÿ”’ Strict Read-Only Listeners If your listener credentials only have `read` permission (no `configure`), you must consume from a pre-created queue. This is the most secure pattern for production. ### 1. Admin: Pre-create and bind the queue ```ts import { bindQueueToExchange } from "@glowlabs-org/events-sdk"; await bindQueueToExchange({ username: "admin", password: "your-admin-password", exchange: "glow.zone-1.events", queue: "my.precreated.queue", routingKey: "audit.pushed.v1", // only audit.pushed v1 events }); ``` ### 2. Listener: Consume from the pre-created queue ```ts import { createGlowEventListener } from "@glowlabs-org/events-sdk"; const listener = createGlowEventListener({ username: "listener", password: "your-listener-password", zoneId: 1, queueName: "my.precreated.queue", }); ``` - The listener will only consume from the pre-created queue and will not attempt to create or bind anything. - This pattern is required for production environments with strict access control. --- ## ๐Ÿงฉ Advanced: Multiple Listeners/Emitters You can create multiple listeners or emitters in the same process, each with its own configuration (e.g., for different credentials, exchanges, or RabbitMQ URLs). This is useful for multi-tenant, multi-topic, or advanced scenarios. **Every listener receives every event for the bound routing key(s).** --- ## ๐Ÿงช Extending Event Types To add new event types or versions: 1. Create a new schema in `src/schemas/`. 2. Add the event type and version to `eventTypeRegistry` in `src/event-registry.ts`. 3. Update the base event type in `src/base-event.ts` if needed. --- ## ๐Ÿ—๏ธ Build & Publish to npm To build and publish the SDK to npm: ```bash make build make publish make clean ``` - The first time, run `npm login` to authenticate with npm. - For scoped packages (like `@glowlabs-org/events-sdk`), the Makefile uses `--access public` for publishing. --- ## License MIT