Skip to content

feat: fee forecasting, dependency ordering, portfolio aggregation, and SDK diagnostics - #528

Merged
Just-Bamford merged 9 commits into
Sorokit:mainfrom
Chidimj:feat/sdk-forecasting-diagnostics-dependencies-portfolio
Aug 28, 2026
Merged

feat: fee forecasting, dependency ordering, portfolio aggregation, and SDK diagnostics#528
Just-Bamford merged 9 commits into
Sorokit:mainfrom
Chidimj:feat/sdk-forecasting-diagnostics-dependencies-portfolio

Conversation

@Chidimj

@Chidimj Chidimj commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Implements four SDK capabilities. Each is a self-contained module with its own tests; rationale for the design decisions is in the individual commit messages.

#523 — Historical fee forecasting (src/transaction/feeForecast.ts)
forecastFees(daysAhead) returns a predicted fee with a confidence range, trend, volatility, and the data window used. Insufficient history is reported explicitly rather than as a degraded number. The strategy sits behind a FeeForecastModel interface so the model can be swapped without touching the public API. evaluateForecastAccuracy walk-forward scores a model against a historical dataset.

#526 — Dependency analysis and deterministic ordering (src/transaction/dependencyGraph.ts)
Kahn's algorithm with a lexicographic tie-break, so a graph with several valid orders always yields the same one. Detects circular, missing, duplicate, and unsatisfiable dependencies with structured errors naming the offending chain, and identifies parallel-safe execution levels. Cycle detection is iterative, so deep graphs cannot overflow the stack.

#525 — Multi-wallet portfolio aggregation (src/account/portfolioAggregation.ts)
aggregatePortfolio(wallets, options) normalises balances by asset identifier while preserving per-wallet attribution, detects duplicate account sources, and computes allocation and concentration metrics. Missing price data is represented as null and excluded from totals rather than treated as zero.

#527 — SDK health checks and diagnostics (src/shared/diagnostics.ts)
checkSdkHealth() sweeps Horizon, Soroban RPC, the wallet adapter, and network configuration concurrently; runDiagnostics() adds a runtime check and aggregates actionable recommendations. Every check is independently executable. Checks are read-only — the wallet check calls only isAvailable(), never connect() — and expose no keys or secrets; tests assert both.

Verification

  • 152 new tests across the four suites, all passing.
  • npx tsc --noEmit reports 22 errors, unchanged from main; none in the new files.
  • npx eslint clean on all new files.
  • Full suite matches the main baseline — no new failures. The pre-existing failures (freighter/lobstr integration, logger, priceSubscriptions, scheduler, shared, soroban) and the intermittent ERR_WORKER_OUT_OF_MEMORY worker crash are present on unmodified main and are untouched by this branch.

All four modules are exported through their own barrel and through src/index.ts.

Closes #523
Closes #525
Closes #526
Closes #527

Fee estimation previously reflected only present network conditions, so
applications scheduling batched or delayed transactions had no way to
reason about how fees might evolve.

Adds a forecasting layer over normalised historical fee observations:

- recordFeeObservation/getFeeObservations/clearFeeObservations collect
  timestamped fee data per network in a bounded in-memory store. The
  existing getFeeHistory store keeps only bare numbers with no
  timestamps, which cannot support a trend fit.
- normalizeFeeHistory validates and sorts observations, discarding
  malformed timestamps and fees, and screens outliers with a modified
  z-score over the median absolute deviation. MAD is used rather than a
  standard z-score because the mean and standard deviation are dragged
  by the very fee spikes the screen exists to catch. A zero MAD is
  treated as degenerate and skips screening, since scoring against it
  would discard every value that is not exactly the median.
- forecastFees(daysAhead, options) returns a predicted fee with a
  confidence range, the fitted trend per day, the observed volatility,
  and the data window used. Insufficient history and invalid daysAhead
  are reported as an explicit unavailable result rather than a silently
  degraded number.
- The strategy sits behind the FeeForecastModel interface so a more
  sophisticated model can replace the default without touching the
  public API. The default is an ordinary-least-squares fit whose
  prediction interval widens with residual error and with distance from
  the observed data, so volatile inputs and distant horizons both
  produce wider ranges. Confidence levels map to z-values through a
  rational probit approximation, accurate to four decimals against
  published values, avoiding any statistics dependency.
- evaluateForecastAccuracy walk-forward scores a model against a
  historical dataset, reporting MAE, MAPE, RMSE and confidence-range
  coverage. Zero actuals are excluded from MAPE so a single zero cannot
  poison the mean with Infinity.

Recording is a synchronous in-memory write performing no I/O, so
collecting observations does not block transaction operations.

Exported through src/transaction/index.ts and src/index.ts.

Closes Sorokit#523
Applications composing multi-step workflows had to work out transaction
order by hand, and incorrect ordering caused avoidable failures that
were hard to trace back to the missing edge.

Adds a dependency-graph layer over declared transaction nodes:

- validateDependencies collects every problem in one pass rather than
  failing on the first: duplicate ids, blank or non-string ids,
  self-dependencies, missing dependencies, and circular chains. Each
  error is structured with a code and the implicated chain, so callers
  can identify the offending edge without parsing a message.
- planTransactionExecution validates before doing any ordering work, so
  an invalid graph never yields a partial or misleading order. On
  success it returns both the flat order and the parallel-safe levels,
  with the caller's payloads carried through for the execution step.
- Ordering uses Kahn's algorithm draining the ready set in
  lexicographic order, so a graph admitting several valid orders always
  produces the same one. Parallel levels fall out of the same pass:
  each level depends only on earlier levels.
- resolveTransactionOrder and findParallelizableTransactions wrap the
  planner for callers that prefer a thrown DependencyGraphError, which
  carries the full structured error list.

Cycle detection uses an iterative DFS rather than recursion so a deep
chain cannot overflow the stack, and each distinct cycle is reported
once, rotated to start at its smallest member so the entry point does
not change the report. Tests cover a 10,000-node chain in both the
acyclic and cyclic cases.

This is deliberately separate from resolveExecutionOrder in bundles.ts,
which is scoped to an existing bundle's lifecycle and does not detect
missing or duplicate ids, report structured errors, identify parallel
groups, or order deterministically.

Exported through src/transaction/index.ts and src/index.ts.

Closes Sorokit#526
Users holding several wallets had to fetch each account's balances and
compute combined exposure themselves, with no abstraction that merged
accounts while keeping track of which wallet each holding came from.

Adds aggregatePortfolio(wallets, options):

- Balances are normalised by a canonical asset identifier. The native
  asset collapses to "native"; every other asset is keyed by code and
  issuer, because two issuers can use the same code and merging them
  would silently combine unrelated assets.
- Each holding keeps a per-wallet attribution breakdown, so combined
  totals never lose the source. Repeated listings of one asset within a
  wallet fold into a single attribution row.
- Duplicate account sources are detected and reported, and counted only
  once in the totals, so re-supplying a wallet cannot inflate the
  portfolio.
- Valuation is optional. A holding with no price gets a null value and
  null allocation, is excluded from the total, and is listed in
  coverage.missingPriceAssetIds. Missing price data is never treated as
  zero, since an unpriced asset is unknown rather than worthless.
  Allocation percentages are computed over priced value only, so an
  unpriced holding does not dilute everything else's share.
- Concentration metrics report the largest allocation and a
  Herfindahl-Hirschman index over the priced portion, alongside asset
  and wallet counts.

Aggregation is deliberately kept separate from wallet connection
lifecycle: the module performs no network calls and operates only on
already-normalised account data, so any provider can feed it.

Exported through src/account/index.ts and src/index.ts.

Closes Sorokit#525
Applications had no unified way to tell whether their SDK environment
was working. A failure could originate from Horizon, Soroban RPC, a
wallet adapter, configuration, or network selection, and each had to be
probed by hand.

Adds a diagnostics subsystem:

- checkSdkHealth() runs a lightweight sweep across Horizon, Soroban RPC,
  the configured wallet adapter, and network configuration, returning a
  structured report with the SDK version, network, and per-check detail.
  The two endpoint checks run concurrently, so the call costs about one
  round trip rather than two.
- runDiagnostics() adds a runtime capability check and rolls every
  finding into de-duplicated issue and recommendation lists plus a count
  of checks by status.
- Every check is exported individually, so an application can run only
  what its environment warrants — a Node service can skip the wallet
  check entirely.
- Status is healthy / degraded / unavailable, with a fourth `skipped`
  state for checks that did not apply. Skipped checks are excluded when
  reducing to an overall status, so an absent wallet adapter does not
  make a healthy environment look degraded.
- Latency is captured for external dependencies and is null for local
  checks. Timeouts are distinguished from transport failures so the
  recommendation can point at options.timeoutMs rather than at
  connectivity.
- The RPC check calls the node's own getHealth method rather than merely
  opening a socket, so a running-but-unhealthy node is reported as such.
- Configuration validation catches the misconfigurations that otherwise
  surface as confusing runtime errors: unknown network, malformed URL,
  unsupported scheme, plaintext HTTP on mainnet, and a passphrase that
  does not match the selected network. A mismatched passphrase is
  unavailable rather than degraded, since it invalidates every signature
  for the target network.

Diagnostics are strictly read-only. The wallet check calls only
isAvailable(), never connect() or signTransaction(), since prompting the
user would mutate application state. No check reads or reports a private
key, secret, or other sensitive wallet data, and tests assert both
properties.

Exported through src/shared/index.ts and src/index.ts.

Closes Sorokit#527
…ecasting-diagnostics-dependencies-portfolio

# Conflicts:
#	src/index.ts
#	src/transaction/index.ts
…iagnostics-dependencies-portfolio

# Conflicts:
#	src/index.ts
…diagnostics-dependencies-portfolio

# Conflicts:
#	src/index.ts
The cycle de-duplication key joined with a raw NUL byte written directly
into the source, which made git treat dependencyGraph.ts as a binary
file. Writing it as the \u0000 escape keeps the same separator - which
cannot collide with an id containing whitespace - while leaving the file
as plain UTF-8 text.

Also restores the blank line between barrel sections in src/index.ts
that was lost while resolving the merge.
@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

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

@Just-Bamford
Just-Bamford merged commit 6ae83f9 into Sorokit:main Aug 28, 2026
Johnalex-hub added a commit to Johnalex-hub/core that referenced this pull request Aug 28, 2026
…workflows

Resolve barrel export conflicts in src/index.ts and src/transaction/index.ts.

Both sides were purely additive re-export blocks appended to the same
region of each barrel, so the resolution keeps both:

- HEAD: spending policy, contract state history, multi-sig execution,
  wallet security audit exports.
- upstream/main (Sorokit#528): fee forecasting, dependency graph, portfolio
  aggregation, SDK diagnostics exports.

No symbol collisions between the two sets. Verified: tsc error count
unchanged from the pre-merge baseline (22, all pre-existing), and all
310 tests across both sides' new suites pass.
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