Table of Contents

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

transport IClientTransport
contractService IContractService
spendingService ISpendingService
intentStorage IArkadeIntentStorage
contractStorage IContractStorage
vtxoStorage IVtxoStorage
walletProvider IWalletProvider
blockchain IBitcoinBlockchain
cipher IAesGcmCipher
options IOptions<ArkadeIntentsOptions>
time TimeProvider
logger ILogger<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

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.

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

swapId string

The swap's id.

minConfirmations int?

How many confirmations the L1 funding must have. Omitted, the deployment's OnchainClaimConfirmations applies.

cancellationToken CancellationToken

Cancels 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

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 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

walletId string

The wallet taking delivery.

amountSats long

The size to ask for, on 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 is sealed to it and the Arkade claim can be pushed without us online — or null when there is no covclaimd, in which case no packet is sent at all and the claim is ours alone to make.

l1RefundAddress BitcoinAddress

Where 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.

amountSide RfqAmountSide

Which leg amountSats pins, 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.

solverCard SolverCard

The solver's published card, when there is one. Supplying it holds the solver to its own advertised limits and fee.

payoutContract ArkContract

A 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.

cancellationToken CancellationToken

Cancels 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

walletId string

Wallet owning M's receiver key and recovery state.

amountSats long

Exact Arkade amount required by L.

rfqTransport IRfqTransport

Ingress solver transport.

covclaimdPubKey string

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

l1RefundAddress BitcoinAddress

Merchant's destination if the source HTLC must be refunded.

secret SwapLinkSecret

Client-owned outgoing route secret, reused for this ingress.

payoutAddress ArkAddress

Already-verified outgoing Arkade lock L.

receiverContract ArkContract

Wallet-owned contract supplying M's receiver and L1 refund 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 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

swapId string

The swap's id.

refundAddress BitcoinAddress

Where to pay, overriding the address recorded at negotiation. Omitted, the recorded one is used.

cancellationToken CancellationToken

Cancels 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

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.

amountSide RfqAmountSide

Which leg the size pins.

rfqTransport IRfqTransport

How to reach the solver.

solverCard SolverCard

The solver's registry card, to hold it to its published terms.

cancellationToken CancellationToken

Cancels 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.