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
Summary
packages/stellar-sdk-helpers/src/accrual-keeper.tsreimplements several pieces of logic that already exist elsewhere instellar-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
withKeeperRetryreimplementswithRetry(packages/shared/src/utils.ts), whichtx.tsalready uses.withRetryalready supports attempts, base delay, doubling backoff, and ashouldRetrypredicate, everythingwithKeeperRetryneeds. ExtendwithRetryto optionally report{ value, attempts }and accept a logging hook, then havetx.ts, the accrual keeper, and the migration keeper all share it.KeeperRpcServer(now inkeeper-tx.ts) hand-declares 4 methods that already exist on the realrpc.Servertype from@stellar/stellar-sdk. Replace withPick<rpc.Server, "getAccount" | "simulateTransaction" | "sendTransaction" | "getTransaction">.SkippedAdapter.reasonis 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.discoverLiveAdapters(accrual-keeper.ts) anddiscoverMigrationVaults(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
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
withKeeperRetryis removed in favor of an extendedwithRetrydescribeSendErrorandsleep/defaultSleepare each defined once and imported, not duplicated (describeSendErrorexported fromtx.ts;sleep/errorMessage/withKeeperRetry/KeeperRetryError/KeeperLogger/KeeperFailuremoved to a sharedkeeper-retry.ts, used by both keepers)KeeperRpcServeris aPick<rpc.Server, ...>instead of a hand-written interfaceretryOutcome()inkeeper-retry.ts, used by both keepers)submitKeeperOperationinkeeper-tx.ts: the priorHash branch returns before ever reachingKeypair.fromSecret, so a retry only re-derives it if the prior attempt failed before obtaining a hash at all)SkippedAdapter.reasonnames the actual protocol, not a hardcoded stringdiscoverLiveAdaptersanddiscoverMigrationVaults)stellar-sdk-helpers, including the accrual keeper's original suite unchanged)