@arkade-os/swap 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1219 @@
1
+ import { ReadonlyIdentity, IWallet, Identity, asset, VHTLC } from '@arkade-os/sdk';
2
+ import { DiscoveredMarket } from '@arkade-os/solver-discovery';
3
+
4
+ type AssetSwapStatus = "pending" | "cancelling" | "fulfilled" | "cancelled" | "recoverable" | "awaiting_fill" | "claimable" | "claimed" | "refunded_l1";
5
+ /** The sentinel asset id for BTC itself, as opposed to a 68-hex asset id.
6
+ * Lives here with the {@link AssetSwap} fields it describes so the market and
7
+ * restore layers share one spelling instead of re-typing the literal. */
8
+ declare const BTC_ASSET_ID = "btc";
9
+ interface AssetSwapFallbackSecretsV1 {
10
+ version: 1;
11
+ type: "stored";
12
+ senderPrivateKeyHex: string;
13
+ /** Onchain-send only. A lightning send's preimage belongs to the payee. */
14
+ preimageHex?: string;
15
+ }
16
+ type AssetSwapFallbackSecrets = AssetSwapFallbackSecretsV1;
17
+ interface AssetSwap {
18
+ /** Funding txid — the swap's identity. */
19
+ id: string;
20
+ /** 'btc' or a 68-hex asset id. */
21
+ fromAsset: string;
22
+ toAsset: string;
23
+ /** Atomic amounts as strings (bigint is not JSON-safe). */
24
+ fromAmount: string;
25
+ /** The covenant wantAmount — a floor, the fill pays >= this. */
26
+ toAmount: string;
27
+ swapAddress: string;
28
+ /** Hex pkScript of the swap contract — the indexer monitoring key. */
29
+ swapPkScript: string;
30
+ /** TLV offer — needed to rebuild the contract for cancel. */
31
+ offerHex: string;
32
+ fundingTxid: string;
33
+ spentTxid?: string;
34
+ status: AssetSwapStatus;
35
+ createdAt: number;
36
+ completedAt?: number;
37
+ /** RFQ pair string, e.g. `arkade:BTC->onchain:BTC`. */
38
+ pair?: string;
39
+ /** `sha256(P)`, hex. Public, and how a restore confirms a candidate
40
+ * derivation is the right one. */
41
+ paymentHash?: string;
42
+ /**
43
+ * The HD descriptor this swap's secrets derive from. Public — it is what
44
+ * lets the record carry no secrets at all. Present iff the swap was
45
+ * created on a wallet that can allocate.
46
+ */
47
+ signingDescriptor?: string;
48
+ /** P, hex, when the user supplied a preimage that is not seed-derived. */
49
+ preimageHex?: string;
50
+ /**
51
+ * Complete stored-arm secrets for wallets that cannot derive. Versioned
52
+ * and discriminated so restore can rebuild both the sender identity and,
53
+ * for onchain sends, P.
54
+ */
55
+ fallbackSecrets?: AssetSwapFallbackSecrets;
56
+ /** The L1 HTLC's pkScript, hex — the chain-watch key. */
57
+ htlcPkScriptHex?: string;
58
+ htlcLocktime?: number;
59
+ /** The L1 funding txid, once observed. */
60
+ l1Txid?: string;
61
+ }
62
+ /** All swaps, newest-first. Insertion order is not chronological — the restore
63
+ * scan rebuilds records in tx-scan order — so sort at read to keep
64
+ * newest-first canonical for every consumer. */
65
+ declare const getAssetSwapsOrThrow: (repository: AssetSwapRepository) => Promise<AssetSwap[]>;
66
+ /** The consumer read: a broken backend reads as no swaps rather than crashing
67
+ * a history view. Mutations must use {@link getAssetSwapsOrThrow} instead —
68
+ * swallowing the read there would let "the backend is gone" masquerade as "no
69
+ * such swap" and skip the write silently. */
70
+ declare const getAssetSwaps: (repository: AssetSwapRepository) => Promise<AssetSwap[]>;
71
+ /** Add a swap; no-op if the id is already stored. Returns the updated list.
72
+ * THROWS on a failed write — nothing irreversible may happen until this record
73
+ * is durable, so the caller must not fund on a failure. */
74
+ declare const addAssetSwap: (repository: AssetSwapRepository, swap: AssetSwap) => Promise<AssetSwap[]>;
75
+ /** Merge changes into a swap by id. Returns the updated list.
76
+ * THROWS on a failed read or write, like {@link addAssetSwap} — use this for a
77
+ * write that gates something irreversible. Transitions written *after* the
78
+ * irreversible act belong on {@link updateAssetSwapBestEffort}. */
79
+ declare const updateAssetSwap: (repository: AssetSwapRepository, id: string, changes: Partial<Omit<AssetSwap, "id">>) => Promise<AssetSwap[]>;
80
+ /**
81
+ * {@link updateAssetSwap} for transitions that follow an irreversible action (a
82
+ * broadcast claim, a spent lockup): failing the caller there would report as
83
+ * failed a swap whose funds already moved, and a stale status is recoverable —
84
+ * crash recovery re-derives the true state from the chain
85
+ * (`classifyOnchainHtlc`).
86
+ *
87
+ * `persisted` is the part that must not be hidden: a caller that notifies on a
88
+ * change, or treats one as terminal, has to know the store did not agree.
89
+ */
90
+ declare const updateAssetSwapBestEffort: (repository: AssetSwapRepository, id: string, changes: Partial<Omit<AssetSwap, "id">>) => Promise<{
91
+ swaps: AssetSwap[];
92
+ persisted: boolean;
93
+ }>;
94
+
95
+ /** A registry discovery result held for reuse. Refetchable — unlike a swap
96
+ * record, losing it costs one network round trip — but it must survive a cold
97
+ * boot: serving it stale is what keeps quoting alive while a registry is down. */
98
+ interface MarketsCacheEntry {
99
+ markets: DiscoveredMarket[];
100
+ fetchedAt: number;
101
+ }
102
+ /**
103
+ * Everything the package persists, following the monorepo repository
104
+ * convention (versioned interface, AsyncDisposable, one backend per
105
+ * platform — see the Boltz plugin's SwapRepository). Consumers construct
106
+ * exactly one of these; there is no second storage seam.
107
+ *
108
+ * Durable records (swaps) and rebuildable state (the restore scan's txid
109
+ * cursor, the markets cache) live side by side because they share a
110
+ * lifetime: all three belong to one wallet on one device, and a consumer
111
+ * that wipes one wants all three gone.
112
+ *
113
+ * ponytail: no query filters — every consumer reads all swaps and filters
114
+ * in memory; mirror the Boltz plugin's GetSwapsFilter when a consumer needs
115
+ * subset queries.
116
+ */
117
+ interface AssetSwapRepository extends AsyncDisposable {
118
+ readonly version: 1;
119
+ /** Insert or replace a swap by id. Store the record whole: `fallbackSecrets`
120
+ * is secret-bearing, and a field-mapped backend that drops it loses the
121
+ * stored arm's claim and refund keys. */
122
+ saveSwap(swap: AssetSwap): Promise<void>;
123
+ /** All stored swaps, in no particular order — `getAssetSwaps` is the
124
+ * canonical newest-first read. */
125
+ getAllSwaps(): Promise<AssetSwap[]>;
126
+ /** Sent txids already checked for offer packets (see restore.ts). */
127
+ getScannedTxids(): Promise<Set<string>>;
128
+ markTxidsScanned(txids: Iterable<string>): Promise<void>;
129
+ /** Cached registry markets, or undefined on a miss. */
130
+ getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
131
+ saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
132
+ clear(): Promise<void>;
133
+ }
134
+ declare class InMemoryAssetSwapRepository implements AssetSwapRepository {
135
+ readonly version: 1;
136
+ private readonly swaps;
137
+ private readonly scanned;
138
+ private readonly markets;
139
+ saveSwap(swap: AssetSwap): Promise<void>;
140
+ getAllSwaps(): Promise<AssetSwap[]>;
141
+ getScannedTxids(): Promise<Set<string>>;
142
+ markTxidsScanned(txids: Iterable<string>): Promise<void>;
143
+ getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
144
+ saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
145
+ clear(): Promise<void>;
146
+ [Symbol.asyncDispose](): Promise<void>;
147
+ }
148
+
149
+ /** L1 confirmation-depth and reorg margin between dependent timelocks. */
150
+ declare const ONCHAIN_ORDER_MARGIN_SECONDS: number;
151
+ /** Don't broadcast a claim with less than this before the refund leaf opens:
152
+ * MTP lag plus confirmation time. Past this point the safe move is to let the
153
+ * swap die and take the covenant refund — claiming into the counterparty's
154
+ * live refund window risks losing the race AND publishing P. */
155
+ declare const ONCHAIN_CLAIM_MARGIN_SECONDS: number;
156
+ /** Bounds on the confirmation depth a quote may demand. */
157
+ declare const MAX_MIN_CONFIRMATIONS = 6;
158
+ /** Conservative block interval for converting depths into wall-clock time. */
159
+ declare const ONCHAIN_SECONDS_PER_BLOCK = 600;
160
+ /**
161
+ * Outputs below this are unspendable in practice; builders refuse them.
162
+ *
163
+ * 330, not 546: Bitcoin Core's dust threshold is a function of the OUTPUT
164
+ * type, and 546 is the P2PKH figure. Both payout scripts on this corridor are
165
+ * taproot — the claim pays the user's Arkade-side L1 address and the refund
166
+ * pays the trader's — for which the threshold is 330 (the same number
167
+ * `FALLBACK_WALLET_DUST_AMOUNT` already uses in the core SDK). Holding the
168
+ * P2PKH number here rejects payouts between 330 and 546 that the network
169
+ * would relay perfectly well, which on a refund path means refusing to return
170
+ * funds that could have been returned.
171
+ *
172
+ * Should a caller ever pass a legacy `payoutPkScript`, this floor is too low
173
+ * for that output and the spend would be non-standard; the threshold would
174
+ * then have to be derived from the script rather than fixed.
175
+ *
176
+ * `BigInt(330)` rather than a `330n` literal, matching the rest of the
177
+ * package: a bigint literal needs an ES2020 target, and this source is read
178
+ * directly by consumers that target lower — forcing every one of them to
179
+ * raise their own target for a single constant. The compiled output is
180
+ * identical.
181
+ */
182
+ declare const ONCHAIN_DUST_SATS: bigint;
183
+ /** 32 random bytes. The user generates P for BOTH onchain directions. */
184
+ declare const newPreimage: () => Uint8Array;
185
+ /** `sha256(P)`, hex — the wire `payment_hash`, same convention as BOLT11. */
186
+ declare const paymentHashOf: (preimage: Uint8Array) => string;
187
+ type OnchainNetwork = "bitcoin" | "testnet" | "regtest";
188
+ interface OnchainHtlcParams {
189
+ /** `sha256(P)`, hex; the HASH160 commitment is derived internally. */
190
+ paymentHash: string;
191
+ /** x-only key that claims with the preimage. */
192
+ claimKey: Uint8Array;
193
+ /** x-only key that refunds after the locktime. */
194
+ refundKey: Uint8Array;
195
+ /** Absolute unix seconds (consensus matures it against median-time-past). */
196
+ refundLocktime: number;
197
+ }
198
+ interface OnchainHtlc {
199
+ address: string;
200
+ /** `0x5120…` — the P2TR output script. */
201
+ pkScript: Uint8Array;
202
+ leaves: {
203
+ claim: Uint8Array;
204
+ refund: Uint8Array;
205
+ };
206
+ /** Serialized control blocks per leaf, ready for a script-path witness. */
207
+ controlBlocks: {
208
+ claim: Uint8Array;
209
+ refund: Uint8Array;
210
+ };
211
+ paymentHash: string;
212
+ refundLocktime: number;
213
+ }
214
+ /**
215
+ * Derive the two-leaf taproot HTLC. Internal key is the BIP-341 NUMS point, so
216
+ * there is no key-path spend, ever:
217
+ *
218
+ * claim: `OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 <h160> OP_EQUALVERIFY <claimKey> OP_CHECKSIG`
219
+ * refund: `<locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP <refundKey> OP_CHECKSIG`
220
+ *
221
+ * The claim leaf's `OP_SIZE 32 OP_EQUALVERIFY` prefix pins the witness
222
+ * preimage to exactly 32 bytes before it's hashed — the same shape real HTLC
223
+ * scripts (e.g. BOLT3's) carry, and this contract's preimage is always
224
+ * exactly 32 bytes by construction.
225
+ *
226
+ * Pure derivation — pinned byte-for-byte by the golden test; any drift here
227
+ * changes addresses on BOTH sides of a swap.
228
+ */
229
+ declare function onchainHtlcScript(params: OnchainHtlcParams, network: OnchainNetwork): OnchainHtlc;
230
+ interface HtlcUtxo {
231
+ txid: string;
232
+ vout: number;
233
+ amount: bigint;
234
+ }
235
+ interface SpendResult {
236
+ txHex: string;
237
+ txid: string;
238
+ /** `utxo.amount − fee` — what actually lands at the payout script. */
239
+ payoutAmount: bigint;
240
+ }
241
+ /** Script-path spend of the claim leaf; the witness reveals P — that is how
242
+ * the counterparty learns it, so never build this unless the claim will win
243
+ * (see {@link claimOnchainFill}). `sign` is BIP340 over the claim key. */
244
+ declare const buildHtlcClaim: (input: {
245
+ htlc: OnchainHtlc;
246
+ utxo: HtlcUtxo;
247
+ preimage: Uint8Array;
248
+ payoutPkScript: Uint8Array;
249
+ feeRateSatVb: number;
250
+ sign: (sighash: Uint8Array) => Promise<Uint8Array>;
251
+ }) => Promise<SpendResult>;
252
+ /** Script-path spend of the refund leaf; consensus-valid only once nLockTime
253
+ * has matured against median-time-past — gate on {@link ChainSource.getMtp},
254
+ * not wall clock. `sign` is BIP340 over the refund key. */
255
+ declare const buildHtlcRefund: (input: {
256
+ htlc: OnchainHtlc;
257
+ utxo: HtlcUtxo;
258
+ payoutPkScript: Uint8Array;
259
+ feeRateSatVb: number;
260
+ sign: (sighash: Uint8Array) => Promise<Uint8Array>;
261
+ }) => Promise<SpendResult>;
262
+ interface ChainUtxo extends HtlcUtxo {
263
+ confirmations: number;
264
+ }
265
+ /** The package's whole view of Bitcoin L1. An esplora-backed implementation
266
+ * belongs to the caller (a reference one lives in the test suite); the package
267
+ * itself stays backend-free. */
268
+ interface ChainSource {
269
+ /** Confirmed+mempool outputs paying a script; used to detect the fill. */
270
+ getScriptUtxos(pkScript: Uint8Array): Promise<ChainUtxo[]>;
271
+ /** The spend of an outpoint, if any — where P is extracted from. */
272
+ getSpendingTx(txid: string, vout: number): Promise<{
273
+ txHex: string;
274
+ } | null>;
275
+ broadcast(txHex: string): Promise<string>;
276
+ /** Current median-time-past, unix seconds — gates refund broadcasting. */
277
+ getMtp(): Promise<number>;
278
+ }
279
+ /** Read P out of a claim spend's witness: the 32-byte item whose sha256 is the
280
+ * payment hash. Null when the tx reveals no matching preimage (e.g. a refund
281
+ * spend, or an unrelated tx). */
282
+ declare function extractPreimage(txHex: string, paymentHash: string): Uint8Array | null;
283
+ /** Poll {@link ChainSource} until the HTLC is funded to the required depth.
284
+ * Picks the largest qualifying output when several exist. Throws (reason
285
+ * `fill_timeout`) once `deadline` (unix seconds) passes without one. */
286
+ declare function awaitOnchainFill(chain: ChainSource, htlc: OnchainHtlc, minConfirmations: number, options?: {
287
+ pollMs?: number;
288
+ deadline?: number;
289
+ }): Promise<ChainUtxo>;
290
+ /**
291
+ * Claim the fill: build the claim spend and broadcast it. Broadcasting
292
+ * publishes P (mempool) — by design, it is how the solver gets paid — so this
293
+ * refuses (reason `claim_window_closed`) when less than
294
+ * {@link ONCHAIN_CLAIM_MARGIN_SECONDS} remains before the refund leaf opens:
295
+ * past that point, let the swap die and take the covenant refund instead of
296
+ * racing the counterparty's refund with P exposed.
297
+ */
298
+ declare function claimOnchainFill(chain: ChainSource, input: {
299
+ htlc: OnchainHtlc;
300
+ utxo: HtlcUtxo;
301
+ preimage: Uint8Array;
302
+ payoutPkScript: Uint8Array;
303
+ feeRateSatVb: number;
304
+ sign: (sighash: Uint8Array) => Promise<Uint8Array>;
305
+ /** Injected for tests; defaults to wall clock. */
306
+ now?: number;
307
+ }): Promise<{
308
+ txid: string;
309
+ payoutAmount: bigint;
310
+ }>;
311
+ /** Where an onchain HTLC stands, for crash recovery (see the store docs:
312
+ * persisting the record BEFORE funding is what makes this classification —
313
+ * and the claim — possible after a restart). */
314
+ type OnchainHtlcPhase = {
315
+ phase: "unfunded";
316
+ } | {
317
+ phase: "awaiting_confirmations";
318
+ utxo: ChainUtxo;
319
+ } | {
320
+ phase: "claimable";
321
+ utxo: ChainUtxo;
322
+ }
323
+ /**
324
+ * The refund leaf has matured, which means the CLAIM WINDOW IS CLOSED —
325
+ * `claimOnchainFill` throws `claim_window_closed` from here, by design.
326
+ * A recovery caller reading this phase must not try to claim: the correct
327
+ * action is to let the counterparty's L1 refund settle and take the
328
+ * Arkade-side covenant refund. Reaching this phase on a swap you expected
329
+ * to claim means the claim was missed, not that it is still available.
330
+ */
331
+ | {
332
+ phase: "refundable";
333
+ utxo: ChainUtxo;
334
+ } | {
335
+ phase: "claimed";
336
+ txid: string;
337
+ preimage: Uint8Array;
338
+ } | {
339
+ phase: "swept";
340
+ txid: string;
341
+ };
342
+ /**
343
+ * Classify an HTLC from chain state alone. `funding` (the known outpoint from
344
+ * the stored record) is what distinguishes "never funded" from "funded and
345
+ * already spent": without it a spent HTLC looks unfunded.
346
+ *
347
+ * `claimed` carries the preimage read from the spend's witness — the receipt;
348
+ * `swept` is a spend that reveals no preimage (the counterparty's refund).
349
+ */
350
+ declare function classifyOnchainHtlc(chain: ChainSource, input: {
351
+ htlc: OnchainHtlc;
352
+ minConfirmations: number;
353
+ funding?: {
354
+ txid: string;
355
+ vout: number;
356
+ };
357
+ }): Promise<OnchainHtlcPhase>;
358
+
359
+ /**
360
+ * Domain separator for the preimage derivation.
361
+ *
362
+ * NArk scopes its tag by protocol+provider (`Arkade-Boltz-Preimage-v1`,
363
+ * `SwapsManagementService.cs:128`) so any Arkade SDK reproduces the same
364
+ * preimage. NArk has no RFQ corridor yet, so this tag defines the scheme
365
+ * rather than mirroring one; it is deliberately distinct from the Boltz tag,
366
+ * or the same wallet key would derive one preimage for both corridors.
367
+ */
368
+ declare const RFQ_PREIMAGE_TAG = "Arkade-RFQ-Preimage-v1";
369
+ /**
370
+ * `TAG ‖ xonly(32) ‖ u32le(index)` — the message that gets BIP-340 signed.
371
+ *
372
+ * Anchored on the canonical x-only key rather than the descriptor string:
373
+ * restore reconstructs a bare descriptor that serialises differently from the
374
+ * signing descriptor used at create time, and only the key agrees across both.
375
+ */
376
+ declare function buildPreimageMessage(xonly: Uint8Array, index: number): Uint8Array;
377
+ /** No secrets at rest: everything re-derives from the seed plus this. */
378
+ interface DerivedSwapSecrets {
379
+ derivable: true;
380
+ /** Public. Persist it on the swap record; it is what restore keys off. */
381
+ signingDescriptor: string;
382
+ /** Onchain-send only, when the caller supplied P instead of deriving it. */
383
+ preimage?: Uint8Array;
384
+ }
385
+ /** The wallet could not allocate. These are real secrets — persist them. */
386
+ interface StoredSwapSecrets {
387
+ derivable: false;
388
+ senderPrivateKey: Uint8Array;
389
+ /** Onchain-send only. A lightning send's preimage belongs to the payee. */
390
+ preimage?: Uint8Array;
391
+ }
392
+ /**
393
+ * Which arm a swap got. The discriminant makes the persistence obligation a
394
+ * type-level fact: a consumer written against {@link DerivedSwapSecrets} alone
395
+ * fails to compile when handed the stored arm.
396
+ */
397
+ type SwapSecrets = DerivedSwapSecrets | StoredSwapSecrets;
398
+ /**
399
+ * Allocate a descriptor for one swap, or `undefined` when the wallet cannot.
400
+ *
401
+ * Allocates — never peeks. `getCurrentSigningDescriptor` returns the same
402
+ * descriptor until the wallet rotates, and two swaps sharing a descriptor
403
+ * derive the *identical* preimage, so one solver learning its own preimage
404
+ * would learn the other swap's.
405
+ *
406
+ * Cost of allocating: the index is consumed even when the quote is later
407
+ * refused, and a swap index never turns into a funded receive contract, so a
408
+ * long run of swaps widens the "unused" gap a seed-only `restore()` scan sees
409
+ * (see the README's gap-limit note). Restores that keep the swap repository
410
+ * are unaffected — `adoptSwapDescriptor` re-claims each record's index.
411
+ */
412
+ declare function deriveSwapSecrets(wallet: IWallet): Promise<DerivedSwapSecrets | undefined>;
413
+ /**
414
+ * The fallback arm. Separate from {@link deriveSwapSecrets} so nothing can
415
+ * fabricate a preimage while probing for a derived one.
416
+ */
417
+ declare function randomSwapSecrets(opts?: {
418
+ preimage?: boolean | Uint8Array;
419
+ }): StoredSwapSecrets;
420
+ /**
421
+ * Serialize or restore the secrets arm a persisted record describes. Normal
422
+ * HD swaps store only `signingDescriptor`; caller-supplied preimages add
423
+ * `preimageHex`; fallback swaps use `fallbackSecrets` so both P and the
424
+ * sender identity survive a restart.
425
+ */
426
+ declare function rfqSecretsToRecord(secrets: SwapSecrets): {
427
+ signingDescriptor?: string;
428
+ preimageHex?: string;
429
+ fallbackSecrets?: AssetSwapFallbackSecrets;
430
+ };
431
+ declare function rfqSecretsOfRecord(record: {
432
+ signingDescriptor?: string;
433
+ preimageHex?: string;
434
+ fallbackSecrets?: AssetSwapFallbackSecrets;
435
+ }): SwapSecrets | undefined;
436
+ /**
437
+ * Claim a restored swap's index so a later allocation cannot reissue it —
438
+ * which would derive that swap's preimage a second time, for a different swap.
439
+ * Monotonic; a no-op on a wallet that cannot allocate.
440
+ */
441
+ declare function adoptSwapDescriptor(wallet: IWallet, signingDescriptor: string): Promise<void>;
442
+ /** Why a wallet cannot produce a swap's sender key. Different instructions to
443
+ * a user: restore the other wallet, or accept that this record never carried
444
+ * the secrets at all. */
445
+ type RefundBlockedReason =
446
+ /** The record names no arm: neither `signingDescriptor` nor `fallbackSecrets`. */
447
+ "no-secrets"
448
+ /** It names an arm this version cannot read. */
449
+ | "unreadable-secrets"
450
+ /** The descriptor belongs to another seed, or this wallet is static. */
451
+ | "foreign-descriptor";
452
+ /**
453
+ * The wallet cannot produce this swap's sender key, so no local refund is
454
+ * possible: not a failure to retry, a capability this wallet does not have.
455
+ *
456
+ * Thrown where the cause is discovered rather than translated at the edge, so
457
+ * a `refundArkade` wired through {@link senderIdentityForSwapRecord} reports it
458
+ * to `RfqSwapManager` unwrapped — which is what stops the manager grinding
459
+ * against a push that can never work for the whole refund window.
460
+ */
461
+ declare class RefundNotLocallyPossibleError extends Error {
462
+ readonly reason: RefundBlockedReason;
463
+ readonly name = "RefundNotLocallyPossibleError";
464
+ constructor(reason: RefundBlockedReason, message: string, options?: {
465
+ cause?: unknown;
466
+ });
467
+ }
468
+ /**
469
+ * The VHTLC `sender` identity — the signer for every interactive refund.
470
+ *
471
+ * The capability probe is not the check that matters: `signerForDescriptor`
472
+ * falls back to the plain wallet identity for a descriptor it cannot derive —
473
+ * a different seed, a static wallet — and that identity signs happily with the
474
+ * wrong key, which surfaces only as a solver rejection or a dead claim script.
475
+ * So the check is on what comes back: only a descriptor-bound signer carries
476
+ * `signSchnorrDeterministic`.
477
+ */
478
+ declare function senderIdentityForRfqSecrets(wallet: IWallet, secrets: SwapSecrets): Promise<Identity>;
479
+ /**
480
+ * The VHTLC `sender` identity for a stored swap record, or a typed refusal.
481
+ *
482
+ * The record→identity composition {@link rfqSecretsOfRecord} deliberately does
483
+ * not do: it stays total so history iteration can call it, so *this* is where
484
+ * "no secrets on the record" becomes a refusal rather than an `undefined` the
485
+ * caller has to remember to check.
486
+ *
487
+ * **Wire `refundArkade` here, not to {@link senderIdentityForRfqSecrets}.**
488
+ * Only two of the three causes are throws; a caller one level down would skip
489
+ * the third silently and turn it into a `TypeError` at the push site, which
490
+ * `RfqSwapManager` then treats as retryable and grinds against for the whole
491
+ * refund window.
492
+ *
493
+ * Takes the record shape structurally, matching {@link rfqSecretsOfRecord}, so
494
+ * either record type can be passed.
495
+ */
496
+ declare function senderIdentityForSwapRecord(wallet: IWallet, record: {
497
+ signingDescriptor?: string;
498
+ preimageHex?: string;
499
+ fallbackSecrets?: AssetSwapFallbackSecrets;
500
+ }): Promise<Identity>;
501
+ /** The `sender` x-only pubkey, the only half the request flow needs. */
502
+ declare function senderPubkeyForRfqSecrets(wallet: IWallet, secrets: SwapSecrets): Promise<Uint8Array>;
503
+ /**
504
+ * An identity that signs with `aux_rand = 0`, which is what makes the
505
+ * derivation reproducible. `DescriptorIdentity` satisfies it and throws rather
506
+ * than degrading to a random-aux signer.
507
+ */
508
+ interface DeterministicSigner extends ReadonlyIdentity {
509
+ signSchnorrDeterministic(messageHash: Uint8Array): Promise<Uint8Array>;
510
+ }
511
+ declare function isDeterministicSigner(value: unknown): value is DeterministicSigner;
512
+ /**
513
+ * `sha256(sign(sha256(msg)))`, with the signing key and the message key being
514
+ * the same identity — passing a key in separately is how this silently derives
515
+ * an unrecoverable preimage.
516
+ */
517
+ declare function derivePreimage(signer: DeterministicSigner): Promise<Uint8Array>;
518
+ /** The onchain-send preimage, re-derived or read back off the stored arm. */
519
+ declare function preimageForRfqSecrets(wallet: IWallet, secrets: SwapSecrets): Promise<Uint8Array>;
520
+
521
+ /** Legs are `<corridor>:<asset>`; a pair is directional, `from->to`. Arkade
522
+ * asset legs stay coarse (`arkade:ASSET`) — the exact asset ids ride the
523
+ * request profile, mirroring how the offer TLV identifies assets. */
524
+ declare const ARKADE_BTC = "arkade:BTC";
525
+ declare const ARKADE_ASSET = "arkade:ASSET";
526
+ declare const LIGHTNING_BTC = "lightning:BTC";
527
+ declare const ONCHAIN_BTC = "onchain:BTC";
528
+ declare const rfqPair: (from: string, to: string) => string;
529
+ /** The implemented pair: pay a BOLT11 invoice out of an Arkade balance. */
530
+ declare const LIGHTNING_SEND_PAIR: string;
531
+ /** On-board via Lightning: pay the solver's hold invoice, land on Arkade. */
532
+ declare const LIGHTNING_RECEIVE_PAIR: string;
533
+ /** Off-board: Arkade sats out to a Bitcoin-L1 HTLC. */
534
+ declare const ONCHAIN_SEND_PAIR: string;
535
+ /** On-board: a Bitcoin-L1 HTLC in, Arkade sats out. */
536
+ declare const ONCHAIN_RECEIVE_PAIR: string;
537
+ /** The closed refusal set. Treat any unknown reason as a generic decline. */
538
+ type RfqRefusalReason = "unsupported_pair" | "unsupported_payload" | "amount_out_of_range" | "exposure_cap" | "invoice_expired" | "quote_conflict" | "pricing_unavailable";
539
+ /** Lifecycle vocabulary; states after which nothing more will happen. */
540
+ declare const RFQ_TERMINAL_STATES: readonly ["settled", "refused", "expired", "refunded", "stuck"];
541
+ /** A refusal from the solver, carrying its closed-set reason. */
542
+ declare class SwapRefusal extends Error {
543
+ readonly reason: string;
544
+ readonly rfqId: string | undefined;
545
+ constructor(reason: string, rfqId?: string);
546
+ }
547
+ /** The solver's address does not match the local derivation. NEVER fund past this. */
548
+ declare class AddressMismatch extends Error {
549
+ readonly derived: string;
550
+ readonly quoted: string | undefined;
551
+ constructor(derived: string, quoted?: string);
552
+ }
553
+ /** A fresh client-chosen negotiation id: 32 random bytes, lowercase hex. */
554
+ declare const newRfqId: () => string;
555
+ interface RfqQuote {
556
+ v: 1;
557
+ type: "rfq_quote";
558
+ rfq_id: string;
559
+ pair: string;
560
+ from_amount: number;
561
+ to_amount: number;
562
+ solver_pubkey: string;
563
+ valid_until: number;
564
+ /** HTLC-class quotes only; absent for arkade↔arkade. */
565
+ refund_locktime?: number;
566
+ profile: {
567
+ [key: string]: unknown;
568
+ payment_hash?: string;
569
+ lockup_address?: string;
570
+ };
571
+ [key: string]: unknown;
572
+ }
573
+ interface RfqStatus {
574
+ v: 1;
575
+ type: "rfq_status";
576
+ rfq_id: string;
577
+ state: string;
578
+ updated_at: number;
579
+ profile: Record<string, unknown>;
580
+ [key: string]: unknown;
581
+ }
582
+ /** The rfq_request for the lightning send profile. A BOLT11 profile is always
583
+ * exact-out: the invoice fixes the amount, so none is restated here.
584
+ * `senderPubkey` is the trader's own key for the VHTLC's sender-side leaves
585
+ * (see {@link lightningSendVtxoScript}) — required, never sent anywhere else,
586
+ * never trusted by the solver as anything but a pubkey to bind into the
587
+ * script. On the wire it's `client_refund_pubkey` (docs/rfq-protocol.md —
588
+ * the solver's schema is `.strict()`, so both the wrong name AND the missing
589
+ * required field would refuse every request). */
590
+ declare const lightningSendRequest: (input: {
591
+ rfqId: string;
592
+ invoice: string;
593
+ refundAddress: string;
594
+ senderPubkey: Uint8Array;
595
+ }) => Record<string, unknown>;
596
+ /** The rfq_request for an arkade↔arkade swap. Exactly one side may name an
597
+ * asset id per direction (BTC has none); the pair string stays coarse and the
598
+ * ids ride the profile, like the offer TLV. Forward-looking: the wire shape is
599
+ * specified, the reference solver does not serve it yet. */
600
+ declare const arkadeSwapRequest: (input: {
601
+ rfqId: string;
602
+ /** Asset the trader deposits; omit when depositing BTC. */
603
+ offerAsset?: asset.AssetId;
604
+ /** Asset the trader wants; omit when wanting BTC. */
605
+ wantAsset?: asset.AssetId;
606
+ amountSide: "from" | "to";
607
+ /** Integer base units of the side named by `amountSide`. */
608
+ amount: number;
609
+ }) => Record<string, unknown>;
610
+ /** Funding gate: refuse unless ≥90 min remain before the refund path opens.
611
+ * 90 because the refund CLTV matures against median-time-past (BIP-113),
612
+ * which lags wall clock by ~1h — a smaller wall-clock margin is no margin. */
613
+ declare const MIN_HEADROOM_SECONDS: number;
614
+ /** Compare-only check of the solver's address against YOUR derivation.
615
+ * Throws {@link AddressMismatch}; returns the address so calls chain. */
616
+ declare const verifyLockupAddress: (quote: RfqQuote, derivedAddress: string) => string;
617
+ /** The user's gates, checked immediately before funding — never at quote
618
+ * time. Throws with a stable `reason` property. `invoiceExpiresAt` applies to
619
+ * BOLT11 profiles only; `onchain` adds the L1-HTLC gates (§ guardrails of the
620
+ * onchain spec) and is required for the onchain pairs.
621
+ *
622
+ * The lightning-receive leg does NOT use this: see {@link assertReceivable}.
623
+ * `refund_locktime` is the SOLVER's on both receive corridors, so
624
+ * `MIN_HEADROOM_SECONDS` gates the wrong side on either — but only the
625
+ * lightning leg has a second clock that can actually run out (the hold
626
+ * invoice's), which is what the split buys. The onchain-receive leg stays here
627
+ * until its own deadline gets the same treatment; the headroom check is merely
628
+ * over-strict there, never unsafe. */
629
+ declare const assertFundable: (input: {
630
+ quote: RfqQuote;
631
+ invoiceExpiresAt?: number;
632
+ now: number;
633
+ onchain?: {
634
+ htlcLocktime: number;
635
+ minConfirmations: number;
636
+ /** "send" = arkade->onchain (the L1 timelock-order gate applies). */
637
+ direction: "send" | "receive";
638
+ };
639
+ }) => void;
640
+ interface RfqTransport {
641
+ requestQuote(payload: Record<string, unknown>): Promise<RfqQuote>;
642
+ status(rfqId: string): Promise<RfqStatus | null>;
643
+ close(): Promise<void>;
644
+ }
645
+ /** HTTP: POST /v1/swap for quotes, GET /v1/rfq/<rfq_id> for status.
646
+ * `fetchImpl` is injectable for tests and non-global-fetch runtimes. */
647
+ declare const httpTransport: (baseUrl: string, options?: {
648
+ fetchImpl?: typeof fetch;
649
+ }) => RfqTransport;
650
+ /** Minimal WebSocket surface the relay transport needs — satisfied by the DOM
651
+ * WebSocket and by `ws` alike, so neither becomes a dependency. */
652
+ interface RelaySocket {
653
+ send(data: string): void;
654
+ close(): void;
655
+ addEventListener(type: "open" | "message" | "error", listener: (event: any) => void): void;
656
+ }
657
+ /** Relay: both parties outbound, addressed by x-only pubkey, speaking the dev
658
+ * broker framing. Nostr (directed kind + NIP-44) replaces only this function.
659
+ * One socket; replies correlated by rfq_id. */
660
+ declare const relayTransport: (relayUrl: string, options: {
661
+ solverPubkey: string;
662
+ clientPubkey: string;
663
+ WebSocketCtor?: new (url: string) => RelaySocket;
664
+ timeoutMs?: number;
665
+ }) => RfqTransport;
666
+ /** The solver's unilateral-claim delay, derived from the Ark server's reported
667
+ * exit delay exactly as the reference solver derives it — both sides read the
668
+ * SAME server, so the derivation (not a quote field) is what keeps the two
669
+ * scripts identical. */
670
+ declare const unilateralClaimDelay: (serverExitDelaySeconds: number) => number;
671
+ /** VHTLC's `unilateralRefund` tier: sender + solver, no server, one 512s step
672
+ * past `claimDelay` — the middle rung between the fully-collaborative paths
673
+ * and the sender's last-resort `unilateralRefundWithoutReceiver`. Same
674
+ * already-rounded `claimDelay` input as {@link unilateralClaimDelay}
675
+ * produces — one rounding, shared across all three tiers. */
676
+ declare const unilateralRefundDelay: (claimDelay: number) => number;
677
+ /** VHTLC's `unilateralRefundWithoutReceiver` tier: sender alone, needs
678
+ * nobody — two 512s steps past `claimDelay`, past {@link
679
+ * unilateralRefundDelay}. */
680
+ declare const unilateralRefundWithoutReceiverDelay: (claimDelay: number) => number;
681
+ /** Compile the lightning-send VHTLC from the quote's binding fields plus the
682
+ * trader's own data. `paymentHash` is the BOLT11 payment hash (`sha256(P)`,
683
+ * hex); the script's HASH160 commitment is derived from it here, which is why
684
+ * the trader never needs to see `P`.
685
+ *
686
+ * Every quote gets the full eight-leaf contract: VHTLC's own six
687
+ * (`claim`/`refund`/`refundWithoutReceiver`/`unilateralClaim`/
688
+ * `unilateralRefund`/`unilateralRefundWithoutReceiver`), plus two more the
689
+ * emulator co-signs under a covenant pinning the payout to a pre-committed
690
+ * destination — `nonInteractiveClaim` (server + emulator, pays the solver's
691
+ * own `receiverPkScript`, no solver signature needed) and
692
+ * `nonInteractiveRefund` (server + solver + emulator, pays the trader's own
693
+ * `refundPkScript`, no timelock and no trader signature needed — see {@link
694
+ * VHTLC.Options.nonInteractiveRefund}'s doc comment for why that matters).
695
+ */
696
+ declare function lightningSendVtxoScript(params: {
697
+ /** Binding field #1: the solver's x-only key, from the quote. */
698
+ solverPubkey: Uint8Array;
699
+ /** Binding field #2: when the trader's refund path opens, from the quote. */
700
+ refundLocktime: number;
701
+ /** The Ark server's x-only key — the trader's OWN connection. */
702
+ serverPubkey: Uint8Array;
703
+ /** BOLT11 payment hash, hex — from the trader's OWN invoice decode. */
704
+ paymentHash: string;
705
+ /** From {@link unilateralClaimDelay} over the trader's OWN server info.
706
+ * {@link unilateralRefundDelay} and {@link unilateralRefundWithoutReceiverDelay}
707
+ * derive from this same value — one rounding, shared across all three tiers. */
708
+ claimDelay: number;
709
+ /** Emulator x-only key — the SOLVER's deployment, not the trader's own.
710
+ * Not fetched here or anywhere in this package; see {@link
711
+ * requestLightningSend}'s `emulatorPubkey` parameter for where it comes
712
+ * from and why. */
713
+ emulatorPubkey: Uint8Array;
714
+ /** Where a refund must pay: the trader's P2TR pkScript (34 bytes). Also
715
+ * `nonInteractiveRefund`'s covenant destination. */
716
+ refundPkScript: Uint8Array;
717
+ /** The trader's own key — VHTLC's `sender` role. Required on every
718
+ * interactive refund-side leaf; the trader generates and persists it
719
+ * (see {@link requestLightningSend}'s own obligations). */
720
+ senderPubkey: Uint8Array;
721
+ /** The solver's own claim destination, from the quote
722
+ * (`profile.receiver_pk_script`) — needed only so `nonInteractiveClaim`'s
723
+ * covenant key can be derived; the trader does not otherwise use or trust
724
+ * this value. P2TR pkScript, 34 bytes. */
725
+ receiverPkScript: Uint8Array;
726
+ }): InstanceType<typeof VHTLC.ScriptV2>;
727
+ /** The BOLT11 facts the trader read from its OWN decode — this module takes
728
+ * the facts, not the decoder, so any wallet's existing decoder serves. */
729
+ interface InvoiceFacts {
730
+ /** The raw BOLT11 — what travels in the request profile. */
731
+ raw: string;
732
+ /** `sha256(P)`, LOWERCASE hex (64 chars) — {@link verifyReceiveInvoice}
733
+ * compares it byte-for-byte against `paymentHashOf`, which emits lowercase. */
734
+ paymentHash: string;
735
+ amountSats: number;
736
+ /** Absolute expiry, unix seconds. */
737
+ expiresAt: number;
738
+ }
739
+ /**
740
+ * The lightning-send user flow, mirroring `createOffer`'s shape: quote →
741
+ * derive locally → verify → gate. Pure of funding on purpose — it returns the
742
+ * address and amount, and the caller funds with its own wallet
743
+ * (`wallet.send({ address, amount })`) before `quote.valid_until`, after
744
+ * which the user may go OFFLINE: filling is non-interactive. Success reveals
745
+ * the preimage in the solver's claim witness (also served via status as
746
+ * `settled`); failure refunds to `refundAddress`.
747
+ *
748
+ * Throws {@link SwapRefusal} (closed reason), {@link AddressMismatch} (never
749
+ * fund), a gate error with a stable `reason`, or
750
+ * {@link LockupRegistrationFailed} — the last one alone means the quote is
751
+ * still good and the same call can be retried once local storage is.
752
+ *
753
+ * Broadcasts nothing, but does write locally: the lockup is registered with the
754
+ * wallet's contract manager before the address is returned, exactly as
755
+ * `createOffer` registers its covenant — so the lockup is watched from the
756
+ * moment it lands and out of generic coin selection, and a persistence failure
757
+ * throws while nothing is funded. `RfqSwapManager` re-registers as a backstop
758
+ * for older records; a repeat write is a no-op.
759
+ *
760
+ * Allocates a fresh `sender` key per call and returns it as `senderPubkey`
761
+ * plus `secrets`. On an HD wallet `secrets` holds only a public descriptor and
762
+ * nothing needs protecting; otherwise it holds the raw key and the caller MUST
763
+ * persist it, or every interactive refund path is gone. `nonInteractiveRefund`
764
+ * still recovers the funds without it — but it needs the SOLVER's active
765
+ * cooperation, not just infrastructure uptime, so losing the key with an
766
+ * unwilling solver is a total loss.
767
+ */
768
+ declare function requestLightningSend(wallet: IWallet, arkServerUrl: string,
769
+ /** Covenant co-signer (emulator) x-only key — the SOLVER's deployment,
770
+ * not the trader's. This library does NOT fetch or verify it: clients
771
+ * have no network path to the emulator, only the solver and covclaimd
772
+ * do. The caller must obtain this out-of-band, before calling this
773
+ * function, from the solver's signed registry/corridor card (its
774
+ * `emulator_pubkey`, added in arkade-os/solver-registry#18) or an
775
+ * equivalent source it
776
+ * independently trusts, and is responsible for having checked it against
777
+ * that trusted value itself — the same never-trust-only-compare rule as
778
+ * {@link verifyLockupAddress}, just applied by the caller instead of
779
+ * here, because there is nothing in this module to derive it against. */
780
+ emulatorPubkey: Uint8Array, transport: RfqTransport, params: {
781
+ invoice: InvoiceFacts;
782
+ rfqId?: string;
783
+ }): Promise<{
784
+ rfqId: string;
785
+ quote: RfqQuote;
786
+ /** The trader's OWN derivation — the only address to fund. */
787
+ address: string;
788
+ /** What the lockup must carry: the quote's `from_amount` (the invoice
789
+ * amount plus the corridor's fee), in sats. */
790
+ fundAmount: number;
791
+ /** The covenant's scriptPubKey, for watching the lockup and its spend. */
792
+ swapPkScript: Uint8Array;
793
+ /** The covenant itself. Hand it to `RfqSwapManager` as the record's
794
+ * `lockup` (with `address`): without it the manager can only poll, and
795
+ * cannot retire the row this call just wrote. */
796
+ script: InstanceType<typeof VHTLC.ScriptV2>;
797
+ /** Where a failed swap refunds. */
798
+ refundAddress: string;
799
+ /** The VHTLC `sender` x-only key, bound into the covenant. Public. */
800
+ senderPubkey: Uint8Array;
801
+ /** How the `sender` key is recovered later. Persist it with the record —
802
+ * on the derivable arm it holds nothing secret. */
803
+ secrets: SwapSecrets;
804
+ }>;
805
+ /**
806
+ * Map an arkade↔arkade quote onto `createOffer` terms. The trader takes the
807
+ * quote by creating and funding the offer covenant before `valid_until` —
808
+ * the non-interactive fill: the covenant only releases the deposit to a
809
+ * transaction that delivers the quoted want-amount to the trader, so the
810
+ * solver fills or nothing moves. There is no rfq_fill message and no refund
811
+ * timelock; an unfilled offer is cancelled cooperatively (`cancelOffer`).
812
+ */
813
+ declare const offerTermsFromQuote: (quote: RfqQuote, assets: {
814
+ wantAsset?: asset.AssetId;
815
+ offerAsset?: asset.AssetId;
816
+ }) => {
817
+ wantAmount: bigint;
818
+ wantAsset?: asset.AssetId;
819
+ offerAsset?: asset.AssetId;
820
+ };
821
+ /** The rfq_request for `arkade:BTC->onchain:BTC`. Exact-out means "this much
822
+ * lands in the L1 HTLC". `senderPubkey` is the user's own key for the
823
+ * VHTLC's sender-side leaves — same role as in {@link lightningSendRequest}.
824
+ * On the wire it's `client_refund_pubkey`, same as there. */
825
+ declare const onchainSendRequest: (input: {
826
+ rfqId: string;
827
+ /** `sha256(P)`, hex — user-chosen; see {@link paymentHashOf}. */
828
+ paymentHash: string;
829
+ /** User's x-only L1 key for the HTLC's claim leaf. */
830
+ payoutPubkey: Uint8Array;
831
+ /** User's arkade address — where the covenant refund must pay. */
832
+ refundAddress: string;
833
+ senderPubkey: Uint8Array;
834
+ amount: number;
835
+ amountSide: "from" | "to";
836
+ }) => Record<string, unknown>;
837
+ /** The rfq_request for `lightning:BTC->arkade:BTC`: pay the solver's hold
838
+ * invoice, land on Arkade. The trader generates `P`, keeps it, and sends only
839
+ * `H` plus `P` sealed to covclaimd — the solver never sees `P` until it
840
+ * appears in a claim witness. `payoutPubkey` is the trader's own x-only
841
+ * Arkade key — the covenant's `receiver` role on this leg, so the trader can
842
+ * claim the lockup itself without covclaimd. */
843
+ declare const lightningReceiveRequest: (input: {
844
+ rfqId: string;
845
+ /** `H = sha256(P)`, hex — trader-chosen; see {@link paymentHashOf}. */
846
+ paymentHash: string;
847
+ /** Trader's arkade address — where the swapped sats land. */
848
+ payoutAddress: string;
849
+ /** Trader's x-only arkade key — the covenant's `receiver` role. */
850
+ payoutPubkey: Uint8Array;
851
+ /** `P` sealed to covclaimd, base64 — `sealClaimPacket(...).ciphertext`. */
852
+ claimPacket: string;
853
+ amount: number;
854
+ amountSide: "from" | "to";
855
+ }) => Record<string, unknown>;
856
+ /** The rfq_request for `onchain:BTC->arkade:BTC`. The user funds the L1 HTLC
857
+ * (holding its refund role) and receives Arkade; P travels sealed to
858
+ * covclaimd (see `sealClaimPacket`) so the user can go offline after
859
+ * funding. `payoutPubkey` is the trader's own x-only Arkade key — the
860
+ * covenant's `receiver` role, same as the Lightning receive leg's. */
861
+ declare const onchainReceiveRequest: (input: {
862
+ rfqId: string;
863
+ paymentHash: string;
864
+ /** Trader's arkade address — where the swapped sats land. */
865
+ payoutAddress: string;
866
+ /** Trader's x-only arkade key — the covenant's `receiver` role. */
867
+ payoutPubkey: Uint8Array;
868
+ /** Trader's x-only L1 key for the HTLC's refund leaf. */
869
+ refundPubkey: Uint8Array;
870
+ /** `P` sealed to covclaimd, base64 — `sealClaimPacket(...).ciphertext`. */
871
+ claimPacket: string;
872
+ amount: number;
873
+ amountSide: "from" | "to";
874
+ }) => Record<string, unknown>;
875
+ /**
876
+ * The pure core of {@link requestOnchainSend}: derive BOTH contracts locally
877
+ * from the quote's binding fields plus the user's own data, and refuse on any
878
+ * mismatch. Binding: `solver_pubkey`, `refund_locktime`, `htlc_pubkey`,
879
+ * `htlc_locktime`, `min_confirmations`; `lockup_address` and `htlc_address`
880
+ * are compare-only.
881
+ */
882
+ declare function deriveOnchainSend(input: {
883
+ quote: RfqQuote;
884
+ paymentHash: string;
885
+ payoutPubkey: Uint8Array;
886
+ serverPubkey: Uint8Array;
887
+ emulatorPubkey: Uint8Array;
888
+ claimDelay: number;
889
+ hrp: string;
890
+ l1Network: OnchainNetwork;
891
+ refundAddress: string;
892
+ /** The user's own key for the VHTLC's sender-side leaves — same role as
893
+ * in {@link requestLightningSend}. */
894
+ senderPubkey: Uint8Array;
895
+ }): {
896
+ address: string;
897
+ swapPkScript: Uint8Array;
898
+ /** The lockup covenant itself — what the contract row is registered from,
899
+ * so the row can never key on a script other than the derived one. */
900
+ script: InstanceType<typeof VHTLC.ScriptV2>;
901
+ htlc: OnchainHtlc;
902
+ refundLocktime: number;
903
+ htlcLocktime: number;
904
+ minConfirmations: number;
905
+ };
906
+ /**
907
+ * The `arkade:BTC->onchain:BTC` user flow, mirroring `requestLightningSend`:
908
+ * quote → derive BOTH contracts locally → verify → gate. Pure of funding —
909
+ * the caller funds `address` with its own wallet before `quote.valid_until`.
910
+ *
911
+ * Registers the arkade lockup before returning the address, on the same terms
912
+ * as {@link requestLightningSend} — including {@link LockupRegistrationFailed},
913
+ * the one throw here that does not mean "walk away from this quote". The L1
914
+ * HTLC is not a contract row: it lives on bitcoin, not on Ark, and the wallet's
915
+ * contract manager knows nothing of it.
916
+ *
917
+ * Two obligations, both LOUD:
918
+ * - **Persist `secrets` (with the record) BEFORE funding.** On an HD wallet it
919
+ * is a public descriptor and both the preimage and the `sender` key
920
+ * re-derive from the seed; otherwise it carries the raw secrets, and losing
921
+ * them forfeits the L1 claim and every interactive refund path — leaving
922
+ * recovery dependent on the solver via `nonInteractiveRefund`.
923
+ * - **Stay claim-capable.** Unlike lightning-send the user cannot go fully
924
+ * offline: it must claim the L1 HTLC (`awaitOnchainFill` →
925
+ * `claimOnchainFill`) before `htlc.refundLocktime`. Missing that window
926
+ * forfeits the fill and falls back to the Arkade covenant refund.
927
+ */
928
+ declare function requestOnchainSend(wallet: IWallet, arkServerUrl: string,
929
+ /** Covenant co-signer (emulator) x-only key — same parameter, same
930
+ * caller obligation, as {@link requestLightningSend}'s. */
931
+ emulatorPubkey: Uint8Array, transport: RfqTransport, params: {
932
+ amount: number;
933
+ amountSide: "from" | "to";
934
+ /** User's x-only L1 key that will claim the HTLC. */
935
+ payoutPubkey: Uint8Array;
936
+ /** Optional caller-owned P. Persist it with the returned secrets before funding. */
937
+ preimage?: Uint8Array;
938
+ rfqId?: string;
939
+ }): Promise<{
940
+ rfqId: string;
941
+ quote: RfqQuote;
942
+ /** The user's OWN arkade lockup derivation — the only address to fund. */
943
+ address: string;
944
+ fundAmount: number;
945
+ swapPkScript: Uint8Array;
946
+ /** The arkade covenant itself — the record's `lockup` for
947
+ * `RfqSwapManager`, same role as {@link requestLightningSend}'s. */
948
+ script: InstanceType<typeof VHTLC.ScriptV2>;
949
+ refundAddress: string;
950
+ /** The EXPECTED L1 fill, derived locally — watch and claim against this. */
951
+ htlc: OnchainHtlc;
952
+ /** The VHTLC `sender` x-only key, bound into the covenant. Public. */
953
+ senderPubkey: Uint8Array;
954
+ /** How the preimage and the `sender` key are recovered later. Persist it
955
+ * with the record BEFORE funding. */
956
+ secrets: SwapSecrets;
957
+ }>;
958
+ /** Default floor for the window between the last moment the hold invoice can
959
+ * be paid and the solver's refund leaf opening. */
960
+ declare const MIN_CLAIM_WINDOW_SECONDS: number;
961
+ /**
962
+ * Bind the SOLVER's hold invoice to the quote and to the trader's own `H`.
963
+ *
964
+ * This is the only field the trader hands to a third party, and the only
965
+ * attack on this corridor with no on-chain trace: an invoice on some other
966
+ * payment hash is paid to the solver in full and no lockup on `H` is ever
967
+ * funded. NEVER publish an invoice that has not passed this.
968
+ *
969
+ * The decoder is injected — `@arkade-os/swap` takes no BOLT11 dependency — but
970
+ * unlike {@link requestLightningSend}, which takes the caller's facts about
971
+ * the caller's OWN invoice, the comparison lives here: a caller-supplied
972
+ * summary of an adversary's invoice checks nothing.
973
+ *
974
+ * There is no check for "is this actually a hold invoice": on the wire it is
975
+ * indistinguishable from an ordinary one.
976
+ *
977
+ * Reasons: `invoice_undecodable` | `invoice_hash_mismatch` |
978
+ * `invoice_amount_mismatch` | `quote_malformed`.
979
+ */
980
+ declare const verifyReceiveInvoice: (input: {
981
+ invoice: string;
982
+ decode: (bolt11: string) => InvoiceFacts;
983
+ /** `sha256(P)`, hex — the trader's OWN. */
984
+ paymentHash: string;
985
+ quote: RfqQuote;
986
+ }) => {
987
+ payDeadline: number;
988
+ };
989
+ /**
990
+ * The receive leg's gate, checked before the invoice is published. Separate
991
+ * from {@link assertFundable} because the semantics invert: `refund_locktime`
992
+ * belongs to the SOLVER here, so BIP-113's median-time-past lag extends the
993
+ * trader's claim window instead of shrinking it. What can actually run out is
994
+ * the hold invoice's own window — minutes, not the quote's hour — which is why
995
+ * the claim window is measured from `payDeadline`, the last moment a payer can
996
+ * arm the swap, and not from `now`.
997
+ *
998
+ * `maxPayAmount` is an opt-in absolute ceiling on what the payer is asked for:
999
+ * `assertQuotedAmount` pins the side the request named, so with
1000
+ * `amountSide: "to"` the price is the free variable. Optional because a bad
1001
+ * price is visible to the caller before anything is published — unlike an
1002
+ * opaque invoice or an underfunded lockup.
1003
+ *
1004
+ * Reasons: `quote_expired` | `missing_refund_locktime` | `claim_window_too_short` |
1005
+ * `price_too_high` | `quote_malformed` | `invalid_gate_input`.
1006
+ */
1007
+ declare const assertReceivable: (input: {
1008
+ quote: RfqQuote;
1009
+ /** From {@link verifyReceiveInvoice}: `min(invoice expiry, valid_until)`. */
1010
+ payDeadline: number;
1011
+ now: number;
1012
+ minClaimWindowSeconds?: number;
1013
+ /** Absolute sats ceiling on `from_amount`. */
1014
+ maxPayAmount?: number;
1015
+ }) => void;
1016
+ /** Compile the RECEIVE-direction VHTLC: the same eight-leaf tree as {@link
1017
+ * lightningSendVtxoScript} with the roles inverted — the trader is the
1018
+ * `receiver` (it generated `P` and claims the lockup with it), the solver is
1019
+ * the `sender` (it funds the lockup and holds the refund recourse). One
1020
+ * function shared by both receive corridors, mirroring the send legs' sharing
1021
+ * of `lightningSendVtxoScript`. */
1022
+ declare function receiveVtxoScript(params: {
1023
+ /** Binding field #1: the solver's x-only key, from the quote — VHTLC's
1024
+ * `sender` role on the receive corridors. */
1025
+ solverPubkey: Uint8Array;
1026
+ /** Binding field #2: the SOLVER's own refund deadline on these legs, from
1027
+ * the quote — after it the solver may reclaim an unclaimed lockup. */
1028
+ refundLocktime: number;
1029
+ /** The Ark server's x-only key — the trader's OWN connection. */
1030
+ serverPubkey: Uint8Array;
1031
+ /** `sha256(P)`, hex — the trader's OWN preimage hash. */
1032
+ paymentHash: string;
1033
+ /** From {@link unilateralClaimDelay} over the trader's OWN server info. */
1034
+ claimDelay: number;
1035
+ /** Emulator x-only key — see {@link requestLightningSend}'s parameter. */
1036
+ emulatorPubkey: Uint8Array;
1037
+ /** The solver's covenant refund destination, from the quote
1038
+ * (`profile.solver_refund_pk_script`) — the one tree parameter nothing
1039
+ * else on the wire determines. */
1040
+ solverRefundPkScript: Uint8Array;
1041
+ /** The trader's own x-only Arkade key — VHTLC's `receiver` role on these
1042
+ * legs, so the trader can claim without covclaimd. */
1043
+ payoutPubkey: Uint8Array;
1044
+ /** The trader's own Arkade payout pkScript (decoded from its payout
1045
+ * address) — `nonInteractiveClaim`'s pinned destination. */
1046
+ payoutPkScript: Uint8Array;
1047
+ }): InstanceType<typeof VHTLC.ScriptV2>;
1048
+ /**
1049
+ * The pure core of {@link requestLightningReceive}: derive the solver-funded
1050
+ * covenant locally from the quote's binding fields plus the trader's own data
1051
+ * and refuse on any address mismatch. The trader funds nothing on Arkade on
1052
+ * this leg — verification is still what makes paying the hold invoice safe:
1053
+ * the lockup the solver will fund must be the tree whose claim paths pay the
1054
+ * trader.
1055
+ */
1056
+ declare function deriveLightningReceive(input: {
1057
+ quote: RfqQuote;
1058
+ paymentHash: string;
1059
+ payoutPubkey: Uint8Array;
1060
+ payoutAddress: string;
1061
+ serverPubkey: Uint8Array;
1062
+ emulatorPubkey: Uint8Array;
1063
+ claimDelay: number;
1064
+ hrp: string;
1065
+ }): {
1066
+ address: string;
1067
+ swapPkScript: Uint8Array;
1068
+ script: InstanceType<typeof VHTLC.ScriptV2>;
1069
+ /** The solver's hold invoice on `H` — what the trader pays to arm the swap. */
1070
+ invoice: string;
1071
+ refundLocktime: number;
1072
+ };
1073
+ /**
1074
+ * The `lightning:BTC->arkade:BTC` user flow: quote → derive the covenant
1075
+ * locally → verify → gate. Returns the solver's hold invoice to PAY — the
1076
+ * payment itself is the trader's own Lightning wallet's job, exactly as
1077
+ * funding is the caller's job on the send corridors. Once the paid HTLC is
1078
+ * held, the solver funds the lockup; the trader claims it with `P` (its own,
1079
+ * generated here) — itself via the collaborative claim leaf
1080
+ * ({@link claimReceiveLockup} in `claim.ts`), or via covclaimd if it is
1081
+ * offline.
1082
+ *
1083
+ * Three obligations, all of them before the invoice is handed to a payer:
1084
+ *
1085
+ * 1. Persist `secrets` and `expectedAmount`. The preimage and the payout key
1086
+ * re-derive from `secrets` (or, on a non-HD wallet, are carried by it), and
1087
+ * without `expectedAmount` the claim has nothing to compare the funded
1088
+ * value against.
1089
+ * 2. Stay online. covclaimd cannot claim this covenant today, so the offline
1090
+ * path the claim packet exists for does not run yet: an unclaimed lockup is
1091
+ * reclaimed by the solver at `refund_locktime` and the payer refunded.
1092
+ * 3. On {@link LockupRegistrationFailed}, call this function again once the
1093
+ * store is working. The failed attempt is inert — it returned no invoice,
1094
+ * so nothing can be paid into the lockup nobody is watching — and the new
1095
+ * call derives its own preimage and `rfq_id`. Re-registering `error.script`
1096
+ * does NOT resume it: the invoice and `secrets` were never handed back.
1097
+ *
1098
+ * The invoice is the solver's, so it is verified here against the trader's own
1099
+ * `H` and the quote ({@link verifyReceiveInvoice}) before it is returned —
1100
+ * nothing publishable comes back from a failed check, registration included.
1101
+ * Pay before `invoiceExpiresAt`: the hold-invoice window is minutes, not the
1102
+ * quote's `valid_until`.
1103
+ */
1104
+ declare function requestLightningReceive(wallet: IWallet, arkServerUrl: string,
1105
+ /** Covenant co-signer (emulator) x-only key — same parameter, same
1106
+ * caller obligation, as {@link requestLightningSend}'s. */
1107
+ emulatorPubkey: Uint8Array, transport: RfqTransport, params: {
1108
+ amount: number;
1109
+ amountSide: "from" | "to";
1110
+ /** covclaimd's 33-byte compressed pubkey (from its own info endpoint)
1111
+ * — the claim packet seals to it and only it can ever read `P` early. */
1112
+ covclaimdPubkey: Uint8Array;
1113
+ /** The caller's own BOLT11 decoder, applied to the SOLVER's invoice.
1114
+ * Required: an optional verifier is one integrators skip, and this is
1115
+ * the check whose absence loses the whole payment. */
1116
+ decodeInvoice: (bolt11: string) => InvoiceFacts;
1117
+ /** Opt-in ceiling, in sats, on what the payer will be asked for. */
1118
+ maxPayAmount?: number;
1119
+ rfqId?: string;
1120
+ }): Promise<{
1121
+ rfqId: string;
1122
+ quote: RfqQuote;
1123
+ /** The solver's hold invoice — what the trader pays, for `payAmount`.
1124
+ * Verified against this swap's `H` and the quote's `from_amount`. */
1125
+ invoice: string;
1126
+ /** What the trader pays: the quote's `from_amount`. */
1127
+ payAmount: number;
1128
+ /** What the solver's lockup must carry: the quote's `to_amount`. Persist
1129
+ * it with the record — `pushClaim` refuses to publish `P` for less, and
1130
+ * captured at claim time instead it would be whatever the solver funded. */
1131
+ expectedAmount: number;
1132
+ /** Last moment the invoice can be paid, unix seconds: `min(invoice
1133
+ * expiry, valid_until)`. Absolute on purpose — a countdown returned from
1134
+ * here is stale before the caller reads it; derive one at display time. */
1135
+ invoiceExpiresAt: number;
1136
+ /** The trader's OWN derivation of the lockup the solver must fund. */
1137
+ address: string;
1138
+ swapPkScript: Uint8Array;
1139
+ script: InstanceType<typeof VHTLC.ScriptV2>;
1140
+ payoutAddress: string;
1141
+ /** The trader's covenant `receiver` key, bound into the tree. Public. */
1142
+ payoutPubkey: Uint8Array;
1143
+ /** How the preimage and the payout key are recovered later. Persist it
1144
+ * with the record BEFORE paying the invoice. */
1145
+ secrets: SwapSecrets;
1146
+ }>;
1147
+ /**
1148
+ * The pure core of {@link requestOnchainReceive}: derive BOTH contracts
1149
+ * locally — the solver-funded Arkade covenant and the L1 HTLC the trader
1150
+ * funds — and refuse on any mismatch. Binding: `solver_pubkey`,
1151
+ * `refund_locktime`, `claim_pubkey`, `htlc_locktime`, `min_confirmations`;
1152
+ * `lockup_address` and `htlc_address` are compare-only.
1153
+ */
1154
+ declare function deriveOnchainReceive(input: {
1155
+ quote: RfqQuote;
1156
+ paymentHash: string;
1157
+ payoutPubkey: Uint8Array;
1158
+ payoutAddress: string;
1159
+ /** The trader's own x-only L1 key — the HTLC's refund role. */
1160
+ refundPubkey: Uint8Array;
1161
+ serverPubkey: Uint8Array;
1162
+ emulatorPubkey: Uint8Array;
1163
+ claimDelay: number;
1164
+ hrp: string;
1165
+ l1Network: OnchainNetwork;
1166
+ }): {
1167
+ address: string;
1168
+ swapPkScript: Uint8Array;
1169
+ script: InstanceType<typeof VHTLC.ScriptV2>;
1170
+ /** The L1 HTLC the trader funds, derived locally — fund only this. */
1171
+ htlc: OnchainHtlc;
1172
+ refundLocktime: number;
1173
+ htlcLocktime: number;
1174
+ minConfirmations: number;
1175
+ };
1176
+ /**
1177
+ * The `onchain:BTC->arkade:BTC` user flow: quote → derive BOTH contracts
1178
+ * locally → verify → gate. Returns the L1 HTLC to fund (`htlc.address`, for
1179
+ * `fundAmount`) — the funding transaction itself is the trader's own L1
1180
+ * wallet's job, exactly as on the send corridors. After `min_confirmations`
1181
+ * the solver funds the Arkade lockup; the trader claims it with `P` (its
1182
+ * own), itself or via covclaimd.
1183
+ *
1184
+ * Persist `secrets` BEFORE funding, and note the direction's own deadline:
1185
+ * if the swap never settles, the L1 HTLC's refund leaf (the trader's
1186
+ * `refundPubkey`) opens at `htlc.refundLocktime` — `buildHtlcRefund` takes it
1187
+ * back from there.
1188
+ */
1189
+ declare function requestOnchainReceive(wallet: IWallet, arkServerUrl: string,
1190
+ /** Covenant co-signer (emulator) x-only key — same parameter, same
1191
+ * caller obligation, as {@link requestLightningSend}'s. */
1192
+ emulatorPubkey: Uint8Array, transport: RfqTransport, params: {
1193
+ amount: number;
1194
+ amountSide: "from" | "to";
1195
+ /** Trader's x-only L1 key for the HTLC's refund leaf. */
1196
+ refundPubkey: Uint8Array;
1197
+ /** covclaimd's 33-byte compressed pubkey — see {@link requestLightningReceive}. */
1198
+ covclaimdPubkey: Uint8Array;
1199
+ rfqId?: string;
1200
+ }): Promise<{
1201
+ rfqId: string;
1202
+ quote: RfqQuote;
1203
+ /** The trader's OWN derivation of the lockup the solver must fund. */
1204
+ address: string;
1205
+ /** What the trader's L1 funding must carry: the quote's `from_amount`. */
1206
+ fundAmount: number;
1207
+ /** What the solver's lockup must carry: the quote's `to_amount`. Persist
1208
+ * it with the record — see {@link requestLightningReceive}. */
1209
+ expectedAmount: number;
1210
+ swapPkScript: Uint8Array;
1211
+ script: InstanceType<typeof VHTLC.ScriptV2>;
1212
+ /** The EXPECTED L1 contract, derived locally — fund only this address. */
1213
+ htlc: OnchainHtlc;
1214
+ payoutAddress: string;
1215
+ payoutPubkey: Uint8Array;
1216
+ secrets: SwapSecrets;
1217
+ }>;
1218
+
1219
+ export { buildPreimageMessage as $, type AssetSwapRepository as A, BTC_ASSET_ID as B, type ChainSource as C, type DerivedSwapSecrets as D, RFQ_TERMINAL_STATES as E, type RefundBlockedReason as F, RefundNotLocallyPossibleError as G, type HtlcUtxo as H, InMemoryAssetSwapRepository as I, type RelaySocket as J, type RfqQuote as K, LIGHTNING_BTC as L, type MarketsCacheEntry as M, type RfqRefusalReason as N, type OnchainHtlc as O, SwapRefusal as P, type SwapSecrets as Q, type RfqStatus as R, type StoredSwapSecrets as S, addAssetSwap as T, adoptSwapDescriptor as U, arkadeSwapRequest as V, assertFundable as W, assertReceivable as X, awaitOnchainFill as Y, buildHtlcClaim as Z, buildHtlcRefund as _, type AssetSwap as a, claimOnchainFill as a0, classifyOnchainHtlc as a1, deriveLightningReceive as a2, deriveOnchainReceive as a3, deriveOnchainSend as a4, derivePreimage as a5, deriveSwapSecrets as a6, extractPreimage as a7, getAssetSwaps as a8, getAssetSwapsOrThrow as a9, unilateralClaimDelay as aA, unilateralRefundDelay as aB, unilateralRefundWithoutReceiverDelay as aC, updateAssetSwap as aD, updateAssetSwapBestEffort as aE, verifyLockupAddress as aF, verifyReceiveInvoice as aG, httpTransport as aa, isDeterministicSigner as ab, lightningReceiveRequest as ac, lightningSendRequest as ad, lightningSendVtxoScript as ae, newPreimage as af, newRfqId as ag, offerTermsFromQuote as ah, onchainHtlcScript as ai, onchainReceiveRequest as aj, onchainSendRequest as ak, paymentHashOf as al, preimageForRfqSecrets as am, randomSwapSecrets as an, receiveVtxoScript as ao, relayTransport as ap, requestLightningReceive as aq, requestLightningSend as ar, requestOnchainReceive as as, requestOnchainSend as at, rfqPair as au, rfqSecretsOfRecord as av, rfqSecretsToRecord as aw, senderIdentityForRfqSecrets as ax, senderIdentityForSwapRecord as ay, senderPubkeyForRfqSecrets as az, type RfqTransport as b, type ChainUtxo as c, type OnchainHtlcPhase as d, ARKADE_ASSET as e, ARKADE_BTC as f, AddressMismatch as g, type AssetSwapFallbackSecrets as h, type AssetSwapStatus as i, type DeterministicSigner as j, type InvoiceFacts as k, LIGHTNING_RECEIVE_PAIR as l, LIGHTNING_SEND_PAIR as m, MAX_MIN_CONFIRMATIONS as n, MIN_CLAIM_WINDOW_SECONDS as o, MIN_HEADROOM_SECONDS as p, ONCHAIN_BTC as q, ONCHAIN_CLAIM_MARGIN_SECONDS as r, ONCHAIN_DUST_SATS as s, ONCHAIN_ORDER_MARGIN_SECONDS as t, ONCHAIN_RECEIVE_PAIR as u, ONCHAIN_SECONDS_PER_BLOCK as v, ONCHAIN_SEND_PAIR as w, type OnchainHtlcParams as x, type OnchainNetwork as y, RFQ_PREIMAGE_TAG as z };