Skip to content

Commit adeb44b

Browse files
authored
Merge pull request #19 from aibtcdev/fix/better-error-handling
Add logging / improve error handling
2 parents bda1a0a + 07a3a58 commit adeb44b

23 files changed

+1299
-586
lines changed

src/config.ts

+24-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Env } from '../worker-configuration';
2+
import { ApiError } from './utils/api-error';
3+
import { ErrorCode } from './utils/error-catalog';
24

35
/**
46
* Singleton configuration class for the application
5-
*
7+
*
68
* Provides centralized access to configuration settings and environment variables
79
*/
810
export class AppConfig {
@@ -11,7 +13,7 @@ export class AppConfig {
1113

1214
/**
1315
* Private constructor to enforce singleton pattern
14-
*
16+
*
1517
* @param env - The Cloudflare Worker environment
1618
*/
1719
private constructor(env: Env) {
@@ -20,7 +22,7 @@ export class AppConfig {
2022

2123
/**
2224
* Gets the singleton instance of AppConfig
23-
*
25+
*
2426
* @param env - The Cloudflare Worker environment (required on first call)
2527
* @returns The AppConfig singleton instance
2628
* @throws Error if called without env before initialization
@@ -29,17 +31,22 @@ export class AppConfig {
2931
if (!AppConfig.instance && env) {
3032
AppConfig.instance = new AppConfig(env);
3133
} else if (!AppConfig.instance) {
32-
throw new Error('AppConfig must be initialized with environment variables first');
34+
throw new ApiError(ErrorCode.CONFIG_ERROR, {
35+
reason: 'AppConfig must be initialized with environment variables first',
36+
});
3337
}
3438
return AppConfig.instance;
3539
}
3640

3741
/**
3842
* Returns the application configuration settings
39-
*
43+
*
4044
* @returns Configuration object with all application settings
4145
*/
4246
public getConfig() {
47+
// Check if Hiro API key is available
48+
const hasHiroApiKey = !!this.env.HIRO_API_KEY;
49+
4350
return {
4451
// supported services for API caching
4552
// each entry is a durable object that handles requests
@@ -54,9 +61,21 @@ export class AppConfig {
5461
RETRY_DELAY: 1000, // multiplied by retry attempt number
5562
// how often to warm the cache, should be shorter than the cache TTL
5663
ALARM_INTERVAL_MS: 300000, // 5 minutes
64+
// Hiro API specific rate limiting settings
65+
HIRO_API_RATE_LIMIT: {
66+
// Adjust based on whether we have an API key
67+
// Hiro limits: 50 RPM without key, 500 RPM with key
68+
MAX_REQUESTS_PER_MINUTE: hasHiroApiKey ? 500 : 50,
69+
// Convert to our interval format
70+
get MAX_REQUESTS_PER_INTERVAL() {
71+
return this.MAX_REQUESTS_PER_MINUTE;
72+
},
73+
INTERVAL_MS: 60000, // 1 minute
74+
},
5775
// environment variables
5876
SUPABASE_URL: this.env.SUPABASE_URL,
5977
SUPABASE_SERVICE_KEY: this.env.SUPABASE_SERVICE_KEY,
78+
HIRO_API_KEY: this.env.HIRO_API_KEY,
6079
};
6180
}
6281
}

src/durable-objects/bns-do.ts

+64-56
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { DurableObject } from 'cloudflare:workers';
22
import { Env } from '../../worker-configuration';
33
import { AppConfig } from '../config';
44
import { validateStacksAddress } from '@stacks/transactions';
5-
import { createJsonResponse } from '../utils/requests-responses-util';
65
import { getKnownAddresses } from '../utils/address-store-util';
76
import { getNameFromAddress, initStacksFetcher } from '../utils/bns-v2-util';
7+
import { ApiError } from '../utils/api-error';
8+
import { ErrorCode } from '../utils/error-catalog';
9+
import { handleRequest } from '../utils/request-handler';
810

911
/**
1012
* Durable Object class for the BNS (Blockchain Naming System) API
11-
*
13+
*
1214
* This Durable Object handles BNS name lookups for Stacks addresses.
1315
* It provides endpoints to retrieve BNS names associated with Stacks addresses
1416
* and maintains a cache of these associations to reduce API calls.
@@ -40,13 +42,13 @@ export class BnsApiDO extends DurableObject<Env> {
4042

4143
/**
4244
* Alarm handler that periodically updates the BNS name cache
43-
*
45+
*
4446
* This method is triggered by the Durable Object's alarm system and:
4547
* 1. Retrieves all known Stacks addresses from KV storage
4648
* 2. Updates the BNS name for each address
4749
* 3. Stores the results in KV cache with the configured TTL
4850
* 4. Logs statistics about the update process
49-
*
51+
*
5052
* @returns A promise that resolves when the alarm handler completes
5153
*/
5254
async alarm(): Promise<void> {
@@ -97,75 +99,81 @@ export class BnsApiDO extends DurableObject<Env> {
9799

98100
/**
99101
* Main request handler for the BNS API Durable Object
100-
*
102+
*
101103
* Handles the following endpoints:
102104
* - / - Returns a list of supported endpoints
103105
* - /names/{address} - Returns the BNS name for the given Stacks address
104-
*
106+
*
105107
* @param request - The incoming HTTP request
106108
* @returns A Response object with the requested data or an error message
107109
*/
108110
async fetch(request: Request): Promise<Response> {
109111
const url = new URL(request.url);
110112
const path = url.pathname;
111113

112-
if (!path.startsWith(this.BASE_PATH)) {
113-
return createJsonResponse(
114-
{
115-
error: `Request at ${path} does not start with base path ${this.BASE_PATH}`,
116-
},
117-
404
118-
);
119-
}
114+
return handleRequest(
115+
async () => {
116+
if (!path.startsWith(this.BASE_PATH)) {
117+
throw new ApiError(ErrorCode.NOT_FOUND, {
118+
resource: path,
119+
basePath: this.BASE_PATH,
120+
});
121+
}
120122

121-
// Remove base path to get the endpoint
122-
const endpoint = path.replace(this.BASE_PATH, '');
123+
// Remove base path to get the endpoint
124+
const endpoint = path.replace(this.BASE_PATH, '');
123125

124-
// Handle root path
125-
if (endpoint === '' || endpoint === '/') {
126-
return createJsonResponse({
127-
message: `Supported endpoints: ${this.SUPPORTED_ENDPOINTS.join(', ')}`,
128-
});
129-
}
126+
// Handle root path
127+
if (endpoint === '' || endpoint === '/') {
128+
return {
129+
message: `Supported endpoints: ${this.SUPPORTED_ENDPOINTS.join(', ')}`,
130+
};
131+
}
130132

131-
if (endpoint === '/names') {
132-
return createJsonResponse({
133-
message: `Please provide an address to look up the name for, e.g. /names/SP2QEZ06AGJ3RKJPBV14SY1V5BBFNAW33D96YPGZF`,
134-
});
135-
}
133+
if (endpoint === '/names') {
134+
return {
135+
message: `Please provide an address to look up the name for, e.g. /names/SP2QEZ06AGJ3RKJPBV14SY1V5BBFNAW33D96YPGZF`,
136+
};
137+
}
136138

137-
// Handle name lookups
138-
if (endpoint.startsWith('/names/')) {
139-
const address = endpoint.replace('/names/', '');
140-
const validAddress = validateStacksAddress(address);
141-
if (!validAddress) {
142-
return createJsonResponse({ error: `Invalid address ${address}, valid Stacks address required` }, 400);
143-
}
144-
const cacheKey = `${this.CACHE_PREFIX}_names_${address}`;
145-
const cachedName = await this.env.AIBTCDEV_CACHE_KV.get<string>(cacheKey);
146-
if (cachedName) {
147-
return createJsonResponse(cachedName);
148-
}
149-
const name = await getNameFromAddress(address);
150-
151-
if (name === '') {
152-
return createJsonResponse(
153-
{
154-
error: `No registered name found for address ${address}`,
155-
},
156-
404
157-
);
158-
}
139+
// Handle name lookups
140+
if (endpoint.startsWith('/names/')) {
141+
const address = endpoint.replace('/names/', '');
142+
const validAddress = validateStacksAddress(address);
143+
if (!validAddress) {
144+
throw new ApiError(ErrorCode.INVALID_REQUEST, {
145+
reason: `Invalid address ${address}, valid Stacks address required`,
146+
});
147+
}
159148

160-
await this.env.AIBTCDEV_CACHE_KV.put(cacheKey, name, { expirationTtl: this.CACHE_TTL });
161-
return createJsonResponse(name);
162-
}
149+
const cacheKey = `${this.CACHE_PREFIX}_names_${address}`;
150+
const cachedName = await this.env.AIBTCDEV_CACHE_KV.get<string>(cacheKey);
151+
if (cachedName) {
152+
return cachedName;
153+
}
163154

164-
return createJsonResponse(
165-
{
166-
error: `Unsupported endpoint: ${endpoint}, supported endpoints: ${this.SUPPORTED_ENDPOINTS.join(', ')}`,
155+
const name = await getNameFromAddress(address);
156+
157+
if (name === '') {
158+
throw new ApiError(ErrorCode.NOT_FOUND, {
159+
resource: `name for address ${address}`,
160+
reason: 'No registered name found',
161+
});
162+
}
163+
164+
await this.env.AIBTCDEV_CACHE_KV.put(cacheKey, name, { expirationTtl: this.CACHE_TTL });
165+
return name;
166+
}
167+
168+
throw new ApiError(ErrorCode.NOT_FOUND, {
169+
resource: endpoint,
170+
supportedEndpoints: this.SUPPORTED_ENDPOINTS,
171+
});
167172
},
168-
404
173+
this.env,
174+
{
175+
slowThreshold: 2000, // BNS lookups can be slow
176+
}
169177
);
170178
}
171179
}

0 commit comments

Comments
 (0)