@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,24 @@
|
|
|
1
|
+
import type { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import type { ChainEventRecord, EventProjection, EventScope, EventStore } from "../../substream/types.js";
|
|
3
|
+
export interface EventStreamSpec {
|
|
4
|
+
id: string;
|
|
5
|
+
scope: EventScope;
|
|
6
|
+
address: PublicKey;
|
|
7
|
+
getChainLastIndex(): Promise<bigint>;
|
|
8
|
+
collect(lastSignature?: string): Promise<ChainEventRecord[]>;
|
|
9
|
+
}
|
|
10
|
+
export declare class ProjectionRegistry {
|
|
11
|
+
private projections;
|
|
12
|
+
register(projection: EventProjection): void;
|
|
13
|
+
apply(events: ChainEventRecord[]): Promise<void>;
|
|
14
|
+
get(id: string): EventProjection | null;
|
|
15
|
+
}
|
|
16
|
+
export declare class ChainEventSyncer {
|
|
17
|
+
private events;
|
|
18
|
+
private projections;
|
|
19
|
+
constructor(events: EventStore, projections?: ProjectionRegistry);
|
|
20
|
+
registerProjection(projection: EventProjection): void;
|
|
21
|
+
checkAndSync(spec: EventStreamSpec): Promise<boolean>;
|
|
22
|
+
applyLiveEvent(spec: EventStreamSpec, event: ChainEventRecord): Promise<void>;
|
|
23
|
+
private sync;
|
|
24
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { substreamLogger } from "../../utils/logger.js";
|
|
2
|
+
export class ProjectionRegistry {
|
|
3
|
+
projections = [];
|
|
4
|
+
register(projection) {
|
|
5
|
+
const existing = this.projections.find((p) => p.id === projection.id);
|
|
6
|
+
if (existing) {
|
|
7
|
+
throw new Error(`Projection already registered: ${projection.id}`);
|
|
8
|
+
}
|
|
9
|
+
this.projections.push(projection);
|
|
10
|
+
}
|
|
11
|
+
async apply(events) {
|
|
12
|
+
if (events.length === 0)
|
|
13
|
+
return;
|
|
14
|
+
for (const projection of this.projections) {
|
|
15
|
+
const matched = events.filter((event) => projection.matches(event));
|
|
16
|
+
if (matched.length > 0)
|
|
17
|
+
await projection.apply(matched);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
get(id) {
|
|
21
|
+
return this.projections.find((projection) => projection.id === id) ?? null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class ChainEventSyncer {
|
|
25
|
+
events;
|
|
26
|
+
projections;
|
|
27
|
+
constructor(events, projections = new ProjectionRegistry()) {
|
|
28
|
+
this.events = events;
|
|
29
|
+
this.projections = projections;
|
|
30
|
+
}
|
|
31
|
+
registerProjection(projection) {
|
|
32
|
+
this.projections.register(projection);
|
|
33
|
+
}
|
|
34
|
+
async checkAndSync(spec) {
|
|
35
|
+
const cursor = await this.events.getCursor(spec.scope);
|
|
36
|
+
const localIndex = cursor?.lastIndex ?? -1n;
|
|
37
|
+
let chainLastIndex;
|
|
38
|
+
try {
|
|
39
|
+
chainLastIndex = await spec.getChainLastIndex();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
substreamLogger.warn({ err: error, streamId: spec.id }, "substream alignment check failed");
|
|
43
|
+
await this.sync(spec);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (localIndex >= chainLastIndex)
|
|
47
|
+
return true;
|
|
48
|
+
await this.sync(spec);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
async applyLiveEvent(spec, event) {
|
|
52
|
+
const outcome = await this.events.apply(spec.scope, event);
|
|
53
|
+
if (outcome.kind === "gap") {
|
|
54
|
+
await this.sync(spec);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (outcome.kind === "applied") {
|
|
58
|
+
await this.projections.apply([event]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async sync(spec) {
|
|
62
|
+
const cursor = await this.events.getCursor(spec.scope);
|
|
63
|
+
const records = await spec.collect(cursor?.lastSignature ?? undefined);
|
|
64
|
+
if (records.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
const outcome = await this.events.applyBatch(spec.scope, records);
|
|
67
|
+
if (outcome.kind === "gap") {
|
|
68
|
+
substreamLogger.warn({
|
|
69
|
+
streamId: spec.id,
|
|
70
|
+
scopeType: spec.scope.scopeType,
|
|
71
|
+
scopeKey: spec.scope.scopeKey,
|
|
72
|
+
expected: outcome.expected.toString(),
|
|
73
|
+
got: outcome.got.toString(),
|
|
74
|
+
}, "chain event batch apply found a gap; RPC history window may be exhausted");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await this.projections.apply(records);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Context } from "../../context.js";
|
|
2
|
+
import type { ChainEventRecord, EventProjection, EventScope, EventScopeType, EventStore, ProjectionStore } from "../../substream/types.js";
|
|
3
|
+
import type { VoidifyProgram } from "../../voidify/program.js";
|
|
4
|
+
import type { EventStreamSpec } from "./index.js";
|
|
5
|
+
export interface LiveEventContext {
|
|
6
|
+
ctx: Context;
|
|
7
|
+
voidify: VoidifyProgram;
|
|
8
|
+
event: unknown;
|
|
9
|
+
signature: string;
|
|
10
|
+
slot: number;
|
|
11
|
+
}
|
|
12
|
+
export interface LiveEventAdapter {
|
|
13
|
+
eventName: string;
|
|
14
|
+
toRecord(args: LiveEventContext): Promise<ChainEventRecord>;
|
|
15
|
+
}
|
|
16
|
+
export interface SubstreamModuleRuntime {
|
|
17
|
+
ctx: Context;
|
|
18
|
+
voidify: VoidifyProgram;
|
|
19
|
+
events: EventStore;
|
|
20
|
+
projections: ProjectionStore;
|
|
21
|
+
sync(scope: EventScope): Promise<void>;
|
|
22
|
+
rebuildProjection(scope: EventScope, projectionId: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface SubstreamModule {
|
|
25
|
+
id: string;
|
|
26
|
+
scopeType: EventScopeType;
|
|
27
|
+
parseScopeKey(scopeKey: string): EventScope | null;
|
|
28
|
+
createStream(ctx: Context, voidify: VoidifyProgram, scope: EventScope): EventStreamSpec;
|
|
29
|
+
liveEvents?: LiveEventAdapter[];
|
|
30
|
+
createProjections?(store: ProjectionStore): EventProjection[];
|
|
31
|
+
createClientApi?(runtime: SubstreamModuleRuntime): unknown;
|
|
32
|
+
}
|
|
33
|
+
export declare class SubstreamModuleRegistry {
|
|
34
|
+
private modules;
|
|
35
|
+
constructor(modules: SubstreamModule[]);
|
|
36
|
+
getByScopeType(scopeType: string): SubstreamModule | null;
|
|
37
|
+
getById(id: string): SubstreamModule | null;
|
|
38
|
+
parseScope(scopeType: string, scopeKey: string): EventScope | null;
|
|
39
|
+
createStream(ctx: Context, voidify: VoidifyProgram, scope: EventScope): EventStreamSpec;
|
|
40
|
+
liveEvents(): Array<LiveEventAdapter & {
|
|
41
|
+
module: SubstreamModule;
|
|
42
|
+
}>;
|
|
43
|
+
createProjections(store: ProjectionStore): EventProjection[];
|
|
44
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class SubstreamModuleRegistry {
|
|
2
|
+
modules;
|
|
3
|
+
constructor(modules) {
|
|
4
|
+
this.modules = modules;
|
|
5
|
+
}
|
|
6
|
+
getByScopeType(scopeType) {
|
|
7
|
+
return this.modules.find((m) => m.scopeType === scopeType) ?? null;
|
|
8
|
+
}
|
|
9
|
+
getById(id) {
|
|
10
|
+
return this.modules.find((m) => m.id === id) ?? null;
|
|
11
|
+
}
|
|
12
|
+
parseScope(scopeType, scopeKey) {
|
|
13
|
+
return this.getByScopeType(scopeType)?.parseScopeKey(scopeKey) ?? null;
|
|
14
|
+
}
|
|
15
|
+
createStream(ctx, voidify, scope) {
|
|
16
|
+
const module = this.getByScopeType(scope.scopeType);
|
|
17
|
+
if (!module) {
|
|
18
|
+
throw new Error(`No substream module registered for ${scope.scopeType}`);
|
|
19
|
+
}
|
|
20
|
+
return module.createStream(ctx, voidify, scope);
|
|
21
|
+
}
|
|
22
|
+
liveEvents() {
|
|
23
|
+
return this.modules.flatMap((module) => (module.liveEvents ?? []).map((event) => ({ ...event, module })));
|
|
24
|
+
}
|
|
25
|
+
createProjections(store) {
|
|
26
|
+
return this.modules.flatMap((module) => module.createProjections ? module.createProjections(store) : []);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Connection, ConfirmedSignatureInfo, PublicKey } from "@solana/web3.js";
|
|
2
|
+
export declare function fetchSignaturesForAddress(connection: Connection, address: PublicKey, lastSignature?: string, batchSize?: number): Promise<ConfirmedSignatureInfo[]>;
|
|
3
|
+
export interface TransactionEvent {
|
|
4
|
+
signature: string;
|
|
5
|
+
logs: string[];
|
|
6
|
+
slot: number | null;
|
|
7
|
+
blockTime: number | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function syncTransactions(connection: Connection, address: PublicKey, lastSignature?: string, batchSize?: number): Promise<TransactionEvent[]>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export async function fetchSignaturesForAddress(connection, address, lastSignature, batchSize = 1000) {
|
|
2
|
+
const signatures = [];
|
|
3
|
+
let beforeSignature = undefined;
|
|
4
|
+
while (true) {
|
|
5
|
+
const batch = await connection.getSignaturesForAddress(address, {
|
|
6
|
+
limit: batchSize,
|
|
7
|
+
before: beforeSignature,
|
|
8
|
+
until: lastSignature,
|
|
9
|
+
});
|
|
10
|
+
if (batch.length === 0)
|
|
11
|
+
break;
|
|
12
|
+
signatures.push(...batch);
|
|
13
|
+
if (batch.length < batchSize)
|
|
14
|
+
break;
|
|
15
|
+
beforeSignature = batch[batch.length - 1].signature;
|
|
16
|
+
}
|
|
17
|
+
signatures.reverse();
|
|
18
|
+
return signatures;
|
|
19
|
+
}
|
|
20
|
+
export async function syncTransactions(connection, address, lastSignature, batchSize = 1000) {
|
|
21
|
+
const signatures = await fetchSignaturesForAddress(connection, address, lastSignature, batchSize);
|
|
22
|
+
if (signatures.length === 0) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const events = [];
|
|
26
|
+
for (const sigInfo of signatures) {
|
|
27
|
+
const tx = await connection.getTransaction(sigInfo.signature, {
|
|
28
|
+
maxSupportedTransactionVersion: 0,
|
|
29
|
+
});
|
|
30
|
+
if (!tx || !tx.meta) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
events.push({
|
|
34
|
+
signature: sigInfo.signature,
|
|
35
|
+
logs: tx.meta.logMessages || [],
|
|
36
|
+
slot: tx.slot ?? null,
|
|
37
|
+
blockTime: tx.blockTime ?? null,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return events;
|
|
41
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Context, SubstreamMode } from "../context.js";
|
|
2
|
+
import type { ChainEventWire, SubstreamRepos } from "../substream/types.js";
|
|
3
|
+
import type { BuiltinSubstreamModuleApis } from "../substream/modules/index.js";
|
|
4
|
+
export interface SubstreamCliConfig {
|
|
5
|
+
timeout?: number;
|
|
6
|
+
mode?: SubstreamMode;
|
|
7
|
+
healthCacheMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CursorWire {
|
|
10
|
+
lastIndex: string | null;
|
|
11
|
+
lastSignature: string | null;
|
|
12
|
+
lastSyncAt: number;
|
|
13
|
+
}
|
|
14
|
+
export interface EventsApiResponse {
|
|
15
|
+
events: ChainEventWire[];
|
|
16
|
+
total: number;
|
|
17
|
+
cursor: CursorWire | null;
|
|
18
|
+
}
|
|
19
|
+
export declare class SubstreamCliClient {
|
|
20
|
+
private readonly runtime;
|
|
21
|
+
private initialized;
|
|
22
|
+
constructor(ctx: Context, repos?: SubstreamRepos, config?: SubstreamCliConfig);
|
|
23
|
+
init(): Promise<void>;
|
|
24
|
+
module<K extends keyof BuiltinSubstreamModuleApis>(id: K): BuiltinSubstreamModuleApis[K];
|
|
25
|
+
module(id: string): unknown;
|
|
26
|
+
private ensureInitialized;
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createSubstreamRuntime, } from "../substream/runtime.js";
|
|
2
|
+
export class SubstreamCliClient {
|
|
3
|
+
runtime;
|
|
4
|
+
initialized = false;
|
|
5
|
+
constructor(ctx, repos, config) {
|
|
6
|
+
const resolved = repos ?? ctx.substream.makeRepos?.();
|
|
7
|
+
if (!resolved) {
|
|
8
|
+
throw new Error("SubstreamCliClient: repos not provided and ctx.substream.makeRepos is not configured. " +
|
|
9
|
+
"Pass stores explicitly or set ctx.substream.makeRepos (Node: makeSQLiteStores(path); Browser: makeIndexedDBStores(dbName)).");
|
|
10
|
+
}
|
|
11
|
+
this.runtime = createSubstreamRuntime(ctx, resolved, config);
|
|
12
|
+
}
|
|
13
|
+
async init() {
|
|
14
|
+
if (this.initialized)
|
|
15
|
+
return;
|
|
16
|
+
await this.runtime.initialize();
|
|
17
|
+
this.initialized = true;
|
|
18
|
+
}
|
|
19
|
+
module(id) {
|
|
20
|
+
this.ensureInitialized();
|
|
21
|
+
return this.runtime.module(id);
|
|
22
|
+
}
|
|
23
|
+
ensureInitialized() {
|
|
24
|
+
if (!this.initialized) {
|
|
25
|
+
throw new Error("SubstreamCliClient not initialized. Call init() first.");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { Dexie } from "dexie";
|
|
2
|
+
class VoidifyDexie extends Dexie {
|
|
3
|
+
events;
|
|
4
|
+
projection_states;
|
|
5
|
+
cursors;
|
|
6
|
+
constructor(name) {
|
|
7
|
+
super(name);
|
|
8
|
+
this.version(1).stores({
|
|
9
|
+
events: "++id, &[scope_type+scope_key+event_index], event_name, signature, address",
|
|
10
|
+
projection_states: "[projection_id+entity_key], projection_id",
|
|
11
|
+
cursors: "[scope_type+scope_key]",
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const dbCache = new Map();
|
|
16
|
+
async function getDb(dbName) {
|
|
17
|
+
let pending = dbCache.get(dbName);
|
|
18
|
+
if (!pending) {
|
|
19
|
+
pending = (async () => {
|
|
20
|
+
const db = new VoidifyDexie(dbName);
|
|
21
|
+
await db.open();
|
|
22
|
+
return db;
|
|
23
|
+
})();
|
|
24
|
+
dbCache.set(dbName, pending);
|
|
25
|
+
}
|
|
26
|
+
return pending;
|
|
27
|
+
}
|
|
28
|
+
function guardEvent(cursor, eventIndex) {
|
|
29
|
+
const last = cursor?.lastIndex ?? -1n;
|
|
30
|
+
if (eventIndex <= last)
|
|
31
|
+
return "duplicate";
|
|
32
|
+
if (eventIndex > last + 1n) {
|
|
33
|
+
return { kind: "gap", expected: last + 1n, got: eventIndex };
|
|
34
|
+
}
|
|
35
|
+
return "applied";
|
|
36
|
+
}
|
|
37
|
+
function rowToCursor(row) {
|
|
38
|
+
return {
|
|
39
|
+
scopeType: row.scope_type,
|
|
40
|
+
scopeKey: row.scope_key,
|
|
41
|
+
lastIndex: row.last_index === null ? null : BigInt(row.last_index),
|
|
42
|
+
lastSignature: row.last_signature,
|
|
43
|
+
lastSyncAt: row.last_sync_at,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function eventToRow(event) {
|
|
47
|
+
return {
|
|
48
|
+
scope_type: event.scopeType,
|
|
49
|
+
scope_key: event.scopeKey,
|
|
50
|
+
event_name: event.eventName,
|
|
51
|
+
event_index: eventIndexToNumber(event.eventIndex),
|
|
52
|
+
signature: event.signature,
|
|
53
|
+
slot: event.slot,
|
|
54
|
+
block_time: event.blockTime,
|
|
55
|
+
address: event.address,
|
|
56
|
+
payload: event.payload,
|
|
57
|
+
created_at: event.createdAt,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function rowToEvent(row) {
|
|
61
|
+
return {
|
|
62
|
+
scopeType: row.scope_type,
|
|
63
|
+
scopeKey: row.scope_key,
|
|
64
|
+
eventName: row.event_name,
|
|
65
|
+
eventIndex: BigInt(row.event_index),
|
|
66
|
+
signature: row.signature,
|
|
67
|
+
slot: row.slot,
|
|
68
|
+
blockTime: row.block_time,
|
|
69
|
+
address: row.address,
|
|
70
|
+
payload: row.payload,
|
|
71
|
+
createdAt: row.created_at,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function compareEvents(a, b) {
|
|
75
|
+
return a.eventIndex < b.eventIndex ? -1 : a.eventIndex > b.eventIndex ? 1 : 0;
|
|
76
|
+
}
|
|
77
|
+
function eventIndexToNumber(eventIndex) {
|
|
78
|
+
if (eventIndex < 0n || eventIndex > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
79
|
+
throw new RangeError(`Event index ${eventIndex.toString()} is outside the safe integer range`);
|
|
80
|
+
}
|
|
81
|
+
return Number(eventIndex);
|
|
82
|
+
}
|
|
83
|
+
class IndexedDBEventStore {
|
|
84
|
+
dbName;
|
|
85
|
+
db = null;
|
|
86
|
+
constructor(dbName) {
|
|
87
|
+
this.dbName = dbName;
|
|
88
|
+
}
|
|
89
|
+
async initialize() {
|
|
90
|
+
this.db = await getDb(this.dbName);
|
|
91
|
+
}
|
|
92
|
+
getDb() {
|
|
93
|
+
if (!this.db) {
|
|
94
|
+
throw new Error("Database not initialized. Call initialize() first.");
|
|
95
|
+
}
|
|
96
|
+
return this.db;
|
|
97
|
+
}
|
|
98
|
+
async apply(scope, event) {
|
|
99
|
+
return this.applyBatch(scope, [event]);
|
|
100
|
+
}
|
|
101
|
+
async applyBatch(scope, events) {
|
|
102
|
+
if (events.length === 0) {
|
|
103
|
+
const cursor = await this.getCursor(scope);
|
|
104
|
+
return { kind: "applied", cursor: cursor?.lastIndex ?? -1n };
|
|
105
|
+
}
|
|
106
|
+
const db = this.getDb();
|
|
107
|
+
const sorted = [...events].sort(compareEvents);
|
|
108
|
+
return db.transaction("rw", [db.events, db.cursors], async () => {
|
|
109
|
+
const cursorRow = await db.cursors.get([scope.scopeType, scope.scopeKey]);
|
|
110
|
+
let cursor = cursorRow ? rowToCursor(cursorRow) : null;
|
|
111
|
+
let last = {
|
|
112
|
+
kind: "applied",
|
|
113
|
+
cursor: cursor?.lastIndex ?? -1n,
|
|
114
|
+
};
|
|
115
|
+
for (const event of sorted) {
|
|
116
|
+
const guard = guardEvent(cursor, event.eventIndex);
|
|
117
|
+
if (typeof guard !== "string") {
|
|
118
|
+
Dexie.currentTransaction?.abort();
|
|
119
|
+
return guard;
|
|
120
|
+
}
|
|
121
|
+
if (guard === "duplicate") {
|
|
122
|
+
last = { kind: "duplicate", cursor: cursor?.lastIndex ?? -1n };
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
await db.events.add(eventToRow(event));
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
await db.cursors.put({
|
|
128
|
+
scope_type: scope.scopeType,
|
|
129
|
+
scope_key: scope.scopeKey,
|
|
130
|
+
last_index: eventIndexToNumber(event.eventIndex),
|
|
131
|
+
last_signature: event.signature,
|
|
132
|
+
last_sync_at: now,
|
|
133
|
+
});
|
|
134
|
+
cursor = {
|
|
135
|
+
scopeType: scope.scopeType,
|
|
136
|
+
scopeKey: scope.scopeKey,
|
|
137
|
+
lastIndex: event.eventIndex,
|
|
138
|
+
lastSignature: event.signature,
|
|
139
|
+
lastSyncAt: now,
|
|
140
|
+
};
|
|
141
|
+
last = { kind: "applied", cursor: event.eventIndex };
|
|
142
|
+
}
|
|
143
|
+
return last;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async list(scope, opts) {
|
|
147
|
+
const db = this.getDb();
|
|
148
|
+
const rows = await db.events
|
|
149
|
+
.where("[scope_type+scope_key+event_index]")
|
|
150
|
+
.between([scope.scopeType, scope.scopeKey, Dexie.minKey], [scope.scopeType, scope.scopeKey, Dexie.maxKey])
|
|
151
|
+
.reverse()
|
|
152
|
+
.toArray();
|
|
153
|
+
const start = opts?.offset ?? 0;
|
|
154
|
+
const end = opts?.limit === undefined ? undefined : start + opts.limit;
|
|
155
|
+
return rows.slice(start, end).map(rowToEvent);
|
|
156
|
+
}
|
|
157
|
+
async getAfter(scope, afterIndex) {
|
|
158
|
+
const db = this.getDb();
|
|
159
|
+
const cutoff = afterIndex === undefined ? -Infinity : eventIndexToNumber(afterIndex);
|
|
160
|
+
const rows = await db.events
|
|
161
|
+
.where("[scope_type+scope_key+event_index]")
|
|
162
|
+
.between([scope.scopeType, scope.scopeKey, cutoff], [scope.scopeType, scope.scopeKey, Dexie.maxKey], false, true)
|
|
163
|
+
.toArray();
|
|
164
|
+
return rows.map(rowToEvent);
|
|
165
|
+
}
|
|
166
|
+
async count(scope) {
|
|
167
|
+
const db = this.getDb();
|
|
168
|
+
return db.events
|
|
169
|
+
.where("[scope_type+scope_key+event_index]")
|
|
170
|
+
.between([scope.scopeType, scope.scopeKey, Dexie.minKey], [scope.scopeType, scope.scopeKey, Dexie.maxKey])
|
|
171
|
+
.count();
|
|
172
|
+
}
|
|
173
|
+
async getCursor(scope) {
|
|
174
|
+
const db = this.getDb();
|
|
175
|
+
const row = await db.cursors.get([scope.scopeType, scope.scopeKey]);
|
|
176
|
+
return row ? rowToCursor(row) : null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
class IndexedDBProjectionStore {
|
|
180
|
+
dbName;
|
|
181
|
+
db = null;
|
|
182
|
+
constructor(dbName) {
|
|
183
|
+
this.dbName = dbName;
|
|
184
|
+
}
|
|
185
|
+
async initialize() {
|
|
186
|
+
this.db = await getDb(this.dbName);
|
|
187
|
+
}
|
|
188
|
+
getDb() {
|
|
189
|
+
if (!this.db) {
|
|
190
|
+
throw new Error("Database not initialized. Call initialize() first.");
|
|
191
|
+
}
|
|
192
|
+
return this.db;
|
|
193
|
+
}
|
|
194
|
+
async get(projectionId, key) {
|
|
195
|
+
const row = await this.getDb().projection_states.get([projectionId, key]);
|
|
196
|
+
return row ? rowToProjectionState(row) : null;
|
|
197
|
+
}
|
|
198
|
+
async put(record) {
|
|
199
|
+
await this.getDb().projection_states.put(projectionStateToRow(record));
|
|
200
|
+
}
|
|
201
|
+
async list(projectionId) {
|
|
202
|
+
const rows = await this.getDb()
|
|
203
|
+
.projection_states.where("projection_id")
|
|
204
|
+
.equals(projectionId)
|
|
205
|
+
.toArray();
|
|
206
|
+
return rows.map(rowToProjectionState);
|
|
207
|
+
}
|
|
208
|
+
async clear(projectionId) {
|
|
209
|
+
await this.getDb()
|
|
210
|
+
.projection_states.where("projection_id")
|
|
211
|
+
.equals(projectionId)
|
|
212
|
+
.delete();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function projectionStateToRow(record) {
|
|
216
|
+
return {
|
|
217
|
+
projection_id: record.projectionId,
|
|
218
|
+
entity_key: record.key,
|
|
219
|
+
value: record.value,
|
|
220
|
+
updated_at: record.updatedAt,
|
|
221
|
+
last_event_index: record.lastEventIndex === null
|
|
222
|
+
? null
|
|
223
|
+
: eventIndexToNumber(record.lastEventIndex),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function rowToProjectionState(row) {
|
|
227
|
+
return {
|
|
228
|
+
projectionId: row.projection_id,
|
|
229
|
+
key: row.entity_key,
|
|
230
|
+
value: row.value,
|
|
231
|
+
updatedAt: row.updated_at,
|
|
232
|
+
lastEventIndex: row.last_event_index === null ? null : BigInt(row.last_event_index),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
export function makeIndexedDBStores(dbName, _programId) {
|
|
236
|
+
const events = new IndexedDBEventStore(dbName);
|
|
237
|
+
const projections = new IndexedDBProjectionStore(dbName);
|
|
238
|
+
return {
|
|
239
|
+
events,
|
|
240
|
+
projections,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import type { ApplyOutcome, ChainEventRecord, EventCursor, EventScope, EventStore, ProjectionStateRecord, ProjectionStore, SubstreamStores } from "../../substream/types.js";
|
|
3
|
+
export declare class SQLiteEventStore implements EventStore {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Database.Database);
|
|
6
|
+
initialize(): Promise<void>;
|
|
7
|
+
apply(scope: EventScope, event: ChainEventRecord): Promise<ApplyOutcome>;
|
|
8
|
+
applyBatch(scope: EventScope, events: ChainEventRecord[]): Promise<ApplyOutcome>;
|
|
9
|
+
list(scope: EventScope, opts?: {
|
|
10
|
+
offset?: number;
|
|
11
|
+
limit?: number;
|
|
12
|
+
}): Promise<ChainEventRecord[]>;
|
|
13
|
+
getAfter(scope: EventScope, afterIndex?: bigint): Promise<ChainEventRecord[]>;
|
|
14
|
+
count(scope: EventScope): Promise<number>;
|
|
15
|
+
getCursor(scope: EventScope): Promise<EventCursor | null>;
|
|
16
|
+
}
|
|
17
|
+
export declare class SQLiteProjectionStore implements ProjectionStore {
|
|
18
|
+
private db;
|
|
19
|
+
constructor(db: Database.Database);
|
|
20
|
+
initialize(): Promise<void>;
|
|
21
|
+
get(projectionId: string, key: string): Promise<ProjectionStateRecord | null>;
|
|
22
|
+
put(record: ProjectionStateRecord): Promise<void>;
|
|
23
|
+
list(projectionId: string): Promise<ProjectionStateRecord[]>;
|
|
24
|
+
clear(projectionId: string): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare function makeSQLiteStores(path: string, _programId?: string): SubstreamStores;
|