Skip to content

Commit 9dfec45

Browse files
committed
feat: class interface
1 parent 198cdaa commit 9dfec45

File tree

6 files changed

+301
-230
lines changed

6 files changed

+301
-230
lines changed
+24-141
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,30 @@
1-
import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
2-
import Fastify, {
3-
FastifyInstance,
4-
FastifyPluginCallback,
5-
FastifyReply,
6-
FastifyRequest,
7-
} from 'fastify';
8-
import { Server } from 'http';
9-
import { request } from 'undici';
10-
import { logger, PINO_CONFIG } from './util/logger';
11-
import { timeout } from './util/helpers';
12-
import { Payload, PayloadSchema } from './schemas';
13-
import { Predicate, ThenThat } from './schemas/predicate';
14-
15-
export type OnEventCallback = (uuid: string, payload: Payload) => Promise<void>;
16-
17-
type ServerOptions = {
18-
server: {
19-
host: string;
20-
port: number;
21-
auth_token: string;
22-
external_hostname: string;
23-
};
24-
chainhook_node: {
25-
hostname: string;
26-
port: number;
27-
};
28-
};
29-
30-
/**
31-
* Starts the chainhook event server.
32-
* @returns Fastify instance
33-
*/
34-
export async function startServer(
35-
opts: ServerOptions,
36-
predicates: [Predicate],
37-
callback: OnEventCallback
38-
) {
39-
const base_path = `http://${opts.chainhook_node.hostname}:${opts.chainhook_node.port}`;
40-
41-
async function waitForNode(this: FastifyInstance) {
42-
logger.info(`EventServer connecting to chainhook node...`);
43-
while (true) {
44-
try {
45-
await request(`${base_path}/ping`, { method: 'GET', throwOnError: true });
46-
break;
47-
} catch (error) {
48-
logger.error(error, 'Chainhook node not available, retrying...');
49-
await timeout(1000);
50-
}
51-
}
52-
}
53-
54-
async function registerPredicates(this: FastifyInstance) {
55-
logger.info(predicates, `EventServer registering predicates on ${base_path}...`);
56-
for (const predicate of predicates) {
57-
const thenThat: ThenThat = {
58-
http_post: {
59-
url: `http://${opts.server.external_hostname}/chainhook/${predicate.uuid}`,
60-
authorization_header: `Bearer ${opts.server.auth_token}`,
61-
},
62-
};
63-
try {
64-
const body = predicate;
65-
if ('mainnet' in body.networks) body.networks.mainnet.then_that = thenThat;
66-
if ('testnet' in body.networks) body.networks.testnet.then_that = thenThat;
67-
await request(`${base_path}/v1/chainhooks`, {
68-
method: 'POST',
69-
body: JSON.stringify(body),
70-
headers: { 'content-type': 'application/json' },
71-
throwOnError: true,
72-
});
73-
logger.info(`EventServer registered '${predicate.name}' predicate (${predicate.uuid})`);
74-
} catch (error) {
75-
logger.error(error, `EventServer unable to register predicate`);
76-
}
77-
}
1+
import { FastifyInstance } from 'fastify';
2+
import {
3+
ServerOptions,
4+
ChainhookNodeOptions,
5+
ServerPredicate,
6+
OnEventCallback,
7+
buildServer,
8+
} from './server';
9+
10+
export class ChainhookEventServer {
11+
private fastify?: FastifyInstance;
12+
private serverOpts: ServerOptions;
13+
private chainhookOpts: ChainhookNodeOptions;
14+
15+
constructor(serverOpts: ServerOptions, chainhookOpts: ChainhookNodeOptions) {
16+
this.serverOpts = serverOpts;
17+
this.chainhookOpts = chainhookOpts;
7818
}
7919

80-
async function removePredicates(this: FastifyInstance) {
81-
logger.info(`EventServer closing predicates...`);
82-
for (const predicate of predicates) {
83-
try {
84-
await request(`${base_path}/v1/chainhooks/${predicate.chain}/${predicate.uuid}`, {
85-
method: 'DELETE',
86-
headers: { 'content-type': 'application/json' },
87-
throwOnError: true,
88-
});
89-
logger.info(`EventServer removed '${predicate.name}' predicate (${predicate.uuid})`);
90-
} catch (error) {
91-
logger.error(error, `EventServer unable to deregister predicate`);
92-
}
93-
}
20+
async start(predicates: [ServerPredicate], callback: OnEventCallback) {
21+
if (this.fastify) return;
22+
this.fastify = await buildServer(this.serverOpts, this.chainhookOpts, predicates, callback);
23+
return this.fastify.listen({ host: this.serverOpts.hostname, port: this.serverOpts.port });
9424
}
9525

96-
async function isEventAuthorized(request: FastifyRequest, reply: FastifyReply) {
97-
const authHeader = request.headers.authorization;
98-
if (authHeader && authHeader === `Bearer ${opts.server.auth_token}`) {
99-
return;
100-
}
101-
await reply.code(403).send();
26+
async close() {
27+
await this.fastify?.close();
28+
this.fastify = undefined;
10229
}
103-
104-
const EventServer: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTypeProvider> = (
105-
fastify,
106-
options,
107-
done
108-
) => {
109-
fastify.addHook('preHandler', isEventAuthorized);
110-
fastify.post(
111-
'/chainhook/:uuid',
112-
{
113-
schema: {
114-
params: Type.Object({
115-
uuid: Type.String({ format: 'uuid' }),
116-
}),
117-
body: PayloadSchema,
118-
},
119-
},
120-
async (request, reply) => {
121-
try {
122-
await callback(request.params.uuid, request.body);
123-
} catch (error) {
124-
logger.error(error, `EventServer error processing payload`);
125-
await reply.code(422).send();
126-
}
127-
await reply.code(200).send();
128-
}
129-
);
130-
done();
131-
};
132-
133-
const fastify = Fastify({
134-
trustProxy: true,
135-
logger: PINO_CONFIG,
136-
pluginTimeout: 0, // Disable so ping can retry indefinitely
137-
bodyLimit: 41943040, // 40 MB
138-
}).withTypeProvider<TypeBoxTypeProvider>();
139-
140-
fastify.addHook('onReady', waitForNode);
141-
fastify.addHook('onReady', registerPredicates);
142-
fastify.addHook('onClose', removePredicates);
143-
await fastify.register(EventServer);
144-
145-
await fastify.listen({ host: opts.server.host, port: opts.server.port });
146-
return fastify;
14730
}

components/client/typescript/src/schemas/bitcoin/predicate.ts renamed to components/client/typescript/src/schemas/bitcoin/if_this.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Type } from '@sinclair/typebox';
1+
import { Static, Type } from '@sinclair/typebox';
2+
import { ThenThatSchema } from '../predicate';
23

34
export const BitcoinIfThisTxIdSchema = Type.Object({
45
scope: Type.Literal('txid'),
@@ -70,3 +71,38 @@ export const BitcoinIfThisOrdinalsFeedSchema = Type.Object({
7071
scope: Type.Literal('ordinals_protocol'),
7172
operation: Type.Literal('inscription_feed'),
7273
});
74+
75+
export const BitcoinIfThisOptionsSchema = Type.Object({
76+
start_block: Type.Optional(Type.Integer()),
77+
end_block: Type.Optional(Type.Integer()),
78+
expire_after_occurrence: Type.Optional(Type.Integer()),
79+
include_proof: Type.Optional(Type.Boolean()),
80+
include_inputs: Type.Optional(Type.Boolean()),
81+
include_outputs: Type.Optional(Type.Boolean()),
82+
include_witness: Type.Optional(Type.Boolean()),
83+
});
84+
85+
export const BitcoinIfThisSchema = Type.Union([
86+
BitcoinIfThisTxIdSchema,
87+
BitcoinIfThisOpReturnStartsWithSchema,
88+
BitcoinIfThisOpReturnEqualsSchema,
89+
BitcoinIfThisOpReturnEndsWithSchema,
90+
BitcoinIfThisP2PKHSchema,
91+
BitcoinIfThisP2SHSchema,
92+
BitcoinIfThisP2WPKHSchema,
93+
BitcoinIfThisP2WSHSchema,
94+
BitcoinIfThisStacksBlockCommittedSchema,
95+
BitcoinIfThisStacksLeaderKeyRegisteredSchema,
96+
BitcoinIfThisStacksStxTransferredSchema,
97+
BitcoinIfThisStacksStxLockedSchema,
98+
BitcoinIfThisOrdinalsFeedSchema,
99+
]);
100+
export type BitcoinIfThis = Static<typeof BitcoinIfThisSchema>;
101+
102+
export const BitcoinIfThisThenThatSchema = Type.Composite([
103+
BitcoinIfThisOptionsSchema,
104+
Type.Object({
105+
if_this: BitcoinIfThisSchema,
106+
then_that: ThenThatSchema,
107+
}),
108+
]);
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Static, Type } from '@sinclair/typebox';
22
import { StacksEvent } from './stacks';
33
import { BitcoinEvent } from './bitcoin';
4-
import { IfThisSchema } from './predicate';
4+
import { BitcoinIfThisSchema } from './bitcoin/if_this';
5+
import { StacksIfThisSchema } from './stacks/if_this';
56

67
const EventArray = Type.Union([Type.Array(StacksEvent), Type.Array(BitcoinEvent)]);
78

@@ -10,7 +11,7 @@ export const PayloadSchema = Type.Object({
1011
rollback: EventArray,
1112
chainhook: Type.Object({
1213
uuid: Type.String(),
13-
predicate: IfThisSchema,
14+
predicate: Type.Union([BitcoinIfThisSchema, StacksIfThisSchema]),
1415
}),
1516
});
1617
export type Payload = Static<typeof PayloadSchema>;
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,44 @@
11
import { Static, Type } from '@sinclair/typebox';
2-
import {
3-
BitcoinIfThisTxIdSchema,
4-
BitcoinIfThisOpReturnStartsWithSchema,
5-
BitcoinIfThisOpReturnEqualsSchema,
6-
BitcoinIfThisOpReturnEndsWithSchema,
7-
BitcoinIfThisP2PKHSchema,
8-
BitcoinIfThisP2SHSchema,
9-
BitcoinIfThisP2WPKHSchema,
10-
BitcoinIfThisP2WSHSchema,
11-
BitcoinIfThisStacksBlockCommittedSchema,
12-
BitcoinIfThisStacksLeaderKeyRegisteredSchema,
13-
BitcoinIfThisStacksStxTransferredSchema,
14-
BitcoinIfThisStacksStxLockedSchema,
15-
BitcoinIfThisOrdinalsFeedSchema,
16-
} from './bitcoin/predicate';
17-
import {
18-
StacksIfThisTxIdSchema,
19-
StacksIfThisBlockHeightHigherThanSchema,
20-
StacksIfThisFtEventSchema,
21-
StacksIfThisNftEventSchema,
22-
StacksIfThisStxEventSchema,
23-
StacksIfThisPrintEventSchema,
24-
StacksIfThisContractCallSchema,
25-
StacksIfThisContractDeploymentSchema,
26-
StacksIfThisContractDeploymentTraitSchema,
27-
} from './stacks/predicate';
2+
import { BitcoinIfThisThenThatSchema } from './bitcoin/if_this';
3+
import { StacksIfThisThenThatSchema } from './stacks/if_this';
284

29-
export const IfThisSchema = Type.Union([
30-
BitcoinIfThisTxIdSchema,
31-
BitcoinIfThisOpReturnStartsWithSchema,
32-
BitcoinIfThisOpReturnEqualsSchema,
33-
BitcoinIfThisOpReturnEndsWithSchema,
34-
BitcoinIfThisP2PKHSchema,
35-
BitcoinIfThisP2SHSchema,
36-
BitcoinIfThisP2WPKHSchema,
37-
BitcoinIfThisP2WSHSchema,
38-
BitcoinIfThisStacksBlockCommittedSchema,
39-
BitcoinIfThisStacksLeaderKeyRegisteredSchema,
40-
BitcoinIfThisStacksStxTransferredSchema,
41-
BitcoinIfThisStacksStxLockedSchema,
42-
BitcoinIfThisOrdinalsFeedSchema,
43-
StacksIfThisTxIdSchema,
44-
StacksIfThisBlockHeightHigherThanSchema,
45-
StacksIfThisFtEventSchema,
46-
StacksIfThisNftEventSchema,
47-
StacksIfThisStxEventSchema,
48-
StacksIfThisPrintEventSchema,
49-
StacksIfThisContractCallSchema,
50-
StacksIfThisContractDeploymentSchema,
51-
StacksIfThisContractDeploymentTraitSchema,
52-
]);
53-
export type IfThis = Static<typeof IfThisSchema>;
54-
55-
export const ThenThatSchema = Type.Union([
56-
Type.Object({
57-
file_append: Type.Object({
58-
path: Type.String(),
59-
}),
60-
}),
61-
Type.Object({
62-
http_post: Type.Object({
63-
url: Type.String({ format: 'uri' }),
64-
authorization_header: Type.String(),
65-
}),
5+
export const ThenThatFileAppendSchema = Type.Object({
6+
file_append: Type.Object({
7+
path: Type.String(),
668
}),
67-
]);
68-
export type ThenThat = Static<typeof ThenThatSchema>;
9+
});
10+
export type ThenThatFileAppend = Static<typeof ThenThatFileAppendSchema>;
6911

70-
export const IfThisThenThatSchema = Type.Object({
71-
start_block: Type.Optional(Type.Integer()),
72-
end_block: Type.Optional(Type.Integer()),
73-
expire_after_occurrence: Type.Optional(Type.Integer()),
74-
include_proof: Type.Optional(Type.Boolean()),
75-
include_inputs: Type.Optional(Type.Boolean()),
76-
include_outputs: Type.Optional(Type.Boolean()),
77-
include_witness: Type.Optional(Type.Boolean()),
78-
if_this: IfThisSchema,
79-
then_that: ThenThatSchema,
12+
export const ThenThatHttpPostSchema = Type.Object({
13+
http_post: Type.Object({
14+
url: Type.String({ format: 'uri' }),
15+
authorization_header: Type.String(),
16+
}),
8017
});
81-
export type IfThisThenThat = Static<typeof IfThisThenThatSchema>;
18+
export type ThenThatHttpPost = Static<typeof ThenThatHttpPostSchema>;
19+
20+
export const ThenThatSchema = Type.Union([ThenThatFileAppendSchema, ThenThatHttpPostSchema]);
21+
export type ThenThat = Static<typeof ThenThatSchema>;
8222

83-
export const PredicateSchema = Type.Object({
23+
export const PredicateHeaderSchema = Type.Object({
8424
uuid: Type.String({ format: 'uuid' }),
8525
name: Type.String(),
8626
version: Type.Integer(),
8727
chain: Type.String(),
88-
networks: Type.Union([
89-
Type.Object({
90-
mainnet: IfThisThenThatSchema,
91-
}),
92-
Type.Object({
93-
testnet: IfThisThenThatSchema,
94-
}),
95-
]),
9628
});
29+
export type PredicateHeader = Static<typeof PredicateHeaderSchema>;
30+
31+
export const PredicateSchema = Type.Composite([
32+
PredicateHeaderSchema,
33+
Type.Object({
34+
networks: Type.Union([
35+
Type.Object({
36+
mainnet: Type.Union([BitcoinIfThisThenThatSchema, StacksIfThisThenThatSchema]),
37+
}),
38+
Type.Object({
39+
testnet: Type.Union([BitcoinIfThisThenThatSchema, StacksIfThisThenThatSchema]),
40+
}),
41+
]),
42+
}),
43+
]);
9744
export type Predicate = Static<typeof PredicateSchema>;

0 commit comments

Comments
 (0)