chore: adopt a formatter — fix mangled whitespace, add .editorconfig and SDK lint - #2
Open
timo126 wants to merge 116 commits into
Open
chore: adopt a formatter — fix mangled whitespace, add .editorconfig and SDK lint#2timo126 wants to merge 116 commits into
timo126 wants to merge 116 commits into
Conversation
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.
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
…fold-162 feat: micro-frontend scaffold (accensa#162)
feat: payments filter (accensa#158)
…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
force-pushed
the
chore-98-adopt-formatter
branch
from
August 29, 2026 09:18
6982901 to
6716760
Compare
timo126
force-pushed
the
chore-98-adopt-formatter
branch
from
August 29, 2026 12:26
baa0da5 to
57567be
Compare
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.
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+.gitattributesso 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 lintMerge conflict resolution
apps/web/src/app/api/sync/route.tshad unresolved conflict markers. Both imports (isAuthorizedCronRequestandcreateHmac) are used — kept both.Mangled string literals repaired (16 instances)
A grep for
[a-zA-Z]"[a-zA-Z]acrossapps/web/srcconfirmed 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"TO→COLUMN "timestamp" TOsync/route.ts:362:dashboard's"Sync now"button→dashboard's "Sync now" buttonstellar-events.ts:13:e.g."native"or"USDC:GA..."→e.g. "native" or "USDC:GA..."Post-fix grep confirms zero remaining mangled strings.
.editorconfigadded — matches the Prettier config (2-space indent, LF, UTF-8)..gitattributesadded — enforceseol=lfrepo-wide so Windows contributors never commit CRLF.ESLint extended to
packages/sdk— flat config withtypescript-eslintrecommended rules +lintscript added topackage.json.apps/demo-merchantESLint 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.mdupdated — now mentions.editorconfig, ESLint coverage, and the full pre-commit workflow..gitignoreupdated — prevents local tooling artifacts (node_dist/,*.zip, etc.) from being committed.Commit 2:
style: apply prettier formatting across the entire codebaseA 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-revsRecords the format commit in
.git-blame-ignore-revsalongside the two earlier format commits.Acceptance criteria checklist
packages/sdk/index.ts)054f864).git-blame-ignore-revsformat:checkruns in CI over every workspace (already inci.yml)packages/sdk;apps/demo-merchantgap justified.editorconfigcommitted;CONTRIBUTING.mdmentions the formatter