Skip to content

Commit 198cdaa

Browse files
committed
feat: predicate schemas
1 parent 478f68d commit 198cdaa

File tree

15 files changed

+436
-354
lines changed

15 files changed

+436
-354
lines changed

components/client/typescript/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
"version": "1.0.0",
44
"description": "TS client for Chainhook",
55
"main": "./dist/index.js",
6+
"typings": "./dist/index.d.ts",
67
"scripts": {
7-
"build": "tsc --project tsconfig.build.json",
8+
"build": "rimraf ./dist && tsc --project tsconfig.build.json",
89
"test": "jest"
910
},
1011
"repository": {
+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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+
}
78+
}
79+
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+
}
94+
}
95+
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();
102+
}
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;
147+
}

components/client/typescript/src/schemas/bitcoin/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Type } from '@sinclair/typebox';
2-
import { BlockIdentifier, Nullable } from '..';
2+
import { Nullable, BlockIdentifier } from '../common';
33

44
const InscriptionRevealed = Type.Object({
55
content_bytes: Type.String(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Type } from '@sinclair/typebox';
2+
3+
export const BitcoinIfThisTxIdSchema = Type.Object({
4+
scope: Type.Literal('txid'),
5+
equals: Type.String(),
6+
});
7+
8+
export const BitcoinIfThisOpReturnStartsWithSchema = Type.Object({
9+
scope: Type.Literal('outputs'),
10+
op_return: Type.Object({
11+
starts_with: Type.String(),
12+
}),
13+
});
14+
15+
export const BitcoinIfThisOpReturnEqualsSchema = Type.Object({
16+
scope: Type.Literal('outputs'),
17+
op_return: Type.Object({
18+
equals: Type.String(),
19+
}),
20+
});
21+
22+
export const BitcoinIfThisOpReturnEndsWithSchema = Type.Object({
23+
scope: Type.Literal('outputs'),
24+
op_return: Type.Object({
25+
ends_with: Type.String(),
26+
}),
27+
});
28+
29+
export const BitcoinIfThisP2PKHSchema = Type.Object({
30+
scope: Type.Literal('outputs'),
31+
p2pkh: Type.String(),
32+
});
33+
34+
export const BitcoinIfThisP2SHSchema = Type.Object({
35+
scope: Type.Literal('outputs'),
36+
p2sh: Type.String(),
37+
});
38+
39+
export const BitcoinIfThisP2WPKHSchema = Type.Object({
40+
scope: Type.Literal('outputs'),
41+
p2wpkh: Type.String(),
42+
});
43+
44+
export const BitcoinIfThisP2WSHSchema = Type.Object({
45+
scope: Type.Literal('outputs'),
46+
p2wsh: Type.String(),
47+
});
48+
49+
export const BitcoinIfThisStacksBlockCommittedSchema = Type.Object({
50+
scope: Type.Literal('stacks_protocol'),
51+
operation: Type.Literal('block_committed'),
52+
});
53+
54+
export const BitcoinIfThisStacksLeaderKeyRegisteredSchema = Type.Object({
55+
scope: Type.Literal('stacks_protocol'),
56+
operation: Type.Literal('leader_key_registered'),
57+
});
58+
59+
export const BitcoinIfThisStacksStxTransferredSchema = Type.Object({
60+
scope: Type.Literal('stacks_protocol'),
61+
operation: Type.Literal('stx_transfered'),
62+
});
63+
64+
export const BitcoinIfThisStacksStxLockedSchema = Type.Object({
65+
scope: Type.Literal('stacks_protocol'),
66+
operation: Type.Literal('stx_locked'),
67+
});
68+
69+
export const BitcoinIfThisOrdinalsFeedSchema = Type.Object({
70+
scope: Type.Literal('ordinals_protocol'),
71+
operation: Type.Literal('inscription_feed'),
72+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { TSchema, Type } from '@sinclair/typebox';
2+
3+
export const Nullable = <T extends TSchema>(type: T) => Type.Union([type, Type.Null()]);
4+
5+
export const BlockIdentifier = Type.Object({
6+
index: Type.Integer(),
7+
hash: Type.String(),
8+
});
9+
10+
export const TransactionIdentifier = Type.Object({
11+
hash: Type.String(),
12+
});

components/client/typescript/src/schemas/index.ts

+2-51
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,10 @@
1-
import { Static, TSchema, Type } from '@sinclair/typebox';
1+
import { Static, Type } from '@sinclair/typebox';
22
import { StacksEvent } from './stacks';
33
import { BitcoinEvent } from './bitcoin';
4-
5-
export const Nullable = <T extends TSchema>(type: T) => Type.Union([type, Type.Null()]);
6-
7-
export const BlockIdentifier = Type.Object({
8-
index: Type.Integer(),
9-
hash: Type.String(),
10-
});
11-
12-
export const TransactionIdentifier = Type.Object({
13-
hash: Type.String(),
14-
});
4+
import { IfThisSchema } from './predicate';
155

166
const EventArray = Type.Union([Type.Array(StacksEvent), Type.Array(BitcoinEvent)]);
177

18-
export const IfThisSchema = Type.Object({
19-
scope: Type.String(),
20-
operation: Type.String(),
21-
});
22-
export type IfThis = Static<typeof IfThisSchema>;
23-
24-
export const ThenThatSchema = Type.Union([
25-
Type.Object({
26-
file_append: Type.Object({
27-
path: Type.String(),
28-
}),
29-
}),
30-
Type.Object({
31-
http_post: Type.Object({
32-
url: Type.String({ format: 'uri' }),
33-
authorization_header: Type.String(),
34-
}),
35-
}),
36-
]);
37-
38-
export const IfThisThenThatSchema = Type.Object({
39-
start_block: Type.Optional(Type.Integer()),
40-
end_block: Type.Optional(Type.Integer()),
41-
if_this: IfThisSchema,
42-
then_that: ThenThatSchema,
43-
});
44-
45-
export const PredicateSchema = Type.Object({
46-
uuid: Type.String({ format: 'uuid' }),
47-
name: Type.String(),
48-
version: Type.Integer(),
49-
chain: Type.String(),
50-
networks: Type.Object({
51-
mainnet: Type.Optional(IfThisThenThatSchema),
52-
testnet: Type.Optional(IfThisThenThatSchema),
53-
}),
54-
});
55-
export type Predicate = Static<typeof PredicateSchema>;
56-
578
export const PayloadSchema = Type.Object({
589
apply: EventArray,
5910
rollback: EventArray,

0 commit comments

Comments
 (0)