Table of Contents

Class ArkadeIntentsService

Namespace
NArk.ArkadeIntents.Services
Assembly
NArk.ArkadeIntents.dll

One entry point for every kind of Arkade intent swap.

public sealed class ArkadeIntentsService
Inheritance
ArkadeIntentsService
Inherited Members

Remarks

The corridors are genuinely different underneath — an asset swap settles against an offer on the stream, the Lightning legs negotiate by RFQ against a covenant — but they all end up as the same ArkadeSwapIntent and are all watched by the same monitor. Callers should not have to know which of three classes owns a given swap in order to list it, or to do the obvious thing to it.

The part that is more than a facade is AdvanceAsync(string, CancellationToken). The monitor already moves a swap to Claimable or Refundable, but until now nothing acted on that — the status was a fact with no consequence, and the consequence is where the money is. This closes that loop while refusing to guess: see ArkadeIntentPolicy for the line between something that follows and something that is the caller's call.

Constructors

ArkadeIntentsService(AssetIntentsManager, LightningIntentsClient, IArkadeIntentStorage, IVtxoStorage, IClientTransport, OnchainIntentsClient?, TimeProvider?, ILogger<ArkadeIntentsService>?)

Creates the service.

public ArkadeIntentsService(AssetIntentsManager assets, LightningIntentsClient lightning, IArkadeIntentStorage intentStorage, IVtxoStorage vtxoStorage, IClientTransport transport, OnchainIntentsClient? onchain = null, TimeProvider? time = null, ILogger<ArkadeIntentsService>? logger = null)

Parameters

assets AssetIntentsManager

The asset-swap corridors.

lightning LightningIntentsClient

Both Lightning corridors.

intentStorage IArkadeIntentStorage

Where every kind of swap is recorded.

vtxoStorage IVtxoStorage

The chain view reconciliation compares against.

transport IClientTransport
onchain OnchainIntentsClient

The off-board corridor, or null. Absent, an off-board swap's L1 leg is never acted on — which the advance pass reports rather than hides.

time TimeProvider

Clock for the timelock comparisons; defaults to the system clock.

logger ILogger<ArkadeIntentsService>

Optional logger.

Methods

AdvanceAllAsync(string?, CancellationToken)

Advance every swap that calls for it.

public Task<IReadOnlyList<ArkadeIntentAdvance>> AdvanceAllAsync(string? walletId = null, CancellationToken cancellationToken = default)

Parameters

walletId string

Narrow to one wallet.

cancellationToken CancellationToken

Cancels between swaps.

Returns

Task<IReadOnlyList<ArkadeIntentAdvance>>

One result per swap that needed something, in the order attempted.

Remarks

Both statuses this acts on are time-bounded — a claim window closes, a refund competes with nothing but is still money left lying about — so this is meant to be run on a timer, not once.

AdvanceAsync(string, CancellationToken)

Do whatever this swap's kind and status call for, if anything.

public Task<ArkadeIntentAdvance> AdvanceAsync(string swapId, CancellationToken cancellationToken = default)

Parameters

swapId string

The swap.

cancellationToken CancellationToken

Cancels before the spend.

Returns

Task<ArkadeIntentAdvance>

What was decided and whether it ran.

Remarks

Failures are returned rather than thrown, because the useful caller is a loop over many swaps and one that cannot proceed must not stop the others. A swap that needs nothing comes back with None and Acted: false, which is a normal answer rather than a problem. Composition-owned swaps are also returned as no-ops: ComposedSwapExecutionClient owns their recovery, refund, and validated non-interactive transitions under the route lock.

Exceptions

InvalidOperationException

No such swap.

CancelAssetSwapAsync(string, CancellationToken)

Cancel a pending asset swap and take the deposit back.

public Task<ArkadeSwapIntent> CancelAssetSwapAsync(string swapId, CancellationToken cancellationToken = default)

Parameters

swapId string

The swap to cancel.

cancellationToken CancellationToken

Cancels before spending.

Returns

Task<ArkadeSwapIntent>

The updated intent.

Remarks

Deliberately not something AdvanceAsync(string, CancellationToken) will ever do on its own: a pending swap is waiting to be filled, which is what was asked for.

ClaimLightningReceiveAsync(string, CancellationToken)

Claim a funded Lightning receive swap, publishing the preimage.

public Task<ArkadeSwapIntent> ClaimLightningReceiveAsync(string swapId, CancellationToken cancellationToken = default)

Parameters

swapId string

The swap to claim.

cancellationToken CancellationToken

Cancels before the spend.

Returns

Task<ArkadeSwapIntent>

The updated intent.

Remarks

Exposed directly for the same reason as the refund, and with more urgency: the window closes when the solver's own reclaim path opens, so a caller who wants to take delivery now should not have to go through a sweep to do it.

ClaimLightningReceiveNonInteractiveAsync(string, CancellationToken)

Claims a funded receive through the emulator without a wallet signer, paying the covenant's pinned destination.

public Task<ArkadeSwapIntent> ClaimLightningReceiveNonInteractiveAsync(string swapId, CancellationToken cancellationToken = default)

Parameters

swapId string

The recorded receive swap, including its stored preimage for watch-only wallets.

cancellationToken CancellationToken

Cancels before submission; submission reveals the preimage.

Returns

Task<ArkadeSwapIntent>

The fulfilled intent after the emulator submits the claim.

Remarks

Linked routes require exact funding and validated H/M-to-L linkage; submission discloses P even if it fails.

ClaimOnchainReceiveAsync(string, CancellationToken)

Claim a funded on-board, publishing the preimage.

public Task<ArkadeSwapIntent> ClaimOnchainReceiveAsync(string swapId, CancellationToken cancellationToken = default)

Parameters

swapId string

The swap to claim.

cancellationToken CancellationToken

Cancels before the spend.

Returns

Task<ArkadeSwapIntent>

The updated intent.

Remarks

Exposed directly for the same reason the Lightning claim is, and with the same urgency: the window closes when the solver's own reclaim opens.

ClaimOnchainReceiveNonInteractiveAsync(string, CancellationToken)

Claims the funded Arkade side of an on-board without a wallet signer, through its pinned covenant payout.

public Task<ArkadeSwapIntent> ClaimOnchainReceiveNonInteractiveAsync(string swapId, CancellationToken cancellationToken = default)

Parameters

swapId string

The recorded on-board, including its stored preimage for watch-only wallets.

cancellationToken CancellationToken

Cancels before submission; submission reveals the preimage.

Returns

Task<ArkadeSwapIntent>

The fulfilled intent after the emulator submits the claim.

Remarks

Linked routes require exact funding and validated H/M-to-L linkage; submission discloses P even if it fails.

CreateAssetSwapAsync(CreateSwapRequest, CancellationToken)

Deposit BTC for an Arkade asset, or the reverse.

public Task<ArkadeSwapIntent> CreateAssetSwapAsync(CreateSwapRequest request, CancellationToken cancellationToken = default)

Parameters

request CreateSwapRequest

The swap to offer.

cancellationToken CancellationToken

Cancels before funding.

Returns

Task<ArkadeSwapIntent>

The recorded intent.

GetAsync(string, CancellationToken)

One swap by id, whatever corridor it belongs to.

public Task<ArkadeSwapIntent?> GetAsync(string swapId, CancellationToken cancellationToken = default)

Parameters

swapId string

The correlation id.

cancellationToken CancellationToken

Cancels the query.

Returns

Task<ArkadeSwapIntent>

The swap, or null.

GetSolverStatusAsync<TStatusProfile>(string, IRfqTransport, CancellationToken)

Ask the solver where it thinks a negotiation stands.

public Task<RfqStatus<TStatusProfile>?> GetSolverStatusAsync<TStatusProfile>(string swapId, IRfqTransport rfqTransport, CancellationToken cancellationToken = default)

Parameters

swapId string

The correlation id the negotiation was opened under.

rfqTransport IRfqTransport

How to reach the solver.

cancellationToken CancellationToken

Cancels the round trip.

Returns

Task<RfqStatus<TStatusProfile>>

The solver's view, or null when it has no record.

Type Parameters

TStatusProfile

The corridor's status-profile shape.

Remarks

Diagnostic only, and never the money path — a funded swap is observable on-chain whether or not the solver answers, which is why nothing here acts on the reply. What it adds is the one thing the chain cannot express: WHY nothing has happened. Refused, expired and not-yet all look identical from our side, and only the counterparty can tell them apart.

ListAsync(ArkadeSwapIntentStatus?, string?, CancellationToken)

Every swap, whatever corridor it belongs to.

public Task<IReadOnlyCollection<ArkadeSwapIntent>> ListAsync(ArkadeSwapIntentStatus? status = null, string? walletId = null, CancellationToken cancellationToken = default)

Parameters

status ArkadeSwapIntentStatus?

Narrow to one status.

walletId string

Narrow to one wallet.

cancellationToken CancellationToken

Cancels the query.

Returns

Task<IReadOnlyCollection<ArkadeSwapIntent>>

The matching swaps.

ReadLockupFateAsync(string, CancellationToken)

Read what the chain says became of a swap's lockup, without acting on it.

public Task<LockupFateResult> ReadLockupFateAsync(string swapId, CancellationToken cancellationToken = default)

Parameters

swapId string

The swap.

cancellationToken CancellationToken

Cancels the lookups.

Returns

Task<LockupFateResult>

The verdict, with its proof or its stuck outputs.

Remarks

Corridor-neutral: all four HTLC corridors settle into the same covenant, so what a spend of it means does not depend on which one negotiated the swap.

Exceptions

InvalidOperationException

No such swap, or it records no payment hash.

ReceiveFromLightningAsync(string, long, IRfqTransport, string?, SolverCard?, RfqAmountSide, CancellationToken)

Be paid over Lightning and take delivery on Arkade.

public Task<PendingLightningReceive> ReceiveFromLightningAsync(string walletId, long amountSats, IRfqTransport rfqTransport, string? covclaimdPubKey, SolverCard? solverCard = null, RfqAmountSide amountSide = RfqAmountSide.To, CancellationToken cancellationToken = default)

Parameters

walletId string

The wallet taking delivery.

amountSats long

The size to ask for, in sats — of the leg amountSide names.

rfqTransport IRfqTransport

How to reach a solver.

covclaimdPubKey string

covclaimd's key, read live, or null to send no claim packet.

solverCard SolverCard

The solver's published card, when there is one.

amountSide RfqAmountSide

Which leg amountSats pins, and so who absorbs the solver's spread — what lands on Arkade (To, the default) or what the payer is billed (From). A merchant minting an invoice for an order total wants the latter.

cancellationToken CancellationToken

Cancels the negotiation.

Returns

Task<PendingLightningReceive>

The invoice to hand to a payer, and what is needed to claim.

ReceiveFromOnchainAsync(string, long, IRfqTransport, string?, BitcoinAddress, RfqAmountSide, SolverCard?, ArkContract?, CancellationToken)

On-board Bitcoin L1 sats into an Arkade balance.

public Task<PendingOnchainReceive> ReceiveFromOnchainAsync(string walletId, long amountSats, IRfqTransport rfqTransport, string? covclaimdPubKey, BitcoinAddress l1RefundAddress, RfqAmountSide amountSide = RfqAmountSide.From, SolverCard? solverCard = null, ArkContract? payoutContract = null, CancellationToken cancellationToken = default)

Parameters

walletId string

The wallet taking delivery.

amountSats long

The size to ask for, on the leg amountSide names.

rfqTransport IRfqTransport

How to reach a solver.

covclaimdPubKey string

covclaimd's key, read live, or null to send no claim packet.

l1RefundAddress BitcoinAddress

Where the L1 HTLC pays if it has to be taken back.

amountSide RfqAmountSide

Which leg amountSats pins, and so who absorbs the solver's spread — what we send on L1 (From, the default) or what lands on Arkade.

solverCard SolverCard

The solver's published card, when there is one.

payoutContract ArkContract

A contract to take the payout key from instead of deriving a fresh one, so the swap costs no HD index of its own. See the corridor's own remarks for why that matters to recovery.

cancellationToken CancellationToken

Cancels the negotiation.

Returns

Task<PendingOnchainReceive>

The L1 address to fund, and what is needed to claim afterwards.

Remarks

Funds nothing itself: the L1 funding transaction belongs to the caller's own Bitcoin wallet, since the sats being on-boarded are by definition not on Arkade yet.

Exceptions

InvalidOperationException

No onchain corridor is registered.

ReconcileAsync(string?, CancellationToken)

Re-derive every open swap's status from the chain, and report what was behind.

public Task<ArkadeReconciliation> ReconcileAsync(string? walletId = null, CancellationToken cancellationToken = default)

Parameters

walletId string

Narrow to one wallet.

cancellationToken CancellationToken

Cancels between swaps.

Returns

Task<ArkadeReconciliation>

What was corrected, and what is still unconfirmed.

Remarks

The monitor only ever reacts to changes it is present for, so anything that happened while this process was down is missed permanently — a claim window can open and close in that gap. Run this at startup, before the first AdvanceAllAsync(string?, CancellationToken), or the sweep acts on a picture that stopped being true when the process did.

RefundIfUnresolvedAsync(string, CancellationToken)

Resolve an unfinished send swap: read what the chain says, and refund only if nothing else already ended it.

public Task<RefundOutcome> RefundIfUnresolvedAsync(string swapId, CancellationToken cancellationToken = default)

Parameters

swapId string

The swap.

cancellationToken CancellationToken

Cancels before the spend.

Returns

Task<RefundOutcome>

What was found and what was done about it.

Remarks

The recovery entry point, as distinct from RefundLightningSendAsync(string, CancellationToken), which is the action. Use this one when the local picture may be stale — after downtime, or from a wallet whose rows were rebuilt: pushing a refund at a lockup the counterparty already claimed is a wasted fee, and reading the fate first is what makes that decidable without trusting anyone's account of it.

Covers both send legs. The on-board is not among them and cannot be — it never funded an Arkade covenant, so its recourse is RefundOnchainReceiveAsync(string, BitcoinAddress?, CancellationToken) on L1.

RefundLightningSendAsync(string, CancellationToken)

Push a Lightning send swap's refund back to our own address.

public Task<ArkadeSwapIntent> RefundLightningSendAsync(string swapId, CancellationToken cancellationToken = default)

Parameters

swapId string

The swap to refund.

cancellationToken CancellationToken

Cancels before the spend.

Returns

Task<ArkadeSwapIntent>

The updated intent.

Remarks

Takes refundWithoutReceiver: our key plus the Arkade server, once refund_locktime has passed. Of the covenant's several refund leaves it is the only one a client can start on its own, and that is worth being precise about rather than discovering at the moment it is needed:

  • refund (immediate) and refundWithoutServer (after a CSV) both need the solver's signature, and the RFQ protocol carries no message asking for one — the solver may push a refund of its own accord, but that is an operator action on its side, not something we can request.
  • nonInteractiveRefund is the solver's to push, not ours.
  • unilateralRefundWithoutReceiver needs nobody, but reaching it means unrolling the VTXO to the chain, and an eight-leaf covenant lands there carrying enough script data that the exit costs more than it recovers.

So this is the recourse, and it is exposed directly rather than only through AdvanceAsync(string, CancellationToken), because wanting the money back now is the caller's call to make at whatever moment they like — a sweep on a timer is a convenience, not the only way in.

RefundNonInteractiveAsync(string, CancellationToken)

Refunds an outgoing Lightning, onchain or EVM corridor through its ninth covenant leaf without a wallet signer or preimage.

public Task<ArkadeSwapIntent> RefundNonInteractiveAsync(string swapId, CancellationToken cancellationToken = default)

Parameters

swapId string

The recorded outgoing swap; its funded contract must include the ninth leaf.

cancellationToken CancellationToken

Cancels before submission.

Returns

Task<ArkadeSwapIntent>

The cancelled intent after the emulator submits the pinned refund.

RefundOnchainReceiveAsync(string, BitcoinAddress?, CancellationToken)

Take back an on-board's L1 funding once its refund leaf has matured.

public Task<OnchainRefundOutcome> RefundOnchainReceiveAsync(string swapId, BitcoinAddress? refundAddress = null, CancellationToken cancellationToken = default)

Parameters

swapId string

The swap to refund.

refundAddress BitcoinAddress

Where to pay, overriding the address recorded at negotiation.

cancellationToken CancellationToken

Cancels before the broadcast.

Returns

Task<OnchainRefundOutcome>

What the attempt found; not refunding yet is an ordinary answer.

Remarks

The on-board's only recourse. Unlike the send corridors there is no Arkade covenant to refund — nothing of ours was ever funded there — so if the solver never delivers, this is the way the sats come home.

RestoreAssetSwapsAsync(string, IReadOnlyCollection<string>, CancellationToken)

Rebuild asset-swap rows from the chain after the store that held them is gone.

public Task<OfferRestoreResult> RestoreAssetSwapsAsync(string walletId, IReadOnlyCollection<string> candidateTxids, CancellationToken cancellationToken = default)

Parameters

walletId string

The wallet the rebuilt rows belong to.

candidateTxids IReadOnlyCollection<string>

Transactions worth looking at — a wallet's sent history, minus whatever an earlier pass already answered.

cancellationToken CancellationToken

Cancels between transactions.

Returns

Task<OfferRestoreResult>

What was rebuilt, what was answered, and what still has no outcome.

Remarks

The companion to ReconcileAsync(string?, CancellationToken) and not a substitute for it: reconciliation corrects rows that exist, and this one exists for when they do not. Run it after a wallet restore, then reconcile.

A restored swap can be watched and filled but not cancelled — the wire offer carries the maker's x-only key, which rebuilds the address and cannot sign. See OfferRestore.

SendToLightningAsync(string, string, IRfqTransport, SolverCard?, CancellationToken)

Pay a BOLT11 out of an Arkade balance.

public Task<FundedLightningSend> SendToLightningAsync(string walletId, string invoice, IRfqTransport rfqTransport, SolverCard? solverCard = null, CancellationToken cancellationToken = default)

Parameters

walletId string

The wallet paying.

invoice string

The BOLT11 to pay.

rfqTransport IRfqTransport

How to reach a solver.

solverCard SolverCard

The solver's published card, when there is one. Supplying it holds the solver to its own advertised limits and fee: a quote is whatever arrived on a socket, while the card is signed and tied to a discoverable identity, and only comparing the two catches a solver quoting differently from how it advertises. Omitting it is not a check skipped but a check that does not apply — a deployment naming one solver outright has no published terms to hold it to.

cancellationToken CancellationToken

Cancels before funding.

Returns

Task<FundedLightningSend>

The funded swap.

SendToOnchainAsync(string, BitcoinAddress, long, IRfqTransport, RfqAmountSide, SolverCard?, CancellationToken)

Off-board an Arkade balance to Bitcoin L1.

public Task<FundedOnchainSend> SendToOnchainAsync(string walletId, BitcoinAddress payoutAddress, long amountSats, IRfqTransport rfqTransport, RfqAmountSide amountSide = RfqAmountSide.To, SolverCard? solverCard = null, CancellationToken cancellationToken = default)

Parameters

walletId string

The wallet paying, and receiving any refund.

payoutAddress BitcoinAddress

The Bitcoin L1 address the off-board pays out to.

amountSats long

The size, on the leg amountSide names.

rfqTransport IRfqTransport

How to reach a solver.

amountSide RfqAmountSide

Which leg amountSats pins, and so who absorbs the solver's spread — what lands on L1 (To, the default) or what leaves the Arkade balance.

solverCard SolverCard

The solver's published card, when there is one.

cancellationToken CancellationToken

Cancels before funding.

Returns

Task<FundedOnchainSend>

The funded swap.

Remarks

The corridor's own entry point was reachable only by resolving OnchainIntentsClient directly, while its claim and refund were already driven through here — so the one facade that exists to spare callers knowing which class owns a swap could start every corridor but this one.

Exceptions

InvalidOperationException

No onchain corridor is registered.