Skip to content

Commit

Permalink
Merge branch 'fix/ordinals-multiple-parents' of github.com:hirosystem…
Browse files Browse the repository at this point in the history
…s/chainhook into fix/ordinals-multiple-parents
  • Loading branch information
rafaelcr committed Feb 5, 2025
2 parents cd7b30f + d61e045 commit c4fa2da
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 25 deletions.
24 changes: 19 additions & 5 deletions components/client/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Static, Type } from '@fastify/type-provider-typebox';
import { BitcoinIfThisOptionsSchema, BitcoinIfThisSchema } from './schemas/bitcoin/if_this';
import { StacksIfThisOptionsSchema, StacksIfThisSchema } from './schemas/stacks/if_this';
import { logger } from './util/logger';
import { PredicateSchema } from './schemas/predicate';

const EventObserverOptionsSchema = Type.Object({
/** Event observer host name (usually '0.0.0.0') */
Expand Down Expand Up @@ -40,6 +41,9 @@ const EventObserverOptionsSchema = Type.Object({
* up to date. If they become obsolete, we will attempt to re-register them.
*/
predicate_health_check_interval_ms: Type.Optional(Type.Integer({ default: 5000 })),
predicate_re_register_callback: Type.Optional(
Type.Function([PredicateSchema], Type.Promise(PredicateSchema))
),
});
/** Chainhook event observer configuration options */
export type EventObserverOptions = Static<typeof EventObserverOptionsSchema>;
Expand Down Expand Up @@ -126,14 +130,24 @@ export class ChainhookEventObserver {
this.fastify = await buildServer(this.observer, this.chainhook, predicates, callback);
await this.fastify.listen({ host: this.observer.hostname, port: this.observer.port });
if (this.observer.predicate_health_check_interval_ms && this.healthCheckTimer === undefined) {
this.healthCheckTimer = setInterval(() => {
predicateHealthCheck(this.observer, this.chainhook).catch(err =>
logger.error(err, `ChainhookEventObserver predicate health check error`)
);
}, this.observer.predicate_health_check_interval_ms);
this.scheduleHealthCheck();
}
}

private scheduleHealthCheck() {
this.healthCheckTimer = setTimeout(() => {
void predicateHealthCheck(this.observer, this.chainhook)
.catch(err => {
logger.error(err, `ChainhookEventObserver predicate health check error`);
})
.finally(() => {
if (this.healthCheckTimer) {
this.scheduleHealthCheck();
}
});
}, this.observer.predicate_health_check_interval_ms);
}

/**
* Stop the Chainhook event server gracefully.
*/
Expand Down
56 changes: 40 additions & 16 deletions components/client/typescript/src/predicates.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as fs from 'fs';
import * as fs from 'fs/promises';
import * as path from 'path';
import { logger } from './util/logger';
import {
Expand All @@ -17,17 +17,32 @@ const RegisteredPredicates = new Map<string, Predicate>();

const CompiledPredicateSchema = TypeCompiler.Compile(PredicateSchema);

// Async version of fs.existsSync
async function pathExists(path: string): Promise<boolean> {
try {
await fs.access(path);
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).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.
*/
export function recallPersistedPredicatesFromDisk(basePath: string): Map<string, Predicate> {
export async function recallPersistedPredicatesFromDisk(
basePath: string
): Promise<Map<string, Predicate>> {
RegisteredPredicates.clear();
try {
if (!fs.existsSync(basePath)) return RegisteredPredicates;
for (const file of fs.readdirSync(basePath)) {
if (!(await pathExists(basePath))) return RegisteredPredicates;
for (const file of await fs.readdir(basePath)) {
if (file.endsWith('.json')) {
const text = fs.readFileSync(path.join(basePath, file), 'utf-8');
const text = await fs.readFile(path.join(basePath, file), 'utf-8');
const predicate = JSON.parse(text) as JSON;
if (CompiledPredicateSchema.Check(predicate)) {
logger.info(
Expand All @@ -44,11 +59,11 @@ export function recallPersistedPredicatesFromDisk(basePath: string): Map<string,
return RegisteredPredicates;
}

export function savePredicateToDisk(basePath: string, predicate: Predicate) {
export async function savePredicateToDisk(basePath: string, predicate: Predicate) {
const predicatePath = `${basePath}/predicate-${encodeURIComponent(predicate.name)}.json`;
try {
fs.mkdirSync(basePath, { recursive: true });
fs.writeFileSync(predicatePath, JSON.stringify(predicate, null, 2));
await fs.mkdir(basePath, { recursive: true });
await fs.writeFile(predicatePath, JSON.stringify(predicate, null, 2));
logger.info(
`ChainhookEventObserver persisted predicate '${predicate.name}' (${predicate.uuid}) to disk`
);
Expand All @@ -60,13 +75,18 @@ export function savePredicateToDisk(basePath: string, predicate: Predicate) {
}
}

function deletePredicateFromDisk(basePath: string, predicate: Predicate) {
async function deletePredicateFromDisk(basePath: string, predicate: Predicate) {
const predicatePath = `${basePath}/predicate-${encodeURIComponent(predicate.name)}.json`;
if (fs.existsSync(predicatePath)) {
fs.rmSync(predicatePath);
try {
await fs.rm(predicatePath);
logger.info(
`ChainhookEventObserver deleted predicate '${predicate.name}' (${predicate.uuid}) from disk`
);
} catch (error: unknown) {
// ignore if the file doesn't exist
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
logger.error(error, `Failed to delete predicate`);
}
}
}

Expand Down Expand Up @@ -146,11 +166,15 @@ async function registerPredicate(
authorization_header: `Bearer ${observer.auth_token}`,
},
};
const newPredicate = pendingPredicate as Predicate;
let newPredicate = pendingPredicate as Predicate;
newPredicate.uuid = 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 request(`${chainhook.base_url}${path}`, {
method: 'POST',
Expand All @@ -161,7 +185,7 @@ async function registerPredicate(
logger.info(
`ChainhookEventObserver registered '${newPredicate.name}' predicate (${newPredicate.uuid})`
);
savePredicateToDisk(observer.predicate_disk_file_path, newPredicate);
await savePredicateToDisk(observer.predicate_disk_file_path, newPredicate);
RegisteredPredicates.set(newPredicate.name, newPredicate);
} catch (error) {
logger.error(error, `ChainhookEventObserver unable to register predicate`);
Expand All @@ -186,7 +210,7 @@ async function removePredicate(
throwOnError: true,
});
logger.info(`ChainhookEventObserver removed predicate '${predicate.name}' (${predicate.uuid})`);
deletePredicateFromDisk(observer.predicate_disk_file_path, predicate);
await deletePredicateFromDisk(observer.predicate_disk_file_path, predicate);
} catch (error) {
logger.error(error, `ChainhookEventObserver unable to deregister predicate`);
}
Expand All @@ -203,7 +227,7 @@ export async function registerAllPredicatesOnObserverReady(
logger.info(`ChainhookEventObserver does not have predicates to register`);
return;
}
const diskPredicates = recallPersistedPredicatesFromDisk(observer.predicate_disk_file_path);
const diskPredicates = await recallPersistedPredicatesFromDisk(observer.predicate_disk_file_path);
for (const predicate of predicates)
await registerPredicate(predicate, diskPredicates, observer, chainhook);
}
Expand All @@ -213,7 +237,7 @@ export async function removeAllPredicatesOnObserverClose(
observer: EventObserverOptions,
chainhook: ChainhookNodeOptions
) {
const diskPredicates = recallPersistedPredicatesFromDisk(observer.predicate_disk_file_path);
const diskPredicates = await recallPersistedPredicatesFromDisk(observer.predicate_disk_file_path);
if (diskPredicates.size === 0) {
logger.info(`ChainhookEventObserver does not have predicates to close`);
return;
Expand Down
8 changes: 8 additions & 0 deletions components/client/typescript/src/schemas/stacks/if_this.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ export const StacksIfThisPrintEventSchema = Type.Object({
});
export type StacksIfThisPrintEvent = Static<typeof StacksIfThisPrintEventSchema>;

export const StacksIfThisPrintEventRegexSchema = Type.Object({
scope: Type.Literal('print_event'),
contract_identifier: Type.String(),
matches_regex: Type.String(),
});
export type StacksIfThisPrintEventRegex = Static<typeof StacksIfThisPrintEventRegexSchema>;

export const StacksIfThisContractCallSchema = Type.Object({
scope: Type.Literal('contract_call'),
contract_identifier: Type.String(),
Expand Down Expand Up @@ -111,6 +118,7 @@ export const StacksIfThisSchema = Type.Union([
StacksIfThisNftEventSchema,
StacksIfThisStxEventSchema,
StacksIfThisPrintEventSchema,
StacksIfThisPrintEventRegexSchema,
StacksIfThisContractCallSchema,
StacksIfThisContractDeploymentSchema,
StacksIfThisContractDeploymentTraitSchema,
Expand Down
8 changes: 4 additions & 4 deletions components/client/typescript/tests/predicates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('predicates', () => {
await server.start([testPredicate], async () => {});

expect(fs.existsSync(`${observer.predicate_disk_file_path}/predicate-test.json`)).toBe(true);
const disk = recallPersistedPredicatesFromDisk(observer.predicate_disk_file_path);
const disk = await recallPersistedPredicatesFromDisk(observer.predicate_disk_file_path);
const storedPredicate = disk.get('test');
expect(storedPredicate).not.toBeUndefined();
expect(storedPredicate?.name).toBe(testPredicate.name);
Expand All @@ -102,8 +102,8 @@ describe('predicates', () => {
});

describe('pre-stored', () => {
beforeEach(() => {
savePredicateToDisk(observer.predicate_disk_file_path, {
beforeEach(async () => {
await savePredicateToDisk(observer.predicate_disk_file_path, {
uuid: 'e2777d77-473a-4c1d-9012-152deb36bf4c',
name: 'test',
version: 1,
Expand Down Expand Up @@ -164,7 +164,7 @@ describe('predicates', () => {

mockAgent.assertNoPendingInterceptors();
expect(fs.existsSync(`${observer.predicate_disk_file_path}/predicate-test.json`)).toBe(true);
const disk = recallPersistedPredicatesFromDisk(observer.predicate_disk_file_path);
const disk = await recallPersistedPredicatesFromDisk(observer.predicate_disk_file_path);
const storedPredicate = disk.get('test');
// Should have a different uuid
expect(storedPredicate?.uuid).not.toBe('e2777d77-473a-4c1d-9012-152deb36bf4c');
Expand Down

0 comments on commit c4fa2da

Please sign in to comment.