@lunarislab/state-sync 1.0.2 → 1.1.2
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/classes/Client.d.ts +7 -19
- package/classes/Client.js +9 -99
- package/classes/Saver.d.ts +22 -0
- package/classes/Saver.js +66 -0
- package/classes/SimpleEmitter.d.ts +13 -0
- package/classes/SimpleEmitter.js +31 -0
- package/classes/Syncer.d.ts +21 -0
- package/classes/Syncer.js +60 -0
- package/classes/Watcher.d.ts +19 -22
- package/classes/Watcher.js +52 -66
- package/index.d.ts +2 -2
- package/index.js +2 -2
- package/package.json +3 -3
- package/types/Client.d.ts +5 -6
- package/types/Level.d.ts +81 -0
- package/types/Level.js +4 -0
- package/types/Log.d.ts +30 -0
- package/types/Log.js +2 -0
- package/types/Saver.d.ts +16 -0
- package/types/Saver.js +3 -0
- package/types/Syncer.d.ts +20 -0
- package/types/Syncer.js +2 -0
- package/types/Watcher.d.ts +6 -126
- package/types/Watcher.js +0 -2
package/classes/Client.d.ts
CHANGED
@@ -1,30 +1,18 @@
|
|
1
|
-
import {
|
1
|
+
import { PublicClient, WalletClient } from "viem";
|
2
2
|
import { IClientConfig, IStateSyncEvents } from "../types/Client";
|
3
|
-
import { Watcher } from "./Watcher";
|
4
|
-
import { ISimplifiedWatcher, IWatcherConfig, LevelDBConstructor } from "../types/Watcher";
|
5
3
|
import { EventSignature } from "abitype/dist/types/human-readable/types/signatures";
|
6
|
-
import {
|
4
|
+
import { Syncer } from "./Syncer";
|
5
|
+
import { ISyncerConfig } from "../types/Syncer";
|
6
|
+
import { SimpleEmitter } from "./SimpleEmitter";
|
7
|
+
import { LevelDBConstructor } from "../types/Level";
|
7
8
|
/**
|
8
9
|
* The StateSync client class.
|
9
10
|
*/
|
10
|
-
export declare class StateSync
|
11
|
+
export declare class StateSync extends SimpleEmitter<IStateSyncEvents> {
|
11
12
|
public: PublicClient;
|
12
13
|
wallet: WalletClient;
|
13
14
|
level: LevelDBConstructor;
|
14
15
|
levelPath: string;
|
15
|
-
timeout: number;
|
16
|
-
protected watchers: Map<string, Watcher<any>>;
|
17
|
-
protected emitter: EventEmitter;
|
18
|
-
protected startBlockNum: bigint;
|
19
|
-
protected syncedBlockNum: bigint;
|
20
|
-
protected gap: bigint;
|
21
|
-
protected lastBlockNum: bigint;
|
22
|
-
protected eventCount: bigint;
|
23
16
|
constructor(config: IClientConfig);
|
24
|
-
|
25
|
-
protected _processLogs(logs: GetLogsReturnType<undefined, AbiEvent[]>): Promise<void>;
|
26
|
-
protected _sync(): Promise<void>;
|
27
|
-
on<T extends keyof IStateSyncEvents>(eventName: T, callback: IStateSyncEvents[T]): EventEmitter;
|
28
|
-
start(): Promise<import("viem").WatchBlocksReturnType>;
|
29
|
-
createWatcher<Signature extends EventSignature>(config: IWatcherConfig<Signature>): Promise<ISimplifiedWatcher>;
|
17
|
+
createSyncer<signature extends EventSignature>(config: ISyncerConfig<signature>): Promise<Syncer<signature>>;
|
30
18
|
}
|
package/classes/Client.js
CHANGED
@@ -1,116 +1,26 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.StateSync = void 0;
|
4
|
-
const
|
5
|
-
const
|
6
|
-
const events_1 = require("events");
|
4
|
+
const Syncer_1 = require("./Syncer");
|
5
|
+
const SimpleEmitter_1 = require("./SimpleEmitter");
|
7
6
|
/**
|
8
7
|
* The StateSync client class.
|
9
8
|
*/
|
10
|
-
class StateSync {
|
9
|
+
class StateSync extends SimpleEmitter_1.SimpleEmitter {
|
11
10
|
constructor(config) {
|
12
|
-
|
13
|
-
this.startBlockNum = 0n;
|
14
|
-
this.syncedBlockNum = 0n;
|
15
|
-
this.gap = 500000n;
|
16
|
-
this.lastBlockNum = 0n;
|
17
|
-
this.eventCount = 0n;
|
11
|
+
super();
|
18
12
|
this.public = config.public;
|
19
13
|
this.wallet = config.wallet;
|
20
14
|
this.level = config.level;
|
21
15
|
this.levelPath = config.levelPath || "";
|
22
|
-
this.
|
23
|
-
|
24
|
-
|
25
|
-
;
|
26
|
-
async _setInitialBlockNumbers() {
|
27
|
-
const lastSyncedBlocksNumbers = Array.from(this.watchers.values()).map(s => s.lastSyncedBlockNumber);
|
28
|
-
const smalledSyncedBlockNumber = lastSyncedBlocksNumbers.reduce((min, value) => (value < min ? value : min));
|
29
|
-
this.startBlockNum = smalledSyncedBlockNumber;
|
30
|
-
this.syncedBlockNum = smalledSyncedBlockNumber;
|
31
|
-
this.lastBlockNum = await this.public.getBlockNumber();
|
32
|
-
return this.public.watchBlocks({ onBlock: (block) => this.lastBlockNum = block.number });
|
33
|
-
}
|
34
|
-
;
|
35
|
-
async _processLogs(logs) {
|
36
|
-
// Send logs to watcher classes to handle the storing logic.
|
37
|
-
for (const watcher of this.watchers.values()) {
|
38
|
-
const eventName = watcher.event.name;
|
39
|
-
const watcherLogs = logs.filter(log => watcher.addresses.includes((0, viem_1.checksumAddress)(log.address)) && log.eventName === eventName);
|
40
|
-
// make sure logs are well ordered by logIndex
|
41
|
-
watcherLogs.sort((a, b) => a.logIndex - b.logIndex);
|
42
|
-
await watcher.onLogs(watcherLogs);
|
43
|
-
await watcher.setSyncedBlockNumber(this.syncedBlockNum);
|
44
|
-
}
|
45
|
-
}
|
46
|
-
;
|
47
|
-
async _sync() {
|
48
|
-
const stopInitialWatcher = await this._setInitialBlockNumbers();
|
49
|
-
const contractsAddresses = Array.from(this.watchers.values()).map(s => s.addresses).flat(1);
|
50
|
-
const eventsABI = Array.from(this.watchers.values()).map(s => s.event);
|
51
|
-
while (this.syncedBlockNum < this.lastBlockNum) {
|
52
|
-
try {
|
53
|
-
await new Promise(x => setTimeout(x, this.timeout));
|
54
|
-
// Fetch logs from the network.
|
55
|
-
const logs = await this.public.getLogs({
|
56
|
-
fromBlock: this.syncedBlockNum,
|
57
|
-
toBlock: this.syncedBlockNum + this.gap,
|
58
|
-
address: contractsAddresses,
|
59
|
-
events: eventsABI,
|
60
|
-
});
|
61
|
-
await this._processLogs(logs);
|
62
|
-
this.syncedBlockNum + this.gap > this.lastBlockNum ?
|
63
|
-
this.syncedBlockNum = this.lastBlockNum :
|
64
|
-
this.syncedBlockNum += this.gap;
|
65
|
-
this.eventCount += BigInt(logs.length);
|
66
|
-
this.emitter.emit("sync", {
|
67
|
-
startBlockNum: this.startBlockNum,
|
68
|
-
endBlockNum: this.lastBlockNum,
|
69
|
-
syncedBlockNum: this.syncedBlockNum,
|
70
|
-
eventCount: this.eventCount
|
71
|
-
});
|
72
|
-
}
|
73
|
-
catch (e) {
|
74
|
-
const error = e;
|
75
|
-
if (error.name === "LimitExceededRpcError") {
|
76
|
-
this.gap = this.gap / 2n;
|
77
|
-
this.emitter.emit("syncRateLimitExeeded", this.gap);
|
78
|
-
continue;
|
79
|
-
}
|
80
|
-
throw error;
|
81
|
-
}
|
82
|
-
}
|
83
|
-
stopInitialWatcher();
|
84
|
-
}
|
85
|
-
;
|
86
|
-
on(eventName, callback) {
|
87
|
-
return this.emitter.on(eventName, callback);
|
88
|
-
}
|
89
|
-
;
|
90
|
-
async start() {
|
91
|
-
await this._sync();
|
92
|
-
this.emitter.emit('ready');
|
93
|
-
return this.public.watchBlocks({
|
94
|
-
onBlock: async (block) => {
|
95
|
-
this.syncedBlockNum = block.number;
|
96
|
-
this.emitter.emit('newBlock', block.number);
|
97
|
-
const contractsAddresses = Array.from(this.watchers.values()).map(s => s.addresses).flat(1);
|
98
|
-
const eventsABI = Array.from(this.watchers.values()).map(s => s.event);
|
99
|
-
// Fetch logs from the network.
|
100
|
-
const logs = await this.public.getLogs({
|
101
|
-
blockHash: block.hash,
|
102
|
-
address: contractsAddresses,
|
103
|
-
events: eventsABI,
|
104
|
-
});
|
105
|
-
await this._processLogs(logs);
|
16
|
+
this.public.watchBlocks({
|
17
|
+
onBlock: (block) => {
|
18
|
+
this.emit("newBlock", { block });
|
106
19
|
}
|
107
20
|
});
|
108
21
|
}
|
109
|
-
async
|
110
|
-
|
111
|
-
await watcher.connect();
|
112
|
-
this.watchers.set(config.dbID, watcher);
|
113
|
-
return watcher;
|
22
|
+
async createSyncer(config) {
|
23
|
+
return new Syncer_1.Syncer(this, config);
|
114
24
|
}
|
115
25
|
;
|
116
26
|
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { EventSignature } from "abitype/dist/types/human-readable/types/signatures";
|
2
|
+
import { ICommit, ISaverConfig, getItemIDFn, onLogFn } from "../types/Saver";
|
3
|
+
import { StateSync } from "./Client";
|
4
|
+
import { Log } from "../types/Log";
|
5
|
+
import { IAbstractLevel, IDatabaseEnv, IDatabaseItem } from "../types/Level";
|
6
|
+
export declare class Saver<signature extends EventSignature> {
|
7
|
+
protected client: StateSync;
|
8
|
+
lastBlockNum: bigint;
|
9
|
+
protected dbID: string;
|
10
|
+
protected onLog: onLogFn<signature>;
|
11
|
+
protected getItemID: getItemIDFn<signature>;
|
12
|
+
db: IAbstractLevel<string, IDatabaseItem>;
|
13
|
+
constructor(client: StateSync, config: ISaverConfig<signature>);
|
14
|
+
protected _updateEnv(): Promise<void>;
|
15
|
+
getEnv(): Promise<IDatabaseEnv>;
|
16
|
+
init(): Promise<void>;
|
17
|
+
/**
|
18
|
+
* saveLogs is optimized to not call mutiple times the db for the same item.
|
19
|
+
* Instead it will calculate the state in memory and save the final result in the db.
|
20
|
+
*/
|
21
|
+
saveLogs(logs: Log<signature>[]): Promise<ICommit[]>;
|
22
|
+
}
|
package/classes/Saver.js
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.Saver = void 0;
|
4
|
+
class Saver {
|
5
|
+
constructor(client, config) {
|
6
|
+
this.client = client;
|
7
|
+
this.lastBlockNum = 0n;
|
8
|
+
this.dbID = config.dbID;
|
9
|
+
this.onLog = config.onLog;
|
10
|
+
this.getItemID = config.getItemID ? config.getItemID : (log) => log.address;
|
11
|
+
this.db = new this.client.level(client.levelPath + `/${config.dbID}`.replace('//', "/"), { valueEncoding: "json" });
|
12
|
+
}
|
13
|
+
;
|
14
|
+
// save some env variables in the db
|
15
|
+
async _updateEnv() {
|
16
|
+
const env = {
|
17
|
+
lastBlockNum: this.lastBlockNum.toString()
|
18
|
+
};
|
19
|
+
await this.db.put("ENV", env);
|
20
|
+
}
|
21
|
+
;
|
22
|
+
async getEnv() {
|
23
|
+
return await this.db.get("ENV").catch(e => undefined);
|
24
|
+
}
|
25
|
+
;
|
26
|
+
async init() {
|
27
|
+
await this.db.open();
|
28
|
+
const env = await this.getEnv();
|
29
|
+
this.lastBlockNum = env?.lastBlockNum ? BigInt(env.lastBlockNum) : 0n;
|
30
|
+
await this._updateEnv();
|
31
|
+
}
|
32
|
+
;
|
33
|
+
/**
|
34
|
+
* saveLogs is optimized to not call mutiple times the db for the same item.
|
35
|
+
* Instead it will calculate the state in memory and save the final result in the db.
|
36
|
+
*/
|
37
|
+
async saveLogs(logs) {
|
38
|
+
if (logs.length === 0)
|
39
|
+
return [];
|
40
|
+
const ids = logs.map((log) => this.getItemID(log));
|
41
|
+
const oldDatas = await this.db.getMany(ids);
|
42
|
+
// generate cache
|
43
|
+
const newDatas = new Map();
|
44
|
+
for (const index in ids) {
|
45
|
+
const oldData = oldDatas[index];
|
46
|
+
const log = logs[index];
|
47
|
+
const id = this.getItemID(log);
|
48
|
+
const newData = this.onLog(log, oldData);
|
49
|
+
newDatas.set(id, newData);
|
50
|
+
if (this.lastBlockNum < log.blockNumber)
|
51
|
+
this.lastBlockNum = log.blockNumber;
|
52
|
+
}
|
53
|
+
;
|
54
|
+
// save results
|
55
|
+
const arr = Array.from(newDatas.entries());
|
56
|
+
const payload = arr.map(([address, newData]) => ({
|
57
|
+
type: "put",
|
58
|
+
key: address,
|
59
|
+
value: newData
|
60
|
+
}));
|
61
|
+
await this.db.batch(payload);
|
62
|
+
await this._updateEnv();
|
63
|
+
return payload;
|
64
|
+
}
|
65
|
+
}
|
66
|
+
exports.Saver = Saver;
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { EventEmitter } from "events";
|
2
|
+
export type BaseEvents = {
|
3
|
+
[key: string]: any;
|
4
|
+
};
|
5
|
+
export declare class SimpleEmitter<events extends BaseEvents> {
|
6
|
+
protected emitter: EventEmitter;
|
7
|
+
constructor();
|
8
|
+
on<E extends keyof events>(event: E, listener: (data: events[E]) => void): void;
|
9
|
+
once<E extends keyof events>(event: E, listener: (data: events[E]) => void): void;
|
10
|
+
emit<E extends keyof events>(event: E, data?: events[E]): boolean;
|
11
|
+
removeListener<E extends keyof events>(event: E, listener: (data: events[E]) => void): void;
|
12
|
+
removeAllListeners(): void;
|
13
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.SimpleEmitter = void 0;
|
4
|
+
const events_1 = require("events");
|
5
|
+
class SimpleEmitter {
|
6
|
+
constructor() {
|
7
|
+
this.emitter = new events_1.EventEmitter();
|
8
|
+
}
|
9
|
+
;
|
10
|
+
on(event, listener) {
|
11
|
+
this.emitter.on(event, listener);
|
12
|
+
}
|
13
|
+
;
|
14
|
+
once(event, listener) {
|
15
|
+
this.emitter.once(event, listener);
|
16
|
+
}
|
17
|
+
;
|
18
|
+
emit(event, data) {
|
19
|
+
return this.emitter.emit(event, data);
|
20
|
+
}
|
21
|
+
;
|
22
|
+
removeListener(event, listener) {
|
23
|
+
this.emitter.removeListener(event, listener);
|
24
|
+
}
|
25
|
+
;
|
26
|
+
removeAllListeners() {
|
27
|
+
this.emitter.removeAllListeners();
|
28
|
+
}
|
29
|
+
;
|
30
|
+
}
|
31
|
+
exports.SimpleEmitter = SimpleEmitter;
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { EventSignature } from "abitype/dist/types/human-readable/types/signatures";
|
2
|
+
import { ISyncerConfig, ISyncerEvents } from "../types/Syncer";
|
3
|
+
import { StateSync } from "./Client";
|
4
|
+
import { Watcher } from "./Watcher";
|
5
|
+
import { Saver } from "./Saver";
|
6
|
+
import { Log } from "../types/Log";
|
7
|
+
import { Address } from "abitype";
|
8
|
+
import { SimpleEmitter } from "./SimpleEmitter";
|
9
|
+
export declare class Syncer<signature extends EventSignature> extends SimpleEmitter<ISyncerEvents<signature>> {
|
10
|
+
protected client: StateSync;
|
11
|
+
protected logs: Log<signature>[];
|
12
|
+
protected lastPush: number;
|
13
|
+
protected watcher: Watcher<signature>;
|
14
|
+
protected saver: Saver<signature>;
|
15
|
+
protected commitInterval: number;
|
16
|
+
constructor(client: StateSync, config: ISyncerConfig<signature>);
|
17
|
+
protected _commit(): Promise<void>;
|
18
|
+
start(): Promise<void>;
|
19
|
+
addContracts(contracts: Address[]): Promise<void>;
|
20
|
+
removeContracts(contracts: Address[]): Promise<void>;
|
21
|
+
}
|
@@ -0,0 +1,60 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.Syncer = void 0;
|
4
|
+
const Watcher_1 = require("./Watcher");
|
5
|
+
const Saver_1 = require("./Saver");
|
6
|
+
const SimpleEmitter_1 = require("./SimpleEmitter");
|
7
|
+
class Syncer extends SimpleEmitter_1.SimpleEmitter {
|
8
|
+
constructor(client, config) {
|
9
|
+
super();
|
10
|
+
this.client = client;
|
11
|
+
this.logs = [];
|
12
|
+
this.lastPush = 0;
|
13
|
+
this.commitInterval = config.commitInterval || 100;
|
14
|
+
this.watcher = new Watcher_1.Watcher(client, {
|
15
|
+
contracts: config.contracts,
|
16
|
+
event: config.event
|
17
|
+
});
|
18
|
+
this.saver = new Saver_1.Saver(client, {
|
19
|
+
dbID: config.dbID,
|
20
|
+
getItemID: config.getItemID,
|
21
|
+
onLog: config.onLog
|
22
|
+
});
|
23
|
+
}
|
24
|
+
;
|
25
|
+
async _commit() {
|
26
|
+
const date = Date.now();
|
27
|
+
// If the last push was less than waitTime ago, don't commit
|
28
|
+
if (this.lastPush > date - this.commitInterval)
|
29
|
+
return;
|
30
|
+
this.lastPush = date;
|
31
|
+
// Wait for commit interval to avoid letting some logs in the queue
|
32
|
+
await new Promise(x => setTimeout(x, this.commitInterval));
|
33
|
+
// Clear queue and save logs
|
34
|
+
const logs = this.logs.splice(0, this.logs.length);
|
35
|
+
const commits = await this.saver.saveLogs(logs);
|
36
|
+
this.emit("commit", { commits });
|
37
|
+
}
|
38
|
+
async start() {
|
39
|
+
await this.saver.init();
|
40
|
+
this.watcher.on('log', async (log) => {
|
41
|
+
this.logs.push(log);
|
42
|
+
await this._commit();
|
43
|
+
});
|
44
|
+
await this.watcher.start();
|
45
|
+
}
|
46
|
+
;
|
47
|
+
async addContracts(contracts) {
|
48
|
+
await this.watcher.addContracts(contracts);
|
49
|
+
}
|
50
|
+
;
|
51
|
+
async removeContracts(contracts) {
|
52
|
+
await this.watcher.removeContracts(contracts);
|
53
|
+
for (const contract of contracts) {
|
54
|
+
await this.saver.db.del(contract);
|
55
|
+
}
|
56
|
+
;
|
57
|
+
}
|
58
|
+
;
|
59
|
+
}
|
60
|
+
exports.Syncer = Syncer;
|
package/classes/Watcher.d.ts
CHANGED
@@ -1,27 +1,24 @@
|
|
1
|
-
import {
|
2
|
-
import { IWatcherEvents, Log } from "../types/Watcher";
|
3
|
-
import { IAbstractLevel, IWatcherConfig, WatcherSchema } from "../types/Watcher";
|
1
|
+
import { Address } from "viem";
|
4
2
|
import { StateSync } from "./Client";
|
5
|
-
import { EventSignature } from "abitype/dist/types/human-readable/types/signatures";
|
6
3
|
import { EventEmitter } from "events";
|
7
|
-
|
4
|
+
import { EventSignature } from "abitype/dist/types/human-readable/types/signatures";
|
5
|
+
import { IWatcherConfig, IWatcherEvents } from "../types/Watcher";
|
6
|
+
/**
|
7
|
+
* Watcher class is a reader class that watch events from the blockchain and send them as events via event emitter.
|
8
|
+
* Events can then be received by the "Saver" class witch will save them in a local DB.
|
9
|
+
*/
|
10
|
+
export declare class Watcher<signature extends EventSignature> {
|
8
11
|
protected client: StateSync;
|
9
|
-
protected config: IWatcherConfig<Signature>;
|
10
|
-
protected id: string;
|
11
|
-
event: AbiEvent;
|
12
|
-
addresses: Address[];
|
13
|
-
lastSyncedBlockNumber: bigint;
|
14
12
|
protected emitter: EventEmitter;
|
15
|
-
|
16
|
-
|
17
|
-
protected
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
onLogs(logs: Log<any>[]): Promise<void>;
|
13
|
+
protected currentBlockNum: bigint;
|
14
|
+
protected contracts: Address[];
|
15
|
+
protected event: EventSignature;
|
16
|
+
protected requestInterval: number;
|
17
|
+
protected started: boolean;
|
18
|
+
constructor(client: StateSync, config: IWatcherConfig<signature>);
|
19
|
+
protected _getLogs(contracts: Address[], fromBlock?: bigint): Promise<void>;
|
20
|
+
start(): void;
|
21
|
+
addContracts(contracts: Address[]): Promise<void>;
|
22
|
+
removeContracts(contracts: Address[]): Promise<void>;
|
23
|
+
on<Evt extends keyof IWatcherEvents>(event: Evt, cb: IWatcherEvents[Evt]): EventEmitter;
|
27
24
|
}
|
package/classes/Watcher.js
CHANGED
@@ -3,87 +3,73 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Watcher = void 0;
|
4
4
|
const viem_1 = require("viem");
|
5
5
|
const events_1 = require("events");
|
6
|
+
/**
|
7
|
+
* Watcher class is a reader class that watch events from the blockchain and send them as events via event emitter.
|
8
|
+
* Events can then be received by the "Saver" class witch will save them in a local DB.
|
9
|
+
*/
|
6
10
|
class Watcher {
|
7
11
|
constructor(client, config) {
|
8
12
|
this.client = client;
|
9
|
-
this.
|
10
|
-
this.
|
11
|
-
this.id = config.dbID;
|
12
|
-
this.event = (0, viem_1.parseAbiItem)(config.event);
|
13
|
-
this.addresses = config.addresses.map(add => (0, viem_1.checksumAddress)(add));
|
14
|
-
this.lastSyncedBlockNumber = 0n;
|
13
|
+
this.currentBlockNum = 0n;
|
14
|
+
this.started = false;
|
15
15
|
this.emitter = new events_1.EventEmitter();
|
16
|
+
this.contracts = config.contracts || [];
|
17
|
+
this.requestInterval = config.requestInterval || 50;
|
18
|
+
this.event = config.event;
|
19
|
+
this.client.on("newBlock", ({ block }) => this.currentBlockNum = block.number);
|
16
20
|
}
|
17
21
|
;
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
22
|
+
async _getLogs(contracts, fromBlock = 0n) {
|
23
|
+
let gap = this.currentBlockNum;
|
24
|
+
let block = fromBlock;
|
25
|
+
while (block < this.currentBlockNum) {
|
26
|
+
await new Promise(x => setTimeout(x, this.requestInterval));
|
27
|
+
try {
|
28
|
+
const logs = await this.client.public.getLogs({
|
29
|
+
fromBlock: block,
|
30
|
+
toBlock: block + gap,
|
31
|
+
address: contracts,
|
32
|
+
event: (0, viem_1.parseAbiItem)(this.event),
|
33
|
+
});
|
34
|
+
logs.forEach(l => this.emitter.emit("logs", l));
|
35
|
+
}
|
36
|
+
catch (e) {
|
37
|
+
const err = e;
|
38
|
+
if (err.name !== "LimitExceededRpcError")
|
39
|
+
throw err;
|
40
|
+
gap = gap / 2n;
|
41
|
+
continue;
|
42
|
+
}
|
43
|
+
}
|
33
44
|
}
|
34
45
|
;
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
this.
|
46
|
+
start() {
|
47
|
+
if (this.started)
|
48
|
+
throw new Error("Already started");
|
49
|
+
this.started = true;
|
50
|
+
this._getLogs(this.contracts).then(() => {
|
51
|
+
this.client.on("newBlock", () => this._getLogs(this.contracts));
|
52
|
+
});
|
39
53
|
}
|
40
54
|
;
|
41
|
-
async
|
42
|
-
|
43
|
-
|
44
|
-
this.lastSyncedBlockNumber = log.blockNumber;
|
45
|
-
await this.db.put(log.address, newData);
|
46
|
-
await this._updateEnv();
|
47
|
-
this.emitter.emit("change", {
|
48
|
-
id: log.address,
|
49
|
-
state: newData
|
50
|
-
});
|
55
|
+
async addContracts(contracts) {
|
56
|
+
await this._getLogs(contracts);
|
57
|
+
this.contracts.push(...contracts);
|
51
58
|
}
|
52
59
|
;
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
return;
|
60
|
-
const addresses = logs.map((l) => (0, viem_1.checksumAddress)(l.address));
|
61
|
-
const oldData = await this.db.getMany(addresses);
|
62
|
-
// generate Watchers
|
63
|
-
const newDatas = new Map();
|
64
|
-
for (const index in addresses) {
|
65
|
-
const watcher = oldData[index];
|
66
|
-
const log = logs[index];
|
67
|
-
const identifier = this.config.extractIdentifier ? this.config.extractIdentifier(log) : addresses[index];
|
68
|
-
const newData = this.config.onLog(log, watcher);
|
69
|
-
newDatas.set(identifier, newData);
|
70
|
-
this.emitter.emit("change", {
|
71
|
-
id: identifier,
|
72
|
-
state: newData
|
73
|
-
});
|
60
|
+
async removeContracts(contracts) {
|
61
|
+
for (const contract of contracts) {
|
62
|
+
const index = this.contracts.indexOf(contract);
|
63
|
+
if (index === -1)
|
64
|
+
continue;
|
65
|
+
this.contracts.splice(index, 1);
|
74
66
|
}
|
75
67
|
;
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
key: address,
|
81
|
-
value: newData
|
82
|
-
}));
|
83
|
-
await this.db.batch(payload);
|
84
|
-
await this._updateEnv();
|
68
|
+
}
|
69
|
+
;
|
70
|
+
on(event, cb) {
|
71
|
+
return this.emitter.on(event, cb);
|
85
72
|
}
|
86
73
|
;
|
87
74
|
}
|
88
75
|
exports.Watcher = Watcher;
|
89
|
-
;
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
@@ -15,6 +15,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
15
15
|
};
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
17
17
|
__exportStar(require("./classes/Client"), exports);
|
18
|
-
__exportStar(require("./classes/
|
18
|
+
__exportStar(require("./classes/Saver"), exports);
|
19
19
|
__exportStar(require("./types/Client"), exports);
|
20
|
-
__exportStar(require("./types/
|
20
|
+
__exportStar(require("./types/Saver"), exports);
|
package/package.json
CHANGED
@@ -4,17 +4,17 @@
|
|
4
4
|
"abitype": "^1.0.8",
|
5
5
|
"abstract-level": "^2.0.2",
|
6
6
|
"events": "^3.3.0",
|
7
|
-
"level": "^9.0.0",
|
8
7
|
"path": "^0.12.7",
|
9
8
|
"viem": "^2.21.58"
|
10
9
|
},
|
11
10
|
"name": "@lunarislab/state-sync",
|
12
|
-
"version": "1.
|
11
|
+
"version": "1.1.2",
|
13
12
|
"main": "index.js",
|
14
13
|
"scripts": {
|
15
14
|
"test": "ts-node ./test.ts",
|
16
15
|
"build": "tsc && cp package.json dist/ && cp README.md dist/",
|
17
|
-
"sendNpm": "cd ./dist && npm version patch && npm publish --access=public"
|
16
|
+
"sendNpm": "cd ./dist && npm version patch && npm publish --access=public",
|
17
|
+
"publish": "tsc && cp package.json dist/ && cp README.md dist/ && cd ./dist && npm publish --access=public && cd ../ && npm version patch"
|
18
18
|
},
|
19
19
|
"author": "",
|
20
20
|
"license": "ISC",
|
package/types/Client.d.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
import { PublicClient, WalletClient } from "viem";
|
2
|
-
import { LevelDBConstructor } from "./
|
1
|
+
import { Block, PublicClient, WalletClient } from "viem";
|
2
|
+
import { LevelDBConstructor } from "./Level";
|
3
3
|
export interface IClientConfig {
|
4
4
|
public: PublicClient;
|
5
5
|
wallet: WalletClient;
|
@@ -14,8 +14,7 @@ export interface SyncParams {
|
|
14
14
|
eventCount: bigint;
|
15
15
|
}
|
16
16
|
export interface IStateSyncEvents {
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
syncRateLimitExceeded: (newBlockRange: number) => void;
|
17
|
+
newBlock: {
|
18
|
+
block: Block;
|
19
|
+
};
|
21
20
|
}
|
package/types/Level.d.ts
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
import { AbstractBatchOperation, AbstractBatchOptions, AbstractChainedBatch, AbstractIterator, AbstractIteratorOptions, AbstractKeyIterator, AbstractKeyIteratorOptions, AbstractOpenOptions, AbstractValueIterator, AbstractValueIteratorOptions } from "abstract-level";
|
2
|
+
export interface IDatabaseItem {
|
3
|
+
[key: string]: number | boolean | string;
|
4
|
+
}
|
5
|
+
export interface IDatabaseEnv {
|
6
|
+
lastBlockNum: string;
|
7
|
+
}
|
8
|
+
/**
|
9
|
+
* For more modularity, LevelDB instance is injected by user, so we need to provide a generic type for LevelDB.
|
10
|
+
* Every LevelDB implementation must implement this (minimized) interface to avoid errors.
|
11
|
+
*/
|
12
|
+
export interface LevelDBConstructor<K = any, V = any> {
|
13
|
+
new (path: string, options?: any): IAbstractLevel<K, V>;
|
14
|
+
}
|
15
|
+
export declare class IAbstractLevel<KDefault = string, VDefault = string> {
|
16
|
+
/**
|
17
|
+
* Open the database.
|
18
|
+
*/
|
19
|
+
open(): Promise<void>;
|
20
|
+
open(options: AbstractOpenOptions): Promise<void>;
|
21
|
+
/**
|
22
|
+
* Get a value from the database by {@link key}.
|
23
|
+
*/
|
24
|
+
get(key: KDefault): Promise<VDefault>;
|
25
|
+
get(key: KDefault, callback: (err: any, value: VDefault) => void): Promise<VDefault>;
|
26
|
+
/**
|
27
|
+
* Get multiple values from the database by an array of {@link keys}.
|
28
|
+
*/
|
29
|
+
getMany(keys: KDefault[]): Promise<VDefault[]>;
|
30
|
+
/**
|
31
|
+
* Add a new entry or overwrite an existing entry.
|
32
|
+
*/
|
33
|
+
put(key: KDefault, value: VDefault): Promise<void>;
|
34
|
+
/**
|
35
|
+
* Delete an entry by {@link key}.
|
36
|
+
*/
|
37
|
+
del(key: KDefault): Promise<void>;
|
38
|
+
/**
|
39
|
+
* Perform multiple _put_ and/or _del_ operations in bulk.
|
40
|
+
*/
|
41
|
+
batch(operations: Array<AbstractBatchOperation<typeof this, KDefault, VDefault>>): Promise<void>;
|
42
|
+
batch<K = KDefault, V = VDefault>(operations: Array<AbstractBatchOperation<typeof this, K, V>>, options: AbstractBatchOptions<K, V>): Promise<void>;
|
43
|
+
batch(): AbstractChainedBatch<typeof this, KDefault, VDefault>;
|
44
|
+
/**
|
45
|
+
* Create an iterator. For example:
|
46
|
+
*
|
47
|
+
* ```js
|
48
|
+
* for await (const [key, value] of db.iterator({ gte: 'a' })) {
|
49
|
+
* console.log([key, value])
|
50
|
+
* }
|
51
|
+
* ```
|
52
|
+
*/
|
53
|
+
iterator(): AbstractIterator<typeof this, KDefault, VDefault>;
|
54
|
+
iterator<K = KDefault, V = VDefault>(options: AbstractIteratorOptions<K, V>): AbstractIterator<typeof this, K, V>;
|
55
|
+
/**
|
56
|
+
* Create a key iterator. For example:
|
57
|
+
*
|
58
|
+
* ```js
|
59
|
+
* for await (const key of db.keys({ gte: 'a' })) {
|
60
|
+
* console.log(key)
|
61
|
+
* }
|
62
|
+
* ```
|
63
|
+
*/
|
64
|
+
keys(): AbstractKeyIterator<typeof this, KDefault>;
|
65
|
+
keys<K = KDefault>(options: AbstractKeyIteratorOptions<K>): AbstractKeyIterator<typeof this, K>;
|
66
|
+
/**
|
67
|
+
* Create a value iterator. For example:
|
68
|
+
*
|
69
|
+
* ```js
|
70
|
+
* for await (const value of db.values({ gte: 'a' })) {
|
71
|
+
* console.log(value)
|
72
|
+
* }
|
73
|
+
* ```
|
74
|
+
*/
|
75
|
+
values(): AbstractValueIterator<typeof this, KDefault, VDefault>;
|
76
|
+
values<K = KDefault, V = VDefault>(options: AbstractValueIteratorOptions<K, V>): AbstractValueIterator<typeof this, K, V>;
|
77
|
+
/**
|
78
|
+
* Delete all entries or a range.
|
79
|
+
*/
|
80
|
+
clear(): Promise<void>;
|
81
|
+
}
|
package/types/Level.js
ADDED
package/types/Log.d.ts
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
import { ParseAbiItem, AbiParametersToPrimitiveTypes, AbiParameter } from 'abitype';
|
2
|
+
import { EventSignature } from "abitype/dist/types/human-readable/types/signatures";
|
3
|
+
import { Log as viemLog } from "viem";
|
4
|
+
/**
|
5
|
+
* Convert the signature to ABI type with `ParseAbiItem` and extract the inputs as `AbiParameter[]`.
|
6
|
+
* So the returned type can be used in `AbiParametersToPrimitiveTypes`.
|
7
|
+
*/
|
8
|
+
export type EventAbiInputs<signature extends EventSignature> = ParseAbiItem<signature> extends {
|
9
|
+
inputs: infer Inputs;
|
10
|
+
} ? Inputs extends readonly AbiParameter[] ? Inputs : never : never;
|
11
|
+
/**
|
12
|
+
* Not necessary but more readable, it just wrap the `AbiParametersToPrimitiveTypes` with the previous type `EventAbiInputs`.
|
13
|
+
*/
|
14
|
+
export type EventArgsTypes<signature extends EventSignature> = AbiParametersToPrimitiveTypes<EventAbiInputs<signature>>;
|
15
|
+
/**
|
16
|
+
* Check if all the keys have a name. To decide if final type is object or array.
|
17
|
+
*/
|
18
|
+
type HasAllNames<Inputs extends {
|
19
|
+
name: string;
|
20
|
+
}[]> = undefined extends Inputs[number]['name'] ? false : true;
|
21
|
+
/**
|
22
|
+
* Create the object using both EventAbiInputs for keys and the converted types for values.
|
23
|
+
*/
|
24
|
+
export type EventArgs<signature extends EventSignature> = HasAllNames<EventAbiInputs<signature>> extends true ? {
|
25
|
+
[K in EventAbiInputs<signature>[number]['name']]: EventArgsTypes<signature>[number];
|
26
|
+
} : EventArgsTypes<signature>;
|
27
|
+
export type Log<signature extends EventSignature> = viemLog & {
|
28
|
+
args: EventArgs<signature>;
|
29
|
+
};
|
30
|
+
export {};
|
package/types/Log.js
ADDED
package/types/Saver.d.ts
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
import { EventSignature } from "abitype/dist/types/human-readable/types/signatures";
|
2
|
+
import { Log } from "./Log";
|
3
|
+
import { IDatabaseItem } from "./Level";
|
4
|
+
import { Address } from "abitype";
|
5
|
+
export type onLogFn<signature extends EventSignature> = (event: Log<signature>, oldData?: IDatabaseItem) => IDatabaseItem;
|
6
|
+
export type getItemIDFn<signature extends EventSignature> = (log: Log<signature>) => Address;
|
7
|
+
export interface ISaverConfig<signature extends EventSignature> {
|
8
|
+
dbID: string;
|
9
|
+
getItemID?: getItemIDFn<signature>;
|
10
|
+
onLog: onLogFn<signature>;
|
11
|
+
}
|
12
|
+
export interface ICommit {
|
13
|
+
type: "put";
|
14
|
+
key: string;
|
15
|
+
value: IDatabaseItem;
|
16
|
+
}
|
package/types/Saver.js
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
import { IDatabaseItem } from "./Level";
|
2
|
+
import { ISaverConfig } from "./Saver";
|
3
|
+
import { EventSignature } from "abitype/dist/types/human-readable/types/signatures";
|
4
|
+
import { IWatcherConfig } from "./Watcher";
|
5
|
+
import { Log } from "./Log";
|
6
|
+
export type ISyncerConfig<signature extends EventSignature> = ISaverConfig<signature> & IWatcherConfig<signature> & {
|
7
|
+
commitInterval?: number;
|
8
|
+
};
|
9
|
+
export interface ISyncerEvents<signature extends EventSignature> {
|
10
|
+
commit: {
|
11
|
+
commits: {
|
12
|
+
type: "put";
|
13
|
+
key: string;
|
14
|
+
value: IDatabaseItem;
|
15
|
+
}[];
|
16
|
+
};
|
17
|
+
log: {
|
18
|
+
log: Log<signature>;
|
19
|
+
};
|
20
|
+
}
|
package/types/Syncer.js
ADDED
package/types/Watcher.d.ts
CHANGED
@@ -1,131 +1,11 @@
|
|
1
|
-
import { AbstractBatchOperation, AbstractBatchOptions, AbstractChainedBatch, AbstractIterator, AbstractIteratorOptions, AbstractKeyIterator, AbstractKeyIteratorOptions, AbstractOpenOptions, AbstractValueIterator, AbstractValueIteratorOptions } from "abstract-level";
|
2
|
-
import { AbiEvent, Address, Log as viemLog } from "viem";
|
3
|
-
import { ParseAbiItem, AbiParametersToPrimitiveTypes, AbiParameter } from 'abitype';
|
4
1
|
import { EventSignature } from "abitype/dist/types/human-readable/types/signatures";
|
5
|
-
|
6
|
-
|
7
|
-
* So the returned type can be used in `AbiParametersToPrimitiveTypes`.
|
8
|
-
*/
|
9
|
-
export type EventAbiInputs<signature extends EventSignature> = ParseAbiItem<signature> extends {
|
10
|
-
inputs: infer Inputs;
|
11
|
-
} ? Inputs extends readonly AbiParameter[] ? Inputs : never : never;
|
12
|
-
/**
|
13
|
-
* Not necessary but more readable, it just wrap the `AbiParametersToPrimitiveTypes` with the previous type `EventAbiInputs`.
|
14
|
-
*/
|
15
|
-
export type EventArgsTypes<signature extends EventSignature> = AbiParametersToPrimitiveTypes<EventAbiInputs<signature>>;
|
16
|
-
/**
|
17
|
-
* Check if all the keys have a name. To decide if final type is object or array.
|
18
|
-
*/
|
19
|
-
type HasAllNames<Inputs extends {
|
20
|
-
name: string;
|
21
|
-
}[]> = undefined extends Inputs[number]['name'] ? false : true;
|
22
|
-
/**
|
23
|
-
* Create the object using both EventAbiInputs for keys and the converted types for values.
|
24
|
-
*/
|
25
|
-
export type EventArgs<signature extends EventSignature> = HasAllNames<EventAbiInputs<signature>> extends true ? {
|
26
|
-
[K in EventAbiInputs<signature>[number]['name']]: EventArgsTypes<signature>[number];
|
27
|
-
} : EventArgsTypes<signature>;
|
28
|
-
export type Log<signature extends EventSignature> = viemLog & {
|
29
|
-
args: EventArgs<signature>;
|
30
|
-
};
|
31
|
-
export interface ISimplifiedWatcher {
|
32
|
-
db: IAbstractLevel<string, WatcherSchema>;
|
33
|
-
event: AbiEvent;
|
34
|
-
addresses: Address[];
|
35
|
-
lastSyncedBlockNumber: bigint;
|
36
|
-
on: <T extends keyof IWatcherEvents>(eventName: T, callback: IWatcherEvents[T]) => void;
|
37
|
-
}
|
38
|
-
export interface IWatcherEvents {
|
39
|
-
change: (data: {
|
40
|
-
id: string;
|
41
|
-
state: any;
|
42
|
-
}) => void;
|
43
|
-
}
|
44
|
-
export type WatcherSchema = {
|
45
|
-
[key: string]: number | boolean | string;
|
46
|
-
};
|
2
|
+
import { Address } from "viem";
|
3
|
+
import { Log } from "./Log";
|
47
4
|
export interface IWatcherConfig<signature extends EventSignature> {
|
48
|
-
dbID: string;
|
49
5
|
event: signature;
|
50
|
-
|
51
|
-
|
52
|
-
addresses: Address[];
|
6
|
+
requestInterval?: number;
|
7
|
+
contracts?: Address[];
|
53
8
|
}
|
54
|
-
export interface
|
55
|
-
|
56
|
-
}
|
57
|
-
/**
|
58
|
-
* For more modularity, LevelDB instance is injected by user, so we need to provide a generic type for LevelDB.
|
59
|
-
* Every LevelDB implementation must implement this (minimized) interface to avoid errors.
|
60
|
-
*/
|
61
|
-
export interface LevelDBConstructor<K = any, V = any> {
|
62
|
-
new (path: string, options?: any): IAbstractLevel<K, V>;
|
63
|
-
}
|
64
|
-
export declare class IAbstractLevel<KDefault = string, VDefault = string> {
|
65
|
-
/**
|
66
|
-
* Open the database.
|
67
|
-
*/
|
68
|
-
open(): Promise<void>;
|
69
|
-
open(options: AbstractOpenOptions): Promise<void>;
|
70
|
-
/**
|
71
|
-
* Get a value from the database by {@link key}.
|
72
|
-
*/
|
73
|
-
get(key: KDefault): Promise<VDefault>;
|
74
|
-
get(key: KDefault, callback: (err: any, value: VDefault) => void): Promise<VDefault>;
|
75
|
-
/**
|
76
|
-
* Get multiple values from the database by an array of {@link keys}.
|
77
|
-
*/
|
78
|
-
getMany(keys: KDefault[]): Promise<VDefault[]>;
|
79
|
-
/**
|
80
|
-
* Add a new entry or overwrite an existing entry.
|
81
|
-
*/
|
82
|
-
put(key: KDefault, value: VDefault): Promise<void>;
|
83
|
-
/**
|
84
|
-
* Delete an entry by {@link key}.
|
85
|
-
*/
|
86
|
-
del(key: KDefault): Promise<void>;
|
87
|
-
/**
|
88
|
-
* Perform multiple _put_ and/or _del_ operations in bulk.
|
89
|
-
*/
|
90
|
-
batch(operations: Array<AbstractBatchOperation<typeof this, KDefault, VDefault>>): Promise<void>;
|
91
|
-
batch<K = KDefault, V = VDefault>(operations: Array<AbstractBatchOperation<typeof this, K, V>>, options: AbstractBatchOptions<K, V>): Promise<void>;
|
92
|
-
batch(): AbstractChainedBatch<typeof this, KDefault, VDefault>;
|
93
|
-
/**
|
94
|
-
* Create an iterator. For example:
|
95
|
-
*
|
96
|
-
* ```js
|
97
|
-
* for await (const [key, value] of db.iterator({ gte: 'a' })) {
|
98
|
-
* console.log([key, value])
|
99
|
-
* }
|
100
|
-
* ```
|
101
|
-
*/
|
102
|
-
iterator(): AbstractIterator<typeof this, KDefault, VDefault>;
|
103
|
-
iterator<K = KDefault, V = VDefault>(options: AbstractIteratorOptions<K, V>): AbstractIterator<typeof this, K, V>;
|
104
|
-
/**
|
105
|
-
* Create a key iterator. For example:
|
106
|
-
*
|
107
|
-
* ```js
|
108
|
-
* for await (const key of db.keys({ gte: 'a' })) {
|
109
|
-
* console.log(key)
|
110
|
-
* }
|
111
|
-
* ```
|
112
|
-
*/
|
113
|
-
keys(): AbstractKeyIterator<typeof this, KDefault>;
|
114
|
-
keys<K = KDefault>(options: AbstractKeyIteratorOptions<K>): AbstractKeyIterator<typeof this, K>;
|
115
|
-
/**
|
116
|
-
* Create a value iterator. For example:
|
117
|
-
*
|
118
|
-
* ```js
|
119
|
-
* for await (const value of db.values({ gte: 'a' })) {
|
120
|
-
* console.log(value)
|
121
|
-
* }
|
122
|
-
* ```
|
123
|
-
*/
|
124
|
-
values(): AbstractValueIterator<typeof this, KDefault, VDefault>;
|
125
|
-
values<K = KDefault, V = VDefault>(options: AbstractValueIteratorOptions<K, V>): AbstractValueIterator<typeof this, K, V>;
|
126
|
-
/**
|
127
|
-
* Delete all entries or a range.
|
128
|
-
*/
|
129
|
-
clear(): Promise<void>;
|
9
|
+
export interface IWatcherEvents {
|
10
|
+
log: (log: Log<any>) => void;
|
130
11
|
}
|
131
|
-
export {};
|