UNPKG

@lit-protocol/e2e

Version:

Lit Protocol E2E testing package for running comprehensive integration tests

178 lines 7.31 kB
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