@voidifydao/sdk 1.0.0 → 2.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.
Files changed (59) hide show
  1. package/README.md +105 -0
  2. package/dist/cli/config/init.js +1 -7
  3. package/dist/cli/config/loader.js +0 -1
  4. package/dist/cli/config/types.d.ts +0 -1
  5. package/dist/cli/deposit.js +45 -29
  6. package/dist/cli/helpers.js +0 -3
  7. package/dist/cli/progress.d.ts +5 -0
  8. package/dist/cli/progress.js +37 -0
  9. package/dist/cli/relayer.js +16 -7
  10. package/dist/cli/withdraw.js +20 -6
  11. package/dist/context.d.ts +1 -4
  12. package/dist/context.js +14 -12
  13. package/dist/idl/voidify/idl.d.ts +1 -1
  14. package/dist/idl/voidify/idl.json +1 -1
  15. package/dist/index.d.ts +2 -3
  16. package/dist/index.js +0 -2
  17. package/dist/relayer/server/server.js +29 -13
  18. package/dist/relayer/server/switchboard.js +15 -6
  19. package/dist/relayer/types.d.ts +1 -0
  20. package/dist/substream/chain/index.d.ts +10 -3
  21. package/dist/substream/chain/index.js +14 -7
  22. package/dist/substream/chain/registry.d.ts +2 -2
  23. package/dist/substream/chain/utils.d.ts +2 -1
  24. package/dist/substream/chain/utils.js +35 -11
  25. package/dist/substream/client.d.ts +6 -1
  26. package/dist/substream/database/indexeddb.js +3 -0
  27. package/dist/substream/database/sqlite.d.ts +1 -0
  28. package/dist/substream/database/sqlite.js +6 -0
  29. package/dist/substream/modules/deposit.d.ts +2 -1
  30. package/dist/substream/modules/deposit.js +24 -20
  31. package/dist/substream/modules/relayer.d.ts +2 -1
  32. package/dist/substream/modules/relayer.js +39 -33
  33. package/dist/substream/runtime.d.ts +19 -4
  34. package/dist/substream/runtime.js +216 -16
  35. package/dist/substream/server/server.d.ts +2 -0
  36. package/dist/substream/server/server.js +42 -8
  37. package/dist/substream/types.d.ts +21 -0
  38. package/dist/types/index.d.ts +0 -1
  39. package/dist/types/index.js +1 -1
  40. package/dist/voidify/deposit.d.ts +2 -0
  41. package/dist/voidify/deposit.js +1 -1
  42. package/dist/voidify/program.d.ts +0 -4
  43. package/dist/voidify/program.js +0 -10
  44. package/dist/voidify/relayer/list.d.ts +2 -0
  45. package/dist/voidify/relayer/list.js +1 -1
  46. package/dist/voidify/withdraw.d.ts +7 -2
  47. package/dist/voidify/withdraw.js +68 -10
  48. package/package.json +5 -4
  49. package/dist/idl/voidify-staking/idl.d.ts +0 -93
  50. package/dist/idl/voidify-staking/idl.js +0 -1
  51. package/dist/idl/voidify-staking/idl.json +0 -87
  52. package/dist/staking/commands.d.ts +0 -3
  53. package/dist/staking/commands.js +0 -13
  54. package/dist/staking/index.d.ts +0 -2
  55. package/dist/staking/index.js +0 -2
  56. package/dist/staking/program.d.ts +0 -18
  57. package/dist/staking/program.js +0 -40
  58. package/dist/types/errors.d.ts +0 -1
  59. package/dist/types/errors.js +0 -16
@@ -2,12 +2,19 @@ import * as sb from "@switchboard-xyz/on-demand";
2
2
  import { CrossbarClient } from "@switchboard-xyz/common";
3
3
  import { signAndSend } from "../../utils/tx.js";
4
4
  import { VoidifyProgram } from "../../voidify/program.js";
5
+ import { relayerLogger as logger } from "../../utils/logger.js";
5
6
  async function getQuoteSlot(ctx, quotePDA) {
7
+ const SWITCHBOARD_QUOTE_ACCOUNT_PAYLOAD_OFFSET = 42;
8
+ const SWITCHBOARD_QUOTE_TAIL_DISCRIMINATOR = "SBOD";
6
9
  try {
7
- const signatures = await ctx.connection.getSignaturesForAddress(quotePDA, {
8
- limit: 1,
9
- });
10
- return signatures.length ? signatures[0].slot : 0;
10
+ const account = await ctx.connection.getAccountInfo(quotePDA, "confirmed");
11
+ if (!account)
12
+ return 0;
13
+ const quote = sb.OracleQuote.decode(Buffer.from(account.data).subarray(SWITCHBOARD_QUOTE_ACCOUNT_PAYLOAD_OFFSET));
14
+ if (quote.tailDiscriminator !== SWITCHBOARD_QUOTE_TAIL_DISCRIMINATOR) {
15
+ throw new Error(`Invalid Switchboard quote discriminator: ${quote.tailDiscriminator}`);
16
+ }
17
+ return quote.slot;
11
18
  }
12
19
  catch (error) {
13
20
  throw new Error("Failed to fetch quote slot from chain", {
@@ -18,13 +25,15 @@ async function getQuoteSlot(ctx, quotePDA) {
18
25
  export async function updateQuote(ctx, feedID) {
19
26
  const voidifyProgram = new VoidifyProgram(ctx.connection, ctx.programId);
20
27
  const oracleConfig = await voidifyProgram.program.account.oracleConfig.fetch(voidifyProgram.oracleConfig());
21
- const maxPriceAgeSlots = (BigInt(oracleConfig.maxPriceAgeSecs.toString()) * 1000n) / 400n;
28
+ const maxPriceAgeSlots = (BigInt(oracleConfig.maxPriceAgeSecs.toString()) * 5n) / 2n;
22
29
  const queue = await sb.Queue.loadDefault(await sb.AnchorUtils.loadProgramFromConnection(ctx.connection));
23
30
  const crossbar = CrossbarClient.default();
24
31
  const currentSlot = await ctx.connection.getSlot("confirmed");
25
32
  const [quotePDA] = sb.OracleQuote.getCanonicalPubkey(queue.pubkey, [feedID]);
26
33
  const quoteSlot = await getQuoteSlot(ctx, quotePDA);
27
- if (BigInt(currentSlot - quoteSlot) >= maxPriceAgeSlots) {
34
+ const currentAgeSlots = BigInt(currentSlot - quoteSlot);
35
+ if (currentAgeSlots >= maxPriceAgeSlots) {
36
+ logger.info({ currentAgeSlots, maxPriceAgeSlots }, "Updating switchboard oracle");
28
37
  const ixs = await queue.fetchManagedUpdateIxs(crossbar, [feedID], {
29
38
  variableOverrides: {},
30
39
  instructionIdx: 0,
@@ -6,6 +6,7 @@ export interface WithdrawRequestBody {
6
6
  amount: string;
7
7
  fee: string;
8
8
  treasury: string;
9
+ rpcUrl?: string;
9
10
  }
10
11
  export interface WithdrawResponse {
11
12
  success: boolean;
@@ -1,11 +1,17 @@
1
1
  import type { PublicKey } from "@solana/web3.js";
2
- import type { ChainEventRecord, EventProjection, EventScope, EventStore } from "../../substream/types.js";
2
+ import type { ChainEventRecord, EventProjection, EventScope, EventStore, SyncProgress, SyncStatusReporter } from "../../substream/types.js";
3
+ export interface ChainSyncOptions {
4
+ reporter?: SyncStatusReporter;
5
+ }
6
+ export interface ChainSyncRun {
7
+ updateProgress(progress: SyncProgress): void;
8
+ }
3
9
  export interface EventStreamSpec {
4
10
  id: string;
5
11
  scope: EventScope;
6
12
  address: PublicKey;
7
13
  getChainLastIndex(): Promise<bigint>;
8
- collect(lastSignature?: string): Promise<ChainEventRecord[]>;
14
+ collectBatches(lastSignature?: string, run?: ChainSyncRun): AsyncIterable<ChainEventRecord[]>;
9
15
  }
10
16
  export declare class ProjectionRegistry {
11
17
  private projections;
@@ -18,7 +24,8 @@ export declare class ChainEventSyncer {
18
24
  private projections;
19
25
  constructor(events: EventStore, projections?: ProjectionRegistry);
20
26
  registerProjection(projection: EventProjection): void;
21
- checkAndSync(spec: EventStreamSpec): Promise<boolean>;
27
+ checkAndSync(spec: EventStreamSpec, run?: ChainSyncRun): Promise<boolean>;
22
28
  applyLiveEvent(spec: EventStreamSpec, event: ChainEventRecord): Promise<void>;
23
29
  private sync;
30
+ private applyRecords;
24
31
  }
@@ -31,7 +31,7 @@ export class ChainEventSyncer {
31
31
  registerProjection(projection) {
32
32
  this.projections.register(projection);
33
33
  }
34
- async checkAndSync(spec) {
34
+ async checkAndSync(spec, run) {
35
35
  const cursor = await this.events.getCursor(spec.scope);
36
36
  const localIndex = cursor?.lastIndex ?? -1n;
37
37
  let chainLastIndex;
@@ -40,12 +40,12 @@ export class ChainEventSyncer {
40
40
  }
41
41
  catch (error) {
42
42
  substreamLogger.warn({ err: error, streamId: spec.id }, "substream alignment check failed");
43
- await this.sync(spec);
43
+ await this.sync(spec, run);
44
44
  return false;
45
45
  }
46
46
  if (localIndex >= chainLastIndex)
47
47
  return true;
48
- await this.sync(spec);
48
+ await this.sync(spec, run);
49
49
  return false;
50
50
  }
51
51
  async applyLiveEvent(spec, event) {
@@ -58,11 +58,17 @@ export class ChainEventSyncer {
58
58
  await this.projections.apply([event]);
59
59
  }
60
60
  }
61
- async sync(spec) {
61
+ async sync(spec, run) {
62
62
  const cursor = await this.events.getCursor(spec.scope);
63
- const records = await spec.collect(cursor?.lastSignature ?? undefined);
63
+ for await (const records of spec.collectBatches(cursor?.lastSignature ?? undefined, run)) {
64
+ const shouldContinue = await this.applyRecords(spec, records);
65
+ if (!shouldContinue)
66
+ return;
67
+ }
68
+ }
69
+ async applyRecords(spec, records) {
64
70
  if (records.length === 0)
65
- return;
71
+ return true;
66
72
  const outcome = await this.events.applyBatch(spec.scope, records);
67
73
  if (outcome.kind === "gap") {
68
74
  substreamLogger.warn({
@@ -72,8 +78,9 @@ export class ChainEventSyncer {
72
78
  expected: outcome.expected.toString(),
73
79
  got: outcome.got.toString(),
74
80
  }, "chain event batch apply found a gap; RPC history window may be exhausted");
75
- return;
81
+ return false;
76
82
  }
77
83
  await this.projections.apply(records);
84
+ return true;
78
85
  }
79
86
  }
@@ -1,7 +1,7 @@
1
1
  import type { Context } from "../../context.js";
2
2
  import type { ChainEventRecord, EventProjection, EventScope, EventScopeType, EventStore, ProjectionStore } from "../../substream/types.js";
3
3
  import type { VoidifyProgram } from "../../voidify/program.js";
4
- import type { EventStreamSpec } from "./index.js";
4
+ import type { ChainSyncOptions, EventStreamSpec } from "./index.js";
5
5
  export interface LiveEventContext {
6
6
  ctx: Context;
7
7
  voidify: VoidifyProgram;
@@ -18,7 +18,7 @@ export interface SubstreamModuleRuntime {
18
18
  voidify: VoidifyProgram;
19
19
  events: EventStore;
20
20
  projections: ProjectionStore;
21
- sync(scope: EventScope): Promise<void>;
21
+ sync(scope: EventScope, options?: ChainSyncOptions): Promise<void>;
22
22
  rebuildProjection(scope: EventScope, projectionId: string): Promise<void>;
23
23
  }
24
24
  export interface SubstreamModule {
@@ -1,4 +1,5 @@
1
1
  import { Connection, ConfirmedSignatureInfo, PublicKey } from "@solana/web3.js";
2
+ import type { SyncProgress } from "../../substream/types.js";
2
3
  export declare function fetchSignaturesForAddress(connection: Connection, address: PublicKey, lastSignature?: string, batchSize?: number): Promise<ConfirmedSignatureInfo[]>;
3
4
  export interface TransactionEvent {
4
5
  signature: string;
@@ -6,4 +7,4 @@ export interface TransactionEvent {
6
7
  slot: number | null;
7
8
  blockTime: number | null;
8
9
  }
9
- export declare function syncTransactions(connection: Connection, address: PublicKey, lastSignature?: string, batchSize?: number): Promise<TransactionEvent[]>;
10
+ export declare function syncTransactionBatches(connection: Connection, address: PublicKey, lastSignature?: string, signatureBatchSize?: number, transactionBatchSize?: number, updateProgress?: (progress: SyncProgress) => void): AsyncGenerator<TransactionEvent[]>;
@@ -1,12 +1,17 @@
1
+ import pRetry from "p-retry";
2
+ const retryConnection = (operation) => pRetry(operation, {
3
+ minTimeout: 1000,
4
+ retries: 5,
5
+ });
1
6
  export async function fetchSignaturesForAddress(connection, address, lastSignature, batchSize = 1000) {
2
7
  const signatures = [];
3
8
  let beforeSignature = undefined;
4
9
  while (true) {
5
- const batch = await connection.getSignaturesForAddress(address, {
10
+ const batch = await retryConnection(() => connection.getSignaturesForAddress(address, {
6
11
  limit: batchSize,
7
12
  before: beforeSignature,
8
13
  until: lastSignature,
9
- });
14
+ }));
10
15
  if (batch.length === 0)
11
16
  break;
12
17
  signatures.push(...batch);
@@ -17,25 +22,44 @@ export async function fetchSignaturesForAddress(connection, address, lastSignatu
17
22
  signatures.reverse();
18
23
  return signatures;
19
24
  }
20
- export async function syncTransactions(connection, address, lastSignature, batchSize = 1000) {
21
- const signatures = await fetchSignaturesForAddress(connection, address, lastSignature, batchSize);
25
+ export async function* syncTransactionBatches(connection, address, lastSignature, signatureBatchSize = 1000, transactionBatchSize = 50, updateProgress) {
26
+ const signatures = await fetchSignaturesForAddress(connection, address, lastSignature, signatureBatchSize);
22
27
  if (signatures.length === 0) {
23
- return [];
28
+ return;
24
29
  }
25
30
  const events = [];
31
+ let current = 0;
26
32
  for (const sigInfo of signatures) {
27
- const tx = await connection.getTransaction(sigInfo.signature, {
28
- maxSupportedTransactionVersion: 0,
33
+ const tx = await retryConnection(async () => {
34
+ const transaction = await connection.getTransaction(sigInfo.signature, {
35
+ maxSupportedTransactionVersion: 0,
36
+ });
37
+ if (!transaction?.meta) {
38
+ throw new Error(`transaction unavailable: ${sigInfo.signature}`);
39
+ }
40
+ return transaction;
29
41
  });
30
- if (!tx || !tx.meta) {
31
- continue;
42
+ const meta = tx.meta;
43
+ if (!meta) {
44
+ throw new Error(`transaction unavailable: ${sigInfo.signature}`);
32
45
  }
33
46
  events.push({
34
47
  signature: sigInfo.signature,
35
- logs: tx.meta.logMessages || [],
48
+ logs: meta.logMessages || [],
36
49
  slot: tx.slot ?? null,
37
50
  blockTime: tx.blockTime ?? null,
38
51
  });
52
+ current += 1;
53
+ updateProgress?.({
54
+ current,
55
+ total: signatures.length,
56
+ signature: sigInfo.signature,
57
+ });
58
+ if (events.length >= transactionBatchSize) {
59
+ yield events.splice(0, events.length);
60
+ }
61
+ }
62
+ if (events.length > 0) {
63
+ yield events.splice(0, events.length);
39
64
  }
40
- return events;
41
65
  }
@@ -1,5 +1,5 @@
1
1
  import type { Context, SubstreamMode } from "../context.js";
2
- import type { ChainEventWire, SubstreamRepos } from "../substream/types.js";
2
+ import type { ChainEventWire, SubstreamRepos, SyncStatus } from "../substream/types.js";
3
3
  import type { BuiltinSubstreamModuleApis } from "../substream/modules/index.js";
4
4
  export interface SubstreamCliConfig {
5
5
  timeout?: number;
@@ -7,6 +7,8 @@ export interface SubstreamCliConfig {
7
7
  healthCacheMs?: number;
8
8
  }
9
9
  export interface CursorWire {
10
+ scopeType: string;
11
+ scopeKey: string;
10
12
  lastIndex: string | null;
11
13
  lastSignature: string | null;
12
14
  lastSyncAt: number;
@@ -15,6 +17,9 @@ export interface EventsApiResponse {
15
17
  events: ChainEventWire[];
16
18
  total: number;
17
19
  cursor: CursorWire | null;
20
+ syncStatus: (Omit<SyncStatus, "cursor"> & {
21
+ cursor: CursorWire | null;
22
+ }) | null;
18
23
  }
19
24
  export declare class SubstreamCliClient {
20
25
  private readonly runtime;
@@ -198,6 +198,9 @@ class IndexedDBProjectionStore {
198
198
  async put(record) {
199
199
  await this.getDb().projection_states.put(projectionStateToRow(record));
200
200
  }
201
+ async delete(projectionId, key) {
202
+ await this.getDb().projection_states.delete([projectionId, key]);
203
+ }
201
204
  async list(projectionId) {
202
205
  const rows = await this.getDb()
203
206
  .projection_states.where("projection_id")
@@ -20,6 +20,7 @@ export declare class SQLiteProjectionStore implements ProjectionStore {
20
20
  initialize(): Promise<void>;
21
21
  get(projectionId: string, key: string): Promise<ProjectionStateRecord | null>;
22
22
  put(record: ProjectionStateRecord): Promise<void>;
23
+ delete(projectionId: string, key: string): Promise<void>;
23
24
  list(projectionId: string): Promise<ProjectionStateRecord[]>;
24
25
  clear(projectionId: string): Promise<void>;
25
26
  }
@@ -241,6 +241,12 @@ export class SQLiteProjectionStore {
241
241
  ? null
242
242
  : eventIndexToNumber(record.lastEventIndex));
243
243
  }
244
+ async delete(projectionId, key) {
245
+ this.db
246
+ .prepare(`DELETE FROM projection_states
247
+ WHERE projection_id = ? AND entity_key = ?`)
248
+ .run(projectionId, key);
249
+ }
244
250
  async list(projectionId) {
245
251
  const rows = this.db
246
252
  .prepare(`SELECT * FROM projection_states
@@ -1,7 +1,8 @@
1
1
  import type { DepositRecord, EventCursor, EventScope } from "../../substream/types.js";
2
2
  import type { SubstreamModule } from "../../substream/chain/registry.js";
3
+ import type { ChainSyncOptions } from "../../substream/chain/index.js";
3
4
  export interface DepositModuleApi {
4
- sync(denomination: bigint): Promise<void>;
5
+ sync(denomination: bigint, options?: ChainSyncOptions): Promise<void>;
5
6
  list(denomination: bigint, opts?: {
6
7
  offset?: number;
7
8
  limit?: number;
@@ -1,7 +1,7 @@
1
1
  import { toBigInt } from "../../utils/anchor-events.js";
2
2
  import { bytesToBigInt, bytesToHex } from "../../utils/bytes.js";
3
3
  import { parseEventsFromLogs } from "../../utils/anchor-events.js";
4
- import { syncTransactions } from "../../substream/chain/utils.js";
4
+ import { syncTransactionBatches, } from "../../substream/chain/utils.js";
5
5
  export function depositScope(denomination) {
6
6
  return { scopeType: "deposit", scopeKey: denomination.toString() };
7
7
  }
@@ -35,8 +35,8 @@ export const depositModule = {
35
35
  ],
36
36
  createClientApi(runtime) {
37
37
  return {
38
- async sync(denomination) {
39
- await runtime.sync(depositScope(denomination));
38
+ async sync(denomination, options) {
39
+ await runtime.sync(depositScope(denomination), options);
40
40
  },
41
41
  async list(denomination, opts) {
42
42
  const rows = await runtime.events.list(depositScope(denomination), opts);
@@ -65,27 +65,31 @@ function createDepositStream(ctx, voidify, denomination) {
65
65
  const poolAccount = await voidify.program.account.pool.fetch(address);
66
66
  return BigInt(poolAccount.merkleTree.nextIndex) - 1n;
67
67
  },
68
- collect: async (lastSignature) => {
69
- const transactions = await syncTransactions(ctx.connection, address, lastSignature);
70
- const records = [];
71
- for (const tx of transactions) {
72
- const decoded = parseEventsFromLogs(tx.logs, voidify.program.coder.events);
73
- for (const item of decoded) {
74
- if (item.name !== "depositEvent")
75
- continue;
76
- records.push(depositEventToChainEvent({
77
- event: item.data,
78
- signature: tx.signature,
79
- slot: tx.slot,
80
- blockTime: tx.blockTime,
81
- address,
82
- }));
83
- }
68
+ collectBatches: async function* (lastSignature, run) {
69
+ for await (const transactions of syncTransactionBatches(ctx.connection, address, lastSignature, undefined, undefined, run?.updateProgress)) {
70
+ yield collectDepositRecords(transactions, voidify, address);
84
71
  }
85
- return records;
86
72
  },
87
73
  };
88
74
  }
75
+ function collectDepositRecords(transactions, voidify, address) {
76
+ const records = [];
77
+ for (const tx of transactions) {
78
+ const decoded = parseEventsFromLogs(tx.logs, voidify.program.coder.events);
79
+ for (const item of decoded) {
80
+ if (item.name !== "depositEvent")
81
+ continue;
82
+ records.push(depositEventToChainEvent({
83
+ event: item.data,
84
+ signature: tx.signature,
85
+ slot: tx.slot,
86
+ blockTime: tx.blockTime,
87
+ address,
88
+ }));
89
+ }
90
+ }
91
+ return records;
92
+ }
89
93
  function depositEventToChainEvent(args) {
90
94
  const event = args.event;
91
95
  const denomination = toBigInt(event.denomination);
@@ -1,7 +1,8 @@
1
1
  import type { EventScope, RelayerRecord } from "../../substream/types.js";
2
2
  import type { SubstreamModule } from "../../substream/chain/registry.js";
3
+ import type { ChainSyncOptions } from "../../substream/chain/index.js";
3
4
  export interface RelayerModuleApi {
4
- sync(): Promise<void>;
5
+ sync(options?: ChainSyncOptions): Promise<void>;
5
6
  list(): Promise<RelayerRecord[]>;
6
7
  get(pubkey: string): Promise<RelayerRecord | null>;
7
8
  rebuild(): Promise<void>;
@@ -2,7 +2,7 @@ import { PublicKey } from "@solana/web3.js";
2
2
  import { normalizePayload } from "../../substream/chain/events.js";
3
3
  import { parseEventsFromLogs, } from "../../utils/anchor-events.js";
4
4
  import { substreamLogger } from "../../utils/logger.js";
5
- import { syncTransactions } from "../../substream/chain/utils.js";
5
+ import { syncTransactionBatches, } from "../../substream/chain/utils.js";
6
6
  export const RELAYER_SCOPE = {
7
7
  scopeType: "relayer",
8
8
  scopeKey: "global",
@@ -32,7 +32,7 @@ export const relayerModule = {
32
32
  const decoded = { name: eventName, data: event };
33
33
  const pubkey = decoded.data.relayer.toBase58();
34
34
  const cfg = decoded.name === "relayerRegisteredEvent"
35
- ? await fetchRelayerConfig(voidify, pubkey)
35
+ ? await fetchRelayerConfig(voidify, pubkey, false)
36
36
  : null;
37
37
  return relayerEventToChainEvent({
38
38
  event: decoded,
@@ -48,8 +48,8 @@ export const relayerModule = {
48
48
  },
49
49
  createClientApi(runtime) {
50
50
  return {
51
- async sync() {
52
- await runtime.sync(RELAYER_SCOPE);
51
+ async sync(options) {
52
+ await runtime.sync(RELAYER_SCOPE, options);
53
53
  },
54
54
  async list() {
55
55
  const rows = await runtime.projections.list("relayer");
@@ -75,33 +75,37 @@ function createRelayerStream(ctx, voidify) {
75
75
  const counter = await voidify.program.account.relayerEventCounter.fetch(counterPda);
76
76
  return BigInt(counter.count.toString()) - 1n;
77
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
- }
78
+ collectBatches: async function* (lastSignature, run) {
79
+ for await (const transactions of syncTransactionBatches(ctx.connection, ctx.programId, lastSignature, undefined, undefined, run?.updateProgress)) {
80
+ yield await collectRelayerRecords(transactions, voidify, ctx.programId);
100
81
  }
101
- return records;
102
82
  },
103
83
  };
104
84
  }
85
+ async function collectRelayerRecords(transactions, voidify, address) {
86
+ const records = [];
87
+ for (const tx of transactions) {
88
+ const decoded = parseEventsFromLogs(tx.logs, voidify.program.coder.events);
89
+ for (const item of decoded) {
90
+ if (!isIndexedRelayerEventName(item.name))
91
+ continue;
92
+ const event = item;
93
+ const pubkey = event.data.relayer.toBase58();
94
+ const cfg = event.name === "relayerRegisteredEvent"
95
+ ? await fetchRelayerConfig(voidify, pubkey, true)
96
+ : null;
97
+ records.push(relayerEventToChainEvent({
98
+ event,
99
+ signature: tx.signature,
100
+ slot: tx.slot,
101
+ blockTime: tx.blockTime,
102
+ address,
103
+ config: cfg,
104
+ }));
105
+ }
106
+ }
107
+ return records;
108
+ }
105
109
  function relayerEventToChainEvent(args) {
106
110
  const payload = normalizePayload(args.event.data);
107
111
  if (args.event.name === "relayerRegisteredEvent") {
@@ -122,14 +126,16 @@ function relayerEventToChainEvent(args) {
122
126
  function isIndexedRelayerEventName(name) {
123
127
  return INDEXED_RELAYER_EVENT_NAMES.has(name);
124
128
  }
125
- async function fetchRelayerConfig(voidify, relayerPubkey) {
129
+ async function fetchRelayerConfig(voidify, relayerPubkey, quietNotFound = false) {
126
130
  try {
127
131
  const cfgKey = voidify.relayerConfig(new PublicKey(relayerPubkey));
128
132
  const cfg = await voidify.program.account.relayerConfig.fetch(cfgKey);
129
133
  return { name: cfg.name, url: cfg.url, feeBps: cfg.feeBps };
130
134
  }
131
135
  catch (err) {
132
- substreamLogger.warn({ err, relayerPubkey }, "fetchRelayerConfig failed");
136
+ if (!quietNotFound) {
137
+ substreamLogger.warn({ err, relayerPubkey }, "fetchRelayerConfig failed");
138
+ }
133
139
  return null;
134
140
  }
135
141
  }
@@ -151,8 +157,10 @@ class RelayerProjection {
151
157
  const existing = await this.store.get(this.id, pubkey);
152
158
  const current = existing ? valueToRelayerState(existing.value) : null;
153
159
  const next = reduceRelayerEvent(current, event);
154
- if (!next)
160
+ if (!next) {
161
+ await this.store.delete(this.id, pubkey);
155
162
  continue;
163
+ }
156
164
  await this.store.put({
157
165
  projectionId: this.id,
158
166
  key: pubkey,
@@ -202,9 +210,7 @@ function reduceRelayerEvent(existing, event) {
202
210
  }
203
211
  case "relayerSlashedEvent":
204
212
  case "relayerUnregisteredEvent": {
205
- record.stakeAmount = 0n;
206
- record.isActive = false;
207
- break;
213
+ return null;
208
214
  }
209
215
  case "relayerUpdatedEvent": {
210
216
  if (event.payload.feeBps !== null && event.payload.feeBps !== undefined) {
@@ -1,6 +1,6 @@
1
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";
2
+ import type { EventScope, EventStore, ProjectionStore, SyncStatus, SubstreamStores } from "../substream/types.js";
3
+ import { type ChainSyncOptions, ChainEventSyncer, ProjectionRegistry } from "../substream/chain/index.js";
4
4
  import { SubstreamModuleRegistry, type SubstreamModule, type SubstreamModuleRuntime } from "../substream/chain/registry.js";
5
5
  import { VoidifyProgram } from "../voidify/program.js";
6
6
  export interface SubstreamRuntimeConfig {
@@ -19,20 +19,35 @@ export declare class SubstreamRuntime implements SubstreamModuleRuntime {
19
19
  private readonly mode;
20
20
  private readonly timeout;
21
21
  private readonly healthCacheMs;
22
+ private readonly syncInFlight;
23
+ private readonly syncStatuses;
22
24
  private healthState;
23
25
  private initialized;
24
26
  constructor(ctx: Context, stores: SubstreamStores, config?: SubstreamRuntimeConfig, modules?: readonly SubstreamModule[]);
25
27
  initialize(): Promise<void>;
26
28
  module(id: string): unknown;
27
- sync(scope: EventScope): Promise<void>;
28
- syncLocal(scope: EventScope): Promise<void>;
29
+ sync(scope: EventScope, options?: ChainSyncOptions): Promise<void>;
30
+ syncLocal(scope: EventScope, options?: ChainSyncOptions): Promise<void>;
31
+ syncLocalInBackground(scope: EventScope): SyncStatus;
29
32
  applyLiveEvent(eventScope: EventScope): Promise<void>;
30
33
  applyLiveRecord(scope: EventScope, record: Parameters<ChainEventSyncer["applyLiveEvent"]>[1]): Promise<void>;
31
34
  rebuildProjection(scope: EventScope, projectionId: string): Promise<void>;
35
+ getSyncStatus(scope: EventScope, mode?: "local" | "remote"): SyncStatus;
32
36
  private get baseUrl();
33
37
  private resolveMode;
34
38
  private healthCheck;
35
39
  private syncRemote;
40
+ private applyRemoteEvents;
41
+ private runScopedSync;
42
+ private syncKey;
43
+ private ensureSyncStatus;
44
+ private setSyncStatus;
45
+ private updateSyncStatus;
46
+ private waitForRemoteSync;
47
+ private sleep;
36
48
  private fetchRemoteEvents;
49
+ private remoteEventsUrl;
50
+ private fetchWithTimeout;
51
+ private fetchRemoteSyncStatus;
37
52
  }
38
53
  export declare function createSubstreamRuntime(ctx: Context, stores: SubstreamStores, config?: SubstreamRuntimeConfig, modules?: readonly SubstreamModule[]): SubstreamRuntime;