univo 0.1.15
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/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +71 -0
- package/dist/client.d.mts +24 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.mjs +43 -0
- package/dist/index.d.mts +113 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.mjs +516 -0
- package/dist/realtime.d.mts +50 -0
- package/dist/realtime.d.ts +50 -0
- package/dist/realtime.mjs +210 -0
- package/dist/shared/univo.rTUTP64t.mjs +154 -0
- package/package.json +55 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import WS from 'ws';
|
|
2
|
+
import { WebSocket } from 'partysocket';
|
|
3
|
+
import { http } from './client.mjs';
|
|
4
|
+
import { c as createLogger, r as retry, h as hexToNumber, j as raise, m as mutex, k as assert } from './shared/univo.rTUTP64t.mjs';
|
|
5
|
+
|
|
6
|
+
function createSocket(url, opts) {
|
|
7
|
+
const ws = new WebSocket(url, [], { WebSocket: WS });
|
|
8
|
+
ws.binaryType = "arraybuffer";
|
|
9
|
+
if (opts.onOpen) ws.onopen = opts.onOpen;
|
|
10
|
+
if (opts.onError) ws.onerror = opts.onError;
|
|
11
|
+
if (opts.onClose) ws.onclose = opts.onClose;
|
|
12
|
+
if (opts.onMessage) ws.onmessage = opts.onMessage;
|
|
13
|
+
return ws;
|
|
14
|
+
}
|
|
15
|
+
function defineTransport(url) {
|
|
16
|
+
let id = 0;
|
|
17
|
+
const requests = /* @__PURE__ */ new Map();
|
|
18
|
+
async function request(opts) {
|
|
19
|
+
return await new Promise((resolve) => {
|
|
20
|
+
const body = Object.assign({ id: id++ }, opts);
|
|
21
|
+
requests.set(body.id, {
|
|
22
|
+
handler: (data) => {
|
|
23
|
+
requests.delete(body.id);
|
|
24
|
+
resolve(data);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
socket.send(JSON.stringify(body));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
31
|
+
async function subscribe(param, handler) {
|
|
32
|
+
const id2 = await request({ method: "eth_subscribe", params: [param] });
|
|
33
|
+
subscriptions.set(id2, { param, handler, latest: Date.now() });
|
|
34
|
+
return {
|
|
35
|
+
async unsubscribe() {
|
|
36
|
+
subscriptions.delete(id2);
|
|
37
|
+
await request({ method: "eth_unsubscribe", params: [id2] }).catch(() => {
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const socket = createSocket(url, {
|
|
43
|
+
onError(cause) {
|
|
44
|
+
console.error(new Error("Socket error", { cause }));
|
|
45
|
+
},
|
|
46
|
+
onOpen() {
|
|
47
|
+
if (subscriptions.size) {
|
|
48
|
+
const handlers = [];
|
|
49
|
+
for (const [id2, subscription] of subscriptions) {
|
|
50
|
+
handlers.push(subscription);
|
|
51
|
+
request({ method: "eth_unsubscribe", params: [id2] }).catch(() => {
|
|
52
|
+
});
|
|
53
|
+
subscriptions.delete(id2);
|
|
54
|
+
}
|
|
55
|
+
for (const handler of handlers) {
|
|
56
|
+
request({ method: "eth_subscribe", params: [handler.param] }).then((id2) => {
|
|
57
|
+
subscriptions.set(id2, handler);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
onMessage(event) {
|
|
63
|
+
const data = JSON.parse(event.data);
|
|
64
|
+
if (data.method === "eth_subscription") {
|
|
65
|
+
const subscription = subscriptions.get(data.params.subscription);
|
|
66
|
+
if (!subscription) {
|
|
67
|
+
return request({ method: "eth_unsubscribe", params: [data.params.subscription] }).catch(() => {
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
subscription.latest = Date.now();
|
|
71
|
+
return subscription.handler(data.params.result);
|
|
72
|
+
}
|
|
73
|
+
const _request = requests.get(data.id);
|
|
74
|
+
if (_request) _request.handler(data.result);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return { request, subscribe };
|
|
78
|
+
}
|
|
79
|
+
function defineBlockchain(opts) {
|
|
80
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
81
|
+
const chain = [];
|
|
82
|
+
function setHeadBlock(newBlock) {
|
|
83
|
+
chain.push(newBlock);
|
|
84
|
+
opts.onBlockAdded(newBlock);
|
|
85
|
+
if (chain.length > 100) chain.shift();
|
|
86
|
+
}
|
|
87
|
+
function removeHeadBlock() {
|
|
88
|
+
chain.pop();
|
|
89
|
+
}
|
|
90
|
+
function getHeadBlock() {
|
|
91
|
+
const block = chain[chain.length - 1];
|
|
92
|
+
assert(block !== void 0, "Expected non-empty chain when retrieving latest block");
|
|
93
|
+
return block;
|
|
94
|
+
}
|
|
95
|
+
function getOldestBlock() {
|
|
96
|
+
const block = chain[0];
|
|
97
|
+
assert(block !== void 0, "Expected non-empty chain when retrieving oldest block");
|
|
98
|
+
return block;
|
|
99
|
+
}
|
|
100
|
+
async function reconcile(newBlock) {
|
|
101
|
+
if (chain.length === 0) {
|
|
102
|
+
return setHeadBlock(newBlock);
|
|
103
|
+
}
|
|
104
|
+
if (hexToNumber(newBlock.number) < hexToNumber(getOldestBlock().number)) {
|
|
105
|
+
log.info(`Received block ${hexToNumber(newBlock.number)} older than our local chain`);
|
|
106
|
+
throw new Error("Not implemented");
|
|
107
|
+
}
|
|
108
|
+
if (chain.some((block) => block.hash === newBlock.hash)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (getHeadBlock().hash === newBlock.parentHash) {
|
|
112
|
+
return setHeadBlock(newBlock);
|
|
113
|
+
}
|
|
114
|
+
if (chain.some((block) => block.hash === newBlock.parentHash)) {
|
|
115
|
+
while (getHeadBlock().hash !== newBlock.parentHash) removeHeadBlock();
|
|
116
|
+
return setHeadBlock(newBlock);
|
|
117
|
+
}
|
|
118
|
+
if (newBlock.parentHash === "0x0000000000000000000000000000000000000000000000000000000000000000") {
|
|
119
|
+
while (chain.length > 0) removeHeadBlock();
|
|
120
|
+
return setHeadBlock(newBlock);
|
|
121
|
+
}
|
|
122
|
+
const parentBlock = await retry(opts.getBlockByHash, [newBlock.parentHash], 5);
|
|
123
|
+
if (!parentBlock) throw new Error(`Failed to fetch parent block ${hexToNumber(newBlock.number)} ${newBlock.parentHash.slice(0, 16)}`);
|
|
124
|
+
assert(parentBlock.hash === newBlock.parentHash, "Expected block hashes to match");
|
|
125
|
+
await reconcile(parentBlock);
|
|
126
|
+
return await reconcile(newBlock);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
reconcile: mutex(reconcile)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function createRealtimeStream(opts) {
|
|
133
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
134
|
+
const handlers = /* @__PURE__ */ new Set();
|
|
135
|
+
function onBlockAdded(head) {
|
|
136
|
+
for (const handler of handlers) {
|
|
137
|
+
try {
|
|
138
|
+
const result = handler(head);
|
|
139
|
+
if (result instanceof Promise) {
|
|
140
|
+
result.catch(() => {
|
|
141
|
+
log.error("Failed to process handler");
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error instanceof Error) {
|
|
146
|
+
log.error(error.message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function createRealtimeStreamForChain(chain) {
|
|
152
|
+
async function getBlockByHash(hash) {
|
|
153
|
+
const block = await opts.transport.request({
|
|
154
|
+
method: "eth_getBlockByHash",
|
|
155
|
+
params: [hash, false]
|
|
156
|
+
});
|
|
157
|
+
return { chain, number: block.number, hash: block.hash, parentHash: block.parentHash };
|
|
158
|
+
}
|
|
159
|
+
const latest = defineBlockchain({ onBlockAdded, getBlockByHash, quiet: opts.quiet });
|
|
160
|
+
async function onNewHead(head) {
|
|
161
|
+
try {
|
|
162
|
+
await latest.reconcile(head);
|
|
163
|
+
} catch (cause) {
|
|
164
|
+
log.error("Failed to reconcile latest head");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
opts.transport.subscribe("newHeads", (head) => onNewHead({ chain, number: head.number, hash: head.hash, parentHash: head.parentHash })).then(() => log.debug(`Initialised subscription for chain ${hexToNumber(chain)}`)).catch((cause) => raise(`Failed to initialise subscription for chain ${hexToNumber(chain)}`, { cause }));
|
|
168
|
+
}
|
|
169
|
+
retry(opts.transport.request, [{ method: "eth_chainId", params: [] }], 5).then((chain) => createRealtimeStreamForChain(chain)).catch(() => log.error("Failed to load current chain"));
|
|
170
|
+
return {
|
|
171
|
+
on(_, handler) {
|
|
172
|
+
handlers.add(handler);
|
|
173
|
+
return { unsubscribe: () => handlers.delete(handler) };
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function realtime(opts) {
|
|
178
|
+
const defaultOptions = { quiet: false };
|
|
179
|
+
const options = Object.assign(defaultOptions, opts);
|
|
180
|
+
const log = createLogger(options);
|
|
181
|
+
if (options.endpoints.length === 0) throw new Error("Must provide at least one url to `endpoints`");
|
|
182
|
+
function createClient(endpoint) {
|
|
183
|
+
const client = http(endpoint);
|
|
184
|
+
let id = 0;
|
|
185
|
+
let pending = [];
|
|
186
|
+
return {
|
|
187
|
+
async publish(head) {
|
|
188
|
+
try {
|
|
189
|
+
const heads = pending.concat(head);
|
|
190
|
+
const response = await client.request({ jsonrpc: "2.0", id: id++, method: "public_writeBlocks", params: [endpoint, heads] });
|
|
191
|
+
if (response.error) throw new Error(response.error.message);
|
|
192
|
+
pending = [];
|
|
193
|
+
} catch (cause) {
|
|
194
|
+
log.warn(`Failed to publish realtime block ${head.chain}:${head.number}`);
|
|
195
|
+
pending.push(head);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const clients = options.endpoints.map((endpoint) => createClient(endpoint));
|
|
201
|
+
createRealtimeStream(options).on("block", async (head) => {
|
|
202
|
+
await Promise.all(
|
|
203
|
+
clients.map(async (client) => {
|
|
204
|
+
await client.publish(head);
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { createRealtimeStream, defineTransport, realtime };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const name = "univo";
|
|
2
|
+
const version = "0.1.15";
|
|
3
|
+
|
|
4
|
+
function createException(message) {
|
|
5
|
+
return `univo:${message}`;
|
|
6
|
+
}
|
|
7
|
+
function catchException(unknown, exception) {
|
|
8
|
+
if (unknown instanceof Error) {
|
|
9
|
+
if (unknown.message === exception) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function getException(unknown) {
|
|
16
|
+
if (unknown instanceof Error) {
|
|
17
|
+
if (unknown.message.startsWith("univo")) {
|
|
18
|
+
const [_, message] = unknown.message.split(":");
|
|
19
|
+
if (message) return message;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createLogger(opts) {
|
|
25
|
+
return {
|
|
26
|
+
debug(...any) {
|
|
27
|
+
if (!opts.quiet && process.env.LOG_LEVEL === "DEBUG") {
|
|
28
|
+
console.log(`[${name}@${version}] DEBUG`, ...any);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
info(...any) {
|
|
32
|
+
if (!opts.quiet && (process.env.LOG_LEVEL === "DEBUG" || process.env.LOG_LEVEL === "INFO")) {
|
|
33
|
+
console.log(`[${name}@${version}] INFO`, ...any);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
warn(...any) {
|
|
37
|
+
if (!opts.quiet && (process.env.LOG_LEVEL === "DEBUG" || process.env.LOG_LEVEL === "INFO" || process.env.LOG_LEVEL === "WARN")) {
|
|
38
|
+
console.log(`[${name}@${version}] WARN`, ...any);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
error(...any) {
|
|
42
|
+
if (process.env.LOG_LEVEL === "DEBUG" || process.env.LOG_LEVEL === "INFO" || process.env.LOG_LEVEL === "WARN" || process.env.LOG_LEVEL === "ERROR") {
|
|
43
|
+
console.log(`[${name}@${version}] ERROR`, ...any);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const encoder = new TextEncoder();
|
|
49
|
+
function assert(value, message) {
|
|
50
|
+
if (value === false || value === null || value === void 0) {
|
|
51
|
+
throw new Error(message ?? `Assertion failed: value is ${String(value)}`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
async function getSignature(opts) {
|
|
56
|
+
const keyData = encoder.encode(opts.key);
|
|
57
|
+
const key = await crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
58
|
+
if (typeof opts.body === "string") {
|
|
59
|
+
const dataToAuthenticate = encoder.encode(opts.body);
|
|
60
|
+
const hmac2 = await crypto.subtle.sign("HMAC", key, dataToAuthenticate);
|
|
61
|
+
return [...new Uint8Array(hmac2)].map((binary) => binary.toString(16).padStart(2, "0")).join("");
|
|
62
|
+
}
|
|
63
|
+
const hmac = await crypto.subtle.sign("HMAC", key, opts.body);
|
|
64
|
+
return [...new Uint8Array(hmac)].map((binary) => binary.toString(16).padStart(2, "0")).join("");
|
|
65
|
+
}
|
|
66
|
+
async function verifySignature(opts) {
|
|
67
|
+
const keyData = encoder.encode(opts.key);
|
|
68
|
+
const key = await crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
|
|
69
|
+
const pairs = opts.signature.match(/[\da-f]{2}/gi);
|
|
70
|
+
if (!pairs) return false;
|
|
71
|
+
if (typeof opts.body === "string") {
|
|
72
|
+
const dataToAuthenticate = encoder.encode(opts.body);
|
|
73
|
+
const hmac2 = new Uint8Array(pairs.map((byte) => Number.parseInt(byte, 16))).buffer;
|
|
74
|
+
return await crypto.subtle.verify("HMAC", key, hmac2, dataToAuthenticate);
|
|
75
|
+
}
|
|
76
|
+
const hmac = new Uint8Array(pairs.map((byte) => Number.parseInt(byte, 16))).buffer;
|
|
77
|
+
return await crypto.subtle.verify("HMAC", key, hmac, opts.body);
|
|
78
|
+
}
|
|
79
|
+
function hexToNumber(hex) {
|
|
80
|
+
return Number.parseInt(hex, 16);
|
|
81
|
+
}
|
|
82
|
+
function mutex(fn) {
|
|
83
|
+
let locked = false;
|
|
84
|
+
return async (...args) => {
|
|
85
|
+
if (locked) return;
|
|
86
|
+
locked = true;
|
|
87
|
+
await fn(...args);
|
|
88
|
+
locked = false;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function isAddressEqual(a, b) {
|
|
92
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
async function retry(fn, args, retries, __count = 1) {
|
|
95
|
+
try {
|
|
96
|
+
return await fn(...args);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (__count > retries) throw error;
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3 * 2 ** __count));
|
|
100
|
+
return retry(fn, args, retries, __count + 1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function raise(...args) {
|
|
104
|
+
if (args[0] instanceof Error) throw args[0];
|
|
105
|
+
throw new Error(...args);
|
|
106
|
+
}
|
|
107
|
+
function nonNullable(value) {
|
|
108
|
+
return value !== null && value !== void 0;
|
|
109
|
+
}
|
|
110
|
+
async function compress(input) {
|
|
111
|
+
const encoded = new TextEncoder().encode(input);
|
|
112
|
+
const stream = new CompressionStream("gzip");
|
|
113
|
+
const writer = stream.writable.getWriter();
|
|
114
|
+
writer.write(encoded);
|
|
115
|
+
writer.close();
|
|
116
|
+
const chunks = [];
|
|
117
|
+
const reader = stream.readable.getReader();
|
|
118
|
+
while (true) {
|
|
119
|
+
const { done, value } = await reader.read();
|
|
120
|
+
if (done) break;
|
|
121
|
+
chunks.push(value);
|
|
122
|
+
}
|
|
123
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
124
|
+
const result = new Uint8Array(totalLength);
|
|
125
|
+
let offset = 0;
|
|
126
|
+
for (const chunk of chunks) {
|
|
127
|
+
result.set(chunk, offset);
|
|
128
|
+
offset += chunk.length;
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
async function decompress(input) {
|
|
133
|
+
const stream = new DecompressionStream("gzip");
|
|
134
|
+
const writer = stream.writable.getWriter();
|
|
135
|
+
writer.write(input);
|
|
136
|
+
writer.close();
|
|
137
|
+
const chunks = [];
|
|
138
|
+
const reader = stream.readable.getReader();
|
|
139
|
+
while (true) {
|
|
140
|
+
const { done, value } = await reader.read();
|
|
141
|
+
if (done) break;
|
|
142
|
+
chunks.push(value);
|
|
143
|
+
}
|
|
144
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
145
|
+
const result = new Uint8Array(totalLength);
|
|
146
|
+
let offset = 0;
|
|
147
|
+
for (const chunk of chunks) {
|
|
148
|
+
result.set(chunk, offset);
|
|
149
|
+
offset += chunk.length;
|
|
150
|
+
}
|
|
151
|
+
return new TextDecoder().decode(result);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export { createException as a, catchException as b, createLogger as c, decompress as d, version as e, getSignature as f, getException as g, hexToNumber as h, isAddressEqual as i, raise as j, assert as k, name as l, mutex as m, nonNullable as n, compress as o, retry as r, verifySignature as v };
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "univo",
|
|
3
|
+
"license": "MIT",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "0.1.15",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"univo": "dist/cli.mjs"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./client": {
|
|
18
|
+
"types": "./dist/client.d.ts",
|
|
19
|
+
"import": "./dist/client.mjs"
|
|
20
|
+
},
|
|
21
|
+
"./realtime": {
|
|
22
|
+
"types": "./dist/realtime.d.ts",
|
|
23
|
+
"import": "./dist/realtime.mjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "vitest",
|
|
31
|
+
"build": "unbuild",
|
|
32
|
+
"univo": "NODE_ENV=production LOG_LEVEL=DEBUG jiti ./src/cli",
|
|
33
|
+
"build:release": "pnpm build && changelogen --release --push && pnpm build && npm publish"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"citty": "^0.1.6",
|
|
37
|
+
"dotenv": "^17.2.2",
|
|
38
|
+
"open": "^10.2.0",
|
|
39
|
+
"partysocket": "^1.1.3",
|
|
40
|
+
"ws": "^8.18.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@hono/node-server": "^1.14.1",
|
|
44
|
+
"@types/node": "^20.11.27",
|
|
45
|
+
"@types/ws": "^8.18.1",
|
|
46
|
+
"changelogen": "^0.6.1",
|
|
47
|
+
"hono": "^4.7.8",
|
|
48
|
+
"jiti": "^2.4.2",
|
|
49
|
+
"msw": "^2.10.2",
|
|
50
|
+
"typescript": "^5.8.2",
|
|
51
|
+
"unbuild": "^3.5.0",
|
|
52
|
+
"viem": "2.45.1",
|
|
53
|
+
"vitest": "^3.1.3"
|
|
54
|
+
}
|
|
55
|
+
}
|