Class OnchainIntentsClient
- Namespace
- NArk.ArkadeIntents.Onchain
- Assembly
- NArk.ArkadeIntents.dll
The arkade:BTC->onchain:BTC corridor: off-board an Arkade balance to Bitcoin L1.
public sealed class OnchainIntentsClient : IOnchainIngressQuoteClient
- Inheritance
-
OnchainIntentsClient
- Implements
- Inherited Members
Remarks
Two contracts, two chains, one secret. The client funds an Arkade covenant the solver can only take by publishing a preimage; the solver funds an L1 HTLC only that preimage releases. The client holds it, so the client is paid first and the solver is paid by the act of being paid.
Nothing here trusts the solver's account of anything. Both addresses are rebuilt locally and compared against the quote's rendering of them, the L1 funding is read off the chain rather than reported, and the preimage is derived from the wallet's own key so a lost record does not lose the claim.
Constructors
OnchainIntentsClient(IClientTransport, IContractService, ISpendingService, IArkadeIntentStorage, IContractStorage, IVtxoStorage, IWalletProvider, IBitcoinBlockchain, IAesGcmCipher?, IOptions<ArkadeIntentsOptions>?, TimeProvider?, ILogger<OnchainIntentsClient>?)
The arkade:BTC->onchain:BTC corridor: off-board an Arkade balance to Bitcoin L1.
public OnchainIntentsClient(IClientTransport transport, IContractService contractService, ISpendingService spendingService, IArkadeIntentStorage intentStorage, IContractStorage contractStorage, IVtxoStorage vtxoStorage, IWalletProvider walletProvider, IBitcoinBlockchain blockchain, IAesGcmCipher? cipher = null, IOptions<ArkadeIntentsOptions>? options = null, TimeProvider? time = null, ILogger<OnchainIntentsClient>? logger = null)
Parameters
transportIClientTransportcontractServiceIContractServicespendingServiceISpendingServiceintentStorageIArkadeIntentStoragecontractStorageIContractStoragevtxoStorageIVtxoStoragewalletProviderIWalletProviderblockchainIBitcoinBlockchaincipherIAesGcmCipheroptionsIOptions<ArkadeIntentsOptions>timeTimeProviderloggerILogger<OnchainIntentsClient>
Remarks
Two contracts, two chains, one secret. The client funds an Arkade covenant the solver can only take by publishing a preimage; the solver funds an L1 HTLC only that preimage releases. The client holds it, so the client is paid first and the solver is paid by the act of being paid.
Nothing here trusts the solver's account of anything. Both addresses are rebuilt locally and compared against the quote's rendering of them, the L1 funding is read off the chain rather than reported, and the preimage is derived from the wallet's own key so a lost record does not lose the claim.
Methods
ClaimNonInteractiveAsync(string, CancellationToken)
Claims the funded Arkade side of an on-board without a wallet signer, through its pinned covenant payout.
public Task<ArkadeSwapIntent> ClaimNonInteractiveAsync(string swapId, CancellationToken cancellationToken = default)
Parameters
swapIdstringThe recorded on-board, 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.
ClaimOnchainAsync(string, int?, CancellationToken)
Claim the solver's L1 HTLC, if it is there and confirmed enough to act on.
public Task<OnchainIntentsClient.OnchainClaimOutcome> ClaimOnchainAsync(string swapId, int? minConfirmations = null, CancellationToken cancellationToken = default)
Parameters
swapIdstringThe swap's id.
minConfirmationsint?How many confirmations the L1 funding must have. Omitted, the deployment's OnchainClaimConfirmations applies.
cancellationTokenCancellationTokenCancels before the broadcast.
Returns
- Task<OnchainIntentsClient.OnchainClaimOutcome>
What the pass found; Claimed false is ordinary.
Remarks
Called on every advance pass rather than when something says it is time, because nothing does: the solver's funding lands on a chain no VTXO event reports. "Not yet" is the normal answer and is not an error.
The claim publishes the preimage, which is what pays the solver on the Arkade side. It is therefore refused for less than the swap promised — an underfunded HTLC claimed anyway would hand over the secret for a fraction of the price.
Exceptions
- InvalidOperationException
The swap is unknown, or not an off-board.
ClaimOnchainReceiveAsync(string, CancellationToken)
Take delivery cooperatively using the receiver's wallet signer, revealing the preimage.
public Task<ArkadeSwapIntent> ClaimOnchainReceiveAsync(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 solver claim our L1 funding. So it is not optional tidy-up in either direction — leave it unclaimed and the solver reclaims its lockup, after which our only move is the L1 refund.
Exceptions
- InvalidOperationException
No such swap, the wrong direction, nothing funded yet, or the solver's reclaim window has already opened.
ReceiveFromOnchainAsync(string, long, IRfqTransport, string?, BitcoinAddress, RfqAmountSide, SolverCard?, ArkContract?, CancellationToken)
Negotiate an on-board and verify everything the solver sent back.
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
walletIdstringThe wallet taking delivery.
amountSatslongThe size to ask for, on the leg
amountSidenames.rfqTransportIRfqTransportHow to reach the solver.
covclaimdPubKeystringcovclaimd's compressed key, read live from its own endpoint, so the preimage is sealed to it and the Arkade claim can be pushed without us online — or
nullwhen there is no covclaimd, in which case no packet is sent at all and the claim is ours alone to make.l1RefundAddressBitcoinAddressWhere the L1 HTLC pays if we have to take it back. Neither contract commits to it, so it is remembered on the row — a swap whose row is lost can still be refunded once rebuilt, but the sats land wherever that rebuild names.
amountSideRfqAmountSideWhich leg
amountSatspins, and so who absorbs the solver's spread. From — the default — fixes what we send on L1, which is what a caller with a particular UTXO to spend means. To fixes what lands on Arkade and leaves the L1 figure to the solver.solverCardSolverCardThe solver's published card, when there is one. Supplying it holds the solver to its own advertised limits and fee.
payoutContractArkContractA contract to take the payout key from instead of deriving a fresh one. Supply the one this swap already belongs to — an invoice's own payment contract, say — and the swap costs no HD index of its own.
cancellationTokenCancellationTokenCancels the negotiation. Nothing is funded here either way.
Returns
- Task<PendingOnchainReceive>
The L1 address to fund, and everything needed to claim once the solver responds.
Remarks
Returns rather than funds. The L1 funding transaction is the caller's own wallet's job, exactly as the off-board leaves its L1 claim destination to the caller: this SDK holds an Arkade wallet, and the sats being on-boarded are by definition not in it yet.
That funding must be EXACT, and it must be a single output. The reference solver looks at the address for one output whose value equals the quoted amount — not a sum, and not "at least" — so none of the ways a payment usually goes slightly wrong can be recovered from.
- Underfunding cannot be topped up. A second payment is a second output, and the solver's claim spends one input, so the two are never added together: it is two mismatches, not one match.
- Overfunding is not taken. The solver will not match an output of the wrong value, so the sats are not lost to it — they sit at the HTLC until RefundOnchainReceiveAsync(string, BitcoinAddress?, CancellationToken) takes them back after its locktime.
- A mismatch that confirms is refused on the spot rather than left to time out, which is in the funder's favour: the sooner the swap is known dead, the sooner the refund can start.
A caller that cannot guarantee the exact amount — anything driven by a person typing into a wallet, or by a flow that accepts partial payment — should not offer this corridor without also offering one that tolerates it. Quoting per payment rather than per order is the other half of the same constraint: the reference solver drops an unfunded quote fifteen minutes after making it.
Pass payoutContract where the caller already derived a key for this same
payment. Deriving a second one is not free: an HD wallet is recovered by scanning until
GapLimit consecutive indices come back unused, so every index spent on a swap that is
never funded shortens the run a restore can cross. A flow that mints an address per attempt —
an invoice, an order — reaches that limit at twice the rate if the swap takes an index of its
own, and what lies beyond it is not found by a seed restore.
Exceptions
- RfqRefusedException
The solver declined to quote.
- OnchainReceiveNotFundableException
A safety gate refused — fund nothing.
ReceiveFromOnchainIntoAsync(string, long, IRfqTransport, string?, BitcoinAddress, SwapLinkSecret, ArkAddress, ArkContract, string, SolverCard?, string?, CancellationToken)
Negotiates exact-output onchain ingress whose non-interactive claim pays outgoing Arkade lock L.
public Task<PendingOnchainReceive> ReceiveFromOnchainIntoAsync(string walletId, long amountSats, IRfqTransport rfqTransport, string? covclaimdPubKey, BitcoinAddress l1RefundAddress, SwapLinkSecret secret, ArkAddress payoutAddress, ArkContract receiverContract, string outgoingSwapId, SolverCard? solverCard = null, string? rfqId = null, CancellationToken cancellationToken = default)
Parameters
walletIdstringWallet owning M's receiver key and recovery state.
amountSatslongExact Arkade amount required by L.
rfqTransportIRfqTransportIngress solver transport.
covclaimdPubKeystringcovclaimd's key for the claim packet, or
nullto send none.l1RefundAddressBitcoinAddressMerchant's destination if the source HTLC must be refunded.
secretSwapLinkSecretClient-owned outgoing route secret, reused for this ingress.
payoutAddressArkAddressAlready-verified outgoing Arkade lock L.
receiverContractArkContractWallet-owned contract supplying M's receiver and L1 refund 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 negotiation; this method funds neither rail.
Returns
- Task<PendingOnchainReceive>
The source HTLC and verified M covenant, imported with P saved in SDK intent storage.
Remarks
Claiming M reveals P while creating L, before downstream settlement; ingress is not merchant settlement.
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
swapIdstringThe swap's id.
refundAddressBitcoinAddressWhere to pay, overriding the address recorded at negotiation. Omitted, the recorded one is used.
cancellationTokenCancellationTokenCancels before the broadcast.
Returns
- Task<OnchainRefundOutcome>
What the pass found; Refunded false is ordinary.
Remarks
Called on every advance pass rather than when something says it is time, for the reason the off-board's claim is: what it waits for is a median time past on a chain no VTXO event reports. "Not yet" is the normal answer and is not an error.
Refused outright once the swap is Fulfilled. Past that we have published the preimage, so the solver can claim this same HTLC — and a refund racing it is at best a wasted fee and at worst an attempt to take both legs.
Exceptions
- InvalidOperationException
The swap is unknown, or not an on-board.
SendToOnchainAsync(string, BitcoinAddress, long, RfqAmountSide, IRfqTransport, SolverCard?, CancellationToken)
Negotiate an off-board and fund its Arkade side.
public Task<FundedOnchainSend> SendToOnchainAsync(string walletId, BitcoinAddress payoutAddress, long amountSats, RfqAmountSide amountSide, IRfqTransport rfqTransport, SolverCard? solverCard = null, CancellationToken cancellationToken = default)
Parameters
walletIdstringThe wallet paying, and receiving any refund.
payoutAddressBitcoinAddressThe Bitcoin L1 address the off-board pays out to.
amountSatslongThe size, on the leg
amountSidenames.amountSideRfqAmountSideWhich leg the size pins.
rfqTransportIRfqTransportHow to reach the solver.
solverCardSolverCardThe solver's registry card, to hold it to its published terms.
cancellationTokenCancellationTokenCancels before funding; after funding the swap is live regardless.
Returns
- Task<FundedOnchainSend>
The funded swap.
Exceptions
- RfqRefusedException
The solver declined to quote.
- OnchainSendNotFundableException
A safety gate refused — nothing was funded.