@arkade-os/swap 0.0.7 → 0.0.8
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 +271 -12
- package/dist/{chunk-WGRU2DBF.js → chunk-6ZUS47GA.js} +15 -1
- package/dist/{chunk-ZDTRQZE2.js → chunk-TU4NGZDP.js} +59 -22
- package/dist/index.cjs +1150 -427
- package/dist/index.d.cts +292 -1139
- package/dist/index.d.ts +292 -1139
- package/dist/index.js +759 -87
- package/dist/nostr.d.cts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +1 -1
- package/dist/repositories/realm/index.cjs +49 -5
- package/dist/repositories/realm/index.d.cts +28 -6
- package/dist/repositories/realm/index.d.ts +28 -6
- package/dist/repositories/realm/index.js +50 -6
- package/dist/repositories/sqlite/index.cjs +45 -4
- package/dist/repositories/sqlite/index.d.cts +20 -7
- package/dist/repositories/sqlite/index.d.ts +20 -7
- package/dist/repositories/sqlite/index.js +46 -5
- package/dist/repository-BcZ9LXRP.d.ts +1862 -0
- package/dist/repository-Dso34L4D.d.cts +1862 -0
- package/dist/{rfq-DfT9dAss.d.cts → rfq-C-aq5LDJ.d.cts} +63 -2
- package/dist/{rfq-DfT9dAss.d.ts → rfq-C-aq5LDJ.d.ts} +63 -2
- package/package.json +2 -2
- package/dist/repository-BwnZ8N62.d.cts +0 -236
- package/dist/repository-BwnZ8N62.d.ts +0 -236
|
@@ -0,0 +1,1862 @@
|
|
|
1
|
+
import { DiscoveredMarket } from '@arkade-os/solver-discovery';
|
|
2
|
+
import { IWallet, ProvisionedKey, ProvisionedClaimSecret, RestArkProvider, RestIndexerProvider, VHTLC, Identity, IContractManager } from '@arkade-os/sdk';
|
|
3
|
+
import { w as RfqStatus, x as RfqTransport, a as OnchainHtlc, e as ChainUtxo, C as ChainSource, s as OnchainHtlcPhase } from './rfq-C-aq5LDJ.js';
|
|
4
|
+
|
|
5
|
+
type AssetSwapStatus = "pending" | "cancelling" | "fulfilled" | "cancelled" | "recoverable" | "awaiting_fill" | "claimable" | "claimed" | "refunded_l1";
|
|
6
|
+
/** The sentinel asset id for BTC itself, as opposed to a 68-hex asset id.
|
|
7
|
+
* Lives here with the {@link AssetSwap} fields it describes so the market and
|
|
8
|
+
* restore layers share one spelling instead of re-typing the literal. */
|
|
9
|
+
declare const BTC_ASSET_ID = "btc";
|
|
10
|
+
/**
|
|
11
|
+
* The record fields a wallet-provisioned secret becomes — what
|
|
12
|
+
* {@link swapSecretsToRecord} emits, and what every record type carrying swap
|
|
13
|
+
* secrets embeds.
|
|
14
|
+
*
|
|
15
|
+
* A named type rather than four fields restated per record: the mapper and the
|
|
16
|
+
* records it feeds must agree exactly, and a record that silently omits one of
|
|
17
|
+
* these round-trips a swap whose preimage cannot be re-derived. Embedding makes
|
|
18
|
+
* the omission a compile error instead.
|
|
19
|
+
*
|
|
20
|
+
* **Only `preimageHex` is secret.** `signingDescriptor` and `preimageSaltHex`
|
|
21
|
+
* are public derivation inputs — they must survive a field-mapped backend, but
|
|
22
|
+
* they leak nothing without the seed.
|
|
23
|
+
*/
|
|
24
|
+
interface SwapSecretsProjection {
|
|
25
|
+
/**
|
|
26
|
+
* The wallet descriptor this swap's sender key comes from — a fresh HD
|
|
27
|
+
* child, or a static wallet's `tr(pubkey)`. Public — the signer
|
|
28
|
+
* re-derives from the wallet, so the record carries no key material.
|
|
29
|
+
*/
|
|
30
|
+
signingDescriptor?: string;
|
|
31
|
+
/** P, hex, when it cannot be re-derived from the seed at all: the user
|
|
32
|
+
* supplied it, or the signer cannot sign deterministically. The swap's only
|
|
33
|
+
* claim secret when present. */
|
|
34
|
+
preimageHex?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The salt P derives from, hex, on the salted arm — what a static wallet
|
|
37
|
+
* gets instead of storing P. **Public**, and unlike every other field here
|
|
38
|
+
* it is minted per swap: it is what stops one repeating key from handing
|
|
39
|
+
* every swap the same preimage.
|
|
40
|
+
*/
|
|
41
|
+
preimageSaltHex?: string;
|
|
42
|
+
}
|
|
43
|
+
interface AssetSwap extends SwapSecretsProjection {
|
|
44
|
+
/** Funding txid — the swap's identity. */
|
|
45
|
+
id: string;
|
|
46
|
+
/** 'btc' or a 68-hex asset id. */
|
|
47
|
+
fromAsset: string;
|
|
48
|
+
toAsset: string;
|
|
49
|
+
/** Atomic amounts as strings (bigint is not JSON-safe). */
|
|
50
|
+
fromAmount: string;
|
|
51
|
+
/** The covenant wantAmount — a floor, the fill pays >= this. */
|
|
52
|
+
toAmount: string;
|
|
53
|
+
swapAddress: string;
|
|
54
|
+
/** Hex pkScript of the swap contract — the indexer monitoring key. */
|
|
55
|
+
swapPkScript: string;
|
|
56
|
+
/** TLV offer — needed to rebuild the contract for cancel. */
|
|
57
|
+
offerHex: string;
|
|
58
|
+
fundingTxid: string;
|
|
59
|
+
spentTxid?: string;
|
|
60
|
+
status: AssetSwapStatus;
|
|
61
|
+
createdAt: number;
|
|
62
|
+
completedAt?: number;
|
|
63
|
+
/** RFQ pair string, e.g. `arkade:BTC->onchain:BTC`. */
|
|
64
|
+
pair?: string;
|
|
65
|
+
/** `sha256(P)`, hex. Public, and how a restore confirms a candidate
|
|
66
|
+
* derivation is the right one. */
|
|
67
|
+
paymentHash?: string;
|
|
68
|
+
/** The L1 HTLC's pkScript, hex — the chain-watch key. */
|
|
69
|
+
htlcPkScriptHex?: string;
|
|
70
|
+
htlcLocktime?: number;
|
|
71
|
+
/** The L1 funding txid, once observed. */
|
|
72
|
+
l1Txid?: string;
|
|
73
|
+
}
|
|
74
|
+
/** All swaps, newest-first. Insertion order is not chronological — the restore
|
|
75
|
+
* scan rebuilds records in tx-scan order — so sort at read to keep
|
|
76
|
+
* newest-first canonical for every consumer. */
|
|
77
|
+
declare const getAssetSwapsOrThrow: (repository: AssetSwapRepository) => Promise<AssetSwap[]>;
|
|
78
|
+
/** The consumer read: a broken backend reads as no swaps rather than crashing
|
|
79
|
+
* a history view. Mutations must use {@link getAssetSwapsOrThrow} instead —
|
|
80
|
+
* swallowing the read there would let "the backend is gone" masquerade as "no
|
|
81
|
+
* such swap" and skip the write silently. */
|
|
82
|
+
declare const getAssetSwaps: (repository: AssetSwapRepository) => Promise<AssetSwap[]>;
|
|
83
|
+
/** Add a swap; no-op if the id is already stored. Returns the updated list.
|
|
84
|
+
* THROWS on a failed write — nothing irreversible may happen until this record
|
|
85
|
+
* is durable, so the caller must not fund on a failure. */
|
|
86
|
+
declare const addAssetSwap: (repository: AssetSwapRepository, swap: AssetSwap) => Promise<AssetSwap[]>;
|
|
87
|
+
/** Merge changes into a swap by id. Returns the updated list.
|
|
88
|
+
* THROWS on a failed read or write, like {@link addAssetSwap} — use this for a
|
|
89
|
+
* write that gates something irreversible. Transitions written *after* the
|
|
90
|
+
* irreversible act belong on {@link updateAssetSwapBestEffort}. */
|
|
91
|
+
declare const updateAssetSwap: (repository: AssetSwapRepository, id: string, changes: Partial<Omit<AssetSwap, "id">>) => Promise<AssetSwap[]>;
|
|
92
|
+
/**
|
|
93
|
+
* {@link updateAssetSwap} for transitions that follow an irreversible action (a
|
|
94
|
+
* broadcast claim, a spent lockup): failing the caller there would report as
|
|
95
|
+
* failed a swap whose funds already moved, and a stale status is recoverable —
|
|
96
|
+
* crash recovery re-derives the true state from the chain
|
|
97
|
+
* (`classifyOnchainHtlc`).
|
|
98
|
+
*
|
|
99
|
+
* `persisted` is the part that must not be hidden: a caller that notifies on a
|
|
100
|
+
* change, or treats one as terminal, has to know the store did not agree.
|
|
101
|
+
*/
|
|
102
|
+
declare const updateAssetSwapBestEffort: (repository: AssetSwapRepository, id: string, changes: Partial<Omit<AssetSwap, "id">>) => Promise<{
|
|
103
|
+
swaps: AssetSwap[];
|
|
104
|
+
persisted: boolean;
|
|
105
|
+
}>;
|
|
106
|
+
/**
|
|
107
|
+
* The record fields a wallet-provisioned secret becomes.
|
|
108
|
+
*
|
|
109
|
+
* `signingDescriptor` is public and always stored — it is what recovers the
|
|
110
|
+
* signer. Then at most one of: `preimageHex`, when the wallet says it cannot
|
|
111
|
+
* re-derive P and it becomes the swap's only claim secret; or
|
|
112
|
+
* `preimageSaltHex`, the public input a derivable-but-repeating key needs.
|
|
113
|
+
*/
|
|
114
|
+
declare const swapSecretsToRecord: (secrets: ProvisionedKey | ProvisionedClaimSecret) => SwapSecretsProjection & {
|
|
115
|
+
signingDescriptor: string;
|
|
116
|
+
};
|
|
117
|
+
/** Why a wallet cannot produce a swap's preimage. */
|
|
118
|
+
type PreimageBlockedReason =
|
|
119
|
+
/** The record carries no `signingDescriptor`. */
|
|
120
|
+
"no-secrets"
|
|
121
|
+
/** `preimageHex` or `preimageSaltHex` is present but not 32 bytes of hex. */
|
|
122
|
+
| "malformed-record"
|
|
123
|
+
/**
|
|
124
|
+
* Nothing to derive from: a descriptor that repeats across swaps, with
|
|
125
|
+
* neither a stored preimage nor a salt — or one this wallet holds no key
|
|
126
|
+
* for. Merged deliberately: `contractSigner` reports a key it does not
|
|
127
|
+
* hold as a plain `Error` for static wallets and a `ForeignDescriptorError`
|
|
128
|
+
* for HD ones, so splitting the two here would mean matching on message
|
|
129
|
+
* text, which is the thing this type exists to avoid. The `cause` carries
|
|
130
|
+
* whichever it was.
|
|
131
|
+
*/
|
|
132
|
+
| "not-derivable"
|
|
133
|
+
/** Derived, but it does not hash to the record's `paymentHash`. */
|
|
134
|
+
| "hash-mismatch";
|
|
135
|
+
/**
|
|
136
|
+
* The wallet cannot produce this swap's preimage, and which of the four ways
|
|
137
|
+
* is `reason`.
|
|
138
|
+
*
|
|
139
|
+
* Deliberately **not** {@link RefundNotLocallyPossibleError}: that one means
|
|
140
|
+
* "no local refund is possible", and `RfqSwapManager` acts on it by reporting
|
|
141
|
+
* `needs_counterparty`. A claim-path read failure is a different verdict, and
|
|
142
|
+
* borrowing the refund error would have the manager announce one for the
|
|
143
|
+
* other.
|
|
144
|
+
*/
|
|
145
|
+
declare class PreimageNotRecoverableError extends Error {
|
|
146
|
+
readonly reason: PreimageBlockedReason;
|
|
147
|
+
readonly name = "PreimageNotRecoverableError";
|
|
148
|
+
constructor(reason: PreimageBlockedReason, message: string, options?: {
|
|
149
|
+
cause?: unknown;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The preimage a swap record claims with — stored, or re-derived from the
|
|
154
|
+
* wallet.
|
|
155
|
+
*
|
|
156
|
+
* The record-shaped inverse of {@link swapSecretsToRecord}, and the one place
|
|
157
|
+
* that knows which of a record's fields `contractPreimage` needs. Wire claim
|
|
158
|
+
* paths here rather than composing it by hand: a caller that forgets to pass
|
|
159
|
+
* `preimageSaltHex` gets a *wrong* preimage from a wallet that can derive,
|
|
160
|
+
* not an error.
|
|
161
|
+
*
|
|
162
|
+
* Verifies the result against `paymentHash` when the record carries one. The
|
|
163
|
+
* salted arm has two inputs that can be wrong — the key and the salt — where
|
|
164
|
+
* the HD arm had one, and a wrong P otherwise surfaces as an opaque script
|
|
165
|
+
* failure at claim time, long after the mistake.
|
|
166
|
+
*
|
|
167
|
+
* Every refusal is a {@link PreimageNotRecoverableError} carrying a `reason`,
|
|
168
|
+
* so a caller can tell "this record predates the descriptor" from "the salt is
|
|
169
|
+
* corrupt" without reading message text.
|
|
170
|
+
*/
|
|
171
|
+
declare const preimageForSwapRecord: (wallet: IWallet, record: SwapSecretsProjection & {
|
|
172
|
+
paymentHash?: string;
|
|
173
|
+
}) => Promise<Uint8Array>;
|
|
174
|
+
|
|
175
|
+
/** True for the states after which the solver will report nothing further. */
|
|
176
|
+
declare const isRfqTerminal: (state: string) => boolean;
|
|
177
|
+
/**
|
|
178
|
+
* The terminal states that mean the swap is OVER and the lockup is already
|
|
179
|
+
* gone — the solver either claimed it (`settled`, revealing the preimage) or
|
|
180
|
+
* returned it (`refunded`). A trader seeing one of these has nothing left to
|
|
181
|
+
* do.
|
|
182
|
+
*
|
|
183
|
+
* Deliberately narrower than {@link RFQ_TERMINAL_STATES}: `refused`, `expired`
|
|
184
|
+
* and `stuck` are terminal for the NEGOTIATION but say nothing about whether
|
|
185
|
+
* the trader's sats are still sitting at the lockup. A trader that funded just
|
|
186
|
+
* as the quote expired, or whose solver wedged mid-payment, is exactly the
|
|
187
|
+
* trader who needs the refund most — so those states must not short-circuit
|
|
188
|
+
* it. {@link refundIfUnresolved} treats them as "keep going", and lets the
|
|
189
|
+
* on-chain VTXO lookup be the authority on whether anything is actually there.
|
|
190
|
+
*/
|
|
191
|
+
declare const RFQ_RESOLVED_STATES: readonly ["settled", "refunded"];
|
|
192
|
+
/**
|
|
193
|
+
* Poll a swap's status until it reaches a terminal state.
|
|
194
|
+
*
|
|
195
|
+
* Same shape and conventions as {@link awaitOnchainFill}: a `pollMs` interval,
|
|
196
|
+
* an optional unix-seconds `deadline`, and a thrown error carrying a stable
|
|
197
|
+
* `reason` when that deadline passes.
|
|
198
|
+
*
|
|
199
|
+
* A `null` status (the solver has no record of this `rfq_id`) is treated as
|
|
200
|
+
* "not yet", not as an answer — a status route can legitimately 404 for a
|
|
201
|
+
* moment after a quote is issued. The deadline is what bounds that wait.
|
|
202
|
+
*
|
|
203
|
+
* Transport errors are NOT swallowed; a failing `status()` call rejects this
|
|
204
|
+
* function. Callers polling across a long refund window should expect to
|
|
205
|
+
* restart it after a network blip — nothing is lost by doing so, since the
|
|
206
|
+
* refund path this feeds is gated on an absolute timelock that does not
|
|
207
|
+
* expire.
|
|
208
|
+
*/
|
|
209
|
+
declare function awaitRfqResolution(transport: RfqTransport, rfqId: string, options?: {
|
|
210
|
+
pollMs?: number;
|
|
211
|
+
deadline?: number;
|
|
212
|
+
}): Promise<RfqStatus>;
|
|
213
|
+
/** The Ark surface the refund push needs — narrower than a full provider, and
|
|
214
|
+
* satisfied by {@link RestArkProvider}. Same seam style as `RestoreIndexer`. */
|
|
215
|
+
type RefundArkProvider = Pick<RestArkProvider, "getInfo" | "submitTx" | "finalizeTx">;
|
|
216
|
+
/** The indexer surface the lockup lookup needs. */
|
|
217
|
+
type RefundIndexer = Pick<RestIndexerProvider, "getVtxos">;
|
|
218
|
+
/** A still-refundable virtual output sitting at the swap lockup. */
|
|
219
|
+
interface LockupVtxo {
|
|
220
|
+
txid: string;
|
|
221
|
+
vout: number;
|
|
222
|
+
value: number;
|
|
223
|
+
/**
|
|
224
|
+
* The batch this output lived in expired and the operator swept it, so it
|
|
225
|
+
* is no longer a live leaf — it can be RECOVERED, but not spent offchain.
|
|
226
|
+
*
|
|
227
|
+
* It is still the trader's money and it is still visible, which is why
|
|
228
|
+
* {@link findLockupVtxos} returns it. What it is not is refundable by
|
|
229
|
+
* {@link pushRefundWithoutReceiver}: that builds an offchain Ark
|
|
230
|
+
* transaction, and the SDK's own predicates make the two states mutually
|
|
231
|
+
* exclusive — `canSpendOffchain` is false exactly when `canRecoverOnchain`
|
|
232
|
+
* is true (`wallet/vtxo.ts`), and the latter is documented as "must be
|
|
233
|
+
* recovered into a fresh batch rather than spent offchain". Holding the
|
|
234
|
+
* trader's `sender` key does not change that; a sweep removes the leaf from
|
|
235
|
+
* the live tree, not the signature from the trader.
|
|
236
|
+
*
|
|
237
|
+
* `packages/boltz-swap` splits on exactly this fact rather than working
|
|
238
|
+
* around it: `settleRefundWithoutReceiver` sends a live VTXO through an
|
|
239
|
+
* offchain tx and a recoverable one through `joinBatch` — "a swept
|
|
240
|
+
* (recoverable) VTXO is no longer a live leaf, so it can only be reclaimed
|
|
241
|
+
* by re-registering it into a batch".
|
|
242
|
+
*
|
|
243
|
+
* So the remedy is recovery (renewing the output into a fresh batch),
|
|
244
|
+
* after which the ordinary CLTV refund works again. This package does not
|
|
245
|
+
* build that round — see {@link pushRefundWithoutReceiver}, which refuses
|
|
246
|
+
* rather than submitting a spend that cannot succeed.
|
|
247
|
+
*/
|
|
248
|
+
recoverable: boolean;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Thrown when a refund was asked for over outputs that have been swept.
|
|
252
|
+
*
|
|
253
|
+
* Carries the outpoints so a caller can act — recover exactly those, then
|
|
254
|
+
* retry — instead of reading a server rejection and guessing. `reason` follows
|
|
255
|
+
* the same convention as `awaitOnchainFill`'s `fill_timeout` and
|
|
256
|
+
* `claimOnchainFill`'s `claim_window_closed`.
|
|
257
|
+
*
|
|
258
|
+
* **The remedy already exists; this package does not reimplement it.** The SDK
|
|
259
|
+
* recovers swept outputs by re-registering them into a fresh batch, through
|
|
260
|
+
* `IVtxoManager.recoverVtxos()` — the same batch round `packages/boltz-swap`
|
|
261
|
+
* reaches via its own `joinBatch`. It reads the wallet's registered-contract
|
|
262
|
+
* snapshot (`recoverVtxos` → `wallet.getVtxos({ withRecoverable: true })` →
|
|
263
|
+
* `contractSnapshot()` → `contractManager.getContractsWithVtxos()`), so it
|
|
264
|
+
* covers a swap lockup as soon as that lockup is registered as a contract —
|
|
265
|
+
* which is what {@link RfqSwapManagerDeps.contracts} does. Registration is
|
|
266
|
+
* therefore not only a latency optimization; it is what turns a swept lockup
|
|
267
|
+
* from a dead end into something the ordinary wallet path can recover.
|
|
268
|
+
*
|
|
269
|
+
* Two caveats a caller must hold, neither enforceable from here:
|
|
270
|
+
*
|
|
271
|
+
* - **The wallet must hold the lockup's `sender` key**, because recovery
|
|
272
|
+
* settles through `refundWithoutReceiver` — the leaf `vhtlc-v2` annotates
|
|
273
|
+
* these VTXOs with.
|
|
274
|
+
* - **`refundLocktime` must have matured.** That leaf carries a CLTV, so a
|
|
275
|
+
* recovery round including this VTXO earlier is rejected. `recoverVtxos`
|
|
276
|
+
* sweeps every recoverable output in ONE settlement and has no CLTV
|
|
277
|
+
* awareness, so recovering early can fail the whole batch rather than just
|
|
278
|
+
* this output. `packages/boltz-swap` encodes the same rule as "pre-CLTV
|
|
279
|
+
* recoverable → skipped".
|
|
280
|
+
*/
|
|
281
|
+
declare class LockupNeedsRecoveryError extends Error {
|
|
282
|
+
readonly name = "LockupNeedsRecoveryError";
|
|
283
|
+
readonly reason = "needs_recovery";
|
|
284
|
+
/** `txid:vout` for each output that must be recovered first. */
|
|
285
|
+
readonly outpoints: string[];
|
|
286
|
+
/**
|
|
287
|
+
* The contract's `refundLocktime`. Recovering before this matures is the
|
|
288
|
+
* hazard described above: `recoverVtxos()` sweeps EVERY recoverable output
|
|
289
|
+
* into one settlement with no CLTV awareness, so an early attempt can fail
|
|
290
|
+
* the whole batch — including unrelated outputs that were otherwise fine.
|
|
291
|
+
*
|
|
292
|
+
* Exposed as a value, not only inside the message, so a caller can encode
|
|
293
|
+
* `packages/boltz-swap`'s "pre-CLTV recoverable → skipped" rule without
|
|
294
|
+
* parsing prose. Seconds-based locktimes mature against the chain tip's
|
|
295
|
+
* timestamp rather than wall clock, so treat this as a floor to wait past,
|
|
296
|
+
* not an exact alarm.
|
|
297
|
+
*/
|
|
298
|
+
readonly recoverableAfter: bigint;
|
|
299
|
+
constructor(outpoints: string[], recoverableAfter: bigint);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Every output still sitting at the lockup script — spendable AND
|
|
303
|
+
* swept-but-recoverable, each tagged with which it is.
|
|
304
|
+
*
|
|
305
|
+
* All of them, not the first: a trader may fund a lockup in more than one
|
|
306
|
+
* send, and refunding only `vtxos[0]` returns part of the money and strands
|
|
307
|
+
* the rest at a script whose other refund paths are all longer.
|
|
308
|
+
*
|
|
309
|
+
* BOTH queries, because they are disjoint sets and `spendableOnly` alone goes
|
|
310
|
+
* blind at exactly the wrong moment. A lockup whose batch expiry passed is
|
|
311
|
+
* swept into the recoverable set, and this function exists to serve swaps that
|
|
312
|
+
* sat unresolved — which are precisely the ones most likely to have got there.
|
|
313
|
+
* Reading only the spendable set would report `nothing_to_refund` over money
|
|
314
|
+
* that is still sitting at the script, which is worse than an error: it looks
|
|
315
|
+
* like a resolved swap. `packages/boltz-swap` merges the same two queries for
|
|
316
|
+
* the same reason (`arkade-swaps.ts`'s `refundableVtxos`).
|
|
317
|
+
*
|
|
318
|
+
* **Visible is not the same as refundable.** A `recoverable` output cannot be
|
|
319
|
+
* spent offchain at all — see {@link LockupVtxo.recoverable} — so this set is
|
|
320
|
+
* "what is there", not "what {@link pushRefundWithoutReceiver} can take back".
|
|
321
|
+
* That function refuses the recoverable ones by name rather than submitting a
|
|
322
|
+
* spend the server must reject.
|
|
323
|
+
*
|
|
324
|
+
* This read — not the RFQ's reported state — is the authority on whether
|
|
325
|
+
* there is anything left at the lockup.
|
|
326
|
+
*
|
|
327
|
+
* **Not replaced by the contract manager's VTXO state, deliberately.** Once a
|
|
328
|
+
* lockup is registered (see `RfqSwapManagerDeps.contracts`) the wallet tracks
|
|
329
|
+
* these same outputs, and `getContractsWithVtxos` plus `canSpendOffchain` /
|
|
330
|
+
* `canRecoverOnchain` would classify them. That is a WEAKER answer here on two
|
|
331
|
+
* counts: it serves the wallet REPOSITORY, which a degraded sync will happily
|
|
332
|
+
* hand back stale (`getSyncState()` reports `degraded` and returns cached rows
|
|
333
|
+
* rather than failing), and its height-based expiry test needs a chain tip this
|
|
334
|
+
* module does not have. The two queries below ask the indexer itself and need
|
|
335
|
+
* neither. Ask-the-indexer, don't-trust-local-state — the same posture
|
|
336
|
+
* {@link readLockupFate} takes, and for the same reason: this decides money.
|
|
337
|
+
*/
|
|
338
|
+
declare function findLockupVtxos(indexer: RefundIndexer, swapPkScript: Uint8Array): Promise<LockupVtxo[]>;
|
|
339
|
+
/**
|
|
340
|
+
* The indexer surface the lockup-spend read needs: the vtxo lookup, plus the
|
|
341
|
+
* raw transactions those vtxos were spent by. Same narrow-seam style as
|
|
342
|
+
* {@link RefundIndexer} and `restore.ts`'s `RestoreIndexer`, and satisfied by
|
|
343
|
+
* {@link RestIndexerProvider}.
|
|
344
|
+
*/
|
|
345
|
+
type LockupSpendIndexer = Pick<RestIndexerProvider, "getVtxos" | "getVirtualTxs">;
|
|
346
|
+
/**
|
|
347
|
+
* What chain data says became of a swap lockup — the whole answer, with no
|
|
348
|
+
* solver involvement and nothing taken on the solver's word.
|
|
349
|
+
*/
|
|
350
|
+
interface LockupSpend {
|
|
351
|
+
/** What the vtxo's `spentBy` names — the checkpoint, never the ark
|
|
352
|
+
* transaction. */
|
|
353
|
+
checkpointTxid: string;
|
|
354
|
+
/** The ark transaction that spent the above checkpoint output. What
|
|
355
|
+
* history correlation matches on; absent when the indexer omitted it. */
|
|
356
|
+
arkTxid?: string;
|
|
357
|
+
}
|
|
358
|
+
type LockupFate =
|
|
359
|
+
/** At least one output at the lockup is still unspent. Not over. */
|
|
360
|
+
{
|
|
361
|
+
fate: "open";
|
|
362
|
+
}
|
|
363
|
+
/** Spent by a witness carrying a preimage that HASHES to the quote's
|
|
364
|
+
* `payment_hash`. Only the claim leaf can reveal one, and the only
|
|
365
|
+
* legitimate way the solver obtains it is by completing its side. */
|
|
366
|
+
| {
|
|
367
|
+
fate: "claimed";
|
|
368
|
+
preimage: Uint8Array;
|
|
369
|
+
spends: readonly LockupSpend[];
|
|
370
|
+
}
|
|
371
|
+
/** Fully spent, and nothing that spent it revealed a matching preimage —
|
|
372
|
+
* so the money went back to the trader. See {@link readLockupFate}. */
|
|
373
|
+
| {
|
|
374
|
+
fate: "returned";
|
|
375
|
+
spends: readonly LockupSpend[];
|
|
376
|
+
}
|
|
377
|
+
/** Nothing was learned: no outputs visible, an output spent by nothing the
|
|
378
|
+
* indexer names, a spend it could not produce, or a blob that would not
|
|
379
|
+
* decode. Never an answer. */
|
|
380
|
+
| {
|
|
381
|
+
fate: "unknown";
|
|
382
|
+
};
|
|
383
|
+
/**
|
|
384
|
+
* Decide from chain data alone whether a swap lockup settled, came back, or is
|
|
385
|
+
* still live.
|
|
386
|
+
*
|
|
387
|
+
* **Why this is decidable without asking anyone.** The lockup's claim leaf can
|
|
388
|
+
* only be spent by revealing `P`, so a spend witness carrying a value that
|
|
389
|
+
* hashes to the quote's `payment_hash` is proof the claim leaf was used — and
|
|
390
|
+
* the only legitimate way the counterparty obtains `P` is by completing its
|
|
391
|
+
* side of the swap. Every OTHER leaf is a refund: `nonInteractiveRefund` is
|
|
392
|
+
* covenant-pinned to the trader's own address (`enforcePayTo(senderPkScript)`),
|
|
393
|
+
* and `refund`, `refundWithoutReceiver`, `unilateralRefund` and
|
|
394
|
+
* `unilateralRefundWithoutReceiver` all require the trader's own signature. So
|
|
395
|
+
* "spent, but not by a hash-verified claim" means the money went back to the
|
|
396
|
+
* trader, and nothing here has to trust a counterparty to say so.
|
|
397
|
+
*
|
|
398
|
+
* **A matching witness SHAPE is not proof.** Only a candidate that hashes to
|
|
399
|
+
* `paymentHash` may be read as a claim; a 32-byte item that hashes to anything
|
|
400
|
+
* else is just bytes, and is treated as a refund. Getting this wrong in the
|
|
401
|
+
* permissive direction would report "settled" for a swap that actually
|
|
402
|
+
* refunded, which is precisely the fact a trader is relying on.
|
|
403
|
+
*
|
|
404
|
+
* **`unknown` is not `returned`.** An empty vtxo set (indexer lag, or a lockup
|
|
405
|
+
* not visible yet), a `spentBy` the indexer cannot produce a transaction for,
|
|
406
|
+
* or a blob that will not decode all come back as `unknown`. `getVirtualTxs`
|
|
407
|
+
* may legitimately return fewer transactions than were asked for, so the
|
|
408
|
+
* observed set is counted rather than assumed complete. The caller's correct
|
|
409
|
+
* response to `unknown` is the same as to `open`: keep watching, and let the
|
|
410
|
+
* refund timelock — which no outage can move — be what ends the wait.
|
|
411
|
+
*
|
|
412
|
+
* Ask-the-indexer, don't-trust-local-state: read fresh on every poll, never
|
|
413
|
+
* cached, the same posture {@link findLockupVtxos} already establishes.
|
|
414
|
+
*/
|
|
415
|
+
declare function readLockupFate(indexer: LockupSpendIndexer, input: {
|
|
416
|
+
swapPkScript: Uint8Array;
|
|
417
|
+
/** `sha256(P)`, hex — the quote's `payment_hash`. */
|
|
418
|
+
paymentHash: string;
|
|
419
|
+
}): Promise<LockupFate>;
|
|
420
|
+
/**
|
|
421
|
+
* Build, sign, and push the `refundWithoutReceiver` spend: return every funded
|
|
422
|
+
* output at the lockup to the trader's refund address.
|
|
423
|
+
*
|
|
424
|
+
* The leaf is `CLTV(refundLocktime) + <sender> + <server>` — the trader's own
|
|
425
|
+
* VHTLC `sender` key and the Arkade server, and NOBODY else. In particular the
|
|
426
|
+
* emulator is not involved: it co-signs only the two covenant leaves
|
|
427
|
+
* (`nonInteractiveClaim` / `nonInteractiveRefund`), which is why the solver's
|
|
428
|
+
* own escape hatch has to go through it and this one does not. So unlike that
|
|
429
|
+
* push, this transaction is submitted SIGNED, and the only counterparty is the
|
|
430
|
+
* Arkade server doing what it does for any collaborative spend.
|
|
431
|
+
*
|
|
432
|
+
* One aggregate output, not one per input — again unlike the solver's covenant
|
|
433
|
+
* refund, which needs index-aligned outputs because its ArkadeScript inspects
|
|
434
|
+
* the output at the current input's index. This leaf carries no covenant, so a
|
|
435
|
+
* single output paying the whole balance is both valid and cheaper.
|
|
436
|
+
*
|
|
437
|
+
* `refundPkScript` defaults to the destination the contract itself commits to
|
|
438
|
+
* (`nonInteractiveRefund`'s `senderPkScript`, i.e. the address the trader gave
|
|
439
|
+
* at quote time), so the ordinary call cannot send the refund somewhere the
|
|
440
|
+
* trader did not intend. It is overridable because this leaf, having no
|
|
441
|
+
* covenant, genuinely does permit any destination.
|
|
442
|
+
*
|
|
443
|
+
* **Consensus, not wall clock, decides when this is spendable.** A seconds
|
|
444
|
+
* locktime matures against median-time-past, which trails real time by roughly
|
|
445
|
+
* an hour, so a push issued the moment `refundLocktime` passes can be rejected
|
|
446
|
+
* until enough blocks land. That is expected, not a failure — see
|
|
447
|
+
* {@link refundIfUnresolved}, which retries.
|
|
448
|
+
*
|
|
449
|
+
* **Swept outputs are refused, not attempted.** This is an OFFCHAIN spend, and
|
|
450
|
+
* a swept output is no longer a live leaf: `canSpendOffchain` and
|
|
451
|
+
* `canRecoverOnchain` are mutually exclusive by construction, so a recoverable
|
|
452
|
+
* input cannot be spent this way whatever key signs it (see
|
|
453
|
+
* {@link LockupVtxo.recoverable}). Because every input lands in ONE aggregate
|
|
454
|
+
* transaction, a single swept output would take the live ones down with it —
|
|
455
|
+
* so the whole push is refused with {@link LockupNeedsRecoveryError} naming the
|
|
456
|
+
* outpoints, rather than submitted and rejected. Filtering them out silently
|
|
457
|
+
* would be worse still: it would report success over money that never moved.
|
|
458
|
+
*/
|
|
459
|
+
declare function pushRefundWithoutReceiver(ark: RefundArkProvider, input: {
|
|
460
|
+
script: InstanceType<typeof VHTLC.ScriptV2>;
|
|
461
|
+
/** The `sender` signer. Build it from the swap record with
|
|
462
|
+
* {@link senderIdentityForSwapRecord} — on an HD wallet that resolves
|
|
463
|
+
* from the seed, with no stored key bytes anywhere, and every way the
|
|
464
|
+
* wallet can fail to produce it arrives as one typed
|
|
465
|
+
* {@link RefundNotLocallyPossibleError} the manager reads as permanent
|
|
466
|
+
* rather than retrying for the rest of the refund window. */
|
|
467
|
+
sender: Identity;
|
|
468
|
+
vtxos: readonly LockupVtxo[];
|
|
469
|
+
/** Defaults to the contract's own committed refund destination. */
|
|
470
|
+
refundPkScript?: Uint8Array;
|
|
471
|
+
}): Promise<{
|
|
472
|
+
arkTxid: string;
|
|
473
|
+
amount: number;
|
|
474
|
+
}>;
|
|
475
|
+
/**
|
|
476
|
+
* How long past `refundLocktime` to keep retrying the push before giving up
|
|
477
|
+
* and surfacing the server's refusal.
|
|
478
|
+
*
|
|
479
|
+
* Two hours because the CLTV matures against median-time-past (BIP-113), which
|
|
480
|
+
* lags wall clock by about an hour, plus room for a slow block. This is the
|
|
481
|
+
* mirror of `MIN_HEADROOM_SECONDS`, which refuses to FUND without 90 minutes
|
|
482
|
+
* of the same margin.
|
|
483
|
+
*/
|
|
484
|
+
declare const REFUND_MTP_LAG_SECONDS: number;
|
|
485
|
+
type RefundOutcome =
|
|
486
|
+
/** The solver resolved it — claimed (`settled`) or returned it (`refunded`). */
|
|
487
|
+
{
|
|
488
|
+
outcome: "resolved";
|
|
489
|
+
status: RfqStatus;
|
|
490
|
+
}
|
|
491
|
+
/** The trader took it back via `refundWithoutReceiver`. */
|
|
492
|
+
| {
|
|
493
|
+
outcome: "refunded";
|
|
494
|
+
arkTxid: string;
|
|
495
|
+
amount: number;
|
|
496
|
+
status: RfqStatus | null;
|
|
497
|
+
}
|
|
498
|
+
/** The refund window opened but the lockup holds nothing to return. */
|
|
499
|
+
| {
|
|
500
|
+
outcome: "nothing_to_refund";
|
|
501
|
+
status: RfqStatus | null;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* The money is still at the lockup, but its batch was swept, so no offchain
|
|
505
|
+
* spend can take it back until it is recovered into a fresh batch. Returned
|
|
506
|
+
* rather than retried: unlike a median-time-past refusal, no amount of
|
|
507
|
+
* waiting fixes this — see {@link LockupNeedsRecoveryError}. Recover the
|
|
508
|
+
* named outpoints, then call this again.
|
|
509
|
+
*/
|
|
510
|
+
| {
|
|
511
|
+
outcome: "needs_recovery";
|
|
512
|
+
outpoints: string[];
|
|
513
|
+
vtxos: LockupVtxo[];
|
|
514
|
+
status: RfqStatus | null;
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* Ask first, then fall back: watch the swap for the solver to resolve it, and
|
|
518
|
+
* if `refundLocktime` matures without that happening, take the lockup back
|
|
519
|
+
* with `refundWithoutReceiver`.
|
|
520
|
+
*
|
|
521
|
+
* This is the whole trader-side failure story in one call. It polls `status()`
|
|
522
|
+
* — the only "asking" this protocol has (see the module doc) — and returns as
|
|
523
|
+
* soon as the solver reports `settled` or `refunded`. Otherwise, once the
|
|
524
|
+
* quote's `refund_locktime` passes, it looks up what is actually at the lockup
|
|
525
|
+
* and pushes the refund.
|
|
526
|
+
*
|
|
527
|
+
* Two behaviours worth knowing:
|
|
528
|
+
*
|
|
529
|
+
* - **A dead negotiation is not a reason to stop.** `refused`, `expired` and
|
|
530
|
+
* `stuck` are terminal states, but a trader can be holding a funded lockup in
|
|
531
|
+
* every one of them, so they do not end the wait — only `settled`/`refunded`
|
|
532
|
+
* do (see {@link RFQ_RESOLVED_STATES}). What ends it otherwise is the
|
|
533
|
+
* timelock.
|
|
534
|
+
* - **The first push after the deadline may legitimately fail.** Median-time-
|
|
535
|
+
* past trails wall clock, so the server can still consider the leaf locked
|
|
536
|
+
* for a while after `refundLocktime` passes in real time. Failures are
|
|
537
|
+
* retried at the poll interval until `attemptDeadline`, after which the last
|
|
538
|
+
* error is rethrown rather than swallowed.
|
|
539
|
+
* - **A swept lockup ends the wait instead of consuming it.** Once the batch
|
|
540
|
+
* is gone the CLTV refund is not "not yet" but "not this way", so it returns
|
|
541
|
+
* `needs_recovery` naming the outpoints rather than retrying until the
|
|
542
|
+
* deadline. Recover them and call again.
|
|
543
|
+
*
|
|
544
|
+
* Safe to call late, and safe to call again: a caller recovering from a crash
|
|
545
|
+
* well past the deadline skips straight to the push, and a lockup that is
|
|
546
|
+
* already empty comes back as `nothing_to_refund` instead of an error.
|
|
547
|
+
*/
|
|
548
|
+
declare function refundIfUnresolved(transport: RfqTransport, ark: RefundArkProvider, indexer: RefundIndexer, input: {
|
|
549
|
+
rfqId: string;
|
|
550
|
+
script: InstanceType<typeof VHTLC.ScriptV2>;
|
|
551
|
+
/** @see pushRefundWithoutReceiver */
|
|
552
|
+
sender: Identity;
|
|
553
|
+
/** `refund_locktime` from the quote, unix seconds. */
|
|
554
|
+
refundLocktime: number;
|
|
555
|
+
/** Defaults to the contract's own committed refund destination. */
|
|
556
|
+
refundPkScript?: Uint8Array;
|
|
557
|
+
pollMs?: number;
|
|
558
|
+
/** Stop retrying the push at this unix time, rethrowing the last
|
|
559
|
+
* error. Defaults to `refundLocktime + REFUND_MTP_LAG_SECONDS`. */
|
|
560
|
+
attemptDeadline?: number;
|
|
561
|
+
/** Injected for tests; defaults to wall clock, in unix seconds. */
|
|
562
|
+
now?: () => number;
|
|
563
|
+
}): Promise<RefundOutcome>;
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Where a monitored RFQ swap stands, and which of those states end it.
|
|
567
|
+
*
|
|
568
|
+
* Its own module rather than a corner of `swapManager.ts` because the
|
|
569
|
+
* dependency runs the other way: the record layer decides retention from
|
|
570
|
+
* {@link isRfqSwapTerminal}, and the manager persists through the record
|
|
571
|
+
* layer. With the vocabulary here, neither has to import the other at runtime.
|
|
572
|
+
* `swapManager.ts` re-exports all three names, so nothing about the public
|
|
573
|
+
* surface moved.
|
|
574
|
+
*/
|
|
575
|
+
/**
|
|
576
|
+
* Where a monitored swap stands.
|
|
577
|
+
*
|
|
578
|
+
* `claimable` and `claimed` are the states of a swap the TRADER has something
|
|
579
|
+
* to claim on: the L1 fill on an onchain send, and the solver-funded lockup on
|
|
580
|
+
* a receive. Only `lightning_send` has neither — there the solver claims the
|
|
581
|
+
* lockup, and the trader's only move is the refund.
|
|
582
|
+
*/
|
|
583
|
+
type RfqSwapState =
|
|
584
|
+
/** Live; nothing actionable yet. On a receive leg this covers the whole
|
|
585
|
+
* stretch before the solver funds anything. */
|
|
586
|
+
"pending"
|
|
587
|
+
/** There is something for the trader to take, and the window to take it is
|
|
588
|
+
* open: the confirmed L1 fill on an onchain send, or a lockup funded for at
|
|
589
|
+
* least `expectedAmount` on a receive. */
|
|
590
|
+
| "claimable"
|
|
591
|
+
/**
|
|
592
|
+
* The trader's claim has been made — its L1 broadcast on an onchain send,
|
|
593
|
+
* its Arkade submission on a receive.
|
|
594
|
+
*
|
|
595
|
+
* **On a receive this is a local belief and not a chain fact**, which is
|
|
596
|
+
* why it is not terminal: `settled` is the chain's answer, and `refunded`
|
|
597
|
+
* is still reachable from here if the claim never lands and the solver
|
|
598
|
+
* takes the lockup back.
|
|
599
|
+
*/
|
|
600
|
+
| "claimed"
|
|
601
|
+
/**
|
|
602
|
+
* This wallet will not act, and only the counterparty can change that.
|
|
603
|
+
*
|
|
604
|
+
* On a send leg: the Arkade refund cannot be pushed from here — no secrets
|
|
605
|
+
* on the record, a descriptor from another seed, or nothing wired to act —
|
|
606
|
+
* so the lockup comes back only if the counterparty claims it or the wallet
|
|
607
|
+
* that can sign it is restored. On a receive leg: the trader holds no
|
|
608
|
+
* refund at all, so this is a lockup that cannot be claimed — funded for
|
|
609
|
+
* less than the swap agreed (publishing `P` for it is the whole attack
|
|
610
|
+
* `LockupAmountMismatchError` exists to refuse), or one whose claim window
|
|
611
|
+
* shut unclaimed. `RfqSwapCommon.blockedReason` says which.
|
|
612
|
+
*
|
|
613
|
+
* **Not terminal, and not a dead end.** The money is still at the lockup,
|
|
614
|
+
* so the counterparty's move is still observable and still ends the swap;
|
|
615
|
+
* and the refusal is re-checked every pass, so restoring the right wallet,
|
|
616
|
+
* wiring the callbacks, or the solver topping the lockup up returns the
|
|
617
|
+
* swap to `pending` and resumes the normal drive. For an onchain-send swap
|
|
618
|
+
* it says nothing about the L1 half, which keeps being driven and claimed.
|
|
619
|
+
*/
|
|
620
|
+
| "needs_counterparty"
|
|
621
|
+
/**
|
|
622
|
+
* Terminal: the lockup was spent by a hash-verified claim. Read off chain,
|
|
623
|
+
* never reported.
|
|
624
|
+
*
|
|
625
|
+
* On a send leg that claim is the counterparty's, and it is proof the
|
|
626
|
+
* counterparty completed its side. On a receive leg it is the TRADER's own
|
|
627
|
+
* — matched by the hash and not by our txid, so a claim that lands without
|
|
628
|
+
* us still counts (see `RfqSwapManager`).
|
|
629
|
+
*/
|
|
630
|
+
| "settled"
|
|
631
|
+
/**
|
|
632
|
+
* Terminal: the lockup was spent by something other than a claim.
|
|
633
|
+
*
|
|
634
|
+
* On a send leg that is the money coming back, by the solver's hand or the
|
|
635
|
+
* trader's. **On a receive leg it is a LOSS**: the lockup was the solver's
|
|
636
|
+
* money, every non-claim leaf is the solver's, and a swap that ends here
|
|
637
|
+
* ended with the trader's incoming payment never arriving. It is also where
|
|
638
|
+
* a receive swap ends when its window closes with nothing left to observe —
|
|
639
|
+
* see `RfqSwapManager`.
|
|
640
|
+
*/
|
|
641
|
+
| "refunded"
|
|
642
|
+
/** Terminal: an action failed and its window closed. */
|
|
643
|
+
| "failed";
|
|
644
|
+
/** The states after which the manager stops monitoring a swap. Deliberately
|
|
645
|
+
* without `needs_counterparty`: retiring on it would unwatch a funded lockup
|
|
646
|
+
* whose claim is still the thing that ends the swap. */
|
|
647
|
+
declare const RFQ_SWAP_TERMINAL_STATES: readonly ["settled", "refunded", "failed"];
|
|
648
|
+
declare const isRfqSwapTerminal: (state: RfqSwapState) => boolean;
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* What the manager needs to register a swap's lockup with the wallet, so the
|
|
652
|
+
* indexer pushes its funding and its spend instead of being asked every few
|
|
653
|
+
* seconds.
|
|
654
|
+
*
|
|
655
|
+
* Both fields are things the caller already holds. `script` is the very object
|
|
656
|
+
* `pushRefundWithoutReceiver` and `pushClaim` take, so a caller wired to act has
|
|
657
|
+
* it in hand; `address` is the request entrypoint's own return value. The
|
|
658
|
+
* address is taken rather than re-derived on purpose — the row's address must be
|
|
659
|
+
* the one that was actually funded, and a local re-derivation would silently use
|
|
660
|
+
* the SDK's default network, which is the exact bug `registerOfferContract`
|
|
661
|
+
* guards against.
|
|
662
|
+
*/
|
|
663
|
+
interface RfqSwapLockup {
|
|
664
|
+
/** The covenant. Its `pkScript` MUST equal the record's `lockupPkScript`. */
|
|
665
|
+
script: InstanceType<typeof VHTLC.ScriptV2>;
|
|
666
|
+
/** The Arkade address that was funded. */
|
|
667
|
+
address: string;
|
|
668
|
+
}
|
|
669
|
+
interface RfqSwapCommon {
|
|
670
|
+
/** The negotiation id — this record's identity. */
|
|
671
|
+
rfqId: string;
|
|
672
|
+
state: RfqSwapState;
|
|
673
|
+
/** The Arkade lockup's scriptPubKey — `swapPkScript` from any of the four
|
|
674
|
+
* request entrypoints. This is what the manager watches to decide the swap:
|
|
675
|
+
* it is the only handle on the covenant whose spend witness says whether
|
|
676
|
+
* the swap settled or came back. */
|
|
677
|
+
lockupPkScript: Uint8Array;
|
|
678
|
+
/** The covenant behind {@link lockupPkScript}, when the caller wants the
|
|
679
|
+
* lockup registered with a contract manager. Optional: without it the
|
|
680
|
+
* manager still watches the swap on its timer, it just cannot subscribe.
|
|
681
|
+
* See {@link RfqSwapManagerDeps.contracts}. */
|
|
682
|
+
lockup?: RfqSwapLockup;
|
|
683
|
+
/**
|
|
684
|
+
* `sha256(P)`, hex — the quote's `payment_hash`. The claim leaf can only
|
|
685
|
+
* be spent by revealing a value that hashes to this, which is what makes a
|
|
686
|
+
* settlement provable rather than reported. For an onchain send this is
|
|
687
|
+
* the SAME hash the L1 `htlc` carries: one `P` unlocks both legs.
|
|
688
|
+
*
|
|
689
|
+
* True of the three corridors that exist today and only of them: a hashlock
|
|
690
|
+
* belongs to a CORRIDOR, and a banco-style one settles without any. The
|
|
691
|
+
* stored record already says so — the hash lives in `profile.hashlock`, not
|
|
692
|
+
* on `RfqSwapRecord` — and this field follows onto the per-corridor swap
|
|
693
|
+
* types when the first such corridor lands. Do not read the current shape as
|
|
694
|
+
* settled.
|
|
695
|
+
*/
|
|
696
|
+
paymentHash: string;
|
|
697
|
+
/**
|
|
698
|
+
* `refund_locktime` from the quote, unix seconds.
|
|
699
|
+
*
|
|
700
|
+
* Whose deadline it is inverts with the direction, and so does what to do
|
|
701
|
+
* about it. On a send leg it is the TRADER's: the lockup is the trader's
|
|
702
|
+
* money and this gates the refund that takes it back, so it is a moment to
|
|
703
|
+
* act AFTER. On a receive leg it is the SOLVER's: the trader has no refund
|
|
704
|
+
* leaf at all, and this is the moment to have claimed BEFORE.
|
|
705
|
+
*/
|
|
706
|
+
refundLocktime: number;
|
|
707
|
+
createdAt: number;
|
|
708
|
+
updatedAt: number;
|
|
709
|
+
/** Set once the trader's own `refundWithoutReceiver` push landed. */
|
|
710
|
+
refundArkTxid?: string;
|
|
711
|
+
/**
|
|
712
|
+
* The ark transactions that SPENT the lockup, stamped from the chain read
|
|
713
|
+
* that ended the swap — `LockupFate.spends`, whichever verdict it reached.
|
|
714
|
+
*
|
|
715
|
+
* The counterparty's move, on every leg but one: a solver claim on a send,
|
|
716
|
+
* a solver reclaim on a receive, and — the exception — the trader's own
|
|
717
|
+
* claim when a receive settles. What they have in common is that no local
|
|
718
|
+
* action produced them, so nothing else on this record can name them:
|
|
719
|
+
* {@link refundArkTxid} names only a push this wallet made, and
|
|
720
|
+
* `claimArkTxid` only a submission it made.
|
|
721
|
+
*
|
|
722
|
+
* Stamped so a terminal record answers "which transaction ended this" from
|
|
723
|
+
* storage. Without it the only source is another read of the lockup — a
|
|
724
|
+
* network round trip per terminal swap, which is what activity correlation
|
|
725
|
+
* has to pay on the offline-first path where it is least affordable.
|
|
726
|
+
*
|
|
727
|
+
* Absent when the swap ended without a chain verdict, or when the indexer
|
|
728
|
+
* named the checkpoint but not the ark transaction — the same `arkTxid`
|
|
729
|
+
* `LockupSpend` declares optional, for the same reason.
|
|
730
|
+
*/
|
|
731
|
+
lockupSpendArkTxids?: string[];
|
|
732
|
+
/** Why `state` is `failed`. */
|
|
733
|
+
failure?: string;
|
|
734
|
+
/** Why `state` is `needs_counterparty`. Distinct from {@link failure},
|
|
735
|
+
* which means an action was attempted and did not work. */
|
|
736
|
+
blockedReason?: string;
|
|
737
|
+
}
|
|
738
|
+
/** `arkade:BTC->lightning:BTC`. Nothing for the trader to claim: the solver
|
|
739
|
+
* claims the lockup with the preimage it learns by paying the invoice — which
|
|
740
|
+
* is exactly why that spend's witness is proof the payment landed. */
|
|
741
|
+
interface LightningSendSwap extends RfqSwapCommon {
|
|
742
|
+
kind: "lightning_send";
|
|
743
|
+
}
|
|
744
|
+
/** `arkade:BTC->onchain:BTC`. Carries the L1 half the trader must claim. */
|
|
745
|
+
interface OnchainSendSwap extends RfqSwapCommon {
|
|
746
|
+
kind: "onchain_send";
|
|
747
|
+
/** The locally derived HTLC from `requestOnchainSend` — the manager reads
|
|
748
|
+
* `pkScript`, `paymentHash` and `refundLocktime` off it to classify. */
|
|
749
|
+
htlc: OnchainHtlc;
|
|
750
|
+
/** `profile.min_confirmations` from the quote. */
|
|
751
|
+
minConfirmations: number;
|
|
752
|
+
/** The fill's outpoint, learned on first sighting. Without it a SPENT
|
|
753
|
+
* HTLC reads as never funded — see {@link classifyOnchainHtlc}. */
|
|
754
|
+
funding?: {
|
|
755
|
+
txid: string;
|
|
756
|
+
vout: number;
|
|
757
|
+
};
|
|
758
|
+
/** Our L1 claim's txid. */
|
|
759
|
+
claimTxid?: string;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* `lightning:BTC->arkade:BTC`. The inverted leg: the SOLVER funds the lockup
|
|
763
|
+
* and the TRADER claims it, and that claim is what publishes `P` and lets the
|
|
764
|
+
* solver settle the payer's held Lightning HTLC.
|
|
765
|
+
*
|
|
766
|
+
* Two consequences shape how this record is driven, both of them absent from
|
|
767
|
+
* the send legs:
|
|
768
|
+
*
|
|
769
|
+
* - **There is no trader-side refund.** Every non-claim leaf of this covenant
|
|
770
|
+
* is the solver's, so the manager never calls
|
|
771
|
+
* {@link RfqSwapManagerCallbacks.refundArkade} for one of these. A swap that
|
|
772
|
+
* is not claimed is simply lost — the solver reclaims at
|
|
773
|
+
* {@link RfqSwapCommon.refundLocktime} and the payer is refunded when the
|
|
774
|
+
* held HTLC lapses.
|
|
775
|
+
* - **The claim is the whole swap, and it is on a deadline.** The trader must
|
|
776
|
+
* be online for it: covclaimd cannot claim this covenant today, so the claim
|
|
777
|
+
* packet's offline path does not run.
|
|
778
|
+
*/
|
|
779
|
+
interface LightningReceiveSwap extends RfqSwapCommon {
|
|
780
|
+
kind: "lightning_receive";
|
|
781
|
+
/**
|
|
782
|
+
* What the lockup must carry — the quote's `to_amount`, captured at REQUEST
|
|
783
|
+
* time and persisted with the record.
|
|
784
|
+
*
|
|
785
|
+
* **Not re-derivable, and not optional.** Captured at claim time it would
|
|
786
|
+
* be whatever the solver funded, which is the dust-funding attack rather
|
|
787
|
+
* than a check on it. A record that reaches the manager without a finite
|
|
788
|
+
* value here is reported `needs_counterparty` and never claimed: a
|
|
789
|
+
* comparison against `undefined` or `NaN` is false, so an unusable
|
|
790
|
+
* comparand does not fail the value gate, it deletes it.
|
|
791
|
+
*/
|
|
792
|
+
expectedAmount: number;
|
|
793
|
+
/** Our Arkade claim's txid, once submitted. Set from the callback's return
|
|
794
|
+
* and never from a chain read — the chain's answer is `settled`. */
|
|
795
|
+
claimArkTxid?: string;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* A monitored swap.
|
|
799
|
+
*
|
|
800
|
+
* This is a live record, not a serialization format: `lockupPkScript` and
|
|
801
|
+
* `htlc` hold derived `Uint8Array`s, and `RfqSwapRecord` is its storable
|
|
802
|
+
* projection. Give the manager a {@link RfqSwapManagerDeps.repository} and it
|
|
803
|
+
* writes and rebuilds these itself, through
|
|
804
|
+
* {@link RfqSwapManager.restoreFromRepository}. A caller keeping its own store
|
|
805
|
+
* projects it in {@link RfqSwapManagerCallbacks.saveSwap} instead, rebuilds it
|
|
806
|
+
* on restart the way it was made — `lightningSendVtxoScript` /
|
|
807
|
+
* `receiveVtxoScript` / `onchainHtlcScript` over the quote's binding fields —
|
|
808
|
+
* and hands the result to {@link RfqSwapManager.start}.
|
|
809
|
+
*
|
|
810
|
+
* **`onchain:BTC->arkade:BTC` is deliberately not a member yet.** Its Arkade
|
|
811
|
+
* half is the same solver-funded lockup as {@link LightningReceiveSwap}'s, but
|
|
812
|
+
* it also has an L1 half the trader funds and must take back itself
|
|
813
|
+
* (`buildHtlcRefund` at the HTLC's own `htlc_locktime`), which is a second
|
|
814
|
+
* deadline, a second observation seam and a second action callback. Adding the
|
|
815
|
+
* lockup half alone would produce a manager that silently lets that L1 refund
|
|
816
|
+
* window pass — the one failure mode {@link RfqSwapManager} refuses elsewhere
|
|
817
|
+
* by name (see `driveOnchain`'s missing-`ChainSource` check). Until the L1
|
|
818
|
+
* refund is driven too, that corridor is better served by the request and claim
|
|
819
|
+
* functions directly than by a monitor that covers half of it.
|
|
820
|
+
*/
|
|
821
|
+
type RfqSwap = LightningSendSwap | OnchainSendSwap | LightningReceiveSwap;
|
|
822
|
+
/** What the manager should do next about an onchain-send swap's L1 half. */
|
|
823
|
+
type OnchainSendAction =
|
|
824
|
+
/** Not funded yet, or not confirmed deep enough. */
|
|
825
|
+
"wait"
|
|
826
|
+
/** Funded, confirmed, and far enough from the refund leaf to claim safely. */
|
|
827
|
+
| "claim"
|
|
828
|
+
/** The claim is off the table for good; the money comes back through the
|
|
829
|
+
* Arkade lockup instead. */
|
|
830
|
+
| "claim_window_closed"
|
|
831
|
+
/** Our claim already landed (only the trader holds P). */
|
|
832
|
+
| "claimed"
|
|
833
|
+
/** The solver took its L1 refund — the fill is gone. */
|
|
834
|
+
| "swept";
|
|
835
|
+
/**
|
|
836
|
+
* The decision a "poll status, refund on timeout" loop gets wrong.
|
|
837
|
+
*
|
|
838
|
+
* {@link OnchainHtlcPhase} runs `unfunded -> awaiting_confirmations ->
|
|
839
|
+
* claimable -> (refundable | claimed | swept)`, and `refundable` does NOT mean
|
|
840
|
+
* "time to refund the L1 HTLC" — the trader has no key on that leaf; it is the
|
|
841
|
+
* SOLVER's refund, and reaching it means the trader's claim was missed. So the
|
|
842
|
+
* L1 claim has to be driven before it, and from it the only remaining move is
|
|
843
|
+
* the Arkade-side refund.
|
|
844
|
+
*
|
|
845
|
+
* There is a second, quieter trap between those two functions:
|
|
846
|
+
* `classifyOnchainHtlc` reports `claimable` right up until median-time-past
|
|
847
|
+
* reaches `refundLocktime`, while `claimOnchainFill` refuses from
|
|
848
|
+
* {@link ONCHAIN_CLAIM_MARGIN_SECONDS} before it — because broadcasting
|
|
849
|
+
* publishes P, and doing that into the counterparty's live refund window risks
|
|
850
|
+
* losing the race AND giving away the preimage. Driving straight off the phase
|
|
851
|
+
* would therefore spend that whole margin throwing `claim_window_closed` at
|
|
852
|
+
* every poll and never fall back. This function applies the margin, so
|
|
853
|
+
* "claimable" here means claimable by `claimOnchainFill` too.
|
|
854
|
+
*/
|
|
855
|
+
declare function nextOnchainAction(input: {
|
|
856
|
+
phase: OnchainHtlcPhase;
|
|
857
|
+
/** `htlc.refundLocktime` — when the solver's L1 refund leaf opens. */
|
|
858
|
+
htlcLocktime: number;
|
|
859
|
+
/** Unix seconds. */
|
|
860
|
+
now: number;
|
|
861
|
+
}): OnchainSendAction;
|
|
862
|
+
/** What the trader's own `refundWithoutReceiver` push returned, or `null` when
|
|
863
|
+
* the lockup held nothing to return. */
|
|
864
|
+
type ArkadeRefundResult = {
|
|
865
|
+
arkTxid: string;
|
|
866
|
+
amount: number;
|
|
867
|
+
} | null;
|
|
868
|
+
/**
|
|
869
|
+
* The money-moving half, injected. The manager decides when; these do it.
|
|
870
|
+
*
|
|
871
|
+
* Neither action gets a retry loop of its own here — see the module doc. Take
|
|
872
|
+
* `arkadeRefunder` for `refundArkade` rather than assembling it: it composes
|
|
873
|
+
* the atomic push (`findLockupVtxos` + `senderIdentityForSwapRecord` +
|
|
874
|
+
* `pushRefundWithoutReceiver`) and keeps the three rules below structural.
|
|
875
|
+
*
|
|
876
|
+
* Do NOT wire `refundArkade` to `refundIfUnresolved`: that function is the
|
|
877
|
+
* single-swap version of this whole class and brings its own status polling
|
|
878
|
+
* and its own MTP retry loop, which would nest inside the manager's.
|
|
879
|
+
*
|
|
880
|
+
* Resolve the sender key through `senderIdentityForSwapRecord`: it is
|
|
881
|
+
* what turns "this wallet cannot sign this swap" into
|
|
882
|
+
* {@link RefundNotLocallyPossibleError}, which the manager reports as
|
|
883
|
+
* `needs_counterparty` instead of retrying for the whole refund window.
|
|
884
|
+
*/
|
|
885
|
+
interface RfqSwapManagerCallbacks {
|
|
886
|
+
/** Build and broadcast the L1 claim. See `claimOnchainFill`. */
|
|
887
|
+
claimOnchain: (swap: OnchainSendSwap, utxo: ChainUtxo) => Promise<{
|
|
888
|
+
txid: string;
|
|
889
|
+
}>;
|
|
890
|
+
/**
|
|
891
|
+
* Claim the solver-funded lockup on a receive leg, revealing `P`. Wire it
|
|
892
|
+
* to `pushClaim` — the outputs are supplied, so `findLockupVtxos` has
|
|
893
|
+
* already been called and `claimReceiveLockup`'s wait would only sit on a
|
|
894
|
+
* lockup the manager has just seen.
|
|
895
|
+
*
|
|
896
|
+
* **Pass `expectedAmount` and `partiallyClaimed` straight through.** The
|
|
897
|
+
* manager checks the funded value before calling this, but that check
|
|
898
|
+
* decides WHEN to act; `pushClaim`'s decides whether `P` is published, and
|
|
899
|
+
* it is the one that runs with nothing between it and the signature. Two
|
|
900
|
+
* checks, one of which is load-bearing — do not drop the inner one because
|
|
901
|
+
* the outer one exists.
|
|
902
|
+
*
|
|
903
|
+
* Required here, like {@link claimOnchain}: a receive swap monitored with
|
|
904
|
+
* nothing wired to claim it is a swap that quietly expires, and a compile
|
|
905
|
+
* error is the right way to learn that. A consumer that drives only the
|
|
906
|
+
* kinds needing neither installs
|
|
907
|
+
* {@link AvailableRfqSwapManagerCallbacks} instead and takes the runtime
|
|
908
|
+
* refusal in its place.
|
|
909
|
+
*/
|
|
910
|
+
claimLockup: (swap: LightningReceiveSwap, vtxos: readonly LockupVtxo[], options: {
|
|
911
|
+
/** A claim of ours is already out, so `P` is public and the value
|
|
912
|
+
* gate has nothing left to protect — pass this to `pushClaim` so a
|
|
913
|
+
* funding that arrived piecemeal can still be swept. */
|
|
914
|
+
partiallyClaimed: boolean;
|
|
915
|
+
}) => Promise<{
|
|
916
|
+
arkTxid: string;
|
|
917
|
+
amount: number;
|
|
918
|
+
}>;
|
|
919
|
+
/**
|
|
920
|
+
* Push `refundWithoutReceiver` for every output at the lockup. See
|
|
921
|
+
* `pushRefundWithoutReceiver`; return `null` for an empty lockup. Never
|
|
922
|
+
* called for a {@link LightningReceiveSwap} — that leg's refund leaf is the
|
|
923
|
+
* solver's.
|
|
924
|
+
*
|
|
925
|
+
* Only {@link RefundNotLocallyPossibleError} is read as permanent. Every
|
|
926
|
+
* other throw is treated as transient and RETRIED once a poll: each attempt
|
|
927
|
+
* reports through `onSwapFailed` unwrapped, and the swap ends `failed` only
|
|
928
|
+
* past `refundLocktime + REFUND_MTP_LAG_SECONDS`. That is deliberate for a
|
|
929
|
+
* genuinely transient failure — `LockupNeedsRecoveryError` is the
|
|
930
|
+
* case it is built for, since recovery is something the caller can perform
|
|
931
|
+
* while the window is still open — but it means a wiring mistake that
|
|
932
|
+
* throws unconditionally does not read as `needs_counterparty`. It reports
|
|
933
|
+
* once a poll for the rest of the refund window and then ends `failed`.
|
|
934
|
+
* Throw {@link RefundNotLocallyPossibleError} for anything this wallet will
|
|
935
|
+
* never be able to do.
|
|
936
|
+
*/
|
|
937
|
+
refundArkade: (swap: RfqSwap) => Promise<ArkadeRefundResult>;
|
|
938
|
+
/**
|
|
939
|
+
* Whether a local refund is possible at all — the record's secrets, against
|
|
940
|
+
* this wallet. Called every pass, including *before* the refund window
|
|
941
|
+
* opens, so a swap nobody can refund says so while the solver can still
|
|
942
|
+
* act, instead of at the deadline; and so restoring the right wallet lifts
|
|
943
|
+
* the state again. Never called for a receive swap: there is no local
|
|
944
|
+
* refund there to probe for.
|
|
945
|
+
*
|
|
946
|
+
* Optional: omit to answer "yes" and learn at push time, from
|
|
947
|
+
* {@link RefundNotLocallyPossibleError}. Local by contract — no network
|
|
948
|
+
* call belongs here.
|
|
949
|
+
*/
|
|
950
|
+
canRefundArkade?: (swap: RfqSwap) => Promise<{
|
|
951
|
+
ok: true;
|
|
952
|
+
} | {
|
|
953
|
+
ok: false;
|
|
954
|
+
reason: string;
|
|
955
|
+
}>;
|
|
956
|
+
/**
|
|
957
|
+
* Persist the record. Called after any pass that changed it.
|
|
958
|
+
*
|
|
959
|
+
* With {@link RfqSwapManagerDeps.repository} wired this is the SECOND
|
|
960
|
+
* write of the pass, not the only one: the manager writes the canonical
|
|
961
|
+
* `RfqSwapRecord` itself and then calls this. Both must succeed for the
|
|
962
|
+
* pass to count as persisted, so a rejection here still holds waiters and
|
|
963
|
+
* finalization back exactly as it does without a repository. A consumer
|
|
964
|
+
* whose `saveSwap` writes that same repository by hand should
|
|
965
|
+
* drop the duplicate when it wires the dep, and keep this for genuinely
|
|
966
|
+
* secondary sinks: metrics, a cache, a second store.
|
|
967
|
+
*/
|
|
968
|
+
saveSwap: (swap: RfqSwap) => Promise<void>;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* What {@link RfqSwapManager.setCallbacks} accepts: the full contract, minus
|
|
972
|
+
* the two claims that are already kind-gated at dispatch, and minus
|
|
973
|
+
* `saveSwap`. A consumer driving only lightning sends reaches neither claim,
|
|
974
|
+
* and stubbing them to throw is not a contract — it is a lie the compiler
|
|
975
|
+
* waves through.
|
|
976
|
+
*
|
|
977
|
+
* `saveSwap` is optional for the same reason one step removed: with
|
|
978
|
+
* {@link RfqSwapManagerDeps.repository} wired the manager writes the record
|
|
979
|
+
* itself, so a consumer with no second sink has nothing to put here, and a
|
|
980
|
+
* no-op stub would be that same lie. Omitting BOTH is the documented
|
|
981
|
+
* process-local mode: state is kept in memory and dies with the process,
|
|
982
|
+
* which is what a manager with no callbacks at all already does today.
|
|
983
|
+
*
|
|
984
|
+
* The strict {@link RfqSwapManagerCallbacks} is untouched and still means
|
|
985
|
+
* "fully wired", so a helper that takes one and calls `claimOnchain` keeps its
|
|
986
|
+
* guarantee. Only the parameter widens, which every existing caller satisfies.
|
|
987
|
+
*
|
|
988
|
+
* The compile-time guarantee this trades away is bought back at runtime: a
|
|
989
|
+
* kind whose claim is missing blocks — non-terminal, re-evaluated every pass,
|
|
990
|
+
* and lifted the moment `setCallbacks` supplies it.
|
|
991
|
+
*/
|
|
992
|
+
type AvailableRfqSwapManagerCallbacks = Omit<RfqSwapManagerCallbacks, "claimOnchain" | "claimLockup" | "saveSwap"> & Partial<Pick<RfqSwapManagerCallbacks, "claimOnchain" | "claimLockup" | "saveSwap">>;
|
|
993
|
+
/** The actions the manager executes on a caller's behalf. */
|
|
994
|
+
type RfqSwapActionName = "claimOnchain" | "claimLockup" | "refundArkade";
|
|
995
|
+
interface RfqSwapManagerEvents {
|
|
996
|
+
/** Every state change, including ones that read as going backwards.
|
|
997
|
+
* `claimed -> claimable` is legal and expected on a receive swap the solver
|
|
998
|
+
* funds piecemeal: a lockup topped up after a claim is a new claimable
|
|
999
|
+
* event, and the label says so before the sweep goes out. Treat these
|
|
1000
|
+
* states as a description of what to do next, not as a progress bar. */
|
|
1001
|
+
onSwapUpdate?: (swap: RfqSwap, previous: RfqSwapState) => void;
|
|
1002
|
+
/** Fired once, when a swap leaves monitoring `settled` or `refunded`.
|
|
1003
|
+
* A swap that ends `failed` reports through `onSwapFailed` instead — the
|
|
1004
|
+
* two are mutually exclusive. */
|
|
1005
|
+
onSwapCompleted?: (swap: RfqSwap) => void;
|
|
1006
|
+
/** Fired for any action that threw — including ones the manager will retry
|
|
1007
|
+
* on the next pass — and once more when the swap finally ends `failed`. */
|
|
1008
|
+
onSwapFailed?: (swap: RfqSwap, error: Error) => void;
|
|
1009
|
+
onActionExecuted?: (swap: RfqSwap, action: RfqSwapActionName) => void;
|
|
1010
|
+
}
|
|
1011
|
+
type SwapUpdateListener = NonNullable<RfqSwapManagerEvents["onSwapUpdate"]>;
|
|
1012
|
+
type SwapCompletedListener = NonNullable<RfqSwapManagerEvents["onSwapCompleted"]>;
|
|
1013
|
+
type SwapFailedListener = NonNullable<RfqSwapManagerEvents["onSwapFailed"]>;
|
|
1014
|
+
type ActionExecutedListener = NonNullable<RfqSwapManagerEvents["onActionExecuted"]>;
|
|
1015
|
+
interface RfqSwapManagerConfig {
|
|
1016
|
+
/** Drive claims and refunds automatically (default: true). With this off
|
|
1017
|
+
* the manager still watches and reports, so a caller can act by hand off
|
|
1018
|
+
* `claimable`. */
|
|
1019
|
+
enableAutoActions?: boolean;
|
|
1020
|
+
/** How often to run a pass, ms. Default 5000 — the same interval
|
|
1021
|
+
* `awaitOnchainFill` and `refundIfUnresolved` poll at. */
|
|
1022
|
+
pollIntervalMs?: number;
|
|
1023
|
+
/** Injected for tests; defaults to wall clock, in unix seconds — the same
|
|
1024
|
+
* convention `refundIfUnresolved` uses. */
|
|
1025
|
+
now?: () => number;
|
|
1026
|
+
events?: RfqSwapManagerEvents;
|
|
1027
|
+
}
|
|
1028
|
+
/** The contract-manager surface this needs, narrowed for injection — the same
|
|
1029
|
+
* seam style as {@link LockupSpendIndexer} and `refund.ts`'s
|
|
1030
|
+
* {@link RefundArkProvider}, and satisfied structurally by a real
|
|
1031
|
+
* `ContractManager` (`await wallet.getContractManager()`). */
|
|
1032
|
+
type SwapContractRegistry = Pick<IContractManager, "createContract" | "getContracts" | "onContractEvent" | "setContractWatchState">;
|
|
1033
|
+
/**
|
|
1034
|
+
* The record store the manager writes to, narrowed to the four RFQ methods —
|
|
1035
|
+
* the same seam style as {@link LockupSpendIndexer} and
|
|
1036
|
+
* {@link SwapContractRegistry}, and satisfied structurally by a real
|
|
1037
|
+
* `AssetSwapRepository`.
|
|
1038
|
+
*
|
|
1039
|
+
* Narrowed rather than imported whole so this module keeps no runtime edge to
|
|
1040
|
+
* `repository.ts`, and so a caller with a different backing store — a server
|
|
1041
|
+
* process holding many wallets' records in one table — can satisfy it without
|
|
1042
|
+
* implementing the offer-swap and markets halves it has no use for.
|
|
1043
|
+
*
|
|
1044
|
+
* Expect TWO calls per dirty swap per poll: `getRfqSwap` then `saveRfqSwap`.
|
|
1045
|
+
* The read is deliberate — the store is the system of record, so a consumer's
|
|
1046
|
+
* own edit to a record's origin half must not be overwritten by a copy the
|
|
1047
|
+
* manager took at boot — but a backend where a keyed read is expensive should
|
|
1048
|
+
* know it is on the write path, not just the restore path.
|
|
1049
|
+
*/
|
|
1050
|
+
interface RfqSwapRecordStore {
|
|
1051
|
+
saveRfqSwap(record: RfqSwapRecord): Promise<void>;
|
|
1052
|
+
getRfqSwap(rfqId: string): Promise<RfqSwapRecord | undefined>;
|
|
1053
|
+
getAllRfqSwaps(): Promise<RfqSwapRecord[]>;
|
|
1054
|
+
removeRfqSwap(rfqId: string): Promise<void>;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* A swap was handed to the manager with a repository wired, no origin, and no
|
|
1058
|
+
* record already in the store.
|
|
1059
|
+
*
|
|
1060
|
+
* Thrown at the door rather than at the first write, because the write happens
|
|
1061
|
+
* a pass later and by then the funding is broadcast: a swap admitted here and
|
|
1062
|
+
* refused at `save` would be monitored, acted on, and unwritable — the record
|
|
1063
|
+
* would exist only in memory while its lockup held money, and a restart would
|
|
1064
|
+
* lose it. The remedy is to pass the origin, which the caller has: it is what
|
|
1065
|
+
* the request entrypoint returned.
|
|
1066
|
+
*/
|
|
1067
|
+
declare class RfqSwapOriginRequired extends Error {
|
|
1068
|
+
/** The swap that could not be admitted. */
|
|
1069
|
+
readonly rfqId: string;
|
|
1070
|
+
constructor(rfqId: string);
|
|
1071
|
+
}
|
|
1072
|
+
/** One stored record that could not be turned back into a live swap. */
|
|
1073
|
+
interface RfqRestoreFailure {
|
|
1074
|
+
rfqId: string;
|
|
1075
|
+
/** Why — a covenant that does not derive the funded address, a lockup with
|
|
1076
|
+
* no contract row, a corridor with no handler registered. */
|
|
1077
|
+
error: Error;
|
|
1078
|
+
}
|
|
1079
|
+
interface RfqRestoreOptions {
|
|
1080
|
+
/**
|
|
1081
|
+
* Where each record's covenant parameters come from. Defaults to the
|
|
1082
|
+
* lockup's own contract row, read through
|
|
1083
|
+
* {@link RfqSwapManagerDeps.contracts}.
|
|
1084
|
+
*
|
|
1085
|
+
* Override it when the covenant lives somewhere else — a consumer keeping
|
|
1086
|
+
* its own copy of `VHTLCV2ContractHandler.serializeParams(...)`, or a
|
|
1087
|
+
* process with no contract manager at all. Whatever it returns is still
|
|
1088
|
+
* checked against the record's funded address by `rebuildRfqSwap`, so an
|
|
1089
|
+
* override cannot produce a swap watching the wrong covenant.
|
|
1090
|
+
*/
|
|
1091
|
+
params?: (record: RfqSwapRecord) => Promise<LockupParams>;
|
|
1092
|
+
}
|
|
1093
|
+
/** What {@link RfqSwapManager.restoreFromRepository} did. Every stored record
|
|
1094
|
+
* is in exactly one of the three lists. */
|
|
1095
|
+
interface RfqRestoreResult {
|
|
1096
|
+
/** Rebuilt: monitored, or kept as finished when already terminal. */
|
|
1097
|
+
restored: RfqSwap[];
|
|
1098
|
+
/** Kept in the store, but not rebuildable right now. */
|
|
1099
|
+
failed: RfqRestoreFailure[];
|
|
1100
|
+
/** Removed: terminal and past `RFQ_SWAP_RETENTION_SECONDS`. */
|
|
1101
|
+
pruned: string[];
|
|
1102
|
+
}
|
|
1103
|
+
/** The observation seams. None is owned by the manager, and none holds keys —
|
|
1104
|
+
* same philosophy as `onchainHtlc.ts`'s `ChainSource`. There is no
|
|
1105
|
+
* `RfqTransport` here on purpose: nothing this manager decides depends on the
|
|
1106
|
+
* solver answering (see the module doc). */
|
|
1107
|
+
interface RfqSwapManagerDeps {
|
|
1108
|
+
/** Arkade access. Required: this is how a swap's resolution is determined,
|
|
1109
|
+
* for both legs. */
|
|
1110
|
+
indexer: LockupSpendIndexer;
|
|
1111
|
+
/** L1 access. Required to monitor onchain-send swaps; a lightning-only
|
|
1112
|
+
* caller can leave it out. */
|
|
1113
|
+
chain?: ChainSource;
|
|
1114
|
+
/**
|
|
1115
|
+
* Where the manager persists RFQ swap records, when a caller wants it to.
|
|
1116
|
+
*
|
|
1117
|
+
* Wiring it makes the repository the CANONICAL sink: every pass that
|
|
1118
|
+
* changed a swap is written here as an {@link RfqSwapRecord}, and
|
|
1119
|
+
* {@link RfqSwapManager.restoreFromRepository} reads it back. That removes
|
|
1120
|
+
* the origin trap a caller otherwise hits assembling the write by hand —
|
|
1121
|
+
* `updateRfqSwapRecord` needs the existing record and
|
|
1122
|
+
* `createRfqSwapRecord` needs request-time facts the live swap does not
|
|
1123
|
+
* carry, so the first write of a swap cannot be composed from the swap
|
|
1124
|
+
* alone. The manager resolves that itself, which is what
|
|
1125
|
+
* {@link RfqSwapManager.addSwap}'s `origin` parameter is for.
|
|
1126
|
+
*
|
|
1127
|
+
* Optional, like every other dep here: without it the manager persists
|
|
1128
|
+
* through {@link RfqSwapManagerCallbacks.saveSwap} exactly as before, and
|
|
1129
|
+
* without either it keeps its state in memory.
|
|
1130
|
+
*/
|
|
1131
|
+
repository?: RfqSwapRecordStore;
|
|
1132
|
+
/**
|
|
1133
|
+
* The wallet's contract manager, when there is one. Optional in the same
|
|
1134
|
+
* way {@link chain} is: a caller with no wallet, or one that only wants the
|
|
1135
|
+
* timer, still gets a fully working manager — the subscription is a
|
|
1136
|
+
* LATENCY optimization and nothing depends on it.
|
|
1137
|
+
*
|
|
1138
|
+
* Supplying it buys two things. The lockup gets REGISTERED, which is what
|
|
1139
|
+
* puts it in the wallet's own contract set at all — a prerequisite for
|
|
1140
|
+
* anything that has to act on the lockup before its batch expires, since an
|
|
1141
|
+
* expired lockup is swept and loses every cooperative path. And the indexer
|
|
1142
|
+
* PUSHES its funding and its spend, so a settlement is noticed when it
|
|
1143
|
+
* happens rather than up to `pollIntervalMs` later.
|
|
1144
|
+
*
|
|
1145
|
+
* Prefer `await wallet.getContractManager()` over constructing one, the way
|
|
1146
|
+
* `createOffer` does.
|
|
1147
|
+
*/
|
|
1148
|
+
contracts?: SwapContractRegistry;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Watches a set of live RFQ swaps and drives each to its end.
|
|
1152
|
+
*
|
|
1153
|
+
* One pass per swap, in this order, every
|
|
1154
|
+
* {@link RfqSwapManagerConfig.pollIntervalMs} — and additionally the moment a
|
|
1155
|
+
* contract event names that swap's lockup, which changes only WHEN a pass runs,
|
|
1156
|
+
* never what it concludes (see {@link subscribe}):
|
|
1157
|
+
*
|
|
1158
|
+
* 0. **Register the lockup**, if a contract manager was supplied and it is not
|
|
1159
|
+
* registered yet. Best-effort; never blocks the steps below.
|
|
1160
|
+
* 1. **Ask the chain what became of the lockup** — {@link readLockupFate}. A
|
|
1161
|
+
* spend whose witness HASHES to the quote's `payment_hash` ends the swap
|
|
1162
|
+
* `settled`; a lockup fully spent by anything else ends it `refunded`.
|
|
1163
|
+
* Anything the indexer could not answer is `unknown`, which is NOT an
|
|
1164
|
+
* answer: the pass carries on to the steps below, whose deadlines an indexer
|
|
1165
|
+
* outage has no bearing on.
|
|
1166
|
+
* 2. **Drive the trader's claim.** On an onchain send that is the L1 fill — see
|
|
1167
|
+
* {@link nextOnchainAction}. On a receive it is the lockup itself, and it
|
|
1168
|
+
* ends the pass: that leg has no step 3.
|
|
1169
|
+
* 3. **Take the lockup back**, send legs only, once `refundLocktime` has passed
|
|
1170
|
+
* and step 1 has not ended the swap. This runs for onchain-send too,
|
|
1171
|
+
* including after a successful claim: the trader's lockup is still funded and
|
|
1172
|
+
* still theirs to recover if the solver never comes for it. When no local
|
|
1173
|
+
* refund is possible at all — no secrets, another wallet's descriptor,
|
|
1174
|
+
* nothing wired — the swap reports `needs_counterparty` instead of retrying
|
|
1175
|
+
* a push that cannot work.
|
|
1176
|
+
*
|
|
1177
|
+
* **What step 1 proves depends on the direction.** On a send leg every non-claim
|
|
1178
|
+
* leaf pays the trader's own committed address or needs the trader's own
|
|
1179
|
+
* signature, so "spent, but not by a hash-verified claim" means the money came
|
|
1180
|
+
* back. On a receive leg those leaves are the SOLVER's and the claim leaf is the
|
|
1181
|
+
* trader's, so the same two readings mean the opposite things — `settled` is the
|
|
1182
|
+
* trader's own claim landing, `refunded` is the solver taking back a lockup the
|
|
1183
|
+
* trader failed to claim. The read is identical; only the state docs differ.
|
|
1184
|
+
*
|
|
1185
|
+
* Two things about the receive arm that are easy to get wrong, and are asserted
|
|
1186
|
+
* in the tests rather than left to be inferred:
|
|
1187
|
+
*
|
|
1188
|
+
* - **A claim is matched by its preimage, never by our txid.** The covenant's
|
|
1189
|
+
* `nonInteractiveClaim` leaf is pinned to the trader's own payout script, so a
|
|
1190
|
+
* claim that lands without us — covclaimd, the day it works — still pays the
|
|
1191
|
+
* trader and is still `settled`. Matching on the txid we submitted would turn
|
|
1192
|
+
* that success into an anomaly.
|
|
1193
|
+
* - **`LockupFate.fate === "claimed"` maps to the state `settled`, never to the
|
|
1194
|
+
* state `claimed`.** The two words live one layer apart: the fate is the
|
|
1195
|
+
* chain's, the state is ours, and the state `claimed` means only that we
|
|
1196
|
+
* submitted something.
|
|
1197
|
+
*/
|
|
1198
|
+
declare class RfqSwapManager {
|
|
1199
|
+
private readonly deps;
|
|
1200
|
+
private readonly config;
|
|
1201
|
+
private callbacks;
|
|
1202
|
+
private readonly swapUpdateListeners;
|
|
1203
|
+
private readonly swapCompletedListeners;
|
|
1204
|
+
private readonly swapFailedListeners;
|
|
1205
|
+
private readonly actionExecutedListeners;
|
|
1206
|
+
private readonly monitored;
|
|
1207
|
+
/** Monitored swaps by lockup script hex, so a contract event — which names
|
|
1208
|
+
* a script and nothing else — can find the swap it belongs to. */
|
|
1209
|
+
private readonly byLockupScript;
|
|
1210
|
+
/**
|
|
1211
|
+
* Swaps whose lockup registration has been SETTLED one way or another,
|
|
1212
|
+
* mapped to whether a contract row actually resulted. Membership is what
|
|
1213
|
+
* stops a per-pass retry from becoming a per-pass round trip; the value is
|
|
1214
|
+
* what keeps a swap that could never be registered from later trying to
|
|
1215
|
+
* retire a row that does not exist, which would report a spurious failure
|
|
1216
|
+
* on a swap that in fact succeeded.
|
|
1217
|
+
*/
|
|
1218
|
+
private readonly registered;
|
|
1219
|
+
/**
|
|
1220
|
+
* Swaps whose `refundArkade` answered {@link RefundNotLocallyPossibleError}
|
|
1221
|
+
* in this process. Membership stops the push from being re-issued every
|
|
1222
|
+
* pass — it cannot start working on its own, and re-issuing it is the
|
|
1223
|
+
* grind `needs_counterparty` exists to remove. Only
|
|
1224
|
+
* {@link RfqSwapManagerCallbacks.canRefundArkade} clears it, so a caller
|
|
1225
|
+
* with no probe learns again on the next start, when the wallet that can
|
|
1226
|
+
* sign may well have been restored.
|
|
1227
|
+
*/
|
|
1228
|
+
private readonly refundRefused;
|
|
1229
|
+
/**
|
|
1230
|
+
* The last error a receive swap's claim callback threw, by rfqId.
|
|
1231
|
+
*
|
|
1232
|
+
* Kept only to tell two terminal outcomes apart once the claim window
|
|
1233
|
+
* shuts: a swap whose claim was attempted and kept failing ends `failed`
|
|
1234
|
+
* with that reason, while one that simply never became claimable ends
|
|
1235
|
+
* `refunded`. Without it a broken claim callback would resolve a caller's
|
|
1236
|
+
* {@link waitForSwapCompletion} as an ordinary unwind.
|
|
1237
|
+
*
|
|
1238
|
+
* Process-local, like {@link refundRefused}: after a restart the same swap
|
|
1239
|
+
* ends `refunded` instead, which costs the caller a reason and nothing else
|
|
1240
|
+
* — every throw was already reported through `onSwapFailed` as it happened.
|
|
1241
|
+
*/
|
|
1242
|
+
private readonly lastClaimError;
|
|
1243
|
+
/**
|
|
1244
|
+
* The lockup outpoints a receive swap's claim callback has already been
|
|
1245
|
+
* handed, by rfqId.
|
|
1246
|
+
*
|
|
1247
|
+
* What this exists to prevent: a claim SUCCEEDS, and for the next few
|
|
1248
|
+
* passes the indexer still lists those outputs as unspent. Without a
|
|
1249
|
+
* record of what was already claimed, every one of those passes would
|
|
1250
|
+
* re-submit the same spend, fail against the server, and report a swap
|
|
1251
|
+
* that in fact worked as failing. With one, a re-claim happens only when
|
|
1252
|
+
* an outpoint appears that was never claimed — a lockup funded piecemeal,
|
|
1253
|
+
* which is legitimate and which `partiallyClaimed` exists for.
|
|
1254
|
+
*
|
|
1255
|
+
* Process-local: after a restart a swap with a live claim tries once more.
|
|
1256
|
+
* That is the recovery case rather than the spam one — a claim that never
|
|
1257
|
+
* landed leaves its outputs unspent, and one that did leaves a single
|
|
1258
|
+
* rejection.
|
|
1259
|
+
*/
|
|
1260
|
+
private readonly claimedOutpoints;
|
|
1261
|
+
/** Live `onContractEvent` subscription, held so `stop()` can drop it. */
|
|
1262
|
+
private unsubscribeContracts;
|
|
1263
|
+
/** Terminal records, kept so a late {@link waitForSwapCompletion} still
|
|
1264
|
+
* answers instead of throwing "not found". Cleared by {@link removeSwap}. */
|
|
1265
|
+
private readonly finished;
|
|
1266
|
+
private readonly waiters;
|
|
1267
|
+
/**
|
|
1268
|
+
* The request-time origin of each swap the manager may have to CREATE a
|
|
1269
|
+
* record for, by rfqId.
|
|
1270
|
+
*
|
|
1271
|
+
* Needed only for a swap the store has never seen: once a record exists,
|
|
1272
|
+
* `updateRfqSwapRecord` carries the origin half through and the map is
|
|
1273
|
+
* redundant. It is kept anyway for the swap's whole life, so a record the
|
|
1274
|
+
* store loses between passes is rewritten rather than lost — and dropped
|
|
1275
|
+
* by {@link removeSwap} and by retention, which are the two places a swap
|
|
1276
|
+
* stops being this manager's business.
|
|
1277
|
+
*/
|
|
1278
|
+
private readonly origins;
|
|
1279
|
+
/** Records changed during the current pass, flushed to the repository and
|
|
1280
|
+
* through `saveSwap`. */
|
|
1281
|
+
private readonly dirty;
|
|
1282
|
+
/** Race guard: one action at a time per swap. */
|
|
1283
|
+
private readonly inProgress;
|
|
1284
|
+
private timer;
|
|
1285
|
+
private running;
|
|
1286
|
+
constructor(deps: RfqSwapManagerDeps, config?: RfqSwapManagerConfig);
|
|
1287
|
+
/** Wire the money-moving half. Without it the manager only watches. The
|
|
1288
|
+
* two claims may be omitted for a consumer that drives no kind reaching
|
|
1289
|
+
* them — see {@link AvailableRfqSwapManagerCallbacks}. */
|
|
1290
|
+
setCallbacks(callbacks: AvailableRfqSwapManagerCallbacks): void;
|
|
1291
|
+
onSwapUpdate(listener: SwapUpdateListener): () => void;
|
|
1292
|
+
onSwapCompleted(listener: SwapCompletedListener): () => void;
|
|
1293
|
+
onSwapFailed(listener: SwapFailedListener): () => void;
|
|
1294
|
+
onActionExecuted(listener: ActionExecutedListener): () => void;
|
|
1295
|
+
/**
|
|
1296
|
+
* Rebuild every stored swap and take over monitoring them.
|
|
1297
|
+
*
|
|
1298
|
+
* The composition a consumer otherwise writes by hand, and the one place
|
|
1299
|
+
* all four pieces meet: retention decides what to keep
|
|
1300
|
+
* (`shouldRetainRfqSwap`), the lockup's contract row supplies the covenant
|
|
1301
|
+
* (`lockupContractParams`), `rebuildRfqSwap` turns a record back into a
|
|
1302
|
+
* live swap, and each rebuilt swap arrives with its own origin, so nothing
|
|
1303
|
+
* is asked of the caller.
|
|
1304
|
+
*
|
|
1305
|
+
* **Deliberately not part of {@link start}.** A consumer that wants to
|
|
1306
|
+
* look at its records — count them, show them, prune and stop — is not
|
|
1307
|
+
* forced to start driving money to do it. Call this first and `start()`
|
|
1308
|
+
* after; a manager already running polls the restored swaps at once.
|
|
1309
|
+
*
|
|
1310
|
+
* Retention runs BEFORE the rebuild, so a record past
|
|
1311
|
+
* `RFQ_SWAP_RETENTION_SECONDS` costs no contract lookup on its way to being
|
|
1312
|
+
* dropped.
|
|
1313
|
+
*
|
|
1314
|
+
* **A record that cannot be rebuilt is reported, never swallowed and never
|
|
1315
|
+
* fatal.** `rebuildRfqSwap` throws by design when the covenant params do
|
|
1316
|
+
* not derive the funded address, and `lockupContractParams` throws
|
|
1317
|
+
* `LockupContractMissing` when the wallet has no row for the lockup — both
|
|
1318
|
+
* say something true about that one record, and neither is a reason to
|
|
1319
|
+
* strand the others. They come back in {@link RfqRestoreResult.failed}.
|
|
1320
|
+
*/
|
|
1321
|
+
restoreFromRepository(options?: RfqRestoreOptions): Promise<RfqRestoreResult>;
|
|
1322
|
+
/**
|
|
1323
|
+
* Drop stored records that are terminal and past
|
|
1324
|
+
* `RFQ_SWAP_RETENTION_SECONDS`, and return their ids.
|
|
1325
|
+
*
|
|
1326
|
+
* `needs_counterparty` is never dropped, however old: the money is still at
|
|
1327
|
+
* the lockup and the counterparty's move still ends the swap. That rule
|
|
1328
|
+
* lives in `shouldRetainRfqSwap`, which this defers to rather than
|
|
1329
|
+
* restating.
|
|
1330
|
+
*
|
|
1331
|
+
* Public because retention is a caller's cadence, not the manager's: a
|
|
1332
|
+
* long-lived process wants it on a timer of its own, a mobile app wants it
|
|
1333
|
+
* at boot. {@link restoreFromRepository} runs it first, so the boot path
|
|
1334
|
+
* needs no separate call.
|
|
1335
|
+
*/
|
|
1336
|
+
pruneRetiredSwaps(): Promise<string[]>;
|
|
1337
|
+
private dropRetired;
|
|
1338
|
+
private requireRepository;
|
|
1339
|
+
/** The default covenant source: the wallet's own contract row for each
|
|
1340
|
+
* lockup, which is where registration put it before the address could be
|
|
1341
|
+
* funded. */
|
|
1342
|
+
private paramsFromContracts;
|
|
1343
|
+
/**
|
|
1344
|
+
* Load records and begin monitoring. Runs one pass immediately — a caller
|
|
1345
|
+
* resuming after a restart may be well past a deadline already — then
|
|
1346
|
+
* every `pollIntervalMs`. Records that are already terminal are kept only
|
|
1347
|
+
* so {@link waitForSwapCompletion} can answer for them.
|
|
1348
|
+
*
|
|
1349
|
+
* Calling it again while running loads the records and returns rather than
|
|
1350
|
+
* re-arming — dropping them silently would strand a funded swap on a
|
|
1351
|
+
* caller's harmless double-start.
|
|
1352
|
+
*
|
|
1353
|
+
* The signature is unchanged with a {@link RfqSwapManagerDeps.repository}
|
|
1354
|
+
* wired; the origins are resolved from the store instead, by the same rule
|
|
1355
|
+
* {@link addSwap} applies. A swap the store has never seen throws
|
|
1356
|
+
* {@link RfqSwapOriginRequired} — hand that one to `addSwap` with its
|
|
1357
|
+
* origin, or restore the whole set with {@link restoreFromRepository},
|
|
1358
|
+
* which needs no caller input at all. Every swap is checked before any is
|
|
1359
|
+
* tracked, so a bad one in the list does not leave a half-loaded manager.
|
|
1360
|
+
*/
|
|
1361
|
+
start(swaps?: readonly RfqSwap[]): Promise<void>;
|
|
1362
|
+
/**
|
|
1363
|
+
* Stop monitoring and clear the timer. In-flight actions are not
|
|
1364
|
+
* cancellable and run to completion; outstanding
|
|
1365
|
+
* {@link waitForSwapCompletion} promises are left pending, since
|
|
1366
|
+
* stop/start is a pause rather than a cancellation.
|
|
1367
|
+
*
|
|
1368
|
+
* The contract subscription is dropped too — an open stream with nothing
|
|
1369
|
+
* reacting to it is a leak, and {@link start} puts it back. What is NOT
|
|
1370
|
+
* undone is the contract registration: those rows are the wallet's, they
|
|
1371
|
+
* outlive this manager's lifecycle, and dropping them would unwatch a
|
|
1372
|
+
* lockup that is still funded.
|
|
1373
|
+
*/
|
|
1374
|
+
stop(): Promise<void>;
|
|
1375
|
+
/**
|
|
1376
|
+
* Begin monitoring a swap. Polled immediately when the manager is running,
|
|
1377
|
+
* so a just-funded swap does not wait out a whole interval.
|
|
1378
|
+
*
|
|
1379
|
+
* `origin` is the request-time half a live swap cannot carry — the corridor,
|
|
1380
|
+
* the funded address, the corridor's profile, the funding txid — and it is
|
|
1381
|
+
* what lets the manager write this swap's FIRST record. Supply it whenever
|
|
1382
|
+
* a {@link RfqSwapManagerDeps.repository} is wired and the swap is new. It
|
|
1383
|
+
* may be omitted for a swap the store already holds a record for, which is
|
|
1384
|
+
* then read to confirm it; omitting it for one the store has never seen
|
|
1385
|
+
* throws {@link RfqSwapOriginRequired} rather than admitting a swap whose
|
|
1386
|
+
* record could never be written. With no repository wired the parameter is
|
|
1387
|
+
* inert.
|
|
1388
|
+
*/
|
|
1389
|
+
addSwap(swap: RfqSwap, origin?: RfqSwapOrigin): Promise<void>;
|
|
1390
|
+
/**
|
|
1391
|
+
* Settle where this swap's record will come from, before it is monitored.
|
|
1392
|
+
*
|
|
1393
|
+
* Three ways it can be answered, in order: the caller passed an origin, one
|
|
1394
|
+
* is already remembered from an earlier `addSwap`, or the store holds a
|
|
1395
|
+
* record — which is the origin, already written. Only the last costs a read,
|
|
1396
|
+
* and only when the first two are absent.
|
|
1397
|
+
*/
|
|
1398
|
+
private admit;
|
|
1399
|
+
/** Forget a swap entirely, monitored or finished.
|
|
1400
|
+
*
|
|
1401
|
+
* Its contract row is left alone: registration is a wallet-level fact about
|
|
1402
|
+
* a script that may still hold money, and this call says only that THIS
|
|
1403
|
+
* manager stops driving the swap. Retiring the row is reserved for a swap
|
|
1404
|
+
* that reached a terminal state, where the lockup is provably done. */
|
|
1405
|
+
removeSwap(rfqId: string): Promise<void>;
|
|
1406
|
+
/** Every swap still being monitored. */
|
|
1407
|
+
getPendingSwaps(): Promise<RfqSwap[]>;
|
|
1408
|
+
hasSwap(rfqId: string): Promise<boolean>;
|
|
1409
|
+
/** True while an action for this swap holds the per-swap lock. */
|
|
1410
|
+
isProcessing(rfqId: string): Promise<boolean>;
|
|
1411
|
+
getStats(): Promise<{
|
|
1412
|
+
isRunning: boolean;
|
|
1413
|
+
monitoredSwaps: number;
|
|
1414
|
+
finishedSwaps: number;
|
|
1415
|
+
inProgress: number;
|
|
1416
|
+
pollIntervalMs: number;
|
|
1417
|
+
}>;
|
|
1418
|
+
/**
|
|
1419
|
+
* Run one monitoring pass over every swap now.
|
|
1420
|
+
*
|
|
1421
|
+
* {@link start} calls this on an interval, but it is public on purpose: a
|
|
1422
|
+
* caller that sleeps its process (a mobile app resuming, a service worker
|
|
1423
|
+
* waking) wants a pass on that event rather than at the next tick. Passes
|
|
1424
|
+
* do not overlap per swap — the in-progress lock makes a concurrent call a
|
|
1425
|
+
* no-op for any swap already being worked on.
|
|
1426
|
+
*/
|
|
1427
|
+
poll(): Promise<void>;
|
|
1428
|
+
/**
|
|
1429
|
+
* Resolve once this swap's PAYOUT is decided — which for onchain-send is
|
|
1430
|
+
* the L1 claim, not the end of the record's life: once `claimTxid` is set
|
|
1431
|
+
* the trader has the coins it swapped for, and what remains is the manager
|
|
1432
|
+
* watching the Arkade lockup close. That holds however the record is
|
|
1433
|
+
* labelled afterwards, `needs_counterparty` included. Lightning-send has no
|
|
1434
|
+
* such split and resolves at `settled`/`refunded`, and so does lightning
|
|
1435
|
+
* receive — see {@link isPayoutDecided} for why its own claim txid does not
|
|
1436
|
+
* decide it.
|
|
1437
|
+
*
|
|
1438
|
+
* Rejects only on `failed`. `refunded` resolves: on a send leg a refund is
|
|
1439
|
+
* an outcome the caller asked this manager to drive, not an exception. On a
|
|
1440
|
+
* receive leg it is the swap being lost, which is still an answer and not
|
|
1441
|
+
* an error — read `state`, do not infer success from resolution.
|
|
1442
|
+
*/
|
|
1443
|
+
waitForSwapCompletion(rfqId: string): Promise<RfqSwapOutcome>;
|
|
1444
|
+
private track;
|
|
1445
|
+
/** Drops the swap from BOTH indexes. The event index is the one that stops
|
|
1446
|
+
* a late event finding a swap that is gone; `pollSwap`'s own
|
|
1447
|
+
* `monitored` check would also catch it, and deliberately still does —
|
|
1448
|
+
* either alone is sufficient, which is what keeps a future change to one of
|
|
1449
|
+
* them from silently re-driving a cancelled swap. */
|
|
1450
|
+
private untrack;
|
|
1451
|
+
/**
|
|
1452
|
+
* Turn the indexer's push into an extra reason to run a pass — and nothing
|
|
1453
|
+
* more.
|
|
1454
|
+
*
|
|
1455
|
+
* **This is deliberately not a source of truth.** An event names a script;
|
|
1456
|
+
* the reaction is to run the ordinary pass for the swap at that script, and
|
|
1457
|
+
* that pass re-reads the lockup through {@link readLockupFate} exactly as
|
|
1458
|
+
* the timer's pass does. So an event that is missed, duplicated, reordered
|
|
1459
|
+
* or outright FORGED can only cost or save latency — it can never change
|
|
1460
|
+
* what this manager believes about a swap, and it can never on its own
|
|
1461
|
+
* cause a claim or a refund. That property is what makes it safe to bolt a
|
|
1462
|
+
* best-effort stream onto a money path, and it must survive any future
|
|
1463
|
+
* change here: the moment an event is BELIEVED rather than merely acted on,
|
|
1464
|
+
* a relay outage becomes a correctness problem instead of a latency one.
|
|
1465
|
+
*
|
|
1466
|
+
* The timer stays armed regardless, and is the failsafe. Every deadline
|
|
1467
|
+
* that moves money — `refundLocktime`, the L1 claim window — is an absolute
|
|
1468
|
+
* timelock that passes whether or not a single event ever arrives.
|
|
1469
|
+
*/
|
|
1470
|
+
private subscribe;
|
|
1471
|
+
/**
|
|
1472
|
+
* Register this swap's lockup with the wallet's contract manager, once.
|
|
1473
|
+
*
|
|
1474
|
+
* The backstop, not the primary site: `requestLightningSend` /
|
|
1475
|
+
* `requestOnchainSend` register before the caller can fund, so this covers
|
|
1476
|
+
* swaps whose records predate that — and costs nothing when it does not,
|
|
1477
|
+
* since `createContract` is first-writer-wins.
|
|
1478
|
+
*
|
|
1479
|
+
* Best-effort by design: a failure here is reported and retried on the next
|
|
1480
|
+
* pass, and never aborts the pass it is part of. Registration buys latency
|
|
1481
|
+
* and puts the lockup in the wallet's contract set; it decides nothing. The
|
|
1482
|
+
* money path below it reads the indexer directly and is gated on timelocks
|
|
1483
|
+
* that a missing contract row has no bearing on, so failing the pass over
|
|
1484
|
+
* this would trade a real deadline for a bookkeeping one.
|
|
1485
|
+
*/
|
|
1486
|
+
private ensureRegistered;
|
|
1487
|
+
/** Stop watching a finished swap's lockup. Retained, not deleted: the row
|
|
1488
|
+
* is what keeps the lockup's own VTXOs annotatable and its history
|
|
1489
|
+
* readable, while `retained` is what drops it from the subscription and
|
|
1490
|
+
* the poll — a settled swap that stayed watched would cost the wallet a
|
|
1491
|
+
* script for its whole life. Best-effort — the swap is over either way. */
|
|
1492
|
+
private retireContract;
|
|
1493
|
+
private arm;
|
|
1494
|
+
private pollSwap;
|
|
1495
|
+
private runPass;
|
|
1496
|
+
/**
|
|
1497
|
+
* The receive leg's whole state machine: claim the solver-funded lockup
|
|
1498
|
+
* while the window is open, and recognise the shapes in which it can be
|
|
1499
|
+
* lost.
|
|
1500
|
+
*
|
|
1501
|
+
* **The window closes at `refundLocktime`, on wall clock, with no margin.**
|
|
1502
|
+
* Both halves of that are deliberate. It closes there because publishing
|
|
1503
|
+
* `P` into the solver's live refund window risks losing the race and
|
|
1504
|
+
* handing over the preimage anyway — the hazard `ONCHAIN_CLAIM_MARGIN_SECONDS`
|
|
1505
|
+
* guards on the L1 side. It takes no margin because the two situations are
|
|
1506
|
+
* not alike: that one budgets for confirmation depth, while this claim is an
|
|
1507
|
+
* offchain spend that lands in seconds. Wall clock is already the
|
|
1508
|
+
* conservative reading — the solver's leaf is a CLTV, which matures against
|
|
1509
|
+
* median-time-past, and MTP trails wall clock — so the real window extends
|
|
1510
|
+
* PAST this deadline rather than ending before it. Every second of margin
|
|
1511
|
+
* subtracted here is a second of live claim window given away for nothing.
|
|
1512
|
+
*
|
|
1513
|
+
* **The trader has no move after it.** Nothing here can take the lockup
|
|
1514
|
+
* back, so once the window shuts the swap is the solver's to resolve and
|
|
1515
|
+
* this manager's job is to watch it happen and then stop.
|
|
1516
|
+
*/
|
|
1517
|
+
private driveReceiveClaim;
|
|
1518
|
+
/**
|
|
1519
|
+
* Claim what the solver funded, once it is enough.
|
|
1520
|
+
*
|
|
1521
|
+
* The value gate here decides WHEN to act. `pushClaim`'s decides whether
|
|
1522
|
+
* `P` is published, and runs with nothing between it and the signature —
|
|
1523
|
+
* the check that matters is the inner one, and this is not a reason to
|
|
1524
|
+
* relax it.
|
|
1525
|
+
*/
|
|
1526
|
+
private claimIfFunded;
|
|
1527
|
+
/** `handled` ends the pass; `continue` falls through to the refund gate. */
|
|
1528
|
+
private driveOnchain;
|
|
1529
|
+
private driveArkadeRefund;
|
|
1530
|
+
/** Whether any of these outputs has never been handed to the claim
|
|
1531
|
+
* callback — the only reason to claim a lockup a second time. */
|
|
1532
|
+
private hasUnclaimedOutpoint;
|
|
1533
|
+
private rememberClaimed;
|
|
1534
|
+
/**
|
|
1535
|
+
* L1 progress, which past the refund window must not overwrite a refusal.
|
|
1536
|
+
* The two halves are independent — a claimed fill says nothing about
|
|
1537
|
+
* whether this wallet can take the Arkade lockup back — and `claimed` is
|
|
1538
|
+
* re-asserted from chain on every pass, so without this a blocked swap
|
|
1539
|
+
* would flip between the two states forever. The claim itself always runs;
|
|
1540
|
+
* only the label defers, and only once the refund is the live half.
|
|
1541
|
+
*/
|
|
1542
|
+
private setOnchainState;
|
|
1543
|
+
/** The probe's refusal reason, or `undefined` when a local refund is
|
|
1544
|
+
* possible as far as anyone here can tell. A probe that throws is treated
|
|
1545
|
+
* as a refusal: a capability check that cannot answer is not a yes. */
|
|
1546
|
+
private probeRefusal;
|
|
1547
|
+
/** Report that no local refund will happen, without ending the swap. */
|
|
1548
|
+
private block;
|
|
1549
|
+
/** The way back out, taken as soon as the swap becomes actionable again.
|
|
1550
|
+
* Back to what the record can prove, not to `pending` unconditionally: a
|
|
1551
|
+
* swap that already made its claim has a txid for it, and reporting that
|
|
1552
|
+
* swap as `pending` would un-say something true. */
|
|
1553
|
+
private unblock;
|
|
1554
|
+
/**
|
|
1555
|
+
* Record which ark transactions ended the lockup.
|
|
1556
|
+
*
|
|
1557
|
+
* Only the ones the indexer actually named: `LockupSpend.arkTxid` is
|
|
1558
|
+
* optional, and a checkpoint txid is not what history correlates on — a
|
|
1559
|
+
* record carrying one would name a transaction the wallet's own activity
|
|
1560
|
+
* never shows. Fewer txids is the right failure here.
|
|
1561
|
+
*
|
|
1562
|
+
* Assigned rather than merged: the fate is one read of the whole lockup,
|
|
1563
|
+
* so it is the complete answer for this swap, and a swap only reaches a
|
|
1564
|
+
* verdict once.
|
|
1565
|
+
*/
|
|
1566
|
+
private stampLockupSpends;
|
|
1567
|
+
private touch;
|
|
1568
|
+
private setState;
|
|
1569
|
+
/** Terminal failure. The `onSwapFailed` emission is left to
|
|
1570
|
+
* {@link finalize}, so this does not double-report. */
|
|
1571
|
+
private fail;
|
|
1572
|
+
private emitFailed;
|
|
1573
|
+
private emitAction;
|
|
1574
|
+
/**
|
|
1575
|
+
* Flush a changed record to every sink that is wired, and say whether all
|
|
1576
|
+
* of them took it.
|
|
1577
|
+
*
|
|
1578
|
+
* Up to two writes, in this order: the canonical `RfqSwapRecord` to
|
|
1579
|
+
* {@link RfqSwapManagerDeps.repository}, then
|
|
1580
|
+
* {@link RfqSwapManagerCallbacks.saveSwap}. Both gate — a rejection from
|
|
1581
|
+
* either leaves the record dirty and monitored, so waiters stay unsettled
|
|
1582
|
+
* and a terminal swap is not finalized until the write it claims lands.
|
|
1583
|
+
* That is exactly today's rule for `saveSwap`, applied to whichever sinks
|
|
1584
|
+
* exist; wiring the repository does not weaken it.
|
|
1585
|
+
*
|
|
1586
|
+
* The canonical write goes FIRST and a failure there skips the second.
|
|
1587
|
+
* `saveSwap` is a projection of the record, and projecting a state the
|
|
1588
|
+
* record of record has just refused would leave the secondary sink ahead
|
|
1589
|
+
* of the primary — the one ordering that survives no restart.
|
|
1590
|
+
*
|
|
1591
|
+
* With neither wired the state is process-local, which is what a manager
|
|
1592
|
+
* with no callbacks has always done.
|
|
1593
|
+
*/
|
|
1594
|
+
private save;
|
|
1595
|
+
/** The canonical write. True when there is no repository to write to. */
|
|
1596
|
+
private saveRecord;
|
|
1597
|
+
private originOrThrow;
|
|
1598
|
+
/**
|
|
1599
|
+
* Drop a terminal swap from monitoring and report it exactly once.
|
|
1600
|
+
*
|
|
1601
|
+
* `onSwapCompleted` and `onSwapFailed` are mutually exclusive here, unlike
|
|
1602
|
+
* Boltz's manager, which fires completion for every swap that leaves
|
|
1603
|
+
* monitoring including the failed ones — a listener named "completed" that
|
|
1604
|
+
* also fires on failure is a trap worth not inheriting.
|
|
1605
|
+
*/
|
|
1606
|
+
private finalize;
|
|
1607
|
+
private settleWaiters;
|
|
1608
|
+
}
|
|
1609
|
+
/** What {@link RfqSwapManager.waitForSwapCompletion} reports. `txid` is the
|
|
1610
|
+
* trader's own claim — L1 for a claimed onchain send, Arkade for a claimed
|
|
1611
|
+
* receive — or the ark txid for a refund the trader pushed; a solver-side
|
|
1612
|
+
* settlement or refund carries none, and a receive swap that ended `refunded`
|
|
1613
|
+
* carries none either, however far its claim got (see {@link outcomeOf}). So
|
|
1614
|
+
* a `txid` here always names something that happened, and `state` remains the
|
|
1615
|
+
* only thing to read for whether the swap paid out. */
|
|
1616
|
+
interface RfqSwapOutcome {
|
|
1617
|
+
state: RfqSwapState;
|
|
1618
|
+
txid?: string;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* The swap kinds this projection covers — all three the manager monitors.
|
|
1623
|
+
*
|
|
1624
|
+
* `onchain_send` carries an L1 half nothing else can rebuild. Its Arkade lockup
|
|
1625
|
+
* has a contract row like the others, but the HTLC is Bitcoin L1, not an Arkade
|
|
1626
|
+
* contract, so no row exists for it; and `OnchainHtlc` exposes only derived
|
|
1627
|
+
* values — `address`, `pkScript`, `leaves`, `controlBlocks` — never the
|
|
1628
|
+
* `claimKey`/`refundKey` `onchainHtlcScript` takes as inputs. So they ride in
|
|
1629
|
+
* that corridor's {@link RfqSwapOrigin.profile}, and without them a restored
|
|
1630
|
+
* swap would let its L1 refund window pass unwatched.
|
|
1631
|
+
*/
|
|
1632
|
+
type PersistableRfqSwap = LightningSendSwap | LightningReceiveSwap | OnchainSendSwap;
|
|
1633
|
+
/**
|
|
1634
|
+
* The serialized covenant parameters a rebuild is given.
|
|
1635
|
+
*
|
|
1636
|
+
* `VHTLCV2ContractHandler`'s own wire shape — what `serializeParams` writes and
|
|
1637
|
+
* `createScript` reads — which is exactly what a lockup's contract row stores
|
|
1638
|
+
* under `params`.
|
|
1639
|
+
*/
|
|
1640
|
+
type LockupParams = Record<string, string>;
|
|
1641
|
+
/**
|
|
1642
|
+
* How long a retired swap's record is kept, in SECONDS.
|
|
1643
|
+
*
|
|
1644
|
+
* Terminal records are history, not garbage: the covenant's spender is a
|
|
1645
|
+
* transaction the wallet never signed, so its own history cannot reconstruct
|
|
1646
|
+
* them. Kept for a month, then dropped so a hot wallet's store stays bounded.
|
|
1647
|
+
*
|
|
1648
|
+
* Seconds, not milliseconds, because that is the unit `RfqSwap.updatedAt`
|
|
1649
|
+
* carries; the manager stamps it from `RfqSwapManagerConfig.now`, which is
|
|
1650
|
+
* "wall clock, in unix seconds". Comparing it against `Date.now()` would drop
|
|
1651
|
+
* every terminal record after ~43 minutes.
|
|
1652
|
+
*/
|
|
1653
|
+
declare const RFQ_SWAP_RETENTION_SECONDS: number;
|
|
1654
|
+
/** The immutable request-time half, and only what EVERY corridor has. Hex for
|
|
1655
|
+
* everything binary, so the record is plain JSON and survives any
|
|
1656
|
+
* structured-clone backend unchanged. */
|
|
1657
|
+
interface RfqSwapOrigin {
|
|
1658
|
+
/**
|
|
1659
|
+
* Which corridor this is. Resolves the handler that owns {@link profile};
|
|
1660
|
+
* see `rfqCorridor.ts`.
|
|
1661
|
+
*
|
|
1662
|
+
* The manager's own union, not an open string: `RfqSwapManager` branches on
|
|
1663
|
+
* `kind` to decide what to drive, so a corridor it does not know could be
|
|
1664
|
+
* persisted and rebuilt here and then never driven — which is the failure
|
|
1665
|
+
* this whole file is arranged to make impossible.
|
|
1666
|
+
*/
|
|
1667
|
+
kind: PersistableRfqSwap["kind"];
|
|
1668
|
+
/**
|
|
1669
|
+
* The Arkade address that was funded.
|
|
1670
|
+
*
|
|
1671
|
+
* Both the swap's handle on its covenant — {@link lockupContractParams}
|
|
1672
|
+
* looks the contract row up by the script it decodes to — and, being taken
|
|
1673
|
+
* from the entry point rather than re-derived, the check that the
|
|
1674
|
+
* parameters a caller supplies belong to THIS swap.
|
|
1675
|
+
*/
|
|
1676
|
+
lockupAddress: string;
|
|
1677
|
+
/**
|
|
1678
|
+
* The corridor's own half, as plain JSON — written by the caller from the
|
|
1679
|
+
* request result, kept current by the handler's `project`.
|
|
1680
|
+
*
|
|
1681
|
+
* Opaque here on purpose. Nothing in this file, the repository or the
|
|
1682
|
+
* IndexedDB store interprets it, which is what lets a new corridor ship
|
|
1683
|
+
* without touching any of them. It carries the corridor's keys as well as
|
|
1684
|
+
* its state: `signer` (which wallet key signs this leg) and, on a corridor
|
|
1685
|
+
* locked to a preimage, `hashlock` — see `rfqProfileParts.ts`, and write
|
|
1686
|
+
* both with `rfqSecretsProfile` rather than by hand.
|
|
1687
|
+
*
|
|
1688
|
+
* Not a consumer scratchpad: every write merges it as `{ ...profile,
|
|
1689
|
+
* ...handler.project(swap) }`, so a consumer key colliding with one the
|
|
1690
|
+
* handler projects is silently overwritten on every pass.
|
|
1691
|
+
*/
|
|
1692
|
+
profile: Record<string, unknown>;
|
|
1693
|
+
/** Consumer display metadata. The rebuild ignores it — `RfqSwapCommon`
|
|
1694
|
+
* carries no amount of its own. */
|
|
1695
|
+
amount?: number;
|
|
1696
|
+
/**
|
|
1697
|
+
* The ark transaction that funded {@link lockupAddress}.
|
|
1698
|
+
*
|
|
1699
|
+
* Origin, not manager state: the caller broadcasts the funding and knows
|
|
1700
|
+
* its txid, while the manager watches the lockup by script and never
|
|
1701
|
+
* learns it. So it is written once at record creation, like {@link amount},
|
|
1702
|
+
* and no corridor `project` emits it.
|
|
1703
|
+
*/
|
|
1704
|
+
fundingArkTxid?: string;
|
|
1705
|
+
}
|
|
1706
|
+
/** The stored record: the origin plus the manager's mutable state. */
|
|
1707
|
+
interface RfqSwapRecord extends RfqSwapOrigin {
|
|
1708
|
+
rfqId: string;
|
|
1709
|
+
state: RfqSwapState;
|
|
1710
|
+
createdAt: number;
|
|
1711
|
+
updatedAt: number;
|
|
1712
|
+
refundArkTxid?: string;
|
|
1713
|
+
/** The ark transactions that spent the lockup, stamped by the manager from
|
|
1714
|
+
* the chain read that ended the swap. See
|
|
1715
|
+
* `RfqSwapCommon.lockupSpendArkTxids`. */
|
|
1716
|
+
lockupSpendArkTxids?: string[];
|
|
1717
|
+
failure?: string;
|
|
1718
|
+
blockedReason?: string;
|
|
1719
|
+
}
|
|
1720
|
+
/** First write, at the moment the caller hands the swap to the manager. */
|
|
1721
|
+
declare function createRfqSwapRecord(origin: RfqSwapOrigin, swap: PersistableRfqSwap): RfqSwapRecord;
|
|
1722
|
+
/**
|
|
1723
|
+
* Every later write. The origin half is carried through untouched.
|
|
1724
|
+
*
|
|
1725
|
+
* The mutable half is REPLACED, not merged. `managerState` omits a key the live
|
|
1726
|
+
* swap no longer carries, so spreading it over the old record could only ever
|
|
1727
|
+
* set these fields, never clear them. The manager clears them on purpose: it
|
|
1728
|
+
* deletes `blockedReason` when a swap leaves `needs_counterparty`, precisely
|
|
1729
|
+
* because a stale `blockedReason` reads as a live refusal.
|
|
1730
|
+
*/
|
|
1731
|
+
declare function updateRfqSwapRecord(record: RfqSwapRecord, swap: PersistableRfqSwap): RfqSwapRecord;
|
|
1732
|
+
/**
|
|
1733
|
+
* The immutable half of a stored record, on its own.
|
|
1734
|
+
*
|
|
1735
|
+
* A record IS an origin plus manager state, so `record` where an
|
|
1736
|
+
* {@link RfqSwapOrigin} is wanted type-checks — and is a bug. Spread into
|
|
1737
|
+
* {@link createRfqSwapRecord} it carries the OLD state's `failure`,
|
|
1738
|
+
* `blockedReason` and `refundArkTxid` past `managerState`, which omits a field
|
|
1739
|
+
* the live swap no longer has and therefore cannot clear one. That is the same
|
|
1740
|
+
* trap {@link updateRfqSwapRecord} strips those three fields to avoid; this is
|
|
1741
|
+
* how a caller holding only a record gets an origin that is safe to keep.
|
|
1742
|
+
*
|
|
1743
|
+
* What `RfqSwapManager.restoreFromRepository` remembers for each record it
|
|
1744
|
+
* rebuilds, so a later write can create the record again if the store lost it.
|
|
1745
|
+
*/
|
|
1746
|
+
declare function rfqSwapOriginOf(record: RfqSwapRecord): RfqSwapOrigin;
|
|
1747
|
+
/**
|
|
1748
|
+
* Rebuild the live record. Pure, and synchronous, given the covenant's
|
|
1749
|
+
* parameters.
|
|
1750
|
+
*
|
|
1751
|
+
* Hand the result to {@link RfqSwapManager.start}. Take `params` from the
|
|
1752
|
+
* lockup's contract row — {@link lockupContractParams} is the one-liner — or
|
|
1753
|
+
* from a copy of `VHTLCV2ContractHandler.serializeParams(script.options)` a
|
|
1754
|
+
* consumer keeps itself; either way they are checked against the address that
|
|
1755
|
+
* was actually funded, so the `lockupPkScript` this produces is the one the
|
|
1756
|
+
* funded lockup is keyed by.
|
|
1757
|
+
*/
|
|
1758
|
+
declare function rebuildRfqSwap(record: RfqSwapRecord, params: LockupParams): PersistableRfqSwap;
|
|
1759
|
+
/**
|
|
1760
|
+
* Whether a retired swap's record should be kept.
|
|
1761
|
+
*
|
|
1762
|
+
* `needs_counterparty` is deliberately not terminal and is never dropped: the
|
|
1763
|
+
* money is still at the lockup, the counterparty's move is still what ends the
|
|
1764
|
+
* swap, and the refusal is re-checked every pass; restoring the right wallet
|
|
1765
|
+
* returns it to `pending`.
|
|
1766
|
+
*
|
|
1767
|
+
* @param now Current time in **unix seconds**; the same unit as
|
|
1768
|
+
* `RfqSwap.updatedAt`, which the manager stamps from
|
|
1769
|
+
* `RfqSwapManagerConfig.now`. Pass `Math.floor(Date.now() / 1000)`, never
|
|
1770
|
+
* `Date.now()`: milliseconds against a seconds window would retire every
|
|
1771
|
+
* terminal record after ~43 minutes.
|
|
1772
|
+
*/
|
|
1773
|
+
declare function shouldRetainRfqSwap(record: RfqSwapRecord, now: number): boolean;
|
|
1774
|
+
|
|
1775
|
+
/** A registry discovery result held for reuse. Refetchable — unlike a swap
|
|
1776
|
+
* record, losing it costs one network round trip — but it must survive a cold
|
|
1777
|
+
* boot: serving it stale is what keeps quoting alive while a registry is down. */
|
|
1778
|
+
interface MarketsCacheEntry {
|
|
1779
|
+
markets: DiscoveredMarket[];
|
|
1780
|
+
fetchedAt: number;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Everything the package persists, following the monorepo repository
|
|
1784
|
+
* convention (versioned interface, AsyncDisposable, one backend per
|
|
1785
|
+
* platform — see the Boltz plugin's SwapRepository). Consumers construct
|
|
1786
|
+
* exactly one of these; there is no second storage seam.
|
|
1787
|
+
*
|
|
1788
|
+
* Durable records (swaps) and rebuildable state (the restore scan's txid
|
|
1789
|
+
* cursor, the markets cache) live side by side because they share a
|
|
1790
|
+
* lifetime: all three belong to one wallet on one device, and a consumer
|
|
1791
|
+
* that wipes one wants all three gone.
|
|
1792
|
+
*
|
|
1793
|
+
* ponytail: no query filters — every consumer reads all swaps and filters
|
|
1794
|
+
* in memory; mirror the Boltz plugin's GetSwapsFilter when a consumer needs
|
|
1795
|
+
* subset queries.
|
|
1796
|
+
*/
|
|
1797
|
+
interface AssetSwapRepository extends AsyncDisposable {
|
|
1798
|
+
/** 4 adds `getRfqSwap`. 3 added the other RFQ methods below; 2 was the
|
|
1799
|
+
* released shape — swaps, scan cursor, markets, with `preimageSaltHex` on
|
|
1800
|
+
* the swap record — so an implementor built against either cannot satisfy
|
|
1801
|
+
* this one silently. */
|
|
1802
|
+
readonly version: 4;
|
|
1803
|
+
/** Insert or replace a swap by id. Store the record whole: `preimageHex`
|
|
1804
|
+
* and `preimageSaltHex` both leave the swap unclaimable if a field-mapped
|
|
1805
|
+
* backend drops them — the first is the only claim secret of a swap whose
|
|
1806
|
+
* signer cannot derive, the second the public input every other static
|
|
1807
|
+
* wallet's preimage derives from.
|
|
1808
|
+
*
|
|
1809
|
+
* Records must be **JSON-safe**: the SQLite and Realm backends serialize
|
|
1810
|
+
* the record to JSON, so a `Date` in a consumer-added field comes back a
|
|
1811
|
+
* string, a `Set`/`Map` comes back empty, and a `bigint` throws here —
|
|
1812
|
+
* none of which happens on IndexedDB's structured clone. `AssetSwap` as
|
|
1813
|
+
* declared is JSON-safe; keep added fields that way. */
|
|
1814
|
+
saveSwap(swap: AssetSwap): Promise<void>;
|
|
1815
|
+
/** All stored swaps, in no particular order — `getAssetSwaps` is the
|
|
1816
|
+
* canonical newest-first read. */
|
|
1817
|
+
getAllSwaps(): Promise<AssetSwap[]>;
|
|
1818
|
+
/**
|
|
1819
|
+
* Insert or replace a monitored RFQ swap by `rfqId`.
|
|
1820
|
+
*
|
|
1821
|
+
* Store the record WHOLE. Every field is a covenant tree parameter or the
|
|
1822
|
+
* manager's own state, and a field-mapped backend that drops one round-trips
|
|
1823
|
+
* a record whose covenant `rebuildRfqSwap` cannot reproduce — which surfaces
|
|
1824
|
+
* as a refund that cannot be signed, long after the write.
|
|
1825
|
+
*/
|
|
1826
|
+
saveRfqSwap(record: RfqSwapRecord): Promise<void>;
|
|
1827
|
+
/** One record by key. `undefined` on a miss — retention prunes terminal
|
|
1828
|
+
* records, so absence is ordinary and not an error. */
|
|
1829
|
+
getRfqSwap(rfqId: string): Promise<RfqSwapRecord | undefined>;
|
|
1830
|
+
/** Every stored RFQ swap record, in no particular order. */
|
|
1831
|
+
getAllRfqSwaps(): Promise<RfqSwapRecord[]>;
|
|
1832
|
+
/** Drop one, once it is past retention — see `shouldRetainRfqSwap`. */
|
|
1833
|
+
removeRfqSwap(rfqId: string): Promise<void>;
|
|
1834
|
+
/** Sent txids already checked for offer packets (see restore.ts). */
|
|
1835
|
+
getScannedTxids(): Promise<Set<string>>;
|
|
1836
|
+
markTxidsScanned(txids: Iterable<string>): Promise<void>;
|
|
1837
|
+
/** Cached registry markets, or undefined on a miss. */
|
|
1838
|
+
getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
|
|
1839
|
+
saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
|
|
1840
|
+
clear(): Promise<void>;
|
|
1841
|
+
}
|
|
1842
|
+
declare class InMemoryAssetSwapRepository implements AssetSwapRepository {
|
|
1843
|
+
readonly version: 4;
|
|
1844
|
+
private readonly swaps;
|
|
1845
|
+
private readonly rfqSwaps;
|
|
1846
|
+
private readonly scanned;
|
|
1847
|
+
private readonly markets;
|
|
1848
|
+
saveSwap(swap: AssetSwap): Promise<void>;
|
|
1849
|
+
getAllSwaps(): Promise<AssetSwap[]>;
|
|
1850
|
+
saveRfqSwap(record: RfqSwapRecord): Promise<void>;
|
|
1851
|
+
getRfqSwap(rfqId: string): Promise<RfqSwapRecord | undefined>;
|
|
1852
|
+
getAllRfqSwaps(): Promise<RfqSwapRecord[]>;
|
|
1853
|
+
removeRfqSwap(rfqId: string): Promise<void>;
|
|
1854
|
+
getScannedTxids(): Promise<Set<string>>;
|
|
1855
|
+
markTxidsScanned(txids: Iterable<string>): Promise<void>;
|
|
1856
|
+
getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
|
|
1857
|
+
saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
|
|
1858
|
+
clear(): Promise<void>;
|
|
1859
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
export { isRfqSwapTerminal as $, type AssetSwapRepository as A, BTC_ASSET_ID as B, type RfqSwapActionName as C, type RfqSwapLockup as D, RfqSwapManager as E, type RfqSwapManagerCallbacks as F, type RfqSwapManagerConfig as G, type RfqSwapManagerDeps as H, InMemoryAssetSwapRepository as I, type RfqSwapManagerEvents as J, type RfqSwapOrigin as K, type LockupVtxo as L, type MarketsCacheEntry as M, RfqSwapOriginRequired as N, type OnchainSendAction as O, type PersistableRfqSwap as P, type RfqSwapOutcome as Q, type RfqSwapRecord as R, type SwapSecretsProjection as S, type RfqSwapRecordStore as T, type SwapContractRegistry as U, addAssetSwap as V, awaitRfqResolution as W, createRfqSwapRecord as X, findLockupVtxos as Y, getAssetSwaps as Z, getAssetSwapsOrThrow as _, type AssetSwap as a, isRfqTerminal as a0, nextOnchainAction as a1, preimageForSwapRecord as a2, pushRefundWithoutReceiver as a3, readLockupFate as a4, rebuildRfqSwap as a5, refundIfUnresolved as a6, rfqSwapOriginOf as a7, shouldRetainRfqSwap as a8, swapSecretsToRecord as a9, updateAssetSwap as aa, updateAssetSwapBestEffort as ab, updateRfqSwapRecord as ac, type RefundArkProvider as b, type RefundIndexer as c, type RfqSwap as d, type ArkadeRefundResult as e, type LockupSpendIndexer as f, type RfqSwapState as g, type AssetSwapStatus as h, type AvailableRfqSwapManagerCallbacks as i, type LightningReceiveSwap as j, type LightningSendSwap as k, type LockupFate as l, LockupNeedsRecoveryError as m, type LockupParams as n, type LockupSpend as o, type OnchainSendSwap as p, type PreimageBlockedReason as q, PreimageNotRecoverableError as r, REFUND_MTP_LAG_SECONDS as s, RFQ_RESOLVED_STATES as t, RFQ_SWAP_RETENTION_SECONDS as u, RFQ_SWAP_TERMINAL_STATES as v, type RefundOutcome as w, type RfqRestoreFailure as x, type RfqRestoreOptions as y, type RfqRestoreResult as z };
|