diff --git a/.changeset/big-tools-study.md b/.changeset/big-tools-study.md new file mode 100644 index 000000000..a265b1430 --- /dev/null +++ b/.changeset/big-tools-study.md @@ -0,0 +1,6 @@ +--- +'@lit-protocol/lit-client': patch +'@lit-protocol/networks': patch +--- + +Node operations (pkpSign, decrypt, executeJs, session key signing) now emit request-aware errors, letting users share a requestID for log correlation. diff --git a/packages/lit-client/src/lib/LitClient/createLitClient.ts b/packages/lit-client/src/lib/LitClient/createLitClient.ts index 01ed06683..f497d1a27 100644 --- a/packages/lit-client/src/lib/LitClient/createLitClient.ts +++ b/packages/lit-client/src/lib/LitClient/createLitClient.ts @@ -21,7 +21,13 @@ import { PaymentManager, } from '@lit-protocol/networks'; -import { DEV_PRIVATE_KEY } from '@lit-protocol/constants'; +import { + DEV_PRIVATE_KEY, + LitNodeClientBadConfigError, + LitNodeClientNotReadyError, + ParamsMissingError, + UnsupportedMethodError, +} from '@lit-protocol/constants'; import { AuthContextSchema2, AuthData, @@ -103,7 +109,15 @@ export const createLitClient = async ({ case 'datil': return _createDatilLitClient(); default: - throw new Error(`Network module ${network.id} not supported`); + throw new UnsupportedMethodError( + { + cause: new Error('Unsupported network module'), + info: { + networkId: network.id, + }, + }, + `Network module ${network.id} not supported` + ); } }; @@ -175,7 +189,13 @@ export const _createNagaLitClient = async ( // Initial check to ensure handshakeResult is available after setup if (!_stateManager.getCallbackResult()) { - throw new Error( + throw new LitNodeClientNotReadyError( + { + cause: new Error('Handshake result missing after initialization'), + info: { + operation: 'initialiseClient', + }, + }, 'Initial handshake result is not available from state manager. LitClient cannot be initialized.' ); } @@ -196,7 +216,13 @@ export const _createNagaLitClient = async ( const currentConnectionInfo = _stateManager.getLatestConnectionInfo(); if (!currentHandshakeResult || !currentConnectionInfo) { - throw new Error( + throw new LitNodeClientNotReadyError( + { + cause: new Error('Handshake result unavailable for pkpSign'), + info: { + operation: 'pkpSign', + }, + }, 'Handshake result is not available from state manager at the time of pkpSign.' ); } @@ -276,7 +302,13 @@ export const _createNagaLitClient = async ( const currentConnectionInfo = _stateManager.getLatestConnectionInfo(); if (!currentHandshakeResult || !currentConnectionInfo) { - throw new Error( + 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.' ); } @@ -307,7 +339,8 @@ export const _createNagaLitClient = async ( return await networkModule.api.signSessionKey.handleResponse( result as any, params.requestBody.pkpPublicKey, - jitContext + jitContext, + requestId ); } @@ -322,7 +355,15 @@ export const _createNagaLitClient = async ( const currentConnectionInfo = _stateManager.getLatestConnectionInfo(); if (!currentHandshakeResult || !currentConnectionInfo) { - throw new Error( + 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.' ); } @@ -333,7 +374,15 @@ export const _createNagaLitClient = async ( ); if (!currentHandshakeResult || !currentConnectionInfo) { - throw new Error( + 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.' ); } @@ -375,7 +424,13 @@ export const _createNagaLitClient = async ( const currentConnectionInfo = _stateManager.getLatestConnectionInfo(); if (!currentHandshakeResult || !currentConnectionInfo) { - throw new Error( + throw new LitNodeClientNotReadyError( + { + cause: new Error('Handshake result unavailable for executeJs'), + info: { + operation: 'executeJs', + }, + }, 'Handshake result is not available from state manager at the time of executeJs.' ); } @@ -489,13 +544,27 @@ export const _createNagaLitClient = async ( const currentHandshakeResult = _stateManager.getCallbackResult(); if (!currentHandshakeResult) { - throw new Error( + throw new LitNodeClientNotReadyError( + { + cause: new Error('Handshake result unavailable for encrypt'), + info: { + operation: 'encrypt', + }, + }, 'Handshake result is not available from state manager at the time of encrypt.' ); } if (!currentHandshakeResult.coreNodeConfig?.subnetPubKey) { - throw new Error('subnetPubKey cannot be null'); + throw new LitNodeClientBadConfigError( + { + cause: new Error('Missing subnetPubKey in handshake result'), + info: { + operation: 'encrypt', + }, + }, + 'subnetPubKey cannot be null' + ); } // ========== Convert data to Uint8Array ========== @@ -527,7 +596,15 @@ export const _createNagaLitClient = async ( // ========== Validate Params ========== if (!_validateEncryptionParams(params)) { - throw new Error( + throw new ParamsMissingError( + { + cause: new Error( + 'Required encryption access control parameters missing' + ), + info: { + operation: 'encrypt', + }, + }, 'You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions' ); } @@ -540,7 +617,13 @@ export const _createNagaLitClient = async ( await getHashedAccessControlConditions(params); if (!hashOfConditions) { - throw new Error( + throw new ParamsMissingError( + { + cause: new Error('Failed to hash provided access control parameters'), + info: { + operation: 'encrypt', + }, + }, 'You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions' ); } @@ -606,7 +689,13 @@ export const _createNagaLitClient = async ( const currentConnectionInfo = _stateManager.getLatestConnectionInfo(); if (!currentHandshakeResult || !currentConnectionInfo) { - throw new Error( + throw new LitNodeClientNotReadyError( + { + cause: new Error('Handshake result unavailable for decrypt'), + info: { + operation: 'decrypt', + }, + }, 'Handshake result is not available from state manager at the time of decrypt.' ); } @@ -617,18 +706,40 @@ export const _createNagaLitClient = async ( ); if (!currentHandshakeResult || !currentConnectionInfo) { - throw new Error( + throw new LitNodeClientNotReadyError( + { + cause: new Error('Handshake result unavailable for decrypt'), + info: { + operation: 'decrypt', + }, + }, 'Handshake result is not available from state manager at the time of decrypt.' ); } if (!currentHandshakeResult.coreNodeConfig?.subnetPubKey) { - throw new Error('subnetPubKey cannot be null'); + throw new LitNodeClientBadConfigError( + { + cause: new Error('Missing subnetPubKey in handshake result'), + info: { + operation: 'decrypt', + }, + }, + 'subnetPubKey cannot be null' + ); } // ========== Validate Params ========== if (!_validateEncryptionParams(params)) { - throw new Error( + throw new ParamsMissingError( + { + cause: new Error( + 'Required decryption access control parameters missing' + ), + info: { + operation: 'decrypt', + }, + }, 'You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions' ); } @@ -641,7 +752,13 @@ export const _createNagaLitClient = async ( await getHashedAccessControlConditions(params); if (!hashOfConditions) { - throw new Error( + throw new ParamsMissingError( + { + cause: new Error('Failed to hash provided access control parameters'), + info: { + operation: 'decrypt', + }, + }, 'You must provide either accessControlConditions or evmContractConditions or solRpcConditions or unifiedAccessControlConditions' ); } @@ -1015,7 +1132,15 @@ export const _createNagaLitClient = async ( type DatilNetworkModule = LitNetworkModule; export const _createDatilLitClient = async () => { - throw new Error('Datil is not supported yet'); + throw new UnsupportedMethodError( + { + cause: new Error('Datil network module is not implemented'), + info: { + networkId: 'datil', + }, + }, + 'Datil is not supported yet' + ); }; export type LitClientType = Awaited>; diff --git a/packages/networks/src/networks/vNaga/shared/factories/BaseModuleFactory.ts b/packages/networks/src/networks/vNaga/shared/factories/BaseModuleFactory.ts index 7af72f43d..4302a9cb6 100644 --- a/packages/networks/src/networks/vNaga/shared/factories/BaseModuleFactory.ts +++ b/packages/networks/src/networks/vNaga/shared/factories/BaseModuleFactory.ts @@ -1,4 +1,11 @@ -import { DEV_PRIVATE_KEY, version } from '@lit-protocol/constants'; +import { + DEV_PRIVATE_KEY, + LitNodeClientBadConfigError, + NetworkError, + NodeError, + UnknownError, + version, +} from '@lit-protocol/constants'; import { verifyAndDecryptWithSignatureShares } from '@lit-protocol/crypto'; import { AuthData, @@ -21,7 +28,6 @@ import type { ExpectedAccountOrWalletClient } from '../managers/contract-manager import { createChainManagerFactory } from './BaseChainManagerFactory'; // Shared utilities -import { NetworkError } from '@lit-protocol/constants'; import { combineSignatureShares, mostCommonString, @@ -472,7 +478,14 @@ export function createBaseModule(config: BaseModuleConfig) { const respondingUrlSet = new Set(respondingUrls); if (respondingUrls.length === 0) { - throw new Error( + throw new LitNodeClientBadConfigError( + { + cause: new Error('Handshake result missing node identity keys'), + info: { + operation: 'createJitContext', + handshakeResult, + }, + }, `Handshake response did not include any node identity keys. Received handshake result: ${JSON.stringify( handshakeResult )}` @@ -483,7 +496,17 @@ export function createBaseModule(config: BaseModuleConfig) { const serverKey = handshakeResult.serverKeys[url]; if (!serverKey || !serverKey.nodeIdentityKey) { - throw new Error( + throw new LitNodeClientBadConfigError( + { + cause: new Error( + `Handshake response missing node identity key for node ${url}` + ), + info: { + operation: 'createJitContext', + url, + handshakeResult, + }, + }, `Handshake response missing node identity key for node ${url}. Received handshake result: ${JSON.stringify( handshakeResult )}` @@ -528,7 +551,16 @@ export function createBaseModule(config: BaseModuleConfig) { ); if (filteredNodePrices.length === 0) { - throw new Error( + throw new NetworkError( + { + cause: new Error( + 'Unable to resolve price data for responding handshake nodes' + ), + info: { + operation: 'createJitContext', + respondingUrls: Array.from(respondingUrlSet), + }, + }, 'Unable to resolve price data for responding handshake nodes' ); } @@ -628,7 +660,16 @@ export function createBaseModule(config: BaseModuleConfig) { } if (!requests || requests.length === 0) { - throw new Error('Failed to generate requests for pkpSign.'); + throw new UnknownError( + { + cause: new Error('Request generation produced no entries'), + info: { + operation: 'pkpSign:createRequest', + requestId: _requestId, + }, + }, + 'Failed to generate requests for pkpSign.' + ); } return requests; @@ -642,7 +683,8 @@ export function createBaseModule(config: BaseModuleConfig) { E2EERequestManager.handleEncryptedError( result, jitContext, - 'PKP Sign' + 'PKP Sign', + requestId ); } @@ -652,7 +694,16 @@ export function createBaseModule(config: BaseModuleConfig) { (decryptedJson) => { const pkpSignData = decryptedJson.data; if (!pkpSignData) { - throw new Error('Decrypted response missing data field'); + throw new NodeError( + { + cause: new Error('Decrypted response missing data field'), + info: { + operationName: 'PKP Sign', + requestId, + }, + }, + `PKP Sign failed for request ${requestId}. Decrypted response missing data field` + ); } const wrappedData = { @@ -662,6 +713,10 @@ export function createBaseModule(config: BaseModuleConfig) { const responseData = PKPSignResponseDataSchema.parse(wrappedData); return responseData.values[0]; + }, + { + operationName: 'PKP Sign', + requestId, } ); @@ -754,7 +809,8 @@ export function createBaseModule(config: BaseModuleConfig) { E2EERequestManager.handleEncryptedError( result, jitContext, - 'Decryption' + 'Decryption', + requestId ); } @@ -764,10 +820,23 @@ export function createBaseModule(config: BaseModuleConfig) { (decryptedJson) => { const decryptData = decryptedJson.data; if (!decryptData) { - throw new Error('Decrypted response missing data field'); + throw new NodeError( + { + cause: new Error('Decrypted response missing data field'), + info: { + operationName: 'Decryption', + requestId, + }, + }, + `Decryption failed for request ${requestId}. Decrypted response missing data field` + ); } const responseData = DecryptResponseDataSchema.parse(decryptData); return responseData; + }, + { + operationName: 'Decryption', + requestId, } ); @@ -860,13 +929,15 @@ export function createBaseModule(config: BaseModuleConfig) { handleResponse: async ( result: z.infer, pkpPublicKey: Hex | string, - jitContext: NagaJitContext + jitContext: NagaJitContext, + requestId: string ) => { if (!result.success) { E2EERequestManager.handleEncryptedError( result, jitContext, - 'Session key signing' + 'Session key signing', + requestId ); } @@ -876,9 +947,22 @@ export function createBaseModule(config: BaseModuleConfig) { (decryptedJson) => { const signSessionKeyData = decryptedJson.data; if (!signSessionKeyData) { - throw new Error('Decrypted response missing data field'); + throw new NodeError( + { + cause: new Error('Decrypted response missing data field'), + info: { + operationName: 'Session key signing', + requestId, + }, + }, + `Session key signing failed for request ${requestId}. Decrypted response missing data field` + ); } return signSessionKeyData; + }, + { + operationName: 'Session key signing', + requestId, } ); @@ -972,13 +1056,14 @@ export function createBaseModule(config: BaseModuleConfig) { result: z.infer, pkpPublicKey: Hex | string, jitContext: NagaJitContext, - requestId?: string + requestId: string ) => { if (!result.success) { E2EERequestManager.handleEncryptedError( result, jitContext, - 'Sign Custom Session Key' + 'Sign Custom Session Key', + requestId ); } @@ -988,11 +1073,22 @@ export function createBaseModule(config: BaseModuleConfig) { (decryptedJson) => { const signCustomSessionKeyData = decryptedJson.data; if (!signCustomSessionKeyData) { - throw new Error( - `[${requestId}] Decrypted response missing data field` + throw new NodeError( + { + cause: new Error('Decrypted response missing data field'), + info: { + operationName: 'Sign Custom Session Key', + requestId, + }, + }, + `Sign Custom Session Key failed for request ${requestId}. Decrypted response missing data field` ); } return signCustomSessionKeyData; + }, + { + operationName: 'Sign Custom Session Key', + requestId, } ); @@ -1123,7 +1219,8 @@ export function createBaseModule(config: BaseModuleConfig) { E2EERequestManager.handleEncryptedError( result, jitContext, - 'JS execution' + 'JS execution', + requestId ); } @@ -1134,9 +1231,22 @@ export function createBaseModule(config: BaseModuleConfig) { (decryptedJson) => { const executeJsData = decryptedJson.data; if (!executeJsData) { - throw new Error('Decrypted response missing data field'); + throw new NodeError( + { + cause: new Error('Decrypted response missing data field'), + info: { + operationName: 'JS execution', + requestId, + }, + }, + `JS execution failed for request ${requestId}. Decrypted response missing data field` + ); } return executeJsData; + }, + { + operationName: 'JS execution', + requestId, } ); diff --git a/packages/networks/src/networks/vNaga/shared/managers/api-manager/APIFactory.ts b/packages/networks/src/networks/vNaga/shared/managers/api-manager/APIFactory.ts index 52ed20c98..b8accd5ac 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/api-manager/APIFactory.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/api-manager/APIFactory.ts @@ -6,6 +6,7 @@ import { GenericResultBuilder, } from '@lit-protocol/schemas'; import { RequestItem, NagaJitContext } from '@lit-protocol/types'; +import { NodeError, UnknownError } from '@lit-protocol/constants'; import { E2EERequestManager } from './e2ee-request-manager/E2EERequestManager'; import { composeLitUrl } from '../endpoints-manager/composeLitUrl'; @@ -86,7 +87,16 @@ export function createPKPSignAPI(networkConfig: INetworkConfig) { } if (!requests || requests.length === 0) { - throw new Error('Failed to generate requests for pkpSign.'); + throw new UnknownError( + { + cause: new Error('Request generation produced no entries'), + info: { + operation: 'pkpSign:createRequest', + requestId: _requestId, + }, + }, + 'Failed to generate requests for pkpSign.' + ); } return requests; @@ -98,7 +108,12 @@ export function createPKPSignAPI(networkConfig: INetworkConfig) { jitContext: NagaJitContext ) => { if (!result.success) { - E2EERequestManager.handleEncryptedError(result, jitContext, 'PKP Sign'); + E2EERequestManager.handleEncryptedError( + result, + jitContext, + 'PKP Sign', + requestId + ); } const decryptedValues = E2EERequestManager.decryptBatchResponse( @@ -107,7 +122,16 @@ export function createPKPSignAPI(networkConfig: INetworkConfig) { (decryptedJson) => { const pkpSignData = decryptedJson.data; if (!pkpSignData) { - throw new Error('Decrypted response missing data field'); + throw new NodeError( + { + cause: new Error('Decrypted response missing data field'), + info: { + operationName: 'PKP Sign', + requestId, + }, + }, + `PKP Sign failed for request ${requestId}. Decrypted response missing data field` + ); } const wrappedData = { @@ -117,6 +141,10 @@ export function createPKPSignAPI(networkConfig: INetworkConfig) { const responseData = PKPSignResponseDataSchema.parse(wrappedData); return responseData.values[0]; + }, + { + operationName: 'PKP Sign', + requestId, } ); @@ -194,7 +222,8 @@ export function createDecryptAPI(networkConfig: INetworkConfig) { E2EERequestManager.handleEncryptedError( result, jitContext, - 'Decryption' + 'Decryption', + requestId ); } @@ -204,9 +233,22 @@ export function createDecryptAPI(networkConfig: INetworkConfig) { (decryptedJson) => { const decryptData = decryptedJson.data; if (!decryptData) { - throw new Error('Decrypted response missing data field'); + throw new NodeError( + { + cause: new Error('Decrypted response missing data field'), + info: { + operationName: 'Decryption', + requestId, + }, + }, + `Decryption failed for request ${requestId}. Decrypted response missing data field` + ); } return DecryptResponseDataSchema.parse(decryptData); + }, + { + operationName: 'Decryption', + requestId, } ); @@ -287,7 +329,8 @@ export function createExecuteJsAPI(networkConfig: INetworkConfig) { E2EERequestManager.handleEncryptedError( result, jitContext, - 'JS execution' + 'JS execution', + requestId ); } @@ -297,9 +340,22 @@ export function createExecuteJsAPI(networkConfig: INetworkConfig) { (decryptedJson) => { const executeJsData = decryptedJson.data; if (!executeJsData) { - throw new Error('Decrypted response missing data field'); + throw new NodeError( + { + cause: new Error('Decrypted response missing data field'), + info: { + operationName: 'JS execution', + requestId, + }, + }, + `JS execution failed for request ${requestId}. Decrypted response missing data field` + ); } return executeJsData; + }, + { + operationName: 'JS execution', + requestId, } ); diff --git a/packages/networks/src/networks/vNaga/shared/managers/api-manager/e2ee-request-manager/E2EERequestManager.spec.ts b/packages/networks/src/networks/vNaga/shared/managers/api-manager/e2ee-request-manager/E2EERequestManager.spec.ts new file mode 100644 index 000000000..50143162f --- /dev/null +++ b/packages/networks/src/networks/vNaga/shared/managers/api-manager/e2ee-request-manager/E2EERequestManager.spec.ts @@ -0,0 +1,39 @@ +jest.mock('@lit-protocol/crypto', () => ({ + walletEncrypt: jest.fn(), + walletDecrypt: jest.fn(), +})); + +jest.mock('@lit-protocol/logger', () => ({ + getChildLogger: () => ({ + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }), +})); + +import { NodeError } from '@lit-protocol/constants'; +import { NagaJitContext } from '@lit-protocol/types'; +import { E2EERequestManager } from './E2EERequestManager'; + +describe('E2EERequestManager.handleEncryptedError', () => { + it('includes the requestId in the thrown error message', () => { + const requestId = 'test-request-123'; + const jitContext = { keySet: {} } as unknown as NagaJitContext; + + expect.assertions(3); + + try { + E2EERequestManager.handleEncryptedError( + { error: 'simulated node failure' }, + jitContext, + 'UnitTestOperation', + requestId + ); + } catch (error) { + expect(error).toBeInstanceOf(NodeError); + const message = (error as Error).message; + expect(message).toContain(requestId); + expect(message).toContain('UnitTestOperation'); + } + }); +}); diff --git a/packages/networks/src/networks/vNaga/shared/managers/api-manager/e2ee-request-manager/E2EERequestManager.ts b/packages/networks/src/networks/vNaga/shared/managers/api-manager/e2ee-request-manager/E2EERequestManager.ts index dd0e0120c..0be8ac2ad 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/api-manager/e2ee-request-manager/E2EERequestManager.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/api-manager/e2ee-request-manager/E2EERequestManager.ts @@ -1,4 +1,8 @@ import { walletDecrypt, walletEncrypt } from '@lit-protocol/crypto'; +import { + LitNodeClientBadConfigError, + NodeError, +} from '@lit-protocol/constants'; import { getChildLogger } from '@lit-protocol/logger'; import { NagaJitContext } from '@lit-protocol/types'; import { bytesToHex, stringToBytes } from 'viem'; @@ -25,7 +29,15 @@ const encryptRequestData = ( jitContext: NagaJitContext ): z.infer => { if (!jitContext.keySet[url]) { - throw new Error(`No encryption keys found for node URL: ${url}`); + throw new LitNodeClientBadConfigError( + { + cause: new Error(`Missing encryption keys for node URL: ${url}`), + info: { + url, + }, + }, + `No encryption keys found for node URL: ${url}` + ); } return walletEncrypt( @@ -35,6 +47,11 @@ const encryptRequestData = ( ); }; +interface DecryptBatchResponseOptions { + requestId?: string; + operationName?: string; +} + /** * Generic function to decrypt batch responses using JIT context * @param encryptedResult The encrypted batch result from nodes @@ -45,12 +62,28 @@ const encryptRequestData = ( const decryptBatchResponse = ( encryptedResult: z.infer, jitContext: NagaJitContext, - extractResponseData: (decryptedJson: any) => T + extractResponseData: (decryptedJson: any) => T, + options: DecryptBatchResponseOptions = {} ): T[] => { + const operationName = options.operationName ?? 'Lit network operation'; + const requestId = options.requestId; const parsedResult = GenericEncryptedPayloadSchema.parse(encryptedResult); + const baseMessage = requestId + ? `"${operationName}" failed for request ${requestId}` + : `"${operationName}" failed`; if (!parsedResult.success) { - throw new Error(`Batch decryption failed: ${JSON.stringify(parsedResult)}`); + throw new NodeError( + { + cause: new Error('Batch decryption failed'), + info: { + operationName, + requestId, + parsedResult, + }, + }, + `${baseMessage}. Batch decryption failed: ${JSON.stringify(parsedResult)}` + ); } const decryptedValues: T[] = []; @@ -81,8 +114,18 @@ const decryptBatchResponse = ( const keyData = verificationKeyToSecretKey[verificationKey]; if (!keyData) { - throw new Error( - `No secret key found for verification key: ${verificationKey}` + throw new NodeError( + { + cause: new Error( + `No secret key found for verification key: ${verificationKey}` + ), + info: { + operationName, + requestId, + verificationKey, + }, + }, + `${baseMessage}. No secret key found for verification key: ${verificationKey}` ); } @@ -102,16 +145,36 @@ const decryptBatchResponse = ( const responseData = extractResponseData(parsedData); decryptedValues.push(responseData); } catch (decryptError) { - const errorMessage = - decryptError instanceof Error ? decryptError.message : 'Unknown error'; - throw new Error( - `Failed to decrypt response ${i} with key from ${keyData.url}: ${errorMessage}` + const convertedError = + decryptError instanceof Error + ? decryptError + : new Error(String(decryptError)); + throw new NodeError( + { + cause: convertedError, + info: { + operationName, + requestId, + responseIndex: i, + nodeUrl: keyData.url, + }, + }, + `${baseMessage}. Failed to decrypt response ${i} with key from ${keyData.url}: ${convertedError.message}` ); } } if (decryptedValues.length === 0) { - throw new Error('No responses were successfully decrypted'); + throw new NodeError( + { + cause: new Error('No responses were successfully decrypted'), + info: { + operationName, + requestId, + }, + }, + `${baseMessage}. No responses were successfully decrypted` + ); } return decryptedValues; @@ -120,13 +183,18 @@ const decryptBatchResponse = ( const handleEncryptedError = ( errorResult: any, jitContext: NagaJitContext, - operationName: string + operationName: string, + requestId: string ): never => { + const baseMessage = requestId + ? `"${operationName}" failed for request ${requestId}` + : `"${operationName}" failed`; + if (errorResult.error && errorResult.error.payload) { // Try to decrypt the error payload to get the actual error message try { _logger.info( - {}, + { requestId }, `"${operationName}": Attempting to decrypt error payload for detailed error information...` ); @@ -142,54 +210,97 @@ const handleEncryptedError = ( jitContext, (decryptedJson) => { return decryptedJson.data || decryptedJson; // Return whatever we can get + }, + { + operationName: `${operationName} error payload`, + requestId, } ); _logger.error( - { decryptedErrorValues }, + { requestId, decryptedErrorValues }, `"${operationName}": Decrypted error details from nodes:` ); // Use the actual error message from the nodes const firstError = decryptedErrorValues[0]; if (firstError && firstError.error) { - const errorMessage = firstError.error; + const convertedError = + firstError.error instanceof Error + ? firstError.error + : new Error(String(firstError.error)); const errorDetails = firstError.errorObject ? `. Details: ${firstError.errorObject}` : ''; - throw new Error( - `"${operationName}" failed. ${errorMessage}${errorDetails}` + throw new NodeError( + { + cause: convertedError, + info: { + operationName, + requestId, + rawNodeError: firstError, + }, + }, + `${baseMessage}. ${convertedError.message}${errorDetails}` ); } // If no specific error field, show the full decrypted response - throw new Error( - `"${operationName}" failed. ${JSON.stringify(decryptedErrorValues)}` + throw new NodeError( + { + cause: new Error('Node error payload missing expected structure'), + info: { + operationName, + requestId, + decryptedErrorValues, + }, + }, + `${baseMessage}. ${JSON.stringify(decryptedErrorValues)}` ); } catch (decryptError) { _logger.error( - { decryptError }, + { requestId, decryptError }, `"${operationName}": Failed to decrypt error payload:` ); - // If the decryptError is actually our thrown error with the node's message, re-throw it - if ( - decryptError instanceof Error && - decryptError.message.includes(`"${operationName}" failed.`) - ) { + if (decryptError instanceof NodeError) { throw decryptError; } - throw new Error( - `"${operationName}" failed. The nodes returned an encrypted error response that could not be decrypted. ` + - `This may indicate a configuration or network connectivity issue. ${JSON.stringify( - errorResult - )}. If you are running custom session sigs, it might mean the validation has failed. We will continue to improve this error message to provide more information.` + const convertedError = + decryptError instanceof Error + ? decryptError + : new Error(String(decryptError)); + + // If the decryptError is actually our thrown error with the node's message, re-throw it + throw new NodeError( + { + cause: convertedError, + info: { + operationName, + requestId, + rawError: errorResult, + }, + }, + `${baseMessage}. The nodes returned an encrypted error response that could not be decrypted. ${JSON.stringify( + errorResult + )}. If you are running custom session sigs, it might mean the validation has failed. We will continue to improve this error message to provide more information.` ); } } else { - throw new Error( - `"${operationName}" failed. ${JSON.stringify(errorResult)}` + const rawError = errorResult?.error ?? errorResult; + const normalizedCause = + rawError instanceof Error ? rawError : new Error(String(rawError)); + throw new NodeError( + { + cause: normalizedCause, + info: { + operationName, + requestId, + rawError: errorResult, + }, + }, + `${baseMessage}. ${JSON.stringify(errorResult)}` ); } }; diff --git a/packages/networks/src/networks/vNaga/shared/managers/api-manager/executeJs/executeJs.ts b/packages/networks/src/networks/vNaga/shared/managers/api-manager/executeJs/executeJs.ts index a49ec7397..9f9f11f07 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/api-manager/executeJs/executeJs.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/api-manager/executeJs/executeJs.ts @@ -15,6 +15,7 @@ */ import { findMostCommonResponse } from '@lit-protocol/crypto'; +import { NodeError } from '@lit-protocol/constants'; import { getChildLogger } from '@lit-protocol/logger'; import { ExecuteJsResponse, @@ -268,16 +269,25 @@ export const handleResponse = async ( ); if (!result.success) { + const rawError = 'error' in result ? result.error : 'Unknown error'; _logger.error( { requestId, - error: 'error' in result ? result.error : 'Unknown error', + error: rawError, }, 'executeJs:handleResponse: Batch failed' ); - throw new Error( - `ExecuteJs batch failed: ${JSON.stringify( - 'error' in result ? result.error : 'Unknown error' + throw new NodeError( + { + cause: new Error('ExecuteJs batch failed'), + info: { + operationName: 'executeJs', + requestId, + rawError, + }, + }, + `ExecuteJs batch failed for request ${requestId}: ${JSON.stringify( + rawError )}` ); } @@ -299,8 +309,17 @@ export const handleResponse = async ( const successfulValues = values.filter((value) => value.success); if (successfulValues.length < threshold) { - throw new Error( - `Not enough successful executeJs responses. Expected ${threshold}, got ${successfulValues.length}` + throw new NodeError( + { + cause: new Error('Insufficient successful executeJs responses'), + info: { + operationName: 'executeJs', + requestId, + threshold, + successfulValues: successfulValues.length, + }, + }, + `Not enough successful executeJs responses for request ${requestId}. Expected ${threshold}, got ${successfulValues.length}` ); }