@glowlabs-org/events-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +295 -0
- package/dist/admin.d.ts +38 -0
- package/dist/admin.js +80 -0
- package/dist/consumer.d.ts +14 -0
- package/dist/consumer.js +84 -0
- package/dist/glow-listener.d.ts +31 -0
- package/dist/glow-listener.js +74 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +22 -0
- package/dist/producer.d.ts +8 -0
- package/dist/producer.js +46 -0
- package/dist/schemas/auditPushed.d.ts +69 -0
- package/dist/schemas/auditPushed.js +42 -0
- package/dist/typed-emitter.d.ts +7 -0
- package/dist/typed-emitter.js +17 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.js +2 -0
- package/package.json +46 -0
package/README.md
ADDED
@@ -0,0 +1,295 @@
|
|
1
|
+
# Glow Events SDK
|
2
|
+
|
3
|
+
A TypeScript-first SDK for consuming and emitting typed events on the Glow platform, powered by RabbitMQ (fanout exchange). Provides runtime validation and type inference using Zod schemas.
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
## π Quick Start
|
8
|
+
|
9
|
+
### 1. Install
|
10
|
+
|
11
|
+
```bash
|
12
|
+
npm install @glow/events-sdk
|
13
|
+
```
|
14
|
+
|
15
|
+
### 2. Available Events
|
16
|
+
|
17
|
+
| Event Name | Payload Type | Description |
|
18
|
+
| -------------- | ------------------ | -------------------------------- |
|
19
|
+
| `AuditPushed` | `AuditPushedEvent` | Emitted when an audit is pushed |
|
20
|
+
| `AuditSlashed` | `AuditSlashedId` | Emitted when an audit is slashed |
|
21
|
+
|
22
|
+
#### Event Payload Types
|
23
|
+
|
24
|
+
```ts
|
25
|
+
// AuditPushedEvent
|
26
|
+
import type { AuditPushedEvent } from "@glow/events-sdk";
|
27
|
+
// type AuditPushedEvent = {
|
28
|
+
// farmId: string;
|
29
|
+
// protocolFeeUSDPrice_12Decimals: string;
|
30
|
+
// tokenPrices: { token: string; priceWAD_usd12Decimals: string }[];
|
31
|
+
// expectedProduction_12Decimals: string;
|
32
|
+
// glowRewardSplits: { receiver: string; percent: number }[];
|
33
|
+
// cashRewardSplits: { receiver: string; percent: number }[];
|
34
|
+
// }
|
35
|
+
|
36
|
+
// AuditSlashedId
|
37
|
+
// type AuditSlashedId = `0x${string}`;
|
38
|
+
```
|
39
|
+
|
40
|
+
---
|
41
|
+
|
42
|
+
## β¨ Usage Example
|
43
|
+
|
44
|
+
### Listen to Events
|
45
|
+
|
46
|
+
```ts
|
47
|
+
import { createGlowListener } from "@glow/events-sdk";
|
48
|
+
|
49
|
+
async function main() {
|
50
|
+
const sdk = await createGlowListener({
|
51
|
+
username: "listener", // or "admin"
|
52
|
+
password: "your-password-here",
|
53
|
+
queueName: "my.precreated.queue", // REQUIRED: must be pre-created and bound by an admin
|
54
|
+
// Optionally specify a custom exchange:
|
55
|
+
// exchange: "glow.audit.v2.exchange",
|
56
|
+
});
|
57
|
+
|
58
|
+
sdk.on("AuditPushed", (event) => {
|
59
|
+
console.log("Received AuditPushed:", event.farmId);
|
60
|
+
});
|
61
|
+
|
62
|
+
sdk.on("AuditSlashed", (id) => {
|
63
|
+
console.log("Received AuditSlashed:", id);
|
64
|
+
});
|
65
|
+
|
66
|
+
await sdk.startListener();
|
67
|
+
|
68
|
+
// To stop listening:
|
69
|
+
// await sdk.stopListener();
|
70
|
+
}
|
71
|
+
|
72
|
+
main();
|
73
|
+
```
|
74
|
+
|
75
|
+
### Emit Events (Admin Only)
|
76
|
+
|
77
|
+
```ts
|
78
|
+
import { createGlowListener } from "@glow/events-sdk";
|
79
|
+
|
80
|
+
async function main() {
|
81
|
+
const sdk = await createGlowListener({
|
82
|
+
username: "admin",
|
83
|
+
password: "your-password-here",
|
84
|
+
queueName: "my.precreated.queue", // REQUIRED: must be pre-created and bound by an admin
|
85
|
+
// Optionally specify a custom exchange:
|
86
|
+
// exchange: "glow.audit.v2.exchange",
|
87
|
+
});
|
88
|
+
|
89
|
+
await sdk.emitAuditPushed?.({
|
90
|
+
farmId: "0x...",
|
91
|
+
protocolFeeUSDPrice_12Decimals: "...",
|
92
|
+
tokenPrices: [],
|
93
|
+
expectedProduction_12Decimals: "...",
|
94
|
+
glowRewardSplits: [],
|
95
|
+
cashRewardSplits: [],
|
96
|
+
});
|
97
|
+
|
98
|
+
sdk.emitAuditSlashed?.("0x...");
|
99
|
+
}
|
100
|
+
|
101
|
+
main();
|
102
|
+
```
|
103
|
+
|
104
|
+
---
|
105
|
+
|
106
|
+
## π API Reference
|
107
|
+
|
108
|
+
### `createGlowListener({ username, password, exchange?, queueName })`
|
109
|
+
|
110
|
+
Returns a Promise resolving to an object with:
|
111
|
+
|
112
|
+
- `.on(event, listener)` β Listen to typed events
|
113
|
+
- `.off(event, listener)` β Remove a listener
|
114
|
+
- `.emitAuditPushed(payload)` β Emit an AuditPushed event (if permitted)
|
115
|
+
- `.emitAuditSlashed(id)` β Emit an AuditSlashed event (if permitted)
|
116
|
+
- `.startListener()` β Start listening to events
|
117
|
+
- `.stopListener()` β Stop listening to events
|
118
|
+
|
119
|
+
#### Options
|
120
|
+
|
121
|
+
- `username` (string, required)
|
122
|
+
- `password` (string, required)
|
123
|
+
- `exchange` (string, optional): RabbitMQ exchange name (default: `glow.audit.v1.exchange`)
|
124
|
+
- `queueName` (string, required): Use a pre-created queue (must be set up by an admin)
|
125
|
+
- **Naming Requirement:** Both `queueName` and `exchange` must be dot-separated (e.g., 'glow.audit.v1.exchange').
|
126
|
+
|
127
|
+
---
|
128
|
+
|
129
|
+
## π Permissions & Credentials
|
130
|
+
|
131
|
+
- **Listener credentials:** Can only subscribe to events. Cannot emit events or create new queues.
|
132
|
+
- **Admin credentials:** Can subscribe, emit events, and create/bind new queues and exchanges.
|
133
|
+
|
134
|
+
If you try to emit with listener credentials, the SDK will throw an error.
|
135
|
+
|
136
|
+
---
|
137
|
+
|
138
|
+
## π οΈ Advanced: Admin & Queue Management
|
139
|
+
|
140
|
+
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.
|
141
|
+
|
142
|
+
### `createExchange(options)`
|
143
|
+
|
144
|
+
### `bindQueueToExchange(options)`
|
145
|
+
|
146
|
+
### `deleteExchange(options)`
|
147
|
+
|
148
|
+
### `deleteQueue(options)`
|
149
|
+
|
150
|
+
See the end of this README for full admin/queue management usage examples.
|
151
|
+
|
152
|
+
---
|
153
|
+
|
154
|
+
## π More Details
|
155
|
+
|
156
|
+
### Event Payload Zod Schema
|
157
|
+
|
158
|
+
```ts
|
159
|
+
import { z } from "zod";
|
160
|
+
|
161
|
+
export const AuditPushedDataZ = z
|
162
|
+
.object({
|
163
|
+
farmId: z.string().regex(/^0x[0-9a-fA-F]{64}$/, "bytes32 hex string"),
|
164
|
+
protocolFeeUSDPrice_12Decimals: z
|
165
|
+
.string()
|
166
|
+
.regex(/^[0-9]+$/, "uint256 (decimal) β 12 implied decimals"),
|
167
|
+
tokenPrices: z.array(
|
168
|
+
z.object({
|
169
|
+
token: z.string().regex(/^0x[0-9a-fA-F]{40}$/, "ERCβ20 address"),
|
170
|
+
priceWAD_usd12Decimals: z.string().regex(/^[0-9]+$/),
|
171
|
+
})
|
172
|
+
),
|
173
|
+
expectedProduction_12Decimals: z.string().regex(/^[0-9]+$/),
|
174
|
+
glowRewardSplits: z.array(
|
175
|
+
z.object({
|
176
|
+
receiver: z.string().regex(/^0x[0-9a-fA-F]{40}$/),
|
177
|
+
percent: z.number().min(0).max(100),
|
178
|
+
})
|
179
|
+
),
|
180
|
+
cashRewardSplits: z.array(
|
181
|
+
z.object({
|
182
|
+
receiver: z.string().regex(/^0x[0-9a-fA-F]{40}$/),
|
183
|
+
percent: z.number().min(0).max(100),
|
184
|
+
})
|
185
|
+
),
|
186
|
+
})
|
187
|
+
.strict();
|
188
|
+
|
189
|
+
export type AuditPushedData = z.infer<typeof AuditPushedDataZ>;
|
190
|
+
```
|
191
|
+
|
192
|
+
---
|
193
|
+
|
194
|
+
## ποΈ Admin: Exchange and Queue Management
|
195
|
+
|
196
|
+
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.
|
197
|
+
|
198
|
+
### Usage Example
|
199
|
+
|
200
|
+
```ts
|
201
|
+
import {
|
202
|
+
createExchange,
|
203
|
+
bindQueueToExchange,
|
204
|
+
deleteExchange,
|
205
|
+
deleteQueue,
|
206
|
+
} from "@glow/events-sdk";
|
207
|
+
|
208
|
+
await createExchange({
|
209
|
+
username: "admin",
|
210
|
+
password: "your-password-here",
|
211
|
+
exchange: "glow.audit.v1.exchange",
|
212
|
+
});
|
213
|
+
|
214
|
+
await bindQueueToExchange({
|
215
|
+
username: "admin",
|
216
|
+
password: "your-password-here",
|
217
|
+
exchange: "glow.audit.v1.exchange",
|
218
|
+
queue: "glow-listener-queue",
|
219
|
+
});
|
220
|
+
|
221
|
+
await deleteExchange({
|
222
|
+
username: "admin",
|
223
|
+
password: "your-password-here",
|
224
|
+
exchange: "glow.audit.v1.exchange",
|
225
|
+
});
|
226
|
+
|
227
|
+
await deleteQueue({
|
228
|
+
username: "admin",
|
229
|
+
password: "your-password-here",
|
230
|
+
queue: "glow-listener-queue",
|
231
|
+
});
|
232
|
+
```
|
233
|
+
|
234
|
+
---
|
235
|
+
|
236
|
+
## π Strict Read-Only Listeners
|
237
|
+
|
238
|
+
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.
|
239
|
+
|
240
|
+
### 1. Admin: Pre-create and bind the queue
|
241
|
+
|
242
|
+
```ts
|
243
|
+
import { createAndBindQueue } from "@glow/events-sdk";
|
244
|
+
|
245
|
+
await createAndBindQueue({
|
246
|
+
username: "admin",
|
247
|
+
password: "your-admin-password",
|
248
|
+
exchange: "glow.audit.v1.exchange",
|
249
|
+
queue: "my.precreated.queue",
|
250
|
+
});
|
251
|
+
```
|
252
|
+
|
253
|
+
### 2. Listener: Consume from the pre-created queue
|
254
|
+
|
255
|
+
```ts
|
256
|
+
import { createGlowListener } from "@glow/events-sdk";
|
257
|
+
|
258
|
+
const sdk = await createGlowListener({
|
259
|
+
username: "listener",
|
260
|
+
password: "your-listener-password",
|
261
|
+
queueName: "my.precreated.queue",
|
262
|
+
// Optionally specify a custom exchange:
|
263
|
+
// exchange: "glow.audit.v1.exchange",
|
264
|
+
});
|
265
|
+
```
|
266
|
+
|
267
|
+
- The listener will only consume from the pre-created queue and will not attempt to create or bind anything.
|
268
|
+
- This pattern is required for production environments with strict access control.
|
269
|
+
|
270
|
+
---
|
271
|
+
|
272
|
+
## π§© Advanced: Multiple Listeners/Producers
|
273
|
+
|
274
|
+
You can create multiple listeners or producers 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 exchange.**
|
275
|
+
|
276
|
+
---
|
277
|
+
|
278
|
+
## ποΈ Build & Publish to npm
|
279
|
+
|
280
|
+
To build and publish the SDK to npm:
|
281
|
+
|
282
|
+
```bash
|
283
|
+
make build
|
284
|
+
make publish
|
285
|
+
make clean
|
286
|
+
```
|
287
|
+
|
288
|
+
- The first time, run `npm login` to authenticate with npm.
|
289
|
+
- For scoped packages (like `@glow/events-sdk`), the Makefile uses `--access public` for publishing.
|
290
|
+
|
291
|
+
---
|
292
|
+
|
293
|
+
## License
|
294
|
+
|
295
|
+
MIT
|
package/dist/admin.d.ts
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import amqp from "amqplib";
|
2
|
+
export interface CreateAndBindQueueOptions {
|
3
|
+
username: string;
|
4
|
+
password: string;
|
5
|
+
exchange: string;
|
6
|
+
queue: string;
|
7
|
+
exchangeType?: string;
|
8
|
+
queueOptions?: amqp.Options.AssertQueue;
|
9
|
+
exchangeOptions?: amqp.Options.AssertExchange;
|
10
|
+
}
|
11
|
+
/**
|
12
|
+
* Create a RabbitMQ exchange (admin credentials required).
|
13
|
+
*
|
14
|
+
* @param options - Connection and exchange options
|
15
|
+
* @returns Promise<void>
|
16
|
+
*/
|
17
|
+
export declare function createExchange({ username, password, exchange, exchangeType, exchangeOptions, }: Omit<CreateAndBindQueueOptions, "queue" | "queueOptions">): Promise<void>;
|
18
|
+
/**
|
19
|
+
* Create a RabbitMQ queue and bind it to an exchange (admin credentials required).
|
20
|
+
*
|
21
|
+
* @param options - Connection, queue, and exchange options
|
22
|
+
* @returns Promise<void>
|
23
|
+
*/
|
24
|
+
export declare function bindQueueToExchange({ username, password, exchange, queue, exchangeType, queueOptions, exchangeOptions, }: CreateAndBindQueueOptions): Promise<void>;
|
25
|
+
/**
|
26
|
+
* Delete a RabbitMQ exchange (admin credentials required).
|
27
|
+
*
|
28
|
+
* @param options - Connection and exchange options
|
29
|
+
* @returns Promise<void>
|
30
|
+
*/
|
31
|
+
export declare function deleteExchange({ username, password, exchange, }: Omit<CreateAndBindQueueOptions, "queue" | "queueOptions" | "exchangeType" | "exchangeOptions">): Promise<void>;
|
32
|
+
/**
|
33
|
+
* Delete a RabbitMQ queue (admin credentials required).
|
34
|
+
*
|
35
|
+
* @param options - Connection and queue options
|
36
|
+
* @returns Promise<void>
|
37
|
+
*/
|
38
|
+
export declare function deleteQueue({ username, password, queue, }: Omit<CreateAndBindQueueOptions, "exchange" | "exchangeType" | "exchangeOptions" | "queueOptions">): Promise<void>;
|
package/dist/admin.js
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.createExchange = createExchange;
|
7
|
+
exports.bindQueueToExchange = bindQueueToExchange;
|
8
|
+
exports.deleteExchange = deleteExchange;
|
9
|
+
exports.deleteQueue = deleteQueue;
|
10
|
+
const amqplib_1 = __importDefault(require("amqplib"));
|
11
|
+
function validateName(name, type) {
|
12
|
+
// Require at least two segments separated by dots, only alphanumerics, dashes, underscores, and dots
|
13
|
+
const pattern = /^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
|
14
|
+
if (!pattern.test(name)) {
|
15
|
+
throw new Error(`${type} name '${name}' is invalid. Must be dot-separated (e.g., 'glow.audit.v1.exchange').`);
|
16
|
+
}
|
17
|
+
}
|
18
|
+
/**
|
19
|
+
* Create a RabbitMQ exchange (admin credentials required).
|
20
|
+
*
|
21
|
+
* @param options - Connection and exchange options
|
22
|
+
* @returns Promise<void>
|
23
|
+
*/
|
24
|
+
async function createExchange({ username, password, exchange, exchangeType = "fanout", exchangeOptions = { durable: true }, }) {
|
25
|
+
validateName(exchange, "exchange");
|
26
|
+
const url = new URL(`amqp://${username}:${password}@turntable.proxy.rlwy.net:50784`);
|
27
|
+
const conn = await amqplib_1.default.connect(url.toString());
|
28
|
+
const channel = await conn.createChannel();
|
29
|
+
await channel.assertExchange(exchange, exchangeType, exchangeOptions);
|
30
|
+
await channel.close();
|
31
|
+
await conn.close();
|
32
|
+
}
|
33
|
+
/**
|
34
|
+
* Create a RabbitMQ queue and bind it to an exchange (admin credentials required).
|
35
|
+
*
|
36
|
+
* @param options - Connection, queue, and exchange options
|
37
|
+
* @returns Promise<void>
|
38
|
+
*/
|
39
|
+
async function bindQueueToExchange({ username, password, exchange, queue, exchangeType = "fanout", queueOptions = { durable: false }, exchangeOptions = { durable: true }, }) {
|
40
|
+
validateName(exchange, "exchange");
|
41
|
+
validateName(queue, "queue");
|
42
|
+
const url = new URL(`amqp://${username}:${password}@turntable.proxy.rlwy.net:50784`);
|
43
|
+
const conn = await amqplib_1.default.connect(url.toString());
|
44
|
+
const channel = await conn.createChannel();
|
45
|
+
await channel.assertExchange(exchange, exchangeType, exchangeOptions);
|
46
|
+
await channel.assertQueue(queue, queueOptions);
|
47
|
+
await channel.bindQueue(queue, exchange, "");
|
48
|
+
await channel.close();
|
49
|
+
await conn.close();
|
50
|
+
}
|
51
|
+
/**
|
52
|
+
* Delete a RabbitMQ exchange (admin credentials required).
|
53
|
+
*
|
54
|
+
* @param options - Connection and exchange options
|
55
|
+
* @returns Promise<void>
|
56
|
+
*/
|
57
|
+
async function deleteExchange({ username, password, exchange, }) {
|
58
|
+
validateName(exchange, "exchange");
|
59
|
+
const url = new URL(`amqp://${username}:${password}@turntable.proxy.rlwy.net:50784`);
|
60
|
+
const conn = await amqplib_1.default.connect(url.toString());
|
61
|
+
const channel = await conn.createChannel();
|
62
|
+
await channel.deleteExchange(exchange);
|
63
|
+
await channel.close();
|
64
|
+
await conn.close();
|
65
|
+
}
|
66
|
+
/**
|
67
|
+
* Delete a RabbitMQ queue (admin credentials required).
|
68
|
+
*
|
69
|
+
* @param options - Connection and queue options
|
70
|
+
* @returns Promise<void>
|
71
|
+
*/
|
72
|
+
async function deleteQueue({ username, password, queue, }) {
|
73
|
+
validateName(queue, "queue");
|
74
|
+
const url = new URL(`amqp://${username}:${password}@turntable.proxy.rlwy.net:50784`);
|
75
|
+
const conn = await amqplib_1.default.connect(url.toString());
|
76
|
+
const channel = await conn.createChannel();
|
77
|
+
await channel.deleteQueue(queue);
|
78
|
+
await channel.close();
|
79
|
+
await conn.close();
|
80
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import { AuditPushedDataZ } from "./schemas/auditPushed";
|
2
|
+
import { z } from "zod";
|
3
|
+
export declare function createAuditPushedConsumer({ username, password, onEvent, skipAssertExchange, queueName, skipAssertQueue, exchange, }: {
|
4
|
+
username: string;
|
5
|
+
password: string;
|
6
|
+
onEvent: (evt: z.infer<typeof AuditPushedDataZ>) => void;
|
7
|
+
skipAssertExchange?: boolean;
|
8
|
+
queueName?: string;
|
9
|
+
skipAssertQueue?: boolean;
|
10
|
+
exchange?: string;
|
11
|
+
}): {
|
12
|
+
start: () => Promise<void>;
|
13
|
+
stop: () => Promise<void>;
|
14
|
+
};
|
package/dist/consumer.js
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.createAuditPushedConsumer = createAuditPushedConsumer;
|
7
|
+
const amqplib_1 = __importDefault(require("amqplib"));
|
8
|
+
const auditPushed_1 = require("./schemas/auditPushed");
|
9
|
+
const EXCHANGE = "glow.audit.v1.exchange";
|
10
|
+
function createAuditPushedConsumer({ username, password, onEvent, skipAssertExchange = false, queueName, skipAssertQueue = false, exchange = "glow.audit.v1.exchange", }) {
|
11
|
+
// amqplib types are not always compatible with runtime objects, so we use 'as any' as a workaround
|
12
|
+
let connection = null;
|
13
|
+
let channel = null;
|
14
|
+
let internalQueueName;
|
15
|
+
let consumerTag = null;
|
16
|
+
function buildAmqpUrl() {
|
17
|
+
const url = new URL(`amqp://${username}:${password}@turntable.proxy.rlwy.net:50784`);
|
18
|
+
return url.toString();
|
19
|
+
}
|
20
|
+
async function connect() {
|
21
|
+
if (!connection) {
|
22
|
+
connection = (await amqplib_1.default.connect(buildAmqpUrl()));
|
23
|
+
channel = (await connection.createChannel());
|
24
|
+
}
|
25
|
+
if (channel) {
|
26
|
+
// Strict read-only: skip all asserts/binds, just use the provided queueName
|
27
|
+
if (skipAssertQueue) {
|
28
|
+
if (!queueName) {
|
29
|
+
throw new Error("[Glow SDK] In strict read-only mode (skipAssertQueue: true), you must provide a queueName.");
|
30
|
+
}
|
31
|
+
internalQueueName = queueName;
|
32
|
+
// Do not assert or bind anything
|
33
|
+
return;
|
34
|
+
}
|
35
|
+
// Default pattern: assert exchange, create/bind queue
|
36
|
+
if (!skipAssertExchange) {
|
37
|
+
await channel.assertExchange(exchange, "fanout", { durable: true });
|
38
|
+
}
|
39
|
+
const q = await channel.assertQueue(queueName !== null && queueName !== void 0 ? queueName : "", {
|
40
|
+
exclusive: !queueName,
|
41
|
+
});
|
42
|
+
internalQueueName = q.queue;
|
43
|
+
await channel.bindQueue(internalQueueName, exchange, "");
|
44
|
+
}
|
45
|
+
}
|
46
|
+
async function start() {
|
47
|
+
try {
|
48
|
+
await connect();
|
49
|
+
if (!internalQueueName)
|
50
|
+
throw new Error("Queue not initialized");
|
51
|
+
const { consumerTag: tag } = await channel.consume(internalQueueName, (msg) => {
|
52
|
+
if (msg) {
|
53
|
+
try {
|
54
|
+
const decoded = JSON.parse(msg.content.toString());
|
55
|
+
onEvent(auditPushed_1.AuditPushedDataZ.parse(decoded));
|
56
|
+
channel.ack(msg);
|
57
|
+
}
|
58
|
+
catch (error) {
|
59
|
+
console.error("Failed to process audit pushed event:", error);
|
60
|
+
channel.nack(msg, false, false);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}, { noAck: false });
|
64
|
+
consumerTag = tag;
|
65
|
+
}
|
66
|
+
catch (error) {
|
67
|
+
console.error("Failed to start audit pushed consumer:", error);
|
68
|
+
throw error;
|
69
|
+
}
|
70
|
+
}
|
71
|
+
async function stop() {
|
72
|
+
if (channel && consumerTag && internalQueueName) {
|
73
|
+
await channel.cancel(consumerTag);
|
74
|
+
consumerTag = null;
|
75
|
+
}
|
76
|
+
if (connection) {
|
77
|
+
await connection.close();
|
78
|
+
connection = null;
|
79
|
+
channel = null;
|
80
|
+
internalQueueName = undefined;
|
81
|
+
}
|
82
|
+
}
|
83
|
+
return { start, stop };
|
84
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import type { GlowListener } from "./types";
|
2
|
+
/**
|
3
|
+
* Options for creating a GlowListener instance.
|
4
|
+
*
|
5
|
+
* - Use admin credentials to emit events and create/bind queues/exchanges.
|
6
|
+
* - Use listener credentials for read-only event consumption (with pre-created queues).
|
7
|
+
* - Specify a custom exchange for multi-exchange support.
|
8
|
+
*/
|
9
|
+
interface GlowListenerOptions {
|
10
|
+
/** Username for RabbitMQ authentication (admin or listener) */
|
11
|
+
username: string;
|
12
|
+
/** Password for RabbitMQ authentication */
|
13
|
+
password: string;
|
14
|
+
/**
|
15
|
+
* The pre-created queue to use for the listener. Must be set up by an admin.
|
16
|
+
*/
|
17
|
+
queueName: string;
|
18
|
+
/**
|
19
|
+
* The pre-created exchange to use. Must be set up by an admin. Defaults to 'glow.audit.v1.exchange'.
|
20
|
+
*/
|
21
|
+
exchange?: string;
|
22
|
+
}
|
23
|
+
/**
|
24
|
+
* Create a GlowListener instance for consuming and emitting typed events on the Glow platform.
|
25
|
+
*
|
26
|
+
* - Use admin credentials to emit events and manage queues/exchanges.
|
27
|
+
* - Use listener credentials for read-only event consumption.
|
28
|
+
* - Supports specifying a custom exchange for multi-exchange scenarios.
|
29
|
+
*/
|
30
|
+
export declare function createGlowListener({ username, password, queueName, exchange, }: GlowListenerOptions): Promise<GlowListener>;
|
31
|
+
export type { GlowListener } from "./types";
|
@@ -0,0 +1,74 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.createGlowListener = createGlowListener;
|
4
|
+
const typed_emitter_1 = require("./typed-emitter");
|
5
|
+
const consumer_1 = require("./consumer");
|
6
|
+
const producer_1 = require("./producer");
|
7
|
+
function validateName(name, type) {
|
8
|
+
// Require at least two segments separated by dots, only alphanumerics, dashes, underscores, and dots
|
9
|
+
const pattern = /^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
|
10
|
+
if (!pattern.test(name)) {
|
11
|
+
throw new Error(`${type} name '${name}' is invalid. Must be dot-separated (e.g., 'glow.audit.v1.exchange').`);
|
12
|
+
}
|
13
|
+
}
|
14
|
+
/**
|
15
|
+
* Create a GlowListener instance for consuming and emitting typed events on the Glow platform.
|
16
|
+
*
|
17
|
+
* - Use admin credentials to emit events and manage queues/exchanges.
|
18
|
+
* - Use listener credentials for read-only event consumption.
|
19
|
+
* - Supports specifying a custom exchange for multi-exchange scenarios.
|
20
|
+
*/
|
21
|
+
async function createGlowListener({ username, password, queueName, exchange = "glow.audit.v1.exchange", }) {
|
22
|
+
validateName(queueName, "queue");
|
23
|
+
validateName(exchange, "exchange");
|
24
|
+
const emitter = (0, typed_emitter_1.createTypedEmitter)();
|
25
|
+
// RabbitMQ consumer instance
|
26
|
+
const consumer = (0, consumer_1.createAuditPushedConsumer)({
|
27
|
+
username,
|
28
|
+
password,
|
29
|
+
onEvent: (event) => {
|
30
|
+
emitter.emit("AuditPushed", event);
|
31
|
+
},
|
32
|
+
skipAssertExchange: true,
|
33
|
+
queueName,
|
34
|
+
skipAssertQueue: true,
|
35
|
+
exchange,
|
36
|
+
});
|
37
|
+
// RabbitMQ producer instance (only if emit permission)
|
38
|
+
const producer = (0, producer_1.createAuditPushedProducer)({ username, password, exchange });
|
39
|
+
function emitAuditSlashed(id) {
|
40
|
+
emitter.emit("AuditSlashed", id); // Local event only
|
41
|
+
}
|
42
|
+
async function emitAuditPushed(data) {
|
43
|
+
try {
|
44
|
+
await producer.emit(data);
|
45
|
+
}
|
46
|
+
catch (error) {
|
47
|
+
throw error;
|
48
|
+
}
|
49
|
+
}
|
50
|
+
async function startListener() {
|
51
|
+
try {
|
52
|
+
await consumer.start();
|
53
|
+
}
|
54
|
+
catch (error) {
|
55
|
+
throw error;
|
56
|
+
}
|
57
|
+
}
|
58
|
+
async function stopListener() {
|
59
|
+
try {
|
60
|
+
await consumer.stop();
|
61
|
+
}
|
62
|
+
catch (error) {
|
63
|
+
throw error;
|
64
|
+
}
|
65
|
+
}
|
66
|
+
return {
|
67
|
+
on: emitter.on,
|
68
|
+
off: emitter.off,
|
69
|
+
emitAuditPushed,
|
70
|
+
emitAuditSlashed,
|
71
|
+
startListener,
|
72
|
+
stopListener,
|
73
|
+
};
|
74
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
15
|
+
};
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
17
|
+
__exportStar(require("./producer"), exports);
|
18
|
+
__exportStar(require("./consumer"), exports);
|
19
|
+
__exportStar(require("./schemas/auditPushed"), exports);
|
20
|
+
__exportStar(require("./glow-listener"), exports);
|
21
|
+
__exportStar(require("./types"), exports);
|
22
|
+
__exportStar(require("./admin"), exports);
|
package/dist/producer.js
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.createAuditPushedProducer = createAuditPushedProducer;
|
7
|
+
const amqplib_1 = __importDefault(require("amqplib"));
|
8
|
+
const auditPushed_1 = require("./schemas/auditPushed");
|
9
|
+
function createAuditPushedProducer({ username, password, exchange = "glow.audit.v1.exchange", }) {
|
10
|
+
// amqplib types are not always compatible with runtime objects, so we use 'as any' as a workaround
|
11
|
+
let connection = null;
|
12
|
+
let channel = null;
|
13
|
+
function buildAmqpUrl() {
|
14
|
+
const url = new URL(`amqp://${username}:${password}@turntable.proxy.rlwy.net:50784`);
|
15
|
+
return url.toString();
|
16
|
+
}
|
17
|
+
async function connect() {
|
18
|
+
if (!connection) {
|
19
|
+
connection = (await amqplib_1.default.connect(buildAmqpUrl()));
|
20
|
+
channel = (await connection.createChannel());
|
21
|
+
}
|
22
|
+
if (channel) {
|
23
|
+
await channel.assertExchange(exchange, "fanout", { durable: true });
|
24
|
+
}
|
25
|
+
}
|
26
|
+
async function emit(data) {
|
27
|
+
try {
|
28
|
+
const payload = auditPushed_1.AuditPushedDataZ.parse(data);
|
29
|
+
await connect();
|
30
|
+
channel.publish(exchange, "", // routingKey is ignored for fanout
|
31
|
+
Buffer.from(JSON.stringify(payload)), { persistent: true });
|
32
|
+
}
|
33
|
+
catch (error) {
|
34
|
+
console.error("Failed to emit audit pushed event:", error);
|
35
|
+
throw error;
|
36
|
+
}
|
37
|
+
}
|
38
|
+
async function disconnect() {
|
39
|
+
if (connection) {
|
40
|
+
await connection.close();
|
41
|
+
connection = null;
|
42
|
+
channel = null;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
return { emit, disconnect };
|
46
|
+
}
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import { z } from "zod";
|
2
|
+
export declare const AuditPushedDataZ: z.ZodObject<{
|
3
|
+
farmId: z.ZodString;
|
4
|
+
protocolFeeUSDPrice_12Decimals: z.ZodString;
|
5
|
+
tokenPrices: z.ZodArray<z.ZodObject<{
|
6
|
+
token: z.ZodString;
|
7
|
+
priceWAD_usd12Decimals: z.ZodString;
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
9
|
+
token: string;
|
10
|
+
priceWAD_usd12Decimals: string;
|
11
|
+
}, {
|
12
|
+
token: string;
|
13
|
+
priceWAD_usd12Decimals: string;
|
14
|
+
}>, "many">;
|
15
|
+
expectedProduction_12Decimals: z.ZodString;
|
16
|
+
glowRewardSplits: z.ZodArray<z.ZodObject<{
|
17
|
+
receiver: z.ZodString;
|
18
|
+
percent: z.ZodNumber;
|
19
|
+
}, "strip", z.ZodTypeAny, {
|
20
|
+
receiver: string;
|
21
|
+
percent: number;
|
22
|
+
}, {
|
23
|
+
receiver: string;
|
24
|
+
percent: number;
|
25
|
+
}>, "many">;
|
26
|
+
cashRewardSplits: z.ZodArray<z.ZodObject<{
|
27
|
+
receiver: z.ZodString;
|
28
|
+
percent: z.ZodNumber;
|
29
|
+
}, "strip", z.ZodTypeAny, {
|
30
|
+
receiver: string;
|
31
|
+
percent: number;
|
32
|
+
}, {
|
33
|
+
receiver: string;
|
34
|
+
percent: number;
|
35
|
+
}>, "many">;
|
36
|
+
}, "strict", z.ZodTypeAny, {
|
37
|
+
farmId: string;
|
38
|
+
protocolFeeUSDPrice_12Decimals: string;
|
39
|
+
tokenPrices: {
|
40
|
+
token: string;
|
41
|
+
priceWAD_usd12Decimals: string;
|
42
|
+
}[];
|
43
|
+
expectedProduction_12Decimals: string;
|
44
|
+
glowRewardSplits: {
|
45
|
+
receiver: string;
|
46
|
+
percent: number;
|
47
|
+
}[];
|
48
|
+
cashRewardSplits: {
|
49
|
+
receiver: string;
|
50
|
+
percent: number;
|
51
|
+
}[];
|
52
|
+
}, {
|
53
|
+
farmId: string;
|
54
|
+
protocolFeeUSDPrice_12Decimals: string;
|
55
|
+
tokenPrices: {
|
56
|
+
token: string;
|
57
|
+
priceWAD_usd12Decimals: string;
|
58
|
+
}[];
|
59
|
+
expectedProduction_12Decimals: string;
|
60
|
+
glowRewardSplits: {
|
61
|
+
receiver: string;
|
62
|
+
percent: number;
|
63
|
+
}[];
|
64
|
+
cashRewardSplits: {
|
65
|
+
receiver: string;
|
66
|
+
percent: number;
|
67
|
+
}[];
|
68
|
+
}>;
|
69
|
+
export type AuditPushedData = z.infer<typeof AuditPushedDataZ>;
|
@@ -0,0 +1,42 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.AuditPushedDataZ = void 0;
|
4
|
+
const zod_1 = require("zod");
|
5
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ *
|
6
|
+
* Helper regexes for onβchain primitives *
|
7
|
+
* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
8
|
+
const hexBytes32 = /^0x[0-9a-fA-F]{64}$/;
|
9
|
+
const ethAddress = /^0x[0-9a-fA-F]{40}$/;
|
10
|
+
const uint256 = /^[0-9]+$/; // unsigned bigβint as decimal string
|
11
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ *
|
12
|
+
* AuditPushed data payload *
|
13
|
+
* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
14
|
+
exports.AuditPushedDataZ = zod_1.z
|
15
|
+
.object({
|
16
|
+
farmId: zod_1.z.string().regex(hexBytes32, "bytes32 hex string"),
|
17
|
+
protocolFeeUSDPrice_12Decimals: zod_1.z
|
18
|
+
.string()
|
19
|
+
.regex(uint256, "uint256 (decimal) β 12 implied decimals"),
|
20
|
+
tokenPrices: zod_1.z.array(zod_1.z.object({
|
21
|
+
token: zod_1.z.string().regex(ethAddress, "ERCβ20 address"),
|
22
|
+
priceWAD_usd12Decimals: zod_1.z.string().regex(uint256),
|
23
|
+
})),
|
24
|
+
// might be removed
|
25
|
+
// payers: z.array(
|
26
|
+
// z.object({
|
27
|
+
// payer: z.string().regex(ethAddress, "payer address"),
|
28
|
+
// amountToPay_USD12Decimals: z.string().regex(uint256),
|
29
|
+
// })
|
30
|
+
// ),
|
31
|
+
expectedProduction_12Decimals: zod_1.z.string().regex(uint256),
|
32
|
+
glowRewardSplits: zod_1.z.array(zod_1.z.object({
|
33
|
+
receiver: zod_1.z.string().regex(ethAddress),
|
34
|
+
percent: zod_1.z.number().min(0).max(100),
|
35
|
+
})),
|
36
|
+
cashRewardSplits: zod_1.z.array(zod_1.z.object({
|
37
|
+
receiver: zod_1.z.string().regex(ethAddress),
|
38
|
+
percent: zod_1.z.number().min(0).max(100),
|
39
|
+
})),
|
40
|
+
})
|
41
|
+
.strict()
|
42
|
+
.describe("Glow AuditPushed event payload");
|
@@ -0,0 +1,7 @@
|
|
1
|
+
type Listener<T extends any[]> = (...args: T) => void;
|
2
|
+
export declare function createTypedEmitter<TEvents extends Record<string, any[]>>(): {
|
3
|
+
on: <K extends keyof TEvents>(event: K, listener: Listener<TEvents[K]>) => () => void;
|
4
|
+
off: <K extends keyof TEvents>(event: K, listener: Listener<TEvents[K]>) => void;
|
5
|
+
emit: <K extends keyof TEvents>(event: K, ...args: TEvents[K]) => void;
|
6
|
+
};
|
7
|
+
export {};
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.createTypedEmitter = createTypedEmitter;
|
4
|
+
function createTypedEmitter() {
|
5
|
+
const listeners = {};
|
6
|
+
function on(event, listener) {
|
7
|
+
(listeners[event] || (listeners[event] = [])).push(listener);
|
8
|
+
return () => off(event, listener);
|
9
|
+
}
|
10
|
+
function off(event, listener) {
|
11
|
+
listeners[event] = (listeners[event] || []).filter((l) => l !== listener);
|
12
|
+
}
|
13
|
+
function emit(event, ...args) {
|
14
|
+
(listeners[event] || []).forEach((listener) => listener(...args));
|
15
|
+
}
|
16
|
+
return { on, off, emit };
|
17
|
+
}
|
package/dist/types.d.ts
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
import type { AuditPushedData } from "./schemas/auditPushed";
|
2
|
+
export type AuditPushedEvent = AuditPushedData;
|
3
|
+
export type AuditSlashedId = `0x${string}`;
|
4
|
+
export interface GlowEvents {
|
5
|
+
AuditPushed: [AuditPushedEvent];
|
6
|
+
AuditSlashed: [AuditSlashedId];
|
7
|
+
[key: string]: any[];
|
8
|
+
}
|
9
|
+
export type SdkPermission = "listen" | "emit";
|
10
|
+
export interface SdkAuthConfig {
|
11
|
+
username: string;
|
12
|
+
password: string;
|
13
|
+
permissions: SdkPermission[];
|
14
|
+
}
|
15
|
+
export type GlowListenerEvent = "AuditPushed" | "AuditSlashed";
|
16
|
+
export interface GlowListener {
|
17
|
+
on: <K extends GlowListenerEvent>(event: K, listener: (...args: GlowEvents[K]) => void) => () => void;
|
18
|
+
off: <K extends GlowListenerEvent>(event: K, listener: (...args: GlowEvents[K]) => void) => void;
|
19
|
+
emitAuditPushed?: (data: AuditPushedEvent) => Promise<void>;
|
20
|
+
emitAuditSlashed?: (id: AuditSlashedId) => void;
|
21
|
+
startListener: () => Promise<void>;
|
22
|
+
stopListener: () => Promise<void>;
|
23
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
{
|
2
|
+
"name": "@glowlabs-org/events-sdk",
|
3
|
+
"version": "0.1.0",
|
4
|
+
"description": "Typed event SDK for Glow, powered by Redpanda/Kafka and Zod.",
|
5
|
+
"main": "dist/index.js",
|
6
|
+
"types": "dist/index.d.ts",
|
7
|
+
"files": [
|
8
|
+
"dist"
|
9
|
+
],
|
10
|
+
"publishConfig": {
|
11
|
+
"access": "public"
|
12
|
+
},
|
13
|
+
"repository": {
|
14
|
+
"type": "git",
|
15
|
+
"url": "https://github.com/glowlabs-org/glow-events.git"
|
16
|
+
},
|
17
|
+
"author": "Lironie <julien@glowlabs.org>",
|
18
|
+
"license": "MIT",
|
19
|
+
"keywords": [
|
20
|
+
"kafka",
|
21
|
+
"redpanda",
|
22
|
+
"events",
|
23
|
+
"sdk",
|
24
|
+
"zod",
|
25
|
+
"typescript",
|
26
|
+
"glow"
|
27
|
+
],
|
28
|
+
"scripts": {
|
29
|
+
"build": "tsc -p tsconfig.json",
|
30
|
+
"prepublishOnly": "npm run build"
|
31
|
+
},
|
32
|
+
"dependencies": {
|
33
|
+
"@kafkajs/confluent-schema-registry": "^3.8.0",
|
34
|
+
"amqplib": "^0.10.8",
|
35
|
+
"json-schema": "^0.4.0",
|
36
|
+
"kafkajs": "^2.2.4",
|
37
|
+
"uuid": "^11.1.0",
|
38
|
+
"zod": "^3.24.4",
|
39
|
+
"zod-to-json-schema": "^3.24.5"
|
40
|
+
},
|
41
|
+
"devDependencies": {
|
42
|
+
"@types/amqplib": "^0.10.7",
|
43
|
+
"@types/json-schema": "^7.0.15",
|
44
|
+
"typescript": "^5.8.3"
|
45
|
+
}
|
46
|
+
}
|