Class SolverDiscoveryService
- Namespace
- NArk.ArkadeIntents.Services
- Assembly
- NArk.ArkadeIntents.dll
Client for Arkade Market Discovery v0 and v1: fetches per-network solver indexes, merges
them with local cards, filters/ranks markets for a trade, reads the market's price feed and
derives the maker's wantAmount.
public sealed class SolverDiscoveryService
- Inheritance
-
SolverDiscoveryService
- Inherited Members
Remarks
The trust anchor is each registry the client follows (PR review is the listing gate, git history the audit log, HTTPS the transport integrity); clients may follow several registries and add local cards. Indexes are cached for NArk.ArkadeIntents.Services.SolverDiscoveryService._cacheTtl. Discovery of an external chain does not imply that this client can execute its swaps.
Constructors
SolverDiscoveryService(HttpClient, ILogger<SolverDiscoveryService>?)
public SolverDiscoveryService(HttpClient http, ILogger<SolverDiscoveryService>? logger = null)
Parameters
httpHttpClientloggerILogger<SolverDiscoveryService>
SolverDiscoveryService(HttpClient, TimeSpan, TimeSpan, ILogger<SolverDiscoveryService>?)
public SolverDiscoveryService(HttpClient http, TimeSpan cacheTtl, TimeSpan stalenessThreshold, ILogger<SolverDiscoveryService>? logger = null)
Parameters
httpHttpClientcacheTtlTimeSpanstalenessThresholdTimeSpanloggerILogger<SolverDiscoveryService>
Fields
DefaultSafetyBps
Suggested default client-side safety cushion, in basis points.
public const int DefaultSafetyBps = 50
Field Value
MainnetRegistry
Default per-network index URLs published by the reference registry.
public static readonly Uri MainnetRegistry
Field Value
MutinynetRegistry
public static readonly Uri MutinynetRegistry
Field Value
RegtestRegistry
public static readonly Uri RegtestRegistry
Field Value
SignetRegistry
public static readonly Uri SignetRegistry
Field Value
SupportedVersion
The highest supported discovery version; v0 remains supported.
public const int SupportedVersion = 1
Field Value
Methods
ComputeRequiredDeposit(long, decimal, int, int, MarketSide, long)
The deposit needed to receive at least wantAmount — the exact inverse of
ComputeWantAmount(long, decimal, int, int, MarketSide, long).
public static long ComputeRequiredDeposit(long wantAmount, decimal price, int feeBps, int safetyBps = 50, MarketSide give = MarketSide.Base, long feeFlat = 0)
Parameters
wantAmountlongThe amount to receive, in atomic units of the received side.
pricedecimalThe market price, in quote atomic units per base atomic unit.
feeBpsintThe market's spread.
safetyBpsintThe maker's own cushion on top.
giveMarketSideWhich side the maker deposits.
feeFlatlongThe market's flat fee, in quote atomic units.
Returns
- long
The deposit, in atomic units of the side given; zero when the terms cannot be met.
Remarks
Rounds up, so the deposit never quotes short. Wanting nothing costs nothing, and that is checked before the flat fee goes back on — otherwise asking for zero would quote the flat fee's worth of deposit, and this would stop being an inverse.
ComputeWantAmount(long, decimal, int, int, MarketSide, long)
The amount a maker should ask for, having conceded the solver's terms.
public static long ComputeWantAmount(long depositAtomic, decimal price, int feeBps, int safetyBps = 50, MarketSide give = MarketSide.Base, long feeFlat = 0)
Parameters
depositAtomiclongWhat the maker funds, in atomic units of the side they give.
pricedecimalThe market price, in quote atomic units per base atomic unit.
feeBpsintThe market's spread.
safetyBpsintThe maker's own cushion on top; defaults to DefaultSafetyBps.
giveMarketSideWhich side the maker deposits. Giving base wants quote back, and vice versa.
feeFlatlongThe market's flat fee, in quote atomic units. Zero for none.
Returns
- long
The want amount, in atomic units of the side the maker receives; zero if the fees swallow it.
Remarks
want = floor(D · P · (1 − (fee_bps + safety_bps)/10⁴)) − flat. The spread applies to
the whole deposit and the flat fee is charged on top of it, rather than the spread applying
to what is left after the flat fee — that is the model the RFQ conformance rules use, so one
card prices the same through an offer and through a quote.
Rounding never favours the maker: the spread floors, and the flat fee's conversion (below) ceils. A maker who asks for a satoshi more than the terms allow is a maker whose offer no solver fills.
DiscoverMarketsAsync(string, IReadOnlyList<Uri>?, IReadOnlyList<SolverCard>?, CancellationToken)
Discover markets for network across one or more registries plus any local
cards. Registries whose version or network don't match are skipped; a stale index (generated
more than NArk.ArkadeIntents.Services.SolverDiscoveryService._stalenessThreshold ago) is used but warned about.
public Task<IReadOnlyList<IndexedMarket>> DiscoverMarketsAsync(string network, IReadOnlyList<Uri>? registries = null, IReadOnlyList<SolverCard>? localCards = null, CancellationToken cancellationToken = default)
Parameters
networkstringregistriesIReadOnlyList<Uri>localCardsIReadOnlyList<SolverCard>cancellationTokenCancellationToken
Returns
FetchIndexAsync(Uri, CancellationToken)
Fetch a per-network index, cached for NArk.ArkadeIntents.Services.SolverDiscoveryService._cacheTtl.
public Task<GetSolverRegistryResponse> FetchIndexAsync(Uri registryUrl, CancellationToken cancellationToken = default)
Parameters
registryUrlUriThe index to fetch.
cancellationTokenCancellationTokenCancels the fetch.
Returns
- Task<GetSolverRegistryResponse>
The index, freshly fetched or from cache.
Remarks
An expired entry is refreshed, but a refresh that fails falls back to it rather than propagating. A registry is a static file that changes when someone opens a pull request, so yesterday's copy is very nearly today's — while treating an unreachable host as "no solvers exist" turns a blip in someone else's CDN into a merchant who cannot be paid.
The fallback is bounded by what the caller does with GeneratedAt: DiscoverMarketsAsync(string, IReadOnlyList<Uri>?, IReadOnlyList<SolverCard>?, CancellationToken) warns past its staleness threshold. Failing with nothing cached still throws, because there is no answer to give.
Exceptions
- HttpRequestException
The fetch failed and nothing was cached.
FetchPriceAsync(SolverMarket, CancellationToken)
The market's price P, in quote atomic units per base atomic unit.
public Task<decimal> FetchPriceAsync(SolverMarket market, CancellationToken cancellationToken = default)
Parameters
marketSolverMarketThe market to price.
cancellationTokenCancellationTokenCancels the feed fetch.
Returns
Remarks
A same-asset market carries no feed fields at all and its price is identically 1 — that is the shape every corridor market between BTC and BTC has, so fetching unconditionally is how a client crashes on the first corridor entry it meets. A cross-asset market missing its feed is a malformed card instead, and says so.
Exceptions
- InvalidOperationException
The card declares no usable feed for a cross-asset market.
FilterAndRank(IEnumerable<IndexedMarket>, string, string, long, string?, string?)
Filter discovered markets to one corridor-qualified pair and a base amount inside the bounds, cheapest first at that size.
public static IReadOnlyList<IndexedMarket> FilterAndRank(IEnumerable<IndexedMarket> markets, string baseAssetId, string quoteAssetId, long baseAmount, string? baseCorridor = null, string? quoteCorridor = null)
Parameters
marketsIEnumerable<IndexedMarket>The discovered markets.
baseAssetIdstringThe full canonical base id, or an old bare id for legacy-only markets.
quoteAssetIdstringThe quote id in the same identity format.
baseAmountlongThe size being traded, in base atomic units.
baseCorridorstringThe base side's rail; defaults to arkade.
quoteCorridorstringThe quote side's rail; defaults to arkade.
Returns
- IReadOnlyList<IndexedMarket>
The matching markets, cheapest first.
Remarks
Identity is the corridor-qualified leg pair, never the ticker and no longer the bare id pair:
a solver's BTC/lightning:BTC and BTC/onchain:BTC are both btc-against-btc, and
matching on ids alone would offer a maker either one for a request naming a rail.
Ranking is by the total fee at baseAmount, not by fee_bps: a market
with a lower spread and a flat fee is dearer at small sizes and cheaper at large ones, so the
spread alone puts them in the wrong order at one end or the other.
NormalizePrice(decimal, int)
Normalize a raw feed scalar: divide by 10^priceDecimals.
public static decimal NormalizePrice(decimal raw, int priceDecimals)
Parameters
Returns
- decimal
The price in quote atomic units per base atomic unit.
Remarks
There is no inversion step. A feed is always advertised in base/quote terms, so a market needing the other direction advertises the other feed.
RegistryFor(string)
The default registry index URL for a network name (bitcoin/signet/mutinynet).
public static Uri RegistryFor(string network)
Parameters
networkstring
Returns
ResolveJsonPointer(JsonNode, string)
Resolve an RFC 6901 JSON Pointer (e.g. "/price", "/data/0/px") against a JSON tree.
public static JsonNode ResolveJsonPointer(JsonNode root, string pointer)