Indexer withdraw decoding, validated savings pagination, and 2 contract round-trip tests - #206
Merged
Merged
Conversation
|
@Kinkytech 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! 🚀 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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 #65
closes #63
closes #35
closes #34
Changes
[Backend] — Indexer: decode
withdrawevent #65 — Indexer: decodewithdrawevent:SavingsProjectionService.apply()had acase 'deposit':handler crediting the flexible balance, but nocase 'withdraw':at all — withdrawals silently fell through to the no-opdefaultbranch and were never projected off-chain. Added the handler, but deliberately not as a decrement-by-amount (debit) — the contract'swithdrawevent already carries its own post-withdrawal balance ((owner, amount, new_balance, now), seeflexible.rs'swithdraw()), so the newBalanceService.setBalance()sets the projected balance to that absolute value instead. This is what makes the projection idempotent: replaying the same event twice converges to the same final balance both times, whereas decrementing by the withdrawn amount would double-apply on redelivery. Mirrors the same idempotency approachLockedPlansService.upsertCreatedalready uses forlocked_created.[Backend] — Pagination + filtering helpers for savings lists #63 — Pagination + filtering helpers for savings lists:
GET /savings/goalsandGET /savings/lockedaccepted raw@Query('page')/@Query('limit')with no validation at all —Number(page)on arbitrary input could produceNaN, a negativeskip, or silently-wrong results, never a rejection. While investigating I foundbackend/src/common/dto/pagination-query.dto.ts— a shared, already-testedPaginationQueryDto(page ≥1, limit 1-100, both rejected not clamped) that exists in the codebase but was never actually used anywhere. BuiltSavingsListQueryDtoby extending that shared DTO rather than duplicating page/limit validation, adding only asortfield (asc/desc— direction only, not a free-text column name, so a caller can't request a sort column with no supporting index). Applied it to both endpoints via@Query() query: SavingsListQueryDto, each paired with@UsePipes(new ValidationPipe({ whitelist: true }))— this app has no globalValidationPipe(confirmed: none inmain.tsorapp.module.ts), so a DTO-typed@Query()param's decorators are otherwise inert;users.controller.tsalready establishes this exact per-route workaround for the same reason.GoalsService.listByOwnerPaginatedandLockedPlansService.listByOwnerboth gained an optionalsortparameter controlling the direction of their existing fixed sort column.[Contract] — Test: locked plan enforces unlock time #35 — Test: locked plan enforces unlock time: The individual pieces already existed as separate tests (
locked_withdraw_before_unlock_rejected,locked_withdraw_exact_balance_after_unlock_succeeds), but no single test exercised the issue's exact scenario — create → early-reject → advance ledger time → withdraw → assert balances — as one continuous flow. Addedlocked_plan_enforces_unlock_time_round_tripcovering exactly that, asserting the plan balance at each step (post-creation, post-rejected-early-withdrawal, post-successful-withdrawal).[Contract] — Test: flexible deposit/withdraw round-trip #34 — Test: flexible deposit/withdraw round-trip: Similarly,
flexible_withdraw_over_balance_rejectedandflexible_withdraw_exact_balance_succeedseach isolated one scenario, and the confusingly-named existingflexible_deposit_withdrawtest never actually callswithdraw()at all (despite its name — it only exercisesdeposit). Addedflexible_deposit_withdraw_round_trip: fund → deposit → partial withdraw → assert balance → attempt over-withdraw against the remaining balance → assert rejected and balance unchanged → withdraw the rest → assert zero. The partial-withdraw-then-over-withdraw-against-remainder case wasn't covered by any existing test.Test plan
withdrawevent #65, [Backend] — Pagination + filtering helpers for savings lists #63):npx jeston all 6 touched/new spec files —savings.controller.spec.ts,dto/pagination.dto.spec.ts(new),goals.service.spec.ts,locked-plans.service.spec.ts,balance.service.spec.ts,savings-projection.service.spec.ts— 64/64 passing, including 11 new DTO-validation tests (page/limit boundaries,sortaccept/reject, combined multi-field errors) and new sort-direction tests for both list services (seeded with explicit fake-timer-controlledcreated_at/unlock_atvalues so ASC/DESC order is deterministic to assert against — the real-clock seed loop in the existingbeforeEachruns fast enough that timestamps could collide within a millisecond, so those sort tests use their own isolated setup rather than the shared 25-goal seed).npx tsc --noEmit: zero errors in any touched file — confirmed viagit stashthat the one hit inbalance.service.ts(TS1272, anemitDecoratorMetadata/isolatedModulesinteraction with a decorated constructor param) is pre-existing and unrelated, identical on unmodified code.npx eslint: 0 errors on every touched/new file after--fix(the only remaining warnings are pre-existingno-awaiton fake-repository test doubles I didn't create).cargo test—soroban-env-host's owntestutilsmodule fails to compile with a pre-existinged25519-dalek/ChaCha20Rng/rand_core::CryptoRngtrait-bound error (ChaCha20Rngdoesn't implementDerefMut), unrelated to this repo's code and outside my control; reproduced identically withcargo check --testsand confirmed via a fresh clone ofstowp/Stowmain with zero changes applied — same error, same location. Since I couldn't compile, I verified as thoroughly as I could without it:cargo fmt --checkontest.rsspecifically shows zero diffs — my additions match the crate's formatting exactly. (The crate as a whole has pre-existing formatting drift inadmin.rs/flexible.rs/lib.rs/locked.rs/group_split.rs, confirmed byte-identical between this branch and a fresh clone ofmainviacargo fmt --checkdiff comparison — none of it is mine.)Err(Ok(Error::...))), andenv.ledger().set(LedgerInfo {...})boilerplate from the immediately-adjacent, already-passing tests they combine (flexible_withdraw_over_balance_rejected/flexible_withdraw_exact_balance_succeedsfor [Contract] — Test: flexible deposit/withdraw round-trip #34;locked_withdraw_before_unlock_rejected/locked_withdraw_exact_balance_after_unlock_succeedsfor [Contract] — Test: locked plan enforces unlock time #35) — no new API surface, no new patterns, just recombined in one flow per each issue's literal requirement.