Repository navigation
feat: transactional outbox, TTL keeper, investor recovery, event conv… - #942
Merged
Merged
Conversation
…ention - AstronLabs#858: add an OutboxEvent model + reversible migration, enqueue in the caller's transaction with a unique idempotency key, an at-least-once relay that leaves failed rows pending, scheduled relay/cleanup jobs, and tests covering a crash between commit and publish. - AstronLabs#855: add a permissionless `extend_ttl_batch` to escrow that extends the instance + named borrower records, bounded to 20 entries, rejecting unknown records; tap tests for extension and batch limits. - AstronLabs#856: add investor recovery to yield-vault: register, request, cancel and a delayed execute that moves the entire share position without changing total shares; events + tests including the pre-delay guard. - AstronLabs#857: define and enforce the lower_snake_case event topic convention in schema.cjs, add a generated event catalog, and document dual-emit renames. Existing fixtures were stale on main; refresh them so check passes. Note: backend jest could not run here (node_modules absent); new TS files are parser-checked. Contract suites: escrow 74 passed, yield-vault 20 passed. closes AstronLabs#858 closes AstronLabs#855 closes AstronLabs#856 closes AstronLabs#857
|
@Asta-wizard 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! 🚀 |
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.
closes #858
closes #855
closes #856
closes #857
A summary of the work done for the four issues bundled in
issues.md. Allchanges are in
remitMortage, on the default branchmain.Verification
Run with the pinned toolchains (
contracts/rust-toolchain.toml= Rust 1.96.0,soroban-sdk 22; backend Node/npm deps are not installed in this checkout):
Contracts (executed)
cargo test -p escrow— 74 passed, 0 failed, including the four newtest_ttl_config::extend_ttl_batch_*tests.cargo test -p yield-vault— 20 passed, 0 failed, including the five newrecovery tests (
recovery_takeover_moves_the_share_position_after_the_delay,takeover_cannot_execute_before_the_delay,investor_can_cancel_a_pending_takeover,rotating_the_recovery_address_clears_a_pending_takeover,recovery_address_must_differ_from_the_investor).cargo fmt -p yield-vault -- --check— clean.cargo fmt -p escrow -- --check— only the four pre-existing diffs atlib.rs:193/794/893/1174remain (untouched lines; confirmed by diffing beforeformatting). My inserted code was formatted to match.
cargo clippy -p escrow -p yield-vault --all-targets— no warnings from thenew code. Remaining warnings are pre-existing (
lending-poolunused helper,yield-vault1 * USDCatlib.rs:1028, digit grouping in the old escrowtest fixtures).
test_snapshots/churn is included.Event tooling (executed)
node contracts/event-schemas/schema.cjs check— passes (0 breaking, 0additive, 0 convention violations across 11 contracts / 169 events).
node contracts/event-schemas/schema.cjs catalog— writescontracts/event-schemas/CATALOG.md(169 events).conventionProblems()unit-exercised directly:Loan-Approved→ 1 finding,loan_approved/dispute/raised→ 0 findings.Backend (partially executed)
.github-style yarn/npm deps are absent (backend/node_modulesdoes notexist), so jest could not be run and installing the full dependency tree
is out of scope. The three new TypeScript files and the edited
scheduler.tspass Node's type-stripping parser (
node --experimental-strip-types --check),and the outbox logic is exercised by an in-memory test double that will run
under the repo's existing jest setup.
down.sql, matching the rollback convention (norollback-manifest.jsonentry needed).Pre-existing drift found and addressed
contracts/event-schemas/*.jsonwere stale onmain:schema.cjs checkalready failed with ~26 "fixture out of date" events (e.g.
attchg_a,credit_opened,y_mode) before any change here.updatewas run to bringthe fixtures current as part of feat(contracts): Standardize Event Topic Naming and Payload Layout Across Contracts #857's catalog work;
checknow passes.#858 — Transactional outbox for reliable event publishing
Closes #858
backend/prisma/schema.prisma: newOutboxEventmodel —idempotencyKey(unique),
topic,payload(JSONB),occurredAt,createdAt,publishedAt,attempts,lastError, with pending/retention indexes.backend/prisma/migrations/20261018000000_transactional_outbox/:migration.sqlcreates the table, the two partial indexes, and anOutboxEventLagview (pending count + oldest pending seconds);down.sqldrops them (reversible).
backend/src/services/transactionalOutbox.ts:enqueueOutboxEvent(tx, event)inserts in the caller's transaction(the API takes a Prisma transaction client; a duplicate
idempotencyKeyis a no-op via the unique constraint /
P2002).publishPendingOutboxEvents(publisher)publishes oldest-first in boundedbatches, stamps
publishedAtonly after the publisher resolves, and onfailure leaves the row pending while incrementing
attemptsand recordinglastError— the at-least-once guarantee.cleanupPublishedOutboxEvents()deletes only published rows older than theretention window; pending rows are never touched.
backend/src/jobs/outboxRelay.ts: the relay bridges to the existingenqueueLedgerEventRedis queue and forwards the stable idempotency key asthe job id, so a redelivery is deduplicable.
backend/src/jobs/scheduler.ts:outbox-relayevery minute(
OUTBOX_RELAY_CRON_SCHEDULE) andoutbox-cleanupdaily at 03:30 UTC(
OUTBOX_CLEANUP_CRON_SCHEDULE), both behind the existing leader election.backend/src/__tests__/transactionalOutbox.test.ts: in-memory double coversenqueue, duplicate-key suppression, crash between commit and publish → the
scheduled relay delivers it on the next pass, publish failure → stays
pending and retries, idempotency key forwarded, batch bound, and retention
cleanup that never deletes pending rows.
#855 — Storage TTL extension keeper entry point
Closes #855
contracts/escrow/src/types.rs: newTtlKeeperEntry { borrower, goal_id },the same
(borrower, goal_id)pair that keysDataKey::Borrower.contracts/escrow/src/lib.rs:extend_ttl_batch(env, entries)—permissionless (no auth), extends the instance entry once and each named
persistent record, returns the count, and emits
ttl_ext. Rejects an empty oroversized batch (
> MAX_TTL_KEEPER_BATCH = 20) withInvalidTtlConfigbefore iterating, and rejects a record the contract does not own with
EscrowGoalNotFound.contracts/escrow/src/test_ttl_config.rs:extend_ttl_batch_restores_instance_and_record_ttlsadvances ~20 days into the bump window, asserts the record/instance TTLs are
restored to the configured bumps, plus empty/oversized/unknown-record
rejection tests.
was added because the escrow error enum is at Soroban's 50-variant cap.
#856 — Investor recovery address with delayed takeover
Closes #856
Implemented in
yield-vault, the contract that holds investor share positions.contracts/yield-vault/src/lib.rs:DataKey::RecoveryAddress(investor)andDataKey::PendingRecovery(investor)+PendingRecoverystruct.set_recovery_address(investor, recovery)— investor-authenticated,rejects self-registration, and clears any pending takeover on rotation.
request_recovery(investor)— callable only by the registered recoveryaddress; starts a
RECOVERY_DELAY_LEDGERS(~7 days in production, 10 intests) delay.
cancel_recovery(investor)— investor-authenticated escape hatch.execute_recovery(investor)— recovery-authenticated; panics before thedelay elapses; moves the entire
ShareBalance(zeroing the old key andcrediting the recovery address) without changing
TotalShares, so theexchange rate and other holders are unaffected.
get_recovery_address/get_pending_recoveryviews, andrec_reg/rec_req/rec_cxl/rec_doneevents.key cannot bypass the delay), cancel, rotation-clears-pending, and
self-registration rejection.
#857 — Standardize event topic naming and payload layout
Closes #857
docs/EVENT_TOPIC_CONVENTION.md(new): the rule(
^[a-z][a-z0-9_]{0,31}$on the topic symbol and every literal sub-topic),why it exists, the enforced gate, catalog generation, and the dual-emit
migration procedure for renames.
contracts/event-schemas/schema.cjs:conventionProblems()validates every extracted event key;checknowprints and fails on convention violations (the conformance gate that runs in
.github/workflows/event-schema.yml), andupdaterefuses to write anon-conforming fixture.
catalogcommand generates the published catalog from the extractedfixtures.
contracts/event-schemas/CATALOG.md(new): generated catalog of all 169events — contract, event key, positional topic/payload layout.
violations), so no legacy rename/dual-emit was needed. The dual-emit recipe is
documented for future renames, and the new
ttl_ext/rec_*events followthe convention.
Judgement calls
Backend tests could not be executed.
backend/node_modulesis absent anda full
npm cipulls a very large tree; the workflow forbids fabricatingresults, so I ran what I could (parser checks, schema checks, Rust suites) and
left the jest test in place for CI. The test uses only an injected in-memory
double, so it has no runtime dependency on Prisma/Redis.
Outbox producers adopt the API incrementally. The outbox is fully
implemented, scheduled, and tested, but no existing route was rewritten to
call it: wrapping a core flow like
recordEscrowDepositin a transaction withan outbox insert is a behavioural change for that flow and belongs with the
event it emits. The documented contract (
enqueueOutboxEvent(tx, …)) is theadoption point, and the relay/cleanup halves are already live.
feat(contracts): Add a Storage TTL Extension Keeper Entry Point #855 has no keeper reward. The issue marks the reward optional. Adding a
configurable budget would require new storage and a new error path; I kept the
entry point minimal and permissionless, which is the core requirement.
InvalidTtlConfig/EscrowGoalNotFoundwere reused because the escrow errorenum is already at the 50-variant cap.
feat(contracts): Add an Investor Recovery Address with Delayed Takeover #856 lives in
yield-vault, notlending-pool. The investor'stransferable position is the vault's
ShareBalance; lending-pool positionsare spread across
Investor,DebtBalance, lockup, and yield-mode keys andtransferring all of them is a much larger, riskier change. The vault is where
the "entire position" is a single well-defined balance. Queued withdrawal
entries are explicitly not moved (they belong to the requester) and the
limitation is documented on
execute_recovery.feat(contracts): Standardize Event Topic Naming and Payload Layout Across Contracts #857 enforces the convention but does not rename existing events. The
audit found all 169 catalogued events already match
lower_snake_casewithinthe symbol limit, so renaming would break indexers for no conformance gain.
The dual-emit mechanism is documented for future renames, and the gate now
prevents non-conforming events from landing.
Event fixtures were refreshed. They were already stale on
main(~26events missing). Since the catalog is generated from the fixtures, bringing
them current was required for the feat(contracts): Standardize Event Topic Naming and Payload Layout Across Contracts #857 deliverable; the change is additive and
checkpasses.Test-snapshot churn was not committed. Running the contract tests rewrote
test_snapshots/; those generated files were restored/removed so the commitcontains only source, config, fixtures, catalog, and docs.
No PR was opened and no non-default branch was created.