Skip to content

fix: resolve system settlement authorization, enum migration transactions and unbounded cache growth - #656

Merged
TS-mfon merged 9 commits into
StellarState:devfrom
dotmantissa:fix/issues-628-632-633
Oct 18, 2026
Merged

TS-mfon merged 9 commits into
StellarState:devfrom
dotmantissa:fix/issues-628-632-633

Conversation

@dotmantissa

Copy link
Copy Markdown
Contributor

Description

This PR fixes three defects in the settlement pipeline, the migrations that change Postgres enums, and the in-process TTL cache. It also repairs the compile and schema drift failures that already existed on dev, because the required workflows could not pass without them.

Closes #628
Closes #632
Closes #633

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • 🔧 Configuration change

Key Changes

#628 — automated settlement rejected every matured invoice

SettlementWorker passed invoice.sellerWallet || "system" as the actor wallet. The service compares that value against the seller's Stellar address, and "system" can never equal an address, so every invoice without a recorded seller wallet failed with 403 FORBIDDEN.

  • src/services/settlement.service.ts: exports SYSTEM_SETTLEMENT_ACTOR and isSystemSettlementActor, accepts a systemTrigger, and skips the seller wallet equality check for system actors. The explicit sellerId check still runs, so a caller cannot settle another seller's invoice by claiming to be the system.
  • src/workers/settlement.worker.ts: settles as the system identity with the system_matured trigger instead of impersonating the seller.
  • system_matured has no lifecycle reason of its own, so it is recorded as the existing maturity_settled reason for the status history.

#632 — enum migration failed with SQLSTATE 25001

ALTER TYPE ... ADD VALUE cannot run inside a transaction block, and TypeORM wrapped migrations in one.

  • Every migration that adds an enum member now sets transaction = false.
  • src/config/database.ts runs migrations in each mode. This is required: TypeORM throws ForbiddenTransactionModeOverrideError when a migration overrides the transaction mode under the default all mode, so the flag alone was not enough.
  • Statements stay guarded with IF NOT EXISTS, so a partially applied migration can be retried.

#633 — unbounded in-process cache

TtlCache only dropped an entry when that exact key was read again after expiring, so keys written once and never read stayed resident for the process lifetime.

  • Capacity is capped at 5000 entries by default (maxEntries), with least-recently-used eviction and recency refreshed on reads.
  • Expired entries are pruned on every write and by a background sweeper that is unref'd so it never holds the process open. sweepIntervalMs is configurable and close() stops the timer.
  • size and pruneExpired() are exposed, and invalidatePrefix() removes a namespace slice in-process.

CI repair (pre-existing on dev)

dev did not type check or build, and npm ci refused to install:

  • auto-invest called createInvestment without the required investorWallet.
  • document-export called an IPFS fetchFile helper that did not exist; it is now implemented against the gateway.
  • invoice-refund referenced an underscore-prefixed parameter by its unprefixed name.
  • issuer-analytics used a cache API that no longer exists.
  • package-lock.json was regenerated because it did not match package.json (missing archiver).
  • docs/openapi.json now documents the investor, portfolio, marketplace, seller, watchlist and readiness routes, and the verifier accepts the new alias mounts the same way it already accepted the auth and notification aliases.

Checklist

  • All GitHub Actions workflows are green on this PR (required for merge)
  • Commit messages follow Conventional Commits (feat:, fix:, chore:, etc.) — enforced by CI
  • No secrets, API keys, .env, or credentials committed (see CONTRIBUTING.md)
  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Testing

How to Test

  1. npm run lint && npm run type-check && npm run build
  2. npm run test:ci (1778 tests, 1768 passed, 10 skipped)
  3. npm run verify:openapi
  4. Automated settlement: SettlementWorker.processMaturedInvoices() now calls settleInvoice with actorWallet: "system" and systemTrigger: "system_matured"; tests/unit/workers/settlement.worker.test.ts asserts the call shape.
  5. Migration: tests/migrations/enum-migrations-transaction.test.ts asserts each enum migration opts out of the transaction and that the data source runs in each mode.

Test Coverage

  • Unit tests added/updated
  • Integration tests added/updated
  • E2E tests added/updated (if applicable)
  • Manual testing completed

The SQL migration script (scripts/test-migrations.sh) could not be run in this environment because no PostgreSQL server was available; the enum migration is covered by unit tests instead.

Additional Notes

Each issue was implemented on its own branch (fix/issues-628, fix/issues-632, fix/issues-633) and merged here. The CI repair lives on fix/ci-baseline and is included because the required checks were already failing on dev.

For Reviewers

  • Code quality and readability
  • Test coverage
  • Security implications
  • Performance impact
  • Breaking changes

Please pay particular attention to the system actor bypass in settleInvoice: the seller id check must remain the authorization boundary, and only non-wallet actors should skip the wallet equality check.

Several services did not compile on dev, so the type check and build jobs
could not pass:

- auto-invest passed an investment without the investor wallet that
  createInvestment requires
- document-export called an IPFS fetch helper that did not exist
- invoice-refund referred to an underscore prefixed parameter by its
  unprefixed name
- issuer-analytics used a cache API that no longer existed

The lock file is regenerated as well, since it did not match package.json and
npm ci refused to run.
The settlement worker passed the seller wallet as the acting wallet, falling
back to the literal "system" when no wallet was recorded. The seller wallet
guard then rejected every such settlement with 403 FORBIDDEN because a Stellar
address can never equal "system", so automated settlement cycles failed.

Automated settlement now identifies itself as a system actor and marks the
request as system triggered. The seller wallet equality check is skipped for
system actors while the explicit seller id check still applies, and a wallet
actor that is not the seller is still rejected. The system matured trigger is
recorded as the existing maturity settled lifecycle reason.
ALTER TYPE ... ADD VALUE cannot execute inside a transaction block in
Postgres, and TypeORM wrapped migrations in one, so adding the rejected status
failed with SQLSTATE 25001.

Migrations now run in per-migration transaction mode, and every migration
that adds an enum member opts out of the managed transaction. The same failure
affected the notification type, failed status and admin role migrations, so
they are marked non-transactional too. Each statement stays guarded so a
partially applied migration can be retried.
The in-process map only dropped an entry when that exact key was read again
after it expired, so keys written once and never read stayed resident for the
lifetime of the process. Under real traffic that is an unbounded leak that
ends in a heap exhaustion crash.

The cache now caps itself at five thousand entries by default and evicts the
least recently used one when full, refreshing recency on reads. Expired
entries are pruned whenever a new value is written and by a background sweeper
that does not hold the process open. The sweep interval and capacity are both
configurable, and the cache can be closed explicitly.
… routes

The schema drift check failed because several mounted routers and the
readiness probe had no specification. The routes are documented now, and the
unversioned alias mounts follow the same allowance as the auth and notification
aliases.
@drips-wave

drips-wave Bot commented Oct 18, 2026

Copy link
Copy Markdown

@dotmantissa Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@TS-mfon
TS-mfon merged commit c1ebce4 into StellarState:dev Oct 18, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment