univo 0.2.11 → 0.2.13
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/dist/cli.mjs +2 -3
- package/dist/index.d.mts +2 -189
- package/dist/index.d.ts +2 -189
- package/dist/index.mjs +59 -28
- package/dist/realtime.d.mts +1 -1
- package/dist/realtime.d.ts +1 -1
- package/dist/realtime.mjs +1 -1
- package/dist/rpc.d.mts +4 -9
- package/dist/rpc.d.ts +4 -9
- package/dist/shared/{univo.Cz5ut_5D.mjs → univo.B_UsEiYb.mjs} +1 -1
- package/dist/shared/univo.BwPUSTpl.mjs +221 -0
- package/dist/shared/univo.Cj4s4jNq.d.mts +219 -0
- package/dist/shared/univo.Cj4s4jNq.d.ts +219 -0
- package/dist/transport.d.mts +5 -4
- package/dist/transport.d.ts +5 -4
- package/dist/transport.mjs +4 -188
- package/package.json +1 -3
- package/dist/shared/univo.C0flyVee.mjs +0 -21
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import ws from 'ws';
|
|
2
|
+
import { WebSocket } from 'partysocket';
|
|
3
|
+
import { g as compress, j as raise, c as createLogger } from './univo.B_UsEiYb.mjs';
|
|
4
|
+
|
|
5
|
+
function createException(message) {
|
|
6
|
+
return `univo:${message}`;
|
|
7
|
+
}
|
|
8
|
+
function catchException(unknown, exception) {
|
|
9
|
+
if (unknown instanceof Error) {
|
|
10
|
+
if (unknown.message === exception) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
function getException(unknown) {
|
|
17
|
+
if (unknown instanceof Error) {
|
|
18
|
+
if (unknown.message.startsWith("univo")) {
|
|
19
|
+
const [_, message] = unknown.message.split(":");
|
|
20
|
+
if (message) return message;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const UnknownMethodError = createException("The requested RPC method does not exist on the provided implementation");
|
|
26
|
+
function local(rpc) {
|
|
27
|
+
const request = async (opts) => {
|
|
28
|
+
const method = rpc.request[opts.method];
|
|
29
|
+
if (method === void 0) {
|
|
30
|
+
throw new Error(UnknownMethodError);
|
|
31
|
+
}
|
|
32
|
+
return await method(...opts.params);
|
|
33
|
+
};
|
|
34
|
+
const subscribe = async () => {
|
|
35
|
+
throw new Error("Unable to `subscribe` on `local` transport");
|
|
36
|
+
};
|
|
37
|
+
return { protocol: "local", request, subscribe };
|
|
38
|
+
}
|
|
39
|
+
function createSocket(url, opts) {
|
|
40
|
+
const socket = new WebSocket(url, [], { WebSocket: ws });
|
|
41
|
+
socket.binaryType = "arraybuffer";
|
|
42
|
+
if (opts.onOpen) socket.onopen = opts.onOpen;
|
|
43
|
+
if (opts.onError) socket.onerror = opts.onError;
|
|
44
|
+
if (opts.onClose) socket.onclose = opts.onClose;
|
|
45
|
+
if (opts.onMessage) socket.onmessage = opts.onMessage;
|
|
46
|
+
return socket;
|
|
47
|
+
}
|
|
48
|
+
function wss(url, opts = {}) {
|
|
49
|
+
if (!url.startsWith("wss://")) {
|
|
50
|
+
throw new Error("Websocket connections must start with `wss://`");
|
|
51
|
+
}
|
|
52
|
+
const logger = createLogger({ quiet: opts.quiet ?? false });
|
|
53
|
+
let id = 0;
|
|
54
|
+
const requests = /* @__PURE__ */ new Map();
|
|
55
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
56
|
+
const params = /* @__PURE__ */ new Map();
|
|
57
|
+
const socket = createSocket(url, {
|
|
58
|
+
async onError(cause) {
|
|
59
|
+
logger.error(new Error("Socket error", { cause }));
|
|
60
|
+
},
|
|
61
|
+
async onOpen() {
|
|
62
|
+
if (subscriptions.size === 0) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const params2 = [];
|
|
66
|
+
for (const [id2, subscription] of subscriptions) {
|
|
67
|
+
params2.push(subscription.param);
|
|
68
|
+
request({ method: "eth_unsubscribe", params: [id2] }).catch(() => {
|
|
69
|
+
logger.warn("Failed to cancel old susbcription");
|
|
70
|
+
});
|
|
71
|
+
subscriptions.delete(id2);
|
|
72
|
+
}
|
|
73
|
+
for (const param of params2) {
|
|
74
|
+
request({ method: "eth_subscribe", params: [param] }).then((id2) => {
|
|
75
|
+
subscriptions.set(id2, { param, latest: Date.now() });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
async onMessage(event) {
|
|
80
|
+
const data = JSON.parse(event.data);
|
|
81
|
+
if (data.method === "eth_subscription") {
|
|
82
|
+
const subscription = subscriptions.get(data.params.subscription);
|
|
83
|
+
if (subscription === void 0) {
|
|
84
|
+
return await request({ method: "eth_unsubscribe", params: [data.params.subscription] }).catch(() => {
|
|
85
|
+
logger.warn("Failed to unsubscribe stale subscription");
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const handlers = params.get(subscription.param);
|
|
89
|
+
if (handlers === void 0) {
|
|
90
|
+
return await request({ method: "eth_unsubscribe", params: [data.params.subscription] }).catch(() => {
|
|
91
|
+
logger.warn("Failed to unsubscribe stale subscription");
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
for (const handler2 of handlers) {
|
|
95
|
+
try {
|
|
96
|
+
handler2(data.params.result);
|
|
97
|
+
} catch (cause) {
|
|
98
|
+
logger.error(new Error("Handler error", { cause }));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
subscription.latest = Date.now();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const handler = requests.get(data.id);
|
|
105
|
+
if (handler === void 0) {
|
|
106
|
+
return logger.warn(`Received unknown response for request id ${data.id}...`);
|
|
107
|
+
}
|
|
108
|
+
return handler(data.result);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
const request = async (opts2) => {
|
|
112
|
+
return await new Promise((resolve, reject) => {
|
|
113
|
+
const body = Object.assign({ id: id++ }, opts2);
|
|
114
|
+
const timeout = setTimeout(() => {
|
|
115
|
+
requests.delete(body.id);
|
|
116
|
+
reject(new Error("wss request timed out after 30 seconds"));
|
|
117
|
+
}, 30 * 1e3);
|
|
118
|
+
requests.set(body.id, (data) => {
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
requests.delete(body.id);
|
|
121
|
+
resolve(data);
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
socket.send(JSON.stringify(body));
|
|
125
|
+
} catch (cause) {
|
|
126
|
+
clearTimeout(timeout);
|
|
127
|
+
requests.delete(body.id);
|
|
128
|
+
reject(new Error(ClientConnectionError, { cause }));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
const subscribe = async (param, handler) => {
|
|
133
|
+
const handlers = params.get(param);
|
|
134
|
+
if (handlers === void 0) {
|
|
135
|
+
const handlers2 = (/* @__PURE__ */ new Set()).add(handler);
|
|
136
|
+
params.set(param, handlers2);
|
|
137
|
+
const id2 = await request({ method: "eth_subscribe", params: [param] }).catch((cause) => {
|
|
138
|
+
throw new Error(`Failed to initialise subscription for param ${param}`, { cause });
|
|
139
|
+
});
|
|
140
|
+
subscriptions.set(id2, { param, latest: Date.now() });
|
|
141
|
+
} else {
|
|
142
|
+
handlers.add(handler);
|
|
143
|
+
}
|
|
144
|
+
return async function unsubscribe() {
|
|
145
|
+
const handlers2 = params.get(param);
|
|
146
|
+
if (handlers2 === void 0) {
|
|
147
|
+
return logger.warn("Param already unsubscribed...");
|
|
148
|
+
}
|
|
149
|
+
handlers2.delete(handler);
|
|
150
|
+
if (handlers2.size === 0) {
|
|
151
|
+
await request({ method: "eth_unsubscribe", params: [id] }).catch(() => {
|
|
152
|
+
logger.warn("Failed to unsubscribe unused subscription");
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
function healthcheck() {
|
|
158
|
+
const params2 = [];
|
|
159
|
+
for (const [id2, subscription] of subscriptions) {
|
|
160
|
+
if (Date.now() - subscription.latest > 60 * 1e3) {
|
|
161
|
+
params2.push(subscription.param);
|
|
162
|
+
request({ method: "eth_unsubscribe", params: [id2] }).catch(() => {
|
|
163
|
+
logger.warn("Failed to cancel old susbcription");
|
|
164
|
+
});
|
|
165
|
+
subscriptions.delete(id2);
|
|
166
|
+
logger.warn(`Subscription ${id2} failed health check, reinitialising..`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const param of params2) {
|
|
170
|
+
request({ method: "eth_subscribe", params: [param] }).then((id2) => {
|
|
171
|
+
subscriptions.set(id2, { param, latest: Date.now() });
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
setInterval(healthcheck, 60 * 1e3);
|
|
176
|
+
return { protocol: "wss", request, subscribe };
|
|
177
|
+
}
|
|
178
|
+
function http(url, opts = {}) {
|
|
179
|
+
let id = 0;
|
|
180
|
+
const request = async (options) => {
|
|
181
|
+
let body = JSON.stringify({
|
|
182
|
+
id: id++,
|
|
183
|
+
jsonrpc: "2.0",
|
|
184
|
+
method: options.method,
|
|
185
|
+
params: options.params
|
|
186
|
+
});
|
|
187
|
+
const headers = new Headers();
|
|
188
|
+
headers.set("Content-Type", "application/json");
|
|
189
|
+
if (options.method.startsWith("private_")) {
|
|
190
|
+
if (opts.signingKey === void 0) {
|
|
191
|
+
throw new Error(ClientUnauthorizedError);
|
|
192
|
+
}
|
|
193
|
+
body = await compress(body).catch((cause) => raise(ClientCompressionError, { cause }));
|
|
194
|
+
headers.set("Content-Encoding", "gzip");
|
|
195
|
+
headers.set("Authorization", `Bearer ${opts.signingKey}`);
|
|
196
|
+
}
|
|
197
|
+
const res = await fetch(url, { headers, body, method: "POST", signal: AbortSignal.timeout(30 * 1e3) }).catch((cause) => {
|
|
198
|
+
throw new Error(ClientConnectionError, { cause });
|
|
199
|
+
});
|
|
200
|
+
if (!res.ok || res.status < 200 || res.status >= 300) {
|
|
201
|
+
throw new Error(ClientConnectionError);
|
|
202
|
+
}
|
|
203
|
+
const json = await res.json().catch((cause) => {
|
|
204
|
+
throw new Error(ClientResponseError, { cause });
|
|
205
|
+
});
|
|
206
|
+
if (json.error) {
|
|
207
|
+
throw new Error(json.error.message);
|
|
208
|
+
}
|
|
209
|
+
return json.result;
|
|
210
|
+
};
|
|
211
|
+
const subscribe = async () => {
|
|
212
|
+
throw new Error("Unable to `subscribe` on `http` transport");
|
|
213
|
+
};
|
|
214
|
+
return { protocol: "http", request, subscribe };
|
|
215
|
+
}
|
|
216
|
+
const ClientCompressionError = createException("An error occurred when compressing the request");
|
|
217
|
+
const ClientConnectionError = createException("An errored occurred when connecting to the server");
|
|
218
|
+
const ClientResponseError = createException("An error occurred when reading the servers response");
|
|
219
|
+
const ClientUnauthorizedError = createException("Attempted to execute a private method without providing a request signing key");
|
|
220
|
+
|
|
221
|
+
export { catchException as a, createException as c, getException as g, http as h, local as l, wss as w };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Storage } from 'unstorage';
|
|
2
|
+
|
|
3
|
+
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;
|
|
4
|
+
type Flatten<T> = {
|
|
5
|
+
[K in keyof T as CamelToSnake<Extract<K, string>>]: T[K];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Block -----------------------------------------------------------------------------------------------------------------------------------
|
|
10
|
+
*/
|
|
11
|
+
type Block = {
|
|
12
|
+
eth_chainId: `0x${string}`;
|
|
13
|
+
eth_getBlockByNumber: {
|
|
14
|
+
hash: `0x${string}`;
|
|
15
|
+
number: `0x${string}`;
|
|
16
|
+
parentHash: `0x${string}`;
|
|
17
|
+
};
|
|
18
|
+
eth_getBlockReceipts: Array<{
|
|
19
|
+
blockHash: `0x${string}`;
|
|
20
|
+
logs: Array<{
|
|
21
|
+
address: `0x${string}`;
|
|
22
|
+
topics: `0x${string}`[];
|
|
23
|
+
}>;
|
|
24
|
+
}>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Filters -----------------------------------------------------------------------------------------------------------------------------------
|
|
28
|
+
*/
|
|
29
|
+
type Filter = {
|
|
30
|
+
/** Index blocks with this chain id */
|
|
31
|
+
chain: number;
|
|
32
|
+
/** Index blocks from this start block (inclusive) */
|
|
33
|
+
fromBlock: number;
|
|
34
|
+
/** Index blocks until this stop block (inclusive) */
|
|
35
|
+
toBlock?: number;
|
|
36
|
+
/** Index blocks where this event topic was emitted */
|
|
37
|
+
event?: `0x${string}`;
|
|
38
|
+
/** Index blocks that involve this address */
|
|
39
|
+
address?: `0x${string}`;
|
|
40
|
+
};
|
|
41
|
+
declare function matchFilter(block: Block, filter: Filter): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Events -----------------------------------------------------------------------------------------------------------------------------------
|
|
44
|
+
*/
|
|
45
|
+
type Event<TBlock, TEvent> = {
|
|
46
|
+
/**
|
|
47
|
+
* A human-readable identifier for the event.
|
|
48
|
+
*/
|
|
49
|
+
id: string;
|
|
50
|
+
/**
|
|
51
|
+
* Filters let you define what specific blocks you want this event to index.
|
|
52
|
+
*
|
|
53
|
+
* Blockchains are massive datasets and most of the time we are only ever interested in small portions of it.
|
|
54
|
+
* Sometimes that can be specific events like ERC20 transfers and other times it could be all events emitted
|
|
55
|
+
* by a specific contract. Filters provide a simple way for an event to define exactly which blocks we want
|
|
56
|
+
* to index.
|
|
57
|
+
*
|
|
58
|
+
* Filters reduce costs and improve backfill performance by ensuring we only index the blocks we need and
|
|
59
|
+
* ignore the blocks that don't have the data we are interested in.
|
|
60
|
+
*
|
|
61
|
+
* By default your event will not index any blocks, you must opt-in to indexing by providing atleast one filter.
|
|
62
|
+
*
|
|
63
|
+
* Each property in the filter operates like an AND statement. For example, if you specifiy an `address` and
|
|
64
|
+
* an `event` it implies that you only want to index blocks where the specific `address` emitted the specific
|
|
65
|
+
* `event` topic provided.
|
|
66
|
+
*
|
|
67
|
+
* However, when multiple filters are defined for a given event those filters operate like an OR statement. For
|
|
68
|
+
* example, if we define a second filter looking for a different `address` and `event` our event will now index
|
|
69
|
+
* any block that matches either the first filter or the second filter.
|
|
70
|
+
*
|
|
71
|
+
* Note that filters are a rudimentary method to dramatically reduce the number of blocks your application needs
|
|
72
|
+
* to index. Any advanced filtering should be performed in the event `handler` itself.
|
|
73
|
+
*/
|
|
74
|
+
filters: Filter[];
|
|
75
|
+
/**
|
|
76
|
+
* Synchronously transforms a raw block into a list of events.
|
|
77
|
+
*
|
|
78
|
+
* When a block matches any of filters defined by your event it will be passed to this function to be synchronously
|
|
79
|
+
* transformed into a list of structured events.
|
|
80
|
+
*
|
|
81
|
+
* The shape of the input `block` is determined by the return value of `getBlock` function provided to your `indexer`.
|
|
82
|
+
*
|
|
83
|
+
* The returned output value should be an array containg any valid JavaScript values. Each event that you return from
|
|
84
|
+
* your handler should not depend on any information outside of the input block data. It should directly map a given
|
|
85
|
+
* input (the raw block) to a given output (structured events). This ensures that handler remains idempotent and that
|
|
86
|
+
* repeated calls with the same input block produce the same output events.
|
|
87
|
+
*/
|
|
88
|
+
handler: (block: TBlock) => TEvent[];
|
|
89
|
+
/**
|
|
90
|
+
* Storage adapter to persist events in your off-chain storage.
|
|
91
|
+
*/
|
|
92
|
+
storage: {
|
|
93
|
+
/**
|
|
94
|
+
* Upserts a batch of events into your storage system.
|
|
95
|
+
*
|
|
96
|
+
* After a block is transformed into a list of structured events by your `handler` function, that batch
|
|
97
|
+
* is passed to this `upsert` function so they can be upserted into your storage system.
|
|
98
|
+
*
|
|
99
|
+
* This function must be idempotent. Functionally, this means that if the same batch of events is upserted
|
|
100
|
+
* multiple times it only produces a single set of events in your storage system.
|
|
101
|
+
*/
|
|
102
|
+
upsert: (events: TEvent[]) => Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Deletes a batch of events from your storage system.
|
|
105
|
+
*
|
|
106
|
+
* When your indexer encounters a chain reorganisation, this `delete` function will be invoked with the
|
|
107
|
+
* same batch of events created by that reorganised block. This ensures that any events that were previously
|
|
108
|
+
* upserted into your storage system that are no longer part of the canonical chain are safely removed.
|
|
109
|
+
*/
|
|
110
|
+
delete?: (events: TEvent[]) => Promise<void>;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Indexer -----------------------------------------------------------------------------------------------------------------------------------
|
|
115
|
+
*/
|
|
116
|
+
type Head = {
|
|
117
|
+
hash: `0x${string}`;
|
|
118
|
+
chain: `0x${string}`;
|
|
119
|
+
number: `0x${string}`;
|
|
120
|
+
parentHash: `0x${string}`;
|
|
121
|
+
};
|
|
122
|
+
type Metadata = {
|
|
123
|
+
version: string;
|
|
124
|
+
language: string;
|
|
125
|
+
};
|
|
126
|
+
type Result = {
|
|
127
|
+
status: string;
|
|
128
|
+
event_id: string;
|
|
129
|
+
chain: `0x${string}`;
|
|
130
|
+
block_hash: `0x${string}`;
|
|
131
|
+
block_number: `0x${string}`;
|
|
132
|
+
created_at: number;
|
|
133
|
+
};
|
|
134
|
+
type IndexerOptions<TBlock> = {
|
|
135
|
+
/**
|
|
136
|
+
* Silences all logs including errors.
|
|
137
|
+
*
|
|
138
|
+
* Logs are emitted based on the environment LOG_LEVEL. Set `quiet: true` to surpress all logs.
|
|
139
|
+
* Available log options are `DEBUG`, `INFO`, `WARN`, and `ERROR`.
|
|
140
|
+
*/
|
|
141
|
+
quiet?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Request signing key
|
|
144
|
+
*
|
|
145
|
+
* Facilitates secure communication with a trusted source like the univo dashboard.
|
|
146
|
+
*/
|
|
147
|
+
signingKey: string;
|
|
148
|
+
/**
|
|
149
|
+
* Storage interface for durably persisting indexer metadata.
|
|
150
|
+
*
|
|
151
|
+
* This uses the unified key-value storage API from unstorage. Install the `unstorage` dependency
|
|
152
|
+
* and provide the return value of your `createStorage()` call. Check out the documentation for
|
|
153
|
+
* a full list of [supported storage drivers](https://unstorage.unjs.io/drivers).
|
|
154
|
+
*
|
|
155
|
+
* Functionally, storing metadata is fundamental to ensure the correct operation of your indexer. It
|
|
156
|
+
* ensures that you indexer recovers from downtime and ensures that all blocks are processed correctly during
|
|
157
|
+
* chain reorganisations.
|
|
158
|
+
*/
|
|
159
|
+
metadataStorage: Storage;
|
|
160
|
+
/**
|
|
161
|
+
* Loads raw block data in realtime from a trusted set of RPC sources.
|
|
162
|
+
*
|
|
163
|
+
* Note that historical backfills do not use this function to load block data.
|
|
164
|
+
*
|
|
165
|
+
* The response from this function determines the input data to your `handler` functions in each event.
|
|
166
|
+
* Generally, the shape is a list of RPC methods with their corresponding value from an RPC node. This
|
|
167
|
+
* generic format allows you support the full range of RPC data available to you from your node. This
|
|
168
|
+
* flexibility is important in cases where some methods are supported on specific chains only.
|
|
169
|
+
*
|
|
170
|
+
* Note that there is a minimum set of RPC methods expected on the response notably `eth_chainId`,
|
|
171
|
+
* `eth_getBlockByNumber` and `eth_getBlockReceipts`. These are expected so that we can safely match
|
|
172
|
+
* each block processed against your event filters.
|
|
173
|
+
*
|
|
174
|
+
* Note that we provide only the block number to look up block data. During a chain reorganisation, this
|
|
175
|
+
* makes it possible for different RPC calls to return data for different block hashes. We manually verify
|
|
176
|
+
* that all methods return the expected block hash. However, for any custom methods you add you must manually
|
|
177
|
+
* verify the block hash is consistent with the other known methods.
|
|
178
|
+
*/
|
|
179
|
+
getBlock: (block: {
|
|
180
|
+
chain: `0x${string}`;
|
|
181
|
+
number: string;
|
|
182
|
+
}) => Promise<TBlock | null>;
|
|
183
|
+
};
|
|
184
|
+
type Indexer<TBlock> = {
|
|
185
|
+
fetch: (req: Request) => Promise<Response>;
|
|
186
|
+
event: <TEvent>(event: Event<TBlock, TEvent>) => Event<TBlock, TEvent>;
|
|
187
|
+
};
|
|
188
|
+
declare function indexer<TBlock extends Block>(opts: IndexerOptions<TBlock>): {
|
|
189
|
+
event: <TEvent>(event: Event<TBlock, TEvent>) => Event<TBlock, TEvent>;
|
|
190
|
+
fetch: (req: Request) => Promise<Response>;
|
|
191
|
+
request: {
|
|
192
|
+
public_getFinalizedHeight: (chain: `0x${string}`) => Promise<number>;
|
|
193
|
+
public_deleteReorganisedHead: (head: Head) => Promise<void>;
|
|
194
|
+
public_writeUnfinalizedHeads: (heads: Head[]) => Promise<void>;
|
|
195
|
+
public_writeFinalizedHeads: (heads: Head[]) => Promise<void>;
|
|
196
|
+
private_getMetadata: () => Promise<Metadata>;
|
|
197
|
+
private_getEvents: () => Promise<{
|
|
198
|
+
id: string;
|
|
199
|
+
filters: Flatten<Filter>[];
|
|
200
|
+
}[]>;
|
|
201
|
+
private_writeEvents: (params: {
|
|
202
|
+
events: string[];
|
|
203
|
+
blocks: Block[];
|
|
204
|
+
}) => Promise<{
|
|
205
|
+
failures: Result[];
|
|
206
|
+
}>;
|
|
207
|
+
private_writeEventsAndGetKeys: (params: {
|
|
208
|
+
events: string[];
|
|
209
|
+
head: Head;
|
|
210
|
+
}) => Promise<{
|
|
211
|
+
results: Result[];
|
|
212
|
+
keys: string[];
|
|
213
|
+
}>;
|
|
214
|
+
};
|
|
215
|
+
subscribe: Record<string, never>;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export { indexer as i, matchFilter as m };
|
|
219
|
+
export type { Block as B, Event as E, Flatten as F, Head as H, Indexer as I, Metadata as M, Result as R, Filter as a };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Storage } from 'unstorage';
|
|
2
|
+
|
|
3
|
+
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;
|
|
4
|
+
type Flatten<T> = {
|
|
5
|
+
[K in keyof T as CamelToSnake<Extract<K, string>>]: T[K];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Block -----------------------------------------------------------------------------------------------------------------------------------
|
|
10
|
+
*/
|
|
11
|
+
type Block = {
|
|
12
|
+
eth_chainId: `0x${string}`;
|
|
13
|
+
eth_getBlockByNumber: {
|
|
14
|
+
hash: `0x${string}`;
|
|
15
|
+
number: `0x${string}`;
|
|
16
|
+
parentHash: `0x${string}`;
|
|
17
|
+
};
|
|
18
|
+
eth_getBlockReceipts: Array<{
|
|
19
|
+
blockHash: `0x${string}`;
|
|
20
|
+
logs: Array<{
|
|
21
|
+
address: `0x${string}`;
|
|
22
|
+
topics: `0x${string}`[];
|
|
23
|
+
}>;
|
|
24
|
+
}>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Filters -----------------------------------------------------------------------------------------------------------------------------------
|
|
28
|
+
*/
|
|
29
|
+
type Filter = {
|
|
30
|
+
/** Index blocks with this chain id */
|
|
31
|
+
chain: number;
|
|
32
|
+
/** Index blocks from this start block (inclusive) */
|
|
33
|
+
fromBlock: number;
|
|
34
|
+
/** Index blocks until this stop block (inclusive) */
|
|
35
|
+
toBlock?: number;
|
|
36
|
+
/** Index blocks where this event topic was emitted */
|
|
37
|
+
event?: `0x${string}`;
|
|
38
|
+
/** Index blocks that involve this address */
|
|
39
|
+
address?: `0x${string}`;
|
|
40
|
+
};
|
|
41
|
+
declare function matchFilter(block: Block, filter: Filter): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Events -----------------------------------------------------------------------------------------------------------------------------------
|
|
44
|
+
*/
|
|
45
|
+
type Event<TBlock, TEvent> = {
|
|
46
|
+
/**
|
|
47
|
+
* A human-readable identifier for the event.
|
|
48
|
+
*/
|
|
49
|
+
id: string;
|
|
50
|
+
/**
|
|
51
|
+
* Filters let you define what specific blocks you want this event to index.
|
|
52
|
+
*
|
|
53
|
+
* Blockchains are massive datasets and most of the time we are only ever interested in small portions of it.
|
|
54
|
+
* Sometimes that can be specific events like ERC20 transfers and other times it could be all events emitted
|
|
55
|
+
* by a specific contract. Filters provide a simple way for an event to define exactly which blocks we want
|
|
56
|
+
* to index.
|
|
57
|
+
*
|
|
58
|
+
* Filters reduce costs and improve backfill performance by ensuring we only index the blocks we need and
|
|
59
|
+
* ignore the blocks that don't have the data we are interested in.
|
|
60
|
+
*
|
|
61
|
+
* By default your event will not index any blocks, you must opt-in to indexing by providing atleast one filter.
|
|
62
|
+
*
|
|
63
|
+
* Each property in the filter operates like an AND statement. For example, if you specifiy an `address` and
|
|
64
|
+
* an `event` it implies that you only want to index blocks where the specific `address` emitted the specific
|
|
65
|
+
* `event` topic provided.
|
|
66
|
+
*
|
|
67
|
+
* However, when multiple filters are defined for a given event those filters operate like an OR statement. For
|
|
68
|
+
* example, if we define a second filter looking for a different `address` and `event` our event will now index
|
|
69
|
+
* any block that matches either the first filter or the second filter.
|
|
70
|
+
*
|
|
71
|
+
* Note that filters are a rudimentary method to dramatically reduce the number of blocks your application needs
|
|
72
|
+
* to index. Any advanced filtering should be performed in the event `handler` itself.
|
|
73
|
+
*/
|
|
74
|
+
filters: Filter[];
|
|
75
|
+
/**
|
|
76
|
+
* Synchronously transforms a raw block into a list of events.
|
|
77
|
+
*
|
|
78
|
+
* When a block matches any of filters defined by your event it will be passed to this function to be synchronously
|
|
79
|
+
* transformed into a list of structured events.
|
|
80
|
+
*
|
|
81
|
+
* The shape of the input `block` is determined by the return value of `getBlock` function provided to your `indexer`.
|
|
82
|
+
*
|
|
83
|
+
* The returned output value should be an array containg any valid JavaScript values. Each event that you return from
|
|
84
|
+
* your handler should not depend on any information outside of the input block data. It should directly map a given
|
|
85
|
+
* input (the raw block) to a given output (structured events). This ensures that handler remains idempotent and that
|
|
86
|
+
* repeated calls with the same input block produce the same output events.
|
|
87
|
+
*/
|
|
88
|
+
handler: (block: TBlock) => TEvent[];
|
|
89
|
+
/**
|
|
90
|
+
* Storage adapter to persist events in your off-chain storage.
|
|
91
|
+
*/
|
|
92
|
+
storage: {
|
|
93
|
+
/**
|
|
94
|
+
* Upserts a batch of events into your storage system.
|
|
95
|
+
*
|
|
96
|
+
* After a block is transformed into a list of structured events by your `handler` function, that batch
|
|
97
|
+
* is passed to this `upsert` function so they can be upserted into your storage system.
|
|
98
|
+
*
|
|
99
|
+
* This function must be idempotent. Functionally, this means that if the same batch of events is upserted
|
|
100
|
+
* multiple times it only produces a single set of events in your storage system.
|
|
101
|
+
*/
|
|
102
|
+
upsert: (events: TEvent[]) => Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Deletes a batch of events from your storage system.
|
|
105
|
+
*
|
|
106
|
+
* When your indexer encounters a chain reorganisation, this `delete` function will be invoked with the
|
|
107
|
+
* same batch of events created by that reorganised block. This ensures that any events that were previously
|
|
108
|
+
* upserted into your storage system that are no longer part of the canonical chain are safely removed.
|
|
109
|
+
*/
|
|
110
|
+
delete?: (events: TEvent[]) => Promise<void>;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Indexer -----------------------------------------------------------------------------------------------------------------------------------
|
|
115
|
+
*/
|
|
116
|
+
type Head = {
|
|
117
|
+
hash: `0x${string}`;
|
|
118
|
+
chain: `0x${string}`;
|
|
119
|
+
number: `0x${string}`;
|
|
120
|
+
parentHash: `0x${string}`;
|
|
121
|
+
};
|
|
122
|
+
type Metadata = {
|
|
123
|
+
version: string;
|
|
124
|
+
language: string;
|
|
125
|
+
};
|
|
126
|
+
type Result = {
|
|
127
|
+
status: string;
|
|
128
|
+
event_id: string;
|
|
129
|
+
chain: `0x${string}`;
|
|
130
|
+
block_hash: `0x${string}`;
|
|
131
|
+
block_number: `0x${string}`;
|
|
132
|
+
created_at: number;
|
|
133
|
+
};
|
|
134
|
+
type IndexerOptions<TBlock> = {
|
|
135
|
+
/**
|
|
136
|
+
* Silences all logs including errors.
|
|
137
|
+
*
|
|
138
|
+
* Logs are emitted based on the environment LOG_LEVEL. Set `quiet: true` to surpress all logs.
|
|
139
|
+
* Available log options are `DEBUG`, `INFO`, `WARN`, and `ERROR`.
|
|
140
|
+
*/
|
|
141
|
+
quiet?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Request signing key
|
|
144
|
+
*
|
|
145
|
+
* Facilitates secure communication with a trusted source like the univo dashboard.
|
|
146
|
+
*/
|
|
147
|
+
signingKey: string;
|
|
148
|
+
/**
|
|
149
|
+
* Storage interface for durably persisting indexer metadata.
|
|
150
|
+
*
|
|
151
|
+
* This uses the unified key-value storage API from unstorage. Install the `unstorage` dependency
|
|
152
|
+
* and provide the return value of your `createStorage()` call. Check out the documentation for
|
|
153
|
+
* a full list of [supported storage drivers](https://unstorage.unjs.io/drivers).
|
|
154
|
+
*
|
|
155
|
+
* Functionally, storing metadata is fundamental to ensure the correct operation of your indexer. It
|
|
156
|
+
* ensures that you indexer recovers from downtime and ensures that all blocks are processed correctly during
|
|
157
|
+
* chain reorganisations.
|
|
158
|
+
*/
|
|
159
|
+
metadataStorage: Storage;
|
|
160
|
+
/**
|
|
161
|
+
* Loads raw block data in realtime from a trusted set of RPC sources.
|
|
162
|
+
*
|
|
163
|
+
* Note that historical backfills do not use this function to load block data.
|
|
164
|
+
*
|
|
165
|
+
* The response from this function determines the input data to your `handler` functions in each event.
|
|
166
|
+
* Generally, the shape is a list of RPC methods with their corresponding value from an RPC node. This
|
|
167
|
+
* generic format allows you support the full range of RPC data available to you from your node. This
|
|
168
|
+
* flexibility is important in cases where some methods are supported on specific chains only.
|
|
169
|
+
*
|
|
170
|
+
* Note that there is a minimum set of RPC methods expected on the response notably `eth_chainId`,
|
|
171
|
+
* `eth_getBlockByNumber` and `eth_getBlockReceipts`. These are expected so that we can safely match
|
|
172
|
+
* each block processed against your event filters.
|
|
173
|
+
*
|
|
174
|
+
* Note that we provide only the block number to look up block data. During a chain reorganisation, this
|
|
175
|
+
* makes it possible for different RPC calls to return data for different block hashes. We manually verify
|
|
176
|
+
* that all methods return the expected block hash. However, for any custom methods you add you must manually
|
|
177
|
+
* verify the block hash is consistent with the other known methods.
|
|
178
|
+
*/
|
|
179
|
+
getBlock: (block: {
|
|
180
|
+
chain: `0x${string}`;
|
|
181
|
+
number: string;
|
|
182
|
+
}) => Promise<TBlock | null>;
|
|
183
|
+
};
|
|
184
|
+
type Indexer<TBlock> = {
|
|
185
|
+
fetch: (req: Request) => Promise<Response>;
|
|
186
|
+
event: <TEvent>(event: Event<TBlock, TEvent>) => Event<TBlock, TEvent>;
|
|
187
|
+
};
|
|
188
|
+
declare function indexer<TBlock extends Block>(opts: IndexerOptions<TBlock>): {
|
|
189
|
+
event: <TEvent>(event: Event<TBlock, TEvent>) => Event<TBlock, TEvent>;
|
|
190
|
+
fetch: (req: Request) => Promise<Response>;
|
|
191
|
+
request: {
|
|
192
|
+
public_getFinalizedHeight: (chain: `0x${string}`) => Promise<number>;
|
|
193
|
+
public_deleteReorganisedHead: (head: Head) => Promise<void>;
|
|
194
|
+
public_writeUnfinalizedHeads: (heads: Head[]) => Promise<void>;
|
|
195
|
+
public_writeFinalizedHeads: (heads: Head[]) => Promise<void>;
|
|
196
|
+
private_getMetadata: () => Promise<Metadata>;
|
|
197
|
+
private_getEvents: () => Promise<{
|
|
198
|
+
id: string;
|
|
199
|
+
filters: Flatten<Filter>[];
|
|
200
|
+
}[]>;
|
|
201
|
+
private_writeEvents: (params: {
|
|
202
|
+
events: string[];
|
|
203
|
+
blocks: Block[];
|
|
204
|
+
}) => Promise<{
|
|
205
|
+
failures: Result[];
|
|
206
|
+
}>;
|
|
207
|
+
private_writeEventsAndGetKeys: (params: {
|
|
208
|
+
events: string[];
|
|
209
|
+
head: Head;
|
|
210
|
+
}) => Promise<{
|
|
211
|
+
results: Result[];
|
|
212
|
+
keys: string[];
|
|
213
|
+
}>;
|
|
214
|
+
};
|
|
215
|
+
subscribe: Record<string, never>;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export { indexer as i, matchFilter as m };
|
|
219
|
+
export type { Block as B, Event as E, Flatten as F, Head as H, Indexer as I, Metadata as M, Result as R, Filter as a };
|
package/dist/transport.d.mts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Rpc } from './rpc.mjs';
|
|
2
|
-
import './
|
|
2
|
+
import './shared/univo.Cj4s4jNq.mjs';
|
|
3
3
|
import 'unstorage';
|
|
4
4
|
|
|
5
|
-
type Protocol = "http" | "wss";
|
|
5
|
+
type Protocol = "http" | "wss" | "local";
|
|
6
6
|
type Transport<R extends Rpc, P extends Protocol = Protocol> = {
|
|
7
7
|
/**
|
|
8
8
|
* The underlying transport protocol.
|
|
9
9
|
*/
|
|
10
10
|
protocol: P;
|
|
11
11
|
/**
|
|
12
|
-
* Performs
|
|
12
|
+
* Performs an RPC request
|
|
13
13
|
* @returns The RPC response
|
|
14
14
|
*/
|
|
15
15
|
request: <M extends keyof R["request"]>(opts: {
|
|
@@ -22,6 +22,7 @@ type Transport<R extends Rpc, P extends Protocol = Protocol> = {
|
|
|
22
22
|
*/
|
|
23
23
|
subscribe: <M extends keyof R["subscribe"]>(param: M, handler: (message: R["subscribe"][M]) => void) => Promise<() => Promise<void>>;
|
|
24
24
|
};
|
|
25
|
+
declare function local<R extends Rpc>(rpc: R): Transport<R, "local">;
|
|
25
26
|
declare function wss<R extends Rpc>(url: string, opts?: {
|
|
26
27
|
quiet?: boolean;
|
|
27
28
|
}): Transport<R, "wss">;
|
|
@@ -32,5 +33,5 @@ declare function http<R extends Rpc>(url: string, opts?: {
|
|
|
32
33
|
signingKey?: string;
|
|
33
34
|
}): Transport<R, "http">;
|
|
34
35
|
|
|
35
|
-
export { http, wss };
|
|
36
|
+
export { http, local, wss };
|
|
36
37
|
export type { Transport };
|