Table of Contents

Class LightningIntentsClient

Namespace
NArk.ArkadeIntents.Lightning
Assembly
NArk.ArkadeIntents.dll

Both Lightning corridors: paying a BOLT11 out of an Arkade balance, and being paid over Lightning into one.

public sealed class LightningIntentsClient : ILightningIngressQuoteClient
Inheritance
LightningIntentsClient
Implements
Inherited Members

Remarks

One class in three files, because the two directions are one mechanism seen from either end. The covenant, the leaves, the checks and the bookkeeping are the same; what differs is who occupies which role, and that difference is worth a file name rather than a second type.

They were two types, and the cost showed up twice in one day: nine of their eleven dependencies were identical, so every parameter added to one had to be added to the other, and both times a hand-written construction site somewhere silently took the new argument in an old position. One constructor cannot drift from itself.

See LightningIntentsClient.Send.cs and LightningIntentsClient.Receive.cs for each direction.

Constructors

LightningIntentsClient(IClientTransport, IContractService, ISpendingService, IArkadeIntentStorage, IContractStorage, IVtxoStorage, IWalletProvider, IAesGcmCipher?, IBitcoinBlockchain?, IOptions<ArkadeIntentsOptions>?, TimeProvider?, ILogger<LightningIntentsClient>?)

Creates the client.

public LightningIntentsClient(IClientTransport transport, IContractService contractService, ISpendingService spendingService, IArkadeIntentStorage intentStorage, IContractStorage contractStorage, IVtxoStorage vtxoStorage, IWalletProvider walletProvider, IAesGcmCipher? cipher = null, IBitcoinBlockchain? blockchain = null, IOptions<ArkadeIntentsOptions>? options = null, TimeProvider? time = null, ILogger<LightningIntentsClient>? logger = null)

Parameters

transport IClientTransport

The Arkade server connection.

contractService IContractService

Derives and imports contracts.

spendingService ISpendingService

Builds and submits the spends.

intentStorage IArkadeIntentStorage

Where swaps are recorded.

contractStorage IContractStorage

Where a funded lockup is read back from.

vtxoStorage IVtxoStorage

The chain view a lockup's outputs are found in.

walletProvider IWalletProvider

Signs, and anchors the derived preimage.

cipher IAesGcmCipher

AES-GCM for the claim packet. Defaults to the platform's, which is right everywhere but a browser — see IAesGcmCipher.

blockchain IBitcoinBlockchain

Optional. Supplied, a refund matures on the chain's own median time past; absent, it waits out the worst-case lag instead.

options IOptions<ArkadeIntentsOptions>

Corridor settings: the covenant co-signer override, and the ceiling on what a receive quote may bill the payer.

time TimeProvider

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

logger ILogger<LightningIntentsClient>

Optional logger.

Methods

ClaimAsync(string, CancellationToken)

Take delivery cooperatively using the receiver's wallet signer; linked routes require exact funding before revealing the preimage.

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

Parameters

swapId string

The negotiation's correlation id.

cancellationToken CancellationToken

Cancels before the spend; after it the claim is live regardless.

Returns

Task<ArkadeSwapIntent>

The updated intent.

Remarks

This both takes delivery and pays the solver: the preimage becomes public in the witness, and that is what lets the held invoice settle. So it is not an optional tidy-up — a swap left unclaimed past refund_locktime is one where the solver reclaims its lockup and the payer's money was never earned.

Exceptions

InvalidOperationException

No such swap, the wrong direction, nothing funded yet, or the solver's reclaim window has already opened.

ClaimNonInteractiveAsync(string, CancellationToken)

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

public Task<ArkadeSwapIntent> ClaimNonInteractiveAsync(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.

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

Negotiate a receive swap and verify everything the solver sent back.

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 the solver.

covclaimdPubKey string

covclaimd's compressed key, read live from its own endpoint, so the preimage can be sealed to it and the claim pushed while this client is offline — or null when there is no covclaimd, in which case no packet is sent at all and the claim is this client's own to make.

solverCard SolverCard

The solver's published card, when there is one. Supplying it holds the solver to its own advertised limits and fee. Omitting it is not a check skipped but one that does not apply — a deployment naming a solver outright has no published terms to hold it to.

amountSide RfqAmountSide

Which leg amountSats pins, and so who absorbs the solver's spread. To fixes what lands on Arkade and bills the payer more; From fixes the payer's bill and nets the spread out of the payout. Defaults to To, which is what a caller asking to "receive N sats" means. A merchant minting an invoice for an order total wants From: a LUD-06 wallet checks the invoice against the amount its user approved and refuses anything larger.

cancellationToken CancellationToken

Cancels the negotiation.

Returns

Task<PendingLightningReceive>

The invoice to be paid, and everything needed to claim once it is.

Exceptions

RfqRefusedException

The solver declined to quote.

LightningReceiveNotUsableException

The quote did not survive the client's own checks.

LockupAddressMismatchException

The solver's address is not ours.

ReceiveFromLightningIntoAsync(string, long, IRfqTransport, string?, SwapLinkSecret, ArkAddress, ArkContract, string, SolverCard?, string?, CancellationToken)

Negotiates a receive whose non-interactive claim funds another Arkade swap.

public Task<PendingLightningReceive> ReceiveFromLightningIntoAsync(string walletId, long amountSats, IRfqTransport rfqTransport, string? covclaimdPubKey, SwapLinkSecret secret, ArkAddress payoutAddress, ArkContract receiverContract, string outgoingSwapId, SolverCard? solverCard = null, string? rfqId = null, CancellationToken cancellationToken = default)

Parameters

walletId string

Wallet owning the receiver key and recovery state.

amountSats long

Exact Arkade amount required by the outgoing lock.

rfqTransport IRfqTransport

How to reach the ingress solver.

covclaimdPubKey string

covclaimd's key for the claim packet, or null to send none.

secret SwapLinkSecret

The outgoing route's client-owned secret.

payoutAddress ArkAddress

The already-verified outgoing Arkade lock L.

receiverContract ArkContract

A wallet-owned contract supplying M's receiver key.

outgoingSwapId string

Already-persisted outgoing RFQ id; exact linkage is retained for automatic claims.

solverCard SolverCard

Optional published ingress terms.

rfqId string

Caller-reserved global RFQ identity, or null to generate one.

cancellationToken CancellationToken

Cancels before the quote is published.

Returns

Task<PendingLightningReceive>

The Lightning invoice and verified M covenant whose claim is pinned to L.

Remarks

This creates a second, independent RFQ after the outgoing quote. It reuses only H. Claiming M publishes P while creating L, before an EVM lock is necessarily visible; callers must enforce the downstream solver policy and must never treat the ingress claim as settlement.

RefundAddressOf(VHTLCv2Contract, ECXOnlyPubKey)

The maker's own payout address, as committed when the swap was funded.

public static ArkAddress RefundAddressOf(VHTLCv2Contract contract, ECXOnlyPubKey serverKey)

Parameters

contract VHTLCv2Contract

The rebuilt lockup contract.

serverKey ECXOnlyPubKey

The Arkade server key the address is derived against.

Returns

ArkAddress

Where a refund of this swap pays.

Remarks

Taken from the script the nonInteractiveRefund covenant pins its payout to. That leaf is not the one we spend through, but it is where the destination was committed at funding time, so reading it back is what keeps every refund path — ours and the solver's — paying the same place.

Exceptions

InvalidOperationException

The lockup carries no nonInteractiveRefund leaf, so it never committed to a destination for this to read back.

RefundIfUnresolvedAsync(string, CancellationToken)

The outputs a refund may spend: every live output sitting at the lockup.

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

All of them, not the first. A lockup can hold more than one output — a retried funding, or a counterparty that split it — and refunding one would leave the rest behind with no second path to them: the leaf that made this possible is ours alone, and nothing else will come looking. There is no amount gate here, unlike a claim: a refund publishes no secret and pays an address the covenant already committed to, so taking more than was quoted costs nobody anything and taking less strands it.

Swept outputs are named rather than skipped. Silently filtering them turns "the operator swept your deposit, recover it elsewhere" into "there is nothing here", which is the same sentence a wallet uses for an empty address.

Exceptions

InvalidOperationException

Nothing live is left at the lockup.

InvalidOperationException

No such swap, or the wrong corridor.

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.

RefundSwap(string, CancellationToken)

Refunds an outgoing Lightning, onchain or EVM corridor cooperatively with the sender's wallet signer after maturity.

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

Parameters

swapId string
cancellationToken CancellationToken

Returns

Task<ArkadeSwapIntent>

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

Negotiate, derive locally, verify, gate and fund — the whole maker flow in one call.

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

Parameters

walletId string

The wallet paying the invoice and receiving any refund.

invoice string

The BOLT11 to pay.

rfqTransport IRfqTransport

How to reach the solver.

solverCard SolverCard
cancellationToken CancellationToken

Cancels before funding; after funding the swap is live regardless.

Returns

Task<FundedLightningSend>

The funded swap.

Exceptions

RfqRefusedException

The solver declined to quote.

LockupAddressMismatchException

The solver's address is not ours — nothing was funded.

LightningSendNotFundableException

A safety gate refused — nothing was funded.