1+ import { createLitClient } from '@lit-protocol/lit-client' ;
2+ import {
3+ createEpochSnapshot ,
4+ EpochSnapshot ,
5+ } from './helpers/createEpochSnapshot' ;
6+
17/**
28 * Options used when Shiva spins up a brand-new testnet instance.
39 * Values mirror the Rust manager contract; all fields are optional for our wrapper.
@@ -40,23 +46,17 @@ type FetchOptions = {
4046 body ?: unknown ;
4147} ;
4248
43- /**
44- * Snapshot returned from {@link ShivaClient.inspectEpoch} and {@link ShivaClient.waitForEpochChange}.
45- */
46- // type EpochSnapshot = {
47- // epoch: number | undefined;
48- // nodeEpochs: Array<{ url: string; epoch: number | undefined }>;
49- // threshold: number | undefined;
50- // connectedCount: number | undefined;
51- // latestBlockhash: string | undefined;
52- // rawContext: any;
53- // };
54-
5549/**
5650 * Options for {@link ShivaClient.waitForEpochChange}.
5751 */
5852type WaitForEpochOptions = {
59- baselineEpoch : number | undefined ;
53+ expectedEpoch : number | undefined ;
54+ timeoutMs ?: number ;
55+ intervalMs ?: number ;
56+ } ;
57+
58+ type PollTestnetStateOptions = {
59+ waitFor ?: TestNetState | TestNetState [ ] ;
6060 timeoutMs ?: number ;
6161 intervalMs ?: number ;
6262} ;
@@ -68,26 +68,42 @@ export type ShivaClient = {
6868 baseUrl : string ;
6969 testnetId : string ;
7070 /** Fetch a one-off snapshot of the Lit context and per-node epochs. */
71- // inspectEpoch: () => Promise<EpochSnapshot>;
71+ inspectEpoch : ( ) => Promise < EpochSnapshot > ;
7272 /**
7373 * Poll the Lit client until it reports an epoch different from {@link WaitForEpochOptions.baselineEpoch}.
7474 * Useful immediately after triggering an epoch change via Shiva.
7575 */
76- // waitForEpochChange: (options: WaitForEpochOptions) => Promise<EpochSnapshot>;
76+ waitForEpochChange : ( options : WaitForEpochOptions ) => Promise < EpochSnapshot > ;
7777 /** Invoke Shiva's `/test/action/transition/epoch/wait/<id>` and wait for completion. */
7878 transitionEpochAndWait : ( ) => Promise < boolean > ;
7979 /** Stop a random node and wait for the subsequent epoch change. */
8080 stopRandomNodeAndWait : ( ) => Promise < boolean > ;
8181 /** Query the current state of the managed testnet (Busy, Active, etc.). */
82- pollTestnetState : ( ) => Promise < TestNetState > ;
82+ /**
83+ * @example
84+ * ```ts
85+ * // Wait up to two minutes for the testnet to become active.
86+ * await client.pollTestnetState({ waitFor: 'Active', timeoutMs: 120_000 });
87+ * ```
88+ */
89+ pollTestnetState : (
90+ options ?: PollTestnetStateOptions
91+ ) => Promise < TestNetState > ;
8392 /** Retrieve the full testnet configuration (contract ABIs, RPC URL, etc.). */
8493 getTestnetInfo : ( ) => Promise < unknown > ;
8594 /** Shut down the underlying testnet through the Shiva manager. */
8695 deleteTestnet : ( ) => Promise < boolean > ;
96+
97+ // Setters
98+ setLitClient : (
99+ litClient : Awaited < ReturnType < typeof createLitClient > >
100+ ) => void ;
87101} ;
88102
89- // const DEFAULT_POLL_INTERVAL = 2000;
90- // const DEFAULT_TIMEOUT = 60_000;
103+ const DEFAULT_POLL_INTERVAL = 2000 ;
104+ const DEFAULT_TIMEOUT = 60_000 ;
105+ const DEFAULT_STATE_POLL_INTERVAL = 2000 ;
106+ const DEFAULT_STATE_POLL_TIMEOUT = 60_000 ;
91107
92108const normaliseBaseUrl = ( baseUrl : string ) => {
93109 return baseUrl . endsWith ( '/' ) ? baseUrl . slice ( 0 , - 1 ) : baseUrl ;
@@ -186,33 +202,6 @@ const getOrCreateTestnetId = async (
186202 return response . testnetId ;
187203} ;
188204
189- // const buildEpochSnapshot = (ctx: any): EpochSnapshot => {
190- // const nodeEpochEntries = Object.entries(
191- // ctx?.handshakeResult?.serverKeys ?? {}
192- // );
193- // const nodeEpochs = nodeEpochEntries.map(([url, data]: [string, any]) => ({
194- // url,
195- // epoch: data?.epoch,
196- // }));
197-
198- // const connected = ctx?.handshakeResult?.connectedNodes;
199- // const connectedCount =
200- // typeof connected?.size === 'number'
201- // ? connected.size
202- // : Array.isArray(connected)
203- // ? connected.length
204- // : undefined;
205-
206- // return {
207- // epoch: ctx?.latestConnectionInfo?.epochInfo?.number,
208- // nodeEpochs,
209- // threshold: ctx?.handshakeResult?.threshold,
210- // connectedCount,
211- // latestBlockhash: ctx?.latestBlockhash,
212- // rawContext: ctx,
213- // };
214- // };
215-
216205/**
217206 * Creates a Shiva client wrapper for the provided Lit client instance.
218207 * The wrapper talks to the Shiva manager REST endpoints, auto-discovers (or optionally creates) a testnet,
@@ -228,30 +217,47 @@ export const createShivaClient = async (
228217 options . createRequest
229218 ) ;
230219
231- // const inspectEpoch = async () => {
232- // const ctx = await litClient.getContext();
233- // return buildEpochSnapshot(ctx);
234- // };
235-
236- // const waitForEpochChange = async ({
237- // baselineEpoch,
238- // timeoutMs = DEFAULT_TIMEOUT,
239- // intervalMs = DEFAULT_POLL_INTERVAL,
240- // }: WaitForEpochOptions) => {
241- // const deadline = Date.now() + timeoutMs;
242-
243- // while (Date.now() < deadline) {
244- // await new Promise((resolve) => setTimeout(resolve, intervalMs));
245- // const snapshot = await inspectEpoch();
246- // if (snapshot.epoch !== baselineEpoch) {
247- // return snapshot;
248- // }
249- // }
250-
251- // throw new Error(
252- // `Epoch did not change from ${baselineEpoch} within ${timeoutMs}ms`
253- // );
254- // };
220+ let litClientInstance :
221+ | Awaited < ReturnType < typeof createLitClient > >
222+ | undefined ;
223+
224+ const setLitClient = (
225+ client : Awaited < ReturnType < typeof createLitClient > >
226+ ) => {
227+ litClientInstance = client ;
228+ } ;
229+
230+ const inspectEpoch = async ( ) => {
231+ if ( ! litClientInstance ) {
232+ throw new Error (
233+ `Lit client not set. Please call setLitClient() before using inspectEpoch().`
234+ ) ;
235+ }
236+
237+ return createEpochSnapshot ( litClientInstance ) ;
238+ } ;
239+
240+ const waitForEpochChange = async ( {
241+ expectedEpoch,
242+ timeoutMs = DEFAULT_TIMEOUT ,
243+ intervalMs = DEFAULT_POLL_INTERVAL ,
244+ } : WaitForEpochOptions ) => {
245+ const deadline = Date . now ( ) + timeoutMs ;
246+
247+ while ( Date . now ( ) < deadline ) {
248+ await new Promise ( ( resolve ) => setTimeout ( resolve , intervalMs ) ) ;
249+ const snapshot = await inspectEpoch ( ) ;
250+ if (
251+ snapshot . latestConnectionInfo . epochState . currentNumber !== expectedEpoch
252+ ) {
253+ return snapshot ;
254+ }
255+ }
256+
257+ throw new Error (
258+ `Epoch did not change from ${ expectedEpoch } within ${ timeoutMs } ms`
259+ ) ;
260+ } ;
255261
256262 const transitionEpochAndWait = async ( ) => {
257263 const response = await fetchShiva < boolean > (
@@ -266,15 +272,52 @@ export const createShivaClient = async (
266272 baseUrl ,
267273 `/test/action/stop/random/wait/${ testnetId } `
268274 ) ;
275+
276+ // wait briefly to allow the node to drop from the network
277+ await new Promise ( ( resolve ) => setTimeout ( resolve , 5000 ) ) ;
278+
269279 return Boolean ( response . body ) ;
270280 } ;
271281
272- const pollTestnetState = async ( ) => {
273- const response = await fetchShiva < string > (
274- baseUrl ,
275- `/test/poll/testnet/${ testnetId } `
276- ) ;
277- return ( response . body ?? 'UNKNOWN' ) as TestNetState ;
282+ const pollTestnetState = async (
283+ options : PollTestnetStateOptions = { }
284+ ) : Promise < TestNetState > => {
285+ const {
286+ waitFor,
287+ timeoutMs = DEFAULT_STATE_POLL_TIMEOUT ,
288+ intervalMs = DEFAULT_STATE_POLL_INTERVAL ,
289+ } = options ;
290+
291+ const desiredStates = Array . isArray ( waitFor )
292+ ? waitFor
293+ : waitFor
294+ ? [ waitFor ]
295+ : undefined ;
296+ const deadline = Date . now ( ) + timeoutMs ;
297+
298+ // Continue polling until we hit a desired state or timeout.
299+ // If no desired state is provided, return the first observation .
300+ for ( ; ; ) {
301+ const response = await fetchShiva < string > (
302+ baseUrl ,
303+ `/test/poll/testnet/${ testnetId } `
304+ ) ;
305+ const state = ( response . body ?? 'UNKNOWN' ) as TestNetState ;
306+
307+ if ( ! desiredStates || desiredStates . includes ( state ) ) {
308+ return state ;
309+ }
310+
311+ if ( Date . now ( ) >= deadline ) {
312+ throw new Error (
313+ `Timed out after ${ timeoutMs } ms waiting for testnet ${ testnetId } to reach state ${ desiredStates . join (
314+ ', '
315+ ) } . Last observed state: ${ state } .`
316+ ) ;
317+ }
318+
319+ await new Promise ( ( resolve ) => setTimeout ( resolve , intervalMs ) ) ;
320+ }
278321 } ;
279322
280323 const getTestnetInfo = async ( ) => {
@@ -296,12 +339,15 @@ export const createShivaClient = async (
296339 return {
297340 baseUrl,
298341 testnetId,
299- // inspectEpoch,
300- // waitForEpochChange,
342+ setLitClient,
301343 transitionEpochAndWait,
302344 stopRandomNodeAndWait,
303345 pollTestnetState,
304346 getTestnetInfo,
305347 deleteTestnet,
348+
349+ // utils
350+ inspectEpoch,
351+ waitForEpochChange,
306352 } ;
307353} ;
0 commit comments