univo 0.3.2 → 0.4.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.
@@ -0,0 +1,20 @@
1
+ import { T as Transport, I as IndexerRpc } from './shared/univo.DYd1g40o.mjs';
2
+ import 'viem';
3
+ import './metadata.mjs';
4
+ import './metadata/adapters.mjs';
5
+
6
+ /**
7
+ * Creates a web standard HTTP request handler `(req: Request) => Promise<Response>`.
8
+ *
9
+ * You can use this to deploy your indexer over HTTPs using any of your favourite frameworks like
10
+ * Next.js, Cloudflare Workers, or Bun.
11
+ *
12
+ * Note that we use the provided `signingKey` to prevent any unauthorized access.
13
+ */
14
+ declare function http(opts: {
15
+ quiet?: boolean;
16
+ indexer: Transport<IndexerRpc>;
17
+ signingKey: string;
18
+ }): (req: Request) => Promise<Response>;
19
+
20
+ export { http };
@@ -0,0 +1,20 @@
1
+ import { T as Transport, I as IndexerRpc } from './shared/univo.CEA4vY0E.js';
2
+ import 'viem';
3
+ import './metadata.js';
4
+ import './metadata/adapters.js';
5
+
6
+ /**
7
+ * Creates a web standard HTTP request handler `(req: Request) => Promise<Response>`.
8
+ *
9
+ * You can use this to deploy your indexer over HTTPs using any of your favourite frameworks like
10
+ * Next.js, Cloudflare Workers, or Bun.
11
+ *
12
+ * Note that we use the provided `signingKey` to prevent any unauthorized access.
13
+ */
14
+ declare function http(opts: {
15
+ quiet?: boolean;
16
+ indexer: Transport<IndexerRpc>;
17
+ signingKey: string;
18
+ }): (req: Request) => Promise<Response>;
19
+
20
+ export { http };
package/dist/proxy.mjs ADDED
@@ -0,0 +1,90 @@
1
+ import { g as getException } from './shared/univo.C0flyVee.mjs';
2
+ import { c as createLogger, d as decompress, e as decoder } from './shared/univo.mlthZw0m.mjs';
3
+
4
+ function http(opts) {
5
+ const log = createLogger({ quiet: opts.quiet ?? false, prefix: "[proxy]" });
6
+ return async function proxy(req) {
7
+ let body_buffer;
8
+ try {
9
+ body_buffer = await req.arrayBuffer();
10
+ } catch {
11
+ return Response.json(
12
+ { jsonrpc: "2.0", id: null, error: { code: 0, message: "Invalid request body" } },
13
+ //
14
+ { status: 400 }
15
+ );
16
+ }
17
+ const authorization = req.headers.get("Authorization");
18
+ const authenticated = authorization === `Bearer ${opts.signingKey}`;
19
+ let body_string;
20
+ if (req.headers.get("Content-Encoding") === "gzip") {
21
+ if (!authorization) {
22
+ return Response.json(
23
+ { jsonrpc: "2.0", id: null, error: { code: 0, message: "No bearer token provided" } },
24
+ //
25
+ { status: 400 }
26
+ );
27
+ }
28
+ if (!authenticated) {
29
+ return Response.json(
30
+ { jsonrpc: "2.0", id: null, error: { code: 0, message: "Invalid bearer token" } },
31
+ //
32
+ { status: 400 }
33
+ );
34
+ }
35
+ body_string = await decompress(body_buffer);
36
+ } else {
37
+ body_string = decoder.decode(body_buffer);
38
+ }
39
+ let json;
40
+ try {
41
+ json = JSON.parse(body_string);
42
+ } catch {
43
+ return Response.json(
44
+ { jsonrpc: "2.0", id: null, error: { code: 0, message: "Malformed JSON request body" } },
45
+ //
46
+ { status: 400 }
47
+ );
48
+ }
49
+ if (json.method.startsWith("private_")) {
50
+ if (!authorization) {
51
+ return Response.json(
52
+ { jsonrpc: "2.0", id: json.id, error: { code: 0, message: "No bearer token provided" } },
53
+ //
54
+ { status: 400 }
55
+ );
56
+ }
57
+ if (authorization !== `Bearer ${opts.signingKey}`) {
58
+ return Response.json(
59
+ { jsonrpc: "2.0", id: json.id, error: { code: 0, message: "Invalid bearer token" } },
60
+ //
61
+ { status: 400 }
62
+ );
63
+ }
64
+ }
65
+ try {
66
+ const result = await opts.indexer.request({ method: json.method, params: json.params });
67
+ return Response.json({ jsonrpc: "2.0", id: json.id, result });
68
+ } catch (error) {
69
+ const message = getException(error);
70
+ if (message) {
71
+ log.error(message);
72
+ return Response.json(
73
+ { jsonrpc: "2.0", id: json.id, error: { code: 0, message } },
74
+ //
75
+ { status: 400 }
76
+ );
77
+ }
78
+ if (error instanceof Error) {
79
+ log.error(error.message);
80
+ }
81
+ return Response.json(
82
+ { jsonrpc: "2.0", id: json.id, error: { code: 0, message: "Internal server error" } },
83
+ //
84
+ { status: 500 }
85
+ );
86
+ }
87
+ };
88
+ }
89
+
90
+ export { http };
@@ -1,5 +1,4 @@
1
- import { NodeRpc, IndexerRpc } from './rpc.mjs';
2
- import { Transport } from './transport.mjs';
1
+ import { T as Transport, R as Rpc, I as IndexerRpc } from './shared/univo.DYd1g40o.mjs';
3
2
  import 'viem';
4
3
  import './metadata.mjs';
5
4
  import './metadata/adapters.mjs';
@@ -17,7 +16,7 @@ type RealtimeOptions = {
17
16
  *
18
17
  * Must use the `wss` transport protocol from `univo/transport`.
19
18
  */
20
- node: Transport<NodeRpc, "wss">;
19
+ node: Transport<Rpc, "wss">;
21
20
  /**
22
21
  * Connection to a univo indexer.
23
22
  *
@@ -1,5 +1,4 @@
1
- import { NodeRpc, IndexerRpc } from './rpc.js';
2
- import { Transport } from './transport.js';
1
+ import { T as Transport, R as Rpc, I as IndexerRpc } from './shared/univo.CEA4vY0E.js';
3
2
  import 'viem';
4
3
  import './metadata.js';
5
4
  import './metadata/adapters.js';
@@ -17,7 +16,7 @@ type RealtimeOptions = {
17
16
  *
18
17
  * Must use the `wss` transport protocol from `univo/transport`.
19
18
  */
20
- node: Transport<NodeRpc, "wss">;
19
+ node: Transport<Rpc, "wss">;
21
20
  /**
22
21
  * Connection to a univo indexer.
23
22
  *
package/dist/realtime.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { hexToNumber } from 'viem';
2
- import { c as createLogger, i as iife, m as mutex, r as retry, a as isHexEqual } from './shared/univo.vwk4Xrn3.mjs';
2
+ import { c as createLogger, b as iife, m as mutex, r as retry, i as isHexEqual } from './shared/univo.mlthZw0m.mjs';
3
3
 
4
4
  function defineBlockchain(opts) {
5
5
  const log = createLogger({ quiet: opts.quiet });
@@ -88,6 +88,9 @@ function realtime(opts) {
88
88
  const log = createLogger({ quiet: opts.quiet ?? false, prefix: "[realtime]" });
89
89
  async function getBlockByHash(hash) {
90
90
  const block = await opts.node.request({ method: "eth_getBlockByHash", params: [hash, false] });
91
+ if (block === null) {
92
+ return null;
93
+ }
91
94
  return { number: block.number, hash: block.hash, parent_hash: block.parentHash };
92
95
  }
93
96
  const promise = iife(async () => {
@@ -95,10 +98,16 @@ function realtime(opts) {
95
98
  const chain = await opts.node.request({ method: "eth_chainId", params: [] });
96
99
  log.debug(`Determined chain identifier for connected node: ${hexToNumber(chain)}`);
97
100
  const [chainLatestBlock, chainFinalizedBlock] = await Promise.all([
98
- opts.node.request({ params: ["latest", false], method: "eth_getBlockByNumber" }),
101
+ opts.node.request({ method: "eth_getBlockByNumber", params: ["latest", false] }),
99
102
  //
100
- opts.node.request({ params: ["finalized", false], method: "eth_getBlockByNumber" })
103
+ opts.node.request({ method: "eth_getBlockByNumber", params: ["finalized", false] })
101
104
  ]);
105
+ if (chainLatestBlock === null) {
106
+ throw new Error("Latest block not found");
107
+ }
108
+ if (chainFinalizedBlock === null) {
109
+ throw new Error("Finalized block not found");
110
+ }
102
111
  const unfinalized = defineBlockchain({
103
112
  getBlockByHash,
104
113
  quiet: opts.quiet ?? false,
@@ -165,7 +174,14 @@ function realtime(opts) {
165
174
  method: "eth_getBlockByNumber",
166
175
  params: ["finalized", false]
167
176
  });
177
+ if (finalizedBlock === null) {
178
+ throw new Error("Finalized block not found");
179
+ }
168
180
  const nextFinalizedHeight = hexToNumber(finalizedBlock.number);
181
+ if (nextFinalizedHeight <= chainFinalizedHeight) {
182
+ return log.debug("Chain finalized height has not progressed");
183
+ }
184
+ await opts.indexer.request({ params: [chain], method: "public_writeFinalizedHeight" });
169
185
  const newFinalizedHeads = unfinalized.chain.filter((head) => {
170
186
  if (hexToNumber(head.number) > chainFinalizedHeight && hexToNumber(head.number) <= nextFinalizedHeight) {
171
187
  return true;
@@ -175,6 +191,7 @@ function realtime(opts) {
175
191
  return { chain, ...head };
176
192
  });
177
193
  if (newFinalizedHeads.length === 0) {
194
+ chainFinalizedHeight = nextFinalizedHeight;
178
195
  return log.debug("No new finalized heads to process");
179
196
  }
180
197
  log.debug(`Processing ${newFinalizedHeads.length} finalized head(s) in parallel`);
package/dist/rpc.d.mts CHANGED
@@ -1,327 +1,4 @@
1
- import * as viem from 'viem';
2
- import { Storage } from './metadata.mjs';
1
+ export { I as IndexerRpc, R as Rpc } from './shared/univo.DYd1g40o.mjs';
2
+ import 'viem';
3
+ import './metadata.mjs';
3
4
  import './metadata/adapters.mjs';
4
-
5
- type CamelToSnake<T extends string, P extends string = ""> = string extends T ? string : T extends `${infer C0}${infer R}` ? CamelToSnake<R, `${P}${C0 extends Lowercase<C0> ? "" : "_"}${Lowercase<C0>}`> : P;
6
- type Flatten<T> = {
7
- [K in keyof T as CamelToSnake<Extract<K, string>>]: T[K];
8
- };
9
-
10
- type Rpc = {
11
- /**
12
- * All available methods that can be requested
13
- */
14
- request: Record<string, any>;
15
- /**
16
- * All available event types to subscribe to a stream of values
17
- */
18
- subscribe: Record<string, any>;
19
- };
20
- /**
21
- * Node -----------------------------------------------------------------------------------------------------------------------------------
22
- */
23
- type NodeRpc = {
24
- request: {
25
- /**
26
- * @returns The chain identifier of the node
27
- */
28
- eth_chainId: () => Promise<`0x${string}`>;
29
- /**
30
- * Accepts a block tag and boolean for including receipts
31
- * @returns A block with optional receipts
32
- */
33
- eth_getBlockByNumber: (number: string, receipts: boolean) => Promise<NodeBlock>;
34
- /**
35
- * Accepts a block hash and boolean for including receipts
36
- * @returns A block with optional receipts
37
- */
38
- eth_getBlockByHash: (hash: `0x${string}`, receipts: boolean) => Promise<NodeBlock>;
39
- };
40
- subscribe: {
41
- newHeads: NodeBlock;
42
- };
43
- };
44
- type NodeBlock = {
45
- hash: `0x${string}`;
46
- number: `0x${string}`;
47
- parentHash: `0x${string}`;
48
- };
49
- /**
50
- * Indexer -----------------------------------------------------------------------------------------------------------------------------------
51
- */
52
- type IndexerRpc = {
53
- request: {
54
- /**
55
- * Accepts a chain identifier
56
- * @returns Height of the last finalized block indexed
57
- */
58
- public_getFinalizedHeight: (chain: `0x${string}`) => Promise<number>;
59
- /**
60
- * Accepts a reorganised block and deletes all related events from storage
61
- */
62
- public_deleteReorganisedHead: (head: Head) => Promise<void>;
63
- /**
64
- * Accepts an unfinalized head and writes all events to storage
65
- */
66
- public_writeUnfinalizedHead: (head: Head) => Promise<void>;
67
- /**
68
- * Accepts a finalized head and executes all actions
69
- */
70
- public_writeFinalizedHead: (head: Head) => Promise<void>;
71
- /**
72
- * Accepts a chain identifier and finalizes the indexer
73
- */
74
- public_finalize: (chain: `0x${string}`) => Promise<void>;
75
- /**
76
- * @returns Specified metadata about the indexer
77
- */
78
- private_getMetadata: () => Promise<Metadata>;
79
- /**
80
- * @returns All events defined on this indexer
81
- */
82
- private_getEvents: () => Promise<{
83
- id: string;
84
- filters: Flatten<Filter>[];
85
- }[]>;
86
- /**
87
- * Accepts a list of events and a batch of minified block data and writes to storage
88
- * @returns A list of failures that occured for each event and block pair
89
- */
90
- private_writeEvents: (params: {
91
- events: string[];
92
- blocks: Block[];
93
- }) => Promise<{
94
- failures: Result[];
95
- }>;
96
- /**
97
- * Accepts a block head and writes events to storage
98
- * @returns A list of results and block keys that were accessed as those events were written to storage
99
- */
100
- private_writeEventsAndGetKeys: (params: {
101
- events: string[];
102
- head: Head;
103
- }) => Promise<{
104
- results: Result[];
105
- keys: string[];
106
- }>;
107
- };
108
- subscribe: Record<string, never>;
109
- };
110
-
111
- /**
112
- * Block -----------------------------------------------------------------------------------------------------------------------------------
113
- */
114
- type Block = {
115
- eth_chainId: `0x${string}`;
116
- eth_getBlockByNumber: viem.RpcBlock<"latest", true>;
117
- eth_getBlockReceipts: viem.RpcTransactionReceipt[];
118
- };
119
- /**
120
- * Filters -----------------------------------------------------------------------------------------------------------------------------------
121
- */
122
- type Filter = {
123
- /** Index blocks with this chain id */
124
- chain: number;
125
- /** Index blocks from this start block (inclusive) */
126
- fromBlock: number;
127
- /** Index blocks until this stop block (inclusive) */
128
- toBlock?: number;
129
- /** Index blocks where this event topic was emitted */
130
- event?: `0x${string}`;
131
- /** Index blocks that involve this address */
132
- address?: `0x${string}`;
133
- };
134
- /**
135
- * Events -----------------------------------------------------------------------------------------------------------------------------------
136
- */
137
- type Event<TBlock, TEvent> = {
138
- /**
139
- * A human-readable identifier for the event.
140
- */
141
- id: string;
142
- /**
143
- * Filters let you define what specific blocks you want this event to index.
144
- *
145
- * Blockchains are massive datasets and most of the time we are only ever interested in small portions of it.
146
- * Sometimes that can be specific events like ERC20 transfers and other times it could be all events emitted
147
- * by a specific contract. Filters provide a simple way for an event to define exactly which blocks we want
148
- * to index.
149
- *
150
- * Filters reduce costs and improve backfill performance by ensuring we only index the blocks we need and
151
- * ignore the blocks that don't have the data we are interested in.
152
- *
153
- * By default your event will not index any blocks, you must opt-in to indexing by providing atleast one filter.
154
- *
155
- * Each property in the filter operates like an AND statement. For example, if you specifiy an `address` and
156
- * an `event` it implies that you only want to index blocks where the specific `address` emitted the specific
157
- * `event` topic provided.
158
- *
159
- * However, when multiple filters are defined for a given event those filters operate like an OR statement. For
160
- * example, if we define a second filter looking for a different `address` and `event` our event will now index
161
- * any block that matches either the first filter or the second filter.
162
- *
163
- * Note that filters are a rudimentary method to dramatically reduce the number of blocks your application needs
164
- * to index. Any advanced filtering should be performed in the event `handler` itself.
165
- */
166
- filters: Filter[];
167
- /**
168
- * Synchronously transforms a raw block into a list of events.
169
- *
170
- * When a block matches any of filters defined by your event it will be passed to this function to be synchronously
171
- * transformed into a list of structured events.
172
- *
173
- * The shape of the input `block` is determined by the return value of `getBlock` function provided to your `indexer`.
174
- *
175
- * The returned output value should be an array containg any valid JavaScript values. Each event that you return from
176
- * your handler should not depend on any information outside of the input block data. It should directly map a given
177
- * input (the raw block) to a given output (structured events). This ensures that handler remains idempotent and that
178
- * repeated calls with the same input block produce the same output events.
179
- */
180
- handler: (block: TBlock) => TEvent[];
181
- /**
182
- * Storage adapter to persist events in your off-chain storage.
183
- */
184
- storage: {
185
- /**
186
- * Upserts a batch of events into your storage system.
187
- *
188
- * After a block is transformed into a list of structured events by your `handler` function, that batch
189
- * is passed to this `upsert` function so they can be upserted into your storage system.
190
- *
191
- * This function must be idempotent. Functionally, this means that if the same batch of events is upserted
192
- * multiple times it only produces a single set of events in your storage system.
193
- */
194
- upsert: (events: TEvent[]) => Promise<void>;
195
- /**
196
- * Deletes a batch of events from your storage system.
197
- *
198
- * When your indexer encounters a chain reorganisation, this `delete` function will be invoked with the
199
- * same batch of events created by that reorganised block. This ensures that any events that were previously
200
- * upserted into your storage system that are no longer part of the canonical chain are safely removed.
201
- */
202
- delete: (events: TEvent[]) => Promise<void>;
203
- };
204
- };
205
- /**
206
- * Actions -----------------------------------------------------------------------------------------------------------------------------------
207
- */
208
- type Action<TBlock, TEvent> = {
209
- /**
210
- * A human-readable identifier for the action.
211
- */
212
- id: string;
213
- /**
214
- * The on-chain event that should invoke this action.
215
- */
216
- event: Event<TBlock, TEvent>;
217
- /**
218
- * The action you want to execute when the event _finalizes_ on-chain.
219
- *
220
- * Actions are processed during realtime indexing only and are never invoked during historical
221
- * backfills. Common actions include payment notifications for confirming payments or customer
222
- * deposits, transaction monitoring for KYC/AML compliance tracking, DeFi protocol monitoring,
223
- * or wallet activity notifications or alerts.
224
- *
225
- * Actions may be invoked multiple times. It is important that your application code is
226
- * resilient to this by making use of an idempotency key (usually the id of the event passed
227
- * to your handler).
228
- *
229
- * Actions can be extremely powerful when combined with a durable execution framework like
230
- * Temporal, Inngest, Trigger.dev, Cloudflare Workflows, or Restate.dev to perform more
231
- * advanced workflows that allow you to chain together multiple steps and handle retries.
232
- *
233
- * Actions operate with at-least-once delivery. This means that your actions may be invoked
234
- * multiple times. It is important that your handler is resilient to this by making use of an
235
- * idempotency key (usually the identifier in the event passed to your handler). We guarantee
236
- * that the indexer will not finalize a given block until it achieves a non-erroring execution
237
- * from your action.
238
- */
239
- handler: (event: TEvent) => Promise<void> | void;
240
- };
241
- /**
242
- * Indexer -----------------------------------------------------------------------------------------------------------------------------------
243
- */
244
- type Head = {
245
- hash: `0x${string}`;
246
- chain: `0x${string}`;
247
- number: `0x${string}`;
248
- parent_hash: `0x${string}`;
249
- };
250
- type Metadata = {
251
- version: string;
252
- language: string;
253
- };
254
- type Result = {
255
- status: string;
256
- event_id: string;
257
- hash: `0x${string}`;
258
- chain: `0x${string}`;
259
- number: `0x${string}`;
260
- parent_hash: `0x${string}`;
261
- created_at: number;
262
- };
263
- type IndexerOptions<TBlock> = {
264
- /**
265
- * Silences all logs including errors.
266
- *
267
- * Logs are emitted based on the environment LOG_LEVEL. Set `quiet: true` to surpress all logs.
268
- * Available log options are `DEBUG`, `INFO`, `WARN`, and `ERROR`.
269
- */
270
- quiet?: boolean;
271
- /**
272
- * Request signing key
273
- *
274
- * Facilitates secure communication with a trusted source like the univo dashboard.
275
- */
276
- signingKey: string;
277
- /**
278
- * Storage interface for durably persisting indexer metadata.
279
- *
280
- * Functionally, storing metadata is fundamental to ensure the correct operation of your indexer. It
281
- * ensures that you indexer recovers from downtime and ensures that all blocks are processed correctly during
282
- * chain reorganisations.
283
- */
284
- metadataStorage: Storage;
285
- /**
286
- * Loads raw block data in realtime from a trusted set of RPC sources.
287
- *
288
- * Note that historical backfills do not use this function to load block data.
289
- *
290
- * The response from this function determines the input data to your `handler` functions in each event.
291
- * Generally, the shape is a list of RPC methods with their corresponding value from an RPC node. This
292
- * generic format allows you support the full range of RPC data available to you from your node. This
293
- * flexibility is important in cases where some methods are supported on specific chains only.
294
- *
295
- * Note that there is a minimum set of RPC methods expected on the response notably `eth_chainId`,
296
- * `eth_getBlockByNumber` and `eth_getBlockReceipts`. These are expected so that we can safely match
297
- * each block processed against your event filters.
298
- *
299
- * Note that we provide only the block number to look up block data. During a chain reorganisation, this
300
- * makes it possible for different RPC calls to return data for different block hashes. We manually verify
301
- * that all methods return the expected block hash. However, for any custom methods you add you must manually
302
- * verify the block hash is consistent with the other known methods.
303
- */
304
- getBlock: (block: {
305
- chain: `0x${string}`;
306
- number: string;
307
- }) => Promise<TBlock | null>;
308
- };
309
- type Indexer<TBlock> = {
310
- /**
311
- * A web standard HTTP request handler. This is the entrypoint to your indexer and allows it to be deployed
312
- * to your framework of choice so that it can receive requests and process responses.
313
- */
314
- fetch: (req: Request) => Promise<Response>;
315
- /**
316
- * Define on-chain events you want to record in your off-chain storage system.
317
- */
318
- event: <TEvent>(event: Event<TBlock, TEvent>) => Event<TBlock, TEvent>;
319
- /**
320
- * Perform fire-and-forget effects in response to on-chain events.
321
- */
322
- action: <TEvent>(action: Action<TBlock, TEvent>) => Action<TBlock, TEvent>;
323
- };
324
- declare function defineIndexer<TBlock extends Block>(opts: IndexerOptions<TBlock>): Indexer<TBlock> & IndexerRpc;
325
-
326
- export { defineIndexer as d };
327
- export type { Block as B, Event as E, Filter as F, Head as H, Indexer as I, IndexerRpc, Metadata as M, NodeRpc, Result as R, Rpc };