Skip to content

chore: adopt a formatter — fix mangled whitespace, add .editorconfig and SDK lint - #2

Open
timo126 wants to merge 116 commits into
mainfrom
chore-98-adopt-formatter
Open

chore: adopt a formatter — fix mangled whitespace, add .editorconfig and SDK lint#2
timo126 wants to merge 116 commits into
mainfrom
chore-98-adopt-formatter

Conversation

@timo126

@timo126 timo126 commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Summary

Closes accensa#98

This PR adopts Prettier as the project-wide formatter, repairs all mangled string literals left behind by bad merges, extends ESLint coverage to packages/sdk, and adds .editorconfig + .gitattributes so editors and git agree on whitespace before code is ever committed.

What changed

Commit 1: fix: resolve merge conflict, repair mangled strings, add .editorconfig and SDK lint

Merge conflict resolution

  • apps/web/src/app/api/sync/route.ts had unresolved conflict markers. Both imports (isAuthorizedCronRequest and createHmac) are used — kept both.

Mangled string literals repaired (16 instances)
A grep for [a-zA-Z]"[a-zA-Z] across apps/web/src confirmed 16 locations where quotes were jammed against adjacent words. These are inside comments, JSDoc, and template-literal SQL — syntactically valid, so no linter or formatter will ever flag them. All 16 were fixed by hand.

Examples:

  • db.ts:80: COLUMN"timestamp"TOCOLUMN "timestamp" TO
  • sync/route.ts:362: dashboard's"Sync now"buttondashboard's "Sync now" button
  • stellar-events.ts:13: e.g."native"or"USDC:GA..."e.g. "native" or "USDC:GA..."

Post-fix grep confirms zero remaining mangled strings.

.editorconfig added — matches the Prettier config (2-space indent, LF, UTF-8).

.gitattributes added — enforces eol=lf repo-wide so Windows contributors never commit CRLF.

ESLint extended to packages/sdk — flat config with typescript-eslint recommended rules + lint script added to package.json.

apps/demo-merchant ESLint gap (justified) — plain JS demo app with no build step; adding ESLint there would require a separate config for a handful of rarely-changing files.

CONTRIBUTING.md updated — now mentions .editorconfig, ESLint coverage, and the full pre-commit workflow.

.gitignore updated — prevents local tooling artifacts (node_dist/, *.zip, etc.) from being committed.


Commit 2: style: apply prettier formatting across the entire codebase

A single prettier --write . run that touches only formatting — no logic, no renames, no API changes. Listed in .git-blame-ignore-revs.


Commit 3: chore: add format commit to .git-blame-ignore-revs

Records the format commit in .git-blame-ignore-revs alongside the two earlier format commits.

Acceptance criteria checklist

  • Prettier configured, config matches existing clean files (packages/sdk/index.ts)
  • Whole-repo reformat is a single isolated commit (054f864)
  • Format commit listed in .git-blame-ignore-revs
  • Mangled string literals repaired by hand; grep confirms none remain
  • format:check runs in CI over every workspace (already in ci.yml)
  • ESLint coverage extended to packages/sdk; apps/demo-merchant gap justified
  • .editorconfig committed; CONTRIBUTING.md mentions the formatter

stephanieoghenemega-eng and others added 30 commits August 25, 2026 21:32
Adds scripts/reconcile-payments, an independent package that rebuilds the
ledger-derived columns of `payments` (tx_hash, ledger, payer, amount, asset,
ts) directly from Stellar chain data, using its own SAC transfer decoder and
RPC client rather than importing apps/web's indexer code, and diffs the
result against production row by row (not just a count).

- Rebuild-only mode needs only a public RPC endpoint and the merchant's
  public account address, so a third party can run it with no access to our
  infrastructure - see README.md.
- Diff mode additionally connects read-only to DATABASE_URL and reports
  mismatched/missing rows individually.
- Merchant-reported columns (route, method, request_id, hook_reported_at)
  are explicitly excluded per column via trust-boundary.mjs, never
  reconstructed or compared.
- .github/workflows/reconcile.yml runs it on a daily schedule against
  production and fails the job on any discrepancy.
- Unit tests (node --test) run in ci.yml on every push/PR using only
  synthetic and fixture data (no live RPC/DB); decode.test.mjs cross-checks
  this decoder against the same real captured fixture apps/web's own
  stellar-events.test.ts uses, and confirms they agree.
Adds apps/web/openapi.yaml documenting the indexer API and generates
packages/sdk/generated/api-types.ts from it via openapi-typescript, so
SettleHookPayload is derived from the spec instead of hand-declared and
can no longer silently drift from what /api/hook/settle actually accepts.
CI regenerates the file and fails the build on a mismatch, mirroring the
existing gen:vectors check.
…enant DB routing

Adds apps/web/src/lib/shard-router.ts (pure, deterministic tenant -> shard
resolution via rendezvous/HRW hashing, chosen over hash % shardCount because
it only remaps the tenants whose winning shard actually changed when a shard
is added or removed) and db.ts's withTenantClient(), which opens a connection
to the resolved shard instead of always using DATABASE_URL.

This is deliberately additive: with DATABASE_SHARDS unset (true of every
deployment today), it resolves to the single existing DATABASE_URL shard, so
none of the four existing withClient call sites change behavior. Also adds
migrations/003_tenant_shard_columns.sql, which adds payments.workspace_id
(NOT NULL DEFAULT 'default') so no existing row's meaning changes, applied
idempotently in ensureSchema the same way 001/002 are.

SHARDING.md documents the design, the rollout plan for an actual second
shard, and is explicit about what this does not do (no Vitess/Citus, no
physical second database, no data movement, no call site switched over yet).
…n @accensa/sdk

Adds packages/sdk/receipt-anchor.ts: a registry mapping a ReceiptAnchor ABI
version to its contract method names and decode logic, and
createReceiptAnchorAbi(version) as the factory that resolves one. It has no
@stellar/stellar-sdk dependency and does no I/O - it only knows how to turn a
version's raw scValToNative-decoded return value into the SDK's stable
BatchRecord/boolean shapes, so it stays usable by anything embedding
@accensa/sdk without pulling in a Soroban RPC client.

Wires it into apps/web/src/lib/receipt-anchor.ts: verifyReceiptOnChain and
getBatch now call abi().verifyReceiptMethod/getBatchMethod and decode through
abi().decodeVerifyResult/decodeBatch instead of the hard-coded
'verify_receipt'/'get_batch' method names and root/count/period_start/
period_end field names. RECEIPT_ANCHOR_ABI_VERSION (env, defaulting to the
one version the deployed contract has ever shipped) selects which entry to
use, so a future differently-versioned deployment is a config change instead
of a code change. All four existing exports (BatchRecord, isHash32, getBatch,
verifyReceiptOnChain, RECEIPT_ANCHOR_ID) keep their exact signatures, so
page.tsx, batches/[id]/page.tsx, and api/verify/route.ts are unchanged.

Includes a second, explicitly-labeled example ABI version (not a real
deployed contract) with different method and field names, and
registerReceiptAnchorAbi() for adding versions this SDK build doesn't ship
built in. Tests in packages/sdk/receipt-anchor.test.ts demonstrate both
versions decoding differently-shaped raw contract output to the identical
BatchRecord shape, which is the backward-compatibility property the registry
exists to provide.
Adds `lib/explorer.ts`. `resolveStellarNetwork` reads
`NEXT_PUBLIC_STELLAR_NETWORK` (accepting the `public`/`pubnet` aliases), throws
on an unrecognised value, and falls back to testnet with a one-time console
warning when unset — a labelled default, not a silent one. `explorerTxUrl` and
`explorerContractUrl` build stellar.expert links for the resolved network,
mapping mainnet to the explorer's `public` path.

vitest gets `NEXT_PUBLIC_STELLAR_NETWORK=testnet` so the suite runs against a
defined network rather than the warning path.

Refs accensa#188
…the var

The landing page and the batch page each built a testnet stellar.expert URL
inline. Both now call `explorerContractUrl`, and the app README documents
`NEXT_PUBLIC_STELLAR_NETWORK` — its values, the testnet default, and why a
mainnet deployment must set it.

Refs accensa#188
`RefundPanel` modelled five phases but handled four. `submitting` fell through
to the idle branch, which rendered an enabled "Refund this payment" button
while a wallet prompt was open and a transaction in flight; clicking it
re-entered the preflight and could reach a second signing prompt for a refund
the vault would reject.

Splits the component into a stateful `RefundPanel` and a pure `RefundPanelView`
that renders one phase via an exhaustive `switch`. `submitting` returns an
early note that a signing prompt is open and offers no action; the `default`
branch calls `assertNever(phase)`, so a new unhandled phase is a type error.
Also routes the panel's two explorer links through `explorerTxUrl`.

Closes accensa#186
Renders `RefundPanelView` at each phase: idle offers the action, checking
disables it, submitting exposes no button at all (only the "signing prompt is
open" note), done shows the outcome. Documents the compile-time exhaustiveness
guard a runtime test cannot replace.

Refs accensa#186
`lib/dialog-focus.ts`: `getFocusable` lists tabbable descendants,
`wrapTabTarget` returns where Tab / Shift+Tab should wrap to keep focus in a
dialog (or null when the browser already would), and `focusRestorer` captures
the active element and returns a function that hands focus back. Plain
functions, unit tested without a DOM.

Refs accensa#191
The modal was a plain <div> overlay: no role, no accessible name, no focus
trap, and on close focus fell to <body> instead of the row that opened it — a
screen-reader user could Tab out and read the page behind a dialog they were
never told about.

The card now carries role="dialog", aria-modal, aria-labelledby pointing at its
heading, and a tabindex so it can receive focus. A mount effect moves focus in,
traps Tab with `wrapTabTarget`, keeps Escape working, and restores focus with
`focusRestorer` on unmount. The dead `closeButtonRef` and the parent's modal
effect are gone; `onClose` is memoised so a 15s poll re-render cannot re-trap
focus. Kept the custom overlay rather than native <dialog> to avoid restyling,
which is out of scope. Also swaps the explorer link to `explorerTxUrl`.

Closes accensa#191
SSR-renders PaymentModal and checks role="dialog", aria-modal, the
aria-labelledby / id pairing, the focusable container, and the labelled close
control. Focus trapping and restoration are covered by `dialog-focus.test.ts`.

Refs accensa#191
`fetchAllPayments` follows `/api/payments`' `next_cursor` to the end (it serves
at most 1,000 rows per request), reporting progress per page and capping at
1,000 pages so a non-advancing cursor cannot loop. `filterByRange` returns the
payments inside a `RangeKey` window, its start aligned to midnight UTC to match
`buildRevenueSeries`' bucketing; `'all'` is a passthrough.

Refs accensa#185
The page fetched `/api/payments` with no limit, received the newest 100 rows,
then offered an "All time" range selector — so every figure (total settled,
attributed split, per-route revenue, share bars, chart) was computed from at
most 100 payments while presenting itself as complete. On a page whose purpose
is proportions this is the worst kind of wrong: a truncated sample distorts the
relative share between routes.

Now pages the full history with `fetchAllPayments` (showing a running count
while it loads), then filters to the selected range with `filterByRange` before
building the breakdown and series, so shares are computed against the in-range
total. A perf test records the cost: ~8 ms for 600 payments across all three
ranges, ~15 ms for 3,000, on the dev machine.

Closes accensa#185
`pnpm format:check` fails on `main` (a long line in verify/page.tsx left
unwrapped by accensa#248), which turns the `format` job red on every PR. `prettier
--write` on the one file, no behavioural change.

Refs accensa#188
…vention (accensa#97)

body.txt and issues.json were tracked at the repo root once. Ignore them and
any future PR-body / issue-dump / one-off-script residue under a .scratch/
directory, documented in CONTRIBUTING.md. (Both files are already removed
from the tree; this closes the recurrence path.)
…lind spot (accensa#94)

- concurrency.cancel-in-progress: true -> false, so an overlapping trigger
  no longer kills a sync mid-range (indexing is idempotent; the run was
  simply lost).
- loop window 55m -> 65m so it overlaps the next hourly trigger; with
  self-cancel gone the overlap costs one extra idempotent sync instead of
  leaving a gap when a trigger is dropped under load.
- optional HEARTBEAT_URL pinged after every healthy sync and at clean exit,
  for an external dead-man's switch. If scheduling stops entirely the pings
  stop and the monitor alerts - the signal that did not exist when the
  cursor fell 207 ledgers behind retention.

Every existing in-loop diagnostic (401 assertion, syncedTo check, drained /
skippedLedgers warnings) is unchanged.
…i-asset constraints (accensa#94, accensa#95)

- 'Indexer scheduling - the options weighed': paid Vercel Cron vs an
  external scheduler vs a long-running worker vs keeping the loop, compared
  on cost / reliability / failure modes. Decision: keep the loop with the
  defects fixed now (this PR), migrate to an external scheduler
  (cron-job.org / EventBridge, ~$0) when the team wants the runner cost
  gone - that step needs an account + secret a maintainer must create.
- 'Settling in USDC or multiple assets': ASSET_CONTRACT_IDS, per-asset
  grouping (never sum across assets), the single-token RefundVault
  constraint and the deploy-a-vault-per-asset workaround, and the
  missing-trustline case that must read differently from 'no payments'.
… vault (accensa#95)

Add a USDC example to the .env.local block and a 'Settling in USDC or
multiple assets' subsection pointing at the fuller DEPLOYMENT.md notes.
…ma drift (accensa#91, accensa#92, accensa#93)

- accensa#92: a module-scoped `pg.Pool` (lazy, `PG_POOL_MAX` default 3, idle +
  connection + statement timeouts) replaces `new Client()` per request.
  `withClient` now checks out and releases; `closePool` for shutdown.
  The serverless reasoning is written into db.ts.
- accensa#91: `ensureSchema` is memoised — it ran a dozen DDL statements at the
  top of every /api/sync, /api/payments, /api/hook/settle and /api/routes
  call. `schemaReady` runs it once per warm instance; a failure clears
  the memo so the next request retries. It stays the single schema
  definition; the migrations/ files are the from-scratch mirror.
- accensa#91 drift: `idx_payments_hook_reported` existed only in migrations/002,
  not in ensureSchema, so a code-provisioned DB lacked it. Added.
- accensa#93: `idx_payments_merchant_ts_txhash (merchant_id, ts DESC, tx_hash DESC)`
  matching the /api/payments keyset ORDER BY exactly.
, accensa#93)

migrations/004 recreates `idx_payments_hook_reported` and adds the
`(merchant_id, ts DESC, tx_hash DESC)` keyset composite, so a
`psql -f migrations/*.sql` run against a code-built database is a no-op
rather than a diff.
…ccensa#90)

Answers "how far behind head is the cursor, right now?" from something
other than the thing being monitored. Returns per-merchant lag in
ledgers and time since last sync, an ok/warn/critical rollup, and 503
when any merchant is critical. Thresholds (SYNC_LAG_WARN_LEDGERS 60k,
SYNC_LAG_CRITICAL_LEDGERS 90k, SYNC_STALE_MS 3h) leave hours of head-room
before RPC retention — the arithmetic is in the file.
Polls /api/health on its own 2-hourly schedule — independent of
sync.yml, so it still fires if scheduling stops entirely. Fails the run
and POSTs to ALERT_WEBHOOK (Slack / PagerDuty / Discord) on `critical`
or a non-200; a `warn` is a workflow warning. skippedLedgers and
cessation are covered; wiring the alert to a specific rotation is left
to whoever owns the secret.
docs/RUNBOOK.md: what fired, what to check, what to do — for skipped
ledgers (incident; the honest "no recovery from RPC" answer), excessive
lag, no-recent-sync, and persistent drained:false.
`index.test.ts` drives `attachAccensaHook` with fake req/res objects. That
leaves the actual HTTP lifecycle untested — Express request parsing, header
casing, and the `finish` event — where a subtle regression could silently
stop the `X-PAYMENT-RESPONSE` header from being read and lose a merchant's
attribution with no error anywhere.

Replace the placeholder `e2e.test.ts` with a suite that stands up a real
Express app (a stand-in x402 layer + `attachAccensaHook` + a route) and
drives it with Supertest:

* an unpaid request gets a clean 402 with no settlement header and fires
  no report;
* a paid request is served, carries a valid base64 `X-PAYMENT-RESPONSE`,
  and the hook POSTs the settlement to `${indexerUrl}/api/hook/settle`
  with the right body, `Content-Type`, and `X-Signature`;
* a custom `attribute` fn controls the reported route;
* a failed delivery surfaces through `onError` rather than crashing the
  process on a `finish` listener.

The Accensa indexer is the only mock (a captured `fetch`); the report is
awaited via a deferred promise since the middleware fires it and forgets.
Closes accensa#118.
Split the revenue aggregations so the summation can happen in SQL:

* `seriesFromDayBuckets` owns the range window, the daily→weekly rollup,
  and the chart geometry; `buildRevenueSeries` now folds raw payments
  into per-day totals and defers to it.
* `routeBreakdownFromAggregates` builds the by-route breakdown from
  `GROUP BY method, route` rows; `buildRouteBreakdown` defers to it.
* `assetOptionsFromCounts` builds the asset selector from `GROUP BY asset`
  counts.

No behaviour change — the existing payment-array entry points keep their
contracts and all `revenue-analytics` tests pass unchanged.
Add `GET /api/analytics/revenue`, which does the revenue summation in
PostgreSQL rather than shipping the raw payment rows to the browser:

* `GROUP BY date_trunc('day', ts), asset` for the over-time series;
* `GROUP BY method, route, asset` for the by-route breakdown;
* `GROUP BY asset` for the asset selector's call counts.

Amounts stay exact (`NUMERIC` in, decimal `::text` out), `ts IS NOT NULL`
keeps unconfirmed attributions out of the figures, and the response is
one row per (day, asset) / (route, asset) regardless of table size.

The Revenue by Route page now fetches this endpoint instead of pulling
every payment and folding it in a render pass. Range switching re-windows
client-side with no refetch, and the asset selector and route table now
reflect all payments rather than the most recent 100. Closes accensa#116.
Mocked-DB tests for `/api/analytics/revenue`: auth 401, a DATABASE_URL
500 that leaks nothing, the three GROUP BY queries running and being
regrouped by asset with exact decimals preserved, the null-route rows
folding into one unattributed bucket, and empty structures for a merchant
with no payments.
`/api/payments` already returns keyset (`WHERE (ts, tx_hash) < cursor`)
pagination and a `next_cursor`, but the dashboard only ever showed the
first page. Add a "Load older settlements" control that walks the cursor.

The 15s poll keeps refreshing the head page for freshness; older pages
are pulled on demand and kept across polls (they are historical rows the
poll does not re-serve). `mergePayments` concatenates head + older pages
newest-first and de-duplicates by `tx_hash`, so a new settlement landing
at the head cannot show a row twice against an already-loaded older page.
The control is hidden once the cursor is exhausted and disabled while
offline. Closes accensa#117.
`mergePayments` unit tests: older pages append after the head newest
-first, an overlap after a sync de-duplicates by tx_hash, the head's copy
of a duplicated row wins, and empty inputs are handled.
wagmiiii and others added 26 commits August 28, 2026 21:37
Fix accensa#169, accensa#170, accensa#171, accensa#172: OpenAPI SDK codegen, chain reconciliation, tenant shard router, ABI registry
…es-15-35-37-88

feat: merchant anchoring, visual tests, Storybook, webhook delivery
…ible-payment-rows

fix(dashboard): make payment rows keyboard accessible
…ensa#175) (accensa#293)

- accensa#164: Local WASM-compatible cryptographic signing module. Web Crypto API
  based key derivation, transaction signing without wallet extensions.
- accensa#175: Offline-first sync engine with CRDTs. LWW-Register merge, vector
  clock ordering, localStorage persistence, auto-sync on reconnect,
  and pending operation queue.

Closes accensa#164 accensa#175

Co-authored-by: Ademola <ademola2993k@gmail.com>
…-webhook-ui-141-147

feat: SDK verifyAuth + webhook management UI (accensa#141 accensa#147)
…nate-sync-146-150

feat: paginated sync history + contract event indexing (accensa#146 accensa#150)
…ntend-scaffold-162

feat: micro-frontend architecture scaffold (accensa#162)
…filter-158

feat: add payments list filter and search (accensa#158)
…rmatters-144

feat: multi-currency price formatting helpers for SDK (accensa#144)
…dk-timeout-134-137

feat: custom 404 page + SDK timeout configuration (accensa#134 accensa#137)
feat(sdk): add multi-currency price formatting helpers
…config-history-157

feat: track historical merchant configuration changes (accensa#157)
fix(web): Revenue-by-Route truncation, refund button state, explorer links, modal a11y
Co-authored-by: Ademola <ademola2993k@gmail.com>
…ccensa#160) (accensa#296)

- Fix broken client.ts: dedupe timeoutMs, import error classes from ./errors
- Add AccensaRateLimitError and AccensaTimeoutError to errors.ts
- Add opt-in retryOn429 to fetchWithRetry, honoring Retry-After
- Retry 429 in AccensaClient.getJson with backoff; throw typed error after
- Add in-memory TTL cache for read queries with clearCache()
- Invalidate cache on new sync events

Co-authored-by: codexhange <codexhange@users.noreply.github.com>
@timo126
timo126 force-pushed the chore-98-adopt-formatter branch from 6982901 to 6716760 Compare August 29, 2026 09:18
@timo126
timo126 force-pushed the chore-98-adopt-formatter branch from baa0da5 to 57567be Compare August 29, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore: adopt a formatter — several files carry mangled whitespace from bad merges