@voidifydao/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/config/command.d.ts +2 -0
- package/dist/cli/config/command.js +91 -0
- package/dist/cli/config/init.d.ts +3 -0
- package/dist/cli/config/init.js +88 -0
- package/dist/cli/config/keypair.d.ts +4 -0
- package/dist/cli/config/keypair.js +35 -0
- package/dist/cli/config/loader.d.ts +11 -0
- package/dist/cli/config/loader.js +65 -0
- package/dist/cli/config/types.d.ts +50 -0
- package/dist/cli/config/types.js +33 -0
- package/dist/cli/deposit.d.ts +2 -0
- package/dist/cli/deposit.js +58 -0
- package/dist/cli/helpers.d.ts +12 -0
- package/dist/cli/helpers.js +53 -0
- package/dist/cli/note.d.ts +2 -0
- package/dist/cli/note.js +50 -0
- package/dist/cli/relayer.d.ts +2 -0
- package/dist/cli/relayer.js +60 -0
- package/dist/cli/substream.d.ts +2 -0
- package/dist/cli/substream.js +35 -0
- package/dist/cli/withdraw.d.ts +2 -0
- package/dist/cli/withdraw.js +23 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +30 -0
- package/dist/context.d.ts +45 -0
- package/dist/context.js +77 -0
- package/dist/idl/voidify/idl.d.ts +1313 -0
- package/dist/idl/voidify/idl.js +1 -0
- package/dist/idl/voidify/idl.json +1307 -0
- package/dist/idl/voidify-staking/idl.d.ts +93 -0
- package/dist/idl/voidify-staking/idl.js +1 -0
- package/dist/idl/voidify-staking/idl.json +87 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +10 -0
- package/dist/relayer/server/index.d.ts +6 -0
- package/dist/relayer/server/index.js +32 -0
- package/dist/relayer/server/server.d.ts +24 -0
- package/dist/relayer/server/server.js +158 -0
- package/dist/relayer/server/switchboard.d.ts +2 -0
- package/dist/relayer/server/switchboard.js +42 -0
- package/dist/relayer/types.d.ts +21 -0
- package/dist/relayer/types.js +1 -0
- package/dist/staking/commands.d.ts +3 -0
- package/dist/staking/commands.js +13 -0
- package/dist/staking/index.d.ts +2 -0
- package/dist/staking/index.js +2 -0
- package/dist/staking/program.d.ts +18 -0
- package/dist/staking/program.js +40 -0
- package/dist/substream/chain/events.d.ts +4 -0
- package/dist/substream/chain/events.js +50 -0
- package/dist/substream/chain/index.d.ts +24 -0
- package/dist/substream/chain/index.js +79 -0
- package/dist/substream/chain/registry.d.ts +44 -0
- package/dist/substream/chain/registry.js +28 -0
- package/dist/substream/chain/utils.d.ts +9 -0
- package/dist/substream/chain/utils.js +41 -0
- package/dist/substream/client.d.ts +27 -0
- package/dist/substream/client.js +28 -0
- package/dist/substream/database/indexeddb.d.ts +2 -0
- package/dist/substream/database/indexeddb.js +242 -0
- package/dist/substream/database/sqlite.d.ts +26 -0
- package/dist/substream/database/sqlite.js +275 -0
- package/dist/substream/modules/deposit.d.ts +14 -0
- package/dist/substream/modules/deposit.js +123 -0
- package/dist/substream/modules/index.d.ts +11 -0
- package/dist/substream/modules/index.js +7 -0
- package/dist/substream/modules/relayer.d.ts +10 -0
- package/dist/substream/modules/relayer.js +290 -0
- package/dist/substream/runtime.d.ts +38 -0
- package/dist/substream/runtime.js +163 -0
- package/dist/substream/server/event-listener.d.ts +18 -0
- package/dist/substream/server/event-listener.js +68 -0
- package/dist/substream/server/index.d.ts +3 -0
- package/dist/substream/server/index.js +30 -0
- package/dist/substream/server/server.d.ts +43 -0
- package/dist/substream/server/server.js +216 -0
- package/dist/substream/types.d.ts +94 -0
- package/dist/substream/types.js +1 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/errors.js +16 -0
- package/dist/types/events.d.ts +13 -0
- package/dist/types/events.js +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/amount.d.ts +4 -0
- package/dist/utils/amount.js +41 -0
- package/dist/utils/anchor-events.d.ts +13 -0
- package/dist/utils/anchor-events.js +28 -0
- package/dist/utils/bytes.d.ts +10 -0
- package/dist/utils/bytes.js +29 -0
- package/dist/utils/idl-seed.d.ts +17 -0
- package/dist/utils/idl-seed.js +15 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.js +19 -0
- package/dist/utils/note.d.ts +19 -0
- package/dist/utils/note.js +83 -0
- package/dist/utils/proof.d.ts +11 -0
- package/dist/utils/proof.js +91 -0
- package/dist/utils/tx.d.ts +11 -0
- package/dist/utils/tx.js +62 -0
- package/dist/voidify/deposit.d.ts +10 -0
- package/dist/voidify/deposit.js +40 -0
- package/dist/voidify/index.d.ts +4 -0
- package/dist/voidify/index.js +4 -0
- package/dist/voidify/program.d.ts +36 -0
- package/dist/voidify/program.js +87 -0
- package/dist/voidify/relayer/index.d.ts +1 -0
- package/dist/voidify/relayer/index.js +1 -0
- package/dist/voidify/relayer/list.d.ts +5 -0
- package/dist/voidify/relayer/list.js +16 -0
- package/dist/voidify/withdraw.d.ts +16 -0
- package/dist/voidify/withdraw.js +188 -0
- package/package.json +79 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { normalizePayload } from "../../substream/chain/events.js";
|
|
3
|
+
import { parseEventsFromLogs, } from "../../utils/anchor-events.js";
|
|
4
|
+
import { substreamLogger } from "../../utils/logger.js";
|
|
5
|
+
import { syncTransactions } from "../../substream/chain/utils.js";
|
|
6
|
+
export const RELAYER_SCOPE = {
|
|
7
|
+
scopeType: "relayer",
|
|
8
|
+
scopeKey: "global",
|
|
9
|
+
};
|
|
10
|
+
const RELAYER_LIVE_EVENTS = [
|
|
11
|
+
"relayerRegisteredEvent",
|
|
12
|
+
"relayerUpdatedEvent",
|
|
13
|
+
"relayerActivatedEvent",
|
|
14
|
+
"relayerUnregisteredEvent",
|
|
15
|
+
"relayerDeactivatedEvent",
|
|
16
|
+
"relayerSlashedEvent",
|
|
17
|
+
"withdrawalEvent",
|
|
18
|
+
];
|
|
19
|
+
const INDEXED_RELAYER_EVENT_NAMES = new Set(RELAYER_LIVE_EVENTS);
|
|
20
|
+
export const relayerModule = {
|
|
21
|
+
id: "relayer",
|
|
22
|
+
scopeType: "relayer",
|
|
23
|
+
parseScopeKey(scopeKey) {
|
|
24
|
+
return scopeKey === RELAYER_SCOPE.scopeKey ? RELAYER_SCOPE : null;
|
|
25
|
+
},
|
|
26
|
+
createStream(ctx, voidify) {
|
|
27
|
+
return createRelayerStream(ctx, voidify);
|
|
28
|
+
},
|
|
29
|
+
liveEvents: RELAYER_LIVE_EVENTS.map((eventName) => ({
|
|
30
|
+
eventName,
|
|
31
|
+
async toRecord({ ctx, voidify, event, signature, slot }) {
|
|
32
|
+
const decoded = { name: eventName, data: event };
|
|
33
|
+
const pubkey = decoded.data.relayer.toBase58();
|
|
34
|
+
const cfg = decoded.name === "relayerRegisteredEvent"
|
|
35
|
+
? await fetchRelayerConfig(voidify, pubkey)
|
|
36
|
+
: null;
|
|
37
|
+
return relayerEventToChainEvent({
|
|
38
|
+
event: decoded,
|
|
39
|
+
signature,
|
|
40
|
+
slot,
|
|
41
|
+
address: ctx.programId,
|
|
42
|
+
config: cfg,
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
})),
|
|
46
|
+
createProjections(store) {
|
|
47
|
+
return [new RelayerProjection(store)];
|
|
48
|
+
},
|
|
49
|
+
createClientApi(runtime) {
|
|
50
|
+
return {
|
|
51
|
+
async sync() {
|
|
52
|
+
await runtime.sync(RELAYER_SCOPE);
|
|
53
|
+
},
|
|
54
|
+
async list() {
|
|
55
|
+
const rows = await runtime.projections.list("relayer");
|
|
56
|
+
return rows.map((row) => relayerRecordFromValue(row.value));
|
|
57
|
+
},
|
|
58
|
+
async get(pubkey) {
|
|
59
|
+
const row = await runtime.projections.get("relayer", pubkey);
|
|
60
|
+
return row ? relayerRecordFromValue(row.value) : null;
|
|
61
|
+
},
|
|
62
|
+
async rebuild() {
|
|
63
|
+
await runtime.rebuildProjection(RELAYER_SCOPE, "relayer");
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
function createRelayerStream(ctx, voidify) {
|
|
69
|
+
return {
|
|
70
|
+
id: "relayer",
|
|
71
|
+
scope: RELAYER_SCOPE,
|
|
72
|
+
address: ctx.programId,
|
|
73
|
+
getChainLastIndex: async () => {
|
|
74
|
+
const counterPda = voidify.relayerEventCounter();
|
|
75
|
+
const counter = await voidify.program.account.relayerEventCounter.fetch(counterPda);
|
|
76
|
+
return BigInt(counter.count.toString()) - 1n;
|
|
77
|
+
},
|
|
78
|
+
collect: async (lastSignature) => {
|
|
79
|
+
const transactions = await syncTransactions(ctx.connection, ctx.programId, lastSignature);
|
|
80
|
+
const records = [];
|
|
81
|
+
for (const tx of transactions) {
|
|
82
|
+
const decoded = parseEventsFromLogs(tx.logs, voidify.program.coder.events);
|
|
83
|
+
for (const item of decoded) {
|
|
84
|
+
if (!isIndexedRelayerEventName(item.name))
|
|
85
|
+
continue;
|
|
86
|
+
const event = item;
|
|
87
|
+
const pubkey = event.data.relayer.toBase58();
|
|
88
|
+
const cfg = event.name === "relayerRegisteredEvent"
|
|
89
|
+
? await fetchRelayerConfig(voidify, pubkey)
|
|
90
|
+
: null;
|
|
91
|
+
records.push(relayerEventToChainEvent({
|
|
92
|
+
event,
|
|
93
|
+
signature: tx.signature,
|
|
94
|
+
slot: tx.slot,
|
|
95
|
+
blockTime: tx.blockTime,
|
|
96
|
+
address: ctx.programId,
|
|
97
|
+
config: cfg,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return records;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function relayerEventToChainEvent(args) {
|
|
106
|
+
const payload = normalizePayload(args.event.data);
|
|
107
|
+
if (args.event.name === "relayerRegisteredEvent") {
|
|
108
|
+
payload.config = args.config ?? { name: "", url: "", feeBps: 0 };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
...RELAYER_SCOPE,
|
|
112
|
+
eventName: args.event.name,
|
|
113
|
+
eventIndex: BigInt(String(payload.index)),
|
|
114
|
+
signature: args.signature,
|
|
115
|
+
slot: args.slot ?? null,
|
|
116
|
+
blockTime: args.blockTime ?? null,
|
|
117
|
+
address: args.address.toBase58(),
|
|
118
|
+
payload,
|
|
119
|
+
createdAt: Date.now(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function isIndexedRelayerEventName(name) {
|
|
123
|
+
return INDEXED_RELAYER_EVENT_NAMES.has(name);
|
|
124
|
+
}
|
|
125
|
+
async function fetchRelayerConfig(voidify, relayerPubkey) {
|
|
126
|
+
try {
|
|
127
|
+
const cfgKey = voidify.relayerConfig(new PublicKey(relayerPubkey));
|
|
128
|
+
const cfg = await voidify.program.account.relayerConfig.fetch(cfgKey);
|
|
129
|
+
return { name: cfg.name, url: cfg.url, feeBps: cfg.feeBps };
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
substreamLogger.warn({ err, relayerPubkey }, "fetchRelayerConfig failed");
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
class RelayerProjection {
|
|
137
|
+
store;
|
|
138
|
+
id = "relayer";
|
|
139
|
+
constructor(store) {
|
|
140
|
+
this.store = store;
|
|
141
|
+
}
|
|
142
|
+
matches(event) {
|
|
143
|
+
return event.scopeType === "relayer";
|
|
144
|
+
}
|
|
145
|
+
async apply(events) {
|
|
146
|
+
const sorted = [...events].sort((a, b) => a.eventIndex < b.eventIndex ? -1 : a.eventIndex > b.eventIndex ? 1 : 0);
|
|
147
|
+
for (const event of sorted) {
|
|
148
|
+
const pubkey = relayerPubkeyFromEvent(event);
|
|
149
|
+
if (!pubkey)
|
|
150
|
+
continue;
|
|
151
|
+
const existing = await this.store.get(this.id, pubkey);
|
|
152
|
+
const current = existing ? valueToRelayerState(existing.value) : null;
|
|
153
|
+
const next = reduceRelayerEvent(current, event);
|
|
154
|
+
if (!next)
|
|
155
|
+
continue;
|
|
156
|
+
await this.store.put({
|
|
157
|
+
projectionId: this.id,
|
|
158
|
+
key: pubkey,
|
|
159
|
+
value: relayerStateToValue(next),
|
|
160
|
+
updatedAt: next.lastUpdated,
|
|
161
|
+
lastEventIndex: next.lastEventIndex,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function defaultRelayerRecord(pubkey) {
|
|
167
|
+
return {
|
|
168
|
+
relayerPubkey: pubkey,
|
|
169
|
+
name: "",
|
|
170
|
+
url: "",
|
|
171
|
+
feeBps: 0,
|
|
172
|
+
stakeAmount: 0n,
|
|
173
|
+
isActive: true,
|
|
174
|
+
totalWithdrawals: 0,
|
|
175
|
+
totalSolEarned: 0n,
|
|
176
|
+
totalTokenDeducted: 0n,
|
|
177
|
+
lastUpdated: Date.now(),
|
|
178
|
+
lastEventIndex: -1n,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function reduceRelayerEvent(existing, event) {
|
|
182
|
+
const pubkey = relayerPubkeyFromEvent(event);
|
|
183
|
+
if (!pubkey)
|
|
184
|
+
return existing;
|
|
185
|
+
const record = existing ? { ...existing } : defaultRelayerRecord(pubkey);
|
|
186
|
+
if (event.eventIndex <= record.lastEventIndex)
|
|
187
|
+
return record;
|
|
188
|
+
switch (event.eventName) {
|
|
189
|
+
case "relayerRegisteredEvent": {
|
|
190
|
+
record.stakeAmount = payloadBigInt(event, "stakeAmount");
|
|
191
|
+
record.isActive = true;
|
|
192
|
+
const cfg = event.payload.config;
|
|
193
|
+
record.name = String(cfg?.name ?? "");
|
|
194
|
+
record.url = String(cfg?.url ?? "");
|
|
195
|
+
record.feeBps = Number(cfg?.feeBps ?? 0);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case "relayerDeactivatedEvent": {
|
|
199
|
+
record.stakeAmount = payloadBigInt(event, "remainingStake");
|
|
200
|
+
record.isActive = false;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "relayerSlashedEvent":
|
|
204
|
+
case "relayerUnregisteredEvent": {
|
|
205
|
+
record.stakeAmount = 0n;
|
|
206
|
+
record.isActive = false;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "relayerUpdatedEvent": {
|
|
210
|
+
if (event.payload.feeBps !== null && event.payload.feeBps !== undefined) {
|
|
211
|
+
record.feeBps = Number(event.payload.feeBps);
|
|
212
|
+
}
|
|
213
|
+
if (event.payload.url !== null && event.payload.url !== undefined) {
|
|
214
|
+
record.url = String(event.payload.url);
|
|
215
|
+
}
|
|
216
|
+
if (event.payload.addedAmount !== null &&
|
|
217
|
+
event.payload.addedAmount !== undefined) {
|
|
218
|
+
record.stakeAmount += payloadBigInt(event, "addedAmount");
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "relayerActivatedEvent": {
|
|
223
|
+
record.stakeAmount = payloadBigInt(event, "stakeAmount");
|
|
224
|
+
record.isActive = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case "withdrawalEvent": {
|
|
228
|
+
const fee = payloadBigInt(event, "fee");
|
|
229
|
+
const treasury = payloadBigInt(event, "treasury");
|
|
230
|
+
const tokenDeducted = payloadBigInt(event, "tokenDeducted");
|
|
231
|
+
record.totalWithdrawals += 1;
|
|
232
|
+
record.totalSolEarned += fee + treasury;
|
|
233
|
+
record.totalTokenDeducted += tokenDeducted;
|
|
234
|
+
record.stakeAmount =
|
|
235
|
+
record.stakeAmount >= tokenDeducted
|
|
236
|
+
? record.stakeAmount - tokenDeducted
|
|
237
|
+
: 0n;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
default:
|
|
241
|
+
return existing;
|
|
242
|
+
}
|
|
243
|
+
record.lastEventIndex = event.eventIndex;
|
|
244
|
+
record.lastUpdated = Date.now();
|
|
245
|
+
return record;
|
|
246
|
+
}
|
|
247
|
+
function relayerPubkeyFromEvent(event) {
|
|
248
|
+
const value = event.payload.relayer;
|
|
249
|
+
return value === null || value === undefined ? null : String(value);
|
|
250
|
+
}
|
|
251
|
+
function payloadBigInt(event, key) {
|
|
252
|
+
const value = event.payload[key];
|
|
253
|
+
if (value === null || value === undefined)
|
|
254
|
+
return 0n;
|
|
255
|
+
return BigInt(String(value));
|
|
256
|
+
}
|
|
257
|
+
function relayerStateToValue(state) {
|
|
258
|
+
return {
|
|
259
|
+
relayerPubkey: state.relayerPubkey,
|
|
260
|
+
name: state.name,
|
|
261
|
+
url: state.url,
|
|
262
|
+
feeBps: state.feeBps,
|
|
263
|
+
stakeAmount: state.stakeAmount.toString(),
|
|
264
|
+
isActive: state.isActive,
|
|
265
|
+
totalWithdrawals: state.totalWithdrawals,
|
|
266
|
+
totalSolEarned: state.totalSolEarned.toString(),
|
|
267
|
+
totalTokenDeducted: state.totalTokenDeducted.toString(),
|
|
268
|
+
lastUpdated: state.lastUpdated,
|
|
269
|
+
lastEventIndex: state.lastEventIndex.toString(),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function valueToRelayerState(value) {
|
|
273
|
+
return {
|
|
274
|
+
relayerPubkey: String(value.relayerPubkey),
|
|
275
|
+
name: String(value.name ?? ""),
|
|
276
|
+
url: String(value.url ?? ""),
|
|
277
|
+
feeBps: Number(value.feeBps ?? 0),
|
|
278
|
+
stakeAmount: BigInt(String(value.stakeAmount ?? "0")),
|
|
279
|
+
isActive: Boolean(value.isActive),
|
|
280
|
+
totalWithdrawals: Number(value.totalWithdrawals ?? 0),
|
|
281
|
+
totalSolEarned: BigInt(String(value.totalSolEarned ?? "0")),
|
|
282
|
+
totalTokenDeducted: BigInt(String(value.totalTokenDeducted ?? "0")),
|
|
283
|
+
lastUpdated: Number(value.lastUpdated ?? Date.now()),
|
|
284
|
+
lastEventIndex: BigInt(String(value.lastEventIndex ?? "-1")),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function relayerRecordFromValue(value) {
|
|
288
|
+
const { lastEventIndex: _lastEventIndex, ...record } = valueToRelayerState(value);
|
|
289
|
+
return record;
|
|
290
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Context, SubstreamMode } from "../context.js";
|
|
2
|
+
import type { EventScope, EventStore, ProjectionStore, SubstreamStores } from "../substream/types.js";
|
|
3
|
+
import { ChainEventSyncer, ProjectionRegistry } from "../substream/chain/index.js";
|
|
4
|
+
import { SubstreamModuleRegistry, type SubstreamModule, type SubstreamModuleRuntime } from "../substream/chain/registry.js";
|
|
5
|
+
import { VoidifyProgram } from "../voidify/program.js";
|
|
6
|
+
export interface SubstreamRuntimeConfig {
|
|
7
|
+
mode?: SubstreamMode;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
healthCacheMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class SubstreamRuntime implements SubstreamModuleRuntime {
|
|
12
|
+
readonly ctx: Context;
|
|
13
|
+
readonly events: EventStore;
|
|
14
|
+
readonly projections: ProjectionStore;
|
|
15
|
+
readonly registry: SubstreamModuleRegistry;
|
|
16
|
+
readonly projectionRegistry: ProjectionRegistry;
|
|
17
|
+
readonly syncer: ChainEventSyncer;
|
|
18
|
+
readonly voidify: VoidifyProgram;
|
|
19
|
+
private readonly mode;
|
|
20
|
+
private readonly timeout;
|
|
21
|
+
private readonly healthCacheMs;
|
|
22
|
+
private healthState;
|
|
23
|
+
private initialized;
|
|
24
|
+
constructor(ctx: Context, stores: SubstreamStores, config?: SubstreamRuntimeConfig, modules?: readonly SubstreamModule[]);
|
|
25
|
+
initialize(): Promise<void>;
|
|
26
|
+
module(id: string): unknown;
|
|
27
|
+
sync(scope: EventScope): Promise<void>;
|
|
28
|
+
syncLocal(scope: EventScope): Promise<void>;
|
|
29
|
+
applyLiveEvent(eventScope: EventScope): Promise<void>;
|
|
30
|
+
applyLiveRecord(scope: EventScope, record: Parameters<ChainEventSyncer["applyLiveEvent"]>[1]): Promise<void>;
|
|
31
|
+
rebuildProjection(scope: EventScope, projectionId: string): Promise<void>;
|
|
32
|
+
private get baseUrl();
|
|
33
|
+
private resolveMode;
|
|
34
|
+
private healthCheck;
|
|
35
|
+
private syncRemote;
|
|
36
|
+
private fetchRemoteEvents;
|
|
37
|
+
}
|
|
38
|
+
export declare function createSubstreamRuntime(ctx: Context, stores: SubstreamStores, config?: SubstreamRuntimeConfig, modules?: readonly SubstreamModule[]): SubstreamRuntime;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { ChainEventSyncer, ProjectionRegistry, } from "../substream/chain/index.js";
|
|
2
|
+
import { chainEventFromWire } from "../substream/chain/events.js";
|
|
3
|
+
import { SubstreamModuleRegistry, } from "../substream/chain/registry.js";
|
|
4
|
+
import { createSubstreamRegistry } from "../substream/modules/index.js";
|
|
5
|
+
import { VoidifyProgram } from "../voidify/program.js";
|
|
6
|
+
export class SubstreamRuntime {
|
|
7
|
+
ctx;
|
|
8
|
+
events;
|
|
9
|
+
projections;
|
|
10
|
+
registry;
|
|
11
|
+
projectionRegistry;
|
|
12
|
+
syncer;
|
|
13
|
+
voidify;
|
|
14
|
+
mode;
|
|
15
|
+
timeout;
|
|
16
|
+
healthCacheMs;
|
|
17
|
+
healthState = null;
|
|
18
|
+
initialized = false;
|
|
19
|
+
constructor(ctx, stores, config, modules) {
|
|
20
|
+
this.ctx = ctx;
|
|
21
|
+
this.events = stores.events;
|
|
22
|
+
this.projections = stores.projections;
|
|
23
|
+
this.mode = config?.mode ?? ctx.substream.type;
|
|
24
|
+
this.timeout = config?.timeout ?? 10000;
|
|
25
|
+
this.healthCacheMs = config?.healthCacheMs ?? 10000;
|
|
26
|
+
this.registry = modules
|
|
27
|
+
? new SubstreamModuleRegistry([...modules])
|
|
28
|
+
: createSubstreamRegistry();
|
|
29
|
+
this.projectionRegistry = new ProjectionRegistry();
|
|
30
|
+
for (const projection of this.registry.createProjections(this.projections)) {
|
|
31
|
+
this.projectionRegistry.register(projection);
|
|
32
|
+
}
|
|
33
|
+
this.syncer = new ChainEventSyncer(this.events, this.projectionRegistry);
|
|
34
|
+
this.voidify = new VoidifyProgram(ctx.connection, ctx.programId);
|
|
35
|
+
}
|
|
36
|
+
async initialize() {
|
|
37
|
+
if (this.initialized)
|
|
38
|
+
return;
|
|
39
|
+
await this.events.initialize();
|
|
40
|
+
await this.projections.initialize();
|
|
41
|
+
this.initialized = true;
|
|
42
|
+
}
|
|
43
|
+
module(id) {
|
|
44
|
+
const module = this.registry.getById(id);
|
|
45
|
+
if (!module?.createClientApi) {
|
|
46
|
+
throw new Error(`Unknown substream module: ${id}`);
|
|
47
|
+
}
|
|
48
|
+
return module.createClientApi(this);
|
|
49
|
+
}
|
|
50
|
+
async sync(scope) {
|
|
51
|
+
const mode = await this.resolveMode();
|
|
52
|
+
if (mode === "local") {
|
|
53
|
+
await this.syncLocal(scope);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await this.syncRemote(scope);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (this.mode === "auto") {
|
|
61
|
+
this.healthState = { ok: false, checkedAt: Date.now() };
|
|
62
|
+
await this.syncLocal(scope);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
throw new Error(`Failed to sync substream scope ${scope.scopeType}:${scope.scopeKey}: ${message}`, {
|
|
67
|
+
cause: error,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async syncLocal(scope) {
|
|
72
|
+
await this.syncer.checkAndSync(this.registry.createStream(this.ctx, this.voidify, scope));
|
|
73
|
+
}
|
|
74
|
+
async applyLiveEvent(eventScope) {
|
|
75
|
+
await this.syncLocal(eventScope);
|
|
76
|
+
}
|
|
77
|
+
async applyLiveRecord(scope, record) {
|
|
78
|
+
await this.syncer.applyLiveEvent(this.registry.createStream(this.ctx, this.voidify, scope), record);
|
|
79
|
+
}
|
|
80
|
+
async rebuildProjection(scope, projectionId) {
|
|
81
|
+
const projection = this.projectionRegistry.get(projectionId);
|
|
82
|
+
if (!projection) {
|
|
83
|
+
throw new Error(`Unknown substream projection: ${projectionId}`);
|
|
84
|
+
}
|
|
85
|
+
await this.projections.clear(projectionId);
|
|
86
|
+
const events = await this.events.getAfter(scope);
|
|
87
|
+
await projection.apply(events.filter((event) => projection.matches(event)));
|
|
88
|
+
}
|
|
89
|
+
get baseUrl() {
|
|
90
|
+
const s = this.ctx.substream;
|
|
91
|
+
if (s.type === "local") {
|
|
92
|
+
throw new Error("baseUrl unavailable in local-only substream mode");
|
|
93
|
+
}
|
|
94
|
+
return s.url.replace(/\/$/, "");
|
|
95
|
+
}
|
|
96
|
+
async resolveMode() {
|
|
97
|
+
if (this.mode === "remote")
|
|
98
|
+
return "remote";
|
|
99
|
+
if (this.mode === "local")
|
|
100
|
+
return "local";
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
if (this.healthState &&
|
|
103
|
+
now - this.healthState.checkedAt < this.healthCacheMs) {
|
|
104
|
+
return this.healthState.ok ? "remote" : "local";
|
|
105
|
+
}
|
|
106
|
+
const ok = await this.healthCheck();
|
|
107
|
+
const wasOk = this.healthState?.ok;
|
|
108
|
+
this.healthState = { ok, checkedAt: now };
|
|
109
|
+
if (!ok && wasOk !== false) {
|
|
110
|
+
console.log(`substream server unreachable at ${this.baseUrl}, falling back to local`);
|
|
111
|
+
}
|
|
112
|
+
else if (ok && wasOk === false) {
|
|
113
|
+
console.log(`substream server reachable at ${this.baseUrl}, using remote`);
|
|
114
|
+
}
|
|
115
|
+
return ok ? "remote" : "local";
|
|
116
|
+
}
|
|
117
|
+
async healthCheck() {
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(`${this.baseUrl}/health`, {
|
|
120
|
+
method: "GET",
|
|
121
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
122
|
+
});
|
|
123
|
+
return response.ok;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async syncRemote(scope) {
|
|
130
|
+
const cursor = await this.events.getCursor(scope);
|
|
131
|
+
const afterIndex = cursor?.lastIndex ?? null;
|
|
132
|
+
const events = await this.fetchRemoteEvents(scope, afterIndex);
|
|
133
|
+
const newEvents = events.filter((event) => afterIndex === null || event.eventIndex > afterIndex);
|
|
134
|
+
if (newEvents.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
const outcome = await this.events.applyBatch(scope, newEvents);
|
|
137
|
+
if (outcome.kind === "gap") {
|
|
138
|
+
throw new Error(`Remote events have a gap: expected ${outcome.expected.toString()}, got ${outcome.got.toString()}`);
|
|
139
|
+
}
|
|
140
|
+
await this.projectionRegistry.apply(newEvents);
|
|
141
|
+
}
|
|
142
|
+
async fetchRemoteEvents(scope, afterIndex) {
|
|
143
|
+
const params = new URLSearchParams();
|
|
144
|
+
if (afterIndex !== null && afterIndex !== undefined) {
|
|
145
|
+
params.set("after_index", afterIndex.toString());
|
|
146
|
+
}
|
|
147
|
+
const query = params.toString();
|
|
148
|
+
const url = `${this.baseUrl}/api/events/${scope.scopeType}/${scope.scopeKey}` +
|
|
149
|
+
(query ? `?${query}` : "");
|
|
150
|
+
const response = await fetch(url, {
|
|
151
|
+
method: "GET",
|
|
152
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
153
|
+
});
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw new Error(`Failed to fetch events: ${response.statusText}`);
|
|
156
|
+
}
|
|
157
|
+
const data = (await response.json());
|
|
158
|
+
return data.events.map(chainEventFromWire);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export function createSubstreamRuntime(ctx, stores, config, modules) {
|
|
162
|
+
return new SubstreamRuntime(ctx, stores, config, modules);
|
|
163
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Program } from "@coral-xyz/anchor";
|
|
2
|
+
import type { Voidify } from "../../idl/voidify/idl.js";
|
|
3
|
+
export type EventCallback<T = any> = (event: T, signature: string, slot: number) => Promise<void>;
|
|
4
|
+
export declare class EventListener {
|
|
5
|
+
private program;
|
|
6
|
+
private handlers;
|
|
7
|
+
private anchorListeners;
|
|
8
|
+
private nextListenerId;
|
|
9
|
+
constructor(program: Program<Voidify>);
|
|
10
|
+
registerHandler<T = any>(eventName: string, handler: EventCallback<T>): number;
|
|
11
|
+
unregisterHandler(_listenerId: number): void;
|
|
12
|
+
unregisterEvent(eventName: string): void;
|
|
13
|
+
removeAllListeners(): void;
|
|
14
|
+
getListenerCount(): number;
|
|
15
|
+
getRegisteredEvents(): string[];
|
|
16
|
+
private hasAnchorListener;
|
|
17
|
+
private createAnchorListener;
|
|
18
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { substreamLogger as logger } from "../../utils/logger.js";
|
|
2
|
+
export class EventListener {
|
|
3
|
+
program;
|
|
4
|
+
handlers = new Map();
|
|
5
|
+
anchorListeners = new Map();
|
|
6
|
+
nextListenerId = 1;
|
|
7
|
+
constructor(program) {
|
|
8
|
+
this.program = program;
|
|
9
|
+
}
|
|
10
|
+
registerHandler(eventName, handler) {
|
|
11
|
+
const listenerId = this.nextListenerId++;
|
|
12
|
+
if (!this.handlers.has(eventName)) {
|
|
13
|
+
this.handlers.set(eventName, []);
|
|
14
|
+
}
|
|
15
|
+
this.handlers.get(eventName).push(handler);
|
|
16
|
+
if (!this.hasAnchorListener(eventName)) {
|
|
17
|
+
this.createAnchorListener(eventName);
|
|
18
|
+
}
|
|
19
|
+
return listenerId;
|
|
20
|
+
}
|
|
21
|
+
unregisterHandler(_listenerId) { }
|
|
22
|
+
unregisterEvent(eventName) {
|
|
23
|
+
this.handlers.delete(eventName);
|
|
24
|
+
for (const [anchorId, name] of this.anchorListeners.entries()) {
|
|
25
|
+
if (name === eventName) {
|
|
26
|
+
this.program.removeEventListener(anchorId);
|
|
27
|
+
this.anchorListeners.delete(anchorId);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
removeAllListeners() {
|
|
33
|
+
for (const anchorId of this.anchorListeners.keys()) {
|
|
34
|
+
this.program.removeEventListener(anchorId);
|
|
35
|
+
}
|
|
36
|
+
this.anchorListeners.clear();
|
|
37
|
+
this.handlers.clear();
|
|
38
|
+
}
|
|
39
|
+
getListenerCount() {
|
|
40
|
+
return this.anchorListeners.size;
|
|
41
|
+
}
|
|
42
|
+
getRegisteredEvents() {
|
|
43
|
+
return Array.from(this.handlers.keys());
|
|
44
|
+
}
|
|
45
|
+
hasAnchorListener(eventName) {
|
|
46
|
+
for (const [, name] of this.anchorListeners.entries()) {
|
|
47
|
+
if (name === eventName) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
createAnchorListener(eventName) {
|
|
54
|
+
const anchorListenerId = this.program.addEventListener(eventName, async (event, slot, signature) => {
|
|
55
|
+
logger.info({ eventName, slot, signature }, "event received");
|
|
56
|
+
try {
|
|
57
|
+
const handlers = this.handlers.get(eventName);
|
|
58
|
+
if (handlers && handlers.length > 0) {
|
|
59
|
+
await Promise.all(handlers.map((handler) => handler(event, signature, slot)));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logger.error({ err: error, eventName, slot, signature }, "event handler failed");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
this.anchorListeners.set(anchorListenerId, eventName);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SubstreamService, } from "../../substream/server/server.js";
|
|
2
|
+
import { substreamLogger as logger } from "../../utils/logger.js";
|
|
3
|
+
export async function startSubstream(ctx, options) {
|
|
4
|
+
process.on("unhandledRejection", (reason) => {
|
|
5
|
+
logger.error({ reason }, "unhandledRejection");
|
|
6
|
+
});
|
|
7
|
+
process.on("uncaughtException", (err) => {
|
|
8
|
+
logger.error({ err }, "uncaughtException");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
logger.info({
|
|
12
|
+
port: options.port,
|
|
13
|
+
dbPath: options.dbPath,
|
|
14
|
+
}, "Starting Substream service");
|
|
15
|
+
const service = new SubstreamService(ctx, options);
|
|
16
|
+
await service.start();
|
|
17
|
+
const shutdown = async () => {
|
|
18
|
+
logger.info("Received shutdown signal, stopping Substream service");
|
|
19
|
+
try {
|
|
20
|
+
await service.stop();
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
logger.error({ err }, "substream stop failed");
|
|
24
|
+
}
|
|
25
|
+
process.exit(0);
|
|
26
|
+
};
|
|
27
|
+
process.on("SIGINT", shutdown);
|
|
28
|
+
process.on("SIGTERM", shutdown);
|
|
29
|
+
await new Promise(() => { });
|
|
30
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Context } from "../../context.js";
|
|
2
|
+
import { type SubstreamRuntime } from "../../substream/runtime.js";
|
|
3
|
+
export interface HttpServerConfig {
|
|
4
|
+
port: number;
|
|
5
|
+
host?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SubstreamServiceOptions {
|
|
8
|
+
port: number;
|
|
9
|
+
host?: string;
|
|
10
|
+
dbPath: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class HttpServer {
|
|
13
|
+
private runtime;
|
|
14
|
+
private app;
|
|
15
|
+
private server;
|
|
16
|
+
private readonly host;
|
|
17
|
+
private readonly port;
|
|
18
|
+
constructor(runtime: SubstreamRuntime, config: HttpServerConfig);
|
|
19
|
+
private setupMiddleware;
|
|
20
|
+
private setupRoutes;
|
|
21
|
+
private handleHealth;
|
|
22
|
+
private handleGetEvents;
|
|
23
|
+
private parseScope;
|
|
24
|
+
private parseAfterIndex;
|
|
25
|
+
start(): Promise<void>;
|
|
26
|
+
stop(): Promise<void>;
|
|
27
|
+
getAddress(): string;
|
|
28
|
+
}
|
|
29
|
+
export declare class SubstreamService {
|
|
30
|
+
private ctx;
|
|
31
|
+
private options;
|
|
32
|
+
private database;
|
|
33
|
+
private eventListener;
|
|
34
|
+
private httpServer;
|
|
35
|
+
private program;
|
|
36
|
+
private runtime;
|
|
37
|
+
private isRunning;
|
|
38
|
+
constructor(ctx: Context, options: SubstreamServiceOptions);
|
|
39
|
+
start(): Promise<void>;
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
private startEventListeners;
|
|
42
|
+
private cleanup;
|
|
43
|
+
}
|