UNPKG

@decent-stuff/dc-client

Version:

High-performance WebAssembly client for browser-based querying of Decent Cloud ledger data

214 lines (192 loc) 7.04 kB
import { HttpAgent, Actor, Identity } from '@dfinity/agent'; import { idlFactory } from './canister_idl.js'; /** * Default configuration for the agent */ const defaultConfig = { networkUrl: 'https://icp-api.io', canisterId: 'ggi4a-wyaaa-aaaai-actqq-cai', }; // Singleton agent instance let agent: HttpAgent | null = null; let agentIdentity: Identity | null = null; /** * Get or create an agent instance * @param identity Optional identity to use for the agent * @returns An HttpAgent instance */ export function getAgent(identity?: Identity | null): HttpAgent { // Create new agent if there isn't one or the identity differs if (!agent || agentIdentity !== identity) { try { if (identity) { agent = HttpAgent.createSync({ host: defaultConfig.networkUrl, shouldFetchRootKey: true, identity, }); console.log('Agent created with identity:', identity); } else { agent = HttpAgent.createSync({ host: defaultConfig.networkUrl, shouldFetchRootKey: true, }); console.log('Agent created without identity'); } // Store the identity used for this agent agentIdentity = identity || null; } catch (error) { console.error(`Failed to initialize ${identity ? 'authenticated' : 'anonymous'} HttpAgent`); throw error; } } return agent; } /** * Configure the agent with custom settings * @param config Configuration options */ export function configure(config: Partial<typeof defaultConfig>): void { Object.assign(defaultConfig, config); agent = null; // Reset the agent to force recreation with new config } /** * Type for canister query options */ interface CanisterCallOptions { canisterId?: string; } /** * Query a canister method * @param methodName The name of the method to call * @param args The arguments to pass to the method * @param options Additional options * @returns The result of the query */ export async function queryCanister( methodName: string, args: unknown[], options: CanisterCallOptions = {} ): Promise<unknown> { try { // Input validation if (!methodName || typeof methodName !== 'string') { throw new Error(`Invalid method name: ${methodName}`); } if (!Array.isArray(args)) { console.warn(`Args is not an array, converting: ${args}`); args = [args]; // Convert to array to avoid errors } // Get agent with better error handling let currentAgent; try { currentAgent = getAgent(null); } catch (agentError) { console.error('Failed to create agent:', agentError); throw new Error(`Agent creation failed: ${(agentError as Error).message}`); } const canisterId = options.canisterId || defaultConfig.canisterId; // Create actor with better error handling let actor; try { actor = Actor.createActor(idlFactory, { agent: currentAgent, canisterId, }); } catch (actorError) { console.error('Failed to create actor:', actorError); throw new Error(`Actor creation failed: ${(actorError as Error).message}`); } if (typeof actor[methodName] !== 'function') { throw new Error(`Method ${methodName} not found on the canister interface.`); } // Call method with better error handling try { return await actor[methodName](...args); } catch (callError) { console.error(`Error calling method ${methodName}:`, callError); // Provide more diagnostics for TextDecoder errors const errorMessage = (callError as Error).message; if (errorMessage && errorMessage.includes('TextDecoder')) { console.error( '[CRITICAL] TextDecoder.decode failed - this is likely due to malformed binary data', { errorName: (callError as Error).name, errorStack: (callError as Error).stack, } ); } throw new Error(`Canister method call failed: ${errorMessage}`); } } catch (error) { console.error('Error in queryCanister:', error); throw error; } } /** * Update a canister method (authenticated call) * @param methodName The name of the method to call * @param args The arguments to pass to the method * @param identity The identity to use for the call * @param options Additional options * @returns The result of the update */ export async function updateCanister( methodName: string, args: unknown[], identity: Identity, options: CanisterCallOptions = {} ): Promise<unknown> { try { const currentAgent = getAgent(identity); const canisterId = options.canisterId || defaultConfig.canisterId; const actor = Actor.createActor(idlFactory, { agent: currentAgent, canisterId, }); if (typeof actor[methodName] !== 'function') { throw new Error(`Method "${methodName}" not found on the canister interface.`); } return await actor[methodName](...args); } catch (error) { console.error('Error in updateCanister:', error); throw error; } } /** * Type for ledger data response */ interface LedgerDataResponse { Ok?: [string, Uint8Array]; Err?: string; } /** * Query the ledger canister for new blocks * @param cursor The cursor position to start fetching from * @param bytesBefore Optional bytes before the cursor * @returns The result of the query */ export async function canisterQueryLedgerData(cursor: string, bytesBefore?: [Uint8Array]): Promise<LedgerDataResponse> { console.log('[Fetch] Fetching data from canister, with cursor:', cursor); try { // Wrap canister query with extra error handling let result: LedgerDataResponse; try { result = await queryCanister('data_fetch', [[cursor], bytesBefore || []], {}) as LedgerDataResponse; } catch (queryError) { console.error(`[Fetch] Error in data_fetch query: ${(queryError as Error).message}`, queryError); const errorMessage = (queryError as Error).message; if (errorMessage && errorMessage.includes('TextDecoder')) { console.error('[Fetch] TextDecoder error detected - possibly malformed binary data'); } throw queryError; } console.log( `[Fetch] Successfully fetched fresh data for cursor: ${cursor}` ); return result; } catch (error) { console.error('Error in Fetch:', error); throw error; } }