feat: fee forecasting, dependency ordering, portfolio aggregation, and SDK diagnostics - #528
Merged
Just-Bamford merged 9 commits intoAug 28, 2026
Conversation
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
…tics-dependencies-portfolio
…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.
|
@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! 🚀 |
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.
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.
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 aFeeForecastModelinterface so the model can be swapped without touching the public API.evaluateForecastAccuracywalk-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 asnulland 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 onlyisAvailable(), neverconnect()— and expose no keys or secrets; tests assert both.Verification
npx tsc --noEmitreports 22 errors, unchanged frommain; none in the new files.npx eslintclean on all new files.mainbaseline — no new failures. The pre-existing failures (freighter/lobstr integration, logger, priceSubscriptions, scheduler, shared, soroban) and the intermittentERR_WORKER_OUT_OF_MEMORYworker crash are present on unmodifiedmainand 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