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
transportIClientTransportThe Arkade server connection.
contractServiceIContractServiceDerives and imports contracts.
spendingServiceISpendingServiceBuilds and submits the spends.
intentStorageIArkadeIntentStorageWhere swaps are recorded.
contractStorageIContractStorageWhere a funded lockup is read back from.
vtxoStorageIVtxoStorageThe chain view a lockup's outputs are found in.
walletProviderIWalletProviderSigns, and anchors the derived preimage.
cipherIAesGcmCipherAES-GCM for the claim packet. Defaults to the platform's, which is right everywhere but a browser — see IAesGcmCipher.
blockchainIBitcoinBlockchainOptional. Supplied, a refund matures on the chain's own median time past; absent, it waits out the worst-case lag instead.
optionsIOptions<ArkadeIntentsOptions>Corridor settings: the covenant co-signer override, and the ceiling on what a receive quote may bill the payer.
timeTimeProviderClock for the deadline comparisons; defaults to the system clock.
loggerILogger<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
swapIdstringThe negotiation's correlation id.
cancellationTokenCancellationTokenCancels 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
swapIdstringThe recorded receive swap, including its stored preimage for watch-only wallets.
cancellationTokenCancellationTokenCancels 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
walletIdstringThe wallet taking delivery.
amountSatslongThe size to ask for, in sats — of the leg
amountSidenames.rfqTransportIRfqTransportHow to reach the solver.
covclaimdPubKeystringcovclaimd'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
nullwhen there is no covclaimd, in which case no packet is sent at all and the claim is this client's own to make.solverCardSolverCardThe 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.
amountSideRfqAmountSideWhich leg
amountSatspins, 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.cancellationTokenCancellationTokenCancels 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
walletIdstringWallet owning the receiver key and recovery state.
amountSatslongExact Arkade amount required by the outgoing lock.
rfqTransportIRfqTransportHow to reach the ingress solver.
covclaimdPubKeystringcovclaimd's key for the claim packet, or
nullto send none.secretSwapLinkSecretThe outgoing route's client-owned secret.
payoutAddressArkAddressThe already-verified outgoing Arkade lock L.
receiverContractArkContractA wallet-owned contract supplying M's receiver key.
outgoingSwapIdstringAlready-persisted outgoing RFQ id; exact linkage is retained for automatic claims.
solverCardSolverCardOptional published ingress terms.
rfqIdstringCaller-reserved global RFQ identity, or null to generate one.
cancellationTokenCancellationTokenCancels 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
contractVHTLCv2ContractThe rebuilt lockup contract.
serverKeyECXOnlyPubKeyThe 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
nonInteractiveRefundleaf, 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
swapIdstringThe swap.
cancellationTokenCancellationTokenCancels 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
swapIdstringThe recorded outgoing swap; its funded contract must include the ninth leaf.
cancellationTokenCancellationTokenCancels 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
swapIdstringcancellationTokenCancellationToken
Returns
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
walletIdstringThe wallet paying the invoice and receiving any refund.
invoicestringThe BOLT11 to pay.
rfqTransportIRfqTransportHow to reach the solver.
solverCardSolverCardcancellationTokenCancellationTokenCancels 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.