@lit-protocol/e2e
Version:
Lit Protocol E2E testing package for running comprehensive integration tests
178 lines • 7.31 kB
JavaScript
import { createEpochSnapshot, } from './helpers/createEpochSnapshot';
const DEFAULT_POLL_INTERVAL = 2000;
const DEFAULT_TIMEOUT = 60_000;
const DEFAULT_STATE_POLL_INTERVAL = 2000;
const DEFAULT_STATE_POLL_TIMEOUT = 60_000;
const normaliseBaseUrl = (baseUrl) => {
return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
};
const toJson = async (response) => {
const text = await response.text();
try {
return JSON.parse(text);
}
catch (error) {
throw new Error(`Failed to parse Shiva response as JSON (status ${response.status}): ${text}`);
}
};
const fetchShiva = async (baseUrl, path, options = {}) => {
const url = `${normaliseBaseUrl(baseUrl)}${path.startsWith('/') ? '' : '/'}${path}`;
const response = await fetch(url, {
method: options.method ?? 'GET',
headers: options.method === 'POST'
? {
'Content-Type': 'application/json',
}
: undefined,
body: options.method === 'POST' && options.body
? JSON.stringify(options.body)
: undefined,
});
const parsed = await toJson(response);
if (!response.ok || (parsed.errors && parsed.errors.length > 0)) {
const message = parsed.errors?.join('; ') ??
`Shiva request failed with status ${response.status}`;
throw new Error(message);
}
return parsed;
};
const getTestnetIds = async (baseUrl) => {
const url = `${normaliseBaseUrl(baseUrl)}/test/get/testnets`;
const response = await fetch(url);
if (!response.ok) {
const body = await response.text();
throw new Error(`Failed to fetch testnets from Shiva (status ${response.status}): ${body}`);
}
return (await response.json());
};
const getOrCreateTestnetId = async (baseUrl, providedId, createRequest) => {
if (providedId) {
return providedId;
}
const existing = await getTestnetIds(baseUrl);
if (existing.length > 0) {
return existing[0];
}
if (!createRequest) {
throw new Error('No Shiva testnet is running. Provide a testnetId or a createRequest to start one.');
}
const response = await fetchShiva(baseUrl, '/test/create/testnet', {
method: 'POST',
body: createRequest,
});
if (!response.testnetId) {
throw new Error('Shiva create testnet response did not include testnetId. Received: ' +
JSON.stringify(response));
}
return response.testnetId;
};
/**
* Creates a Shiva client wrapper for the provided Lit client instance.
* The wrapper talks to the Shiva manager REST endpoints, auto-discovers (or optionally creates) a testnet,
* and exposes helpers for triggering and validating epoch transitions.
*/
export const createShivaClient = async (options) => {
const baseUrl = normaliseBaseUrl(options.baseUrl);
const testnetId = await getOrCreateTestnetId(baseUrl, options.testnetId, options.createRequest);
let litClientInstance;
const setLitClient = (client) => {
litClientInstance = client;
};
const inspectEpoch = async () => {
if (!litClientInstance) {
throw new Error(`Lit client not set. Please call setLitClient() before using inspectEpoch().`);
}
return createEpochSnapshot(litClientInstance);
};
const waitForEpochChange = async ({ expectedEpoch, timeoutMs = DEFAULT_TIMEOUT, intervalMs = DEFAULT_POLL_INTERVAL, }) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, intervalMs));
const snapshot = await inspectEpoch();
if (snapshot.latestConnectionInfo.epochState.currentNumber !== expectedEpoch) {
return snapshot;
}
}
throw new Error(`Epoch did not change from ${expectedEpoch} within ${timeoutMs}ms`);
};
const transitionEpochAndWait = async () => {
const response = await fetchShiva(baseUrl, `/test/action/transition/epoch/wait/${testnetId}`);
return Boolean(response.body);
};
const stopRandomNodeAndWait = async () => {
const response = await fetchShiva(baseUrl, `/test/action/stop/random/wait/${testnetId}`);
// wait briefly to allow the node to drop from the network
await new Promise((resolve) => setTimeout(resolve, 5000));
return Boolean(response.body);
};
const pollTestnetState = async (options = {}) => {
const { waitFor, timeoutMs = DEFAULT_STATE_POLL_TIMEOUT, intervalMs = DEFAULT_STATE_POLL_INTERVAL, } = options;
const desiredStates = Array.isArray(waitFor)
? waitFor
: waitFor
? [waitFor]
: undefined;
const deadline = Date.now() + timeoutMs;
// Continue polling until we hit a desired state or timeout.
// If no desired state is provided, return the first observation .
for (;;) {
const response = await fetchShiva(baseUrl, `/test/poll/testnet/${testnetId}`);
const state = (response.body ?? 'UNKNOWN');
if (!desiredStates || desiredStates.includes(state)) {
return state;
}
if (Date.now() >= deadline) {
throw new Error(`Timed out after ${timeoutMs}ms waiting for testnet ${testnetId} to reach state ${desiredStates.join(', ')}. Last observed state: ${state}.`);
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
};
const getTestnetInfo = async () => {
const response = await fetchShiva(baseUrl, `/test/get/info/testnet/${testnetId}`);
return response.body ?? null;
};
const waitForTestnetInfo = async (options = {}) => {
const { timeoutMs = DEFAULT_STATE_POLL_TIMEOUT, intervalMs = DEFAULT_STATE_POLL_INTERVAL, } = options;
const deadline = Date.now() + timeoutMs;
let lastError;
for (;;) {
try {
const info = await getTestnetInfo();
if (info) {
return info;
}
}
catch (error) {
lastError = error;
}
if (Date.now() >= deadline) {
const lastErrorMessage = lastError instanceof Error
? lastError.message
: lastError
? String(lastError)
: 'No response body received.';
throw new Error(`Timed out after ${timeoutMs}ms waiting for testnet info for ${testnetId}. Last error: ${lastErrorMessage}`);
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
};
const deleteTestnet = async () => {
const response = await fetchShiva(baseUrl, `/test/delete/testnet/${testnetId}`);
return Boolean(response.body);
};
return {
baseUrl,
testnetId,
setLitClient,
transitionEpochAndWait,
stopRandomNodeAndWait,
pollTestnetState,
getTestnetInfo,
waitForTestnetInfo,
deleteTestnet,
// utils
inspectEpoch,
waitForEpochChange,
};
};
//# sourceMappingURL=createShivaClient.js.map