UNPKG

@hirosystems/chainhook-client

Version:
212 lines 10.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.predicateHealthCheck = exports.removeAllPredicatesOnObserverClose = exports.registerAllPredicatesOnObserverReady = exports.savePredicateToDisk = exports.recallPersistedPredicatesFromDisk = void 0; const fs = require("fs/promises"); const path = require("path"); const logger_1 = require("./util/logger"); const predicate_1 = require("./schemas/predicate"); const undici_1 = require("undici"); const compiler_1 = require("@sinclair/typebox/compiler"); const crypto_1 = require("crypto"); /** Keeps the on-disk predicates in memory for faster access. */ const RegisteredPredicates = new Map(); const CompiledPredicateSchema = compiler_1.TypeCompiler.Compile(predicate_1.PredicateSchema); // Async version of fs.existsSync async function pathExists(path) { try { await fs.access(path); return true; } catch (error) { if (error.code === 'ENOENT') { return false; } throw error; // Re-throw other errors (e.g., permission issues) } } /** * Looks on disk and returns a map of registered Predicates, where the key is the predicate `name` * as defined by the user. */ async function recallPersistedPredicatesFromDisk(basePath) { RegisteredPredicates.clear(); try { if (!(await pathExists(basePath))) return RegisteredPredicates; for (const file of await fs.readdir(basePath)) { if (file.endsWith('.json')) { const text = await fs.readFile(path.join(basePath, file), 'utf-8'); const predicate = JSON.parse(text); if (CompiledPredicateSchema.Check(predicate)) { logger_1.logger.info(`ChainhookEventObserver recalled predicate '${predicate.name}' (${predicate.uuid}) from disk`); RegisteredPredicates.set(predicate.name, predicate); } } } } catch (error) { logger_1.logger.error(error, `ChainhookEventObserver unable to retrieve persisted predicates from disk`); RegisteredPredicates.clear(); } return RegisteredPredicates; } exports.recallPersistedPredicatesFromDisk = recallPersistedPredicatesFromDisk; async function savePredicateToDisk(basePath, predicate) { const predicatePath = `${basePath}/predicate-${encodeURIComponent(predicate.name)}.json`; try { await fs.mkdir(basePath, { recursive: true }); await fs.writeFile(predicatePath, JSON.stringify(predicate, null, 2)); logger_1.logger.info(`ChainhookEventObserver persisted predicate '${predicate.name}' (${predicate.uuid}) to disk`); } catch (error) { logger_1.logger.error(error, `ChainhookEventObserver unable to persist predicate '${predicate.name}' (${predicate.uuid}) to disk`); } } exports.savePredicateToDisk = savePredicateToDisk; async function deletePredicateFromDisk(basePath, predicate) { const predicatePath = `${basePath}/predicate-${encodeURIComponent(predicate.name)}.json`; try { await fs.rm(predicatePath); logger_1.logger.info(`ChainhookEventObserver deleted predicate '${predicate.name}' (${predicate.uuid}) from disk`); } catch (error) { // ignore if the file doesn't exist if (error.code !== 'ENOENT') { logger_1.logger.error(error, `Failed to delete predicate`); } } } /** Checks the Chainhook node to see if a predicate is still valid and active */ async function isPredicateActive(predicate, chainhook) { try { const result = await (0, undici_1.request)(`${chainhook.base_url}/v1/chainhooks/${predicate.uuid}`, { method: 'GET', headers: { accept: 'application/json' }, throwOnError: true, }); const response = (await result.body.json()); if (response.status == 404) return undefined; if (response.result.enabled == false || response.result.status.type == 'interrupted' || response.result.status.type == 'unconfirmed_expiration' || response.result.status.type == 'confirmed_expiration') { return false; } return true; } catch (error) { logger_1.logger.error(error, `ChainhookEventObserver unable to check if predicate '${predicate.name}' (${predicate.uuid}) is active`); return false; } } /** * Registers a predicate in the Chainhook server. Automatically handles pre-existing predicates * found on disk. */ async function registerPredicate(pendingPredicate, diskPredicates, observer, chainhook) { // First check if we've already registered this predicate in the past, and if so, make sure it's // still active on the Chainhook server. if (observer.node_type === 'chainhook') { const diskPredicate = diskPredicates.get(pendingPredicate.name); if (diskPredicate) { switch (await isPredicateActive(diskPredicate, chainhook)) { case true: logger_1.logger.debug(`ChainhookEventObserver predicate '${diskPredicate.name}' (${diskPredicate.uuid}) is active`); return; case undefined: logger_1.logger.info(`ChainhookEventObserver predicate '${diskPredicate.name}' (${diskPredicate.uuid}) found on disk but not on the Chainhook server`); break; case false: logger_1.logger.info(`ChainhookEventObserver predicate '${diskPredicate.name}' (${diskPredicate.uuid}) was being used but is now inactive, removing for re-regristration`); await removePredicate(diskPredicate, observer, chainhook); break; } } } logger_1.logger.info(`ChainhookEventObserver registering predicate '${pendingPredicate.name}'`); try { // Add the `uuid` and `then_that` portions to the predicate. const thenThat = { http_post: { url: `${observer.external_base_url}/payload`, authorization_header: `Bearer ${observer.auth_token}`, }, }; let newPredicate = pendingPredicate; newPredicate.uuid = (0, crypto_1.randomUUID)(); if (newPredicate.networks.mainnet) newPredicate.networks.mainnet.then_that = thenThat; if (newPredicate.networks.testnet) newPredicate.networks.testnet.then_that = thenThat; if (observer.predicate_re_register_callback) { newPredicate = await observer.predicate_re_register_callback(newPredicate); } const path = observer.node_type === 'chainhook' ? `/v1/chainhooks` : `/v1/observers`; await (0, undici_1.request)(`${chainhook.base_url}${path}`, { method: 'POST', body: JSON.stringify(newPredicate), headers: { 'content-type': 'application/json' }, throwOnError: true, }); logger_1.logger.info(`ChainhookEventObserver registered '${newPredicate.name}' predicate (${newPredicate.uuid})`); await savePredicateToDisk(observer.predicate_disk_file_path, newPredicate); RegisteredPredicates.set(newPredicate.name, newPredicate); } catch (error) { logger_1.logger.error(error, `ChainhookEventObserver unable to register predicate`); } } /** Removes a predicate from the Chainhook server */ async function removePredicate(predicate, observer, chainhook) { const nodeType = observer.node_type ?? 'chainhook'; const path = nodeType === 'chainhook' ? `/v1/chainhooks/${predicate.chain}/${encodeURIComponent(predicate.uuid)}` : `/v1/observers/${encodeURIComponent(predicate.uuid)}`; try { await (0, undici_1.request)(`${chainhook.base_url}${path}`, { method: 'DELETE', headers: { 'content-type': 'application/json' }, throwOnError: true, }); logger_1.logger.info(`ChainhookEventObserver removed predicate '${predicate.name}' (${predicate.uuid})`); await deletePredicateFromDisk(observer.predicate_disk_file_path, predicate); } catch (error) { logger_1.logger.error(error, `ChainhookEventObserver unable to deregister predicate`); } } /** Registers predicates with the Chainhook server when our event observer is booting up */ async function registerAllPredicatesOnObserverReady(predicates, observer, chainhook) { logger_1.logger.info(predicates, `ChainhookEventObserver connected to ${chainhook.base_url}`); if (predicates.length === 0) { logger_1.logger.info(`ChainhookEventObserver does not have predicates to register`); return; } const diskPredicates = await recallPersistedPredicatesFromDisk(observer.predicate_disk_file_path); for (const predicate of predicates) await registerPredicate(predicate, diskPredicates, observer, chainhook); } exports.registerAllPredicatesOnObserverReady = registerAllPredicatesOnObserverReady; /** Removes predicates from the Chainhook server when our event observer is being closed */ async function removeAllPredicatesOnObserverClose(observer, chainhook) { const diskPredicates = await recallPersistedPredicatesFromDisk(observer.predicate_disk_file_path); if (diskPredicates.size === 0) { logger_1.logger.info(`ChainhookEventObserver does not have predicates to close`); return; } logger_1.logger.info(`ChainhookEventObserver closing predicates at ${chainhook.base_url}`); const removals = [...RegisteredPredicates.values()].map(predicate => removePredicate(predicate, observer, chainhook)); await Promise.allSettled(removals); RegisteredPredicates.clear(); } exports.removeAllPredicatesOnObserverClose = removeAllPredicatesOnObserverClose; async function predicateHealthCheck(observer, chainhook) { logger_1.logger.debug(`ChainhookEventObserver performing predicate health check`); for (const predicate of RegisteredPredicates.values()) { // This will be a no-op if the predicate is already active. await registerPredicate(predicate, RegisteredPredicates, observer, chainhook); } } exports.predicateHealthCheck = predicateHealthCheck; //# sourceMappingURL=predicates.js.map