Class NostrRfqTransport
- Namespace
- NArk.ArkadeIntents.Rfq
- Assembly
- NArk.ArkadeIntents.dll
The RFQ transport the protocol actually specifies: NIP-01 over a relay, addressed by pubkey.
public sealed class NostrRfqTransport : IRfqTransport, IDisposable
- Inheritance
-
NostrRfqTransport
- Implements
- Inherited Members
Remarks
Both parties dial out and neither listens, which is what lets a solver run with no inbound port and no DNS name. It is also the only way to use the registry's rendezvous data, since a corridor card carries a discovery pubkey and relays rather than a URL.
Each negotiation uses a fresh identity key by default, which keeps separate swaps unlinkable to
the relay operator and is also what keeps a busy client quotable: solvers meter quote creation
per requester identity — the author key, on a relay — so one stable key spends a shared quota
and starts drawing rate_limited refusals. A stable key costs one fewer ECDH per
negotiation, which is the dominant per-message cost here, and is worth passing only where the
traffic is low enough that the quota is not the binding constraint.
Constructors
NostrRfqTransport(IReadOnlyList<Uri>, string, Key?, TimeSpan?, ILogger<NostrRfqTransport>?)
Creates the transport against every relay the solver's card advertises.
public NostrRfqTransport(IReadOnlyList<Uri> relays, string solverPubkey, Key? identity = null, TimeSpan? timeout = null, ILogger<NostrRfqTransport>? logger = null)
Parameters
relaysIReadOnlyList<Uri>The rendezvous set, in the card's own order. Each is dialled and each carries the same request.
solverPubkeystringThe solver's x-only key, hex — its address on the relay.
identityKeyOur own key. Omit for a fresh one per negotiation, which is the more private default.
timeoutTimeSpan?How long to wait for a reply before giving up. Defaults to 30s.
loggerILogger<NostrRfqTransport>Optional logger; relay-level faults are reported here.
Remarks
Every relay is dialled at once and the first valid reply wins; the rest are torn down. That is not an optimisation but the point of a relay SET: a rendezvous is a place both parties happen to be, and neither side controls which of the card's relays the other is actually connected to at this moment. Dialling one and waiting is a coin flip dressed up as a protocol.
The same event goes to all of them — same id, same signature — so a solver connected to several sees duplicates of one request rather than several requests. The negotiation id makes that idempotent on its side.
Exceptions
- ArgumentException
No relays, or a malformed solver key.
NostrRfqTransport(Uri, string, Key?, TimeSpan?, ILogger<NostrRfqTransport>?)
Creates the transport against a single relay.
public NostrRfqTransport(Uri relay, string solverPubkey, Key? identity = null, TimeSpan? timeout = null, ILogger<NostrRfqTransport>? logger = null)
Parameters
relayUriThe relay to dial, e.g.
wss://relay.example.solverPubkeystringThe solver's x-only key, hex — its address on the relay.
identityKeyOur own key. Omit for a fresh one per negotiation, which is the more private default.
timeoutTimeSpan?How long to wait for a reply before giving up. Defaults to 30s.
loggerILogger<NostrRfqTransport>Optional logger; relay-level faults are reported here.
Remarks
A convenience over the relay-set constructor. A card carries a LIST, and dialling one entry of it makes a single operator's outage look exactly like a solver that declined to answer — so prefer passing everything the card advertises.
Fields
BroadcastKind
Open-RFQ broadcasts. Same range, same retention: none.
public const int BroadcastKind = 24860
Field Value
DirectedKind
Directed RFQ traffic. In NIP-01's ephemeral range, so relays forward it without storing it. Provisional, per the protocol spec.
public const int DirectedKind = 24859
Field Value
Remarks
The range matters to how this transport is written. Nothing is retained, so there is no backlog to catch up from — a subscription that is not already live when the reply is published misses it outright. Hence subscribing before publishing rather than after.
Methods
Dispose()
Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
public void Dispose()
Remarks
Cancels any negotiation still waiting, so it ends as TransportClosedException rather than sitting out the full timeout and then reporting a solver that never failed.
ForCard(SolverCard, Key?, TimeSpan?, ILogger<NostrRfqTransport>?)
Build a transport from a solver's registry card, using every relay it advertises.
public static NostrRfqTransport ForCard(SolverCard card, Key? identity = null, TimeSpan? timeout = null, ILogger<NostrRfqTransport>? logger = null)
Parameters
cardSolverCardThe card, as published or pinned locally.
identityKeyOur own key. Omit for a fresh one per negotiation.
timeoutTimeSpan?How long to wait for a reply. Defaults to 30s.
loggerILogger<NostrRfqTransport>Optional logger.
Returns
- NostrRfqTransport
A transport addressed to the card's discovery key over its whole relay set.
Remarks
The card is where a relay set comes from — a corridor card is REQUIRED to carry
discovery_pubkey and transports, because its rendezvous is live data a maker
will actually contact. Reading the list and then dialling one entry of it, which is what a
caller writing this by hand tends to do, turns one operator's outage into what looks like a
solver refusing to quote.
Non-wss:// entries are dropped rather than dialled. The registry's own schema admits
only wss://, so a plaintext entry is either a malformed card or a downgrade someone
wants us to accept — and this traffic is sealed to the solver's key but not to the relay's,
so who carries it is still worth being strict about. Duplicates are collapsed: a card listing
the same relay twice should cost one connection, not two.
Exceptions
- ArgumentException
The card carries no discovery key, or no relay this client will dial.
GetStatusAsync<TStatusProfile>(string, CancellationToken)
Ask the solver where a negotiation stands.
public Task<RfqStatus<TStatusProfile>?> GetStatusAsync<TStatusProfile>(string rfqId, CancellationToken cancellationToken = default)
Parameters
rfqIdstringThe correlation id.
cancellationTokenCancellationTokenCancels the round trip.
Returns
- Task<RfqStatus<TStatusProfile>>
The status, or
nullwhen the solver has no negotiation under that id.
Type Parameters
TStatusProfileThe corridor's status-profile shape.
Remarks
Best-effort by design — never the money path. A funded swap is observable on-chain whether or not the solver answers.
RequestEvmSendQuoteAsync(EvmSendRfqRequest, CancellationToken)
Asks for an Arkade-to-ERC20 quote using its current numeric-sats wire shape.
public Task<RfqQuote<EvmSendQuoteProfile>> RequestEvmSendQuoteAsync(EvmSendRfqRequest request, CancellationToken cancellationToken = default)
Parameters
requestEvmSendRfqRequestStrict exact-input EVM send request.
cancellationTokenCancellationTokenCancels the round trip.
Returns
- Task<RfqQuote<EvmSendQuoteProfile>>
The solver's binding terms.
RequestQuoteAsync<TRequestProfile, TQuoteProfile>(RfqRequest<TRequestProfile>, CancellationToken)
Ask for a quote and return the solver's binding terms.
public Task<RfqQuote<TQuoteProfile>> RequestQuoteAsync<TRequestProfile, TQuoteProfile>(RfqRequest<TRequestProfile> request, CancellationToken cancellationToken = default)
Parameters
requestRfqRequest<TRequestProfile>The request payload; strict, so it must carry no extra fields.
cancellationTokenCancellationTokenCancels the round trip.
Returns
Type Parameters
TRequestProfileThe corridor's request-profile shape.
TQuoteProfileThe corridor's quote-profile shape.
Exceptions
- RfqRefusedException
The solver declined, with a reason from the closed set.
- InvalidOperationException
The reply was neither a quote nor a refusal, or answered a different negotiation.