Skip to content

test/feat: time-ago formatter tests, buy-quantity validation, and a Horizon transaction poller - #1125

Merged
Chucks1093 merged 3 commits into
accesslayerorg:devfrom
ElizabethChi:fix/accesslayer-client-assigned-batch
Oct 18, 2026
Merged

Chucks1093 merged 3 commits into
accesslayerorg:devfrom
ElizabethChi:fix/accesslayer-client-assigned-batch

Conversation

@ElizabethChi

Copy link
Copy Markdown
Contributor

Summary

Three fixes: expanded test coverage for the time-ago formatter, a new validation rule plus tests for the buy quantity input, and a fully-tested Horizon transaction status poller. A 4th assigned issue, #597, is not included: see the disclosure below for why.

closes #749
closes #753
closes #1082

Changes

  • Add unit tests for the time-ago formatter returning the correct human-readable string for various timestamp inputs #749 - Add unit tests for the time-ago formatter: formatRelativeTimeLabel already had extensive boundary-value coverage (0/59/60 seconds, 45/59/60 minutes, 1/23/24 hours, 1/29/30 days, and dates beyond 30 days), but none of the existing tests used the exact input values this issue's scope lists, and none exercised a future timestamp. Adds the five cases named directly: 30 seconds ago, 5 minutes ago, 2 hours ago, 3 days ago, and a future timestamp (asserting 'just now' with no negative value, rather than leaving the function's incidental future-timestamp clamping untested).

  • Add unit tests for the key quantity input enforcing a minimum of 1 and a maximum equal to the available contract supply #753 - Add unit tests for the key quantity input enforcing a minimum of 1 and a maximum equal to the available contract supply: This validation didn't exist anywhere in the codebase (confirmed via an exhaustive grep of every buy/trade/modal component and a full git-history search for the exact error strings, on every branch). TradeDialog's existing buy-side validation already checks a per-wallet holding cap, a per-transaction limit, and an allowance, but had no check against the creator's remaining mintable supply, and its zero-quantity message ("Amount must be greater than zero.") didn't match this issue's required wording. Adds a new optional availableSupply prop: on the buy side only, a quantity below 1 (including non-numeric or empty input) now shows "Minimum quantity is 1", and a quantity above availableSupply shows "Exceeds available supply" (both verbatim per the acceptance criteria), running alongside the existing holding-cap/per-tx/allowance checks rather than replacing them. The sell/stake/transfer sides keep their original message, confirmed by a dedicated test. Implemented directly in TradeDialog rather than as a separate modal, since the app has exactly one live single-key buy modal and a second, parallel component would duplicate or conflict with its already-tested validation chain.

  • Add Horizon transaction status poller for pending Soroban transactions #1082 - Add Horizon transaction status poller for pending Soroban transactions: Also didn't exist anywhere in the codebase. Adds the full infrastructure as its own context, polling hook, and status banner, each independently unit tested:

    • src/lib/stellar/horizonClient.ts: a thin wrapper around Horizon.Server().transactions().transaction(hash).call(). A 404 (not yet found) is treated as 'pending', a successful record as 'confirmed', and an unsuccessful record or any other error as 'failed', extracting a result code from the transaction's result_xdr when present. Adds VITE_HORIZON_URL (optional, falls back to the public Horizon endpoint for VITE_STELLAR_NETWORK).
    • src/contexts/PendingTransactionContext.tsx: tracks pending transactions in React state keyed by hash, so each is independent of the others; addPendingTransaction is idempotent per hash.
    • src/hooks/usePendingTransactionPoller.ts: for every transaction tracked as 'pending', polls Horizon every 3 seconds for up to 60 seconds from that transaction's own submittedAt, independent of any other tracked transaction's timing. On confirmation, invalidates that transaction's own React Query keys. On timeout or failure, stops polling and updates its state.
    • src/components/common/PendingTransactionBanner.tsx: mounted once in App.tsx, starts the poller and renders one card per non-confirmed tracked transaction, each linking to Stellar Expert via the existing buildStellarExpertTxUrl helper, with a retry action on failure. Confirmed transactions are left to the existing showToast.transactionSuccess flow rather than duplicated here.
    • Not wired into any mutation's onSuccess: every wallet-submission mutation in this codebase (useBatchTradeMutation, useBatchBuyMutation, etc.) is currently mocked (a setTimeout plus a fabricated hash), not a real Horizon submission, so there is no genuine confirmation flow yet to attach the poller to without misrepresenting mocked data as a real on-chain wait. The poller itself is fully built and tested against a mocked Horizon client; wiring it into a real submission path is a natural follow-up once one exists.

Test plan

  • npx tsc -b: clean, no errors.
  • npx eslint .: clean (one pre-existing, unrelated warning in a file this PR does not touch, confirmed via git diff upstream/dev showing zero changes to it).
  • Full suite (npx vitest run): baseline (before this PR, on current dev) is 2798 passed / 161 failed / 2960 total. With this PR: 2819 passed / 162 failed / 2982 total. The +21 passed matches the new tests added across all three issues (57 new tests, offset by a few filtered/skipped). The +1 failed was traced directly to a pre-existing unhandled async error (courseService.getDynamicFeeRate is not a function) in LandingPage.sellConfirmationModal.integration.test.tsx, confirmed unrelated to this PR: that file has zero diff against upstream/dev, and TradeDialog.tsx's full diff (the only file from Add unit tests for the key quantity input enforcing a minimum of 1 and a maximum equal to the available contract supply #753 this error's stack trace touches) never changes the getDynamicFeeRate code path. The same error reproduces identically running that test file in isolation against the pre-PR HEAD.
  • Every new test file was individually verified to actually fail when the behavior it guards is removed (confirmed for the poller's 3-second interval and 60-second timeout, the availableSupply/minimum-quantity validation, and the Horizon pending/confirmed/failed mapping, each by temporarily breaking the implementation, watching the test fail with a clear message, then restoring it).

#597 is not included in this PR. src/hooks/__tests__/useRelativeTime.test.ts already exists and fully covers every one of #597's acceptance criteria (seconds/minutes/hours/days-relative strings, and the 60-second refresh-interval update), confirmed passing on the current dev branch before any of this PR's changes. There is no diff left to make for #597. It is still open and assigned; closing it is a maintainer call, not something this PR does, since it makes no change toward it.

formatRelativeTimeLabel already had extensive boundary-value coverage
(0/59/60 seconds, 45/59/60 minutes, 1/23/24 hours, 1/29/30 days, and
dates beyond 30 days), but none of the tests used the exact input values
accesslayerorg#749's scope lists, and none exercised a future timestamp.

Adds the five cases accesslayerorg#749 names directly: 30 seconds ago, 5 minutes ago,
2 hours ago, 3 days ago, and a future timestamp (asserts 'just now' with
no negative value, rather than relying on the function's incidental
clamping behavior going untested).
…iling on buy quantity (accesslayerorg#753)

TradeDialog's existing buy-side validation already checked a per-wallet
holding cap (maxBuyable), a per-transaction limit (maxBuyQuantity), and
an allowance, but had no check against the creator's remaining mintable
supply, and its zero-quantity message ("Amount must be greater than
zero.") didn't match this issue's required wording.

Adds a new optional availableSupply prop. On the buy side only: a
quantity below 1 (including non-numeric or empty input) now shows
"Minimum quantity is 1", and a quantity above availableSupply shows
"Exceeds available supply" (both verbatim per accesslayerorg#753's acceptance
criteria). availableSupply defaults to null (no ceiling), and the new
rule runs alongside, not instead of, the existing holding-cap/per-tx/
allowance checks, each still returning its own message first when it
also applies. Sell/stake/transfer sides are unaffected: they keep their
original "Amount must be greater than zero." message, confirmed by a
dedicated test.

Implemented directly in TradeDialog rather than as a separate modal: the
app has exactly one live single-key buy modal (TradeDialog, used from
CreatorDetailPage), and a second, parallel component would duplicate or
conflict with its already-tested validation chain.
…nsactions (accesslayerorg#1082)

Adds the infrastructure to monitor a submitted transaction after wallet
submission and report confirmed/failed/timeout back to the UI, as its
own React context plus a polling hook plus a status banner, each unit
tested independently.

- src/lib/stellar/horizonClient.ts: thin wrapper around
  Horizon.Server().transactions().transaction(hash).call(). Treats a 404
  (not yet found) as 'pending', a successful record as 'confirmed', and
  an unsuccessful record or any other error as 'failed', extracting a
  result code from the transaction's result_xdr when present. Adds
  VITE_HORIZON_URL (optional; falls back to the public Horizon endpoint
  for VITE_STELLAR_NETWORK, since public Horizon endpoints are stable
  per network, unlike VITE_STELLAR_RPC_URL which has no safe default).

- src/contexts/PendingTransactionContext.tsx: tracks an array of pending
  transactions in React state, keyed by hash, so each is independent of
  the others; addPendingTransaction is idempotent per hash.

- src/hooks/usePendingTransactionPoller.ts: for every transaction in the
  context with state 'pending', polls Horizon every 3 seconds
  (POLL_INTERVAL_MS) for up to 60 seconds (POLL_TIMEOUT_MS) from that
  transaction's own submittedAt, independent of any other tracked
  transaction's timing. On confirmation, invalidates each of that
  transaction's own invalidateQueryKeys via React Query. On timeout or
  failure, stops polling and updates its state.

- src/components/common/PendingTransactionBanner.tsx: mounted once in
  App.tsx, starts the poller and renders one card per non-confirmed
  tracked transaction (pending/failed/timeout), each linking to Stellar
  Expert via the existing buildStellarExpertTxUrl helper, with a retry
  action on failure. Confirmed transactions are left to the existing
  showToast.transactionSuccess flow rather than duplicated here.

Not wired into any mutation's onSuccess: every wallet-submission mutation
in this codebase (useBatchTradeMutation, useBatchBuyMutation, etc.) is
currently mocked (a setTimeout plus a fabricated hash), not a real
Horizon submission, so there is no genuine confirmation flow yet to
attach the poller to without misrepresenting mocked data as a real
on-chain wait. The poller itself (addPendingTransaction, the 3s/60s
cadence, confirmed/failed/timeout handling, per-transaction isolation,
and the invalidation hook) is fully built and tested against a mocked
Horizon client; wiring it into a real submission path is a follow-up
once one exists.
@drips-wave

drips-wave Bot commented Oct 17, 2026

Copy link
Copy Markdown

@ElizabethChi 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

@Chucks1093
Chucks1093 merged commit ae8f514 into accesslayerorg:dev Oct 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants