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
serverOutputDescriptorThe Arkade server's key.
senderOutputDescriptorThe party that funds.
receiverOutputDescriptorThe party that claims.
hashuint160HASH160 of the preimage.
refundLocktimeLockTimeWhen the sender's timelocked refund path opens.
unilateralClaimDelaySequenceThe claim CSV delay.
unilateralRefundDelaySequenceThe refund CSV delay.
unilateralRefundWithoutReceiverDelaySequenceThe refund-without-receiver CSV delay.
nonInteractiveClaimVHTLCv2NonInteractiveClaimThe covenant claim leaf, identical in both shapes.
refundPkScriptbyte[]Where both shapes' refund covenant must pay.
refundEmulatorPubKeyECXOnlyPubKeyThe 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
xOnlyHexstringThe 32-byte key, hex.
networkNetworkThe 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
contractStorageIContractStorageWhere the lockup was imported.
swapPkScriptstringThe lockup's scriptPubKey, hex.
swapIdstringThe swap's id, for the error message.
networkNetworkThe network the descriptors belong to.
cancellationTokenCancellationTokenCancels 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
eightLeafVHTLCv2ContractThe candidate without the timelocked refund leaf.
nineLeafVHTLCv2ContractThe candidate with it.
quotedstringThe address the solver sent for comparison, if it sent one.
isMainnetboolWhich network's address encoding to compare under.
Returns
- (VHTLCv2Contract Matched, string EightLeafAddress, string NineLeafAddress)
The candidate that matched, or
nullif 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
pubkeybyte[]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
quotedDelaySecondslong?The quote's
profile.refund_without_receiver_delay, ornullwhen it carries none.delays(uint Claim, uint Refund, uint RefundWithoutReceiver)The base ladder from UnilateralDelays(ArkServerInfo).
refundLocktimelongThe quote's absolute refund deadline, unix seconds.
nowlongThe current time, unix seconds.
Returns
- uint
The delay to build the
unilateralRefundWithoutReceiverleaf 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
serverInfoArkServerInfoThe 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.