@glowlabs-org/events-sdk
Version:
Typed event SDK for Glow, powered by RabbitMQ and Zod.
685 lines (535 loc) โข 21.8 kB
Markdown
# 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