Skip to content

Commit 3c27611

Browse files
committed
feat(lit-client, networks): handle edge cases
1 parent cbfc8cb commit 3c27611

File tree

6 files changed

+621
-375
lines changed

6 files changed

+621
-375
lines changed

packages/e2e/src/helper/shiva-client.ts renamed to packages/e2e/src/helper/ShivaClient/createShivaClient.ts

Lines changed: 123 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
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
*/
5852
type 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

92108
const 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
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
type EpochInfo = {
2+
epochLength: number;
3+
number: number;
4+
endTime: number;
5+
retries: number;
6+
timeout: number;
7+
};
8+
9+
type EpochState = {
10+
currentNumber: number;
11+
startTime: number;
12+
};
13+
14+
type NetworkPrice = {
15+
url: string;
16+
prices: Array<number | bigint>;
17+
};
18+
19+
type PriceFeedInfo = {
20+
epochId: number;
21+
minNodeCount: number;
22+
networkPrices: NetworkPrice[];
23+
};
24+
25+
type LatestConnectionInfo = {
26+
epochInfo: EpochInfo;
27+
epochState: EpochState;
28+
minNodeCount: number;
29+
bootstrapUrls: string[];
30+
priceFeedInfo: PriceFeedInfo;
31+
};
32+
33+
type ServerKeyDetails = {
34+
serverPublicKey: string;
35+
subnetPublicKey: string;
36+
networkPublicKey: string;
37+
networkPublicKeySet: string;
38+
clientSdkVersion: string;
39+
hdRootPubkeys: string[];
40+
attestation?: string | null;
41+
latestBlockhash: string;
42+
nodeIdentityKey: string;
43+
nodeVersion: string;
44+
epoch: number;
45+
};
46+
47+
type CoreNodeConfig = {
48+
subnetPubKey: string;
49+
networkPubKey: string;
50+
networkPubKeySet: string;
51+
hdRootPubkeys: string[];
52+
latestBlockhash: string;
53+
};
54+
55+
type HandshakeResult = {
56+
serverKeys: Record<string, ServerKeyDetails>;
57+
connectedNodes: Record<string, unknown> | Set<string>;
58+
coreNodeConfig: CoreNodeConfig | null;
59+
threshold: number;
60+
};
61+
62+
type EpochSnapshotSource = {
63+
latestConnectionInfo?: LatestConnectionInfo | null;
64+
handshakeResult?: HandshakeResult | null;
65+
};
66+
67+
export type EpochSnapshot = EpochSnapshotSource;
68+
69+
export const createEpochSnapshot = async (
70+
litClient: Awaited<
71+
ReturnType<typeof import('@lit-protocol/lit-client').createLitClient>
72+
>
73+
): Promise<EpochSnapshot> => {
74+
const ctx = await litClient.getContext();
75+
76+
const snapshot = {
77+
latestConnectionInfo: ctx?.latestConnectionInfo,
78+
handshakeResult: ctx?.handshakeResult,
79+
};
80+
81+
return snapshot;
82+
};

packages/e2e/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export * from './helper/tests';
55
export { init } from './init';
66

77
export { getOrCreatePkp } from './helper/pkp-utils';
8-
export { createShivaClient } from './helper/shiva-client';
98
export { printAligned } from './helper/utils';
109
export type { AuthContext } from './types';
1110

@@ -16,3 +15,6 @@ export { createTestAccount } from './helper/createTestAccount';
1615
export { createTestEnv } from './helper/createTestEnv';
1716
export type { CreateTestAccountResult } from './helper/createTestAccount';
1817
export { registerPaymentDelegationTicketSuite } from './tickets/delegation.suite';
18+
19+
// -- Shiva
20+
export { createShivaClient } from './helper/ShivaClient/createShivaClient';

0 commit comments

Comments
 (0)