Repository navigation
chore(release): v3.18.0 - #845
Merged
Merged
Conversation
Refs #818 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Per-customer quota (default 100,000 CHEQ per UTC calendar month), reserved transactionally under a per-customer advisory lock. Refs #818 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Removes the API-key/customer-id rejection and enforces FAUCET_MONTHLY_LIMIT_CHEQ, returning 429 with Retry-After and quota details. Quota is released if the upstream faucet call fails. Closes #818 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t amounts An explicit amount is no longer blocked by the per-account balance cap; it is bounded by the remaining monthly quota. Omitting the amount still tops up to the balance cap. Refs #818 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reverts 6979f42 so the balance cap applies to explicit amounts again, and sources that cap from FAUCET_MONTHLY_LIMIT_CHEQ (default 100,000). The same figure is now the most an address can hold and the most a customer can request per calendar month. TESTNET_FAUCET_UPPER_CAP_CHEQ keeps governing only the automatic top-up at account creation. Refs #818 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…UP_CHEQ The setting only drives the automatic top-up at account bootstrap, so name it for that. The old variable is still read as a fallback so existing deployments keep their value. Refs #818 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Both account-bootstrap top-up paths now reserve their amount in the customer's monthly quota and release it if the faucet fails. If the quota is exhausted the top-up is skipped and account creation still succeeds. Refs #818 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ances Lets clients such as the portal show the cap and quota without duplicating the configured value. Refs #818 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ted now - Keep the quota reservation when the faucet call throws (outcome unknown, tokens may have been sent); release it only when the faucet reports a failure - Share one reserve/credit/settle function between POST /account/faucet and the bootstrap top-up - maxRequestable now honours the monthly quota; a request with no amount tops up by that amount instead of failing when the quota is nearly used - Add FAUCET_ADDRESS_CAP_CHEQ (defaults to FAUCET_MONTHLY_LIMIT_CHEQ) so the address cap and the monthly limit are separately named - Remove an unreachable invalid-config branch, de-duplicate the usage query and move the quota summary builder into the helper module Refs #818 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Unit tests for the quota helpers and credit orchestration (including keeping the reservation when the faucet throws), plus a Postgres-backed integration test for the ledger (skipped unless FAUCET_TEST_DATABASE_URL is set). Refs #818 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…when quota is used up POST /account/faucet no longer calls Stripe on every request: the active/trialing subscription status and plan are cached for FAUCET_SUBSCRIPTION_CACHE_SECONDS (default 60, 0 disables). The stored subscription status is still checked on every request, so cancellations apply immediately. When the monthly quota is already used up the endpoint answers 429 before querying the on-chain balance. LogTo role/scope lookups are left uncached because they are shared by every API-key endpoint and a stale cache would delay role removals. Refs #820 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
POST /account/faucet now rejects a request made within FAUCET_MIN_INTERVAL_SECONDS (default 10, 0 disables) of the customer's previous one with 429 and Retry-After. The check runs first as a cheap read, so a tight loop does not reach Stripe, the RPC node or the faucet, and again under the per-customer lock so simultaneous requests cannot slip through. Failed faucet attempts are released and so do not block a retry. The bootstrap top-up is not subject to it. Refs #821 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reservations are now pending, completed, unknown or abandoned. A pending reservation counts towards the quota only for FAUCET_PENDING_TIMEOUT_SECONDS (default 600), so a crash between reserving and calling the faucet no longer leaves quota used; stale ones are marked abandoned the next time the customer reserves. A reservation whose faucet call threw is marked unknown and always keeps counting, so tokens that may have been sent are never refunded. A late confirmation re-counts an abandoned reservation. Adds a migration for the status and completedAt columns. Refs #822 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… createdAt The entity now exposes amountNcheq as a bigint (via a column transformer) instead of a string, which removes the constructor guard, and createdAt is set by the database (DEFAULT now()) so all instances agree on when a request was made. Adds a migration for the column default; the ledger test checks the schema still matches the entity. Refs #827 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The on-chain balance is read before the reservation and does not include transfers still on their way, so simultaneous requests could each be allowed the full room under FAUCET_ADDRESS_CAP_CHEQ. The cap check now also runs under the per-customer lock, subtracting requests made for the same address in the last FAUCET_BALANCE_SETTLE_SECONDS (default 20) from the room, so only as many requests as fit are accepted. The bootstrap top-up is not subject to it. Refs #823 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
faucetRequest rows older than FAUCET_REQUEST_RETENTION_MONTHS (default 13 calendar months, 0 keeps everything) are deleted. Studio has no scheduler, so the cleanup runs in the background from normal reservations, at most once an hour per process, and never delays or fails the request that triggered it. The README documents a manual query as well. Refs #824 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every reservation, completion, release, unknown outcome and rejection (quota, minimum interval, address cap, plus the early checks before Stripe), the sweep of abandoned reservations and the retention cleanup now write one JSON log line with an event name and the customer, address, amount and relevant figures, so a blocked or refunded request can be traced. The README lists the events and a support query. The event tracker is not used because its operations feed usage and billing reports. Refs #825 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ee auth methods Session, API key and machine-to-machine credentials all end in the same scope check, so a unit test pins that the route requires request:faucet:testnet and is not open to unauthenticated callers. The Swagger description and README now state the supported credentials and that an M2M application needs the scope granted in LogTo. No behaviour change: all three methods remain allowed. Refs #826 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
21 tests call the real requestFaucetTokens and getBalances against a real Postgres with Stripe, the faucet, the account lookup and the on-chain balance faked. They cover session, API-key and machine-to-machine requests, the quota, minimum interval, address cap, failure handling, reservation status and the balances response. They use Node's test runner (the controller's dependencies do not load under jest) and a small loader hook for the balance fake, live outside tests/unit, and skip unless FAUCET_TEST_DATABASE_URL is set, so CI is unchanged. The README says how to run them. Refs #828 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- The faucet HTTP call had no timeout, so a hung faucet held the caller (and a quota reservation) for Node's default of several minutes. It now aborts after FAUCET_REQUEST_TIMEOUT_SECONDS (default 30), covering the response body too. POST /account/faucet maps the timeout to 504 with a message that the request may or may not have been processed; the quota stays used because tokens may have been sent - parseNumberEnv now warns, naming the variable and the default used, when a set value is invalid, so a typo in a limit (for example FAUCET_MONTHLY_LIMIT_CHEQ=1,000) is no longer silently replaced by the default Refs #818 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…est.status - Merges the faucet timeout and invalid-config warning from feat/faucet-api-key-cooldown, keeping this branch's settings alongside them - parseNonNegativeIntEnv now warns, naming the variable, when a set value is invalid, like parseNumberEnv - faucetRequest.status is constrained to the four known values with a CHECK constraint (added to the unreleased status migration, with a matching @check on the entity), so the database cannot hold a status the code does not know about Refs #829 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
createdAt is set by the database (DEFAULT now()), but month windows, the minimum interval, the pending timeout, the address-cap settle window and retention were compared against the application clock, so instances with different clocks could disagree about how old a request is. They now all use the database clock: reserve reads clock_timestamp() after taking the lock, and the read paths read it once per call. getQuotaStatus returns the usage together with the window, so the 429 Retry-After and the balances response use the same clock, and completedAt is set by the database. Tests fake the application clock hours ahead and fail against the previous behaviour. Refs #827 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Accept any lowercase bech32 cheqd address in POST /account/faucet, not only the customer's own payment accounts. Malformed addresses return 400. Quota, interval and address-cap limits still apply to the calling customer. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Drop the ENABLE_ACCOUNT_TOPUP bootstrap top-up from account creation and bootstrap, along with TESTNET_INITIAL_TOPUP_CHEQ, TESTNET_MINIMUM_BALANCE and the testnetMinimumBalance response field. Users now request testnet CHEQ themselves via POST /account/faucet. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tion" This reverts commit a2ff8e0.
Stop honouring TESTNET_FAUCET_UPPER_CAP_CHEQ as a fallback for TESTNET_INITIAL_TOPUP_CHEQ. It was the faucet cap (often 100,000), so a deployment that only set the old name got a 100,000 CHEQ initial top-up. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The initial top-up now runs only when the call created the customer's testnet account, not on every account or bootstrap call. Bootstrap no longer reports an error when the monthly quota is exhausted; it skips the top-up, as account creation already did. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Add handler tests for the one-off initial top-up on POST /account and in bootstrap: new vs existing testnet account, top-up to (not by) 10,000, quota exhaustion, faucet failure and ENABLE_ACCOUNT_TOPUP off. Export topupTestnet for them. Set the Dockerfile TESTNET_MINIMUM_BALANCE default to 10000 so it no longer overrides the code default. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e a 20-byte account Move address validation into faucetValidator so a malformed address is rejected with 400 before any subscription lookup, and into helpers/faucet.ts as a pure function with unit tests. Require a 20-byte account address so the API agrees with the portal's form validation. resolveFaucetAccount becomes resolveFaucetTarget and no longer validates. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
## What changed Merges `main` into `develop` to bring in the v3.17.0 release commits (CHANGELOG.md and package.json version only; no code changes). ## Why Keeps `develop` ahead of `main` so the stacked faucet PRs and the next release PR do not conflict. ## How to test Diff against `develop` is limited to `CHANGELOG.md` and `package.json`. ## Risks None. **Use a merge commit, not squash**, so `main` stays an ancestor of `develop`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…#819) ## What changed - `POST /account/faucet` now works with an API key (the `x-api-key` / `customer-id` rejection is removed). The existing `request:faucet:testnet` scope check applies. - New per-customer **monthly faucet quota**: `FAUCET_MONTHLY_LIMIT_CHEQ` (default 100,000 CHEQ, calendar month UTC), shared across all of a customer's addresses. Exceeding it returns `429` with `Retry-After` and quota details. - New **address cap**: `FAUCET_ADDRESS_CAP_CHEQ` is the most a single testnet address can hold before the faucet stops topping it up. It defaults to `FAUCET_MONTHLY_LIMIT_CHEQ`, so out of the box there is one 100,000 CHEQ figure (previously the per-address cap was 10,000). - A request with no `amount` tops the address up by the most that can be requested now: the lower of the room under the address cap and the monthly quota left. `balance.maxRequestable` in the response reports the same figure. - The initial top-up at account bootstrap is debited from the quota. If the quota is exhausted the top-up is skipped and account creation still succeeds. - `TESTNET_FAUCET_UPPER_CAP_CHEQ` renamed to `TESTNET_INITIAL_TOPUP_CHEQ` (it now only drives the bootstrap top-up). The old name is still read as a fallback. - `GET /account/balances` returns a new `faucet` block (cap + quota used/remaining/reset) so clients such as the portal do not need to duplicate the cap. - New `faucetRequest` table and migration; Swagger, README and `example.env` updated. ## Why Closes #818. Studio API-key users could not use the faucet, and the faucet backend (`cheqd/cosmjs`) exempts Studio requests from its own cooldown and email verification, so Studio has to be the layer that limits abuse. Backend hardening is tracked separately in cheqd/cosmjs#12. ## How quota is enforced Quota is reserved in a transaction under a per-customer Postgres advisory lock before the faucet call, so concurrent requests cannot overshoot the limit. The reservation is settled by one shared function (`creditWithinQuota`) used by both the endpoint and the bootstrap top-up: - faucet answers with an error status: it did not credit the account, so the reservation is **released**; - faucet call **throws** (for example a timeout): the outcome is unknown and tokens may have been sent, so the reservation is **kept**. Over-counting is safer than refunding quota for tokens that were credited. ## Edge cases - `cap_reached` responses and faucet failures do not use quota. - Quota exhausted during bootstrap: top-up skipped, no error. - An explicit `amount` above the room under the address cap returns `400`; one that does not fit the quota returns `429`. - The balance read is not serialised with the quota lock. With the cap and quota both at 100,000 this only matters if an address already holds at least 50,000 and two requests run at once; the monthly quota still bounds the total. - A crash between reserving and calling the faucet leaves that amount counted for the month (see follow-up issues). ## How to test 1. `npm run test:unit` (new pure tests in `tests/unit/account/faucet-quota.test.ts`, including the release/keep rules for `creditWithinQuota`). 2. Ledger integration test against Postgres (skipped unless `FAUCET_TEST_DATABASE_URL` is set), covering reserve, release, month boundary, customer isolation, the concurrency guarantee and migration/entity drift: `docker run -d --rm --name faucet-test-db -e POSTGRES_PASSWORD=pw -e POSTGRES_DB=studio -p 55432:5432 postgres:16` `FAUCET_TEST_DATABASE_URL=postgres://postgres:pw@localhost:55432/studio npm run test:unit -- faucet-request.service` 3. I also ran the real `requestFaucetTokens` and `getBalances` handlers against Postgres with Stripe, the faucet and the on-chain balance mocked (20 scenarios, all passing): API-key access, 429 with `Retry-After`, release on faucet failure, quota kept when the faucet throws, address cap and `maxRequestable`, no-amount clamping, 6 parallel requests funding exactly 3, and invalid input. That harness is not committed. 4. Manually: `POST /account/faucet` with an API key; repeat until `429`; check `quota` in the response and in `GET /account/balances`. Not tested end to end: Stripe, the real faucet and LogTo, and the bootstrap top-up path (it reuses the tested ledger and orchestration). ## Release note - The default per-address faucet cap rises from 10,000 to 100,000 CHEQ (`FAUCET_ADDRESS_CAP_CHEQ`), bounded by the 100,000 CHEQ monthly quota per customer. Set `FAUCET_ADDRESS_CAP_CHEQ` to keep a lower per-address cap. - `TESTNET_FAUCET_UPPER_CAP_CHEQ` is renamed `TESTNET_INITIAL_TOPUP_CHEQ`; the old name keeps working as a fallback. ## Risks and rollout - The migration only creates a new table, so deploy order with the app is safe (the migrations workflow runs on push to `develop`). - Deploy this before the portal change (cheqd/studio-portal#258), which reads the new `faucet` field. - The full controller graph only loads on Node 20 (Node 22+ fails on Veramo's JSON import syntax), which is what CI uses. ## Architecture and best-practice review fixes - **Faucet call timeout.** The faucet HTTP call had no timeout, so a hung faucet held the caller (and a quota reservation) for Node's default of several minutes. It now aborts after `FAUCET_REQUEST_TIMEOUT_SECONDS` (default 30), covering the response body too. `POST /account/faucet` maps a timeout to `504` with a message that the request may or may not have been processed; the quota stays used because tokens may have been sent. Unit tests stub `fetch`; verified through the real handler. - **Config typos are no longer silent.** `parseNumberEnv` now warns, naming the variable and the default used, when a set value is invalid, so for example `FAUCET_MONTHLY_LIMIT_CHEQ=1,000` is no longer silently replaced by 100,000. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## [3.18.0-develop.1](3.17.0...3.18.0-develop.1) (2026-10-13) ### Features * add monthly faucet quota service and helpers ([7eef4f7](7eef4f7)), closes [#818](#818) * allow API keys on POST /account/faucet with monthly quota ([6ed02c8](6ed02c8)), closes [#818](#818) * allow API keys on the faucet endpoint with a monthly CHEQ quota ([#819](#819)) ([17dcb04](17dcb04)), closes [cheqd/studio-portal#258](https://github.com/cheqd/studio-portal/issues/258) * apply monthly quota as the per-request limit for explicit faucet amounts ([6979f42](6979f42)), closes [#818](#818) * **db:** add faucetRequest table for monthly faucet quota ([ef43cb1](ef43cb1)), closes [#818](#818) * debit the initial testnet top-up from the monthly faucet quota ([15c8d8c](15c8d8c)), closes [#818](#818) * return faucet cap and monthly quota usage from GET /account/balances ([3ba590d](3ba590d)), closes [#818](#818) * use a single 100k CHEQ cap for faucet balance and monthly quota ([4612b1f](4612b1f)), closes [#818](#818) ### Bug Fixes * time out the faucet call and warn on invalid faucet config ([9b14df1](9b14df1)), closes [#818](#818)
#819) (#829) Stacked on #819 (base is `feat/faucet-api-key-cooldown`); retarget to `develop` once #819 merges. Closes #820, closes #821, closes #822. ## What changed - **#820 Cost per request.** `POST /account/faucet` no longer calls Stripe on every request: the active/trialing subscription status and plan are cached for `FAUCET_SUBSCRIPTION_CACHE_SECONDS` (default 60, `0` disables). The stored subscription status is still checked in the database on every request, so a cancellation takes effect immediately; only a plan change can lag by up to the TTL. When the monthly quota is already used up, the endpoint answers `429` before querying the on-chain balance. - **#821 Burst limiting.** A request made within `FAUCET_MIN_INTERVAL_SECONDS` (default 10, `0` disables) of the customer's previous one gets `429` with `Retry-After`. It is checked first as a cheap read, so a tight loop never reaches Stripe, the RPC node or the faucet, and again under the per-customer lock so simultaneous requests cannot slip through. A failed faucet attempt is released and does not block a retry. The bootstrap top-up is not subject to it. - **#822 Reservation status.** Reservations are now `pending`, `completed`, `unknown` or `abandoned`. A pending reservation counts towards the quota for `FAUCET_PENDING_TIMEOUT_SECONDS` (default 600) and then stops counting, so a crash between reserving and calling the faucet no longer leaves quota used (stale ones are marked `abandoned` the next time the customer reserves). A reservation whose faucet call **threw** is marked `unknown` and always keeps counting, so tokens that may have been sent are never refunded. A late confirmation re-counts an abandoned reservation. New migration `1791500000001` adds `status` and `completedAt` (existing rows become `completed`). ## Decisions and non-goals - LogTo role/scope lookups are **not** cached: they are shared by every API-key endpoint, and a stale cache would delay role removals. Stripe is the larger and faucet-specific cost. - Pending reservations expire rather than being reconciled against the faucet: the faucet has no lookup that links a Studio request to a transfer. The remaining gap is a crash *after* the faucet credited but *before* the row was marked completed, which would refund that amount after the timeout. That window is a single DB write. - No scheduler was added; the sweep runs inside the reservation, under the lock. ## How to test 1. `npm run test:unit` (new tests for the interval helper, `too_frequent`, and the complete / unknown / release rules). 2. Ledger integration test against Postgres (skipped unless `FAUCET_TEST_DATABASE_URL` is set; Node 20): now also covers the minimum interval, simultaneous requests, and every status transition including expiry and late confirmation. See the header of `tests/unit/account/faucet-request.service.test.ts` for the docker command. 3. I also ran the real handlers against Postgres with Stripe, the faucet and the balance read faked: the 20 earlier scenarios (regression), plus: three requests make one Stripe call; an exhausted quota returns `429` with no balance read and no faucet call; an immediate second request returns `429` with no Stripe, RPC or faucet call; five simultaneous requests fund exactly one; a failed attempt does not block a retry; and the status transitions above through the handler. That harness is not committed (see #828). ## Risks and rollout - New default limit: customers calling the faucet more than once every 10 seconds now get `429`. Set `FAUCET_MIN_INTERVAL_SECONDS=0` to disable. - The migration is additive (two columns on the new table), so deploy order is safe. - `FAUCET_PENDING_TIMEOUT_SECONDS` should stay longer than the longest faucet call (Node's default fetch timeout is about 5 minutes). ## Architecture and best-practice review fixes - Merges the faucet timeout and config-warning fixes from #819, and extends the warning to `parseNonNegativeIntEnv`, which backs this PR's settings. - **`faucetRequest.status` is constrained** to the four known values with a `CHECK` constraint (added to the still-unreleased status migration, with a matching `@Check` on the entity), so the database cannot hold a status the code does not know about. The ledger test checks a bad value is rejected and that the schema still matches the entity. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## [3.18.0-develop.2](3.18.0-develop.1...3.18.0-develop.2) (2026-10-13) ### Features * add a minimum interval between faucet requests per customer ([2d3ebf5](2d3ebf5)), closes [#821](#821) * faucet cost, burst limiting and reservation status (follow-ups to [#819](#819)) ([#829](#829)) ([e7d7b67](e7d7b67)), closes [#820](#820) [#821](#821) [#820](#820) [#821](#821) [#822](#822) [#828](#828) * track whether each faucet quota reservation completed ([a371bec](a371bec)), closes [#822](#822) ### Performance Improvements * cache the Stripe subscription lookup and skip the balance read when quota is used up ([dd408e2](dd408e2)), closes [#820](#820)
…idy-ups (follow-ups to #829) (#830) Stacked on #829, which is stacked on #819. The base is `feat/faucet-cost-and-reservations`; retarget as the earlier PRs merge. Closes #823, closes #824, closes #825, closes #826, closes #827, closes #828. ## What changed - **#823 Address cap under the lock.** The on-chain balance is read before the reservation and does not include transfers still on their way, so simultaneous requests could each be allowed the full room under `FAUCET_ADDRESS_CAP_CHEQ`. The cap check now also runs under the per-customer lock, subtracting requests made for the same address in the last `FAUCET_BALANCE_SETTLE_SECONDS` (default 20) from the room, so only as many requests as fit are accepted. The bootstrap top-up is not subject to it. Trade-off: for those 20 seconds a request that already reached the balance is counted twice, which can briefly refuse a legitimate follow-up only when the address is near its cap. - **#824 Retention.** `faucetRequest` rows older than `FAUCET_REQUEST_RETENTION_MONTHS` (default 13 calendar months, `0` keeps everything) are deleted. Studio has no scheduler, so the cleanup runs in the background from normal reservations, at most once an hour per process, and never delays or fails the request that triggered it. A manual query is in the README. - **#825 Logging.** Every reservation, completion, release, unknown outcome and rejection (quota, minimum interval, address cap, plus the early checks before Stripe), the sweep of abandoned reservations and the retention cleanup write one JSON log line with an `event` name and the customer, address, amount and relevant figures. The README lists the events and a support query. The event tracker is deliberately not used: its operations feed usage and billing reports. - **#826 Auth methods.** Session, API key and machine-to-machine credentials all remain allowed (as decided). A unit test pins that they all end in the same `request:faucet:testnet` scope check and that the route is not open to unauthenticated callers; the Swagger description and README state the supported credentials, including that an M2M application needs the scope granted in LogTo. No behaviour change. - **#827 Entity tidy-ups.** `amountNcheq` is a `bigint` in the entity (via a column transformer), and `createdAt` is set by the database (`DEFAULT now()`) so all instances agree on when a request was made. New migration `1791500000002`; the ledger test checks the schema still matches the entity. - **#828 Handler tests.** 21 handler-level tests call the real `requestFaucetTokens` and `getBalances` against a real Postgres with Stripe, the faucet, the account lookup and the on-chain balance faked. They cover session, API-key and M2M requests, the quota, minimum interval, address cap, failure handling, reservation status and the balances response. As agreed, **CI is unchanged**: they skip unless `FAUCET_TEST_DATABASE_URL` is set. They use Node's test runner (a dependency of the controller does not import under jest) and a small loader hook for the balance fake, and live in `tests/integration`, outside jest's `tests/unit` pattern. ## How to test 1. `npm run test:unit` (new pure tests: retention cutoff, address room, logging events, auth rule). 2. With a disposable Postgres, the ledger and handler suites; the README has the exact commands. Ledger suite (jest): reserve/release, interval, statuses, address cap including simultaneous requests, retention, entity round trip and the schema-drift check. Handler suite (Node 20): the 21 end-to-end scenarios. 3. I ran both against Postgres 16 on Node 20: 212 of 212 pass, including the 21 handler tests; local Node 26 runs 186 and skips the 26 database-backed ones. I also confirmed end to end that the retention cleanup is triggered by a normal request, deletes only rows past retention and runs at most once an hour, and that four simultaneous requests for an address with room for one are now refused three times. ## Risks and rollout - Migration `1791500000002` is additive (a column default) and safe in any deploy order. - `FAUCET_BALANCE_SETTLE_SECONDS` can over-refuse for a few seconds near the cap; set it to `0` to turn the in-flight accounting off. - The first request after a deploy per process may delete old rows in the background; with the default 13 months and a table that only just appeared, nothing is deleted for over a year. ## Architecture and best-practice review fixes - Merges the fixes from #819 and #829. - **One clock for every decision.** `createdAt` is set by the database, but month windows, the minimum interval, the pending timeout, the settle window and retention were compared against the application clock, so instances with different clocks could disagree about how old a request is. They now all use the database clock (`clock_timestamp()`, read after the lock is taken), the 429 `Retry-After` and balances response use the same window, and `completedAt` is set by the database. Tests fake the application clock hours ahead; I confirmed they fail against the previous behaviour (a fresh pending reservation was treated as expired, the month was wrong, and `completedAt` was 3 hours off). Merged state verified on Node 20 against Postgres 16: 234 of 234 tests pass, including the 21 handler tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…is documented The POST /account/faucet doc comment had unquoted colons followed by spaces in its description, which is invalid YAML. swagger-jsdoc logged the error and dropped the whole /account/faucet path from the generated spec. Reword the two sentences and regenerate swagger-api.json. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
## [3.18.0-develop.3](3.18.0-develop.2...3.18.0-develop.3) (2026-10-13) ### Features * delete faucet request history past a retention period ([038a079](038a079)), closes [#824](#824) * faucet address-cap race, retention, logging, tests and entity tidy-ups (follow-ups to [#829](#829)) ([#830](#830)) ([766b1cd](766b1cd)), closes [#823](#823) [#824](#824) [#825](#825) [#826](#826) [#827](#827) [#823](#823) [#824](#824) [#825](#825) [#826](#826) [#827](#827) [#828](#828) [#819](#819) * log faucet quota decisions as structured events ([b21176b](b21176b)), closes [#825](#825) ### Bug Fixes * enforce the faucet address cap under the quota lock ([eed180b](eed180b)), closes [#823](#823) * use the database clock for every faucet quota decision ([cb6c856](cb6c856)), closes [#827](#827)
…e (follow-up to #830) (#839) ## What changed Stacked on #830 (base is `feat/faucet-hardening-low`, so the diff is only this change; merge after #819, #829 and #830). - **Any address:** `POST /account/faucet` now accepts any valid lowercase `cheqd` bech32 address, owned or not. Previously an address the customer did not own returned `403`. The address is validated in `faucetValidator` (before any subscription lookup): it must be a lowercase `cheqd` bech32 account address (20 bytes, valid checksum), otherwise `400`. With no address it still funds the customer's own testnet account. - **Initial top-up, once:** with `ENABLE_ACCOUNT_TOPUP=true`, the top-up to `TESTNET_INITIAL_TOPUP_CHEQ` now runs only when the call creates the customer's testnet account (`POST /account` and bootstrap), not on every call. - **Default stays 10,000 CHEQ:** `TESTNET_FAUCET_UPPER_CAP_CHEQ` is no longer a fallback for `TESTNET_INITIAL_TOPUP_CHEQ`. It was the faucet cap (often 100,000), so a deployment that only set the old name got a 100,000 top-up. - **Bootstrap quota:** bootstrap now skips the top-up when the monthly quota is exhausted (logs `faucet.bootstrap_topup_skipped`) instead of returning `500`, matching `POST /account`. ## Why Users should choose how much to top up and where, within a 100,000 CHEQ monthly quota, in one or several requests. ## Behaviour unchanged - Monthly quota (`FAUCET_MONTHLY_LIMIT_CHEQ`, default 100,000) is per customer across all addresses; the 10k initial top-up counts against it. - Per-address cap (`FAUCET_ADDRESS_CAP_CHEQ`, default 100,000) is checked against the address's on-chain balance, so a funded address can be topped up only to the cap. - Minimum interval, reservations and plan/scope checks are as in #830. ## Edge cases - A mainnet address has the same `cheqd1…` prefix, so it is accepted; it receives testnet tokens only. - If the one-off initial top-up fails, nothing retries it; the user uses `POST /account/faucet`. - For existing accounts, the bootstrap `testnetMinimumBalance` flag stays `false` because no check runs. ## How to test - Integration (Node 20, disposable Postgres): `FAUCET_TEST_DATABASE_URL=... npx -y -p node@20 node --import tsx --import ./tests/integration/hooks/register.mjs --test tests/integration/faucet-handler.test.ts` — 35/35 pass, including cases for a non-owned address, malformed/wrong-prefix/uppercase addresses, and the initial top-up (new vs existing account, top up to 10,000, quota exhausted, faucet failure, `ENABLE_ACCOUNT_TOPUP` off) on both `POST /account` and bootstrap (`topupTestnet` is exported for these tests). Removing the new-account gating or the quota skip makes 3 of them fail. - Unit: `npx jest tests/unit/account` — 67 pass, 30 skipped (includes `isValidFaucetAddress`). - `tsc --noEmit` and Prettier clean. ## Risks - Anyone with a faucet-enabled plan can fund any address, up to their own monthly quota. Nothing caps total CHEQ received by one address across customers beyond the 100,000 balance cap. A cross-customer per-address limit could be added. - `docker/Dockerfile` `TESTNET_MINIMUM_BALANCE` default is corrected from 1000 to 10000 so it no longer overrides the code default. An environment that sets 100000 explicitly still wins. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## [3.18.0-develop.4](3.18.0-develop.3...3.18.0-develop.4) (2026-10-13) ### Features * **faucet:** allow funding any valid cheqd testnet address ([7a95c5d](7a95c5d)) * **faucet:** fund any cheqd address; top up new testnet accounts once (follow-up to [#830](#830)) ([#839](#839)) ([1558ccf](1558ccf)), closes [#819](#819) [#829](#829) ### Bug Fixes * **faucet:** keep the initial top-up at 10,000 CHEQ by default ([dc2c0c6](dc2c0c6)) * **faucet:** top up only newly created testnet accounts; skip on quota ([56d3f25](56d3f25)) * **swagger:** make the faucet description valid YAML so the endpoint is documented ([7f2f6ac](7f2f6ac))
POST /account/faucet without an amount now sends FAUCET_DEFAULT_REQUEST_CHEQ (default 10,000), lowered to the room under the address cap or the quota left, instead of taking the whole monthly quota. An explicit amount is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Closes #841. ## What changed `POST /account/faucet` without `amount` now sends `FAUCET_DEFAULT_REQUEST_CHEQ` (default 10,000 CHEQ) instead of the most that can be requested now. The amount is lowered to the room under the address cap or the quota left if either is smaller. An explicit `amount` behaves exactly as before, including the address cap and `429` responses. With defaults, one call that omits `amount` leaves 90% of the monthly quota unused (previously it took all of it). ## Details - New config `FAUCET_DEFAULT_REQUEST_CHEQ` (invalid values fall back loudly, like the other faucet settings). - New pure helper `defaultRequestNcheq` in `src/helpers/faucet-quota.ts`. - OpenAPI text (controller and tracked `swagger-api.json`) and README "Faucet settings" updated. - The account-bootstrap top-up is a separate path and is unchanged. ## Tests - Unit: 4 new cases for `defaultRequestNcheq` (default below cap and quota; above quota left; above room under cap; nothing to send). - Integration (Postgres, Node 20): 2 new cases (fresh request sends 10,000 and leaves 90,000; nearly full address gets only the room left). 37/37 pass. - The existing "tops up to the cap" test now passes an explicit `amount: 40000`, as it relied on the old omit-amount behaviour. - `tsc --noEmit` and Prettier clean. ## Risks Callers relying on the old omit-amount behaviour to top up to the cap now get 10,000 per call; they can pass `amount` to ask for more. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fraseragain
marked this pull request as ready for review
October 14, 2026 15:53
fraseragain
pushed a commit
that referenced
this pull request
Oct 14, 2026
The merge of #845 did not start any workflows on main, so no release, image or production migrations ran. The cause was wording in the PR description that GitHub reads as an instruction not to run workflows. This commit is empty. It only gives main a new push so the release workflows run. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
fraseragain
added a commit
that referenced
this pull request
Oct 14, 2026
Empty commit that gives `main` a new push, so the release workflows run for v3.18.0. ## Why #845 was merged into `main` (merge commit `6020d4a`), but no workflows started. GitHub builds the merge commit message from the PR description, and the description contained wording that GitHub reads as an instruction not to run workflows on push. As a result there was no Semantic Release run, no production image and no production migrations. Production is unchanged (still v3.17.0). ## What this does - No file changes. The only change is one empty commit. - Merging it triggers the normal pipeline on `main`: Semantic Release (expected version 3.18.0, because the `feat` commits from #845 are already in `main`'s history since the last tag), the production Docker image, and the production database migrations (three new ones, all additive to the new `faucetRequest` table). ## Before merging - Production env vars are set (`TESTNET_INITIAL_TOPUP_CHEQ`, `FAUCET_ADDRESS_CAP_CHEQ`) and the production `database-migrations` job has been removed from the app spec. - Keep the merge commit message plain: it must not contain any wording that tells GitHub to bypass workflows. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
🎉 This PR is included in version 3.18.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DRAFT. Do not merge until the checklist below is done. Release of
developtomain: expected version 3.18.0 (minor, from thefeatcommits).What's included
Faucet quota stack plus two follow-ups (44 commits on
developsince v3.17.0):POST /account/faucetworks with an API key (request:faucet:testnetscope). Per-customer monthly quotaFAUCET_MONTHLY_LIMIT_CHEQ(default 100,000 CHEQ, UTC calendar month) and a per-address capFAUCET_ADDRESS_CAP_CHEQ(defaults to the monthly limit).GET /account/balancesnow returns afaucetblock (cap, quota used, reset date) that the portal reads.FAUCET_MIN_INTERVAL_SECONDS(10s) between requests, reservation status.cheqdaddress, not only owned ones. The initial top-up runs only when a testnet account is created.amount,FAUCET_DEFAULT_REQUEST_CHEQ(default 10,000) is sent, limited by the cap and quota left./account/faucetfrom the generated docs. Fixed in feat(faucet): fund any cheqd address; top up new testnet accounts once (follow-up to #830) #839; the generated spec has all 59 paths.Deploy notes
faucetRequesttable with its index and FK, thenstatus/completedAtcolumns and acreatedAtdefault on that same table; nothing touches existing tables):1791500000000,…001,…002. The production "Database migrations" workflow runsnpm run migrateon push tomainand applies them. Old code ignores them, so a rollback of the image is safe.TESTNET_FAUCET_UPPER_CAP_CHEQis no longer read. The initial top-up now comes fromTESTNET_INITIAL_TOPUP_CHEQ(default 10,000) and the per-address cap fromFAUCET_ADDRESS_CAP_CHEQ(default 100,000; it was 10,000).TESTNET_MINIMUM_BALANCEdefault in the Dockerfile changes from 1000 to 10000 (applies only if production does not set it).TESTNET_INITIAL_TOPUP_CHEQ=10000,FAUCET_ADDRESS_CAP_CHEQ=100000. RemoveTESTNET_FAUCET_UPPER_CAP_CHEQ.FAUCET_*settings have defaults (seeexample.env).Before merging (checklist)
database-migrationsjob (staging's hung on every deploy and was removed; see below)mainrequires 1)How to merge
chore(release)commit and release a patch (3.17.1) instead of 3.18.0, andmainwould diverge fromdevelopagain.productionenvironment.faucetblock from Studio.mainintodevelopback-merge PR (merge commit), because Semantic Release commitschore(release): X.Y.Z [skip ci]straight tomain.Verification done on staging (
developat e748f7c)develop(59 paths).🤖 Generated with Claude Code