@decent-stuff/dc-client
Version:
High-performance WebAssembly client for browser-based querying of Decent Cloud ledger data
175 lines (174 loc) • 6.33 kB
JavaScript
import { HttpAgent, Actor } 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 = null;
let agentIdentity = null;
/**
* Get or create an agent instance
* @param identity Optional identity to use for the agent
* @returns An HttpAgent instance
*/
export function getAgent(identity) {
// 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) {
Object.assign(defaultConfig, config);
agent = null; // Reset the agent to force recreation with new config
}
/**
* 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, args, options = {}) {
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.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.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.message;
if (errorMessage && errorMessage.includes('TextDecoder')) {
console.error('[CRITICAL] TextDecoder.decode failed - this is likely due to malformed binary data', {
errorName: callError.name,
errorStack: callError.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, args, identity, options = {}) {
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;
}
}
/**
* 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, bytesBefore) {
console.log('[Fetch] Fetching data from canister, with cursor:', cursor);
try {
// Wrap canister query with extra error handling
let result;
try {
result = await queryCanister('data_fetch', [[cursor], bytesBefore || []], {});
}
catch (queryError) {
console.error(`[Fetch] Error in data_fetch query: ${queryError.message}`, queryError);
const errorMessage = queryError.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;
}
}