@arkade-os/swap 0.0.6 → 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 +274 -13
- package/dist/{chunk-WGRU2DBF.js → chunk-6ZUS47GA.js} +15 -1
- package/dist/{chunk-Q4FAYBXS.js → chunk-TU4NGZDP.js} +108 -37
- package/dist/index.cjs +1205 -449
- package/dist/index.d.cts +292 -1139
- package/dist/index.d.ts +292 -1139
- package/dist/index.js +761 -87
- package/dist/nostr.cjs +15 -9
- package/dist/nostr.d.cts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +4 -11
- 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-3jWha5xA.d.cts → rfq-C-aq5LDJ.d.cts} +82 -9
- package/dist/{rfq-3jWha5xA.d.ts → rfq-C-aq5LDJ.d.ts} +82 -9
- package/package.json +2 -2
- package/dist/repository-BwnZ8N62.d.cts +0 -236
- package/dist/repository-BwnZ8N62.d.ts +0 -236
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,186 @@
|
|
|
1
|
-
import { asset, IWallet, arkade, RestIndexerProvider, Transaction, IContractManager,
|
|
2
|
-
import { A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry } from './repository-
|
|
3
|
-
export {
|
|
1
|
+
import { ProvisionedKey, ProvisionedClaimSecret, asset, IWallet, arkade, RestIndexerProvider, Transaction, IContractManager, VHTLC, Identity, ActivityResolver } from '@arkade-os/sdk';
|
|
2
|
+
import { S as SwapSecretsProjection, R as RfqSwapRecord, A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry, b as RefundArkProvider, c as RefundIndexer, L as LockupVtxo, d as RfqSwap, e as ArkadeRefundResult, f as LockupSpendIndexer, g as RfqSwapState } from './repository-BcZ9LXRP.js';
|
|
3
|
+
export { h as AssetSwapStatus, i as AvailableRfqSwapManagerCallbacks, B as BTC_ASSET_ID, I as InMemoryAssetSwapRepository, j as LightningReceiveSwap, k as LightningSendSwap, l as LockupFate, m as LockupNeedsRecoveryError, n as LockupParams, o as LockupSpend, O as OnchainSendAction, p as OnchainSendSwap, P as PersistableRfqSwap, q as PreimageBlockedReason, r as PreimageNotRecoverableError, s as REFUND_MTP_LAG_SECONDS, t as RFQ_RESOLVED_STATES, u as RFQ_SWAP_RETENTION_SECONDS, v as RFQ_SWAP_TERMINAL_STATES, w as RefundOutcome, x as RfqRestoreFailure, y as RfqRestoreOptions, z as RfqRestoreResult, C as RfqSwapActionName, D as RfqSwapLockup, E as RfqSwapManager, F as RfqSwapManagerCallbacks, G as RfqSwapManagerConfig, H as RfqSwapManagerDeps, J as RfqSwapManagerEvents, K as RfqSwapOrigin, N as RfqSwapOriginRequired, Q as RfqSwapOutcome, T as RfqSwapRecordStore, U as SwapContractRegistry, V as addAssetSwap, W as awaitRfqResolution, X as createRfqSwapRecord, Y as findLockupVtxos, Z as getAssetSwaps, _ as getAssetSwapsOrThrow, $ as isRfqSwapTerminal, a0 as isRfqTerminal, a1 as nextOnchainAction, a2 as preimageForSwapRecord, a3 as pushRefundWithoutReceiver, a4 as readLockupFate, a5 as rebuildRfqSwap, a6 as refundIfUnresolved, a7 as rfqSwapOriginOf, a8 as shouldRetainRfqSwap, a9 as swapSecretsToRecord, aa as updateAssetSwap, ab as updateAssetSwapBestEffort, ac as updateRfqSwapRecord } from './repository-BcZ9LXRP.js';
|
|
4
4
|
import { Network, LocalCardInput, DiscoveredMarket, Side, OfferPlan } from '@arkade-os/solver-discovery';
|
|
5
|
-
import {
|
|
6
|
-
export { A as ARKADE_ASSET,
|
|
5
|
+
import { O as OnchainNetwork, a as OnchainHtlc, b as OnchainHtlcParams } from './rfq-C-aq5LDJ.js';
|
|
6
|
+
export { A as ARKADE_ASSET, c as ARKADE_BTC, d as AddressMismatch, C as ChainSource, e as ChainUtxo, H as HtlcUtxo, I as InvoiceFacts, L as LIGHTNING_BTC, f as LIGHTNING_RECEIVE_PAIR, g as LIGHTNING_SEND_PAIR, h as LightningReceiveTreeParams, i as LightningSendTreeParams, M as MAX_MIN_CONFIRMATIONS, j as MIN_CLAIM_WINDOW_SECONDS, k as MIN_HEADROOM_SECONDS, l as ONCHAIN_BTC, m as ONCHAIN_CLAIM_MARGIN_SECONDS, n as ONCHAIN_DUST_SATS, o as ONCHAIN_ORDER_MARGIN_SECONDS, p as ONCHAIN_RECEIVE_PAIR, q as ONCHAIN_SECONDS_PER_BLOCK, r as ONCHAIN_SEND_PAIR, s as OnchainHtlcPhase, R as RFQ_TERMINAL_STATES, t as RelaySocket, u as RfqQuote, v as RfqRefusalReason, w as RfqStatus, x as RfqTransport, S as SOLO_REFUND_HEADROOM_SECONDS, y as SwapRefusal, z as arkadeAssetLeg, B as arkadeSwapRequest, D as assertFundable, E as assertReceivable, F as awaitOnchainFill, G as buildHtlcClaim, J as buildHtlcRefund, K as claimOnchainFill, N as classifyOnchainHtlc, P as deriveLightningReceive, Q as deriveOnchainReceive, T as deriveOnchainSend, U as extractPreimage, V as httpTransport, W as lightningReceiveRequest, X as lightningSendRequest, Y as lightningSendVtxoScript, Z as newPreimage, _ as newRfqId, $ as offerTermsFromQuote, a0 as onchainHtlcScript, a1 as onchainReceiveRequest, a2 as onchainSendRequest, a3 as paymentHashOf, a4 as receiveVtxoScript, a5 as relayTransport, a6 as requestLightningReceive, a7 as requestLightningSend, a8 as requestOnchainReceive, a9 as requestOnchainSend, aa as rfqPair, ab as unilateralClaimDelay, ac as unilateralRefundDelay, ad as unilateralRefundWithoutReceiverDelay, ae as verifyLockupAddress, af as verifyReceiveInvoice } from './rfq-C-aq5LDJ.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Which wallet key signs this leg. Stored at `profile.signer`.
|
|
10
|
+
*
|
|
11
|
+
* Independent of any hashlock: it feeds `senderIdentityForSwapRecord` on the
|
|
12
|
+
* refund side, and a corridor that settles with no preimage at all still needs
|
|
13
|
+
* it. A corridor whose leg this wallet never signs omits the key entirely — as
|
|
14
|
+
* with everything else here, absent rather than blank.
|
|
15
|
+
*/
|
|
16
|
+
interface RfqSignerProjection {
|
|
17
|
+
/** Public. The wallet re-derives the signer from it; no key material is at
|
|
18
|
+
* rest. */
|
|
19
|
+
signingDescriptor: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* What a leg locked to a preimage records about the LOCK. Stored at
|
|
23
|
+
* `profile.hashlock`, and absent entirely from a corridor that has none.
|
|
24
|
+
*
|
|
25
|
+
* `paymentHash` is identity, not capability — `lightning_send` carries one and
|
|
26
|
+
* can never open it. The preimage fields are the capability, and only a corridor
|
|
27
|
+
* where WE claim ever writes them: `provisionClaimSecret` produces that arm,
|
|
28
|
+
* `provisionRefundKey` does not.
|
|
29
|
+
*/
|
|
30
|
+
type RfqHashlockProjection = Omit<SwapSecretsProjection, "signingDescriptor"> & {
|
|
31
|
+
/** `sha256(P)`, hex. Not recoverable from the covenant, which binds
|
|
32
|
+
* `hash160(P)`. */
|
|
33
|
+
paymentHash: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The composed view `preimageForSwapRecord` reads — the signer's descriptor plus
|
|
37
|
+
* the hashlock's. **Never a stored shape**: nothing writes this object,
|
|
38
|
+
* {@link rfqClaimSecretOf} assembles it from the two keys above, which is what
|
|
39
|
+
* keeps the descriptor stored once.
|
|
40
|
+
*
|
|
41
|
+
* Structurally `SwapSecretsProjection & { paymentHash }`, which is exactly that
|
|
42
|
+
* helper's parameter.
|
|
43
|
+
*/
|
|
44
|
+
type RfqClaimSecretProjection = RfqSignerProjection & RfqHashlockProjection;
|
|
45
|
+
/**
|
|
46
|
+
* The corridor-owned counterpart of `onchainSendProfile`: the one supported way
|
|
47
|
+
* to turn a provisioned secret into stored profile keys.
|
|
48
|
+
*
|
|
49
|
+
* Writes what the provisioning result actually has. Omit `paymentHash` and you
|
|
50
|
+
* get `signer` alone — which is what a non-hashlock corridor calls, and why it
|
|
51
|
+
* never has to reach for `hashlock`.
|
|
52
|
+
*/
|
|
53
|
+
declare const rfqSecretsProfile: (secrets: ProvisionedKey | ProvisionedClaimSecret, paymentHash?: string) => {
|
|
54
|
+
signer: RfqSignerProjection;
|
|
55
|
+
hashlock?: RfqHashlockProjection;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The stored signer projection, for `senderIdentityForSwapRecord`.
|
|
59
|
+
*
|
|
60
|
+
* `undefined` ONLY when the profile carries no `signer` key — a corridor whose
|
|
61
|
+
* leg this wallet does not sign. A `signer` that is present and unusable throws:
|
|
62
|
+
* "this corridor has no local signer" and "this record came back corrupt" are
|
|
63
|
+
* different answers, and `senderIdentityForSwapRecord` turns the first into a
|
|
64
|
+
* permanent `RefundNotLocallyPossibleError("no-secrets")` the manager acts on.
|
|
65
|
+
* Handing it a silently-emptied projection would report "no local refund is
|
|
66
|
+
* possible" for a storage bug.
|
|
67
|
+
*/
|
|
68
|
+
declare const rfqSignerOf: (record: RfqSwapRecord) => RfqSignerProjection | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The claim inputs, or `undefined` when this record's corridor cannot produce P
|
|
71
|
+
* — no hashlock, or a leg we refund rather than claim.
|
|
72
|
+
*
|
|
73
|
+
* Answered by the corridor's handler (`claimSecret`), not by a kind list here:
|
|
74
|
+
* whether a leg claims is the corridor's fact.
|
|
75
|
+
*
|
|
76
|
+
* When the handler DOES claim, this validates and throws
|
|
77
|
+
* `PreimageNotRecoverableError("malformed-record")` rather than returning a
|
|
78
|
+
* partial projection — which `preimageForSwapRecord` would claim with, its hash
|
|
79
|
+
* check being conditional on the very field that went missing.
|
|
80
|
+
*/
|
|
81
|
+
declare const rfqClaimSecretOf: (record: RfqSwapRecord) => RfqClaimSecretProjection | undefined;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* `arkade:BTC->lightning:BTC`. Nothing beyond its keys and the covenant: the
|
|
85
|
+
* solver claims the lockup, the trader's only move is the refund, and the rest
|
|
86
|
+
* of the leg is fully described by the contract row.
|
|
87
|
+
*
|
|
88
|
+
* The one leg with a hashlock it can never open — P belongs to the payee — so
|
|
89
|
+
* `hashlock` here is `{ paymentHash }` alone and `signer` holds a REFUND key.
|
|
90
|
+
*/
|
|
91
|
+
interface LightningSendProfile extends Record<string, unknown> {
|
|
92
|
+
signer: RfqSignerProjection;
|
|
93
|
+
hashlock: RfqHashlockProjection;
|
|
94
|
+
}
|
|
95
|
+
/** `lightning:BTC->arkade:BTC`. */
|
|
96
|
+
interface LightningReceiveProfile extends Record<string, unknown> {
|
|
97
|
+
signer: RfqSignerProjection;
|
|
98
|
+
hashlock: RfqHashlockProjection;
|
|
99
|
+
/** The quote's `to_amount`, captured at REQUEST time. */
|
|
100
|
+
expectedAmount: number;
|
|
101
|
+
/** Where the claim pays. */
|
|
102
|
+
payoutAddress: string;
|
|
103
|
+
/**
|
|
104
|
+
* Our Arkade claim's txid, once submitted.
|
|
105
|
+
*
|
|
106
|
+
* Here rather than on the record's common half, which is for fields all
|
|
107
|
+
* three legs carry: this one is the receive leg's alone, the counterpart of
|
|
108
|
+
* the onchain leg's `claimTxid`. Restoring without it re-arms the value gate
|
|
109
|
+
* against a lockup we have already partly claimed — `claimIfFunded` reads
|
|
110
|
+
* `partiallyClaimed` off exactly this — so the remainder is refused over a
|
|
111
|
+
* preimage that is already public, and a swap that did claim is relabelled
|
|
112
|
+
* `needs_counterparty` once its window shuts.
|
|
113
|
+
*/
|
|
114
|
+
claimArkTxid?: string;
|
|
115
|
+
}
|
|
116
|
+
/** `arkade:BTC->onchain:BTC`. */
|
|
117
|
+
interface OnchainSendProfile extends Record<string, unknown> {
|
|
118
|
+
signer: RfqSignerProjection;
|
|
119
|
+
hashlock: RfqHashlockProjection;
|
|
120
|
+
/** The trader's L1 claim key. */
|
|
121
|
+
claimKey: string;
|
|
122
|
+
/** The solver's L1 key, from `profile.htlc_pubkey`. */
|
|
123
|
+
refundKey: string;
|
|
124
|
+
/** `profile.htlc_locktime` — the trader's L1 recourse deadline, distinct
|
|
125
|
+
* from the arkade lockup's `refundLocktime`. */
|
|
126
|
+
htlcLocktime: number;
|
|
127
|
+
network: OnchainNetwork;
|
|
128
|
+
/**
|
|
129
|
+
* The L1 address the fill was expected at — `htlc.address` from
|
|
130
|
+
* `requestOnchainSend`, which `deriveOnchainSend` has already checked
|
|
131
|
+
* against the quote's own `profile.htlc_address`.
|
|
132
|
+
*
|
|
133
|
+
* The counterpart of the record's `lockupAddress`, and here for the same
|
|
134
|
+
* reason: the inputs above and this address reach the record by independent
|
|
135
|
+
* routes, so requiring the rebuild to reproduce it is what catches a
|
|
136
|
+
* parameter that came back wrong. The arkade lockup gets that check from
|
|
137
|
+
* `rebuildRfqSwap`; this leg, the one whose parameters have no second source
|
|
138
|
+
* anywhere, would otherwise get none — and a swapped or corrupted key
|
|
139
|
+
* derives another perfectly valid HTLC, at an address nobody funded.
|
|
140
|
+
*/
|
|
141
|
+
htlcAddress: string;
|
|
142
|
+
/** `profile.min_confirmations`; gates when the fill is claimable. */
|
|
143
|
+
minConfirmations: number;
|
|
144
|
+
/** The fill's outpoint, learned on first sighting. Without it a SPENT htlc
|
|
145
|
+
* reads as never funded — see `classifyOnchainHtlc`. */
|
|
146
|
+
funding?: {
|
|
147
|
+
txid: string;
|
|
148
|
+
vout: number;
|
|
149
|
+
};
|
|
150
|
+
/** Our own L1 claim, so a restart does not re-broadcast it. */
|
|
151
|
+
claimTxid?: string;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Build the profile from what `requestOnchainSend` returned.
|
|
155
|
+
*
|
|
156
|
+
* Pass the whole result: every field this needs is on it, so the mapping is
|
|
157
|
+
* done here, once, instead of at each call site.
|
|
158
|
+
*
|
|
159
|
+
* That mapping is the reason this exists. `htlcParams.refundLocktime` becomes
|
|
160
|
+
* `htlcLocktime` — the same value under a different name, because the record
|
|
161
|
+
* already has a `refundLocktime` and it is the arkade lockup's, a different
|
|
162
|
+
* deadline entirely. The keys go from bytes to hex. `htlcAddress` is not an
|
|
163
|
+
* input to anything, it is the derived value the rebuild checks the inputs
|
|
164
|
+
* against, so writing it is easy to skip and impossible to reconstruct later.
|
|
165
|
+
* A caller copying fields across by hand gets all three right or restores a
|
|
166
|
+
* swap that watches nothing.
|
|
167
|
+
*
|
|
168
|
+
* The other two corridors have no such builder, deliberately: their profiles
|
|
169
|
+
* are the request result's own fields under their own names, with nothing
|
|
170
|
+
* derived and nothing renamed.
|
|
171
|
+
*
|
|
172
|
+
* The L1 half ONLY. `signer` and `hashlock` come from `rfqSecretsProfile`, which
|
|
173
|
+
* every corridor calls — folding them in here would give this leg a one-call
|
|
174
|
+
* mapper the other two cannot have, and the uniform rule ("`rfqSecretsProfile`
|
|
175
|
+
* first, then whatever the corridor adds") is what keeps the per-corridor
|
|
176
|
+
* instructions short enough to follow.
|
|
177
|
+
*/
|
|
178
|
+
declare function onchainSendProfile(result: {
|
|
179
|
+
htlc: Pick<OnchainHtlc, "address">;
|
|
180
|
+
htlcParams: OnchainHtlcParams;
|
|
181
|
+
l1Network: OnchainNetwork;
|
|
182
|
+
minConfirmations: number;
|
|
183
|
+
}): Omit<OnchainSendProfile, "signer" | "hashlock">;
|
|
7
184
|
|
|
8
185
|
/** The contracts — pure data, shared verbatim with any other implementation. */
|
|
9
186
|
declare const swapPrograms: Record<"wantAsset" | "wantBtc", ReturnType<typeof arkade.parseArtifact>>;
|
|
@@ -219,18 +396,21 @@ declare const validatePlan: (plan: OfferPlan, giveBalance: bigint, dust: bigint)
|
|
|
219
396
|
/** Browser backend over the SDK's shared IndexedDB manager — the same
|
|
220
397
|
* infrastructure the wallet already uses for its Boltz swap repository. */
|
|
221
398
|
declare class IndexedDbAssetSwapRepository implements AssetSwapRepository {
|
|
222
|
-
|
|
223
|
-
readonly
|
|
224
|
-
private dbPromise;
|
|
399
|
+
readonly version: 4;
|
|
400
|
+
private readonly connection;
|
|
225
401
|
constructor(dbName?: string);
|
|
226
402
|
private ensureDb;
|
|
227
403
|
private readStore;
|
|
228
404
|
/** Every write in one place, so none of them can forget to await the
|
|
229
405
|
* commit. Requests need no individual await: a failed one aborts the
|
|
230
|
-
* transaction, which `
|
|
406
|
+
* transaction, which `awaitTransaction` reports. */
|
|
231
407
|
private write;
|
|
232
408
|
saveSwap(swap: AssetSwap): Promise<void>;
|
|
233
409
|
getAllSwaps(): Promise<AssetSwap[]>;
|
|
410
|
+
saveRfqSwap(record: RfqSwapRecord): Promise<void>;
|
|
411
|
+
getRfqSwap(rfqId: string): Promise<RfqSwapRecord | undefined>;
|
|
412
|
+
getAllRfqSwaps(): Promise<RfqSwapRecord[]>;
|
|
413
|
+
removeRfqSwap(rfqId: string): Promise<void>;
|
|
234
414
|
getScannedTxids(): Promise<Set<string>>;
|
|
235
415
|
markTxidsScanned(txids: Iterable<string>): Promise<void>;
|
|
236
416
|
getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
|
|
@@ -464,386 +644,6 @@ interface ClaimPacketInput {
|
|
|
464
644
|
*/
|
|
465
645
|
declare function sealClaimPacket(input: ClaimPacketInput): Promise<SealedClaimPacket>;
|
|
466
646
|
|
|
467
|
-
/** True for the states after which the solver will report nothing further. */
|
|
468
|
-
declare const isRfqTerminal: (state: string) => boolean;
|
|
469
|
-
/**
|
|
470
|
-
* The terminal states that mean the swap is OVER and the lockup is already
|
|
471
|
-
* gone — the solver either claimed it (`settled`, revealing the preimage) or
|
|
472
|
-
* returned it (`refunded`). A trader seeing one of these has nothing left to
|
|
473
|
-
* do.
|
|
474
|
-
*
|
|
475
|
-
* Deliberately narrower than {@link RFQ_TERMINAL_STATES}: `refused`, `expired`
|
|
476
|
-
* and `stuck` are terminal for the NEGOTIATION but say nothing about whether
|
|
477
|
-
* the trader's sats are still sitting at the lockup. A trader that funded just
|
|
478
|
-
* as the quote expired, or whose solver wedged mid-payment, is exactly the
|
|
479
|
-
* trader who needs the refund most — so those states must not short-circuit
|
|
480
|
-
* it. {@link refundIfUnresolved} treats them as "keep going", and lets the
|
|
481
|
-
* on-chain VTXO lookup be the authority on whether anything is actually there.
|
|
482
|
-
*/
|
|
483
|
-
declare const RFQ_RESOLVED_STATES: readonly ["settled", "refunded"];
|
|
484
|
-
/**
|
|
485
|
-
* Poll a swap's status until it reaches a terminal state.
|
|
486
|
-
*
|
|
487
|
-
* Same shape and conventions as {@link awaitOnchainFill}: a `pollMs` interval,
|
|
488
|
-
* an optional unix-seconds `deadline`, and a thrown error carrying a stable
|
|
489
|
-
* `reason` when that deadline passes.
|
|
490
|
-
*
|
|
491
|
-
* A `null` status (the solver has no record of this `rfq_id`) is treated as
|
|
492
|
-
* "not yet", not as an answer — a status route can legitimately 404 for a
|
|
493
|
-
* moment after a quote is issued. The deadline is what bounds that wait.
|
|
494
|
-
*
|
|
495
|
-
* Transport errors are NOT swallowed; a failing `status()` call rejects this
|
|
496
|
-
* function. Callers polling across a long refund window should expect to
|
|
497
|
-
* restart it after a network blip — nothing is lost by doing so, since the
|
|
498
|
-
* refund path this feeds is gated on an absolute timelock that does not
|
|
499
|
-
* expire.
|
|
500
|
-
*/
|
|
501
|
-
declare function awaitRfqResolution(transport: RfqTransport, rfqId: string, options?: {
|
|
502
|
-
pollMs?: number;
|
|
503
|
-
deadline?: number;
|
|
504
|
-
}): Promise<RfqStatus>;
|
|
505
|
-
/** The Ark surface the refund push needs — narrower than a full provider, and
|
|
506
|
-
* satisfied by {@link RestArkProvider}. Same seam style as `RestoreIndexer`. */
|
|
507
|
-
type RefundArkProvider = Pick<RestArkProvider, "getInfo" | "submitTx" | "finalizeTx">;
|
|
508
|
-
/** The indexer surface the lockup lookup needs. */
|
|
509
|
-
type RefundIndexer = Pick<RestIndexerProvider, "getVtxos">;
|
|
510
|
-
/** A still-refundable virtual output sitting at the swap lockup. */
|
|
511
|
-
interface LockupVtxo {
|
|
512
|
-
txid: string;
|
|
513
|
-
vout: number;
|
|
514
|
-
value: number;
|
|
515
|
-
/**
|
|
516
|
-
* The batch this output lived in expired and the operator swept it, so it
|
|
517
|
-
* is no longer a live leaf — it can be RECOVERED, but not spent offchain.
|
|
518
|
-
*
|
|
519
|
-
* It is still the trader's money and it is still visible, which is why
|
|
520
|
-
* {@link findLockupVtxos} returns it. What it is not is refundable by
|
|
521
|
-
* {@link pushRefundWithoutReceiver}: that builds an offchain Ark
|
|
522
|
-
* transaction, and the SDK's own predicates make the two states mutually
|
|
523
|
-
* exclusive — `canSpendOffchain` is false exactly when `canRecoverOnchain`
|
|
524
|
-
* is true (`wallet/vtxo.ts`), and the latter is documented as "must be
|
|
525
|
-
* recovered into a fresh batch rather than spent offchain". Holding the
|
|
526
|
-
* trader's `sender` key does not change that; a sweep removes the leaf from
|
|
527
|
-
* the live tree, not the signature from the trader.
|
|
528
|
-
*
|
|
529
|
-
* `packages/boltz-swap` splits on exactly this fact rather than working
|
|
530
|
-
* around it: `settleRefundWithoutReceiver` sends a live VTXO through an
|
|
531
|
-
* offchain tx and a recoverable one through `joinBatch` — "a swept
|
|
532
|
-
* (recoverable) VTXO is no longer a live leaf, so it can only be reclaimed
|
|
533
|
-
* by re-registering it into a batch".
|
|
534
|
-
*
|
|
535
|
-
* So the remedy is recovery (renewing the output into a fresh batch),
|
|
536
|
-
* after which the ordinary CLTV refund works again. This package does not
|
|
537
|
-
* build that round — see {@link pushRefundWithoutReceiver}, which refuses
|
|
538
|
-
* rather than submitting a spend that cannot succeed.
|
|
539
|
-
*/
|
|
540
|
-
recoverable: boolean;
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* Thrown when a refund was asked for over outputs that have been swept.
|
|
544
|
-
*
|
|
545
|
-
* Carries the outpoints so a caller can act — recover exactly those, then
|
|
546
|
-
* retry — instead of reading a server rejection and guessing. `reason` follows
|
|
547
|
-
* the same convention as `awaitOnchainFill`'s `fill_timeout` and
|
|
548
|
-
* `claimOnchainFill`'s `claim_window_closed`.
|
|
549
|
-
*
|
|
550
|
-
* **The remedy already exists; this package does not reimplement it.** The SDK
|
|
551
|
-
* recovers swept outputs by re-registering them into a fresh batch, through
|
|
552
|
-
* `IVtxoManager.recoverVtxos()` — the same batch round `packages/boltz-swap`
|
|
553
|
-
* reaches via its own `joinBatch`. It reads the wallet's registered-contract
|
|
554
|
-
* snapshot (`recoverVtxos` → `wallet.getVtxos({ withRecoverable: true })` →
|
|
555
|
-
* `contractSnapshot()` → `contractManager.getContractsWithVtxos()`), so it
|
|
556
|
-
* covers a swap lockup as soon as that lockup is registered as a contract —
|
|
557
|
-
* which is what {@link RfqSwapManagerDeps.contracts} does. Registration is
|
|
558
|
-
* therefore not only a latency optimization; it is what turns a swept lockup
|
|
559
|
-
* from a dead end into something the ordinary wallet path can recover.
|
|
560
|
-
*
|
|
561
|
-
* Two caveats a caller must hold, neither enforceable from here:
|
|
562
|
-
*
|
|
563
|
-
* - **The wallet must hold the lockup's `sender` key**, because recovery
|
|
564
|
-
* settles through `refundWithoutReceiver` — the leaf `vhtlc-v2` annotates
|
|
565
|
-
* these VTXOs with.
|
|
566
|
-
* - **`refundLocktime` must have matured.** That leaf carries a CLTV, so a
|
|
567
|
-
* recovery round including this VTXO earlier is rejected. `recoverVtxos`
|
|
568
|
-
* sweeps every recoverable output in ONE settlement and has no CLTV
|
|
569
|
-
* awareness, so recovering early can fail the whole batch rather than just
|
|
570
|
-
* this output. `packages/boltz-swap` encodes the same rule as "pre-CLTV
|
|
571
|
-
* recoverable → skipped".
|
|
572
|
-
*/
|
|
573
|
-
declare class LockupNeedsRecoveryError extends Error {
|
|
574
|
-
readonly name = "LockupNeedsRecoveryError";
|
|
575
|
-
readonly reason = "needs_recovery";
|
|
576
|
-
/** `txid:vout` for each output that must be recovered first. */
|
|
577
|
-
readonly outpoints: string[];
|
|
578
|
-
/**
|
|
579
|
-
* The contract's `refundLocktime`. Recovering before this matures is the
|
|
580
|
-
* hazard described above: `recoverVtxos()` sweeps EVERY recoverable output
|
|
581
|
-
* into one settlement with no CLTV awareness, so an early attempt can fail
|
|
582
|
-
* the whole batch — including unrelated outputs that were otherwise fine.
|
|
583
|
-
*
|
|
584
|
-
* Exposed as a value, not only inside the message, so a caller can encode
|
|
585
|
-
* `packages/boltz-swap`'s "pre-CLTV recoverable → skipped" rule without
|
|
586
|
-
* parsing prose. Seconds-based locktimes mature against the chain tip's
|
|
587
|
-
* timestamp rather than wall clock, so treat this as a floor to wait past,
|
|
588
|
-
* not an exact alarm.
|
|
589
|
-
*/
|
|
590
|
-
readonly recoverableAfter: bigint;
|
|
591
|
-
constructor(outpoints: string[], recoverableAfter: bigint);
|
|
592
|
-
}
|
|
593
|
-
/**
|
|
594
|
-
* Every output still sitting at the lockup script — spendable AND
|
|
595
|
-
* swept-but-recoverable, each tagged with which it is.
|
|
596
|
-
*
|
|
597
|
-
* All of them, not the first: a trader may fund a lockup in more than one
|
|
598
|
-
* send, and refunding only `vtxos[0]` returns part of the money and strands
|
|
599
|
-
* the rest at a script whose other refund paths are all longer.
|
|
600
|
-
*
|
|
601
|
-
* BOTH queries, because they are disjoint sets and `spendableOnly` alone goes
|
|
602
|
-
* blind at exactly the wrong moment. A lockup whose batch expiry passed is
|
|
603
|
-
* swept into the recoverable set, and this function exists to serve swaps that
|
|
604
|
-
* sat unresolved — which are precisely the ones most likely to have got there.
|
|
605
|
-
* Reading only the spendable set would report `nothing_to_refund` over money
|
|
606
|
-
* that is still sitting at the script, which is worse than an error: it looks
|
|
607
|
-
* like a resolved swap. `packages/boltz-swap` merges the same two queries for
|
|
608
|
-
* the same reason (`arkade-swaps.ts`'s `refundableVtxos`).
|
|
609
|
-
*
|
|
610
|
-
* **Visible is not the same as refundable.** A `recoverable` output cannot be
|
|
611
|
-
* spent offchain at all — see {@link LockupVtxo.recoverable} — so this set is
|
|
612
|
-
* "what is there", not "what {@link pushRefundWithoutReceiver} can take back".
|
|
613
|
-
* That function refuses the recoverable ones by name rather than submitting a
|
|
614
|
-
* spend the server must reject.
|
|
615
|
-
*
|
|
616
|
-
* This read — not the RFQ's reported state — is the authority on whether
|
|
617
|
-
* there is anything left at the lockup.
|
|
618
|
-
*
|
|
619
|
-
* **Not replaced by the contract manager's VTXO state, deliberately.** Once a
|
|
620
|
-
* lockup is registered (see `RfqSwapManagerDeps.contracts`) the wallet tracks
|
|
621
|
-
* these same outputs, and `getContractsWithVtxos` plus `canSpendOffchain` /
|
|
622
|
-
* `canRecoverOnchain` would classify them. That is a WEAKER answer here on two
|
|
623
|
-
* counts: it serves the wallet REPOSITORY, which a degraded sync will happily
|
|
624
|
-
* hand back stale (`getSyncState()` reports `degraded` and returns cached rows
|
|
625
|
-
* rather than failing), and its height-based expiry test needs a chain tip this
|
|
626
|
-
* module does not have. The two queries below ask the indexer itself and need
|
|
627
|
-
* neither. Ask-the-indexer, don't-trust-local-state — the same posture
|
|
628
|
-
* {@link readLockupFate} takes, and for the same reason: this decides money.
|
|
629
|
-
*/
|
|
630
|
-
declare function findLockupVtxos(indexer: RefundIndexer, swapPkScript: Uint8Array): Promise<LockupVtxo[]>;
|
|
631
|
-
/**
|
|
632
|
-
* The indexer surface the lockup-spend read needs: the vtxo lookup, plus the
|
|
633
|
-
* raw transactions those vtxos were spent by. Same narrow-seam style as
|
|
634
|
-
* {@link RefundIndexer} and `restore.ts`'s `RestoreIndexer`, and satisfied by
|
|
635
|
-
* {@link RestIndexerProvider}.
|
|
636
|
-
*/
|
|
637
|
-
type LockupSpendIndexer = Pick<RestIndexerProvider, "getVtxos" | "getVirtualTxs">;
|
|
638
|
-
/**
|
|
639
|
-
* What chain data says became of a swap lockup — the whole answer, with no
|
|
640
|
-
* solver involvement and nothing taken on the solver's word.
|
|
641
|
-
*/
|
|
642
|
-
type LockupFate =
|
|
643
|
-
/** At least one output at the lockup is still unspent. Not over. */
|
|
644
|
-
{
|
|
645
|
-
fate: "open";
|
|
646
|
-
}
|
|
647
|
-
/** Spent by a witness carrying a preimage that HASHES to the quote's
|
|
648
|
-
* `payment_hash`. Only the claim leaf can reveal one, and the only
|
|
649
|
-
* legitimate way the solver obtains it is by completing its side. */
|
|
650
|
-
| {
|
|
651
|
-
fate: "claimed";
|
|
652
|
-
preimage: Uint8Array;
|
|
653
|
-
}
|
|
654
|
-
/** Fully spent, and nothing that spent it revealed a matching preimage —
|
|
655
|
-
* so the money went back to the trader. See {@link readLockupFate}. */
|
|
656
|
-
| {
|
|
657
|
-
fate: "returned";
|
|
658
|
-
}
|
|
659
|
-
/** Nothing was learned: no outputs visible, an output spent by nothing the
|
|
660
|
-
* indexer names, a spend it could not produce, or a blob that would not
|
|
661
|
-
* decode. Never an answer. */
|
|
662
|
-
| {
|
|
663
|
-
fate: "unknown";
|
|
664
|
-
};
|
|
665
|
-
/**
|
|
666
|
-
* Decide from chain data alone whether a swap lockup settled, came back, or is
|
|
667
|
-
* still live.
|
|
668
|
-
*
|
|
669
|
-
* **Why this is decidable without asking anyone.** The lockup's claim leaf can
|
|
670
|
-
* only be spent by revealing `P`, so a spend witness carrying a value that
|
|
671
|
-
* hashes to the quote's `payment_hash` is proof the claim leaf was used — and
|
|
672
|
-
* the only legitimate way the counterparty obtains `P` is by completing its
|
|
673
|
-
* side of the swap. Every OTHER leaf is a refund: `nonInteractiveRefund` is
|
|
674
|
-
* covenant-pinned to the trader's own address (`enforcePayTo(senderPkScript)`),
|
|
675
|
-
* and `refund`, `refundWithoutReceiver`, `unilateralRefund` and
|
|
676
|
-
* `unilateralRefundWithoutReceiver` all require the trader's own signature. So
|
|
677
|
-
* "spent, but not by a hash-verified claim" means the money went back to the
|
|
678
|
-
* trader, and nothing here has to trust a counterparty to say so.
|
|
679
|
-
*
|
|
680
|
-
* **A matching witness SHAPE is not proof.** Only a candidate that hashes to
|
|
681
|
-
* `paymentHash` may be read as a claim; a 32-byte item that hashes to anything
|
|
682
|
-
* else is just bytes, and is treated as a refund. Getting this wrong in the
|
|
683
|
-
* permissive direction would report "settled" for a swap that actually
|
|
684
|
-
* refunded, which is precisely the fact a trader is relying on.
|
|
685
|
-
*
|
|
686
|
-
* **`unknown` is not `returned`.** An empty vtxo set (indexer lag, or a lockup
|
|
687
|
-
* not visible yet), a `spentBy` the indexer cannot produce a transaction for,
|
|
688
|
-
* or a blob that will not decode all come back as `unknown`. `getVirtualTxs`
|
|
689
|
-
* may legitimately return fewer transactions than were asked for, so the
|
|
690
|
-
* observed set is counted rather than assumed complete. The caller's correct
|
|
691
|
-
* response to `unknown` is the same as to `open`: keep watching, and let the
|
|
692
|
-
* refund timelock — which no outage can move — be what ends the wait.
|
|
693
|
-
*
|
|
694
|
-
* Ask-the-indexer, don't-trust-local-state: read fresh on every poll, never
|
|
695
|
-
* cached, the same posture {@link findLockupVtxos} already establishes.
|
|
696
|
-
*/
|
|
697
|
-
declare function readLockupFate(indexer: LockupSpendIndexer, input: {
|
|
698
|
-
swapPkScript: Uint8Array;
|
|
699
|
-
/** `sha256(P)`, hex — the quote's `payment_hash`. */
|
|
700
|
-
paymentHash: string;
|
|
701
|
-
}): Promise<LockupFate>;
|
|
702
|
-
/**
|
|
703
|
-
* Build, sign, and push the `refundWithoutReceiver` spend: return every funded
|
|
704
|
-
* output at the lockup to the trader's refund address.
|
|
705
|
-
*
|
|
706
|
-
* The leaf is `CLTV(refundLocktime) + <sender> + <server>` — the trader's own
|
|
707
|
-
* VHTLC `sender` key and the Arkade server, and NOBODY else. In particular the
|
|
708
|
-
* emulator is not involved: it co-signs only the two covenant leaves
|
|
709
|
-
* (`nonInteractiveClaim` / `nonInteractiveRefund`), which is why the solver's
|
|
710
|
-
* own escape hatch has to go through it and this one does not. So unlike that
|
|
711
|
-
* push, this transaction is submitted SIGNED, and the only counterparty is the
|
|
712
|
-
* Arkade server doing what it does for any collaborative spend.
|
|
713
|
-
*
|
|
714
|
-
* One aggregate output, not one per input — again unlike the solver's covenant
|
|
715
|
-
* refund, which needs index-aligned outputs because its ArkadeScript inspects
|
|
716
|
-
* the output at the current input's index. This leaf carries no covenant, so a
|
|
717
|
-
* single output paying the whole balance is both valid and cheaper.
|
|
718
|
-
*
|
|
719
|
-
* `refundPkScript` defaults to the destination the contract itself commits to
|
|
720
|
-
* (`nonInteractiveRefund`'s `senderPkScript`, i.e. the address the trader gave
|
|
721
|
-
* at quote time), so the ordinary call cannot send the refund somewhere the
|
|
722
|
-
* trader did not intend. It is overridable because this leaf, having no
|
|
723
|
-
* covenant, genuinely does permit any destination.
|
|
724
|
-
*
|
|
725
|
-
* **Consensus, not wall clock, decides when this is spendable.** A seconds
|
|
726
|
-
* locktime matures against median-time-past, which trails real time by roughly
|
|
727
|
-
* an hour, so a push issued the moment `refundLocktime` passes can be rejected
|
|
728
|
-
* until enough blocks land. That is expected, not a failure — see
|
|
729
|
-
* {@link refundIfUnresolved}, which retries.
|
|
730
|
-
*
|
|
731
|
-
* **Swept outputs are refused, not attempted.** This is an OFFCHAIN spend, and
|
|
732
|
-
* a swept output is no longer a live leaf: `canSpendOffchain` and
|
|
733
|
-
* `canRecoverOnchain` are mutually exclusive by construction, so a recoverable
|
|
734
|
-
* input cannot be spent this way whatever key signs it (see
|
|
735
|
-
* {@link LockupVtxo.recoverable}). Because every input lands in ONE aggregate
|
|
736
|
-
* transaction, a single swept output would take the live ones down with it —
|
|
737
|
-
* so the whole push is refused with {@link LockupNeedsRecoveryError} naming the
|
|
738
|
-
* outpoints, rather than submitted and rejected. Filtering them out silently
|
|
739
|
-
* would be worse still: it would report success over money that never moved.
|
|
740
|
-
*/
|
|
741
|
-
declare function pushRefundWithoutReceiver(ark: RefundArkProvider, input: {
|
|
742
|
-
script: InstanceType<typeof VHTLC.ScriptV2>;
|
|
743
|
-
/** The `sender` signer. Build it from the swap record with
|
|
744
|
-
* {@link senderIdentityForSwapRecord} — on an HD wallet that resolves
|
|
745
|
-
* from the seed, with no stored key bytes anywhere, and every way the
|
|
746
|
-
* wallet can fail to produce it arrives as one typed
|
|
747
|
-
* {@link RefundNotLocallyPossibleError} the manager reads as permanent
|
|
748
|
-
* rather than retrying for the rest of the refund window. */
|
|
749
|
-
sender: Identity;
|
|
750
|
-
vtxos: readonly LockupVtxo[];
|
|
751
|
-
/** Defaults to the contract's own committed refund destination. */
|
|
752
|
-
refundPkScript?: Uint8Array;
|
|
753
|
-
}): Promise<{
|
|
754
|
-
arkTxid: string;
|
|
755
|
-
amount: number;
|
|
756
|
-
}>;
|
|
757
|
-
/**
|
|
758
|
-
* How long past `refundLocktime` to keep retrying the push before giving up
|
|
759
|
-
* and surfacing the server's refusal.
|
|
760
|
-
*
|
|
761
|
-
* Two hours because the CLTV matures against median-time-past (BIP-113), which
|
|
762
|
-
* lags wall clock by about an hour, plus room for a slow block. This is the
|
|
763
|
-
* mirror of `MIN_HEADROOM_SECONDS`, which refuses to FUND without 90 minutes
|
|
764
|
-
* of the same margin.
|
|
765
|
-
*/
|
|
766
|
-
declare const REFUND_MTP_LAG_SECONDS: number;
|
|
767
|
-
type RefundOutcome =
|
|
768
|
-
/** The solver resolved it — claimed (`settled`) or returned it (`refunded`). */
|
|
769
|
-
{
|
|
770
|
-
outcome: "resolved";
|
|
771
|
-
status: RfqStatus;
|
|
772
|
-
}
|
|
773
|
-
/** The trader took it back via `refundWithoutReceiver`. */
|
|
774
|
-
| {
|
|
775
|
-
outcome: "refunded";
|
|
776
|
-
arkTxid: string;
|
|
777
|
-
amount: number;
|
|
778
|
-
status: RfqStatus | null;
|
|
779
|
-
}
|
|
780
|
-
/** The refund window opened but the lockup holds nothing to return. */
|
|
781
|
-
| {
|
|
782
|
-
outcome: "nothing_to_refund";
|
|
783
|
-
status: RfqStatus | null;
|
|
784
|
-
}
|
|
785
|
-
/**
|
|
786
|
-
* The money is still at the lockup, but its batch was swept, so no offchain
|
|
787
|
-
* spend can take it back until it is recovered into a fresh batch. Returned
|
|
788
|
-
* rather than retried: unlike a median-time-past refusal, no amount of
|
|
789
|
-
* waiting fixes this — see {@link LockupNeedsRecoveryError}. Recover the
|
|
790
|
-
* named outpoints, then call this again.
|
|
791
|
-
*/
|
|
792
|
-
| {
|
|
793
|
-
outcome: "needs_recovery";
|
|
794
|
-
outpoints: string[];
|
|
795
|
-
vtxos: LockupVtxo[];
|
|
796
|
-
status: RfqStatus | null;
|
|
797
|
-
};
|
|
798
|
-
/**
|
|
799
|
-
* Ask first, then fall back: watch the swap for the solver to resolve it, and
|
|
800
|
-
* if `refundLocktime` matures without that happening, take the lockup back
|
|
801
|
-
* with `refundWithoutReceiver`.
|
|
802
|
-
*
|
|
803
|
-
* This is the whole trader-side failure story in one call. It polls `status()`
|
|
804
|
-
* — the only "asking" this protocol has (see the module doc) — and returns as
|
|
805
|
-
* soon as the solver reports `settled` or `refunded`. Otherwise, once the
|
|
806
|
-
* quote's `refund_locktime` passes, it looks up what is actually at the lockup
|
|
807
|
-
* and pushes the refund.
|
|
808
|
-
*
|
|
809
|
-
* Two behaviours worth knowing:
|
|
810
|
-
*
|
|
811
|
-
* - **A dead negotiation is not a reason to stop.** `refused`, `expired` and
|
|
812
|
-
* `stuck` are terminal states, but a trader can be holding a funded lockup in
|
|
813
|
-
* every one of them, so they do not end the wait — only `settled`/`refunded`
|
|
814
|
-
* do (see {@link RFQ_RESOLVED_STATES}). What ends it otherwise is the
|
|
815
|
-
* timelock.
|
|
816
|
-
* - **The first push after the deadline may legitimately fail.** Median-time-
|
|
817
|
-
* past trails wall clock, so the server can still consider the leaf locked
|
|
818
|
-
* for a while after `refundLocktime` passes in real time. Failures are
|
|
819
|
-
* retried at the poll interval until `attemptDeadline`, after which the last
|
|
820
|
-
* error is rethrown rather than swallowed.
|
|
821
|
-
* - **A swept lockup ends the wait instead of consuming it.** Once the batch
|
|
822
|
-
* is gone the CLTV refund is not "not yet" but "not this way", so it returns
|
|
823
|
-
* `needs_recovery` naming the outpoints rather than retrying until the
|
|
824
|
-
* deadline. Recover them and call again.
|
|
825
|
-
*
|
|
826
|
-
* Safe to call late, and safe to call again: a caller recovering from a crash
|
|
827
|
-
* well past the deadline skips straight to the push, and a lockup that is
|
|
828
|
-
* already empty comes back as `nothing_to_refund` instead of an error.
|
|
829
|
-
*/
|
|
830
|
-
declare function refundIfUnresolved(transport: RfqTransport, ark: RefundArkProvider, indexer: RefundIndexer, input: {
|
|
831
|
-
rfqId: string;
|
|
832
|
-
script: InstanceType<typeof VHTLC.ScriptV2>;
|
|
833
|
-
/** @see pushRefundWithoutReceiver */
|
|
834
|
-
sender: Identity;
|
|
835
|
-
/** `refund_locktime` from the quote, unix seconds. */
|
|
836
|
-
refundLocktime: number;
|
|
837
|
-
/** Defaults to the contract's own committed refund destination. */
|
|
838
|
-
refundPkScript?: Uint8Array;
|
|
839
|
-
pollMs?: number;
|
|
840
|
-
/** Stop retrying the push at this unix time, rethrowing the last
|
|
841
|
-
* error. Defaults to `refundLocktime + REFUND_MTP_LAG_SECONDS`. */
|
|
842
|
-
attemptDeadline?: number;
|
|
843
|
-
/** Injected for tests; defaults to wall clock, in unix seconds. */
|
|
844
|
-
now?: () => number;
|
|
845
|
-
}): Promise<RefundOutcome>;
|
|
846
|
-
|
|
847
647
|
/** The Ark surface the claim push needs — the same seam the refund push uses. */
|
|
848
648
|
type ClaimArkProvider = RefundArkProvider;
|
|
849
649
|
/**
|
|
@@ -991,6 +791,47 @@ declare function senderIdentityForSwapRecord(wallet: IWallet, record: {
|
|
|
991
791
|
signingDescriptor?: string;
|
|
992
792
|
}): Promise<Identity>;
|
|
993
793
|
|
|
794
|
+
/**
|
|
795
|
+
* The `refundArkade` callback, assembled.
|
|
796
|
+
*
|
|
797
|
+
* The wiring this replaces was prose in two places — `swapManager.ts`'s
|
|
798
|
+
* callback doc and the README — because it is the one callback whose obvious
|
|
799
|
+
* implementation is wrong: `refundIfUnresolved` is the single-swap version of
|
|
800
|
+
* `RfqSwapManager` itself and brings its own status polling and MTP retry loop,
|
|
801
|
+
* which would nest inside the manager's. Composing the atomic push here makes
|
|
802
|
+
* that mistake something a consumer has to go out of their way to make.
|
|
803
|
+
*
|
|
804
|
+
* The three semantic rules the manager relies on are structural rather than
|
|
805
|
+
* documented: an empty lockup returns `null`, and both
|
|
806
|
+
* {@link RefundNotLocallyPossibleError} and {@link LockupNeedsRecoveryError}
|
|
807
|
+
* propagate untouched — the manager reads the first as permanent and surfaces
|
|
808
|
+
* the second as `needs_recovery`, and catching either here would turn a state
|
|
809
|
+
* the trader must act on into a retry that grinds the window away.
|
|
810
|
+
*/
|
|
811
|
+
|
|
812
|
+
interface ArkadeRefunderDeps {
|
|
813
|
+
ark: RefundArkProvider;
|
|
814
|
+
indexer: RefundIndexer;
|
|
815
|
+
/** Asked for the descriptor's signer; never asked to mint a key. */
|
|
816
|
+
wallet: IWallet;
|
|
817
|
+
/**
|
|
818
|
+
* The record store. The live `RfqSwap` the manager passes carries no
|
|
819
|
+
* `signingDescriptor` — that lives in the record's `profile.signer` — so
|
|
820
|
+
* the refund key is resolved by key, which is what `getRfqSwap` is for.
|
|
821
|
+
*/
|
|
822
|
+
repository: Pick<AssetSwapRepository, "getRfqSwap">;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Build the `refundArkade` callback for `RfqSwapManager.setCallbacks`.
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* manager.setCallbacks({
|
|
829
|
+
* refundArkade: arkadeRefunder({ ark, indexer, wallet, repository }),
|
|
830
|
+
* saveSwap,
|
|
831
|
+
* });
|
|
832
|
+
*/
|
|
833
|
+
declare function arkadeRefunder(deps: ArkadeRefunderDeps): (swap: RfqSwap) => Promise<ArkadeRefundResult>;
|
|
834
|
+
|
|
994
835
|
/** The contract type a swap lockup registers under. `@arkade-os/sdk`'s handler
|
|
995
836
|
* for `VHTLC.ScriptV2` — the covenant script this corridor builds. */
|
|
996
837
|
declare const SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
|
|
@@ -1000,6 +841,25 @@ declare const SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
|
|
|
1000
841
|
* injection style as `SwapContractRegistry`, and satisfied by a real
|
|
1001
842
|
* `ContractManager` (`await wallet.getContractManager()`). */
|
|
1002
843
|
type LockupContractWriter = Pick<IContractManager, "createContract">;
|
|
844
|
+
/** The read seam {@link lockupContractParams} needs. Same narrowing, same
|
|
845
|
+
* `ContractManager` satisfies it. */
|
|
846
|
+
type LockupContractReader = Pick<IContractManager, "getContracts">;
|
|
847
|
+
/**
|
|
848
|
+
* No row for a lockup a record claims was funded.
|
|
849
|
+
*
|
|
850
|
+
* Separate from a rebuild failure on purpose: the record is fine and its money
|
|
851
|
+
* may well be at the address — what is missing is the wallet's copy of the
|
|
852
|
+
* covenant, which registration writes before the address can be funded. So this
|
|
853
|
+
* means the contract store was cleared or was never the one that registered
|
|
854
|
+
* this swap, and the remedy is a store, not a re-quote.
|
|
855
|
+
*/
|
|
856
|
+
declare class LockupContractMissing extends Error {
|
|
857
|
+
/** The lockup whose row is absent. */
|
|
858
|
+
readonly address: string;
|
|
859
|
+
/** Its pkScript hex — the key the row would have been under. */
|
|
860
|
+
readonly script: string;
|
|
861
|
+
constructor(address: string, script: string);
|
|
862
|
+
}
|
|
1003
863
|
/**
|
|
1004
864
|
* The lockup could not be written locally.
|
|
1005
865
|
*
|
|
@@ -1040,769 +900,35 @@ declare class LockupRegistrationFailed extends Error {
|
|
|
1040
900
|
* storage problem from a reason to walk away from the quote.
|
|
1041
901
|
*/
|
|
1042
902
|
declare function registerLockupContract(contracts: LockupContractWriter, script: InstanceType<typeof VHTLC.ScriptV2>, address: string): Promise<void>;
|
|
1043
|
-
|
|
1044
903
|
/**
|
|
1045
|
-
*
|
|
904
|
+
* The stored covenant parameters of a funded lockup — the other half of
|
|
905
|
+
* `rebuildRfqSwap`.
|
|
1046
906
|
*
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1051
|
-
*/
|
|
1052
|
-
type RfqSwapState =
|
|
1053
|
-
/** Live; nothing actionable yet. On a receive leg this covers the whole
|
|
1054
|
-
* stretch before the solver funds anything. */
|
|
1055
|
-
"pending"
|
|
1056
|
-
/** There is something for the trader to take, and the window to take it is
|
|
1057
|
-
* open: the confirmed L1 fill on an onchain send, or a lockup funded for at
|
|
1058
|
-
* least `expectedAmount` on a receive. */
|
|
1059
|
-
| "claimable"
|
|
1060
|
-
/**
|
|
1061
|
-
* The trader's claim has been made — its L1 broadcast on an onchain send,
|
|
1062
|
-
* its Arkade submission on a receive.
|
|
907
|
+
* The row is the wallet's own copy of the tree, written from the covenant
|
|
908
|
+
* before the address could be funded and keyed by the script it derives, which
|
|
909
|
+
* `createContract` refuses to write unless the params reproduce it. That is why
|
|
910
|
+
* an RFQ swap record stores no tree parameters of its own.
|
|
1063
911
|
*
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
* takes the lockup back.
|
|
1068
|
-
*/
|
|
1069
|
-
| "claimed"
|
|
1070
|
-
/**
|
|
1071
|
-
* This wallet will not act, and only the counterparty can change that.
|
|
1072
|
-
*
|
|
1073
|
-
* On a send leg: the Arkade refund cannot be pushed from here — no secrets
|
|
1074
|
-
* on the record, a descriptor from another seed, or nothing wired to act —
|
|
1075
|
-
* so the lockup comes back only if the counterparty claims it or the wallet
|
|
1076
|
-
* that can sign it is restored. On a receive leg: the trader holds no
|
|
1077
|
-
* refund at all, so this is a lockup that cannot be claimed — funded for
|
|
1078
|
-
* less than the swap agreed (publishing `P` for it is the whole attack
|
|
1079
|
-
* `LockupAmountMismatchError` exists to refuse), or one whose claim window
|
|
1080
|
-
* shut unclaimed. {@link RfqSwapCommon.blockedReason} says which.
|
|
1081
|
-
*
|
|
1082
|
-
* **Not terminal, and not a dead end.** The money is still at the lockup,
|
|
1083
|
-
* so the counterparty's move is still observable and still ends the swap;
|
|
1084
|
-
* and the refusal is re-checked every pass, so restoring the right wallet,
|
|
1085
|
-
* wiring the callbacks, or the solver topping the lockup up returns the
|
|
1086
|
-
* swap to `pending` and resumes the normal drive. For an onchain-send swap
|
|
1087
|
-
* it says nothing about the L1 half, which keeps being driven and claimed.
|
|
1088
|
-
*/
|
|
1089
|
-
| "needs_counterparty"
|
|
1090
|
-
/**
|
|
1091
|
-
* Terminal: the lockup was spent by a hash-verified claim. Read off chain,
|
|
1092
|
-
* never reported.
|
|
912
|
+
* Looked up by script rather than address: the script is the row's key, and
|
|
913
|
+
* decoding it here means a record whose address does not decode fails as a bad
|
|
914
|
+
* address instead of as a missing row.
|
|
1093
915
|
*
|
|
1094
|
-
*
|
|
1095
|
-
* counterparty completed its side. On a receive leg it is the TRADER's own
|
|
1096
|
-
* — matched by the hash and not by our txid, so a claim that lands without
|
|
1097
|
-
* us still counts (see {@link RfqSwapManager}).
|
|
1098
|
-
*/
|
|
1099
|
-
| "settled"
|
|
1100
|
-
/**
|
|
1101
|
-
* Terminal: the lockup was spent by something other than a claim.
|
|
1102
|
-
*
|
|
1103
|
-
* On a send leg that is the money coming back, by the solver's hand or the
|
|
1104
|
-
* trader's. **On a receive leg it is a LOSS**: the lockup was the solver's
|
|
1105
|
-
* money, every non-claim leaf is the solver's, and a swap that ends here
|
|
1106
|
-
* ended with the trader's incoming payment never arriving. It is also where
|
|
1107
|
-
* a receive swap ends when its window closes with nothing left to observe —
|
|
1108
|
-
* see {@link RfqSwapManager}.
|
|
1109
|
-
*/
|
|
1110
|
-
| "refunded"
|
|
1111
|
-
/** Terminal: an action failed and its window closed. */
|
|
1112
|
-
| "failed";
|
|
1113
|
-
/** The states after which the manager stops monitoring a swap. Deliberately
|
|
1114
|
-
* without `needs_counterparty`: retiring on it would unwatch a funded lockup
|
|
1115
|
-
* whose claim is still the thing that ends the swap. */
|
|
1116
|
-
declare const RFQ_SWAP_TERMINAL_STATES: readonly ["settled", "refunded", "failed"];
|
|
1117
|
-
declare const isRfqSwapTerminal: (state: RfqSwapState) => boolean;
|
|
1118
|
-
/**
|
|
1119
|
-
* What the manager needs to register a swap's lockup with the wallet, so the
|
|
1120
|
-
* indexer pushes its funding and its spend instead of being asked every few
|
|
1121
|
-
* seconds.
|
|
1122
|
-
*
|
|
1123
|
-
* Both fields are things the caller already holds. `script` is the very object
|
|
1124
|
-
* `pushRefundWithoutReceiver` and `pushClaim` take, so a caller wired to act has
|
|
1125
|
-
* it in hand; `address` is the request entrypoint's own return value. The
|
|
1126
|
-
* address is taken rather than re-derived on purpose — the row's address must be
|
|
1127
|
-
* the one that was actually funded, and a local re-derivation would silently use
|
|
1128
|
-
* the SDK's default network, which is the exact bug `registerOfferContract`
|
|
1129
|
-
* guards against.
|
|
1130
|
-
*/
|
|
1131
|
-
interface RfqSwapLockup {
|
|
1132
|
-
/** The covenant. Its `pkScript` MUST equal the record's `lockupPkScript`. */
|
|
1133
|
-
script: InstanceType<typeof VHTLC.ScriptV2>;
|
|
1134
|
-
/** The Arkade address that was funded. */
|
|
1135
|
-
address: string;
|
|
1136
|
-
}
|
|
1137
|
-
interface RfqSwapCommon {
|
|
1138
|
-
/** The negotiation id — this record's identity. */
|
|
1139
|
-
rfqId: string;
|
|
1140
|
-
state: RfqSwapState;
|
|
1141
|
-
/** The Arkade lockup's scriptPubKey — `swapPkScript` from any of the four
|
|
1142
|
-
* request entrypoints. This is what the manager watches to decide the swap:
|
|
1143
|
-
* it is the only handle on the covenant whose spend witness says whether
|
|
1144
|
-
* the swap settled or came back. */
|
|
1145
|
-
lockupPkScript: Uint8Array;
|
|
1146
|
-
/** The covenant behind {@link lockupPkScript}, when the caller wants the
|
|
1147
|
-
* lockup registered with a contract manager. Optional: without it the
|
|
1148
|
-
* manager still watches the swap on its timer, it just cannot subscribe.
|
|
1149
|
-
* See {@link RfqSwapManagerDeps.contracts}. */
|
|
1150
|
-
lockup?: RfqSwapLockup;
|
|
1151
|
-
/** `sha256(P)`, hex — the quote's `payment_hash`. The claim leaf can only
|
|
1152
|
-
* be spent by revealing a value that hashes to this, which is what makes a
|
|
1153
|
-
* settlement provable rather than reported. For an onchain send this is
|
|
1154
|
-
* the SAME hash the L1 `htlc` carries: one `P` unlocks both legs. */
|
|
1155
|
-
paymentHash: string;
|
|
1156
|
-
/**
|
|
1157
|
-
* `refund_locktime` from the quote, unix seconds.
|
|
1158
|
-
*
|
|
1159
|
-
* Whose deadline it is inverts with the direction, and so does what to do
|
|
1160
|
-
* about it. On a send leg it is the TRADER's: the lockup is the trader's
|
|
1161
|
-
* money and this gates the refund that takes it back, so it is a moment to
|
|
1162
|
-
* act AFTER. On a receive leg it is the SOLVER's: the trader has no refund
|
|
1163
|
-
* leaf at all, and this is the moment to have claimed BEFORE.
|
|
1164
|
-
*/
|
|
1165
|
-
refundLocktime: number;
|
|
1166
|
-
createdAt: number;
|
|
1167
|
-
updatedAt: number;
|
|
1168
|
-
/** Set once the trader's own `refundWithoutReceiver` push landed. */
|
|
1169
|
-
refundArkTxid?: string;
|
|
1170
|
-
/** Why `state` is `failed`. */
|
|
1171
|
-
failure?: string;
|
|
1172
|
-
/** Why `state` is `needs_counterparty`. Distinct from {@link failure},
|
|
1173
|
-
* which means an action was attempted and did not work. */
|
|
1174
|
-
blockedReason?: string;
|
|
1175
|
-
}
|
|
1176
|
-
/** `arkade:BTC->lightning:BTC`. Nothing for the trader to claim: the solver
|
|
1177
|
-
* claims the lockup with the preimage it learns by paying the invoice — which
|
|
1178
|
-
* is exactly why that spend's witness is proof the payment landed. */
|
|
1179
|
-
interface LightningSendSwap extends RfqSwapCommon {
|
|
1180
|
-
kind: "lightning_send";
|
|
1181
|
-
}
|
|
1182
|
-
/** `arkade:BTC->onchain:BTC`. Carries the L1 half the trader must claim. */
|
|
1183
|
-
interface OnchainSendSwap extends RfqSwapCommon {
|
|
1184
|
-
kind: "onchain_send";
|
|
1185
|
-
/** The locally derived HTLC from `requestOnchainSend` — the manager reads
|
|
1186
|
-
* `pkScript`, `paymentHash` and `refundLocktime` off it to classify. */
|
|
1187
|
-
htlc: OnchainHtlc;
|
|
1188
|
-
/** `profile.min_confirmations` from the quote. */
|
|
1189
|
-
minConfirmations: number;
|
|
1190
|
-
/** The fill's outpoint, learned on first sighting. Without it a SPENT
|
|
1191
|
-
* HTLC reads as never funded — see {@link classifyOnchainHtlc}. */
|
|
1192
|
-
funding?: {
|
|
1193
|
-
txid: string;
|
|
1194
|
-
vout: number;
|
|
1195
|
-
};
|
|
1196
|
-
/** Our L1 claim's txid. */
|
|
1197
|
-
claimTxid?: string;
|
|
1198
|
-
}
|
|
1199
|
-
/**
|
|
1200
|
-
* `lightning:BTC->arkade:BTC`. The inverted leg: the SOLVER funds the lockup
|
|
1201
|
-
* and the TRADER claims it, and that claim is what publishes `P` and lets the
|
|
1202
|
-
* solver settle the payer's held Lightning HTLC.
|
|
1203
|
-
*
|
|
1204
|
-
* Two consequences shape how this record is driven, both of them absent from
|
|
1205
|
-
* the send legs:
|
|
1206
|
-
*
|
|
1207
|
-
* - **There is no trader-side refund.** Every non-claim leaf of this covenant
|
|
1208
|
-
* is the solver's, so the manager never calls
|
|
1209
|
-
* {@link RfqSwapManagerCallbacks.refundArkade} for one of these. A swap that
|
|
1210
|
-
* is not claimed is simply lost — the solver reclaims at
|
|
1211
|
-
* {@link RfqSwapCommon.refundLocktime} and the payer is refunded when the
|
|
1212
|
-
* held HTLC lapses.
|
|
1213
|
-
* - **The claim is the whole swap, and it is on a deadline.** The trader must
|
|
1214
|
-
* be online for it: covclaimd cannot claim this covenant today, so the claim
|
|
1215
|
-
* packet's offline path does not run.
|
|
1216
|
-
*/
|
|
1217
|
-
interface LightningReceiveSwap extends RfqSwapCommon {
|
|
1218
|
-
kind: "lightning_receive";
|
|
1219
|
-
/**
|
|
1220
|
-
* What the lockup must carry — the quote's `to_amount`, captured at REQUEST
|
|
1221
|
-
* time and persisted with the record.
|
|
1222
|
-
*
|
|
1223
|
-
* **Not re-derivable, and not optional.** Captured at claim time it would
|
|
1224
|
-
* be whatever the solver funded, which is the dust-funding attack rather
|
|
1225
|
-
* than a check on it. A record that reaches the manager without a finite
|
|
1226
|
-
* value here is reported `needs_counterparty` and never claimed: a
|
|
1227
|
-
* comparison against `undefined` or `NaN` is false, so an unusable
|
|
1228
|
-
* comparand does not fail the value gate, it deletes it.
|
|
1229
|
-
*/
|
|
1230
|
-
expectedAmount: number;
|
|
1231
|
-
/** Our Arkade claim's txid, once submitted. Set from the callback's return
|
|
1232
|
-
* and never from a chain read — the chain's answer is `settled`. */
|
|
1233
|
-
claimArkTxid?: string;
|
|
1234
|
-
}
|
|
1235
|
-
/**
|
|
1236
|
-
* A monitored swap.
|
|
1237
|
-
*
|
|
1238
|
-
* This is a live record, not a serialization format: `lockupPkScript` and
|
|
1239
|
-
* `htlc` hold derived `Uint8Array`s, and
|
|
1240
|
-
* {@link RfqSwapManagerCallbacks.saveSwap} is where a caller projects it into
|
|
1241
|
-
* whatever it stores. Rebuild it on restart the way it was made —
|
|
1242
|
-
* `lightningSendVtxoScript` / `receiveVtxoScript` / `onchainHtlcScript` over
|
|
1243
|
-
* the quote's binding fields — and hand the result to
|
|
1244
|
-
* {@link RfqSwapManager.start}.
|
|
1245
|
-
*
|
|
1246
|
-
* **`onchain:BTC->arkade:BTC` is deliberately not a member yet.** Its Arkade
|
|
1247
|
-
* half is the same solver-funded lockup as {@link LightningReceiveSwap}'s, but
|
|
1248
|
-
* it also has an L1 half the trader funds and must take back itself
|
|
1249
|
-
* (`buildHtlcRefund` at the HTLC's own `htlc_locktime`), which is a second
|
|
1250
|
-
* deadline, a second observation seam and a second action callback. Adding the
|
|
1251
|
-
* lockup half alone would produce a manager that silently lets that L1 refund
|
|
1252
|
-
* window pass — the one failure mode {@link RfqSwapManager} refuses elsewhere
|
|
1253
|
-
* by name (see `driveOnchain`'s missing-`ChainSource` check). Until the L1
|
|
1254
|
-
* refund is driven too, that corridor is better served by the request and claim
|
|
1255
|
-
* functions directly than by a monitor that covers half of it.
|
|
1256
|
-
*/
|
|
1257
|
-
type RfqSwap = LightningSendSwap | OnchainSendSwap | LightningReceiveSwap;
|
|
1258
|
-
/** What the manager should do next about an onchain-send swap's L1 half. */
|
|
1259
|
-
type OnchainSendAction =
|
|
1260
|
-
/** Not funded yet, or not confirmed deep enough. */
|
|
1261
|
-
"wait"
|
|
1262
|
-
/** Funded, confirmed, and far enough from the refund leaf to claim safely. */
|
|
1263
|
-
| "claim"
|
|
1264
|
-
/** The claim is off the table for good; the money comes back through the
|
|
1265
|
-
* Arkade lockup instead. */
|
|
1266
|
-
| "claim_window_closed"
|
|
1267
|
-
/** Our claim already landed (only the trader holds P). */
|
|
1268
|
-
| "claimed"
|
|
1269
|
-
/** The solver took its L1 refund — the fill is gone. */
|
|
1270
|
-
| "swept";
|
|
1271
|
-
/**
|
|
1272
|
-
* The decision a "poll status, refund on timeout" loop gets wrong.
|
|
1273
|
-
*
|
|
1274
|
-
* {@link OnchainHtlcPhase} runs `unfunded -> awaiting_confirmations ->
|
|
1275
|
-
* claimable -> (refundable | claimed | swept)`, and `refundable` does NOT mean
|
|
1276
|
-
* "time to refund the L1 HTLC" — the trader has no key on that leaf; it is the
|
|
1277
|
-
* SOLVER's refund, and reaching it means the trader's claim was missed. So the
|
|
1278
|
-
* L1 claim has to be driven before it, and from it the only remaining move is
|
|
1279
|
-
* the Arkade-side refund.
|
|
1280
|
-
*
|
|
1281
|
-
* There is a second, quieter trap between those two functions:
|
|
1282
|
-
* `classifyOnchainHtlc` reports `claimable` right up until median-time-past
|
|
1283
|
-
* reaches `refundLocktime`, while `claimOnchainFill` refuses from
|
|
1284
|
-
* {@link ONCHAIN_CLAIM_MARGIN_SECONDS} before it — because broadcasting
|
|
1285
|
-
* publishes P, and doing that into the counterparty's live refund window risks
|
|
1286
|
-
* losing the race AND giving away the preimage. Driving straight off the phase
|
|
1287
|
-
* would therefore spend that whole margin throwing `claim_window_closed` at
|
|
1288
|
-
* every poll and never fall back. This function applies the margin, so
|
|
1289
|
-
* "claimable" here means claimable by `claimOnchainFill` too.
|
|
1290
|
-
*/
|
|
1291
|
-
declare function nextOnchainAction(input: {
|
|
1292
|
-
phase: OnchainHtlcPhase;
|
|
1293
|
-
/** `htlc.refundLocktime` — when the solver's L1 refund leaf opens. */
|
|
1294
|
-
htlcLocktime: number;
|
|
1295
|
-
/** Unix seconds. */
|
|
1296
|
-
now: number;
|
|
1297
|
-
}): OnchainSendAction;
|
|
1298
|
-
/** What the trader's own `refundWithoutReceiver` push returned, or `null` when
|
|
1299
|
-
* the lockup held nothing to return. */
|
|
1300
|
-
type ArkadeRefundResult = {
|
|
1301
|
-
arkTxid: string;
|
|
1302
|
-
amount: number;
|
|
1303
|
-
} | null;
|
|
1304
|
-
/**
|
|
1305
|
-
* The money-moving half, injected. The manager decides when; these do it.
|
|
1306
|
-
*
|
|
1307
|
-
* Neither action gets a retry loop of its own here — see the module doc. Do
|
|
1308
|
-
* NOT wire `refundArkade` to `refundIfUnresolved`: that function is the
|
|
1309
|
-
* single-swap version of this whole class and brings its own status polling
|
|
1310
|
-
* and its own MTP retry loop, which would nest inside the manager's. Wire it
|
|
1311
|
-
* to `findLockupVtxos` + `pushRefundWithoutReceiver`, which is the atomic push
|
|
1312
|
-
* `refundIfUnresolved` itself calls.
|
|
1313
|
-
*
|
|
1314
|
-
* Resolve the sender key through `senderIdentityForSwapRecord`: it is
|
|
1315
|
-
* what turns "this wallet cannot sign this swap" into
|
|
1316
|
-
* {@link RefundNotLocallyPossibleError}, which the manager reports as
|
|
1317
|
-
* `needs_counterparty` instead of retrying for the whole refund window.
|
|
1318
|
-
*/
|
|
1319
|
-
interface RfqSwapManagerCallbacks {
|
|
1320
|
-
/** Build and broadcast the L1 claim. See `claimOnchainFill`. */
|
|
1321
|
-
claimOnchain: (swap: OnchainSendSwap, utxo: ChainUtxo) => Promise<{
|
|
1322
|
-
txid: string;
|
|
1323
|
-
}>;
|
|
1324
|
-
/**
|
|
1325
|
-
* Claim the solver-funded lockup on a receive leg, revealing `P`. Wire it
|
|
1326
|
-
* to `pushClaim` — the outputs are supplied, so `findLockupVtxos` has
|
|
1327
|
-
* already been called and `claimReceiveLockup`'s wait would only sit on a
|
|
1328
|
-
* lockup the manager has just seen.
|
|
1329
|
-
*
|
|
1330
|
-
* **Pass `expectedAmount` and `partiallyClaimed` straight through.** The
|
|
1331
|
-
* manager checks the funded value before calling this, but that check
|
|
1332
|
-
* decides WHEN to act; `pushClaim`'s decides whether `P` is published, and
|
|
1333
|
-
* it is the one that runs with nothing between it and the signature. Two
|
|
1334
|
-
* checks, one of which is load-bearing — do not drop the inner one because
|
|
1335
|
-
* the outer one exists.
|
|
1336
|
-
*
|
|
1337
|
-
* Required rather than optional, like {@link claimOnchain}: a receive swap
|
|
1338
|
-
* monitored with nothing wired to claim it is a swap that quietly expires,
|
|
1339
|
-
* and a compile error is the right way to learn that.
|
|
1340
|
-
*/
|
|
1341
|
-
claimLockup: (swap: LightningReceiveSwap, vtxos: readonly LockupVtxo[], options: {
|
|
1342
|
-
/** A claim of ours is already out, so `P` is public and the value
|
|
1343
|
-
* gate has nothing left to protect — pass this to `pushClaim` so a
|
|
1344
|
-
* funding that arrived piecemeal can still be swept. */
|
|
1345
|
-
partiallyClaimed: boolean;
|
|
1346
|
-
}) => Promise<{
|
|
1347
|
-
arkTxid: string;
|
|
1348
|
-
amount: number;
|
|
1349
|
-
}>;
|
|
1350
|
-
/** Push `refundWithoutReceiver` for every output at the lockup. See
|
|
1351
|
-
* `pushRefundWithoutReceiver`; return `null` for an empty lockup. Never
|
|
1352
|
-
* called for a {@link LightningReceiveSwap} — that leg's refund leaf is the
|
|
1353
|
-
* solver's. */
|
|
1354
|
-
refundArkade: (swap: RfqSwap) => Promise<ArkadeRefundResult>;
|
|
1355
|
-
/**
|
|
1356
|
-
* Whether a local refund is possible at all — the record's secrets, against
|
|
1357
|
-
* this wallet. Called every pass, including *before* the refund window
|
|
1358
|
-
* opens, so a swap nobody can refund says so while the solver can still
|
|
1359
|
-
* act, instead of at the deadline; and so restoring the right wallet lifts
|
|
1360
|
-
* the state again. Never called for a receive swap: there is no local
|
|
1361
|
-
* refund there to probe for.
|
|
1362
|
-
*
|
|
1363
|
-
* Optional: omit to answer "yes" and learn at push time, from
|
|
1364
|
-
* {@link RefundNotLocallyPossibleError}. Local by contract — no network
|
|
1365
|
-
* call belongs here.
|
|
1366
|
-
*/
|
|
1367
|
-
canRefundArkade?: (swap: RfqSwap) => Promise<{
|
|
1368
|
-
ok: true;
|
|
1369
|
-
} | {
|
|
1370
|
-
ok: false;
|
|
1371
|
-
reason: string;
|
|
1372
|
-
}>;
|
|
1373
|
-
/** Persist the record. Called after any pass that changed it. */
|
|
1374
|
-
saveSwap: (swap: RfqSwap) => Promise<void>;
|
|
1375
|
-
}
|
|
1376
|
-
/** The actions the manager executes on a caller's behalf. */
|
|
1377
|
-
type RfqSwapActionName = "claimOnchain" | "claimLockup" | "refundArkade";
|
|
1378
|
-
interface RfqSwapManagerEvents {
|
|
1379
|
-
/** Every state change, including ones that read as going backwards.
|
|
1380
|
-
* `claimed -> claimable` is legal and expected on a receive swap the solver
|
|
1381
|
-
* funds piecemeal: a lockup topped up after a claim is a new claimable
|
|
1382
|
-
* event, and the label says so before the sweep goes out. Treat these
|
|
1383
|
-
* states as a description of what to do next, not as a progress bar. */
|
|
1384
|
-
onSwapUpdate?: (swap: RfqSwap, previous: RfqSwapState) => void;
|
|
1385
|
-
/** Fired once, when a swap leaves monitoring `settled` or `refunded`.
|
|
1386
|
-
* A swap that ends `failed` reports through `onSwapFailed` instead — the
|
|
1387
|
-
* two are mutually exclusive. */
|
|
1388
|
-
onSwapCompleted?: (swap: RfqSwap) => void;
|
|
1389
|
-
/** Fired for any action that threw — including ones the manager will retry
|
|
1390
|
-
* on the next pass — and once more when the swap finally ends `failed`. */
|
|
1391
|
-
onSwapFailed?: (swap: RfqSwap, error: Error) => void;
|
|
1392
|
-
onActionExecuted?: (swap: RfqSwap, action: RfqSwapActionName) => void;
|
|
1393
|
-
}
|
|
1394
|
-
type SwapUpdateListener = NonNullable<RfqSwapManagerEvents["onSwapUpdate"]>;
|
|
1395
|
-
type SwapCompletedListener = NonNullable<RfqSwapManagerEvents["onSwapCompleted"]>;
|
|
1396
|
-
type SwapFailedListener = NonNullable<RfqSwapManagerEvents["onSwapFailed"]>;
|
|
1397
|
-
type ActionExecutedListener = NonNullable<RfqSwapManagerEvents["onActionExecuted"]>;
|
|
1398
|
-
interface RfqSwapManagerConfig {
|
|
1399
|
-
/** Drive claims and refunds automatically (default: true). With this off
|
|
1400
|
-
* the manager still watches and reports, so a caller can act by hand off
|
|
1401
|
-
* `claimable`. */
|
|
1402
|
-
enableAutoActions?: boolean;
|
|
1403
|
-
/** How often to run a pass, ms. Default 5000 — the same interval
|
|
1404
|
-
* `awaitOnchainFill` and `refundIfUnresolved` poll at. */
|
|
1405
|
-
pollIntervalMs?: number;
|
|
1406
|
-
/** Injected for tests; defaults to wall clock, in unix seconds — the same
|
|
1407
|
-
* convention `refundIfUnresolved` uses. */
|
|
1408
|
-
now?: () => number;
|
|
1409
|
-
events?: RfqSwapManagerEvents;
|
|
1410
|
-
}
|
|
1411
|
-
/** The contract-manager surface this needs, narrowed for injection — the same
|
|
1412
|
-
* seam style as {@link LockupSpendIndexer} and `refund.ts`'s
|
|
1413
|
-
* {@link RefundArkProvider}, and satisfied structurally by a real
|
|
1414
|
-
* `ContractManager` (`await wallet.getContractManager()`). */
|
|
1415
|
-
type SwapContractRegistry = Pick<IContractManager, "createContract" | "getContracts" | "onContractEvent" | "setContractWatchState">;
|
|
1416
|
-
/** The observation seams. None is owned by the manager, and none holds keys —
|
|
1417
|
-
* same philosophy as `onchainHtlc.ts`'s `ChainSource`. There is no
|
|
1418
|
-
* `RfqTransport` here on purpose: nothing this manager decides depends on the
|
|
1419
|
-
* solver answering (see the module doc). */
|
|
1420
|
-
interface RfqSwapManagerDeps {
|
|
1421
|
-
/** Arkade access. Required: this is how a swap's resolution is determined,
|
|
1422
|
-
* for both legs. */
|
|
1423
|
-
indexer: LockupSpendIndexer;
|
|
1424
|
-
/** L1 access. Required to monitor onchain-send swaps; a lightning-only
|
|
1425
|
-
* caller can leave it out. */
|
|
1426
|
-
chain?: ChainSource;
|
|
1427
|
-
/**
|
|
1428
|
-
* The wallet's contract manager, when there is one. Optional in the same
|
|
1429
|
-
* way {@link chain} is: a caller with no wallet, or one that only wants the
|
|
1430
|
-
* timer, still gets a fully working manager — the subscription is a
|
|
1431
|
-
* LATENCY optimization and nothing depends on it.
|
|
1432
|
-
*
|
|
1433
|
-
* Supplying it buys two things. The lockup gets REGISTERED, which is what
|
|
1434
|
-
* puts it in the wallet's own contract set at all — a prerequisite for
|
|
1435
|
-
* anything that has to act on the lockup before its batch expires, since an
|
|
1436
|
-
* expired lockup is swept and loses every cooperative path. And the indexer
|
|
1437
|
-
* PUSHES its funding and its spend, so a settlement is noticed when it
|
|
1438
|
-
* happens rather than up to `pollIntervalMs` later.
|
|
1439
|
-
*
|
|
1440
|
-
* Prefer `await wallet.getContractManager()` over constructing one, the way
|
|
1441
|
-
* `createOffer` does.
|
|
1442
|
-
*/
|
|
1443
|
-
contracts?: SwapContractRegistry;
|
|
1444
|
-
}
|
|
1445
|
-
/**
|
|
1446
|
-
* Watches a set of live RFQ swaps and drives each to its end.
|
|
1447
|
-
*
|
|
1448
|
-
* One pass per swap, in this order, every
|
|
1449
|
-
* {@link RfqSwapManagerConfig.pollIntervalMs} — and additionally the moment a
|
|
1450
|
-
* contract event names that swap's lockup, which changes only WHEN a pass runs,
|
|
1451
|
-
* never what it concludes (see {@link subscribe}):
|
|
1452
|
-
*
|
|
1453
|
-
* 0. **Register the lockup**, if a contract manager was supplied and it is not
|
|
1454
|
-
* registered yet. Best-effort; never blocks the steps below.
|
|
1455
|
-
* 1. **Ask the chain what became of the lockup** — {@link readLockupFate}. A
|
|
1456
|
-
* spend whose witness HASHES to the quote's `payment_hash` ends the swap
|
|
1457
|
-
* `settled`; a lockup fully spent by anything else ends it `refunded`.
|
|
1458
|
-
* Anything the indexer could not answer is `unknown`, which is NOT an
|
|
1459
|
-
* answer: the pass carries on to the steps below, whose deadlines an indexer
|
|
1460
|
-
* outage has no bearing on.
|
|
1461
|
-
* 2. **Drive the trader's claim.** On an onchain send that is the L1 fill — see
|
|
1462
|
-
* {@link nextOnchainAction}. On a receive it is the lockup itself, and it
|
|
1463
|
-
* ends the pass: that leg has no step 3.
|
|
1464
|
-
* 3. **Take the lockup back**, send legs only, once `refundLocktime` has passed
|
|
1465
|
-
* and step 1 has not ended the swap. This runs for onchain-send too,
|
|
1466
|
-
* including after a successful claim: the trader's lockup is still funded and
|
|
1467
|
-
* still theirs to recover if the solver never comes for it. When no local
|
|
1468
|
-
* refund is possible at all — no secrets, another wallet's descriptor,
|
|
1469
|
-
* nothing wired — the swap reports `needs_counterparty` instead of retrying
|
|
1470
|
-
* a push that cannot work.
|
|
1471
|
-
*
|
|
1472
|
-
* **What step 1 proves depends on the direction.** On a send leg every non-claim
|
|
1473
|
-
* leaf pays the trader's own committed address or needs the trader's own
|
|
1474
|
-
* signature, so "spent, but not by a hash-verified claim" means the money came
|
|
1475
|
-
* back. On a receive leg those leaves are the SOLVER's and the claim leaf is the
|
|
1476
|
-
* trader's, so the same two readings mean the opposite things — `settled` is the
|
|
1477
|
-
* trader's own claim landing, `refunded` is the solver taking back a lockup the
|
|
1478
|
-
* trader failed to claim. The read is identical; only the state docs differ.
|
|
1479
|
-
*
|
|
1480
|
-
* Two things about the receive arm that are easy to get wrong, and are asserted
|
|
1481
|
-
* in the tests rather than left to be inferred:
|
|
1482
|
-
*
|
|
1483
|
-
* - **A claim is matched by its preimage, never by our txid.** The covenant's
|
|
1484
|
-
* `nonInteractiveClaim` leaf is pinned to the trader's own payout script, so a
|
|
1485
|
-
* claim that lands without us — covclaimd, the day it works — still pays the
|
|
1486
|
-
* trader and is still `settled`. Matching on the txid we submitted would turn
|
|
1487
|
-
* that success into an anomaly.
|
|
1488
|
-
* - **`LockupFate.fate === "claimed"` maps to the state `settled`, never to the
|
|
1489
|
-
* state `claimed`.** The two words live one layer apart: the fate is the
|
|
1490
|
-
* chain's, the state is ours, and the state `claimed` means only that we
|
|
1491
|
-
* submitted something.
|
|
916
|
+
* Throws {@link LockupContractMissing} when there is no row.
|
|
1492
917
|
*/
|
|
1493
|
-
declare
|
|
1494
|
-
private readonly deps;
|
|
1495
|
-
private readonly config;
|
|
1496
|
-
private callbacks;
|
|
1497
|
-
private readonly swapUpdateListeners;
|
|
1498
|
-
private readonly swapCompletedListeners;
|
|
1499
|
-
private readonly swapFailedListeners;
|
|
1500
|
-
private readonly actionExecutedListeners;
|
|
1501
|
-
private readonly monitored;
|
|
1502
|
-
/** Monitored swaps by lockup script hex, so a contract event — which names
|
|
1503
|
-
* a script and nothing else — can find the swap it belongs to. */
|
|
1504
|
-
private readonly byLockupScript;
|
|
1505
|
-
/**
|
|
1506
|
-
* Swaps whose lockup registration has been SETTLED one way or another,
|
|
1507
|
-
* mapped to whether a contract row actually resulted. Membership is what
|
|
1508
|
-
* stops a per-pass retry from becoming a per-pass round trip; the value is
|
|
1509
|
-
* what keeps a swap that could never be registered from later trying to
|
|
1510
|
-
* retire a row that does not exist, which would report a spurious failure
|
|
1511
|
-
* on a swap that in fact succeeded.
|
|
1512
|
-
*/
|
|
1513
|
-
private readonly registered;
|
|
1514
|
-
/**
|
|
1515
|
-
* Swaps whose `refundArkade` answered {@link RefundNotLocallyPossibleError}
|
|
1516
|
-
* in this process. Membership stops the push from being re-issued every
|
|
1517
|
-
* pass — it cannot start working on its own, and re-issuing it is the
|
|
1518
|
-
* grind `needs_counterparty` exists to remove. Only
|
|
1519
|
-
* {@link RfqSwapManagerCallbacks.canRefundArkade} clears it, so a caller
|
|
1520
|
-
* with no probe learns again on the next start, when the wallet that can
|
|
1521
|
-
* sign may well have been restored.
|
|
1522
|
-
*/
|
|
1523
|
-
private readonly refundRefused;
|
|
1524
|
-
/**
|
|
1525
|
-
* The last error a receive swap's claim callback threw, by rfqId.
|
|
1526
|
-
*
|
|
1527
|
-
* Kept only to tell two terminal outcomes apart once the claim window
|
|
1528
|
-
* shuts: a swap whose claim was attempted and kept failing ends `failed`
|
|
1529
|
-
* with that reason, while one that simply never became claimable ends
|
|
1530
|
-
* `refunded`. Without it a broken claim callback would resolve a caller's
|
|
1531
|
-
* {@link waitForSwapCompletion} as an ordinary unwind.
|
|
1532
|
-
*
|
|
1533
|
-
* Process-local, like {@link refundRefused}: after a restart the same swap
|
|
1534
|
-
* ends `refunded` instead, which costs the caller a reason and nothing else
|
|
1535
|
-
* — every throw was already reported through `onSwapFailed` as it happened.
|
|
1536
|
-
*/
|
|
1537
|
-
private readonly lastClaimError;
|
|
1538
|
-
/**
|
|
1539
|
-
* The lockup outpoints a receive swap's claim callback has already been
|
|
1540
|
-
* handed, by rfqId.
|
|
1541
|
-
*
|
|
1542
|
-
* What this exists to prevent: a claim SUCCEEDS, and for the next few
|
|
1543
|
-
* passes the indexer still lists those outputs as unspent. Without a
|
|
1544
|
-
* record of what was already claimed, every one of those passes would
|
|
1545
|
-
* re-submit the same spend, fail against the server, and report a swap
|
|
1546
|
-
* that in fact worked as failing. With one, a re-claim happens only when
|
|
1547
|
-
* an outpoint appears that was never claimed — a lockup funded piecemeal,
|
|
1548
|
-
* which is legitimate and which `partiallyClaimed` exists for.
|
|
1549
|
-
*
|
|
1550
|
-
* Process-local: after a restart a swap with a live claim tries once more.
|
|
1551
|
-
* That is the recovery case rather than the spam one — a claim that never
|
|
1552
|
-
* landed leaves its outputs unspent, and one that did leaves a single
|
|
1553
|
-
* rejection.
|
|
1554
|
-
*/
|
|
1555
|
-
private readonly claimedOutpoints;
|
|
1556
|
-
/** Live `onContractEvent` subscription, held so `stop()` can drop it. */
|
|
1557
|
-
private unsubscribeContracts;
|
|
1558
|
-
/** Terminal records, kept so a late {@link waitForSwapCompletion} still
|
|
1559
|
-
* answers instead of throwing "not found". Cleared by {@link removeSwap}. */
|
|
1560
|
-
private readonly finished;
|
|
1561
|
-
private readonly waiters;
|
|
1562
|
-
/** Records changed during the current pass, flushed through `saveSwap`. */
|
|
1563
|
-
private readonly dirty;
|
|
1564
|
-
/** Race guard: one action at a time per swap. */
|
|
1565
|
-
private readonly inProgress;
|
|
1566
|
-
private timer;
|
|
1567
|
-
private running;
|
|
1568
|
-
constructor(deps: RfqSwapManagerDeps, config?: RfqSwapManagerConfig);
|
|
1569
|
-
/** Wire the money-moving half. Without it the manager only watches. */
|
|
1570
|
-
setCallbacks(callbacks: RfqSwapManagerCallbacks): void;
|
|
1571
|
-
onSwapUpdate(listener: SwapUpdateListener): () => void;
|
|
1572
|
-
onSwapCompleted(listener: SwapCompletedListener): () => void;
|
|
1573
|
-
onSwapFailed(listener: SwapFailedListener): () => void;
|
|
1574
|
-
onActionExecuted(listener: ActionExecutedListener): () => void;
|
|
1575
|
-
/**
|
|
1576
|
-
* Load records and begin monitoring. Runs one pass immediately — a caller
|
|
1577
|
-
* resuming after a restart may be well past a deadline already — then
|
|
1578
|
-
* every `pollIntervalMs`. Records that are already terminal are kept only
|
|
1579
|
-
* so {@link waitForSwapCompletion} can answer for them.
|
|
1580
|
-
*
|
|
1581
|
-
* Calling it again while running loads the records and returns rather than
|
|
1582
|
-
* re-arming — dropping them silently would strand a funded swap on a
|
|
1583
|
-
* caller's harmless double-start.
|
|
1584
|
-
*/
|
|
1585
|
-
start(swaps?: readonly RfqSwap[]): Promise<void>;
|
|
1586
|
-
/**
|
|
1587
|
-
* Stop monitoring and clear the timer. In-flight actions are not
|
|
1588
|
-
* cancellable and run to completion; outstanding
|
|
1589
|
-
* {@link waitForSwapCompletion} promises are left pending, since
|
|
1590
|
-
* stop/start is a pause rather than a cancellation.
|
|
1591
|
-
*
|
|
1592
|
-
* The contract subscription is dropped too — an open stream with nothing
|
|
1593
|
-
* reacting to it is a leak, and {@link start} puts it back. What is NOT
|
|
1594
|
-
* undone is the contract registration: those rows are the wallet's, they
|
|
1595
|
-
* outlive this manager's lifecycle, and dropping them would unwatch a
|
|
1596
|
-
* lockup that is still funded.
|
|
1597
|
-
*/
|
|
1598
|
-
stop(): Promise<void>;
|
|
1599
|
-
/** Begin monitoring a swap. Polled immediately when the manager is running,
|
|
1600
|
-
* so a just-funded swap does not wait out a whole interval. */
|
|
1601
|
-
addSwap(swap: RfqSwap): Promise<void>;
|
|
1602
|
-
/** Forget a swap entirely, monitored or finished.
|
|
1603
|
-
*
|
|
1604
|
-
* Its contract row is left alone: registration is a wallet-level fact about
|
|
1605
|
-
* a script that may still hold money, and this call says only that THIS
|
|
1606
|
-
* manager stops driving the swap. Retiring the row is reserved for a swap
|
|
1607
|
-
* that reached a terminal state, where the lockup is provably done. */
|
|
1608
|
-
removeSwap(rfqId: string): Promise<void>;
|
|
1609
|
-
/** Every swap still being monitored. */
|
|
1610
|
-
getPendingSwaps(): Promise<RfqSwap[]>;
|
|
1611
|
-
hasSwap(rfqId: string): Promise<boolean>;
|
|
1612
|
-
/** True while an action for this swap holds the per-swap lock. */
|
|
1613
|
-
isProcessing(rfqId: string): Promise<boolean>;
|
|
1614
|
-
getStats(): Promise<{
|
|
1615
|
-
isRunning: boolean;
|
|
1616
|
-
monitoredSwaps: number;
|
|
1617
|
-
finishedSwaps: number;
|
|
1618
|
-
inProgress: number;
|
|
1619
|
-
pollIntervalMs: number;
|
|
1620
|
-
}>;
|
|
1621
|
-
/**
|
|
1622
|
-
* Run one monitoring pass over every swap now.
|
|
1623
|
-
*
|
|
1624
|
-
* {@link start} calls this on an interval, but it is public on purpose: a
|
|
1625
|
-
* caller that sleeps its process (a mobile app resuming, a service worker
|
|
1626
|
-
* waking) wants a pass on that event rather than at the next tick. Passes
|
|
1627
|
-
* do not overlap per swap — the in-progress lock makes a concurrent call a
|
|
1628
|
-
* no-op for any swap already being worked on.
|
|
1629
|
-
*/
|
|
1630
|
-
poll(): Promise<void>;
|
|
1631
|
-
/**
|
|
1632
|
-
* Resolve once this swap's PAYOUT is decided — which for onchain-send is
|
|
1633
|
-
* the L1 claim, not the end of the record's life: once `claimTxid` is set
|
|
1634
|
-
* the trader has the coins it swapped for, and what remains is the manager
|
|
1635
|
-
* watching the Arkade lockup close. That holds however the record is
|
|
1636
|
-
* labelled afterwards, `needs_counterparty` included. Lightning-send has no
|
|
1637
|
-
* such split and resolves at `settled`/`refunded`, and so does lightning
|
|
1638
|
-
* receive — see {@link isPayoutDecided} for why its own claim txid does not
|
|
1639
|
-
* decide it.
|
|
1640
|
-
*
|
|
1641
|
-
* Rejects only on `failed`. `refunded` resolves: on a send leg a refund is
|
|
1642
|
-
* an outcome the caller asked this manager to drive, not an exception. On a
|
|
1643
|
-
* receive leg it is the swap being lost, which is still an answer and not
|
|
1644
|
-
* an error — read `state`, do not infer success from resolution.
|
|
1645
|
-
*/
|
|
1646
|
-
waitForSwapCompletion(rfqId: string): Promise<RfqSwapOutcome>;
|
|
1647
|
-
private track;
|
|
1648
|
-
/** Drops the swap from BOTH indexes. The event index is the one that stops
|
|
1649
|
-
* a late event finding a swap that is gone; `pollSwap`'s own
|
|
1650
|
-
* `monitored` check would also catch it, and deliberately still does —
|
|
1651
|
-
* either alone is sufficient, which is what keeps a future change to one of
|
|
1652
|
-
* them from silently re-driving a cancelled swap. */
|
|
1653
|
-
private untrack;
|
|
1654
|
-
/**
|
|
1655
|
-
* Turn the indexer's push into an extra reason to run a pass — and nothing
|
|
1656
|
-
* more.
|
|
1657
|
-
*
|
|
1658
|
-
* **This is deliberately not a source of truth.** An event names a script;
|
|
1659
|
-
* the reaction is to run the ordinary pass for the swap at that script, and
|
|
1660
|
-
* that pass re-reads the lockup through {@link readLockupFate} exactly as
|
|
1661
|
-
* the timer's pass does. So an event that is missed, duplicated, reordered
|
|
1662
|
-
* or outright FORGED can only cost or save latency — it can never change
|
|
1663
|
-
* what this manager believes about a swap, and it can never on its own
|
|
1664
|
-
* cause a claim or a refund. That property is what makes it safe to bolt a
|
|
1665
|
-
* best-effort stream onto a money path, and it must survive any future
|
|
1666
|
-
* change here: the moment an event is BELIEVED rather than merely acted on,
|
|
1667
|
-
* a relay outage becomes a correctness problem instead of a latency one.
|
|
1668
|
-
*
|
|
1669
|
-
* The timer stays armed regardless, and is the failsafe. Every deadline
|
|
1670
|
-
* that moves money — `refundLocktime`, the L1 claim window — is an absolute
|
|
1671
|
-
* timelock that passes whether or not a single event ever arrives.
|
|
1672
|
-
*/
|
|
1673
|
-
private subscribe;
|
|
1674
|
-
/**
|
|
1675
|
-
* Register this swap's lockup with the wallet's contract manager, once.
|
|
1676
|
-
*
|
|
1677
|
-
* The backstop, not the primary site: `requestLightningSend` /
|
|
1678
|
-
* `requestOnchainSend` register before the caller can fund, so this covers
|
|
1679
|
-
* swaps whose records predate that — and costs nothing when it does not,
|
|
1680
|
-
* since `createContract` is first-writer-wins.
|
|
1681
|
-
*
|
|
1682
|
-
* Best-effort by design: a failure here is reported and retried on the next
|
|
1683
|
-
* pass, and never aborts the pass it is part of. Registration buys latency
|
|
1684
|
-
* and puts the lockup in the wallet's contract set; it decides nothing. The
|
|
1685
|
-
* money path below it reads the indexer directly and is gated on timelocks
|
|
1686
|
-
* that a missing contract row has no bearing on, so failing the pass over
|
|
1687
|
-
* this would trade a real deadline for a bookkeeping one.
|
|
1688
|
-
*/
|
|
1689
|
-
private ensureRegistered;
|
|
1690
|
-
/** Stop watching a finished swap's lockup. Retained, not deleted: the row
|
|
1691
|
-
* is what keeps the lockup's own VTXOs annotatable and its history
|
|
1692
|
-
* readable, while `retained` is what drops it from the subscription and
|
|
1693
|
-
* the poll — a settled swap that stayed watched would cost the wallet a
|
|
1694
|
-
* script for its whole life. Best-effort — the swap is over either way. */
|
|
1695
|
-
private retireContract;
|
|
1696
|
-
private arm;
|
|
1697
|
-
private pollSwap;
|
|
1698
|
-
private runPass;
|
|
1699
|
-
/**
|
|
1700
|
-
* The receive leg's whole state machine: claim the solver-funded lockup
|
|
1701
|
-
* while the window is open, and recognise the shapes in which it can be
|
|
1702
|
-
* lost.
|
|
1703
|
-
*
|
|
1704
|
-
* **The window closes at `refundLocktime`, on wall clock, with no margin.**
|
|
1705
|
-
* Both halves of that are deliberate. It closes there because publishing
|
|
1706
|
-
* `P` into the solver's live refund window risks losing the race and
|
|
1707
|
-
* handing over the preimage anyway — the hazard `ONCHAIN_CLAIM_MARGIN_SECONDS`
|
|
1708
|
-
* guards on the L1 side. It takes no margin because the two situations are
|
|
1709
|
-
* not alike: that one budgets for confirmation depth, while this claim is an
|
|
1710
|
-
* offchain spend that lands in seconds. Wall clock is already the
|
|
1711
|
-
* conservative reading — the solver's leaf is a CLTV, which matures against
|
|
1712
|
-
* median-time-past, and MTP trails wall clock — so the real window extends
|
|
1713
|
-
* PAST this deadline rather than ending before it. Every second of margin
|
|
1714
|
-
* subtracted here is a second of live claim window given away for nothing.
|
|
1715
|
-
*
|
|
1716
|
-
* **The trader has no move after it.** Nothing here can take the lockup
|
|
1717
|
-
* back, so once the window shuts the swap is the solver's to resolve and
|
|
1718
|
-
* this manager's job is to watch it happen and then stop.
|
|
1719
|
-
*/
|
|
1720
|
-
private driveReceiveClaim;
|
|
1721
|
-
/**
|
|
1722
|
-
* Claim what the solver funded, once it is enough.
|
|
1723
|
-
*
|
|
1724
|
-
* The value gate here decides WHEN to act. `pushClaim`'s decides whether
|
|
1725
|
-
* `P` is published, and runs with nothing between it and the signature —
|
|
1726
|
-
* the check that matters is the inner one, and this is not a reason to
|
|
1727
|
-
* relax it.
|
|
1728
|
-
*/
|
|
1729
|
-
private claimIfFunded;
|
|
1730
|
-
/** `handled` ends the pass; `continue` falls through to the refund gate. */
|
|
1731
|
-
private driveOnchain;
|
|
1732
|
-
private driveArkadeRefund;
|
|
1733
|
-
/** Whether any of these outputs has never been handed to the claim
|
|
1734
|
-
* callback — the only reason to claim a lockup a second time. */
|
|
1735
|
-
private hasUnclaimedOutpoint;
|
|
1736
|
-
private rememberClaimed;
|
|
1737
|
-
/**
|
|
1738
|
-
* L1 progress, which past the refund window must not overwrite a refusal.
|
|
1739
|
-
* The two halves are independent — a claimed fill says nothing about
|
|
1740
|
-
* whether this wallet can take the Arkade lockup back — and `claimed` is
|
|
1741
|
-
* re-asserted from chain on every pass, so without this a blocked swap
|
|
1742
|
-
* would flip between the two states forever. The claim itself always runs;
|
|
1743
|
-
* only the label defers, and only once the refund is the live half.
|
|
1744
|
-
*/
|
|
1745
|
-
private setOnchainState;
|
|
1746
|
-
/** The probe's refusal reason, or `undefined` when a local refund is
|
|
1747
|
-
* possible as far as anyone here can tell. A probe that throws is treated
|
|
1748
|
-
* as a refusal: a capability check that cannot answer is not a yes. */
|
|
1749
|
-
private probeRefusal;
|
|
1750
|
-
/** Report that no local refund will happen, without ending the swap. */
|
|
1751
|
-
private block;
|
|
1752
|
-
/** The way back out, taken as soon as the swap becomes actionable again.
|
|
1753
|
-
* Back to what the record can prove, not to `pending` unconditionally: a
|
|
1754
|
-
* swap that already made its claim has a txid for it, and reporting that
|
|
1755
|
-
* swap as `pending` would un-say something true. */
|
|
1756
|
-
private unblock;
|
|
1757
|
-
private touch;
|
|
1758
|
-
private setState;
|
|
1759
|
-
/** Terminal failure. The `onSwapFailed` emission is left to
|
|
1760
|
-
* {@link finalize}, so this does not double-report. */
|
|
1761
|
-
private fail;
|
|
1762
|
-
private emitFailed;
|
|
1763
|
-
private emitAction;
|
|
1764
|
-
/** Whether the record is now persisted — false only when `saveSwap` threw. */
|
|
1765
|
-
private save;
|
|
1766
|
-
/**
|
|
1767
|
-
* Drop a terminal swap from monitoring and report it exactly once.
|
|
1768
|
-
*
|
|
1769
|
-
* `onSwapCompleted` and `onSwapFailed` are mutually exclusive here, unlike
|
|
1770
|
-
* Boltz's manager, which fires completion for every swap that leaves
|
|
1771
|
-
* monitoring including the failed ones — a listener named "completed" that
|
|
1772
|
-
* also fires on failure is a trap worth not inheriting.
|
|
1773
|
-
*/
|
|
1774
|
-
private finalize;
|
|
1775
|
-
private settleWaiters;
|
|
1776
|
-
}
|
|
1777
|
-
/** What {@link RfqSwapManager.waitForSwapCompletion} reports. `txid` is the
|
|
1778
|
-
* trader's own claim — L1 for a claimed onchain send, Arkade for a claimed
|
|
1779
|
-
* receive — or the ark txid for a refund the trader pushed; a solver-side
|
|
1780
|
-
* settlement or refund carries none, and a receive swap that ended `refunded`
|
|
1781
|
-
* carries none either, however far its claim got (see {@link outcomeOf}). So
|
|
1782
|
-
* a `txid` here always names something that happened, and `state` remains the
|
|
1783
|
-
* only thing to read for whether the swap paid out. */
|
|
1784
|
-
interface RfqSwapOutcome {
|
|
1785
|
-
state: RfqSwapState;
|
|
1786
|
-
txid?: string;
|
|
1787
|
-
}
|
|
918
|
+
declare function lockupContractParams(contracts: LockupContractReader, lockupAddress: string): Promise<Record<string, string>>;
|
|
1788
919
|
|
|
1789
920
|
/**
|
|
1790
921
|
* One swap, flattened to what grouping needs: an identity, a corridor, an
|
|
1791
922
|
* outcome, and every Arkade transaction that belongs to it.
|
|
1792
923
|
*
|
|
1793
924
|
* Deliberately not a stored swap record itself — resolution should stay
|
|
1794
|
-
* testable with plain data rather than a repository.
|
|
1795
|
-
*
|
|
1796
|
-
*
|
|
925
|
+
* testable with plain data rather than a repository. {@link rfqSwapActivityInputs}
|
|
926
|
+
* derives these from the record store and, where a record cannot answer, the
|
|
927
|
+
* funding lockup's VTXOs.
|
|
1797
928
|
*/
|
|
1798
929
|
interface SwapActivityInput {
|
|
1799
930
|
rfqId: string;
|
|
1800
|
-
|
|
1801
|
-
* Literal union rather than `RfqSwapRecord["kind"]` — `RfqSwapRecord` lives
|
|
1802
|
-
* only on the unmerged rfq-persistence branch, not on master. Reconcile
|
|
1803
|
-
* with `RfqSwapRecord["kind"]` once that branch lands.
|
|
1804
|
-
*/
|
|
1805
|
-
kind: "lightning_send" | "lightning_receive" | "onchain_send";
|
|
931
|
+
kind: RfqSwapRecord["kind"];
|
|
1806
932
|
state: RfqSwapState;
|
|
1807
933
|
/** Funding, claim and refund txids, in whatever order. */
|
|
1808
934
|
txids: readonly string[];
|
|
@@ -1824,5 +950,32 @@ interface SwapActivityInput {
|
|
|
1824
950
|
declare function swapActivityResolver(deps: {
|
|
1825
951
|
listSwaps(): Promise<readonly SwapActivityInput[]>;
|
|
1826
952
|
}): ActivityResolver;
|
|
953
|
+
interface RfqSwapActivityDeps {
|
|
954
|
+
repository: Pick<AssetSwapRepository, "getAllRfqSwaps">;
|
|
955
|
+
/**
|
|
956
|
+
* Consulted only for what a record cannot answer: a record written before
|
|
957
|
+
* `fundingArkTxid` existed, and the counterparty's spend on a swap that
|
|
958
|
+
* ended without a refund of ours.
|
|
959
|
+
*
|
|
960
|
+
* Optional because the stored fields are the primary source — cheaper, and
|
|
961
|
+
* they work offline, which is the resolver's whole posture. An indexer that
|
|
962
|
+
* throws costs that record its extra txids and nothing else.
|
|
963
|
+
*/
|
|
964
|
+
indexer?: LockupSpendIndexer;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Every stored RFQ swap, flattened into what {@link swapActivityResolver}
|
|
968
|
+
* groups on.
|
|
969
|
+
*
|
|
970
|
+
* The txids come from four places, in order of preference: the record's own
|
|
971
|
+
* `fundingArkTxid` and `refundArkTxid`, the corridor's `activityTxids` (the
|
|
972
|
+
* receive leg's Arkade claim, the onchain leg's L1 one), and — only when the
|
|
973
|
+
* first two cannot answer — one read of the lockup's VTXOs.
|
|
974
|
+
*
|
|
975
|
+
* A missing txid costs an activity a row, never a wrong one: a swap that
|
|
976
|
+
* contributes fewer txids simply leaves those transactions ungrouped, which is
|
|
977
|
+
* what they already are.
|
|
978
|
+
*/
|
|
979
|
+
declare function rfqSwapActivityInputs(deps: RfqSwapActivityDeps): Promise<SwapActivityInput[]>;
|
|
1827
980
|
|
|
1828
|
-
export { type
|
|
981
|
+
export { ArkadeRefundResult, type ArkadeRefunderDeps, AssetSwap, AssetSwapRepository, type ClaimArkProvider, type ClaimPacketInput, type DiscoverMarketsOptions, IndexedDbAssetSwapRepository, type LightningReceiveProfile, type LightningSendProfile, LockupAmountMismatchError, LockupContractMissing, type LockupContractReader, type LockupContractWriter, LockupRegistrationFailed, LockupSpendIndexer, LockupVtxo, MarketsCacheEntry, OFFER_PACKET_TYPE, type Offer, type OfferContractRetirer, type OfferSwapWatcher, OnchainHtlc, OnchainHtlcParams, OnchainNetwork, type OnchainSendProfile, type PlanError, QUOTE_OPTIONS, RefundArkProvider, type RefundBlockedReason, RefundIndexer, RefundNotLocallyPossibleError, type RestoreIndexer, type RfqClaimSecretProjection, type RfqHashlockProjection, type RfqSignerProjection, RfqSwap, type RfqSwapActivityDeps, RfqSwapRecord, RfqSwapState, SWAP_LOCKUP_CONTRACT_KIND, SWAP_LOCKUP_CONTRACT_LABEL, SWAP_LOCKUP_CONTRACT_TYPE, type SealedClaimPacket, type SpendKind, type SwapActivityInput, SwapSecretsProjection, type Tx, type WatchOfferSwapsParams, arkadeRefunder, awaitLockupFunding, cancelOffer, claimReceiveLockup, classifyDepositSpend, classifySpend, createOffer, decodeOffer, discoverMarkets, encodeOffer, findMarket, lockupContractParams, makeCachedFeedFetch, offerVtxoScript, onchainSendProfile, pushClaim, registerLockupContract, restoreAssetSwaps, retireSettledOfferContracts, rfqClaimSecretOf, rfqSecretsProfile, rfqSignerOf, rfqSwapActivityInputs, sealClaimPacket, senderIdentityForSwapRecord, spendTxidsOf, spendUpdate, swapActivityResolver, swapPrograms, validatePlan, watchOfferSwaps };
|