@hirosystems/chainhook-client
Version:
Chainhook TypeScript client
212 lines • 10.3 kB
JavaScript
;
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