-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
301 additions
and
230 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,147 +1,30 @@ | ||
import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; | ||
import Fastify, { | ||
FastifyInstance, | ||
FastifyPluginCallback, | ||
FastifyReply, | ||
FastifyRequest, | ||
} from 'fastify'; | ||
import { Server } from 'http'; | ||
import { request } from 'undici'; | ||
import { logger, PINO_CONFIG } from './util/logger'; | ||
import { timeout } from './util/helpers'; | ||
import { Payload, PayloadSchema } from './schemas'; | ||
import { Predicate, ThenThat } from './schemas/predicate'; | ||
|
||
export type OnEventCallback = (uuid: string, payload: Payload) => Promise<void>; | ||
|
||
type ServerOptions = { | ||
server: { | ||
host: string; | ||
port: number; | ||
auth_token: string; | ||
external_hostname: string; | ||
}; | ||
chainhook_node: { | ||
hostname: string; | ||
port: number; | ||
}; | ||
}; | ||
|
||
/** | ||
* Starts the chainhook event server. | ||
* @returns Fastify instance | ||
*/ | ||
export async function startServer( | ||
opts: ServerOptions, | ||
predicates: [Predicate], | ||
callback: OnEventCallback | ||
) { | ||
const base_path = `http://${opts.chainhook_node.hostname}:${opts.chainhook_node.port}`; | ||
|
||
async function waitForNode(this: FastifyInstance) { | ||
logger.info(`EventServer connecting to chainhook node...`); | ||
while (true) { | ||
try { | ||
await request(`${base_path}/ping`, { method: 'GET', throwOnError: true }); | ||
break; | ||
} catch (error) { | ||
logger.error(error, 'Chainhook node not available, retrying...'); | ||
await timeout(1000); | ||
} | ||
} | ||
} | ||
|
||
async function registerPredicates(this: FastifyInstance) { | ||
logger.info(predicates, `EventServer registering predicates on ${base_path}...`); | ||
for (const predicate of predicates) { | ||
const thenThat: ThenThat = { | ||
http_post: { | ||
url: `http://${opts.server.external_hostname}/chainhook/${predicate.uuid}`, | ||
authorization_header: `Bearer ${opts.server.auth_token}`, | ||
}, | ||
}; | ||
try { | ||
const body = predicate; | ||
if ('mainnet' in body.networks) body.networks.mainnet.then_that = thenThat; | ||
if ('testnet' in body.networks) body.networks.testnet.then_that = thenThat; | ||
await request(`${base_path}/v1/chainhooks`, { | ||
method: 'POST', | ||
body: JSON.stringify(body), | ||
headers: { 'content-type': 'application/json' }, | ||
throwOnError: true, | ||
}); | ||
logger.info(`EventServer registered '${predicate.name}' predicate (${predicate.uuid})`); | ||
} catch (error) { | ||
logger.error(error, `EventServer unable to register predicate`); | ||
} | ||
} | ||
import { FastifyInstance } from 'fastify'; | ||
import { | ||
ServerOptions, | ||
ChainhookNodeOptions, | ||
ServerPredicate, | ||
OnEventCallback, | ||
buildServer, | ||
} from './server'; | ||
|
||
export class ChainhookEventServer { | ||
private fastify?: FastifyInstance; | ||
private serverOpts: ServerOptions; | ||
private chainhookOpts: ChainhookNodeOptions; | ||
|
||
constructor(serverOpts: ServerOptions, chainhookOpts: ChainhookNodeOptions) { | ||
this.serverOpts = serverOpts; | ||
this.chainhookOpts = chainhookOpts; | ||
} | ||
|
||
async function removePredicates(this: FastifyInstance) { | ||
logger.info(`EventServer closing predicates...`); | ||
for (const predicate of predicates) { | ||
try { | ||
await request(`${base_path}/v1/chainhooks/${predicate.chain}/${predicate.uuid}`, { | ||
method: 'DELETE', | ||
headers: { 'content-type': 'application/json' }, | ||
throwOnError: true, | ||
}); | ||
logger.info(`EventServer removed '${predicate.name}' predicate (${predicate.uuid})`); | ||
} catch (error) { | ||
logger.error(error, `EventServer unable to deregister predicate`); | ||
} | ||
} | ||
async start(predicates: [ServerPredicate], callback: OnEventCallback) { | ||
if (this.fastify) return; | ||
this.fastify = await buildServer(this.serverOpts, this.chainhookOpts, predicates, callback); | ||
return this.fastify.listen({ host: this.serverOpts.hostname, port: this.serverOpts.port }); | ||
} | ||
|
||
async function isEventAuthorized(request: FastifyRequest, reply: FastifyReply) { | ||
const authHeader = request.headers.authorization; | ||
if (authHeader && authHeader === `Bearer ${opts.server.auth_token}`) { | ||
return; | ||
} | ||
await reply.code(403).send(); | ||
async close() { | ||
await this.fastify?.close(); | ||
this.fastify = undefined; | ||
} | ||
|
||
const EventServer: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTypeProvider> = ( | ||
fastify, | ||
options, | ||
done | ||
) => { | ||
fastify.addHook('preHandler', isEventAuthorized); | ||
fastify.post( | ||
'/chainhook/:uuid', | ||
{ | ||
schema: { | ||
params: Type.Object({ | ||
uuid: Type.String({ format: 'uuid' }), | ||
}), | ||
body: PayloadSchema, | ||
}, | ||
}, | ||
async (request, reply) => { | ||
try { | ||
await callback(request.params.uuid, request.body); | ||
} catch (error) { | ||
logger.error(error, `EventServer error processing payload`); | ||
await reply.code(422).send(); | ||
} | ||
await reply.code(200).send(); | ||
} | ||
); | ||
done(); | ||
}; | ||
|
||
const fastify = Fastify({ | ||
trustProxy: true, | ||
logger: PINO_CONFIG, | ||
pluginTimeout: 0, // Disable so ping can retry indefinitely | ||
bodyLimit: 41943040, // 40 MB | ||
}).withTypeProvider<TypeBoxTypeProvider>(); | ||
|
||
fastify.addHook('onReady', waitForNode); | ||
fastify.addHook('onReady', registerPredicates); | ||
fastify.addHook('onClose', removePredicates); | ||
await fastify.register(EventServer); | ||
|
||
await fastify.listen({ host: opts.server.host, port: opts.server.port }); | ||
return fastify; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,97 +1,44 @@ | ||
import { Static, Type } from '@sinclair/typebox'; | ||
import { | ||
BitcoinIfThisTxIdSchema, | ||
BitcoinIfThisOpReturnStartsWithSchema, | ||
BitcoinIfThisOpReturnEqualsSchema, | ||
BitcoinIfThisOpReturnEndsWithSchema, | ||
BitcoinIfThisP2PKHSchema, | ||
BitcoinIfThisP2SHSchema, | ||
BitcoinIfThisP2WPKHSchema, | ||
BitcoinIfThisP2WSHSchema, | ||
BitcoinIfThisStacksBlockCommittedSchema, | ||
BitcoinIfThisStacksLeaderKeyRegisteredSchema, | ||
BitcoinIfThisStacksStxTransferredSchema, | ||
BitcoinIfThisStacksStxLockedSchema, | ||
BitcoinIfThisOrdinalsFeedSchema, | ||
} from './bitcoin/predicate'; | ||
import { | ||
StacksIfThisTxIdSchema, | ||
StacksIfThisBlockHeightHigherThanSchema, | ||
StacksIfThisFtEventSchema, | ||
StacksIfThisNftEventSchema, | ||
StacksIfThisStxEventSchema, | ||
StacksIfThisPrintEventSchema, | ||
StacksIfThisContractCallSchema, | ||
StacksIfThisContractDeploymentSchema, | ||
StacksIfThisContractDeploymentTraitSchema, | ||
} from './stacks/predicate'; | ||
import { BitcoinIfThisThenThatSchema } from './bitcoin/if_this'; | ||
import { StacksIfThisThenThatSchema } from './stacks/if_this'; | ||
|
||
export const IfThisSchema = Type.Union([ | ||
BitcoinIfThisTxIdSchema, | ||
BitcoinIfThisOpReturnStartsWithSchema, | ||
BitcoinIfThisOpReturnEqualsSchema, | ||
BitcoinIfThisOpReturnEndsWithSchema, | ||
BitcoinIfThisP2PKHSchema, | ||
BitcoinIfThisP2SHSchema, | ||
BitcoinIfThisP2WPKHSchema, | ||
BitcoinIfThisP2WSHSchema, | ||
BitcoinIfThisStacksBlockCommittedSchema, | ||
BitcoinIfThisStacksLeaderKeyRegisteredSchema, | ||
BitcoinIfThisStacksStxTransferredSchema, | ||
BitcoinIfThisStacksStxLockedSchema, | ||
BitcoinIfThisOrdinalsFeedSchema, | ||
StacksIfThisTxIdSchema, | ||
StacksIfThisBlockHeightHigherThanSchema, | ||
StacksIfThisFtEventSchema, | ||
StacksIfThisNftEventSchema, | ||
StacksIfThisStxEventSchema, | ||
StacksIfThisPrintEventSchema, | ||
StacksIfThisContractCallSchema, | ||
StacksIfThisContractDeploymentSchema, | ||
StacksIfThisContractDeploymentTraitSchema, | ||
]); | ||
export type IfThis = Static<typeof IfThisSchema>; | ||
|
||
export const ThenThatSchema = Type.Union([ | ||
Type.Object({ | ||
file_append: Type.Object({ | ||
path: Type.String(), | ||
}), | ||
}), | ||
Type.Object({ | ||
http_post: Type.Object({ | ||
url: Type.String({ format: 'uri' }), | ||
authorization_header: Type.String(), | ||
}), | ||
export const ThenThatFileAppendSchema = Type.Object({ | ||
file_append: Type.Object({ | ||
path: Type.String(), | ||
}), | ||
]); | ||
export type ThenThat = Static<typeof ThenThatSchema>; | ||
}); | ||
export type ThenThatFileAppend = Static<typeof ThenThatFileAppendSchema>; | ||
|
||
export const IfThisThenThatSchema = Type.Object({ | ||
start_block: Type.Optional(Type.Integer()), | ||
end_block: Type.Optional(Type.Integer()), | ||
expire_after_occurrence: Type.Optional(Type.Integer()), | ||
include_proof: Type.Optional(Type.Boolean()), | ||
include_inputs: Type.Optional(Type.Boolean()), | ||
include_outputs: Type.Optional(Type.Boolean()), | ||
include_witness: Type.Optional(Type.Boolean()), | ||
if_this: IfThisSchema, | ||
then_that: ThenThatSchema, | ||
export const ThenThatHttpPostSchema = Type.Object({ | ||
http_post: Type.Object({ | ||
url: Type.String({ format: 'uri' }), | ||
authorization_header: Type.String(), | ||
}), | ||
}); | ||
export type IfThisThenThat = Static<typeof IfThisThenThatSchema>; | ||
export type ThenThatHttpPost = Static<typeof ThenThatHttpPostSchema>; | ||
|
||
export const ThenThatSchema = Type.Union([ThenThatFileAppendSchema, ThenThatHttpPostSchema]); | ||
export type ThenThat = Static<typeof ThenThatSchema>; | ||
|
||
export const PredicateSchema = Type.Object({ | ||
export const PredicateHeaderSchema = Type.Object({ | ||
uuid: Type.String({ format: 'uuid' }), | ||
name: Type.String(), | ||
version: Type.Integer(), | ||
chain: Type.String(), | ||
networks: Type.Union([ | ||
Type.Object({ | ||
mainnet: IfThisThenThatSchema, | ||
}), | ||
Type.Object({ | ||
testnet: IfThisThenThatSchema, | ||
}), | ||
]), | ||
}); | ||
export type PredicateHeader = Static<typeof PredicateHeaderSchema>; | ||
|
||
export const PredicateSchema = Type.Composite([ | ||
PredicateHeaderSchema, | ||
Type.Object({ | ||
networks: Type.Union([ | ||
Type.Object({ | ||
mainnet: Type.Union([BitcoinIfThisThenThatSchema, StacksIfThisThenThatSchema]), | ||
}), | ||
Type.Object({ | ||
testnet: Type.Union([BitcoinIfThisThenThatSchema, StacksIfThisThenThatSchema]), | ||
}), | ||
]), | ||
}), | ||
]); | ||
export type Predicate = Static<typeof PredicateSchema>; |
Oops, something went wrong.