Skip to content
This repository was archived by the owner on Mar 18, 2026. It is now read-only.

Commit 58ed57b

Browse files
committed
feat: Implement chainhooks durable object and associated changes
1 parent 35fbca7 commit 58ed57b

5 files changed

Lines changed: 137 additions & 2 deletions

File tree

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class AppConfig {
5050
return {
5151
// supported services for API caching
5252
// each entry is a durable object that handles requests
53-
SUPPORTED_SERVICES: ['/bns', '/hiro-api', '/stx-city', '/supabase', '/contract-calls'],
53+
SUPPORTED_SERVICES: ['/bns', '/hiro-api', '/stx-city', '/supabase', '/contract-calls', '/chainhooks'],
5454
// VALUES BELOW CAN BE OVERRIDDEN BY DURABLE OBJECTS
5555
// default cache TTL used for KV
5656
CACHE_TTL: 900, // 15 minutes
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { DurableObject } from 'cloudflare:workers';
2+
import { Env } from '../../worker-configuration';
3+
import { AppConfig } from '../config';
4+
import { handleRequest } from '../utils/request-handler-util';
5+
import { ApiError } from '../utils/api-error-util';
6+
import { ErrorCode } from '../utils/error-catalog-util';
7+
import { Logger } from '../utils/logger-util';
8+
9+
export class ChainhooksDO extends DurableObject<Env> {
10+
// Configuration constants
11+
private readonly BASE_PATH: string = '/chainhooks';
12+
private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', '');
13+
private readonly SUPPORTED_ENDPOINTS: string[] = ['/post_event'];
14+
15+
constructor(ctx: DurableObjectState, env: Env) {
16+
super(ctx, env);
17+
this.ctx = ctx;
18+
this.env = env;
19+
20+
// Initialize AppConfig with environment
21+
const config = AppConfig.getInstance(env).getConfig();
22+
}
23+
24+
async fetch(request: Request): Promise<Response> {
25+
const url = new URL(request.url);
26+
const path = url.pathname;
27+
const method = request.method;
28+
29+
return handleRequest(
30+
async () => {
31+
if (!path.startsWith(this.BASE_PATH)) {
32+
throw new ApiError(ErrorCode.NOT_FOUND, { resource: path });
33+
}
34+
35+
// Remove base path to get the endpoint
36+
const endpoint = path.replace(this.BASE_PATH, '');
37+
38+
// Handle root path
39+
if (endpoint === '' || endpoint === '/') {
40+
return {
41+
message: `Supported endpoints: ${this.SUPPORTED_ENDPOINTS.join(', ')}`,
42+
};
43+
}
44+
45+
// Handle post_event endpoint
46+
if (endpoint === '/post_event') {
47+
if (method !== 'POST') {
48+
throw new ApiError(ErrorCode.INVALID_REQUEST, {
49+
reason: `Method ${method} not allowed for this endpoint. Use POST.`,
50+
});
51+
}
52+
53+
return await this.handlePostEvent(request);
54+
}
55+
56+
// If we get here, the endpoint is not supported
57+
throw new ApiError(ErrorCode.NOT_FOUND, {
58+
resource: endpoint,
59+
supportedEndpoints: this.SUPPORTED_ENDPOINTS,
60+
});
61+
},
62+
this.env,
63+
{
64+
path,
65+
method,
66+
}
67+
);
68+
}
69+
70+
private async handlePostEvent(request: Request): Promise<any> {
71+
const logger = Logger.getInstance(this.env);
72+
73+
try {
74+
// Clone the request to read the body
75+
const clonedRequest = request.clone();
76+
77+
// Try to parse as JSON first
78+
let body;
79+
try {
80+
body = await clonedRequest.json();
81+
} catch (e) {
82+
// If JSON parsing fails, get the body as text
83+
body = await request.text();
84+
}
85+
86+
// Log the received event
87+
logger.info('Received chainhook event', {
88+
body,
89+
headers: Object.fromEntries(request.headers.entries()),
90+
});
91+
92+
// Store the event in Durable Object storage for later analysis
93+
const eventId = crypto.randomUUID();
94+
await this.ctx.storage.put(`event_${eventId}`, {
95+
timestamp: new Date().toISOString(),
96+
body,
97+
headers: Object.fromEntries(request.headers.entries()),
98+
});
99+
100+
return {
101+
message: 'Event received and logged successfully',
102+
eventId,
103+
};
104+
} catch (error) {
105+
logger.error('Error processing chainhook event', error instanceof Error ? error : new Error(String(error)));
106+
throw new ApiError(ErrorCode.INTERNAL_ERROR, {
107+
reason: 'Failed to process chainhook event',
108+
});
109+
}
110+
}
111+
}

src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import { HiroApiDO } from './durable-objects/hiro-api-do';
55
import { StxCityDO } from './durable-objects/stx-city-do';
66
import { SupabaseDO } from './durable-objects/supabase-do';
77
import { ContractCallsDO } from './durable-objects/contract-calls-do';
8+
import { ChainhooksDO } from './durable-objects/chainhooks-do';
89
import { corsHeaders, createErrorResponse, createSuccessResponse } from './utils/requests-responses-util';
910
import { ApiError } from './utils/api-error-util';
1011
import { ErrorCode } from './utils/error-catalog-util';
1112
import { Logger } from './utils/logger-util';
1213

1314
// export the Durable Object classes we're using
14-
export { BnsApiDO, HiroApiDO, StxCityDO, SupabaseDO, ContractCallsDO };
15+
export { BnsApiDO, HiroApiDO, StxCityDO, SupabaseDO, ContractCallsDO, ChainhooksDO };
1516

1617
export default {
1718
/**
@@ -90,6 +91,12 @@ export default {
9091
let stub = env.CONTRACT_CALLS_DO.get(id); // get the stub for communication
9192
return await stub.fetch(request); // forward the request to the Durable Object
9293
}
94+
95+
if (path.startsWith('/chainhooks')) {
96+
let id: DurableObjectId = env.CHAINHOOKS_DO.idFromName('chainhooks-do'); // create the instance
97+
let stub = env.CHAINHOOKS_DO.get(id); // get the stub for communication
98+
return await stub.fetch(request); // forward the request to the Durable Object
99+
}
93100
} catch (error) {
94101
// Log errors from Durable Objects
95102
const duration = Date.now() - startTime;

worker-configuration.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export interface Env {
1010
STX_CITY_DO: DurableObjectNamespace<import('./src/index').StxCityDO>;
1111
SUPABASE_DO: DurableObjectNamespace<import('./src/index').SupabaseDO>;
1212
CONTRACT_CALLS_DO: DurableObjectNamespace<import('./src/index').ContractCallsDO>;
13+
CHAINHOOKS_DO: DurableObjectNamespace<import('./src/index').ChainhooksDO>;
1314
}

wrangler.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ new_classes = ["HiroApiDO", "SupabaseDO", "StxCityDO", "BnsApiDO"]
1717
tag = "20250323"
1818
new_classes = ["ContractCallsDO"]
1919

20+
[[migrations]]
21+
tag = "20250417"
22+
new_classes = ["ChainhooksDO"]
23+
2024

2125
[env.preview]
2226

@@ -46,6 +50,10 @@ class_name = "BnsApiDO"
4650
name = "CONTRACT_CALLS_DO"
4751
class_name = "ContractCallsDO"
4852

53+
[[env.preview.durable_objects.bindings]]
54+
name = "CHAINHOOKS_DO"
55+
class_name = "ChainhooksDO"
56+
4957

5058
[env.staging]
5159

@@ -75,6 +83,10 @@ class_name = "BnsApiDO"
7583
name = "CONTRACT_CALLS_DO"
7684
class_name = "ContractCallsDO"
7785

86+
[[env.staging.durable_objects.bindings]]
87+
name = "CHAINHOOKS_DO"
88+
class_name = "ChainhooksDO"
89+
7890

7991
[env.production]
8092

@@ -103,3 +115,7 @@ class_name = "BnsApiDO"
103115
[[env.production.durable_objects.bindings]]
104116
name = "CONTRACT_CALLS_DO"
105117
class_name = "ContractCallsDO"
118+
119+
[[env.production.durable_objects.bindings]]
120+
name = "CHAINHOOKS_DO"
121+
class_name = "ChainhooksDO"

0 commit comments

Comments
 (0)