Table of Contents

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

http HttpClient
logger ILogger<SolverDiscoveryService>

SolverDiscoveryService(HttpClient, TimeSpan, TimeSpan, ILogger<SolverDiscoveryService>?)

public SolverDiscoveryService(HttpClient http, TimeSpan cacheTtl, TimeSpan stalenessThreshold, ILogger<SolverDiscoveryService>? logger = null)

Parameters

http HttpClient
cacheTtl TimeSpan
stalenessThreshold TimeSpan
logger ILogger<SolverDiscoveryService>

Fields

DefaultSafetyBps

Suggested default client-side safety cushion, in basis points.

public const int DefaultSafetyBps = 50

Field Value

int

MainnetRegistry

Default per-network index URLs published by the reference registry.

public static readonly Uri MainnetRegistry

Field Value

Uri

MutinynetRegistry

public static readonly Uri MutinynetRegistry

Field Value

Uri

RegtestRegistry

public static readonly Uri RegtestRegistry

Field Value

Uri

SignetRegistry

public static readonly Uri SignetRegistry

Field Value

Uri

SupportedVersion

The highest supported discovery version; v0 remains supported.

public const int SupportedVersion = 1

Field Value

int

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

wantAmount long

The amount to receive, in atomic units of the received side.

price decimal

The market price, in quote atomic units per base atomic unit.

feeBps int

The market's spread.

safetyBps int

The maker's own cushion on top.

give MarketSide

Which side the maker deposits.

feeFlat long

The 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

depositAtomic long

What the maker funds, in atomic units of the side they give.

price decimal

The market price, in quote atomic units per base atomic unit.

feeBps int

The market's spread.

safetyBps int

The maker's own cushion on top; defaults to DefaultSafetyBps.

give MarketSide

Which side the maker deposits. Giving base wants quote back, and vice versa.

feeFlat long

The 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

network string
registries IReadOnlyList<Uri>
localCards IReadOnlyList<SolverCard>
cancellationToken CancellationToken

Returns

Task<IReadOnlyList<IndexedMarket>>

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

registryUrl Uri

The index to fetch.

cancellationToken CancellationToken

Cancels 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

market SolverMarket

The market to price.

cancellationToken CancellationToken

Cancels the feed fetch.

Returns

Task<decimal>

The normalized price.

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

markets IEnumerable<IndexedMarket>

The discovered markets.

baseAssetId string

The full canonical base id, or an old bare id for legacy-only markets.

quoteAssetId string

The quote id in the same identity format.

baseAmount long

The size being traded, in base atomic units.

baseCorridor string

The base side's rail; defaults to arkade.

quoteCorridor string

The 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

raw decimal

The scalar the feed served.

priceDecimals int

The market's declared exponent.

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

network string

Returns

Uri

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)

Parameters

root JsonNode
pointer string

Returns

JsonNode