@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.
- package/README.md +562 -0
- package/dist/chunk-C5P7R7JT.js +1363 -0
- package/dist/index.cjs +3568 -0
- package/dist/index.d.cts +1747 -0
- package/dist/index.d.ts +1747 -0
- package/dist/index.js +2242 -0
- package/dist/nostr.cjs +223 -0
- package/dist/nostr.d.cts +99 -0
- package/dist/nostr.d.ts +99 -0
- package/dist/nostr.js +134 -0
- package/dist/rfq-CRgIOQ_y.d.cts +1219 -0
- package/dist/rfq-CRgIOQ_y.d.ts +1219 -0
- package/package.json +81 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1747 @@
|
|
|
1
|
+
import { asset, IWallet, arkade, RestIndexerProvider, Transaction, IContractManager, RestArkProvider, VHTLC, Identity } from '@arkade-os/sdk';
|
|
2
|
+
import { A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry, R as RfqStatus, b as RfqTransport, O as OnchainHtlc, C as ChainSource, c as ChainUtxo, d as OnchainHtlcPhase } from './rfq-CRgIOQ_y.js';
|
|
3
|
+
export { e as ARKADE_ASSET, f as ARKADE_BTC, g as AddressMismatch, h as AssetSwapFallbackSecrets, i as AssetSwapStatus, B as BTC_ASSET_ID, D as DerivedSwapSecrets, j as DeterministicSigner, H as HtlcUtxo, I as InMemoryAssetSwapRepository, k as InvoiceFacts, L as LIGHTNING_BTC, l as LIGHTNING_RECEIVE_PAIR, m as LIGHTNING_SEND_PAIR, n as MAX_MIN_CONFIRMATIONS, o as MIN_CLAIM_WINDOW_SECONDS, p as MIN_HEADROOM_SECONDS, q as ONCHAIN_BTC, r as ONCHAIN_CLAIM_MARGIN_SECONDS, s as ONCHAIN_DUST_SATS, t as ONCHAIN_ORDER_MARGIN_SECONDS, u as ONCHAIN_RECEIVE_PAIR, v as ONCHAIN_SECONDS_PER_BLOCK, w as ONCHAIN_SEND_PAIR, x as OnchainHtlcParams, y as OnchainNetwork, z as RFQ_PREIMAGE_TAG, E as RFQ_TERMINAL_STATES, F as RefundBlockedReason, G as RefundNotLocallyPossibleError, J as RelaySocket, K as RfqQuote, N as RfqRefusalReason, S as StoredSwapSecrets, P as SwapRefusal, Q as SwapSecrets, T as addAssetSwap, U as adoptSwapDescriptor, V as arkadeSwapRequest, W as assertFundable, X as assertReceivable, Y as awaitOnchainFill, Z as buildHtlcClaim, _ as buildHtlcRefund, $ as buildPreimageMessage, a0 as claimOnchainFill, a1 as classifyOnchainHtlc, a2 as deriveLightningReceive, a3 as deriveOnchainReceive, a4 as deriveOnchainSend, a5 as derivePreimage, a6 as deriveSwapSecrets, a7 as extractPreimage, a8 as getAssetSwaps, a9 as getAssetSwapsOrThrow, aa as httpTransport, ab as isDeterministicSigner, ac as lightningReceiveRequest, ad as lightningSendRequest, ae as lightningSendVtxoScript, af as newPreimage, ag as newRfqId, ah as offerTermsFromQuote, ai as onchainHtlcScript, aj as onchainReceiveRequest, ak as onchainSendRequest, al as paymentHashOf, am as preimageForRfqSecrets, an as randomSwapSecrets, ao as receiveVtxoScript, ap as relayTransport, aq as requestLightningReceive, ar as requestLightningSend, as as requestOnchainReceive, at as requestOnchainSend, au as rfqPair, av as rfqSecretsOfRecord, aw as rfqSecretsToRecord, ax as senderIdentityForRfqSecrets, ay as senderIdentityForSwapRecord, az as senderPubkeyForRfqSecrets, aA as unilateralClaimDelay, aB as unilateralRefundDelay, aC as unilateralRefundWithoutReceiverDelay, aD as updateAssetSwap, aE as updateAssetSwapBestEffort, aF as verifyLockupAddress, aG as verifyReceiveInvoice } from './rfq-CRgIOQ_y.js';
|
|
4
|
+
import { Network, LocalCardInput, DiscoveredMarket, Side, OfferPlan } from '@arkade-os/solver-discovery';
|
|
5
|
+
|
|
6
|
+
/** The contracts — pure data, shared verbatim with any other implementation. */
|
|
7
|
+
declare const swapPrograms: Record<"wantAsset" | "wantBtc", ReturnType<typeof arkade.parseArtifact>>;
|
|
8
|
+
/** A full-fill offer. Exactly one field names an asset: `wantAsset` set = the
|
|
9
|
+
* fill must deliver that asset (the deposit may be BTC or another asset,
|
|
10
|
+
* identified by the funding vtxo itself); `offerAsset` set = the user
|
|
11
|
+
* deposits that asset and wants sats. */
|
|
12
|
+
interface Offer {
|
|
13
|
+
/** The scriptPubKey of the swap contract. */
|
|
14
|
+
swapPkScript: Uint8Array;
|
|
15
|
+
/** Amount the user wants (asset units, or sats when wanting BTC). */
|
|
16
|
+
wantAmount: bigint;
|
|
17
|
+
/** The asset the user wants. Omitted when wanting BTC. */
|
|
18
|
+
wantAsset?: asset.AssetId;
|
|
19
|
+
/** The asset the user deposits. Omitted when depositing BTC. */
|
|
20
|
+
offerAsset?: asset.AssetId;
|
|
21
|
+
/** Maker's taproot scriptPubKey (34 bytes) — where the fill must pay. */
|
|
22
|
+
makerPkScript: Uint8Array;
|
|
23
|
+
/** Maker's x-only key (32 bytes) — the cancel path's `user` signer. */
|
|
24
|
+
makerPublicKey: Uint8Array;
|
|
25
|
+
/** Covenant co-signer (emulator) x-only key (32 bytes). */
|
|
26
|
+
emulatorPubkey: Uint8Array;
|
|
27
|
+
}
|
|
28
|
+
/** Compile the offer's contract: program + args -> taproot tree. */
|
|
29
|
+
declare function offerVtxoScript(offer: Omit<Offer, "swapPkScript">, serverPubkey: Uint8Array): InstanceType<typeof arkade.ArkadeProgramScript>;
|
|
30
|
+
/** Extension packet type tag for Arkade Intents offers. */
|
|
31
|
+
declare const OFFER_PACKET_TYPE = 3;
|
|
32
|
+
/** Serialize an offer to TLV bytes (the packet payload). */
|
|
33
|
+
declare function encodeOffer(offer: Offer): Uint8Array;
|
|
34
|
+
/** Parse TLV bytes into an offer. Throws on malformed or unknown records. */
|
|
35
|
+
declare function decodeOffer(data: Uint8Array): Offer;
|
|
36
|
+
/**
|
|
37
|
+
* Build a new offer for `wallet` (the user). Fund `address` with the side
|
|
38
|
+
* you deposit, embedding the returned extension, and the solver does the rest:
|
|
39
|
+
*
|
|
40
|
+
* // BTC -> asset
|
|
41
|
+
* const o = await createOffer(wallet, ARK, EMULATOR_PUBKEY, { wantAmount: 1000n, wantAsset })
|
|
42
|
+
* await wallet.send({ address: o.address, amount: 1000, extensions: [o.extension] })
|
|
43
|
+
*
|
|
44
|
+
* // asset -> BTC (the sats are the VTXO carrier for the asset)
|
|
45
|
+
* const o = await createOffer(wallet, ARK, EMULATOR_PUBKEY, { wantAmount: 1000n, offerAsset })
|
|
46
|
+
* await wallet.send({ address: o.address, amount: 500,
|
|
47
|
+
* assets: [{ assetId, amount: 1000n }],
|
|
48
|
+
* extensions: [o.extension] })
|
|
49
|
+
*
|
|
50
|
+
* Broadcasts nothing, but does write locally: the covenant is registered with
|
|
51
|
+
* the wallet's contract manager before the address is returned, so the deposit
|
|
52
|
+
* is watched from the moment it lands and is marked as escrow (see
|
|
53
|
+
* {@link registerOfferContract}). Registration deliberately happens *before*
|
|
54
|
+
* funding rather than after: nothing is at stake yet, so a failure can throw
|
|
55
|
+
* and be retried, where the same failure after `wallet.send` would leave a
|
|
56
|
+
* funded deposit unwatched with no way to notice.
|
|
57
|
+
*/
|
|
58
|
+
declare function createOffer(wallet: IWallet, arkServerUrl: string,
|
|
59
|
+
/** Covenant co-signer (emulator) x-only key — the SOLVER's deployment,
|
|
60
|
+
* not the user's. This library does NOT fetch or verify it: clients
|
|
61
|
+
* have no network path to the emulator, only the solver and covclaimd
|
|
62
|
+
* do. The caller must obtain this out-of-band, before calling this
|
|
63
|
+
* function, from the solver's signed registry/corridor card (its
|
|
64
|
+
* `emulator_pubkey`, added in arkade-os/solver-registry#18) or an
|
|
65
|
+
* equivalent source it
|
|
66
|
+
* independently trusts, and is responsible for having checked it
|
|
67
|
+
* against that trusted value itself. */
|
|
68
|
+
emulatorPubkey: Uint8Array, params: {
|
|
69
|
+
wantAmount: bigint;
|
|
70
|
+
wantAsset?: asset.AssetId;
|
|
71
|
+
offerAsset?: asset.AssetId;
|
|
72
|
+
}): Promise<{
|
|
73
|
+
/** The encoded offer, hex. **Persist this** — it is the only input
|
|
74
|
+
* `cancelOffer` needs to rebuild the covenant, and the restore scan reads
|
|
75
|
+
* the same bytes back off the funding tx into `AssetSwap.offerHex`. */
|
|
76
|
+
offerHex: string;
|
|
77
|
+
/** Ready for `wallet.send`'s `extensions` — the caller never handles the packet type. */
|
|
78
|
+
extension: {
|
|
79
|
+
type: number;
|
|
80
|
+
payload: Uint8Array;
|
|
81
|
+
};
|
|
82
|
+
/** The swap address to fund. Nothing exists on chain until the deposit
|
|
83
|
+
* lands here: `createOffer` is pure derivation and broadcasts nothing.
|
|
84
|
+
* Identical offers derive an identical address, so the funding txid — not
|
|
85
|
+
* the address — is what identifies one deposit. */
|
|
86
|
+
address: string;
|
|
87
|
+
/** The covenant's scriptPubKey: the key an indexer watches to spot the
|
|
88
|
+
* deposit and its later spend (`AssetSwap.swapPkScript`). */
|
|
89
|
+
swapPkScript: Uint8Array;
|
|
90
|
+
}>;
|
|
91
|
+
/**
|
|
92
|
+
* Cancel an offer: spend the swap VTXO back to the user. Returns the ark txid.
|
|
93
|
+
*
|
|
94
|
+
* This is the refund path — how a user takes back a deposit no solver filled.
|
|
95
|
+
* **Neither program carries a timelock**, so an unfilled deposit keeps its
|
|
96
|
+
* place at the swap address rather than expiring: no deadline to miss and no
|
|
97
|
+
* "expired" state to unwind, at the cost of the refund being something the
|
|
98
|
+
* user asks for rather than something a clock delivers.
|
|
99
|
+
*
|
|
100
|
+
* Both paths out of the covenant are deliberately asymmetric:
|
|
101
|
+
* - `fulfill` is signed by the **server alone**, but the covenant constrains
|
|
102
|
+
* it to pay output 0 to `makerWP` for at least `wantAmount` — a solver
|
|
103
|
+
* cannot take the deposit without delivering.
|
|
104
|
+
* - `cancel` is a **2-of-2 of the user and the server**, so cancelling is
|
|
105
|
+
* cooperative: the server co-signs. No solver signature is involved, so the
|
|
106
|
+
* refund never depends on the counterparty being reachable.
|
|
107
|
+
*
|
|
108
|
+
* Cancel therefore races a fill rather than pre-empting it. An offer the solver
|
|
109
|
+
* is filling in the same moment may be spent by `fulfill` first, in which case
|
|
110
|
+
* this throws "no spendable VTXO at the swap address" — which means the swap
|
|
111
|
+
* completed, not that anything failed. `restoreAssetSwaps` classifies the two
|
|
112
|
+
* spends apart afterwards by the leaf each took (see `classifySpend`).
|
|
113
|
+
*
|
|
114
|
+
* Marking the deposit as escrow (see {@link registerOfferContract}) does not
|
|
115
|
+
* close this path: the gate's subject is *implicit* coin selection, and cancel
|
|
116
|
+
* names its input outpoint explicitly. The user keeps the only spend route
|
|
117
|
+
* that was ever theirs to take.
|
|
118
|
+
*
|
|
119
|
+
* Identical offers derive the same address, so `fundingTxid` selects the exact
|
|
120
|
+
* deposit; without it the address must hold exactly one spendable VTXO — with
|
|
121
|
+
* several, cancel refuses to guess and throws.
|
|
122
|
+
* `swapAddress` (the funded address) pins the server key the covenant was
|
|
123
|
+
* built with, so cancel keeps working across a server signer rotation; without
|
|
124
|
+
* it a rotated key is detected and reported rather than reading as a missing
|
|
125
|
+
* VTXO.
|
|
126
|
+
*
|
|
127
|
+
* **When the matching swap record is present, this records its own outcome,
|
|
128
|
+
* and that is what makes the live watcher cheap.** `cancel` is a 2-of-2 of
|
|
129
|
+
* user and server, so a cancel can only be the user's own act: on a
|
|
130
|
+
* successful submit this *is* the authoritative answer, and writing it here
|
|
131
|
+
* means `watchOfferSwaps` has nothing left to decide for our own cancels — it
|
|
132
|
+
* sees a terminal record and leaves it alone. The status moves to `cancelling`
|
|
133
|
+
* first so a crash between submit and record leaves a marker rather than a
|
|
134
|
+
* swap that still looks pending.
|
|
135
|
+
*
|
|
136
|
+
* Passing a repository that does not contain the swap record is allowed: the
|
|
137
|
+
* cancel still submits and returns its txid, but no local status is written,
|
|
138
|
+
* so the watcher or restore scan must classify the spend later.
|
|
139
|
+
*
|
|
140
|
+
* When a local record exists, the txid is only knowable after `send()`
|
|
141
|
+
* returns, so a spend event that arrives in that window finds a `cancelling`
|
|
142
|
+
* record and classifies the spend by its covenant leaf instead — the same
|
|
143
|
+
* answer, one indexer read more.
|
|
144
|
+
*/
|
|
145
|
+
declare function cancelOffer(wallet: IWallet, arkServerUrl: string, offerHex: string, opts: {
|
|
146
|
+
repository: AssetSwapRepository;
|
|
147
|
+
fundingTxid?: string;
|
|
148
|
+
swapAddress?: string;
|
|
149
|
+
}): Promise<string>;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Quoting: solver discovery and the pricing guardrails around a plan.
|
|
153
|
+
*
|
|
154
|
+
* The user side of a swap — the layer that works out what one would cost
|
|
155
|
+
* before `offer` commits to it. This is request-for-quote like every other
|
|
156
|
+
* Arkade Intents corridor; what is specific here is that the quote resolves
|
|
157
|
+
* client-side, from the market card the solver publishes (its price feed and
|
|
158
|
+
* its fee) rather than over a relay roundtrip. The card commits a solver to a
|
|
159
|
+
* price; only a fill commits it to this swap, so nothing here is signed and no
|
|
160
|
+
* inventory is reserved.
|
|
161
|
+
*
|
|
162
|
+
* Relay-negotiated quotes are where every corridor converges, this one
|
|
163
|
+
* included. What stays specific to intra-Arkade is the settlement covenant,
|
|
164
|
+
* not the negotiation. See the README's Roles section for why this package
|
|
165
|
+
* says user and solver rather than maker and taker.
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
/** Shared quote options so every quote path agrees.
|
|
169
|
+
* No safety margin on top of the market fee: pricing drift between quote
|
|
170
|
+
* and fill is the solver's risk to manage, not the user's to prepay. */
|
|
171
|
+
declare const QUOTE_OPTIONS: {
|
|
172
|
+
readonly safetyBps: 0;
|
|
173
|
+
};
|
|
174
|
+
/** Feed fetcher with a short per-URL TTL cache. A quote UI refetches the
|
|
175
|
+
* market's price feed on every debounced keystroke, and public feeds
|
|
176
|
+
* (CoinGecko) rate-limit that burst hard enough that big amounts reliably die
|
|
177
|
+
* as "Quote unavailable" mid-typing — one feed value per TTL window is fresh
|
|
178
|
+
* enough for a preview whose rate is re-checked at fill anyway.
|
|
179
|
+
* ponytail: no stale-serve when the fetch itself fails; add one if feeds
|
|
180
|
+
* flake beyond the TTL window (cap the staleness — the feed value becomes
|
|
181
|
+
* the covenant floor, so an old price must never price a real offer).
|
|
182
|
+
* Keyed on the request URL, so it assumes a market's feed URL is stable and
|
|
183
|
+
* amount-invariant (true today); a cache-busting nonce would silently make it
|
|
184
|
+
* a no-op — the flat-feedCalls swap test guards against that regressing. */
|
|
185
|
+
declare const makeCachedFeedFetch: (ttlMs?: number, fetchImpl?: typeof fetch) => typeof fetch;
|
|
186
|
+
interface DiscoverMarketsOptions {
|
|
187
|
+
network: Network;
|
|
188
|
+
/** The network's solver registry index URL; no registry means no markets. */
|
|
189
|
+
registryUrl: string | undefined;
|
|
190
|
+
/** Backs the 1-hour markets cache and its stale fallback. Omit for a
|
|
191
|
+
* one-shot discovery that always hits the registry. */
|
|
192
|
+
repository?: AssetSwapRepository;
|
|
193
|
+
/** Locally pinned solver cards to merge with the registry's markets. */
|
|
194
|
+
localCards?: LocalCardInput[];
|
|
195
|
+
/** Receives discovery warnings (stale index, skipped cards, …). */
|
|
196
|
+
logger?: (...args: unknown[]) => void;
|
|
197
|
+
/** Custom fetch (tests, mobile runtimes). Defaults to global fetch.
|
|
198
|
+
* ponytail: no request deadline here — a caller that needs one wraps its
|
|
199
|
+
* own fetchImpl with an AbortSignal; add one if a hung registry ever
|
|
200
|
+
* strands discovery in practice. */
|
|
201
|
+
fetchImpl?: typeof fetch;
|
|
202
|
+
/** `false` forces a refetch past a fresh cache (a user-triggered reload).
|
|
203
|
+
* It does not disable the stale-cache fallback: an unreachable registry
|
|
204
|
+
* still serves the last known markets rather than none. */
|
|
205
|
+
useCache?: boolean;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Markets from the network's solver registry; [] when none is configured.
|
|
209
|
+
* Registry content changes rarely, so results are cached for an hour and a
|
|
210
|
+
* stale cache backstops an unreachable registry (quotes stay live either way).
|
|
211
|
+
*/
|
|
212
|
+
declare const discoverMarkets: (options: DiscoverMarketsOptions) => Promise<DiscoveredMarket[]>;
|
|
213
|
+
/** Best market for a from/to pair, in either orientation. `give` is the side
|
|
214
|
+
* the sender deposits; `wantSide` skips markets whose receive side is
|
|
215
|
+
* disabled (max = "0"). */
|
|
216
|
+
declare const findMarket: (markets: DiscoveredMarket[], fromId: string, toId: string) => {
|
|
217
|
+
market: DiscoveredMarket | null;
|
|
218
|
+
give: Side;
|
|
219
|
+
} | undefined;
|
|
220
|
+
type PlanError = "insufficient-balance" | "side-disabled" | "below-min" | "above-max" | "below-dust";
|
|
221
|
+
/** Validate a plan against the user's balance and the server dust limit. */
|
|
222
|
+
declare const validatePlan: (plan: OfferPlan, giveBalance: bigint, dust: bigint) => PlanError | undefined;
|
|
223
|
+
|
|
224
|
+
/** Browser backend over the SDK's shared IndexedDB manager — the same
|
|
225
|
+
* infrastructure the wallet already uses for its Boltz swap repository. */
|
|
226
|
+
declare class IndexedDbAssetSwapRepository implements AssetSwapRepository {
|
|
227
|
+
private readonly dbName;
|
|
228
|
+
readonly version: 1;
|
|
229
|
+
private dbPromise;
|
|
230
|
+
constructor(dbName?: string);
|
|
231
|
+
private ensureDb;
|
|
232
|
+
private readStore;
|
|
233
|
+
/** Every write in one place, so none of them can forget to await the
|
|
234
|
+
* commit. Requests need no individual await: a failed one aborts the
|
|
235
|
+
* transaction, which `txDone` reports. */
|
|
236
|
+
private write;
|
|
237
|
+
saveSwap(swap: AssetSwap): Promise<void>;
|
|
238
|
+
getAllSwaps(): Promise<AssetSwap[]>;
|
|
239
|
+
getScannedTxids(): Promise<Set<string>>;
|
|
240
|
+
markTxidsScanned(txids: Iterable<string>): Promise<void>;
|
|
241
|
+
getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
|
|
242
|
+
saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
|
|
243
|
+
/** All stores in one transaction: clearing swaps but keeping scanned txids
|
|
244
|
+
* would leave the restore scan permanently skipping those funding txs, so
|
|
245
|
+
* a partial clear must not be observable. */
|
|
246
|
+
clear(): Promise<void>;
|
|
247
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The subset of a wallet transaction record the swap scan reads.
|
|
252
|
+
*
|
|
253
|
+
* No `assets`: a spend is classified from the covenant leaf it took, read off
|
|
254
|
+
* the spending transaction itself (see {@link classifySpend}). A wallet
|
|
255
|
+
* record's asset field is a net delta — an asset offer's cancel moves the asset
|
|
256
|
+
* out and back, netting to nothing — so it cannot answer the question.
|
|
257
|
+
*/
|
|
258
|
+
interface Tx {
|
|
259
|
+
type: string;
|
|
260
|
+
/** The virtual (ark) txid; the funding tx's identity. */
|
|
261
|
+
redeemTxid: string;
|
|
262
|
+
boardingTxid?: string;
|
|
263
|
+
roundTxid?: string;
|
|
264
|
+
/** Unix seconds. */
|
|
265
|
+
createdAt?: number;
|
|
266
|
+
}
|
|
267
|
+
/** The indexer surface the restore scan needs — narrower than a full provider. */
|
|
268
|
+
type RestoreIndexer = Pick<RestIndexerProvider, "getVirtualTxs" | "getVtxos">;
|
|
269
|
+
/**
|
|
270
|
+
* What became of a deposit, as the spending transaction reports it.
|
|
271
|
+
*
|
|
272
|
+
* `indeterminate` is not a third outcome — it is the absence of one, and the
|
|
273
|
+
* caller decides whether to retry or accept a default.
|
|
274
|
+
*/
|
|
275
|
+
type SpendKind = "cancelled" | "fulfilled" | "indeterminate";
|
|
276
|
+
/**
|
|
277
|
+
* Classify a spend by the covenant leaf it took.
|
|
278
|
+
*
|
|
279
|
+
* The covenant's whole vocabulary is two leaves: `cancel` returns the deposit
|
|
280
|
+
* to the user, `fulfill` is the solver paying for it. A submitted ark tx keeps
|
|
281
|
+
* each input's `tapLeafScript`, so the spend *states* which one it used — this
|
|
282
|
+
* reads an answer rather than inferring one.
|
|
283
|
+
*
|
|
284
|
+
* **Hand it the transaction that actually spends the deposit outpoint, which is
|
|
285
|
+
* the checkpoint, not the ark tx.** A spend is two linked transactions: the
|
|
286
|
+
* checkpoint (`vtxo.spentBy`) takes the deposit outpoint and carries the
|
|
287
|
+
* covenant leaf, and the ark tx (`vtxo.arkTxId`) spends the checkpoint's output
|
|
288
|
+
* — carrying the same leaf, but over an outpoint that is not the deposit's. A
|
|
289
|
+
* caller that offers only the ark tx gets `indeterminate` for every real spend,
|
|
290
|
+
* which is exactly what the first version of this function did.
|
|
291
|
+
* {@link classifyDepositSpend} takes both and picks whichever answers.
|
|
292
|
+
*
|
|
293
|
+
* What it replaces, and why: the previous test asked what the transaction
|
|
294
|
+
* moved. That works only while the deposit is invisible to the wallet. Once the
|
|
295
|
+
* covenant is a registered contract the deposit joins the wallet's own coins,
|
|
296
|
+
* every wallet-level asset figure becomes a *net* delta, and an asset offer's
|
|
297
|
+
* cancel — asset out of the covenant, same asset back to the user — nets to
|
|
298
|
+
* zero and reads exactly like its fill. Leaves do not have that failure mode,
|
|
299
|
+
* and they also survive batching: a solver filling several offers in one tx
|
|
300
|
+
* gives each input its own leaf.
|
|
301
|
+
*
|
|
302
|
+
* `serverPubkey` must be the key the covenant was *funded* against. If it has
|
|
303
|
+
* rotated since, the rebuilt script will not match the offer's own
|
|
304
|
+
* `swapPkScript` and this returns `indeterminate` rather than guessing —
|
|
305
|
+
* `cancelOffer` diagnoses the same mismatch the same way.
|
|
306
|
+
*/
|
|
307
|
+
declare function classifySpend(offer: Offer, serverPubkey: Uint8Array, spendTx: Transaction, deposit: {
|
|
308
|
+
txid: string;
|
|
309
|
+
vout: number;
|
|
310
|
+
}): SpendKind;
|
|
311
|
+
/**
|
|
312
|
+
* The txids that may hold a deposit's spend, in the order worth trying: the
|
|
313
|
+
* checkpoint first, since it is the one carrying the deposit outpoint.
|
|
314
|
+
*/
|
|
315
|
+
declare const spendTxidsOf: (vtxo: {
|
|
316
|
+
spentBy?: string;
|
|
317
|
+
arkTxId?: string;
|
|
318
|
+
}) => string[];
|
|
319
|
+
/**
|
|
320
|
+
* Classify a deposit's spend across both halves of it.
|
|
321
|
+
*
|
|
322
|
+
* A caller holds two txids for one spend — `spentBy` (the checkpoint, which
|
|
323
|
+
* takes the deposit outpoint) and `arkTxId` (the ark tx built on it) — and
|
|
324
|
+
* cannot tell from the outside which shape a given deployment produced: for a
|
|
325
|
+
* settlement they may be the same id. Try each and take the first definite
|
|
326
|
+
* answer, so the classification does not depend on that distinction.
|
|
327
|
+
*/
|
|
328
|
+
declare function classifyDepositSpend(offer: Offer, serverPubkey: Uint8Array, spendTxs: Iterable<Transaction>, deposit: {
|
|
329
|
+
txid: string;
|
|
330
|
+
vout: number;
|
|
331
|
+
}): SpendKind;
|
|
332
|
+
/**
|
|
333
|
+
* Scan the given candidates for offer packets and rebuild the AssetSwap
|
|
334
|
+
* records the store lost. Returns the rebuilt swaps plus the txids that got
|
|
335
|
+
* an authoritative answer (fetched fine, vtxo lookup fine) — the caller
|
|
336
|
+
* persists those so they are never fetched again.
|
|
337
|
+
*
|
|
338
|
+
* ## A spent deposit is classified or left alone — never guessed
|
|
339
|
+
*
|
|
340
|
+
* Whether a spent deposit was filled or cancelled is read from the spending
|
|
341
|
+
* transaction's covenant leaf ({@link classifySpend}), fetched from the same
|
|
342
|
+
* indexer as everything else rather than from the `txs` you pass. A spend that
|
|
343
|
+
* cannot be classified — not fetchable yet, or gone by neither leaf — leaves
|
|
344
|
+
* the funding txid unanswered, so nothing is persisted and a later scan decides
|
|
345
|
+
* it. Nothing sticky is written on a guess.
|
|
346
|
+
*
|
|
347
|
+
* That is a deliberate reversal: this used to restore an unclassifiable spend
|
|
348
|
+
* as `fulfilled`, which a persisted record then made permanent, because the
|
|
349
|
+
* spending tx came from the caller's possibly-lagging history. It no longer
|
|
350
|
+
* does, so the `existingIds` escape hatch is no longer a correction mechanism
|
|
351
|
+
* for a wrong label — it is only a skip list.
|
|
352
|
+
*
|
|
353
|
+
* `serverPubkey` must be the server key the covenants were funded against; a
|
|
354
|
+
* key that has rotated since makes every affected swap unclassifiable rather
|
|
355
|
+
* than misclassified.
|
|
356
|
+
*/
|
|
357
|
+
declare function restoreAssetSwaps(indexer: RestoreIndexer, txs: Tx[], existingIds: ReadonlySet<string>, opts: {
|
|
358
|
+
serverPubkey: Uint8Array;
|
|
359
|
+
scanned?: ReadonlySet<string>;
|
|
360
|
+
}): Promise<{
|
|
361
|
+
restored: AssetSwap[];
|
|
362
|
+
scannedTxids: string[];
|
|
363
|
+
}>;
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The record change a classified spend implies, or `undefined` when it implies
|
|
367
|
+
* none — an already-resolved swap, or a spend nobody could classify.
|
|
368
|
+
*
|
|
369
|
+
* Pure, so a consumer with its own store can apply the same transition without
|
|
370
|
+
* taking the watcher, and so re-delivery of an event is a no-op rather than a
|
|
371
|
+
* rewrite.
|
|
372
|
+
*/
|
|
373
|
+
declare function spendUpdate(swap: AssetSwap, spend: {
|
|
374
|
+
txid: string;
|
|
375
|
+
kind: SpendKind;
|
|
376
|
+
at?: number;
|
|
377
|
+
}): Partial<Omit<AssetSwap, "id">> | undefined;
|
|
378
|
+
/** A running watcher. `idle()` exists because the writes are async: shutdown
|
|
379
|
+
* and tests both need to know when in-flight updates have settled. */
|
|
380
|
+
interface OfferSwapWatcher {
|
|
381
|
+
stop(): void;
|
|
382
|
+
idle(): Promise<void>;
|
|
383
|
+
}
|
|
384
|
+
interface WatchOfferSwapsParams {
|
|
385
|
+
wallet: IWallet;
|
|
386
|
+
/** Same URL `createOffer`/`cancelOffer` take; used to read a spending tx
|
|
387
|
+
* when the exact classifier cannot answer. */
|
|
388
|
+
arkServerUrl: string;
|
|
389
|
+
repository: AssetSwapRepository;
|
|
390
|
+
/** Called after a change is persisted. A notification, not a store. */
|
|
391
|
+
onUpdate?: (swap: AssetSwap) => void;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Subscribe to the wallet's contract events and drive offer swap status.
|
|
395
|
+
*
|
|
396
|
+
* Registration ({@link createOffer}) is what makes this possible: only a
|
|
397
|
+
* registered covenant is watched, so only registered offers produce events.
|
|
398
|
+
* Offers funded before registration existed stay on the restore scan.
|
|
399
|
+
*
|
|
400
|
+
* This depends on the wallet's contract event transport. In Node, callers must
|
|
401
|
+
* provide an `EventSource` implementation or use a runtime where it is enabled;
|
|
402
|
+
* otherwise live updates do not arrive and restore remains the fallback.
|
|
403
|
+
*/
|
|
404
|
+
declare function watchOfferSwaps({ wallet, arkServerUrl, repository, onUpdate, }: WatchOfferSwapsParams): Promise<OfferSwapWatcher>;
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Coverage: whether an offer's script is in the wallet's watched set.
|
|
408
|
+
*
|
|
409
|
+
* Both edges live here because they are one decision seen from two sides.
|
|
410
|
+
* `createOffer` promotes a script the moment it hands the user an address to
|
|
411
|
+
* fund ({@link promoteOfferContract}); the watcher and the restore consumer
|
|
412
|
+
* retire it once nothing at that script holds funds any more
|
|
413
|
+
* ({@link retireSettledOfferContracts}). Identical offers derive one script, so
|
|
414
|
+
* those two can name the same row — and a demotion that lands after a promotion
|
|
415
|
+
* recreates exactly the failure the promotion exists to prevent: an address the
|
|
416
|
+
* user was told to fund, out of the subscription, the poll and every sync.
|
|
417
|
+
*
|
|
418
|
+
* Two things keep them apart:
|
|
419
|
+
*
|
|
420
|
+
* - **Per-script serialization.** `setContractWatchState` is a read-modify-write
|
|
421
|
+
* over the contract row, so promotion and demotion are ordered here rather
|
|
422
|
+
* than left to interleave inside the manager.
|
|
423
|
+
* - **The issuance mark.** A swap record is keyed by its funding txid, so an
|
|
424
|
+
* offer that has been created but not yet funded has no record at all — it is
|
|
425
|
+
* invisible to a liveness check over records, for as long as the user takes
|
|
426
|
+
* to send. {@link promoteOfferContract} therefore records the issuance
|
|
427
|
+
* itself, and a script with an address still waiting for its deposit is never
|
|
428
|
+
* retired.
|
|
429
|
+
*
|
|
430
|
+
* The mark is per process: a second wallet context (another tab, a service
|
|
431
|
+
* worker) issuing an offer while this one retires the same script is not
|
|
432
|
+
* covered, and neither is an address issued before a restart and funded after
|
|
433
|
+
* it. Both leave a funded-but-unwatched row that the restore scan still finds —
|
|
434
|
+
* the same backstop every other best-effort step here relies on.
|
|
435
|
+
*/
|
|
436
|
+
|
|
437
|
+
/** The one contract-manager capability changing coverage needs. */
|
|
438
|
+
type OfferContractRetirer = Pick<IContractManager, "setContractWatchState">;
|
|
439
|
+
/**
|
|
440
|
+
* Retire every offer script in `swaps` that no live record still holds — the
|
|
441
|
+
* batch form of what the watcher does per spend event, for a consumer that
|
|
442
|
+
* applies {@link restoreAssetSwaps} results without running the watcher.
|
|
443
|
+
*
|
|
444
|
+
* Takes the caller's full record list: liveness is a property of all records at
|
|
445
|
+
* a script, so a partial list would retire a script another record still holds.
|
|
446
|
+
*/
|
|
447
|
+
declare function retireSettledOfferContracts(manager: OfferContractRetirer, swaps: AssetSwap[]): Promise<void>;
|
|
448
|
+
|
|
449
|
+
interface SealedClaimPacket {
|
|
450
|
+
/** `ephPub(33) ‖ nonce(12) ‖ ciphertext`, base64 — wire-ready. This is
|
|
451
|
+
* the whole packet: the RFQ request's `claim_packet` field carries
|
|
452
|
+
* exactly this string. */
|
|
453
|
+
ciphertext: string;
|
|
454
|
+
}
|
|
455
|
+
interface ClaimPacketInput {
|
|
456
|
+
preimage: Uint8Array;
|
|
457
|
+
/** covclaimd's public key, 33-byte compressed (from its /v1 info). */
|
|
458
|
+
covclaimdPubkey: Uint8Array;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Seal a preimage to covclaimd. WebCrypto AES-GCM, hence async.
|
|
462
|
+
*
|
|
463
|
+
* The ephemeral key and nonce are generated here and CANNOT be supplied by a
|
|
464
|
+
* caller. That is the point of this signature: AES-GCM under a repeated
|
|
465
|
+
* (key, nonce) pair is a total break — forgery and plaintext recovery, not a
|
|
466
|
+
* degradation — so an optional `nonce` on a production export is a loaded gun
|
|
467
|
+
* whatever the doc comment says. Deterministic sealing lives in the test
|
|
468
|
+
* helper, where no consumer reaches it by accident.
|
|
469
|
+
*/
|
|
470
|
+
declare function sealClaimPacket(input: ClaimPacketInput): Promise<SealedClaimPacket>;
|
|
471
|
+
|
|
472
|
+
/** True for the states after which the solver will report nothing further. */
|
|
473
|
+
declare const isRfqTerminal: (state: string) => boolean;
|
|
474
|
+
/**
|
|
475
|
+
* The terminal states that mean the swap is OVER and the lockup is already
|
|
476
|
+
* gone — the solver either claimed it (`settled`, revealing the preimage) or
|
|
477
|
+
* returned it (`refunded`). A trader seeing one of these has nothing left to
|
|
478
|
+
* do.
|
|
479
|
+
*
|
|
480
|
+
* Deliberately narrower than {@link RFQ_TERMINAL_STATES}: `refused`, `expired`
|
|
481
|
+
* and `stuck` are terminal for the NEGOTIATION but say nothing about whether
|
|
482
|
+
* the trader's sats are still sitting at the lockup. A trader that funded just
|
|
483
|
+
* as the quote expired, or whose solver wedged mid-payment, is exactly the
|
|
484
|
+
* trader who needs the refund most — so those states must not short-circuit
|
|
485
|
+
* it. {@link refundIfUnresolved} treats them as "keep going", and lets the
|
|
486
|
+
* on-chain VTXO lookup be the authority on whether anything is actually there.
|
|
487
|
+
*/
|
|
488
|
+
declare const RFQ_RESOLVED_STATES: readonly ["settled", "refunded"];
|
|
489
|
+
/**
|
|
490
|
+
* Poll a swap's status until it reaches a terminal state.
|
|
491
|
+
*
|
|
492
|
+
* Same shape and conventions as {@link awaitOnchainFill}: a `pollMs` interval,
|
|
493
|
+
* an optional unix-seconds `deadline`, and a thrown error carrying a stable
|
|
494
|
+
* `reason` when that deadline passes.
|
|
495
|
+
*
|
|
496
|
+
* A `null` status (the solver has no record of this `rfq_id`) is treated as
|
|
497
|
+
* "not yet", not as an answer — a status route can legitimately 404 for a
|
|
498
|
+
* moment after a quote is issued. The deadline is what bounds that wait.
|
|
499
|
+
*
|
|
500
|
+
* Transport errors are NOT swallowed; a failing `status()` call rejects this
|
|
501
|
+
* function. Callers polling across a long refund window should expect to
|
|
502
|
+
* restart it after a network blip — nothing is lost by doing so, since the
|
|
503
|
+
* refund path this feeds is gated on an absolute timelock that does not
|
|
504
|
+
* expire.
|
|
505
|
+
*/
|
|
506
|
+
declare function awaitRfqResolution(transport: RfqTransport, rfqId: string, options?: {
|
|
507
|
+
pollMs?: number;
|
|
508
|
+
deadline?: number;
|
|
509
|
+
}): Promise<RfqStatus>;
|
|
510
|
+
/** The Ark surface the refund push needs — narrower than a full provider, and
|
|
511
|
+
* satisfied by {@link RestArkProvider}. Same seam style as `RestoreIndexer`. */
|
|
512
|
+
type RefundArkProvider = Pick<RestArkProvider, "getInfo" | "submitTx" | "finalizeTx">;
|
|
513
|
+
/** The indexer surface the lockup lookup needs. */
|
|
514
|
+
type RefundIndexer = Pick<RestIndexerProvider, "getVtxos">;
|
|
515
|
+
/** A still-refundable virtual output sitting at the swap lockup. */
|
|
516
|
+
interface LockupVtxo {
|
|
517
|
+
txid: string;
|
|
518
|
+
vout: number;
|
|
519
|
+
value: number;
|
|
520
|
+
/**
|
|
521
|
+
* The batch this output lived in expired and the operator swept it, so it
|
|
522
|
+
* is no longer a live leaf — it can be RECOVERED, but not spent offchain.
|
|
523
|
+
*
|
|
524
|
+
* It is still the trader's money and it is still visible, which is why
|
|
525
|
+
* {@link findLockupVtxos} returns it. What it is not is refundable by
|
|
526
|
+
* {@link pushRefundWithoutReceiver}: that builds an offchain Ark
|
|
527
|
+
* transaction, and the SDK's own predicates make the two states mutually
|
|
528
|
+
* exclusive — `canSpendOffchain` is false exactly when `canRecoverOnchain`
|
|
529
|
+
* is true (`wallet/vtxo.ts`), and the latter is documented as "must be
|
|
530
|
+
* recovered into a fresh batch rather than spent offchain". Holding the
|
|
531
|
+
* trader's `sender` key does not change that; a sweep removes the leaf from
|
|
532
|
+
* the live tree, not the signature from the trader.
|
|
533
|
+
*
|
|
534
|
+
* `packages/boltz-swap` splits on exactly this fact rather than working
|
|
535
|
+
* around it: `settleRefundWithoutReceiver` sends a live VTXO through an
|
|
536
|
+
* offchain tx and a recoverable one through `joinBatch` — "a swept
|
|
537
|
+
* (recoverable) VTXO is no longer a live leaf, so it can only be reclaimed
|
|
538
|
+
* by re-registering it into a batch".
|
|
539
|
+
*
|
|
540
|
+
* So the remedy is recovery (renewing the output into a fresh batch),
|
|
541
|
+
* after which the ordinary CLTV refund works again. This package does not
|
|
542
|
+
* build that round — see {@link pushRefundWithoutReceiver}, which refuses
|
|
543
|
+
* rather than submitting a spend that cannot succeed.
|
|
544
|
+
*/
|
|
545
|
+
recoverable: boolean;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Thrown when a refund was asked for over outputs that have been swept.
|
|
549
|
+
*
|
|
550
|
+
* Carries the outpoints so a caller can act — recover exactly those, then
|
|
551
|
+
* retry — instead of reading a server rejection and guessing. `reason` follows
|
|
552
|
+
* the same convention as `awaitOnchainFill`'s `fill_timeout` and
|
|
553
|
+
* `claimOnchainFill`'s `claim_window_closed`.
|
|
554
|
+
*
|
|
555
|
+
* **The remedy already exists; this package does not reimplement it.** The SDK
|
|
556
|
+
* recovers swept outputs by re-registering them into a fresh batch, through
|
|
557
|
+
* `IVtxoManager.recoverVtxos()` — the same batch round `packages/boltz-swap`
|
|
558
|
+
* reaches via its own `joinBatch`. It reads the wallet's registered-contract
|
|
559
|
+
* snapshot (`recoverVtxos` → `wallet.getVtxos({ withRecoverable: true })` →
|
|
560
|
+
* `contractSnapshot()` → `contractManager.getContractsWithVtxos()`), so it
|
|
561
|
+
* covers a swap lockup as soon as that lockup is registered as a contract —
|
|
562
|
+
* which is what {@link RfqSwapManagerDeps.contracts} does. Registration is
|
|
563
|
+
* therefore not only a latency optimization; it is what turns a swept lockup
|
|
564
|
+
* from a dead end into something the ordinary wallet path can recover.
|
|
565
|
+
*
|
|
566
|
+
* Two caveats a caller must hold, neither enforceable from here:
|
|
567
|
+
*
|
|
568
|
+
* - **The wallet must hold the lockup's `sender` key**, because recovery
|
|
569
|
+
* settles through `refundWithoutReceiver` — the leaf `vhtlc-v2` annotates
|
|
570
|
+
* these VTXOs with.
|
|
571
|
+
* - **`refundLocktime` must have matured.** That leaf carries a CLTV, so a
|
|
572
|
+
* recovery round including this VTXO earlier is rejected. `recoverVtxos`
|
|
573
|
+
* sweeps every recoverable output in ONE settlement and has no CLTV
|
|
574
|
+
* awareness, so recovering early can fail the whole batch rather than just
|
|
575
|
+
* this output. `packages/boltz-swap` encodes the same rule as "pre-CLTV
|
|
576
|
+
* recoverable → skipped".
|
|
577
|
+
*/
|
|
578
|
+
declare class LockupNeedsRecoveryError extends Error {
|
|
579
|
+
readonly name = "LockupNeedsRecoveryError";
|
|
580
|
+
readonly reason = "needs_recovery";
|
|
581
|
+
/** `txid:vout` for each output that must be recovered first. */
|
|
582
|
+
readonly outpoints: string[];
|
|
583
|
+
/**
|
|
584
|
+
* The contract's `refundLocktime`. Recovering before this matures is the
|
|
585
|
+
* hazard described above: `recoverVtxos()` sweeps EVERY recoverable output
|
|
586
|
+
* into one settlement with no CLTV awareness, so an early attempt can fail
|
|
587
|
+
* the whole batch — including unrelated outputs that were otherwise fine.
|
|
588
|
+
*
|
|
589
|
+
* Exposed as a value, not only inside the message, so a caller can encode
|
|
590
|
+
* `packages/boltz-swap`'s "pre-CLTV recoverable → skipped" rule without
|
|
591
|
+
* parsing prose. Seconds-based locktimes mature against the chain tip's
|
|
592
|
+
* timestamp rather than wall clock, so treat this as a floor to wait past,
|
|
593
|
+
* not an exact alarm.
|
|
594
|
+
*/
|
|
595
|
+
readonly recoverableAfter: bigint;
|
|
596
|
+
constructor(outpoints: string[], recoverableAfter: bigint);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Every output still sitting at the lockup script — spendable AND
|
|
600
|
+
* swept-but-recoverable, each tagged with which it is.
|
|
601
|
+
*
|
|
602
|
+
* All of them, not the first: a trader may fund a lockup in more than one
|
|
603
|
+
* send, and refunding only `vtxos[0]` returns part of the money and strands
|
|
604
|
+
* the rest at a script whose other refund paths are all longer.
|
|
605
|
+
*
|
|
606
|
+
* BOTH queries, because they are disjoint sets and `spendableOnly` alone goes
|
|
607
|
+
* blind at exactly the wrong moment. A lockup whose batch expiry passed is
|
|
608
|
+
* swept into the recoverable set, and this function exists to serve swaps that
|
|
609
|
+
* sat unresolved — which are precisely the ones most likely to have got there.
|
|
610
|
+
* Reading only the spendable set would report `nothing_to_refund` over money
|
|
611
|
+
* that is still sitting at the script, which is worse than an error: it looks
|
|
612
|
+
* like a resolved swap. `packages/boltz-swap` merges the same two queries for
|
|
613
|
+
* the same reason (`arkade-swaps.ts`'s `refundableVtxos`).
|
|
614
|
+
*
|
|
615
|
+
* **Visible is not the same as refundable.** A `recoverable` output cannot be
|
|
616
|
+
* spent offchain at all — see {@link LockupVtxo.recoverable} — so this set is
|
|
617
|
+
* "what is there", not "what {@link pushRefundWithoutReceiver} can take back".
|
|
618
|
+
* That function refuses the recoverable ones by name rather than submitting a
|
|
619
|
+
* spend the server must reject.
|
|
620
|
+
*
|
|
621
|
+
* This read — not the RFQ's reported state — is the authority on whether
|
|
622
|
+
* there is anything left at the lockup.
|
|
623
|
+
*
|
|
624
|
+
* **Not replaced by the contract manager's VTXO state, deliberately.** Once a
|
|
625
|
+
* lockup is registered (see `RfqSwapManagerDeps.contracts`) the wallet tracks
|
|
626
|
+
* these same outputs, and `getContractsWithVtxos` plus `canSpendOffchain` /
|
|
627
|
+
* `canRecoverOnchain` would classify them. That is a WEAKER answer here on two
|
|
628
|
+
* counts: it serves the wallet REPOSITORY, which a degraded sync will happily
|
|
629
|
+
* hand back stale (`getSyncState()` reports `degraded` and returns cached rows
|
|
630
|
+
* rather than failing), and its height-based expiry test needs a chain tip this
|
|
631
|
+
* module does not have. The two queries below ask the indexer itself and need
|
|
632
|
+
* neither. Ask-the-indexer, don't-trust-local-state — the same posture
|
|
633
|
+
* {@link readLockupFate} takes, and for the same reason: this decides money.
|
|
634
|
+
*/
|
|
635
|
+
declare function findLockupVtxos(indexer: RefundIndexer, swapPkScript: Uint8Array): Promise<LockupVtxo[]>;
|
|
636
|
+
/**
|
|
637
|
+
* The indexer surface the lockup-spend read needs: the vtxo lookup, plus the
|
|
638
|
+
* raw transactions those vtxos were spent by. Same narrow-seam style as
|
|
639
|
+
* {@link RefundIndexer} and `restore.ts`'s `RestoreIndexer`, and satisfied by
|
|
640
|
+
* {@link RestIndexerProvider}.
|
|
641
|
+
*/
|
|
642
|
+
type LockupSpendIndexer = Pick<RestIndexerProvider, "getVtxos" | "getVirtualTxs">;
|
|
643
|
+
/**
|
|
644
|
+
* What chain data says became of a swap lockup — the whole answer, with no
|
|
645
|
+
* solver involvement and nothing taken on the solver's word.
|
|
646
|
+
*/
|
|
647
|
+
type LockupFate =
|
|
648
|
+
/** At least one output at the lockup is still unspent. Not over. */
|
|
649
|
+
{
|
|
650
|
+
fate: "open";
|
|
651
|
+
}
|
|
652
|
+
/** Spent by a witness carrying a preimage that HASHES to the quote's
|
|
653
|
+
* `payment_hash`. Only the claim leaf can reveal one, and the only
|
|
654
|
+
* legitimate way the solver obtains it is by completing its side. */
|
|
655
|
+
| {
|
|
656
|
+
fate: "claimed";
|
|
657
|
+
preimage: Uint8Array;
|
|
658
|
+
}
|
|
659
|
+
/** Fully spent, and nothing that spent it revealed a matching preimage —
|
|
660
|
+
* so the money went back to the trader. See {@link readLockupFate}. */
|
|
661
|
+
| {
|
|
662
|
+
fate: "returned";
|
|
663
|
+
}
|
|
664
|
+
/** Nothing was learned: no outputs visible, an output spent by nothing the
|
|
665
|
+
* indexer names, a spend it could not produce, or a blob that would not
|
|
666
|
+
* decode. Never an answer. */
|
|
667
|
+
| {
|
|
668
|
+
fate: "unknown";
|
|
669
|
+
};
|
|
670
|
+
/**
|
|
671
|
+
* Decide from chain data alone whether a swap lockup settled, came back, or is
|
|
672
|
+
* still live.
|
|
673
|
+
*
|
|
674
|
+
* **Why this is decidable without asking anyone.** The lockup's claim leaf can
|
|
675
|
+
* only be spent by revealing `P`, so a spend witness carrying a value that
|
|
676
|
+
* hashes to the quote's `payment_hash` is proof the claim leaf was used — and
|
|
677
|
+
* the only legitimate way the counterparty obtains `P` is by completing its
|
|
678
|
+
* side of the swap. Every OTHER leaf is a refund: `nonInteractiveRefund` is
|
|
679
|
+
* covenant-pinned to the trader's own address (`enforcePayTo(senderPkScript)`),
|
|
680
|
+
* and `refund`, `refundWithoutReceiver`, `unilateralRefund` and
|
|
681
|
+
* `unilateralRefundWithoutReceiver` all require the trader's own signature. So
|
|
682
|
+
* "spent, but not by a hash-verified claim" means the money went back to the
|
|
683
|
+
* trader, and nothing here has to trust a counterparty to say so.
|
|
684
|
+
*
|
|
685
|
+
* **A matching witness SHAPE is not proof.** Only a candidate that hashes to
|
|
686
|
+
* `paymentHash` may be read as a claim; a 32-byte item that hashes to anything
|
|
687
|
+
* else is just bytes, and is treated as a refund. Getting this wrong in the
|
|
688
|
+
* permissive direction would report "settled" for a swap that actually
|
|
689
|
+
* refunded, which is precisely the fact a trader is relying on.
|
|
690
|
+
*
|
|
691
|
+
* **`unknown` is not `returned`.** An empty vtxo set (indexer lag, or a lockup
|
|
692
|
+
* not visible yet), a `spentBy` the indexer cannot produce a transaction for,
|
|
693
|
+
* or a blob that will not decode all come back as `unknown`. `getVirtualTxs`
|
|
694
|
+
* may legitimately return fewer transactions than were asked for, so the
|
|
695
|
+
* observed set is counted rather than assumed complete. The caller's correct
|
|
696
|
+
* response to `unknown` is the same as to `open`: keep watching, and let the
|
|
697
|
+
* refund timelock — which no outage can move — be what ends the wait.
|
|
698
|
+
*
|
|
699
|
+
* Ask-the-indexer, don't-trust-local-state: read fresh on every poll, never
|
|
700
|
+
* cached, the same posture {@link findLockupVtxos} already establishes.
|
|
701
|
+
*/
|
|
702
|
+
declare function readLockupFate(indexer: LockupSpendIndexer, input: {
|
|
703
|
+
swapPkScript: Uint8Array;
|
|
704
|
+
/** `sha256(P)`, hex — the quote's `payment_hash`. */
|
|
705
|
+
paymentHash: string;
|
|
706
|
+
}): Promise<LockupFate>;
|
|
707
|
+
/**
|
|
708
|
+
* Build, sign, and push the `refundWithoutReceiver` spend: return every funded
|
|
709
|
+
* output at the lockup to the trader's refund address.
|
|
710
|
+
*
|
|
711
|
+
* The leaf is `CLTV(refundLocktime) + <sender> + <server>` — the trader's own
|
|
712
|
+
* VHTLC `sender` key and the Arkade server, and NOBODY else. In particular the
|
|
713
|
+
* emulator is not involved: it co-signs only the two covenant leaves
|
|
714
|
+
* (`nonInteractiveClaim` / `nonInteractiveRefund`), which is why the solver's
|
|
715
|
+
* own escape hatch has to go through it and this one does not. So unlike that
|
|
716
|
+
* push, this transaction is submitted SIGNED, and the only counterparty is the
|
|
717
|
+
* Arkade server doing what it does for any collaborative spend.
|
|
718
|
+
*
|
|
719
|
+
* One aggregate output, not one per input — again unlike the solver's covenant
|
|
720
|
+
* refund, which needs index-aligned outputs because its ArkadeScript inspects
|
|
721
|
+
* the output at the current input's index. This leaf carries no covenant, so a
|
|
722
|
+
* single output paying the whole balance is both valid and cheaper.
|
|
723
|
+
*
|
|
724
|
+
* `refundPkScript` defaults to the destination the contract itself commits to
|
|
725
|
+
* (`nonInteractiveRefund`'s `senderPkScript`, i.e. the address the trader gave
|
|
726
|
+
* at quote time), so the ordinary call cannot send the refund somewhere the
|
|
727
|
+
* trader did not intend. It is overridable because this leaf, having no
|
|
728
|
+
* covenant, genuinely does permit any destination.
|
|
729
|
+
*
|
|
730
|
+
* **Consensus, not wall clock, decides when this is spendable.** A seconds
|
|
731
|
+
* locktime matures against median-time-past, which trails real time by roughly
|
|
732
|
+
* an hour, so a push issued the moment `refundLocktime` passes can be rejected
|
|
733
|
+
* until enough blocks land. That is expected, not a failure — see
|
|
734
|
+
* {@link refundIfUnresolved}, which retries.
|
|
735
|
+
*
|
|
736
|
+
* **Swept outputs are refused, not attempted.** This is an OFFCHAIN spend, and
|
|
737
|
+
* a swept output is no longer a live leaf: `canSpendOffchain` and
|
|
738
|
+
* `canRecoverOnchain` are mutually exclusive by construction, so a recoverable
|
|
739
|
+
* input cannot be spent this way whatever key signs it (see
|
|
740
|
+
* {@link LockupVtxo.recoverable}). Because every input lands in ONE aggregate
|
|
741
|
+
* transaction, a single swept output would take the live ones down with it —
|
|
742
|
+
* so the whole push is refused with {@link LockupNeedsRecoveryError} naming the
|
|
743
|
+
* outpoints, rather than submitted and rejected. Filtering them out silently
|
|
744
|
+
* would be worse still: it would report success over money that never moved.
|
|
745
|
+
*/
|
|
746
|
+
declare function pushRefundWithoutReceiver(ark: RefundArkProvider, input: {
|
|
747
|
+
script: InstanceType<typeof VHTLC.ScriptV2>;
|
|
748
|
+
/** The `sender` signer. Build it from the swap's `secrets` with
|
|
749
|
+
* `senderIdentityForRfqSecrets` — on an HD wallet that resolves from
|
|
750
|
+
* the seed, with no stored key bytes anywhere. */
|
|
751
|
+
sender: Identity;
|
|
752
|
+
vtxos: readonly LockupVtxo[];
|
|
753
|
+
/** Defaults to the contract's own committed refund destination. */
|
|
754
|
+
refundPkScript?: Uint8Array;
|
|
755
|
+
}): Promise<{
|
|
756
|
+
arkTxid: string;
|
|
757
|
+
amount: number;
|
|
758
|
+
}>;
|
|
759
|
+
/**
|
|
760
|
+
* How long past `refundLocktime` to keep retrying the push before giving up
|
|
761
|
+
* and surfacing the server's refusal.
|
|
762
|
+
*
|
|
763
|
+
* Two hours because the CLTV matures against median-time-past (BIP-113), which
|
|
764
|
+
* lags wall clock by about an hour, plus room for a slow block. This is the
|
|
765
|
+
* mirror of `MIN_HEADROOM_SECONDS`, which refuses to FUND without 90 minutes
|
|
766
|
+
* of the same margin.
|
|
767
|
+
*/
|
|
768
|
+
declare const REFUND_MTP_LAG_SECONDS: number;
|
|
769
|
+
type RefundOutcome =
|
|
770
|
+
/** The solver resolved it — claimed (`settled`) or returned it (`refunded`). */
|
|
771
|
+
{
|
|
772
|
+
outcome: "resolved";
|
|
773
|
+
status: RfqStatus;
|
|
774
|
+
}
|
|
775
|
+
/** The trader took it back via `refundWithoutReceiver`. */
|
|
776
|
+
| {
|
|
777
|
+
outcome: "refunded";
|
|
778
|
+
arkTxid: string;
|
|
779
|
+
amount: number;
|
|
780
|
+
status: RfqStatus | null;
|
|
781
|
+
}
|
|
782
|
+
/** The refund window opened but the lockup holds nothing to return. */
|
|
783
|
+
| {
|
|
784
|
+
outcome: "nothing_to_refund";
|
|
785
|
+
status: RfqStatus | null;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* The money is still at the lockup, but its batch was swept, so no offchain
|
|
789
|
+
* spend can take it back until it is recovered into a fresh batch. Returned
|
|
790
|
+
* rather than retried: unlike a median-time-past refusal, no amount of
|
|
791
|
+
* waiting fixes this — see {@link LockupNeedsRecoveryError}. Recover the
|
|
792
|
+
* named outpoints, then call this again.
|
|
793
|
+
*/
|
|
794
|
+
| {
|
|
795
|
+
outcome: "needs_recovery";
|
|
796
|
+
outpoints: string[];
|
|
797
|
+
vtxos: LockupVtxo[];
|
|
798
|
+
status: RfqStatus | null;
|
|
799
|
+
};
|
|
800
|
+
/**
|
|
801
|
+
* Ask first, then fall back: watch the swap for the solver to resolve it, and
|
|
802
|
+
* if `refundLocktime` matures without that happening, take the lockup back
|
|
803
|
+
* with `refundWithoutReceiver`.
|
|
804
|
+
*
|
|
805
|
+
* This is the whole trader-side failure story in one call. It polls `status()`
|
|
806
|
+
* — the only "asking" this protocol has (see the module doc) — and returns as
|
|
807
|
+
* soon as the solver reports `settled` or `refunded`. Otherwise, once the
|
|
808
|
+
* quote's `refund_locktime` passes, it looks up what is actually at the lockup
|
|
809
|
+
* and pushes the refund.
|
|
810
|
+
*
|
|
811
|
+
* Two behaviours worth knowing:
|
|
812
|
+
*
|
|
813
|
+
* - **A dead negotiation is not a reason to stop.** `refused`, `expired` and
|
|
814
|
+
* `stuck` are terminal states, but a trader can be holding a funded lockup in
|
|
815
|
+
* every one of them, so they do not end the wait — only `settled`/`refunded`
|
|
816
|
+
* do (see {@link RFQ_RESOLVED_STATES}). What ends it otherwise is the
|
|
817
|
+
* timelock.
|
|
818
|
+
* - **The first push after the deadline may legitimately fail.** Median-time-
|
|
819
|
+
* past trails wall clock, so the server can still consider the leaf locked
|
|
820
|
+
* for a while after `refundLocktime` passes in real time. Failures are
|
|
821
|
+
* retried at the poll interval until `attemptDeadline`, after which the last
|
|
822
|
+
* error is rethrown rather than swallowed.
|
|
823
|
+
* - **A swept lockup ends the wait instead of consuming it.** Once the batch
|
|
824
|
+
* is gone the CLTV refund is not "not yet" but "not this way", so it returns
|
|
825
|
+
* `needs_recovery` naming the outpoints rather than retrying until the
|
|
826
|
+
* deadline. Recover them and call again.
|
|
827
|
+
*
|
|
828
|
+
* Safe to call late, and safe to call again: a caller recovering from a crash
|
|
829
|
+
* well past the deadline skips straight to the push, and a lockup that is
|
|
830
|
+
* already empty comes back as `nothing_to_refund` instead of an error.
|
|
831
|
+
*/
|
|
832
|
+
declare function refundIfUnresolved(transport: RfqTransport, ark: RefundArkProvider, indexer: RefundIndexer, input: {
|
|
833
|
+
rfqId: string;
|
|
834
|
+
script: InstanceType<typeof VHTLC.ScriptV2>;
|
|
835
|
+
/** @see pushRefundWithoutReceiver */
|
|
836
|
+
sender: Identity;
|
|
837
|
+
/** `refund_locktime` from the quote, unix seconds. */
|
|
838
|
+
refundLocktime: number;
|
|
839
|
+
/** Defaults to the contract's own committed refund destination. */
|
|
840
|
+
refundPkScript?: Uint8Array;
|
|
841
|
+
pollMs?: number;
|
|
842
|
+
/** Stop retrying the push at this unix time, rethrowing the last
|
|
843
|
+
* error. Defaults to `refundLocktime + REFUND_MTP_LAG_SECONDS`. */
|
|
844
|
+
attemptDeadline?: number;
|
|
845
|
+
/** Injected for tests; defaults to wall clock, in unix seconds. */
|
|
846
|
+
now?: () => number;
|
|
847
|
+
}): Promise<RefundOutcome>;
|
|
848
|
+
|
|
849
|
+
/** The Ark surface the claim push needs — the same seam the refund push uses. */
|
|
850
|
+
type ClaimArkProvider = RefundArkProvider;
|
|
851
|
+
/**
|
|
852
|
+
* The lockup is funded for less than the swap agreed.
|
|
853
|
+
*
|
|
854
|
+
* The attack this names: the solver funds the correctly derived script with
|
|
855
|
+
* dust. Deriving the script locally — what protects every other corridor —
|
|
856
|
+
* proves nothing here, because the script was never the lie. Claiming anyway
|
|
857
|
+
* publishes `P`, which is what lets the solver settle the payer's Lightning
|
|
858
|
+
* HTLC in full.
|
|
859
|
+
*/
|
|
860
|
+
declare class LockupAmountMismatchError extends Error {
|
|
861
|
+
readonly name = "LockupAmountMismatchError";
|
|
862
|
+
readonly reason = "amount_mismatch";
|
|
863
|
+
readonly expectedAmount: number;
|
|
864
|
+
readonly lockedAmount: number;
|
|
865
|
+
constructor(expectedAmount: number, lockedAmount: number);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Build, sign, and push the collaborative claim of a receive-corridor lockup:
|
|
869
|
+
* move every funded output at the lockup to the trader's own destination,
|
|
870
|
+
* revealing `P` in the witness — which is also what settles the trader's side
|
|
871
|
+
* of the swap (the solver reads `P` off the public claim).
|
|
872
|
+
*
|
|
873
|
+
* The preimage is checked against the script's committed hash BEFORE anything
|
|
874
|
+
* is signed: a wrong value can never open the leaf, and catching it here
|
|
875
|
+
* beats learning it from the server's rejection.
|
|
876
|
+
*
|
|
877
|
+
* So is the funded VALUE, against `expectedAmount` — and for the same reason,
|
|
878
|
+
* only more sharply: disclosure happens at SUBMIT, since `P` rides the PSBT to
|
|
879
|
+
* the Ark server. A check that waits for the transaction to land has already
|
|
880
|
+
* leaked the secret.
|
|
881
|
+
*
|
|
882
|
+
* Swept outputs are refused, not attempted, exactly as in
|
|
883
|
+
* {@link pushRefundWithoutReceiver} — one aggregate transaction means a
|
|
884
|
+
* single non-live input would take the live ones down with it. That refusal
|
|
885
|
+
* comes first, which is what leaves the value gate a plain sum over live
|
|
886
|
+
* outputs.
|
|
887
|
+
*
|
|
888
|
+
* The server's countersignature is verified before finalizing, per input and
|
|
889
|
+
* against that input's own leaf. It does not protect `P` — that reached the
|
|
890
|
+
* server at submit — but it turns "reported claimed, nothing landed, the
|
|
891
|
+
* solver refunds hours later" into an immediate failure.
|
|
892
|
+
*/
|
|
893
|
+
declare function pushClaim(ark: ClaimArkProvider, input: {
|
|
894
|
+
/** The receive-direction covenant (see `receiveVtxoScript`). */
|
|
895
|
+
script: InstanceType<typeof VHTLC.ScriptV2>;
|
|
896
|
+
/** The trader's `receiver` signer. Build it from the swap's `secrets`
|
|
897
|
+
* with `senderIdentityForRfqSecrets` — on an HD wallet that resolves
|
|
898
|
+
* from the seed, with no stored key bytes anywhere. */
|
|
899
|
+
receiver: Identity;
|
|
900
|
+
/** `P`, 32 bytes — the trader generated it at request time. */
|
|
901
|
+
preimage: Uint8Array;
|
|
902
|
+
vtxos: readonly LockupVtxo[];
|
|
903
|
+
/** Where the claimed sats land — the swap's payout address, decoded. */
|
|
904
|
+
destinationPkScript: Uint8Array;
|
|
905
|
+
/** What the lockup must carry: the quote's `to_amount`, captured at
|
|
906
|
+
* REQUEST time and persisted with the record. Required rather than
|
|
907
|
+
* optional, so the guard cannot be skipped by the records that need
|
|
908
|
+
* it most. */
|
|
909
|
+
expectedAmount: number;
|
|
910
|
+
/** Set when this lockup already carries a claim of ours: `P` is public
|
|
911
|
+
* by then, so the value gate protects nothing and would only strand
|
|
912
|
+
* the remainder. */
|
|
913
|
+
partiallyClaimed?: boolean;
|
|
914
|
+
}): Promise<{
|
|
915
|
+
arkTxid: string;
|
|
916
|
+
amount: number;
|
|
917
|
+
}>;
|
|
918
|
+
/**
|
|
919
|
+
* Wait for the solver-funded lockup to appear at the covenant script.
|
|
920
|
+
*
|
|
921
|
+
* Same conventions as `awaitRfqResolution`: a `pollMs` interval, an optional
|
|
922
|
+
* unix-seconds `deadline`, and a thrown error carrying a stable `reason` when
|
|
923
|
+
* the deadline passes.
|
|
924
|
+
*/
|
|
925
|
+
declare function awaitLockupFunding(indexer: RefundIndexer, swapPkScript: Uint8Array, options?: {
|
|
926
|
+
pollMs?: number;
|
|
927
|
+
deadline?: number;
|
|
928
|
+
}): Promise<readonly LockupVtxo[]>;
|
|
929
|
+
/**
|
|
930
|
+
* The one-call composition: wait for the solver's funding, then push the
|
|
931
|
+
* claim. The polling deadline gates the WAIT only — once the lockup is
|
|
932
|
+
* funded, the claim itself is gated by nothing but the solver's own refund
|
|
933
|
+
* deadline (`refund_locktime` from the quote), which is the number the
|
|
934
|
+
* caller's deadline should be measured against.
|
|
935
|
+
*
|
|
936
|
+
* The wait returns on the first output seen, so a funding the indexer
|
|
937
|
+
* surfaces piecemeal reaches {@link pushClaim}'s value gate short and throws
|
|
938
|
+
* {@link LockupAmountMismatchError}. Nothing was signed, so retrying once the
|
|
939
|
+
* rest lands is safe — and that is also the answer to a genuinely underfunded
|
|
940
|
+
* lockup, which never gets past the gate at all.
|
|
941
|
+
*/
|
|
942
|
+
declare function claimReceiveLockup(indexer: RefundIndexer, ark: ClaimArkProvider, input: Parameters<typeof pushClaim>[1] & {
|
|
943
|
+
/** The covenant's scriptPubKey, from the request flow's `swapPkScript`. */
|
|
944
|
+
swapPkScript: Uint8Array;
|
|
945
|
+
pollMs?: number;
|
|
946
|
+
deadline?: number;
|
|
947
|
+
}): Promise<{
|
|
948
|
+
arkTxid: string;
|
|
949
|
+
amount: number;
|
|
950
|
+
}>;
|
|
951
|
+
|
|
952
|
+
/** The contract type a swap lockup registers under. `@arkade-os/sdk`'s handler
|
|
953
|
+
* for `VHTLC.ScriptV2` — the covenant script this corridor builds. */
|
|
954
|
+
declare const SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
|
|
955
|
+
declare const SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
|
|
956
|
+
declare const SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
|
|
957
|
+
/** The write seam registration needs, narrowed to the one method — the same
|
|
958
|
+
* injection style as `SwapContractRegistry`, and satisfied by a real
|
|
959
|
+
* `ContractManager` (`await wallet.getContractManager()`). */
|
|
960
|
+
type LockupContractWriter = Pick<IContractManager, "createContract">;
|
|
961
|
+
/**
|
|
962
|
+
* The lockup could not be written locally.
|
|
963
|
+
*
|
|
964
|
+
* Deliberately NOT a {@link SwapRefusal} or an {@link AddressMismatch}: those
|
|
965
|
+
* say "the quote is bad, never fund it", while this says "the quote is fine and
|
|
966
|
+
* your own store failed".
|
|
967
|
+
*
|
|
968
|
+
* The throw is the safe point: nothing is funded, and on the receive legs the
|
|
969
|
+
* invoice never left the function, so the abandoned quote is inert and simply
|
|
970
|
+
* retrying the request is the recovery. `script` travels beside `address` so a
|
|
971
|
+
* caller that still holds the swap — `RfqSwapManager`'s `ensureRegistered`, or
|
|
972
|
+
* one resuming from its own record — can retry `registerLockupContract` alone
|
|
973
|
+
* instead of re-quoting. It is NOT enough to resume a request that threw here:
|
|
974
|
+
* that caller never received the invoice or `secrets`.
|
|
975
|
+
*/
|
|
976
|
+
declare class LockupRegistrationFailed extends Error {
|
|
977
|
+
/** The lockup address that was never registered — never fund it: nothing
|
|
978
|
+
* is watching it. */
|
|
979
|
+
readonly address: string;
|
|
980
|
+
/** The covenant the row would have been written from — the other half of
|
|
981
|
+
* `registerLockupContract`, so the write is retryable without a quote. */
|
|
982
|
+
readonly script: InstanceType<typeof VHTLC.ScriptV2>;
|
|
983
|
+
constructor(script: InstanceType<typeof VHTLC.ScriptV2>, address: string, cause: unknown);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Register a lockup covenant so its VTXOs are watched, annotatable and — via
|
|
987
|
+
* `vhtlc-v2`'s own handler, which is never generically spendable — kept out of
|
|
988
|
+
* ordinary coin selection.
|
|
989
|
+
*
|
|
990
|
+
* The row carries script-level facts only. Per-swap identity and key material
|
|
991
|
+
* stay in the swap record: rows are keyed by script and first-writer-wins, so
|
|
992
|
+
* anything per-swap written here is stale from the second swap onward.
|
|
993
|
+
*
|
|
994
|
+
* Takes the derived script rather than a script hex plus params, so the row
|
|
995
|
+
* cannot describe a script other than the one it is keyed by.
|
|
996
|
+
*
|
|
997
|
+
* Throws {@link LockupRegistrationFailed}, so a caller can tell a local
|
|
998
|
+
* storage problem from a reason to walk away from the quote.
|
|
999
|
+
*/
|
|
1000
|
+
declare function registerLockupContract(contracts: LockupContractWriter, script: InstanceType<typeof VHTLC.ScriptV2>, address: string): Promise<void>;
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Where a monitored swap stands.
|
|
1004
|
+
*
|
|
1005
|
+
* `claimable` and `claimed` are the states of a swap the TRADER has something
|
|
1006
|
+
* to claim on: the L1 fill on an onchain send, and the solver-funded lockup on
|
|
1007
|
+
* a receive. Only `lightning_send` has neither — there the solver claims the
|
|
1008
|
+
* lockup, and the trader's only move is the refund.
|
|
1009
|
+
*/
|
|
1010
|
+
type RfqSwapState =
|
|
1011
|
+
/** Live; nothing actionable yet. On a receive leg this covers the whole
|
|
1012
|
+
* stretch before the solver funds anything. */
|
|
1013
|
+
"pending"
|
|
1014
|
+
/** There is something for the trader to take, and the window to take it is
|
|
1015
|
+
* open: the confirmed L1 fill on an onchain send, or a lockup funded for at
|
|
1016
|
+
* least `expectedAmount` on a receive. */
|
|
1017
|
+
| "claimable"
|
|
1018
|
+
/**
|
|
1019
|
+
* The trader's claim has been made — its L1 broadcast on an onchain send,
|
|
1020
|
+
* its Arkade submission on a receive.
|
|
1021
|
+
*
|
|
1022
|
+
* **On a receive this is a local belief and not a chain fact**, which is
|
|
1023
|
+
* why it is not terminal: `settled` is the chain's answer, and `refunded`
|
|
1024
|
+
* is still reachable from here if the claim never lands and the solver
|
|
1025
|
+
* takes the lockup back.
|
|
1026
|
+
*/
|
|
1027
|
+
| "claimed"
|
|
1028
|
+
/**
|
|
1029
|
+
* This wallet will not act, and only the counterparty can change that.
|
|
1030
|
+
*
|
|
1031
|
+
* On a send leg: the Arkade refund cannot be pushed from here — no secrets
|
|
1032
|
+
* on the record, a descriptor from another seed, or nothing wired to act —
|
|
1033
|
+
* so the lockup comes back only if the counterparty claims it or the wallet
|
|
1034
|
+
* that can sign it is restored. On a receive leg: the trader holds no
|
|
1035
|
+
* refund at all, so this is a lockup that cannot be claimed — funded for
|
|
1036
|
+
* less than the swap agreed (publishing `P` for it is the whole attack
|
|
1037
|
+
* `LockupAmountMismatchError` exists to refuse), or one whose claim window
|
|
1038
|
+
* shut unclaimed. {@link RfqSwapCommon.blockedReason} says which.
|
|
1039
|
+
*
|
|
1040
|
+
* **Not terminal, and not a dead end.** The money is still at the lockup,
|
|
1041
|
+
* so the counterparty's move is still observable and still ends the swap;
|
|
1042
|
+
* and the refusal is re-checked every pass, so restoring the right wallet,
|
|
1043
|
+
* wiring the callbacks, or the solver topping the lockup up returns the
|
|
1044
|
+
* swap to `pending` and resumes the normal drive. For an onchain-send swap
|
|
1045
|
+
* it says nothing about the L1 half, which keeps being driven and claimed.
|
|
1046
|
+
*/
|
|
1047
|
+
| "needs_counterparty"
|
|
1048
|
+
/**
|
|
1049
|
+
* Terminal: the lockup was spent by a hash-verified claim. Read off chain,
|
|
1050
|
+
* never reported.
|
|
1051
|
+
*
|
|
1052
|
+
* On a send leg that claim is the counterparty's, and it is proof the
|
|
1053
|
+
* counterparty completed its side. On a receive leg it is the TRADER's own
|
|
1054
|
+
* — matched by the hash and not by our txid, so a claim that lands without
|
|
1055
|
+
* us still counts (see {@link RfqSwapManager}).
|
|
1056
|
+
*/
|
|
1057
|
+
| "settled"
|
|
1058
|
+
/**
|
|
1059
|
+
* Terminal: the lockup was spent by something other than a claim.
|
|
1060
|
+
*
|
|
1061
|
+
* On a send leg that is the money coming back, by the solver's hand or the
|
|
1062
|
+
* trader's. **On a receive leg it is a LOSS**: the lockup was the solver's
|
|
1063
|
+
* money, every non-claim leaf is the solver's, and a swap that ends here
|
|
1064
|
+
* ended with the trader's incoming payment never arriving. It is also where
|
|
1065
|
+
* a receive swap ends when its window closes with nothing left to observe —
|
|
1066
|
+
* see {@link RfqSwapManager}.
|
|
1067
|
+
*/
|
|
1068
|
+
| "refunded"
|
|
1069
|
+
/** Terminal: an action failed and its window closed. */
|
|
1070
|
+
| "failed";
|
|
1071
|
+
/** The states after which the manager stops monitoring a swap. Deliberately
|
|
1072
|
+
* without `needs_counterparty`: retiring on it would unwatch a funded lockup
|
|
1073
|
+
* whose claim is still the thing that ends the swap. */
|
|
1074
|
+
declare const RFQ_SWAP_TERMINAL_STATES: readonly ["settled", "refunded", "failed"];
|
|
1075
|
+
declare const isRfqSwapTerminal: (state: RfqSwapState) => boolean;
|
|
1076
|
+
/**
|
|
1077
|
+
* What the manager needs to register a swap's lockup with the wallet, so the
|
|
1078
|
+
* indexer pushes its funding and its spend instead of being asked every few
|
|
1079
|
+
* seconds.
|
|
1080
|
+
*
|
|
1081
|
+
* Both fields are things the caller already holds. `script` is the very object
|
|
1082
|
+
* `pushRefundWithoutReceiver` and `pushClaim` take, so a caller wired to act has
|
|
1083
|
+
* it in hand; `address` is the request entrypoint's own return value. The
|
|
1084
|
+
* address is taken rather than re-derived on purpose — the row's address must be
|
|
1085
|
+
* the one that was actually funded, and a local re-derivation would silently use
|
|
1086
|
+
* the SDK's default network, which is the exact bug `registerOfferContract`
|
|
1087
|
+
* guards against.
|
|
1088
|
+
*/
|
|
1089
|
+
interface RfqSwapLockup {
|
|
1090
|
+
/** The covenant. Its `pkScript` MUST equal the record's `lockupPkScript`. */
|
|
1091
|
+
script: InstanceType<typeof VHTLC.ScriptV2>;
|
|
1092
|
+
/** The Arkade address that was funded. */
|
|
1093
|
+
address: string;
|
|
1094
|
+
}
|
|
1095
|
+
interface RfqSwapCommon {
|
|
1096
|
+
/** The negotiation id — this record's identity. */
|
|
1097
|
+
rfqId: string;
|
|
1098
|
+
state: RfqSwapState;
|
|
1099
|
+
/** The Arkade lockup's scriptPubKey — `swapPkScript` from any of the four
|
|
1100
|
+
* request entrypoints. This is what the manager watches to decide the swap:
|
|
1101
|
+
* it is the only handle on the covenant whose spend witness says whether
|
|
1102
|
+
* the swap settled or came back. */
|
|
1103
|
+
lockupPkScript: Uint8Array;
|
|
1104
|
+
/** The covenant behind {@link lockupPkScript}, when the caller wants the
|
|
1105
|
+
* lockup registered with a contract manager. Optional: without it the
|
|
1106
|
+
* manager still watches the swap on its timer, it just cannot subscribe.
|
|
1107
|
+
* See {@link RfqSwapManagerDeps.contracts}. */
|
|
1108
|
+
lockup?: RfqSwapLockup;
|
|
1109
|
+
/** `sha256(P)`, hex — the quote's `payment_hash`. The claim leaf can only
|
|
1110
|
+
* be spent by revealing a value that hashes to this, which is what makes a
|
|
1111
|
+
* settlement provable rather than reported. For an onchain send this is
|
|
1112
|
+
* the SAME hash the L1 `htlc` carries: one `P` unlocks both legs. */
|
|
1113
|
+
paymentHash: string;
|
|
1114
|
+
/**
|
|
1115
|
+
* `refund_locktime` from the quote, unix seconds.
|
|
1116
|
+
*
|
|
1117
|
+
* Whose deadline it is inverts with the direction, and so does what to do
|
|
1118
|
+
* about it. On a send leg it is the TRADER's: the lockup is the trader's
|
|
1119
|
+
* money and this gates the refund that takes it back, so it is a moment to
|
|
1120
|
+
* act AFTER. On a receive leg it is the SOLVER's: the trader has no refund
|
|
1121
|
+
* leaf at all, and this is the moment to have claimed BEFORE.
|
|
1122
|
+
*/
|
|
1123
|
+
refundLocktime: number;
|
|
1124
|
+
createdAt: number;
|
|
1125
|
+
updatedAt: number;
|
|
1126
|
+
/** Set once the trader's own `refundWithoutReceiver` push landed. */
|
|
1127
|
+
refundArkTxid?: string;
|
|
1128
|
+
/** Why `state` is `failed`. */
|
|
1129
|
+
failure?: string;
|
|
1130
|
+
/** Why `state` is `needs_counterparty`. Distinct from {@link failure},
|
|
1131
|
+
* which means an action was attempted and did not work. */
|
|
1132
|
+
blockedReason?: string;
|
|
1133
|
+
}
|
|
1134
|
+
/** `arkade:BTC->lightning:BTC`. Nothing for the trader to claim: the solver
|
|
1135
|
+
* claims the lockup with the preimage it learns by paying the invoice — which
|
|
1136
|
+
* is exactly why that spend's witness is proof the payment landed. */
|
|
1137
|
+
interface LightningSendSwap extends RfqSwapCommon {
|
|
1138
|
+
kind: "lightning_send";
|
|
1139
|
+
}
|
|
1140
|
+
/** `arkade:BTC->onchain:BTC`. Carries the L1 half the trader must claim. */
|
|
1141
|
+
interface OnchainSendSwap extends RfqSwapCommon {
|
|
1142
|
+
kind: "onchain_send";
|
|
1143
|
+
/** The locally derived HTLC from `requestOnchainSend` — the manager reads
|
|
1144
|
+
* `pkScript`, `paymentHash` and `refundLocktime` off it to classify. */
|
|
1145
|
+
htlc: OnchainHtlc;
|
|
1146
|
+
/** `profile.min_confirmations` from the quote. */
|
|
1147
|
+
minConfirmations: number;
|
|
1148
|
+
/** The fill's outpoint, learned on first sighting. Without it a SPENT
|
|
1149
|
+
* HTLC reads as never funded — see {@link classifyOnchainHtlc}. */
|
|
1150
|
+
funding?: {
|
|
1151
|
+
txid: string;
|
|
1152
|
+
vout: number;
|
|
1153
|
+
};
|
|
1154
|
+
/** Our L1 claim's txid. */
|
|
1155
|
+
claimTxid?: string;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* `lightning:BTC->arkade:BTC`. The inverted leg: the SOLVER funds the lockup
|
|
1159
|
+
* and the TRADER claims it, and that claim is what publishes `P` and lets the
|
|
1160
|
+
* solver settle the payer's held Lightning HTLC.
|
|
1161
|
+
*
|
|
1162
|
+
* Two consequences shape how this record is driven, both of them absent from
|
|
1163
|
+
* the send legs:
|
|
1164
|
+
*
|
|
1165
|
+
* - **There is no trader-side refund.** Every non-claim leaf of this covenant
|
|
1166
|
+
* is the solver's, so the manager never calls
|
|
1167
|
+
* {@link RfqSwapManagerCallbacks.refundArkade} for one of these. A swap that
|
|
1168
|
+
* is not claimed is simply lost — the solver reclaims at
|
|
1169
|
+
* {@link RfqSwapCommon.refundLocktime} and the payer is refunded when the
|
|
1170
|
+
* held HTLC lapses.
|
|
1171
|
+
* - **The claim is the whole swap, and it is on a deadline.** The trader must
|
|
1172
|
+
* be online for it: covclaimd cannot claim this covenant today, so the claim
|
|
1173
|
+
* packet's offline path does not run.
|
|
1174
|
+
*/
|
|
1175
|
+
interface LightningReceiveSwap extends RfqSwapCommon {
|
|
1176
|
+
kind: "lightning_receive";
|
|
1177
|
+
/**
|
|
1178
|
+
* What the lockup must carry — the quote's `to_amount`, captured at REQUEST
|
|
1179
|
+
* time and persisted with the record.
|
|
1180
|
+
*
|
|
1181
|
+
* **Not re-derivable, and not optional.** Captured at claim time it would
|
|
1182
|
+
* be whatever the solver funded, which is the dust-funding attack rather
|
|
1183
|
+
* than a check on it. A record that reaches the manager without a finite
|
|
1184
|
+
* value here is reported `needs_counterparty` and never claimed: a
|
|
1185
|
+
* comparison against `undefined` or `NaN` is false, so an unusable
|
|
1186
|
+
* comparand does not fail the value gate, it deletes it.
|
|
1187
|
+
*/
|
|
1188
|
+
expectedAmount: number;
|
|
1189
|
+
/** Our Arkade claim's txid, once submitted. Set from the callback's return
|
|
1190
|
+
* and never from a chain read — the chain's answer is `settled`. */
|
|
1191
|
+
claimArkTxid?: string;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* A monitored swap.
|
|
1195
|
+
*
|
|
1196
|
+
* This is a live record, not a serialization format: `lockupPkScript` and
|
|
1197
|
+
* `htlc` hold derived `Uint8Array`s, and
|
|
1198
|
+
* {@link RfqSwapManagerCallbacks.saveSwap} is where a caller projects it into
|
|
1199
|
+
* whatever it stores. Rebuild it on restart the way it was made —
|
|
1200
|
+
* `lightningSendVtxoScript` / `receiveVtxoScript` / `onchainHtlcScript` over
|
|
1201
|
+
* the quote's binding fields — and hand the result to
|
|
1202
|
+
* {@link RfqSwapManager.start}.
|
|
1203
|
+
*
|
|
1204
|
+
* **`onchain:BTC->arkade:BTC` is deliberately not a member yet.** Its Arkade
|
|
1205
|
+
* half is the same solver-funded lockup as {@link LightningReceiveSwap}'s, but
|
|
1206
|
+
* it also has an L1 half the trader funds and must take back itself
|
|
1207
|
+
* (`buildHtlcRefund` at the HTLC's own `htlc_locktime`), which is a second
|
|
1208
|
+
* deadline, a second observation seam and a second action callback. Adding the
|
|
1209
|
+
* lockup half alone would produce a manager that silently lets that L1 refund
|
|
1210
|
+
* window pass — the one failure mode {@link RfqSwapManager} refuses elsewhere
|
|
1211
|
+
* by name (see `driveOnchain`'s missing-`ChainSource` check). Until the L1
|
|
1212
|
+
* refund is driven too, that corridor is better served by the request and claim
|
|
1213
|
+
* functions directly than by a monitor that covers half of it.
|
|
1214
|
+
*/
|
|
1215
|
+
type RfqSwap = LightningSendSwap | OnchainSendSwap | LightningReceiveSwap;
|
|
1216
|
+
/** What the manager should do next about an onchain-send swap's L1 half. */
|
|
1217
|
+
type OnchainSendAction =
|
|
1218
|
+
/** Not funded yet, or not confirmed deep enough. */
|
|
1219
|
+
"wait"
|
|
1220
|
+
/** Funded, confirmed, and far enough from the refund leaf to claim safely. */
|
|
1221
|
+
| "claim"
|
|
1222
|
+
/** The claim is off the table for good; the money comes back through the
|
|
1223
|
+
* Arkade lockup instead. */
|
|
1224
|
+
| "claim_window_closed"
|
|
1225
|
+
/** Our claim already landed (only the trader holds P). */
|
|
1226
|
+
| "claimed"
|
|
1227
|
+
/** The solver took its L1 refund — the fill is gone. */
|
|
1228
|
+
| "swept";
|
|
1229
|
+
/**
|
|
1230
|
+
* The decision a "poll status, refund on timeout" loop gets wrong.
|
|
1231
|
+
*
|
|
1232
|
+
* {@link OnchainHtlcPhase} runs `unfunded -> awaiting_confirmations ->
|
|
1233
|
+
* claimable -> (refundable | claimed | swept)`, and `refundable` does NOT mean
|
|
1234
|
+
* "time to refund the L1 HTLC" — the trader has no key on that leaf; it is the
|
|
1235
|
+
* SOLVER's refund, and reaching it means the trader's claim was missed. So the
|
|
1236
|
+
* L1 claim has to be driven before it, and from it the only remaining move is
|
|
1237
|
+
* the Arkade-side refund.
|
|
1238
|
+
*
|
|
1239
|
+
* There is a second, quieter trap between those two functions:
|
|
1240
|
+
* `classifyOnchainHtlc` reports `claimable` right up until median-time-past
|
|
1241
|
+
* reaches `refundLocktime`, while `claimOnchainFill` refuses from
|
|
1242
|
+
* {@link ONCHAIN_CLAIM_MARGIN_SECONDS} before it — because broadcasting
|
|
1243
|
+
* publishes P, and doing that into the counterparty's live refund window risks
|
|
1244
|
+
* losing the race AND giving away the preimage. Driving straight off the phase
|
|
1245
|
+
* would therefore spend that whole margin throwing `claim_window_closed` at
|
|
1246
|
+
* every poll and never fall back. This function applies the margin, so
|
|
1247
|
+
* "claimable" here means claimable by `claimOnchainFill` too.
|
|
1248
|
+
*/
|
|
1249
|
+
declare function nextOnchainAction(input: {
|
|
1250
|
+
phase: OnchainHtlcPhase;
|
|
1251
|
+
/** `htlc.refundLocktime` — when the solver's L1 refund leaf opens. */
|
|
1252
|
+
htlcLocktime: number;
|
|
1253
|
+
/** Unix seconds. */
|
|
1254
|
+
now: number;
|
|
1255
|
+
}): OnchainSendAction;
|
|
1256
|
+
/** What the trader's own `refundWithoutReceiver` push returned, or `null` when
|
|
1257
|
+
* the lockup held nothing to return. */
|
|
1258
|
+
type ArkadeRefundResult = {
|
|
1259
|
+
arkTxid: string;
|
|
1260
|
+
amount: number;
|
|
1261
|
+
} | null;
|
|
1262
|
+
/**
|
|
1263
|
+
* The money-moving half, injected. The manager decides when; these do it.
|
|
1264
|
+
*
|
|
1265
|
+
* Neither action gets a retry loop of its own here — see the module doc. Do
|
|
1266
|
+
* NOT wire `refundArkade` to `refundIfUnresolved`: that function is the
|
|
1267
|
+
* single-swap version of this whole class and brings its own status polling
|
|
1268
|
+
* and its own MTP retry loop, which would nest inside the manager's. Wire it
|
|
1269
|
+
* to `findLockupVtxos` + `pushRefundWithoutReceiver`, which is the atomic push
|
|
1270
|
+
* `refundIfUnresolved` itself calls.
|
|
1271
|
+
*
|
|
1272
|
+
* Resolve the sender key through `senderIdentityForSwapRecord`: it is
|
|
1273
|
+
* what turns "this wallet cannot sign this swap" into
|
|
1274
|
+
* {@link RefundNotLocallyPossibleError}, which the manager reports as
|
|
1275
|
+
* `needs_counterparty` instead of retrying for the whole refund window.
|
|
1276
|
+
*/
|
|
1277
|
+
interface RfqSwapManagerCallbacks {
|
|
1278
|
+
/** Build and broadcast the L1 claim. See `claimOnchainFill`. */
|
|
1279
|
+
claimOnchain: (swap: OnchainSendSwap, utxo: ChainUtxo) => Promise<{
|
|
1280
|
+
txid: string;
|
|
1281
|
+
}>;
|
|
1282
|
+
/**
|
|
1283
|
+
* Claim the solver-funded lockup on a receive leg, revealing `P`. Wire it
|
|
1284
|
+
* to `pushClaim` — the outputs are supplied, so `findLockupVtxos` has
|
|
1285
|
+
* already been called and `claimReceiveLockup`'s wait would only sit on a
|
|
1286
|
+
* lockup the manager has just seen.
|
|
1287
|
+
*
|
|
1288
|
+
* **Pass `expectedAmount` and `partiallyClaimed` straight through.** The
|
|
1289
|
+
* manager checks the funded value before calling this, but that check
|
|
1290
|
+
* decides WHEN to act; `pushClaim`'s decides whether `P` is published, and
|
|
1291
|
+
* it is the one that runs with nothing between it and the signature. Two
|
|
1292
|
+
* checks, one of which is load-bearing — do not drop the inner one because
|
|
1293
|
+
* the outer one exists.
|
|
1294
|
+
*
|
|
1295
|
+
* Required rather than optional, like {@link claimOnchain}: a receive swap
|
|
1296
|
+
* monitored with nothing wired to claim it is a swap that quietly expires,
|
|
1297
|
+
* and a compile error is the right way to learn that.
|
|
1298
|
+
*/
|
|
1299
|
+
claimLockup: (swap: LightningReceiveSwap, vtxos: readonly LockupVtxo[], options: {
|
|
1300
|
+
/** A claim of ours is already out, so `P` is public and the value
|
|
1301
|
+
* gate has nothing left to protect — pass this to `pushClaim` so a
|
|
1302
|
+
* funding that arrived piecemeal can still be swept. */
|
|
1303
|
+
partiallyClaimed: boolean;
|
|
1304
|
+
}) => Promise<{
|
|
1305
|
+
arkTxid: string;
|
|
1306
|
+
amount: number;
|
|
1307
|
+
}>;
|
|
1308
|
+
/** Push `refundWithoutReceiver` for every output at the lockup. See
|
|
1309
|
+
* `pushRefundWithoutReceiver`; return `null` for an empty lockup. Never
|
|
1310
|
+
* called for a {@link LightningReceiveSwap} — that leg's refund leaf is the
|
|
1311
|
+
* solver's. */
|
|
1312
|
+
refundArkade: (swap: RfqSwap) => Promise<ArkadeRefundResult>;
|
|
1313
|
+
/**
|
|
1314
|
+
* Whether a local refund is possible at all — the record's secrets, against
|
|
1315
|
+
* this wallet. Called every pass, including *before* the refund window
|
|
1316
|
+
* opens, so a swap nobody can refund says so while the solver can still
|
|
1317
|
+
* act, instead of at the deadline; and so restoring the right wallet lifts
|
|
1318
|
+
* the state again. Never called for a receive swap: there is no local
|
|
1319
|
+
* refund there to probe for.
|
|
1320
|
+
*
|
|
1321
|
+
* Optional: omit to answer "yes" and learn at push time, from
|
|
1322
|
+
* {@link RefundNotLocallyPossibleError}. Local by contract — no network
|
|
1323
|
+
* call belongs here.
|
|
1324
|
+
*/
|
|
1325
|
+
canRefundArkade?: (swap: RfqSwap) => Promise<{
|
|
1326
|
+
ok: true;
|
|
1327
|
+
} | {
|
|
1328
|
+
ok: false;
|
|
1329
|
+
reason: string;
|
|
1330
|
+
}>;
|
|
1331
|
+
/** Persist the record. Called after any pass that changed it. */
|
|
1332
|
+
saveSwap: (swap: RfqSwap) => Promise<void>;
|
|
1333
|
+
}
|
|
1334
|
+
/** The actions the manager executes on a caller's behalf. */
|
|
1335
|
+
type RfqSwapActionName = "claimOnchain" | "claimLockup" | "refundArkade";
|
|
1336
|
+
interface RfqSwapManagerEvents {
|
|
1337
|
+
/** Every state change, including ones that read as going backwards.
|
|
1338
|
+
* `claimed -> claimable` is legal and expected on a receive swap the solver
|
|
1339
|
+
* funds piecemeal: a lockup topped up after a claim is a new claimable
|
|
1340
|
+
* event, and the label says so before the sweep goes out. Treat these
|
|
1341
|
+
* states as a description of what to do next, not as a progress bar. */
|
|
1342
|
+
onSwapUpdate?: (swap: RfqSwap, previous: RfqSwapState) => void;
|
|
1343
|
+
/** Fired once, when a swap leaves monitoring `settled` or `refunded`.
|
|
1344
|
+
* A swap that ends `failed` reports through `onSwapFailed` instead — the
|
|
1345
|
+
* two are mutually exclusive. */
|
|
1346
|
+
onSwapCompleted?: (swap: RfqSwap) => void;
|
|
1347
|
+
/** Fired for any action that threw — including ones the manager will retry
|
|
1348
|
+
* on the next pass — and once more when the swap finally ends `failed`. */
|
|
1349
|
+
onSwapFailed?: (swap: RfqSwap, error: Error) => void;
|
|
1350
|
+
onActionExecuted?: (swap: RfqSwap, action: RfqSwapActionName) => void;
|
|
1351
|
+
}
|
|
1352
|
+
type SwapUpdateListener = NonNullable<RfqSwapManagerEvents["onSwapUpdate"]>;
|
|
1353
|
+
type SwapCompletedListener = NonNullable<RfqSwapManagerEvents["onSwapCompleted"]>;
|
|
1354
|
+
type SwapFailedListener = NonNullable<RfqSwapManagerEvents["onSwapFailed"]>;
|
|
1355
|
+
type ActionExecutedListener = NonNullable<RfqSwapManagerEvents["onActionExecuted"]>;
|
|
1356
|
+
interface RfqSwapManagerConfig {
|
|
1357
|
+
/** Drive claims and refunds automatically (default: true). With this off
|
|
1358
|
+
* the manager still watches and reports, so a caller can act by hand off
|
|
1359
|
+
* `claimable`. */
|
|
1360
|
+
enableAutoActions?: boolean;
|
|
1361
|
+
/** How often to run a pass, ms. Default 5000 — the same interval
|
|
1362
|
+
* `awaitOnchainFill` and `refundIfUnresolved` poll at. */
|
|
1363
|
+
pollIntervalMs?: number;
|
|
1364
|
+
/** Injected for tests; defaults to wall clock, in unix seconds — the same
|
|
1365
|
+
* convention `refundIfUnresolved` uses. */
|
|
1366
|
+
now?: () => number;
|
|
1367
|
+
events?: RfqSwapManagerEvents;
|
|
1368
|
+
}
|
|
1369
|
+
/** The contract-manager surface this needs, narrowed for injection — the same
|
|
1370
|
+
* seam style as {@link LockupSpendIndexer} and `refund.ts`'s
|
|
1371
|
+
* {@link RefundArkProvider}, and satisfied structurally by a real
|
|
1372
|
+
* `ContractManager` (`await wallet.getContractManager()`). */
|
|
1373
|
+
type SwapContractRegistry = Pick<IContractManager, "createContract" | "getContracts" | "onContractEvent" | "setContractWatchState">;
|
|
1374
|
+
/** The observation seams. None is owned by the manager, and none holds keys —
|
|
1375
|
+
* same philosophy as `onchainHtlc.ts`'s `ChainSource`. There is no
|
|
1376
|
+
* `RfqTransport` here on purpose: nothing this manager decides depends on the
|
|
1377
|
+
* solver answering (see the module doc). */
|
|
1378
|
+
interface RfqSwapManagerDeps {
|
|
1379
|
+
/** Arkade access. Required: this is how a swap's resolution is determined,
|
|
1380
|
+
* for both legs. */
|
|
1381
|
+
indexer: LockupSpendIndexer;
|
|
1382
|
+
/** L1 access. Required to monitor onchain-send swaps; a lightning-only
|
|
1383
|
+
* caller can leave it out. */
|
|
1384
|
+
chain?: ChainSource;
|
|
1385
|
+
/**
|
|
1386
|
+
* The wallet's contract manager, when there is one. Optional in the same
|
|
1387
|
+
* way {@link chain} is: a caller with no wallet, or one that only wants the
|
|
1388
|
+
* timer, still gets a fully working manager — the subscription is a
|
|
1389
|
+
* LATENCY optimization and nothing depends on it.
|
|
1390
|
+
*
|
|
1391
|
+
* Supplying it buys two things. The lockup gets REGISTERED, which is what
|
|
1392
|
+
* puts it in the wallet's own contract set at all — a prerequisite for
|
|
1393
|
+
* anything that has to act on the lockup before its batch expires, since an
|
|
1394
|
+
* expired lockup is swept and loses every cooperative path. And the indexer
|
|
1395
|
+
* PUSHES its funding and its spend, so a settlement is noticed when it
|
|
1396
|
+
* happens rather than up to `pollIntervalMs` later.
|
|
1397
|
+
*
|
|
1398
|
+
* Prefer `await wallet.getContractManager()` over constructing one, the way
|
|
1399
|
+
* `createOffer` does.
|
|
1400
|
+
*/
|
|
1401
|
+
contracts?: SwapContractRegistry;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Watches a set of live RFQ swaps and drives each to its end.
|
|
1405
|
+
*
|
|
1406
|
+
* One pass per swap, in this order, every
|
|
1407
|
+
* {@link RfqSwapManagerConfig.pollIntervalMs} — and additionally the moment a
|
|
1408
|
+
* contract event names that swap's lockup, which changes only WHEN a pass runs,
|
|
1409
|
+
* never what it concludes (see {@link subscribe}):
|
|
1410
|
+
*
|
|
1411
|
+
* 0. **Register the lockup**, if a contract manager was supplied and it is not
|
|
1412
|
+
* registered yet. Best-effort; never blocks the steps below.
|
|
1413
|
+
* 1. **Ask the chain what became of the lockup** — {@link readLockupFate}. A
|
|
1414
|
+
* spend whose witness HASHES to the quote's `payment_hash` ends the swap
|
|
1415
|
+
* `settled`; a lockup fully spent by anything else ends it `refunded`.
|
|
1416
|
+
* Anything the indexer could not answer is `unknown`, which is NOT an
|
|
1417
|
+
* answer: the pass carries on to the steps below, whose deadlines an indexer
|
|
1418
|
+
* outage has no bearing on.
|
|
1419
|
+
* 2. **Drive the trader's claim.** On an onchain send that is the L1 fill — see
|
|
1420
|
+
* {@link nextOnchainAction}. On a receive it is the lockup itself, and it
|
|
1421
|
+
* ends the pass: that leg has no step 3.
|
|
1422
|
+
* 3. **Take the lockup back**, send legs only, once `refundLocktime` has passed
|
|
1423
|
+
* and step 1 has not ended the swap. This runs for onchain-send too,
|
|
1424
|
+
* including after a successful claim: the trader's lockup is still funded and
|
|
1425
|
+
* still theirs to recover if the solver never comes for it. When no local
|
|
1426
|
+
* refund is possible at all — no secrets, another wallet's descriptor,
|
|
1427
|
+
* nothing wired — the swap reports `needs_counterparty` instead of retrying
|
|
1428
|
+
* a push that cannot work.
|
|
1429
|
+
*
|
|
1430
|
+
* **What step 1 proves depends on the direction.** On a send leg every non-claim
|
|
1431
|
+
* leaf pays the trader's own committed address or needs the trader's own
|
|
1432
|
+
* signature, so "spent, but not by a hash-verified claim" means the money came
|
|
1433
|
+
* back. On a receive leg those leaves are the SOLVER's and the claim leaf is the
|
|
1434
|
+
* trader's, so the same two readings mean the opposite things — `settled` is the
|
|
1435
|
+
* trader's own claim landing, `refunded` is the solver taking back a lockup the
|
|
1436
|
+
* trader failed to claim. The read is identical; only the state docs differ.
|
|
1437
|
+
*
|
|
1438
|
+
* Two things about the receive arm that are easy to get wrong, and are asserted
|
|
1439
|
+
* in the tests rather than left to be inferred:
|
|
1440
|
+
*
|
|
1441
|
+
* - **A claim is matched by its preimage, never by our txid.** The covenant's
|
|
1442
|
+
* `nonInteractiveClaim` leaf is pinned to the trader's own payout script, so a
|
|
1443
|
+
* claim that lands without us — covclaimd, the day it works — still pays the
|
|
1444
|
+
* trader and is still `settled`. Matching on the txid we submitted would turn
|
|
1445
|
+
* that success into an anomaly.
|
|
1446
|
+
* - **`LockupFate.fate === "claimed"` maps to the state `settled`, never to the
|
|
1447
|
+
* state `claimed`.** The two words live one layer apart: the fate is the
|
|
1448
|
+
* chain's, the state is ours, and the state `claimed` means only that we
|
|
1449
|
+
* submitted something.
|
|
1450
|
+
*/
|
|
1451
|
+
declare class RfqSwapManager {
|
|
1452
|
+
private readonly deps;
|
|
1453
|
+
private readonly config;
|
|
1454
|
+
private callbacks;
|
|
1455
|
+
private readonly swapUpdateListeners;
|
|
1456
|
+
private readonly swapCompletedListeners;
|
|
1457
|
+
private readonly swapFailedListeners;
|
|
1458
|
+
private readonly actionExecutedListeners;
|
|
1459
|
+
private readonly monitored;
|
|
1460
|
+
/** Monitored swaps by lockup script hex, so a contract event — which names
|
|
1461
|
+
* a script and nothing else — can find the swap it belongs to. */
|
|
1462
|
+
private readonly byLockupScript;
|
|
1463
|
+
/**
|
|
1464
|
+
* Swaps whose lockup registration has been SETTLED one way or another,
|
|
1465
|
+
* mapped to whether a contract row actually resulted. Membership is what
|
|
1466
|
+
* stops a per-pass retry from becoming a per-pass round trip; the value is
|
|
1467
|
+
* what keeps a swap that could never be registered from later trying to
|
|
1468
|
+
* retire a row that does not exist, which would report a spurious failure
|
|
1469
|
+
* on a swap that in fact succeeded.
|
|
1470
|
+
*/
|
|
1471
|
+
private readonly registered;
|
|
1472
|
+
/**
|
|
1473
|
+
* Swaps whose `refundArkade` answered {@link RefundNotLocallyPossibleError}
|
|
1474
|
+
* in this process. Membership stops the push from being re-issued every
|
|
1475
|
+
* pass — it cannot start working on its own, and re-issuing it is the
|
|
1476
|
+
* grind `needs_counterparty` exists to remove. Only
|
|
1477
|
+
* {@link RfqSwapManagerCallbacks.canRefundArkade} clears it, so a caller
|
|
1478
|
+
* with no probe learns again on the next start, when the wallet that can
|
|
1479
|
+
* sign may well have been restored.
|
|
1480
|
+
*/
|
|
1481
|
+
private readonly refundRefused;
|
|
1482
|
+
/**
|
|
1483
|
+
* The last error a receive swap's claim callback threw, by rfqId.
|
|
1484
|
+
*
|
|
1485
|
+
* Kept only to tell two terminal outcomes apart once the claim window
|
|
1486
|
+
* shuts: a swap whose claim was attempted and kept failing ends `failed`
|
|
1487
|
+
* with that reason, while one that simply never became claimable ends
|
|
1488
|
+
* `refunded`. Without it a broken claim callback would resolve a caller's
|
|
1489
|
+
* {@link waitForSwapCompletion} as an ordinary unwind.
|
|
1490
|
+
*
|
|
1491
|
+
* Process-local, like {@link refundRefused}: after a restart the same swap
|
|
1492
|
+
* ends `refunded` instead, which costs the caller a reason and nothing else
|
|
1493
|
+
* — every throw was already reported through `onSwapFailed` as it happened.
|
|
1494
|
+
*/
|
|
1495
|
+
private readonly lastClaimError;
|
|
1496
|
+
/**
|
|
1497
|
+
* The lockup outpoints a receive swap's claim callback has already been
|
|
1498
|
+
* handed, by rfqId.
|
|
1499
|
+
*
|
|
1500
|
+
* What this exists to prevent: a claim SUCCEEDS, and for the next few
|
|
1501
|
+
* passes the indexer still lists those outputs as unspent. Without a
|
|
1502
|
+
* record of what was already claimed, every one of those passes would
|
|
1503
|
+
* re-submit the same spend, fail against the server, and report a swap
|
|
1504
|
+
* that in fact worked as failing. With one, a re-claim happens only when
|
|
1505
|
+
* an outpoint appears that was never claimed — a lockup funded piecemeal,
|
|
1506
|
+
* which is legitimate and which `partiallyClaimed` exists for.
|
|
1507
|
+
*
|
|
1508
|
+
* Process-local: after a restart a swap with a live claim tries once more.
|
|
1509
|
+
* That is the recovery case rather than the spam one — a claim that never
|
|
1510
|
+
* landed leaves its outputs unspent, and one that did leaves a single
|
|
1511
|
+
* rejection.
|
|
1512
|
+
*/
|
|
1513
|
+
private readonly claimedOutpoints;
|
|
1514
|
+
/** Live `onContractEvent` subscription, held so `stop()` can drop it. */
|
|
1515
|
+
private unsubscribeContracts;
|
|
1516
|
+
/** Terminal records, kept so a late {@link waitForSwapCompletion} still
|
|
1517
|
+
* answers instead of throwing "not found". Cleared by {@link removeSwap}. */
|
|
1518
|
+
private readonly finished;
|
|
1519
|
+
private readonly waiters;
|
|
1520
|
+
/** Records changed during the current pass, flushed through `saveSwap`. */
|
|
1521
|
+
private readonly dirty;
|
|
1522
|
+
/** Race guard: one action at a time per swap. */
|
|
1523
|
+
private readonly inProgress;
|
|
1524
|
+
private timer;
|
|
1525
|
+
private running;
|
|
1526
|
+
constructor(deps: RfqSwapManagerDeps, config?: RfqSwapManagerConfig);
|
|
1527
|
+
/** Wire the money-moving half. Without it the manager only watches. */
|
|
1528
|
+
setCallbacks(callbacks: RfqSwapManagerCallbacks): void;
|
|
1529
|
+
onSwapUpdate(listener: SwapUpdateListener): () => void;
|
|
1530
|
+
onSwapCompleted(listener: SwapCompletedListener): () => void;
|
|
1531
|
+
onSwapFailed(listener: SwapFailedListener): () => void;
|
|
1532
|
+
onActionExecuted(listener: ActionExecutedListener): () => void;
|
|
1533
|
+
/**
|
|
1534
|
+
* Load records and begin monitoring. Runs one pass immediately — a caller
|
|
1535
|
+
* resuming after a restart may be well past a deadline already — then
|
|
1536
|
+
* every `pollIntervalMs`. Records that are already terminal are kept only
|
|
1537
|
+
* so {@link waitForSwapCompletion} can answer for them.
|
|
1538
|
+
*
|
|
1539
|
+
* Calling it again while running loads the records and returns rather than
|
|
1540
|
+
* re-arming — dropping them silently would strand a funded swap on a
|
|
1541
|
+
* caller's harmless double-start.
|
|
1542
|
+
*/
|
|
1543
|
+
start(swaps?: readonly RfqSwap[]): Promise<void>;
|
|
1544
|
+
/**
|
|
1545
|
+
* Stop monitoring and clear the timer. In-flight actions are not
|
|
1546
|
+
* cancellable and run to completion; outstanding
|
|
1547
|
+
* {@link waitForSwapCompletion} promises are left pending, since
|
|
1548
|
+
* stop/start is a pause rather than a cancellation.
|
|
1549
|
+
*
|
|
1550
|
+
* The contract subscription is dropped too — an open stream with nothing
|
|
1551
|
+
* reacting to it is a leak, and {@link start} puts it back. What is NOT
|
|
1552
|
+
* undone is the contract registration: those rows are the wallet's, they
|
|
1553
|
+
* outlive this manager's lifecycle, and dropping them would unwatch a
|
|
1554
|
+
* lockup that is still funded.
|
|
1555
|
+
*/
|
|
1556
|
+
stop(): Promise<void>;
|
|
1557
|
+
/** Begin monitoring a swap. Polled immediately when the manager is running,
|
|
1558
|
+
* so a just-funded swap does not wait out a whole interval. */
|
|
1559
|
+
addSwap(swap: RfqSwap): Promise<void>;
|
|
1560
|
+
/** Forget a swap entirely, monitored or finished.
|
|
1561
|
+
*
|
|
1562
|
+
* Its contract row is left alone: registration is a wallet-level fact about
|
|
1563
|
+
* a script that may still hold money, and this call says only that THIS
|
|
1564
|
+
* manager stops driving the swap. Retiring the row is reserved for a swap
|
|
1565
|
+
* that reached a terminal state, where the lockup is provably done. */
|
|
1566
|
+
removeSwap(rfqId: string): Promise<void>;
|
|
1567
|
+
/** Every swap still being monitored. */
|
|
1568
|
+
getPendingSwaps(): Promise<RfqSwap[]>;
|
|
1569
|
+
hasSwap(rfqId: string): Promise<boolean>;
|
|
1570
|
+
/** True while an action for this swap holds the per-swap lock. */
|
|
1571
|
+
isProcessing(rfqId: string): Promise<boolean>;
|
|
1572
|
+
getStats(): Promise<{
|
|
1573
|
+
isRunning: boolean;
|
|
1574
|
+
monitoredSwaps: number;
|
|
1575
|
+
finishedSwaps: number;
|
|
1576
|
+
inProgress: number;
|
|
1577
|
+
pollIntervalMs: number;
|
|
1578
|
+
}>;
|
|
1579
|
+
/**
|
|
1580
|
+
* Run one monitoring pass over every swap now.
|
|
1581
|
+
*
|
|
1582
|
+
* {@link start} calls this on an interval, but it is public on purpose: a
|
|
1583
|
+
* caller that sleeps its process (a mobile app resuming, a service worker
|
|
1584
|
+
* waking) wants a pass on that event rather than at the next tick. Passes
|
|
1585
|
+
* do not overlap per swap — the in-progress lock makes a concurrent call a
|
|
1586
|
+
* no-op for any swap already being worked on.
|
|
1587
|
+
*/
|
|
1588
|
+
poll(): Promise<void>;
|
|
1589
|
+
/**
|
|
1590
|
+
* Resolve once this swap's PAYOUT is decided — which for onchain-send is
|
|
1591
|
+
* the L1 claim, not the end of the record's life: once `claimTxid` is set
|
|
1592
|
+
* the trader has the coins it swapped for, and what remains is the manager
|
|
1593
|
+
* watching the Arkade lockup close. That holds however the record is
|
|
1594
|
+
* labelled afterwards, `needs_counterparty` included. Lightning-send has no
|
|
1595
|
+
* such split and resolves at `settled`/`refunded`, and so does lightning
|
|
1596
|
+
* receive — see {@link isPayoutDecided} for why its own claim txid does not
|
|
1597
|
+
* decide it.
|
|
1598
|
+
*
|
|
1599
|
+
* Rejects only on `failed`. `refunded` resolves: on a send leg a refund is
|
|
1600
|
+
* an outcome the caller asked this manager to drive, not an exception. On a
|
|
1601
|
+
* receive leg it is the swap being lost, which is still an answer and not
|
|
1602
|
+
* an error — read `state`, do not infer success from resolution.
|
|
1603
|
+
*/
|
|
1604
|
+
waitForSwapCompletion(rfqId: string): Promise<RfqSwapOutcome>;
|
|
1605
|
+
private track;
|
|
1606
|
+
/** Drops the swap from BOTH indexes. The event index is the one that stops
|
|
1607
|
+
* a late event finding a swap that is gone; `pollSwap`'s own
|
|
1608
|
+
* `monitored` check would also catch it, and deliberately still does —
|
|
1609
|
+
* either alone is sufficient, which is what keeps a future change to one of
|
|
1610
|
+
* them from silently re-driving a cancelled swap. */
|
|
1611
|
+
private untrack;
|
|
1612
|
+
/**
|
|
1613
|
+
* Turn the indexer's push into an extra reason to run a pass — and nothing
|
|
1614
|
+
* more.
|
|
1615
|
+
*
|
|
1616
|
+
* **This is deliberately not a source of truth.** An event names a script;
|
|
1617
|
+
* the reaction is to run the ordinary pass for the swap at that script, and
|
|
1618
|
+
* that pass re-reads the lockup through {@link readLockupFate} exactly as
|
|
1619
|
+
* the timer's pass does. So an event that is missed, duplicated, reordered
|
|
1620
|
+
* or outright FORGED can only cost or save latency — it can never change
|
|
1621
|
+
* what this manager believes about a swap, and it can never on its own
|
|
1622
|
+
* cause a claim or a refund. That property is what makes it safe to bolt a
|
|
1623
|
+
* best-effort stream onto a money path, and it must survive any future
|
|
1624
|
+
* change here: the moment an event is BELIEVED rather than merely acted on,
|
|
1625
|
+
* a relay outage becomes a correctness problem instead of a latency one.
|
|
1626
|
+
*
|
|
1627
|
+
* The timer stays armed regardless, and is the failsafe. Every deadline
|
|
1628
|
+
* that moves money — `refundLocktime`, the L1 claim window — is an absolute
|
|
1629
|
+
* timelock that passes whether or not a single event ever arrives.
|
|
1630
|
+
*/
|
|
1631
|
+
private subscribe;
|
|
1632
|
+
/**
|
|
1633
|
+
* Register this swap's lockup with the wallet's contract manager, once.
|
|
1634
|
+
*
|
|
1635
|
+
* The backstop, not the primary site: `requestLightningSend` /
|
|
1636
|
+
* `requestOnchainSend` register before the caller can fund, so this covers
|
|
1637
|
+
* swaps whose records predate that — and costs nothing when it does not,
|
|
1638
|
+
* since `createContract` is first-writer-wins.
|
|
1639
|
+
*
|
|
1640
|
+
* Best-effort by design: a failure here is reported and retried on the next
|
|
1641
|
+
* pass, and never aborts the pass it is part of. Registration buys latency
|
|
1642
|
+
* and puts the lockup in the wallet's contract set; it decides nothing. The
|
|
1643
|
+
* money path below it reads the indexer directly and is gated on timelocks
|
|
1644
|
+
* that a missing contract row has no bearing on, so failing the pass over
|
|
1645
|
+
* this would trade a real deadline for a bookkeeping one.
|
|
1646
|
+
*/
|
|
1647
|
+
private ensureRegistered;
|
|
1648
|
+
/** Stop watching a finished swap's lockup. Retained, not deleted: the row
|
|
1649
|
+
* is what keeps the lockup's own VTXOs annotatable and its history
|
|
1650
|
+
* readable, while `retained` is what drops it from the subscription and
|
|
1651
|
+
* the poll — a settled swap that stayed watched would cost the wallet a
|
|
1652
|
+
* script for its whole life. Best-effort — the swap is over either way. */
|
|
1653
|
+
private retireContract;
|
|
1654
|
+
private arm;
|
|
1655
|
+
private pollSwap;
|
|
1656
|
+
private runPass;
|
|
1657
|
+
/**
|
|
1658
|
+
* The receive leg's whole state machine: claim the solver-funded lockup
|
|
1659
|
+
* while the window is open, and recognise the shapes in which it can be
|
|
1660
|
+
* lost.
|
|
1661
|
+
*
|
|
1662
|
+
* **The window closes at `refundLocktime`, on wall clock, with no margin.**
|
|
1663
|
+
* Both halves of that are deliberate. It closes there because publishing
|
|
1664
|
+
* `P` into the solver's live refund window risks losing the race and
|
|
1665
|
+
* handing over the preimage anyway — the hazard `ONCHAIN_CLAIM_MARGIN_SECONDS`
|
|
1666
|
+
* guards on the L1 side. It takes no margin because the two situations are
|
|
1667
|
+
* not alike: that one budgets for confirmation depth, while this claim is an
|
|
1668
|
+
* offchain spend that lands in seconds. Wall clock is already the
|
|
1669
|
+
* conservative reading — the solver's leaf is a CLTV, which matures against
|
|
1670
|
+
* median-time-past, and MTP trails wall clock — so the real window extends
|
|
1671
|
+
* PAST this deadline rather than ending before it. Every second of margin
|
|
1672
|
+
* subtracted here is a second of live claim window given away for nothing.
|
|
1673
|
+
*
|
|
1674
|
+
* **The trader has no move after it.** Nothing here can take the lockup
|
|
1675
|
+
* back, so once the window shuts the swap is the solver's to resolve and
|
|
1676
|
+
* this manager's job is to watch it happen and then stop.
|
|
1677
|
+
*/
|
|
1678
|
+
private driveReceiveClaim;
|
|
1679
|
+
/**
|
|
1680
|
+
* Claim what the solver funded, once it is enough.
|
|
1681
|
+
*
|
|
1682
|
+
* The value gate here decides WHEN to act. `pushClaim`'s decides whether
|
|
1683
|
+
* `P` is published, and runs with nothing between it and the signature —
|
|
1684
|
+
* the check that matters is the inner one, and this is not a reason to
|
|
1685
|
+
* relax it.
|
|
1686
|
+
*/
|
|
1687
|
+
private claimIfFunded;
|
|
1688
|
+
/** `handled` ends the pass; `continue` falls through to the refund gate. */
|
|
1689
|
+
private driveOnchain;
|
|
1690
|
+
private driveArkadeRefund;
|
|
1691
|
+
/** Whether any of these outputs has never been handed to the claim
|
|
1692
|
+
* callback — the only reason to claim a lockup a second time. */
|
|
1693
|
+
private hasUnclaimedOutpoint;
|
|
1694
|
+
private rememberClaimed;
|
|
1695
|
+
/**
|
|
1696
|
+
* L1 progress, which past the refund window must not overwrite a refusal.
|
|
1697
|
+
* The two halves are independent — a claimed fill says nothing about
|
|
1698
|
+
* whether this wallet can take the Arkade lockup back — and `claimed` is
|
|
1699
|
+
* re-asserted from chain on every pass, so without this a blocked swap
|
|
1700
|
+
* would flip between the two states forever. The claim itself always runs;
|
|
1701
|
+
* only the label defers, and only once the refund is the live half.
|
|
1702
|
+
*/
|
|
1703
|
+
private setOnchainState;
|
|
1704
|
+
/** The probe's refusal reason, or `undefined` when a local refund is
|
|
1705
|
+
* possible as far as anyone here can tell. A probe that throws is treated
|
|
1706
|
+
* as a refusal: a capability check that cannot answer is not a yes. */
|
|
1707
|
+
private probeRefusal;
|
|
1708
|
+
/** Report that no local refund will happen, without ending the swap. */
|
|
1709
|
+
private block;
|
|
1710
|
+
/** The way back out, taken as soon as the swap becomes actionable again.
|
|
1711
|
+
* Back to what the record can prove, not to `pending` unconditionally: a
|
|
1712
|
+
* swap that already made its claim has a txid for it, and reporting that
|
|
1713
|
+
* swap as `pending` would un-say something true. */
|
|
1714
|
+
private unblock;
|
|
1715
|
+
private touch;
|
|
1716
|
+
private setState;
|
|
1717
|
+
/** Terminal failure. The `onSwapFailed` emission is left to
|
|
1718
|
+
* {@link finalize}, so this does not double-report. */
|
|
1719
|
+
private fail;
|
|
1720
|
+
private emitFailed;
|
|
1721
|
+
private emitAction;
|
|
1722
|
+
/** Whether the record is now persisted — false only when `saveSwap` threw. */
|
|
1723
|
+
private save;
|
|
1724
|
+
/**
|
|
1725
|
+
* Drop a terminal swap from monitoring and report it exactly once.
|
|
1726
|
+
*
|
|
1727
|
+
* `onSwapCompleted` and `onSwapFailed` are mutually exclusive here, unlike
|
|
1728
|
+
* Boltz's manager, which fires completion for every swap that leaves
|
|
1729
|
+
* monitoring including the failed ones — a listener named "completed" that
|
|
1730
|
+
* also fires on failure is a trap worth not inheriting.
|
|
1731
|
+
*/
|
|
1732
|
+
private finalize;
|
|
1733
|
+
private settleWaiters;
|
|
1734
|
+
}
|
|
1735
|
+
/** What {@link RfqSwapManager.waitForSwapCompletion} reports. `txid` is the
|
|
1736
|
+
* trader's own claim — L1 for a claimed onchain send, Arkade for a claimed
|
|
1737
|
+
* receive — or the ark txid for a refund the trader pushed; a solver-side
|
|
1738
|
+
* settlement or refund carries none, and a receive swap that ended `refunded`
|
|
1739
|
+
* carries none either, however far its claim got (see {@link outcomeOf}). So
|
|
1740
|
+
* a `txid` here always names something that happened, and `state` remains the
|
|
1741
|
+
* only thing to read for whether the swap paid out. */
|
|
1742
|
+
interface RfqSwapOutcome {
|
|
1743
|
+
state: RfqSwapState;
|
|
1744
|
+
txid?: string;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
export { type ArkadeRefundResult, AssetSwap, AssetSwapRepository, ChainSource, ChainUtxo, type ClaimArkProvider, type ClaimPacketInput, type DiscoverMarketsOptions, IndexedDbAssetSwapRepository, type LightningReceiveSwap, type LightningSendSwap, LockupAmountMismatchError, type LockupContractWriter, type LockupFate, LockupNeedsRecoveryError, LockupRegistrationFailed, type LockupSpendIndexer, type LockupVtxo, MarketsCacheEntry, OFFER_PACKET_TYPE, type Offer, type OfferContractRetirer, type OfferSwapWatcher, OnchainHtlc, OnchainHtlcPhase, type OnchainSendAction, type OnchainSendSwap, type PlanError, QUOTE_OPTIONS, REFUND_MTP_LAG_SECONDS, RFQ_RESOLVED_STATES, RFQ_SWAP_TERMINAL_STATES, type RefundArkProvider, type RefundIndexer, type RefundOutcome, type RestoreIndexer, RfqStatus, type RfqSwap, type RfqSwapActionName, type RfqSwapLockup, RfqSwapManager, type RfqSwapManagerCallbacks, type RfqSwapManagerConfig, type RfqSwapManagerDeps, type RfqSwapManagerEvents, type RfqSwapOutcome, type RfqSwapState, RfqTransport, SWAP_LOCKUP_CONTRACT_KIND, SWAP_LOCKUP_CONTRACT_LABEL, SWAP_LOCKUP_CONTRACT_TYPE, type SealedClaimPacket, type SpendKind, type SwapContractRegistry, type Tx, type WatchOfferSwapsParams, awaitLockupFunding, awaitRfqResolution, cancelOffer, claimReceiveLockup, classifyDepositSpend, classifySpend, createOffer, decodeOffer, discoverMarkets, encodeOffer, findLockupVtxos, findMarket, isRfqSwapTerminal, isRfqTerminal, makeCachedFeedFetch, nextOnchainAction, offerVtxoScript, pushClaim, pushRefundWithoutReceiver, readLockupFate, refundIfUnresolved, registerLockupContract, restoreAssetSwaps, retireSettledOfferContracts, sealClaimPacket, spendTxidsOf, spendUpdate, swapPrograms, validatePlan, watchOfferSwaps };
|