Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/faucet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
"@happy.tech/txm": "workspace:0.1.0",
"@hono/node-server": "^1.13.8",
"@scalar/hono-api-reference": "^0.5.175",
"better-sqlite3": "^11.7.0",
"hono": "^4.7.2",
"hono-openapi": "^0.4.4",
"neverthrow": "^8.2.0",
"zod": "^3.23.8",
"better-sqlite3": "^11.7.0",
"kysely": "^0.27.5",
"viem": "^2.21.53"
"neverthrow": "^8.2.0",
"viem": "^2.21.53",
"zod": "^3.23.8"
},
"devDependencies": {
"@happy.tech/happybuild": "workspace:0.1.1",
Expand Down
1 change: 0 additions & 1 deletion apps/faucet/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const envSchema = z.object({
TOKEN_AMOUNT: z.string().transform((s) => BigInt(s)),
FAUCET_DB_PATH: z.string().trim(),
FAUCET_RATE_LIMIT_WINDOW_SECONDS: z.string().transform((s) => Number(s)),
FAUCET_RATE_LIMIT_MAX_REQUESTS: z.string().transform((s) => Number(s)),
})

const parsedEnv = envSchema.safeParse(process.env)
Expand Down
9 changes: 7 additions & 2 deletions apps/faucet/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { formatMs } from "@happy.tech/common"
import type { ContentfulStatusCode } from "hono/utils/http-status"

export abstract class HappyFaucetError extends Error {
Expand All @@ -23,8 +24,12 @@ export class FaucetFetchError extends HappyFaucetError {
}

export class FaucetRateLimitError extends HappyFaucetError {
constructor(message?: string, options?: ErrorOptions) {
super(429, message || "Rate limit exceeded", options)
constructor(timeToWait: number, message?: string, options?: ErrorOptions) {
super(
429,
message || "Rate limit exceeded, please wait " + formatMs(timeToWait, true) + " before requesting again",
options,
)
}
}

Expand Down
7 changes: 6 additions & 1 deletion apps/faucet/src/faucet-usage.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export class FaucetUsageRepository {

async findAllByAddress(address: Address): Promise<Result<FaucetUsage[], Error>> {
const result = await ResultAsync.fromPromise(
db.selectFrom("faucetUsage").selectAll().where("address", "=", address).execute(),
db
.selectFrom("faucetUsage")
.selectAll()
.where("address", "=", address)
.orderBy("occurredAt", "desc")
.execute(),
unknownToError,
)

Expand Down
10 changes: 8 additions & 2 deletions apps/faucet/src/services/faucet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@ export class FaucetService {
if (faucetUsageResult.isErr()) {
return err(faucetUsageResult.error)
}
if (faucetUsageResult.value.length >= env.FAUCET_RATE_LIMIT_MAX_REQUESTS) {
return err(new FaucetRateLimitError())
if (faucetUsageResult.value.length >= 1) {
const lastRequest = faucetUsageResult.value[0]
const timeToWait =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way this can be negative?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we might be missing an if(timeToWait>0)... ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It cannot be negative because, if it were, that would mean the request occurred before the time window, and we're only processing requests within the time window

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the reason is that the DB doesn't store uses longer than the window? This definitely could use a comment, and to be honest, a check wouldn't be a bad idea anyway just in case. e.g. if the service crashes, stays down for some time, then loads before the first prune can go through, the scenario of a negative number seems possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

lastRequest.occurredAt.getTime() + env.FAUCET_RATE_LIMIT_WINDOW_SECONDS * 1000 - Date.now()

if (timeToWait > 0) {
return err(new FaucetRateLimitError(timeToWait))
}
}

const faucetUsage = FaucetUsage.create(address)
Expand Down
1 change: 1 addition & 0 deletions support/common/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export { stringify } from "./utils/string"
export { throttle } from "./utils/throttle"
export { getUrlProtocol } from "./utils/urlProtocol"
export { type UUID, createUUID } from "./utils/uuid"
export { formatMs } from "./utils/time"

// === DATA ========================================================================================

Expand Down
18 changes: 18 additions & 0 deletions support/common/lib/utils/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const unit = {
ms: 1,
s: 1e3,
m: 60e3,
h: 3_600e3,
d: 86_400e3,
} as const;

export function formatMs(ms: number, long = false): string {
const abs = Math.abs(ms);
for (const [abbr, size] of Object.entries(unit).reverse()) {
if (abs >= size) {
const val = Math.round(ms / size);
return long ? `${val} ${abbr}${Math.abs(val) !== 1 ? 's' : ''}` : `${val}${abbr}`;
}
}
return `${ms}ms`;
}
Loading