From d12339d2994090569854dc79a09f81438d141515 Mon Sep 17 00:00:00 2001 From: anson Date: Mon, 3 Nov 2025 15:15:42 +0000 Subject: [PATCH 1/8] feat: add exports --- packages/e2e/src/index.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/e2e/src/index.ts b/packages/e2e/src/index.ts index 77331ae10..7e1b476ec 100644 --- a/packages/e2e/src/index.ts +++ b/packages/e2e/src/index.ts @@ -1,10 +1,17 @@ // re-export -export { init } from './init'; export * from './helper/auth-contexts'; -export * from './helper/tests'; export * from './helper/NetworkManager'; +export * from './helper/tests'; +export { init } from './init'; -export { printAligned } from './helper/utils'; export { getOrCreatePkp } from './helper/pkp-utils'; export { createShivaClient } from './helper/shiva-client'; +export { printAligned } from './helper/utils'; export type { AuthContext } from './types'; + +// re-export new helpers that should be used to refactor the `init.ts` proces +// see packages/e2e/src/tickets/delegation.suite.ts for usage examples +export { createEnvVars } from './helper/createEnvVars'; +export { createTestAccount } from './helper/createTestAccount'; +export { createTestEnv } from './helper/createTestEnv'; +export type { CreateTestAccountResult } from './helper/createTestAccount'; From 3ab3ad6bfcdd126fd97330f33d56f4d2d2aa0883 Mon Sep 17 00:00:00 2001 From: anson Date: Mon, 3 Nov 2025 16:16:02 +0000 Subject: [PATCH 2/8] feat(e2e): register payment delegation ticket suite in index export --- packages/e2e/src/e2e.spec.ts | 2 +- packages/e2e/src/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/e2e/src/e2e.spec.ts b/packages/e2e/src/e2e.spec.ts index b00707756..c2ea2f1d8 100644 --- a/packages/e2e/src/e2e.spec.ts +++ b/packages/e2e/src/e2e.spec.ts @@ -14,9 +14,9 @@ import { createViewPKPsByAddressTest, createViewPKPsByAuthDataTest, init, + registerPaymentDelegationTicketSuite, } from '@lit-protocol/e2e'; import type { AuthContext } from '@lit-protocol/e2e'; -import { registerPaymentDelegationTicketSuite } from './tickets/delegation.suite'; const RPC_OVERRIDE = process.env['LIT_YELLOWSTONE_PRIVATE_RPC_URL']; if (RPC_OVERRIDE) { diff --git a/packages/e2e/src/index.ts b/packages/e2e/src/index.ts index 7e1b476ec..eae8338e3 100644 --- a/packages/e2e/src/index.ts +++ b/packages/e2e/src/index.ts @@ -15,3 +15,4 @@ export { createEnvVars } from './helper/createEnvVars'; export { createTestAccount } from './helper/createTestAccount'; export { createTestEnv } from './helper/createTestEnv'; export type { CreateTestAccountResult } from './helper/createTestAccount'; +export { registerPaymentDelegationTicketSuite } from './tickets/delegation.suite'; From 0f1baecd6e0a8e9713b162ae63b707b63a393931 Mon Sep 17 00:00:00 2001 From: anson Date: Mon, 3 Nov 2025 17:57:20 +0000 Subject: [PATCH 3/8] refactor(WIP): making shiva work --- packages/e2e/src/helper/shiva-client.ts | 120 ++++++++++++------------ 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/packages/e2e/src/helper/shiva-client.ts b/packages/e2e/src/helper/shiva-client.ts index b8357531c..7e1cbfc7d 100644 --- a/packages/e2e/src/helper/shiva-client.ts +++ b/packages/e2e/src/helper/shiva-client.ts @@ -1,5 +1,3 @@ -import type { LitClientInstance } from '../types'; - /** * Options used when Shiva spins up a brand-new testnet instance. * Values mirror the Rust manager contract; all fields are optional for our wrapper. @@ -70,12 +68,12 @@ export type ShivaClient = { baseUrl: string; testnetId: string; /** Fetch a one-off snapshot of the Lit context and per-node epochs. */ - inspectEpoch: () => Promise; + // inspectEpoch: () => Promise; /** * Poll the Lit client until it reports an epoch different from {@link WaitForEpochOptions.baselineEpoch}. * Useful immediately after triggering an epoch change via Shiva. */ - waitForEpochChange: (options: WaitForEpochOptions) => Promise; + // waitForEpochChange: (options: WaitForEpochOptions) => Promise; /** Invoke Shiva's `/test/action/transition/epoch/wait/` and wait for completion. */ transitionEpochAndWait: () => Promise; /** Stop a random node and wait for the subsequent epoch change. */ @@ -153,7 +151,7 @@ const getTestnetIds = async (baseUrl: string): Promise => { return (await response.json()) as string[]; }; -const ensureTestnetId = async ( +const getOrCreateTestnetId = async ( baseUrl: string, providedId?: string, createRequest?: TestNetCreateRequest @@ -179,38 +177,41 @@ const ensureTestnetId = async ( }); if (!response.testnet_id) { - throw new Error('Shiva create testnet response did not include testnet_id'); + throw new Error( + 'Shiva create testnet response did not include testnet_id. Received: ' + + JSON.stringify(response) + ); } return response.testnet_id; }; -const buildEpochSnapshot = (ctx: any): EpochSnapshot => { - const nodeEpochEntries = Object.entries( - ctx?.handshakeResult?.serverKeys ?? {} - ); - const nodeEpochs = nodeEpochEntries.map(([url, data]: [string, any]) => ({ - url, - epoch: data?.epoch, - })); - - const connected = ctx?.handshakeResult?.connectedNodes; - const connectedCount = - typeof connected?.size === 'number' - ? connected.size - : Array.isArray(connected) - ? connected.length - : undefined; - - return { - epoch: ctx?.latestConnectionInfo?.epochInfo?.number, - nodeEpochs, - threshold: ctx?.handshakeResult?.threshold, - connectedCount, - latestBlockhash: ctx?.latestBlockhash, - rawContext: ctx, - }; -}; +// const buildEpochSnapshot = (ctx: any): EpochSnapshot => { +// const nodeEpochEntries = Object.entries( +// ctx?.handshakeResult?.serverKeys ?? {} +// ); +// const nodeEpochs = nodeEpochEntries.map(([url, data]: [string, any]) => ({ +// url, +// epoch: data?.epoch, +// })); + +// const connected = ctx?.handshakeResult?.connectedNodes; +// const connectedCount = +// typeof connected?.size === 'number' +// ? connected.size +// : Array.isArray(connected) +// ? connected.length +// : undefined; + +// return { +// epoch: ctx?.latestConnectionInfo?.epochInfo?.number, +// nodeEpochs, +// threshold: ctx?.handshakeResult?.threshold, +// connectedCount, +// latestBlockhash: ctx?.latestBlockhash, +// rawContext: ctx, +// }; +// }; /** * Creates a Shiva client wrapper for the provided Lit client instance. @@ -218,40 +219,39 @@ const buildEpochSnapshot = (ctx: any): EpochSnapshot => { * and exposes helpers for triggering and validating epoch transitions. */ export const createShivaClient = async ( - litClient: LitClientInstance, options: CreateShivaClientOptions ): Promise => { const baseUrl = normaliseBaseUrl(options.baseUrl); - const testnetId = await ensureTestnetId( + const testnetId = await getOrCreateTestnetId( baseUrl, options.testnetId, options.createRequest ); - const inspectEpoch = async () => { - const ctx = await litClient.getContext(); - return buildEpochSnapshot(ctx); - }; - - const waitForEpochChange = async ({ - baselineEpoch, - timeoutMs = DEFAULT_TIMEOUT, - intervalMs = DEFAULT_POLL_INTERVAL, - }: WaitForEpochOptions) => { - const deadline = Date.now() + timeoutMs; - - while (Date.now() < deadline) { - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - const snapshot = await inspectEpoch(); - if (snapshot.epoch !== baselineEpoch) { - return snapshot; - } - } - - throw new Error( - `Epoch did not change from ${baselineEpoch} within ${timeoutMs}ms` - ); - }; + // const inspectEpoch = async () => { + // const ctx = await litClient.getContext(); + // return buildEpochSnapshot(ctx); + // }; + + // const waitForEpochChange = async ({ + // baselineEpoch, + // timeoutMs = DEFAULT_TIMEOUT, + // intervalMs = DEFAULT_POLL_INTERVAL, + // }: WaitForEpochOptions) => { + // const deadline = Date.now() + timeoutMs; + + // while (Date.now() < deadline) { + // await new Promise((resolve) => setTimeout(resolve, intervalMs)); + // const snapshot = await inspectEpoch(); + // if (snapshot.epoch !== baselineEpoch) { + // return snapshot; + // } + // } + + // throw new Error( + // `Epoch did not change from ${baselineEpoch} within ${timeoutMs}ms` + // ); + // }; const transitionEpochAndWait = async () => { const response = await fetchShiva( @@ -296,8 +296,8 @@ export const createShivaClient = async ( return { baseUrl, testnetId, - inspectEpoch, - waitForEpochChange, + // inspectEpoch, + // waitForEpochChange, transitionEpochAndWait, stopRandomNodeAndWait, pollTestnetState, From cbfc8cb3ebc1d239a1c3f511355737069f2cfee9 Mon Sep 17 00:00:00 2001 From: anson Date: Mon, 3 Nov 2025 18:11:49 +0000 Subject: [PATCH 4/8] refactor: standardize response property names in TestNetResponse --- packages/e2e/src/helper/shiva-client.ts | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/e2e/src/helper/shiva-client.ts b/packages/e2e/src/helper/shiva-client.ts index 7e1cbfc7d..f70f6dcde 100644 --- a/packages/e2e/src/helper/shiva-client.ts +++ b/packages/e2e/src/helper/shiva-client.ts @@ -15,11 +15,11 @@ type TestNetCreateRequest = { }; type TestNetResponse = { - testnet_id: string; + testnetId: string; command: string; - was_canceled: boolean; + wasCanceled: boolean; body: T | null; - last_state_observed: string | null; + lastStateObserved: string | null; messages: string[] | null; errors: string[] | null; }; @@ -43,14 +43,14 @@ type FetchOptions = { /** * Snapshot returned from {@link ShivaClient.inspectEpoch} and {@link ShivaClient.waitForEpochChange}. */ -type EpochSnapshot = { - epoch: number | undefined; - nodeEpochs: Array<{ url: string; epoch: number | undefined }>; - threshold: number | undefined; - connectedCount: number | undefined; - latestBlockhash: string | undefined; - rawContext: any; -}; +// type EpochSnapshot = { +// epoch: number | undefined; +// nodeEpochs: Array<{ url: string; epoch: number | undefined }>; +// threshold: number | undefined; +// connectedCount: number | undefined; +// latestBlockhash: string | undefined; +// rawContext: any; +// }; /** * Options for {@link ShivaClient.waitForEpochChange}. @@ -86,8 +86,8 @@ export type ShivaClient = { deleteTestnet: () => Promise; }; -const DEFAULT_POLL_INTERVAL = 2000; -const DEFAULT_TIMEOUT = 60_000; +// const DEFAULT_POLL_INTERVAL = 2000; +// const DEFAULT_TIMEOUT = 60_000; const normaliseBaseUrl = (baseUrl: string) => { return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; @@ -176,14 +176,14 @@ const getOrCreateTestnetId = async ( body: createRequest, }); - if (!response.testnet_id) { + if (!response.testnetId) { throw new Error( - 'Shiva create testnet response did not include testnet_id. Received: ' + + 'Shiva create testnet response did not include testnetId. Received: ' + JSON.stringify(response) ); } - return response.testnet_id; + return response.testnetId; }; // const buildEpochSnapshot = (ctx: any): EpochSnapshot => { From 3c27611390d07131f5706c9a8852b4eed53db60d Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 5 Nov 2025 16:40:37 +0000 Subject: [PATCH 5/8] feat(lit-client, networks): handle edge cases --- .../createShivaClient.ts} | 200 ++++---- .../helpers/createEpochSnapshot.ts | 82 ++++ packages/e2e/src/index.ts | 4 +- .../src/lib/LitClient/createLitClient.ts | 435 ++++++++---------- .../LitClient/helpers/executeWithHandshake.ts | 149 ++++++ .../state-manager/createStateManager.ts | 126 ++--- 6 files changed, 621 insertions(+), 375 deletions(-) rename packages/e2e/src/helper/{shiva-client.ts => ShivaClient/createShivaClient.ts} (64%) create mode 100644 packages/e2e/src/helper/ShivaClient/helpers/createEpochSnapshot.ts create mode 100644 packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts diff --git a/packages/e2e/src/helper/shiva-client.ts b/packages/e2e/src/helper/ShivaClient/createShivaClient.ts similarity index 64% rename from packages/e2e/src/helper/shiva-client.ts rename to packages/e2e/src/helper/ShivaClient/createShivaClient.ts index f70f6dcde..739bd619c 100644 --- a/packages/e2e/src/helper/shiva-client.ts +++ b/packages/e2e/src/helper/ShivaClient/createShivaClient.ts @@ -1,3 +1,9 @@ +import { createLitClient } from '@lit-protocol/lit-client'; +import { + createEpochSnapshot, + EpochSnapshot, +} from './helpers/createEpochSnapshot'; + /** * Options used when Shiva spins up a brand-new testnet instance. * Values mirror the Rust manager contract; all fields are optional for our wrapper. @@ -40,23 +46,17 @@ type FetchOptions = { body?: unknown; }; -/** - * Snapshot returned from {@link ShivaClient.inspectEpoch} and {@link ShivaClient.waitForEpochChange}. - */ -// type EpochSnapshot = { -// epoch: number | undefined; -// nodeEpochs: Array<{ url: string; epoch: number | undefined }>; -// threshold: number | undefined; -// connectedCount: number | undefined; -// latestBlockhash: string | undefined; -// rawContext: any; -// }; - /** * Options for {@link ShivaClient.waitForEpochChange}. */ type WaitForEpochOptions = { - baselineEpoch: number | undefined; + expectedEpoch: number | undefined; + timeoutMs?: number; + intervalMs?: number; +}; + +type PollTestnetStateOptions = { + waitFor?: TestNetState | TestNetState[]; timeoutMs?: number; intervalMs?: number; }; @@ -68,26 +68,42 @@ export type ShivaClient = { baseUrl: string; testnetId: string; /** Fetch a one-off snapshot of the Lit context and per-node epochs. */ - // inspectEpoch: () => Promise; + inspectEpoch: () => Promise; /** * Poll the Lit client until it reports an epoch different from {@link WaitForEpochOptions.baselineEpoch}. * Useful immediately after triggering an epoch change via Shiva. */ - // waitForEpochChange: (options: WaitForEpochOptions) => Promise; + waitForEpochChange: (options: WaitForEpochOptions) => Promise; /** Invoke Shiva's `/test/action/transition/epoch/wait/` and wait for completion. */ transitionEpochAndWait: () => Promise; /** Stop a random node and wait for the subsequent epoch change. */ stopRandomNodeAndWait: () => Promise; /** Query the current state of the managed testnet (Busy, Active, etc.). */ - pollTestnetState: () => Promise; + /** + * @example + * ```ts + * // Wait up to two minutes for the testnet to become active. + * await client.pollTestnetState({ waitFor: 'Active', timeoutMs: 120_000 }); + * ``` + */ + pollTestnetState: ( + options?: PollTestnetStateOptions + ) => Promise; /** Retrieve the full testnet configuration (contract ABIs, RPC URL, etc.). */ getTestnetInfo: () => Promise; /** Shut down the underlying testnet through the Shiva manager. */ deleteTestnet: () => Promise; + + // Setters + setLitClient: ( + litClient: Awaited> + ) => void; }; -// const DEFAULT_POLL_INTERVAL = 2000; -// const DEFAULT_TIMEOUT = 60_000; +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: string) => { return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; @@ -186,33 +202,6 @@ const getOrCreateTestnetId = async ( return response.testnetId; }; -// const buildEpochSnapshot = (ctx: any): EpochSnapshot => { -// const nodeEpochEntries = Object.entries( -// ctx?.handshakeResult?.serverKeys ?? {} -// ); -// const nodeEpochs = nodeEpochEntries.map(([url, data]: [string, any]) => ({ -// url, -// epoch: data?.epoch, -// })); - -// const connected = ctx?.handshakeResult?.connectedNodes; -// const connectedCount = -// typeof connected?.size === 'number' -// ? connected.size -// : Array.isArray(connected) -// ? connected.length -// : undefined; - -// return { -// epoch: ctx?.latestConnectionInfo?.epochInfo?.number, -// nodeEpochs, -// threshold: ctx?.handshakeResult?.threshold, -// connectedCount, -// latestBlockhash: ctx?.latestBlockhash, -// rawContext: ctx, -// }; -// }; - /** * 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, @@ -228,30 +217,47 @@ export const createShivaClient = async ( options.createRequest ); - // const inspectEpoch = async () => { - // const ctx = await litClient.getContext(); - // return buildEpochSnapshot(ctx); - // }; - - // const waitForEpochChange = async ({ - // baselineEpoch, - // timeoutMs = DEFAULT_TIMEOUT, - // intervalMs = DEFAULT_POLL_INTERVAL, - // }: WaitForEpochOptions) => { - // const deadline = Date.now() + timeoutMs; - - // while (Date.now() < deadline) { - // await new Promise((resolve) => setTimeout(resolve, intervalMs)); - // const snapshot = await inspectEpoch(); - // if (snapshot.epoch !== baselineEpoch) { - // return snapshot; - // } - // } - - // throw new Error( - // `Epoch did not change from ${baselineEpoch} within ${timeoutMs}ms` - // ); - // }; + let litClientInstance: + | Awaited> + | undefined; + + const setLitClient = ( + client: Awaited> + ) => { + 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, + }: WaitForEpochOptions) => { + 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( @@ -266,15 +272,52 @@ export const createShivaClient = async ( 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 () => { - const response = await fetchShiva( - baseUrl, - `/test/poll/testnet/${testnetId}` - ); - return (response.body ?? 'UNKNOWN') as TestNetState; + const pollTestnetState = async ( + options: PollTestnetStateOptions = {} + ): Promise => { + 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') as TestNetState; + + 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 () => { @@ -296,12 +339,15 @@ export const createShivaClient = async ( return { baseUrl, testnetId, - // inspectEpoch, - // waitForEpochChange, + setLitClient, transitionEpochAndWait, stopRandomNodeAndWait, pollTestnetState, getTestnetInfo, deleteTestnet, + + // utils + inspectEpoch, + waitForEpochChange, }; }; diff --git a/packages/e2e/src/helper/ShivaClient/helpers/createEpochSnapshot.ts b/packages/e2e/src/helper/ShivaClient/helpers/createEpochSnapshot.ts new file mode 100644 index 000000000..18c9b4fb7 --- /dev/null +++ b/packages/e2e/src/helper/ShivaClient/helpers/createEpochSnapshot.ts @@ -0,0 +1,82 @@ +type EpochInfo = { + epochLength: number; + number: number; + endTime: number; + retries: number; + timeout: number; +}; + +type EpochState = { + currentNumber: number; + startTime: number; +}; + +type NetworkPrice = { + url: string; + prices: Array; +}; + +type PriceFeedInfo = { + epochId: number; + minNodeCount: number; + networkPrices: NetworkPrice[]; +}; + +type LatestConnectionInfo = { + epochInfo: EpochInfo; + epochState: EpochState; + minNodeCount: number; + bootstrapUrls: string[]; + priceFeedInfo: PriceFeedInfo; +}; + +type ServerKeyDetails = { + serverPublicKey: string; + subnetPublicKey: string; + networkPublicKey: string; + networkPublicKeySet: string; + clientSdkVersion: string; + hdRootPubkeys: string[]; + attestation?: string | null; + latestBlockhash: string; + nodeIdentityKey: string; + nodeVersion: string; + epoch: number; +}; + +type CoreNodeConfig = { + subnetPubKey: string; + networkPubKey: string; + networkPubKeySet: string; + hdRootPubkeys: string[]; + latestBlockhash: string; +}; + +type HandshakeResult = { + serverKeys: Record; + connectedNodes: Record | Set; + coreNodeConfig: CoreNodeConfig | null; + threshold: number; +}; + +type EpochSnapshotSource = { + latestConnectionInfo?: LatestConnectionInfo | null; + handshakeResult?: HandshakeResult | null; +}; + +export type EpochSnapshot = EpochSnapshotSource; + +export const createEpochSnapshot = async ( + litClient: Awaited< + ReturnType + > +): Promise => { + const ctx = await litClient.getContext(); + + const snapshot = { + latestConnectionInfo: ctx?.latestConnectionInfo, + handshakeResult: ctx?.handshakeResult, + }; + + return snapshot; +}; diff --git a/packages/e2e/src/index.ts b/packages/e2e/src/index.ts index eae8338e3..642edf82c 100644 --- a/packages/e2e/src/index.ts +++ b/packages/e2e/src/index.ts @@ -5,7 +5,6 @@ export * from './helper/tests'; export { init } from './init'; export { getOrCreatePkp } from './helper/pkp-utils'; -export { createShivaClient } from './helper/shiva-client'; export { printAligned } from './helper/utils'; export type { AuthContext } from './types'; @@ -16,3 +15,6 @@ export { createTestAccount } from './helper/createTestAccount'; export { createTestEnv } from './helper/createTestEnv'; export type { CreateTestAccountResult } from './helper/createTestAccount'; export { registerPaymentDelegationTicketSuite } from './tickets/delegation.suite'; + +// -- Shiva +export { createShivaClient } from './helper/ShivaClient/createShivaClient'; diff --git a/packages/lit-client/src/lib/LitClient/createLitClient.ts b/packages/lit-client/src/lib/LitClient/createLitClient.ts index f497d1a27..79f7afb28 100644 --- a/packages/lit-client/src/lib/LitClient/createLitClient.ts +++ b/packages/lit-client/src/lib/LitClient/createLitClient.ts @@ -60,6 +60,7 @@ import { extractFileMetadata, inferDataType, } from './helpers/convertDecryptedData'; +import { executeWithHandshake } from './helpers/executeWithHandshake'; import { createPKPViemAccount } from './intergrations/createPkpViemAccount'; import { orchestrateHandshake } from './orchestrateHandshake'; import { @@ -200,148 +201,155 @@ export const _createNagaLitClient = async ( ); } - async function _pkpSign( - params: z.infer & { - bypassAutoHashing?: boolean; - } - ): Promise { - _logger.info( - `🔥 signing on ${params.chain} with ${params.signingScheme} (bypass: ${ - params.bypassAutoHashing || false - })` - ); - - // 🟩 get the fresh handshake results - const currentHandshakeResult = _stateManager.getCallbackResult(); - const currentConnectionInfo = _stateManager.getLatestConnectionInfo(); + const buildHandshakeExecutionContext = async () => { + const handshakeResult = _stateManager.getCallbackResult(); + const connectionInfo = _stateManager.getLatestConnectionInfo(); - if (!currentHandshakeResult || !currentConnectionInfo) { + if (!handshakeResult || !connectionInfo) { throw new LitNodeClientNotReadyError( { - cause: new Error('Handshake result unavailable for pkpSign'), + cause: new Error( + 'Handshake result unavailable while building execution context' + ), info: { - operation: 'pkpSign', + operation: 'buildHandshakeExecutionContext', }, }, - 'Handshake result is not available from state manager at the time of pkpSign.' + 'Handshake result is not available from state manager.' ); } const jitContext = await networkModule.api.createJitContext( - currentConnectionInfo, - currentHandshakeResult + connectionInfo, + handshakeResult ); - // 🟪 Create requests - // 1. This is where the orchestration begins — we delegate the creation of the - // request array to the `networkModule`. It encapsulates logic specific to the - // active network (e.g., pricing, thresholds, metadata) and returns a set of - // structured requests ready to be dispatched to the nodes. - - // Create signing context with optional bypass flag - const signingContext: any = { - pubKey: params.pubKey, - toSign: params.toSign, - signingScheme: params.signingScheme, - }; + return { handshakeResult, connectionInfo, jitContext }; + }; - // Add bypass flag if provided - if (params.bypassAutoHashing) { - signingContext.bypassAutoHashing = true; + const refreshHandshakeExecutionContext = async (reason: string) => { + if (typeof _stateManager.refreshHandshake === 'function') { + _logger.info({ reason }, 'Refreshing handshake via state manager'); + await _stateManager.refreshHandshake(reason); + } else { + _logger.warn( + { reason }, + 'State manager does not expose refreshHandshake; proceeding without manual refresh.' + ); } + return await buildHandshakeExecutionContext(); + }; - const requestArray = (await networkModule.api.pkpSign.createRequest({ - // add chain context (btc, eth, cosmos, solana) - serverKeys: currentHandshakeResult.serverKeys, - pricingContext: { - product: 'SIGN', - userMaxPrice: params.userMaxPrice, - nodePrices: jitContext.nodePrices, - threshold: currentHandshakeResult.threshold, - }, - authContext: params.authContext, - signingContext, - connectionInfo: currentConnectionInfo, - version: networkModule.version, - chain: params.chain, - jitContext, - })) as RequestItem>[]; - - const requestId = requestArray[0].requestId; - - // 🟩 Dispatch requests - // 2. With the request array prepared, we now coordinate the parallel execution - // across multiple nodes. This step handles batching, minimum threshold success - // tracking, and error tolerance. The orchestration layer ensures enough valid - // responses are collected before proceeding. - const result = await dispatchRequests< - z.infer, - z.infer - >(requestArray, requestId, currentHandshakeResult.threshold); + async function _pkpSign( + params: z.infer & { + bypassAutoHashing?: boolean; + } + ): Promise { + _logger.info( + `🔥 signing on ${params.chain} with ${params.signingScheme} (bypass: ${ + params.bypassAutoHashing || false + })` + ); - // 🟪 Handle response - // 3. Once node responses are received and validated, we delegate final - // interpretation and formatting of the result back to the `networkModule`. - // This allows the module to apply network-specific logic such as decoding, - // formatting, or transforming the response into a usable signature object. + return await executeWithHandshake({ + operation: 'pkpSign', + buildContext: buildHandshakeExecutionContext, + refreshContext: refreshHandshakeExecutionContext, + runner: async ({ handshakeResult, connectionInfo, jitContext }) => { + // 🟪 Create requests + // 1. This is where the orchestration begins — we delegate the creation of the + // request array to the `networkModule`. It encapsulates logic specific to the + // active network (e.g., pricing, thresholds, metadata) and returns a set of + // structured requests ready to be dispatched to the nodes. + + const signingContext: any = { + pubKey: params.pubKey, + toSign: params.toSign, + signingScheme: params.signingScheme, + }; + + if (params.bypassAutoHashing) { + signingContext.bypassAutoHashing = true; + } - // Pass the success result to handleResponse - the result structure matches GenericEncryptedPayloadSchema - return await networkModule.api.pkpSign.handleResponse( - result as any, - requestId, - jitContext - ); + const requestArray = (await networkModule.api.pkpSign.createRequest({ + serverKeys: handshakeResult.serverKeys, + pricingContext: { + product: 'SIGN', + userMaxPrice: params.userMaxPrice, + nodePrices: jitContext.nodePrices, + threshold: handshakeResult.threshold, + }, + authContext: params.authContext, + signingContext, + connectionInfo, + version: networkModule.version, + chain: params.chain, + jitContext, + })) as RequestItem>[]; + + const requestId = requestArray[0].requestId; + + // 🟩 Dispatch requests + // 2. With the request array prepared, we now coordinate the parallel execution + // across multiple nodes. This step handles batching, minimum threshold success + // tracking, and error tolerance. The orchestration layer ensures enough valid + // responses are collected before proceeding. + const result = await dispatchRequests< + z.infer, + z.infer + >(requestArray, requestId, handshakeResult.threshold); + + // 🟪 Handle response + // 3. Once node responses are received and validated, we delegate final + // interpretation and formatting of the result back to the `networkModule`. + // This allows the module to apply network-specific logic such as decoding, + // formatting, or transforming the response into a usable signature object. + return await networkModule.api.pkpSign.handleResponse( + result as any, + requestId, + jitContext + ); + }, + }); } async function _signSessionKey(params: { nodeUrls: string[]; requestBody: z.infer; }) { - // 1. 🟩 get the fresh handshake results - const currentHandshakeResult = _stateManager.getCallbackResult(); - const currentConnectionInfo = _stateManager.getLatestConnectionInfo(); - - if (!currentHandshakeResult || !currentConnectionInfo) { - throw new LitNodeClientNotReadyError( - { - cause: new Error('Handshake result unavailable for signSessionKey'), - info: { - operation: 'signSessionKey', - }, - }, - 'Handshake result is not available from state manager at the time of pkpSign.' - ); - } - - const jitContext = await networkModule.api.createJitContext( - currentConnectionInfo, - currentHandshakeResult - ); - - // 2. 🟪 Create requests - const requestArray = await networkModule.api.signSessionKey.createRequest( - params.requestBody, - networkModule.config.httpProtocol, - networkModule.version, - jitContext - ); - - const requestId = requestArray[0].requestId; - - // 3. 🟩 Dispatch requests - const result = await dispatchRequests( - requestArray, - requestId, - currentHandshakeResult.threshold - ); + return await executeWithHandshake({ + operation: 'signSessionKey', + buildContext: buildHandshakeExecutionContext, + refreshContext: refreshHandshakeExecutionContext, + runner: async ({ handshakeResult, connectionInfo, jitContext }) => { + // 2. 🟪 Create requests + const requestArray = + await networkModule.api.signSessionKey.createRequest( + params.requestBody, + networkModule.config.httpProtocol, + networkModule.version, + jitContext + ); - // 4. 🟪 Handle response - return await networkModule.api.signSessionKey.handleResponse( - result as any, - params.requestBody.pkpPublicKey, - jitContext, - requestId - ); + const requestId = requestArray[0].requestId; + + // 3. 🟩 Dispatch requests + const result = await dispatchRequests( + requestArray, + requestId, + handshakeResult.threshold + ); + + // 4. 🟪 Handle response + return await networkModule.api.signSessionKey.handleResponse( + result as any, + params.requestBody.pkpPublicKey, + jitContext, + requestId + ); + }, + }); } async function _signCustomSessionKey(params: { @@ -350,68 +358,35 @@ export const _createNagaLitClient = async ( typeof JsonSignCustomSessionKeyRequestForPkpReturnSchema >; }) { - // 1. 🟩 get the fresh handshake results - const currentHandshakeResult = _stateManager.getCallbackResult(); - const currentConnectionInfo = _stateManager.getLatestConnectionInfo(); - - if (!currentHandshakeResult || !currentConnectionInfo) { - throw new LitNodeClientNotReadyError( - { - cause: new Error( - 'Handshake result unavailable for signCustomSessionKey' - ), - info: { - operation: 'signCustomSessionKey', - }, - }, - 'Handshake result is not available from state manager at the time of pkpSign.' - ); - } - - const jitContext = await networkModule.api.createJitContext( - currentConnectionInfo, - currentHandshakeResult - ); - - if (!currentHandshakeResult || !currentConnectionInfo) { - throw new LitNodeClientNotReadyError( - { - cause: new Error( - 'Handshake result unavailable for signCustomSessionKey' - ), - info: { - operation: 'signCustomSessionKey', - }, - }, - 'Handshake result is not available from state manager at the time of pkpSign.' - ); - } + return await executeWithHandshake({ + operation: 'signCustomSessionKey', + buildContext: buildHandshakeExecutionContext, + refreshContext: refreshHandshakeExecutionContext, + runner: async ({ handshakeResult, jitContext }) => { + const requestArray = + await networkModule.api.signCustomSessionKey.createRequest( + params.requestBody, + networkModule.config.httpProtocol, + networkModule.version, + jitContext + ); - // 2. 🟪 Create requests - const requestArray = - await networkModule.api.signCustomSessionKey.createRequest( - params.requestBody, - networkModule.config.httpProtocol, - networkModule.version, - jitContext - ); + const requestId = requestArray[0].requestId; - const requestId = requestArray[0].requestId; - - // 3. 🟩 Dispatch requests - const result = await dispatchRequests( - requestArray, - requestId, - currentHandshakeResult.threshold - ); + const result = await dispatchRequests( + requestArray, + requestId, + handshakeResult.threshold + ); - // 4. 🟪 Handle response - return await networkModule.api.signCustomSessionKey.handleResponse( - result as any, - params.requestBody.pkpPublicKey, - jitContext, - requestId - ); + return await networkModule.api.signCustomSessionKey.handleResponse( + result as any, + params.requestBody.pkpPublicKey, + jitContext, + requestId + ); + }, + }); } async function _executeJs( @@ -419,75 +394,45 @@ export const _createNagaLitClient = async ( ) { _logger.info(`🔥 executing JS with ${params.code ? 'code' : 'ipfsId'}`); - // 🟩 get the fresh handshake results - const currentHandshakeResult = _stateManager.getCallbackResult(); - const currentConnectionInfo = _stateManager.getLatestConnectionInfo(); - - if (!currentHandshakeResult || !currentConnectionInfo) { - throw new LitNodeClientNotReadyError( - { - cause: new Error('Handshake result unavailable for executeJs'), - info: { - operation: 'executeJs', + return await executeWithHandshake({ + operation: 'executeJs', + buildContext: buildHandshakeExecutionContext, + refreshContext: refreshHandshakeExecutionContext, + runner: async ({ handshakeResult, connectionInfo, jitContext }) => { + const requestArray = (await networkModule.api.executeJs.createRequest({ + pricingContext: { + product: 'LIT_ACTION', + userMaxPrice: params.userMaxPrice, + nodePrices: jitContext.nodePrices, + threshold: handshakeResult.threshold, }, - }, - 'Handshake result is not available from state manager at the time of executeJs.' - ); - } - - const jitContext = await networkModule.api.createJitContext( - currentConnectionInfo, - currentHandshakeResult - ); - - // 🟪 Create requests - // 1. This is where the orchestration begins — we delegate the creation of the - // request array to the `networkModule`. It encapsulates logic specific to the - // active network (e.g., pricing, thresholds, metadata) and returns a set of - // structured requests ready to be dispatched to the nodes. - const requestArray = (await networkModule.api.executeJs.createRequest({ - // add pricing context for Lit Actions - pricingContext: { - product: 'LIT_ACTION', - userMaxPrice: params.userMaxPrice, - nodePrices: jitContext.nodePrices, - threshold: currentHandshakeResult.threshold, - }, - authContext: params.authContext, - executionContext: { - code: params.code, - ipfsId: params.ipfsId, - jsParams: params.jsParams, + authContext: params.authContext, + executionContext: { + code: params.code, + ipfsId: params.ipfsId, + jsParams: params.jsParams, + }, + connectionInfo, + version: networkModule.version, + useSingleNode: params.useSingleNode, + responseStrategy: params.responseStrategy, + jitContext, + })) as RequestItem>[]; + + const requestId = requestArray[0].requestId; + + const result = await dispatchRequests< + z.infer, + z.infer + >(requestArray, requestId, handshakeResult.threshold); + + return await networkModule.api.executeJs.handleResponse( + result as any, + requestId, + jitContext + ); }, - connectionInfo: currentConnectionInfo, - version: networkModule.version, - useSingleNode: params.useSingleNode, - responseStrategy: params.responseStrategy, - jitContext, - })) as RequestItem>[]; - - const requestId = requestArray[0].requestId; - - // 🟩 Dispatch requests - // 2. With the request array prepared, we now coordinate the parallel execution - // across multiple nodes. This step handles batching, minimum threshold success - // tracking, and error tolerance. The orchestration layer ensures enough valid - // responses are collected before proceeding. - const result = await dispatchRequests< - z.infer, - z.infer - >(requestArray, requestId, currentHandshakeResult.threshold); - - // 🟪 Handle response - // 3. Once node responses are received and validated, we delegate final - // interpretation and formatting of the result back to the `networkModule`. - // This allows the module to apply network-specific logic such as decoding, - // formatting, or transforming the response into a usable executeJs result. - return await networkModule.api.executeJs.handleResponse( - result as any, - requestId, - jitContext - ); + }); } /** @@ -636,7 +581,7 @@ export const _createNagaLitClient = async ( // ========== Hash Private Data ========== const hashOfPrivateData = await crypto.subtle.digest( 'SHA-256', - dataAsUint8Array + dataAsUint8Array as BufferSource ); const hashOfPrivateDataStr = uint8arrayToString( new Uint8Array(hashOfPrivateData), diff --git a/packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts b/packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts new file mode 100644 index 000000000..e75b6a31a --- /dev/null +++ b/packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts @@ -0,0 +1,149 @@ +import { getChildLogger } from '@lit-protocol/logger'; +import type { OrchestrateHandshakeResponse } from '../orchestrateHandshake'; + +const _logger = getChildLogger({ + module: 'executeWithHandshake', +}); + +export interface HandshakeExecutionContext { + handshakeResult: OrchestrateHandshakeResponse; + connectionInfo: any; + jitContext: any; +} + +export interface ExecuteWithHandshakeOptions { + operation: string; + buildContext: () => Promise; + refreshContext: (reason: string) => Promise; + runner: (context: HandshakeExecutionContext) => Promise; +} + +type RetryMetadata = { + shouldRetry: boolean; + reason: string; +}; + +export namespace EdgeCase { + export const isMissingVerificationKeyError = (error: unknown): boolean => { + if (!error || typeof error !== 'object') { + return false; + } + + const message = (error as any).message; + const causeMessage = (error as any).cause?.message; + const infoVerificationKey = (error as any).info?.verificationKey; + + const messages = [message, causeMessage].filter( + (text): text is string => typeof text === 'string' + ); + + if ( + messages.some((text) => + text.includes('No secret key found for verification key') + ) + ) { + return true; + } + + return ( + typeof infoVerificationKey === 'string' && + messages.some((text) => + text.includes('No secret key found for verification key') + ) + ); + }; + + export const isNetworkFetchError = (error: unknown): boolean => { + if (!error || typeof error !== 'object') { + return false; + } + + const name = (error as any).name; + const code = (error as any).code; + const infoFullPath = (error as any).info?.fullPath; + const messages = [ + (error as any).message, + (error as any).cause?.message, + ].filter((text): text is string => typeof text === 'string'); + + if (name === 'NetworkError' || code === 'network_error') { + return true; + } + + if ( + messages.some((text) => text.toLowerCase().includes('fetch failed')) || + (typeof infoFullPath === 'string' && + infoFullPath.toLowerCase().includes('execute')) + ) { + return true; + } + + return false; + }; + + export const isNoValidSharesError = (error: unknown): boolean => { + if (!error || typeof error !== 'object') { + return false; + } + + const name = (error as any).name; + const message = (error as any).message as string | undefined; + + if (name === 'NoValidShares') { + return true; + } + + return ( + typeof message === 'string' && + message.toLowerCase().includes('no valid lit action shares to combine') + ); + }; +} + +const deriveRetryMetadata = (error: unknown): RetryMetadata => { + if (EdgeCase.isMissingVerificationKeyError(error)) { + return { shouldRetry: true, reason: 'missing-verification-key' }; + } + + if (EdgeCase.isNetworkFetchError(error)) { + return { shouldRetry: true, reason: 'network-fetch-error' }; + } + + if (EdgeCase.isNoValidSharesError(error)) { + return { shouldRetry: true, reason: 'no-valid-shares' }; + } + + return { shouldRetry: false, reason: '' }; +}; + +export const executeWithHandshake = async ( + options: ExecuteWithHandshakeOptions +): Promise => { + const { operation, buildContext, refreshContext, runner } = options; + + let context = await buildContext(); + + try { + return await runner(context); + } catch (error) { + const retryMetadata = deriveRetryMetadata(error); + + if (retryMetadata.shouldRetry) { + const reason = retryMetadata.reason || 'retry'; + const refreshLabel = `${operation}-${reason}`.replace(/-+/g, '-'); + + _logger.warn( + { + error, + operation, + retryReason: reason, + }, + `${operation} failed; refreshing handshake (${refreshLabel}) and retrying once.` + ); + context = await refreshContext(refreshLabel); + return await runner(context); + } + + throw error; + } +}; diff --git a/packages/networks/src/networks/vNaga/shared/managers/state-manager/createStateManager.ts b/packages/networks/src/networks/vNaga/shared/managers/state-manager/createStateManager.ts index 9ab10a449..908b466fd 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/state-manager/createStateManager.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/state-manager/createStateManager.ts @@ -92,6 +92,75 @@ export const createStateManager = async (params: { throw err; } + const refreshState = async (reason?: string) => { + try { + _logger.info({ reason }, 'Refreshing connection info and handshake callback'); + const newConnectionInfo = + await readOnlyChainManager.api.connection.getConnectionInfo(); + const newBootstrapUrls = newConnectionInfo.bootstrapUrls; + const newEpochInfo = newConnectionInfo.epochInfo; + + latestConnectionInfo = newConnectionInfo; + + const bootstrapUrlsChanged = areStringArraysDifferent( + latestBootstrapUrls, + newBootstrapUrls + ); + + if (bootstrapUrlsChanged) { + _logger.warn( + { + reason, + oldUrls: latestBootstrapUrls, + newUrls: newBootstrapUrls, + }, + 'Bootstrap URLs changed. Updating internal state.' + ); + latestBootstrapUrls = newBootstrapUrls; + } else { + _logger.info( + { reason }, + 'Bootstrap URLs remain unchanged during refresh.' + ); + } + + if (!latestEpochInfo || latestEpochInfo.number !== newEpochInfo.number) { + _logger.info( + { + reason, + previousEpoch: latestEpochInfo?.number, + newEpoch: newEpochInfo.number, + }, + 'Epoch number updated during refresh.' + ); + latestEpochInfo = newEpochInfo; + } else { + _logger.info( + { reason, epoch: newEpochInfo.number }, + 'Epoch number unchanged during refresh.' + ); + } + + callbackResult = await params.callback({ + bootstrapUrls: latestBootstrapUrls, + currentEpoch: latestEpochInfo?.number || 0, + version: params.networkModule.version, + requiredAttestation: params.networkModule.config.requiredAttestation, + minimumThreshold: params.networkModule.config.minimumThreshold, + abortTimeout: params.networkModule.config.abortTimeout, + endpoints: params.networkModule.getEndpoints(), + releaseVerificationConfig: null, + networkModule: params.networkModule, + }); + } catch (error) { + _logger.error( + { error, reason }, + 'Failed to refresh connection info for state manager' + ); + throw error; + } + }; + // --- Setup Staking Event Listener --- const stakingContract = new ethers.Contract( contractManager.stakingContract.address, @@ -116,58 +185,7 @@ export const createStateManager = async (params: { // 2. If state is Active, refresh connection info if (newState === (STAKING_STATES.Active as STAKING_STATES_VALUES)) { try { - _logger.info( - '🖐 Staking state is Active. Fetching latest connection info...' - ); - const newConnectionInfo = - await readOnlyChainManager.api.connection.getConnectionInfo(); - const newBootstrapUrls = newConnectionInfo.bootstrapUrls; - const newEpochInfo = newConnectionInfo.epochInfo; // Get new epoch info - latestConnectionInfo = newConnectionInfo; // Update internal state for connection info - - const bootstrapUrlsChanged = areStringArraysDifferent( - latestBootstrapUrls, - newBootstrapUrls - ); - - if (bootstrapUrlsChanged) { - _logger.warn( - { - oldUrls: latestBootstrapUrls, - newUrls: newBootstrapUrls, - }, - 'Bootstrap URLs changed. Updating internal state.' - ); - latestBootstrapUrls = newBootstrapUrls; // Update internal state - } else { - _logger.info('BootstrapUrls remain unchanged.'); - } - - // Always update epoch info when Active state is processed - if (latestEpochInfo?.number !== newEpochInfo.number) { - _logger.info( - `Epoch number updated from ${latestEpochInfo?.number} to ${newEpochInfo.number}` - ); - latestEpochInfo = newEpochInfo; - } else { - _logger.info( - `Epoch number ${newEpochInfo.number} remains the same.` - ); - } - - // -- callback - callbackResult = await params.callback({ - bootstrapUrls: latestBootstrapUrls, - currentEpoch: latestEpochInfo!.number, - version: params.networkModule.version, - requiredAttestation: - params.networkModule.config.requiredAttestation, - minimumThreshold: params.networkModule.config.minimumThreshold, - abortTimeout: params.networkModule.config.abortTimeout, - endpoints: params.networkModule.getEndpoints(), - releaseVerificationConfig: null, - networkModule: params.networkModule, - }); + await refreshState('staking-state-change'); } catch (error) { _logger.error( { error }, @@ -226,6 +244,10 @@ export const createStateManager = async (params: { return latestConnectionInfo ? { ...latestConnectionInfo } : null; }, + refreshHandshake: async (reason?: string) => { + await refreshState(reason); + }, + /** * Stops the background listeners (blockhash refresh, event listening). */ From 81109399d2ba96ffcaea5c7bd5365bbe87702e16 Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 5 Nov 2025 17:29:22 +0000 Subject: [PATCH 6/8] refactor(lit-client): centralise retry reasons as const --- .../LitClient/helpers/executeWithHandshake.ts | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts b/packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts index e75b6a31a..5528f32d3 100644 --- a/packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts +++ b/packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts @@ -23,6 +23,16 @@ type RetryMetadata = { reason: string; }; +// Pause briefly before retrying so dropped nodes have time to deregister and surviving owners rebroadcast shares. +const RETRY_BACKOFF_MS = 1_000; + +export const RETRY_REASONS = { + missingVerificationKey: 'missing-verification-key', + networkFetch: 'network-fetch-error', + noValidShares: 'no-valid-shares', + generic: 'retry', +} as const; + export namespace EdgeCase { export const isMissingVerificationKeyError = (error: unknown): boolean => { if (!error || typeof error !== 'object') { @@ -102,15 +112,18 @@ export namespace EdgeCase { const deriveRetryMetadata = (error: unknown): RetryMetadata => { if (EdgeCase.isMissingVerificationKeyError(error)) { - return { shouldRetry: true, reason: 'missing-verification-key' }; + return { + shouldRetry: true, + reason: RETRY_REASONS.missingVerificationKey, + }; } if (EdgeCase.isNetworkFetchError(error)) { - return { shouldRetry: true, reason: 'network-fetch-error' }; + return { shouldRetry: true, reason: RETRY_REASONS.networkFetch }; } if (EdgeCase.isNoValidSharesError(error)) { - return { shouldRetry: true, reason: 'no-valid-shares' }; + return { shouldRetry: true, reason: RETRY_REASONS.noValidShares }; } return { shouldRetry: false, reason: '' }; @@ -129,9 +142,25 @@ export const executeWithHandshake = async ( const retryMetadata = deriveRetryMetadata(error); if (retryMetadata.shouldRetry) { - const reason = retryMetadata.reason || 'retry'; + const reason = retryMetadata.reason || RETRY_REASONS.generic; const refreshLabel = `${operation}-${reason}`.replace(/-+/g, '-'); + if ( + reason === 'no-valid-shares' || + reason === 'network-fetch-error' + ) { + await new Promise((resolve) => setTimeout(resolve, RETRY_BACKOFF_MS)); + } + + console.log( + '[executeWithHandshake] retrying operation', + operation, + 'reason:', + reason, + 'refreshLabel:', + refreshLabel + ); + _logger.warn( { error, From f39927d8dd637ed35b9f0d87f9836f55c41c2dfe Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 5 Nov 2025 17:51:32 +0000 Subject: [PATCH 7/8] fmt --- tsconfig.base.json | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tsconfig.base.json b/tsconfig.base.json index bbbf0fe05..3e4cc5d31 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -10,7 +10,11 @@ "importHelpers": true, "target": "ES2022", "module": "ES2022", - "lib": ["ES2022", "dom", "ES2021.String"], + "lib": [ + "ES2022", + "dom", + "ES2021.String" + ], "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", @@ -18,10 +22,20 @@ "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "paths": { - "@lit-protocol/*": ["packages/*/src"], - "@lit-protocol/contracts": ["packages/contracts/dist/index"], - "@lit-protocol/contracts/*": ["packages/contracts/dist/*"] + "@lit-protocol/*": [ + "packages/*/src" + ], + "@lit-protocol/contracts": [ + "packages/contracts/dist/index" + ], + "@lit-protocol/contracts/*": [ + "packages/contracts/dist/*" + ] } }, - "exclude": ["node_modules", "tmp", "dist"] -} + "exclude": [ + "node_modules", + "tmp", + "dist" + ] +} \ No newline at end of file From 115c194fb2aae4e16588b692405ab6b786f1e2a5 Mon Sep 17 00:00:00 2001 From: anson Date: Wed, 5 Nov 2025 18:11:06 +0000 Subject: [PATCH 8/8] refactor(executeWithHandshake): simplify retry logic and remove redundant logging --- .../lib/LitClient/helpers/executeWithHandshake.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts b/packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts index 5528f32d3..042a68def 100644 --- a/packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts +++ b/packages/lit-client/src/lib/LitClient/helpers/executeWithHandshake.ts @@ -145,22 +145,10 @@ export const executeWithHandshake = async ( const reason = retryMetadata.reason || RETRY_REASONS.generic; const refreshLabel = `${operation}-${reason}`.replace(/-+/g, '-'); - if ( - reason === 'no-valid-shares' || - reason === 'network-fetch-error' - ) { + if (reason === 'no-valid-shares' || reason === 'network-fetch-error') { await new Promise((resolve) => setTimeout(resolve, RETRY_BACKOFF_MS)); } - console.log( - '[executeWithHandshake] retrying operation', - operation, - 'reason:', - reason, - 'refreshLabel:', - refreshLabel - ); - _logger.warn( { error,