Skip to content

[Chore] Deduplicate retry/error-handling logic in the accrual keeper #510

Description

@collinsezedike

Summary

packages/stellar-sdk-helpers/src/accrual-keeper.ts reimplements several pieces of logic that already exist elsewhere in stellar-sdk-helpers, instead of reusing them. None of these are correctness bugs, the duplicated versions currently behave the same as the originals, but they're a maintenance risk: a future fix to the shared version won't propagate to the duplicate, and vice versa.

Motivation

This surfaced during review of #484 (the Blend accrual keeper). The correctness/security findings from that review were fixed directly on the PR; these reuse findings were deliberately deferred to keep that PR focused, rather than bundling a refactor into a security fix. Filing them here so they're tracked instead of forgotten.

Update: #469's migration keeper needed most of this same submission/retry logic. Duplicating it a third time would have made the problem worse, so several items below were resolved as part of that work rather than deferred further; see the acceptance criteria for current status.

Proposed Solution

  • withKeeperRetry reimplements withRetry (packages/shared/src/utils.ts), which tx.ts already uses. withRetry already supports attempts, base delay, doubling backoff, and a shouldRetry predicate, everything withKeeperRetry needs. Extend withRetry to optionally report { value, attempts } and accept a logging hook, then have tx.ts, the accrual keeper, and the migration keeper all share it.
  • KeeperRpcServer (now in keeper-tx.ts) hand-declares 4 methods that already exist on the real rpc.Server type from @stellar/stellar-sdk. Replace with Pick<rpc.Server, "getAccount" | "simulateTransaction" | "sendTransaction" | "getTransaction">.
  • SkippedAdapter.reason is hardcoded to the literal "non-blend" regardless of which protocol was actually skipped. Once a second protocol is discoverable (e.g. DeFindex), every skip reports the same undifferentiated reason. Store the actual protocol name instead.
  • getAccount (and its sequence number) is re-fetched from RPC on every adapter/vault iteration in each keeper's sequential submission loop, instead of tracking the sequence locally and incrementing after each successful submit. Real, if modest, avoidable RPC load, worth revisiting once more than a couple of vaults are live.
  • Both discoverLiveAdapters (accrual-keeper.ts) and discoverMigrationVaults (migration-keeper.ts) retry a multi-call closure as one unit. If an early call succeeds but a later one in the same closure fails transiently, the retry re-issues the already-succeeded calls too instead of resuming from the failed one. Splitting each call into its own retry boundary avoids the redundant reads.

Scope

Field Value
Area SDK
Protocol affected None
Network testnet
Breaking change? No (internal refactor, no exported behavior changes)

Alternatives Considered

Leaving it as-is: the duplicated code is currently correct, so there's no urgency, but every future fix to the shared originals (withRetry, describeSendError, defaultSleep) has to be remembered and re-applied to each keeper's private copy, or they silently drift.

Acceptance Criteria

  • withKeeperRetry is removed in favor of an extended withRetry
  • describeSendError and sleep/defaultSleep are each defined once and imported, not duplicated (describeSendError exported from tx.ts; sleep/errorMessage/withKeeperRetry/KeeperRetryError/KeeperLogger/KeeperFailure moved to a shared keeper-retry.ts, used by both keepers)
  • KeeperRpcServer is a Pick<rpc.Server, ...> instead of a hand-written interface
  • Attempts/transient extraction is a single shared helper (retryOutcome() in keeper-retry.ts, used by both keepers)
  • Signing keypair is derived once per adapter, not once per retry attempt (submitKeeperOperation in keeper-tx.ts: the priorHash branch returns before ever reaching Keypair.fromSecret, so a retry only re-derives it if the prior attempt failed before obtaining a hash at all)
  • SkippedAdapter.reason names the actual protocol, not a hardcoded string
  • Each discovery call retries independently instead of one combined closure (now applies to both discoverLiveAdapters and discoverMigrationVaults)
  • Existing keeper test suite still passes with no behavior changes (166 tests passing in stellar-sdk-helpers, including the accrual keeper's original suite unchanged)

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third CampaignchoreBuild, CI, dependency updates, or repository maintenancesdkInvolves Blend or DeFindex SDK helpers in packages/stellar-sdk-helperstrivialSelf-contained task, completable in < 2 hours with no deep protocol knowledge required

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions