Table of Contents

Class LightningCorridor

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

What both Lightning corridors need identically: the CSV ladder, the key conversions, and rebuilding a funded lockup.

public static class LightningCorridor
Inheritance
LightningCorridor
Inherited Members

Remarks

The two corridors differ only in who occupies which covenant role. Everything feeding that construction is the same on both sides, and it must stay the same — the delays especially, since two clients deriving them differently would derive two different addresses for the same swap. Shared here rather than mirrored, so there is one place to be right.

Methods

DeriveBothLockupShapes(OutputDescriptor, OutputDescriptor, OutputDescriptor, uint160, LockTime, Sequence, Sequence, Sequence, VHTLCv2NonInteractiveClaim, byte[], ECXOnlyPubKey)

Build both lockup shapes the covenant suite can take — without and with the timelocked refund leaf — from one shared parameter set.

public static (VHTLCv2Contract EightLeaf, VHTLCv2Contract NineLeaf) DeriveBothLockupShapes(OutputDescriptor server, OutputDescriptor sender, OutputDescriptor receiver, uint160 hash, LockTime refundLocktime, Sequence unilateralClaimDelay, Sequence unilateralRefundDelay, Sequence unilateralRefundWithoutReceiverDelay, VHTLCv2NonInteractiveClaim nonInteractiveClaim, byte[] refundPkScript, ECXOnlyPubKey refundEmulatorPubKey)

Parameters

server OutputDescriptor

The Arkade server's key.

sender OutputDescriptor

The party that funds.

receiver OutputDescriptor

The party that claims.

hash uint160

HASH160 of the preimage.

refundLocktime LockTime

When the sender's timelocked refund path opens.

unilateralClaimDelay Sequence

The claim CSV delay.

unilateralRefundDelay Sequence

The refund CSV delay.

unilateralRefundWithoutReceiverDelay Sequence

The refund-without-receiver CSV delay.

nonInteractiveClaim VHTLCv2NonInteractiveClaim

The covenant claim leaf, identical in both shapes.

refundPkScript byte[]

Where both shapes' refund covenant must pay.

refundEmulatorPubKey ECXOnlyPubKey

The emulator key both shapes' refund covenant tweaks.

Returns

(VHTLCv2Contract EightLeaf, VHTLCv2Contract NineLeaf)

The eight-leaf and nine-leaf contracts, differing from each other in nothing but that leaf.

Remarks

Nothing on the wire says which shape a given solver has deployed, so a client cannot know in advance which to build. All three corridors derive both here and accept whichever matches the solver's quoted address. That stays safe because both shapes pin the refund covenant to the SAME destination as each other — whatever refundPkScript names, which is the client's own address on the send and onchain-send corridors but the SOLVER's on the receive corridor. Either way the two candidates cannot disagree about it.

The refund leaf is taken apart — destination and key, not a built VHTLCv2NonInteractiveRefund — precisely so there is no WithoutReceiver for this method to override and silently discard. A caller cannot pin the flag here, because pinning it is the one thing deriving both shapes exists to avoid.

DescriptorForXOnly(string, Network)

Wrap a counterparty's x-only key as a descriptor.

public static OutputDescriptor DescriptorForXOnly(string xOnlyHex, Network network)

Parameters

xOnlyHex string

The 32-byte key, hex.

network Network

The network the descriptor belongs to.

Returns

OutputDescriptor

A descriptor carrying that key.

Remarks

The parity byte is arbitrary — every leaf commits to the x-only form — so the even prefix is as good as any.

LoadLockupAsync(IContractStorage, string, string, Network, CancellationToken)

Rebuild a funded lockup from the contract imported before it was funded.

public static Task<VHTLCv2Contract> LoadLockupAsync(IContractStorage contractStorage, string swapPkScript, string swapId, Network network, CancellationToken cancellationToken = default)

Parameters

contractStorage IContractStorage

Where the lockup was imported.

swapPkScript string

The lockup's scriptPubKey, hex.

swapId string

The swap's id, for the error message.

network Network

The network the descriptors belong to.

cancellationToken CancellationToken

Cancels the lookup.

Returns

Task<VHTLCv2Contract>

The reconstructed contract.

Remarks

The store is the only record of how a funded script was built, which is why both corridors import before they commit anything — money in a script nobody can rebuild is money nobody can spend.

Exceptions

InvalidOperationException

The contract is not in the store.

MatchQuotedLockup(VHTLCv2Contract, VHTLCv2Contract, string?, bool)

Match a solver's quoted address against both derived lockup shapes.

public static (VHTLCv2Contract? Matched, string EightLeafAddress, string NineLeafAddress) MatchQuotedLockup(VHTLCv2Contract eightLeaf, VHTLCv2Contract nineLeaf, string? quoted, bool isMainnet)

Parameters

eightLeaf VHTLCv2Contract

The candidate without the timelocked refund leaf.

nineLeaf VHTLCv2Contract

The candidate with it.

quoted string

The address the solver sent for comparison, if it sent one.

isMainnet bool

Which network's address encoding to compare under.

Returns

(VHTLCv2Contract Matched, string EightLeafAddress, string NineLeafAddress)

The candidate that matched, or null if neither did, together with both derived addresses — which the caller needs either way, since a refusal has to name them.

Remarks

The comparison itself, without the refusal: all three corridors decide identically, but each refuses in its own exception type, so the decision is shared here and the refusal stays at the call site. A fourth lockup shape is then one edit rather than three.

A null quoted matches NEITHER candidate — a derived address is never null — so a solver that sends no address is refused rather than defaulting to a guessed shape. That is the whole point of returning null here rather than a fallback.

NormalizeToXOnly(byte[])

Accept an emulator key in either encoding and return its x-only form.

public static ECXOnlyPubKey NormalizeToXOnly(byte[] pubkey)

Parameters

pubkey byte[]

A 32-byte x-only key, or a 33-byte compressed one.

Returns

ECXOnlyPubKey

The x-only key the covenant commits to.

Remarks

Parsed, not sliced. Taking the first byte off any 33-byte blob yields 32 bytes shaped exactly like a key — the head of an uncompressed point, say — and the covenant would then commit to a co-signer nobody holds, in a script that is well formed and an address that looks ordinary.

Exceptions

ArgumentException

The bytes are not a key in either encoding.

ResolveSoloRefundDelay(long?, (uint Claim, uint Refund, uint RefundWithoutReceiver), long, long)

Settle the solo-refund rung for one send quote: adopt the solver's published delay once it has been checked, or fall back to the base ladder when the quote carries none.

public static uint ResolveSoloRefundDelay(long? quotedDelaySeconds, (uint Claim, uint Refund, uint RefundWithoutReceiver) delays, long refundLocktime, long now)

Parameters

quotedDelaySeconds long?

The quote's profile.refund_without_receiver_delay, or null when it carries none.

delays (uint Claim, uint Refund, uint RefundWithoutReceiver)

The base ladder from UnilateralDelays(ArkServerInfo).

refundLocktime long

The quote's absolute refund deadline, unix seconds.

now long

The current time, unix seconds.

Returns

uint

The delay to build the unilateralRefundWithoutReceiver leaf with, in seconds.

Remarks

This one rung is negotiated because it cannot always be derived. The solo refund must open after the absolute refund_locktime the solver quotes — otherwise the funder could take back a deposit while the claimant, holding the preimage, still had a live claim — and on a long horizon the base ladder's fixed headroom is not enough to guarantee that. The solver sizes the rung against the horizon it just quoted and publishes the result; the client's job is to check it rather than to guess the same number.

Every bound below is checked because this value goes straight into a script. A delay that is not a whole BIP68 unit would be silently rounded when encoded, so the leaf would time something other than what was agreed; one below the claim rung would let the funder's solo path open before the claimant's; one that does not cover the horizon is the theft window itself.

Precondition: now is before refundLocktime. The horizon check compares against their difference, so on a quote that has already lapsed the difference is negative and every positive delay clears it. That is not a hole in practice — the corridor's own expiry gate refuses a lapsed quote before anything is derived, and the address comparison catches a wrong derivation either way — but this function is public, so the assumption is stated rather than implied. A caller reaching it directly must check expiry itself.

A missing value is not refused. A solver predating the field derives the same base ladder this falls back to, and the client's real protection is the address comparison that follows either way: a solver whose number we did not reproduce quotes an address we do not derive, and nothing gets funded. Refusing outright would drop those deployments for a guarantee the comparison already gives.

Exceptions

QuotedDelayRejectedException

The published delay is one we will not build.

UnilateralDelays(ArkServerInfo)

The three CSV delays the covenant's timelocked leaves use.

public static (uint Claim, uint Refund, uint RefundWithoutReceiver) UnilateralDelays(ArkServerInfo serverInfo)

Parameters

serverInfo ArkServerInfo

The Arkade server's advertised terms.

Returns

(uint Claim, uint Refund, uint RefundWithoutReceiver)

The claim, refund and refund-without-receiver delays, in seconds.

Remarks

The base ladder, from the same public /v1/info both sides read, so neither needs the other to reach it. Two of the three rungs end here: the claim and the two-party refund are never negotiated, because a delay a solver could dictate would be a delay it could stretch.

The solo rung is the exception, and only on a send quote: it may have to reach past this base to stay behind the quoted refund_locktime, so the solver publishes the value it built and the client adopts it after checking it — ResolveSoloRefundDelay(long?, (uint Claim, uint Refund, uint RefundWithoutReceiver), long, long). What comes back from here is that negotiation's floor, not its answer.

The claim and the two-party refund sit level, and only the solo refund gets headroom on top: the one leaf a funder can spend alone is the only one whose timing can take money from a claimant. The base cannot be a constant — the server rejects any script below its configured minimum, and that minimum spans orders of magnitude between deployments. Worse, the rejection lands only when a spend is attempted, so a wrong constant surfaces once money is already committed.

Exceptions

InvalidOperationException

The server denominates its exit delay in blocks.