@arkade-os/swap 0.0.13 → 0.0.15
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 +71 -8
- package/dist/{chunk-IFZOBTTG.js → chunk-7N7XO4PY.js} +82 -3
- package/dist/index.cjs +808 -304
- package/dist/index.d.cts +192 -9
- package/dist/index.d.ts +192 -9
- package/dist/index.js +630 -208
- package/dist/nostr.cjs +80 -3
- package/dist/nostr.d.cts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +1 -1
- package/dist/repositories/realm/index.d.cts +2 -2
- package/dist/repositories/realm/index.d.ts +2 -2
- package/dist/repositories/sqlite/index.d.cts +2 -2
- package/dist/repositories/sqlite/index.d.ts +2 -2
- package/dist/{repository-D5JoeqwB.d.ts → repository-DH5zjdw4.d.ts} +1 -1
- package/dist/{repository-Vfx65uF1.d.cts → repository-rwU1Sv2-.d.cts} +1 -1
- package/dist/{rfq-CYfKvAPp.d.ts → rfq-DirJn_4k.d.cts} +20 -2
- package/dist/{rfq-CYfKvAPp.d.cts → rfq-DirJn_4k.d.ts} +20 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -85,10 +85,11 @@ funds an offer should keep cancelling within reach.
|
|
|
85
85
|
3. **`store`** — the persisted `AssetSwap` records (`getAssetSwaps`/`addAssetSwap`/
|
|
86
86
|
`updateAssetSwap`), thin helpers over an `AssetSwapRepository`. Read failures degrade to an
|
|
87
87
|
empty list; write failures throw so pre-funding records can be retried before money is sent.
|
|
88
|
-
4. **`restore`** — `
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
4. **`restore`** — `registerAssetSwapRestore` attaches durable swap recovery to an explicit
|
|
89
|
+
`wallet.restore()`. The underlying `restoreAssetSwapRepository` / `restoreAssetSwaps` scan sent
|
|
90
|
+
virtual txs for offer packets and bind each funding vtxo to its spend. The scan remains
|
|
91
|
+
available directly for ordinary startup and later reconciliation; answered txids are remembered
|
|
92
|
+
in the repository (`getScannedTxids`/`markTxidsScanned`) so nothing is fetched twice.
|
|
92
93
|
5. **`watch`** — `watchOfferSwaps` drives swap status from the wallet's own contract events, so a
|
|
93
94
|
fill shows up without re-running a scan. Registration is what makes it possible: only a
|
|
94
95
|
registered covenant is watched. See "Live status" below.
|
|
@@ -121,6 +122,56 @@ uncached discovery.
|
|
|
121
122
|
Neither subpath adds a dependency: they take the SDK's structural `SQLExecutor` / `RealmLike`
|
|
122
123
|
handles, so you pass the database you already opened.
|
|
123
124
|
|
|
125
|
+
### Restore an imported wallet
|
|
126
|
+
|
|
127
|
+
Register swap recovery before the application calls the core wallet's explicit `restore()`:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import {
|
|
131
|
+
IndexedDbAssetSwapRepository,
|
|
132
|
+
registerAssetSwapRestore,
|
|
133
|
+
} from "@arkade-os/swap";
|
|
134
|
+
|
|
135
|
+
const repository = new IndexedDbAssetSwapRepository();
|
|
136
|
+
const unregisterSwapRestore = registerAssetSwapRestore(wallet, {
|
|
137
|
+
arkServerUrl,
|
|
138
|
+
repository,
|
|
139
|
+
onResult: ({ changes, coverageError }) => {
|
|
140
|
+
if (coverageError) console.warn("Swap coverage was incomplete", coverageError);
|
|
141
|
+
console.info(`Restored or updated ${changes.length} swaps`);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await wallet.restore();
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Core address, contract, history, and balance recovery finishes before the swap scan starts. The
|
|
149
|
+
helper reads the recovered wallet history, normalizes it for the scan, rebuilds durable records,
|
|
150
|
+
and repairs covenant coverage. Registering it again on the same wallet replaces the prior
|
|
151
|
+
registration through the stable `arkade-os:asset-swap` hook ID, so setup is idempotent. Call
|
|
152
|
+
`unregisterSwapRestore()` when that integration no longer owns the wallet.
|
|
153
|
+
|
|
154
|
+
A direct `Wallet` exposes the indexer and current Ark server key the helper needs. A proxy or
|
|
155
|
+
custom `IWallet` that does not expose them must pass both explicitly:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
registerAssetSwapRestore(serviceWorkerWallet, {
|
|
159
|
+
arkServerUrl,
|
|
160
|
+
repository,
|
|
161
|
+
indexer,
|
|
162
|
+
serverPubkey,
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`coverageError` is result-level: records were already persisted, so the helper still delivers the
|
|
167
|
+
complete result to `onResult` and a later restore can retry coverage safely. Scan, persistence, or
|
|
168
|
+
`onResult` failures reject the hook; `wallet.restore()` reports hook failures through its
|
|
169
|
+
`AggregateError` after attempting the other registered hooks.
|
|
170
|
+
|
|
171
|
+
Keep calling `restoreAssetSwapRepository` directly during ordinary startup or when history may
|
|
172
|
+
arrive later. Hooks run only during an explicit `wallet.restore()`, and the repository cursor makes
|
|
173
|
+
the manual reconciliation idempotent against records the hook already rebuilt.
|
|
174
|
+
|
|
124
175
|
All four carry both record types: asset swaps and the monitored RFQ swaps
|
|
125
176
|
(`saveRfqSwap` / `getRfqSwap` / `getAllRfqSwaps` / `removeRfqSwap`). Each keeps them in a store of their own — a
|
|
126
177
|
second object store on IndexedDB, an `…rfq_swaps` table on SQLite, the `ArkadeRfqSwap` class on
|
|
@@ -319,7 +370,7 @@ message anywhere: **acceptance is funding**.
|
|
|
319
370
|
reference solver serves the Lightning pair today.
|
|
320
371
|
|
|
321
372
|
```ts
|
|
322
|
-
import { httpTransport, requestLightningSend } from "@arkade-os/swap";
|
|
373
|
+
import { httpTransport, requestLightningSend, SwapRefusal } from "@arkade-os/swap";
|
|
323
374
|
|
|
324
375
|
// invoice facts from YOUR OWN decoder — the module takes facts, not a decoder
|
|
325
376
|
const swap = await requestLightningSend(wallet, arkServerUrl, httpTransport(solverUrl), {
|
|
@@ -342,9 +393,21 @@ The trust model is the offer side's, applied to quotes: only `solver_pubkey`,
|
|
|
342
393
|
parameter is the trader's own data, and anything address-shaped from the solver is compare-only
|
|
343
394
|
(`AddressMismatch` means refuse-to-fund). The emulator key is neither: as above, it is a
|
|
344
395
|
per-network pin inside the SDK, not solver data.
|
|
345
|
-
Refusals carry a closed reason set (`SwapRefusal`)
|
|
346
|
-
`
|
|
347
|
-
|
|
396
|
+
Refusals carry a closed reason set (`SwapRefusal`). A solver may also return a recognised
|
|
397
|
+
`error_code` with client-safe context. The error exposes these as `errorCode`, `field`, `actual`,
|
|
398
|
+
`expected`, `limit`, and `unit`, and includes useful numeric context in its message. Match
|
|
399
|
+
`errorCode` for a specific remedy while treating `reason` as the compatible fallback:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
if (error instanceof SwapRefusal && error.errorCode === "invoice_cltv_too_large") {
|
|
403
|
+
console.error(`Invoice CLTV is ${error.actual} blocks; solver limit is ${error.limit}`);
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Unknown diagnostic codes and fields stay generic. `RFQ_REFUSAL_ERROR_CODES` and
|
|
408
|
+
`isRfqRefusalErrorCode` expose the accepted vocabulary. The `swap-lightning-send.program.json`
|
|
409
|
+
bytes are frozen the same way the offer programs are — a golden test pins the compiled leaves and
|
|
410
|
+
scriptPubKey to the reference solver's exact script.
|
|
348
411
|
|
|
349
412
|
Transports are symmetric-outbound: `httpTransport` (POST `/v1/swap`, GET `/v1/rfq/<rfq_id>`),
|
|
350
413
|
`relayTransport` (the dev broker framing), and `nostrRfqTransport` — the production one a
|
|
@@ -342,14 +342,78 @@ var LIGHTNING_RECEIVE_PAIR = rfqPair(LIGHTNING_BTC, ARKADE_BTC);
|
|
|
342
342
|
var ONCHAIN_SEND_PAIR = rfqPair(ARKADE_BTC, ONCHAIN_BTC);
|
|
343
343
|
var ONCHAIN_RECEIVE_PAIR = rfqPair(ONCHAIN_BTC, ARKADE_BTC);
|
|
344
344
|
var RFQ_TERMINAL_STATES = ["settled", "refused", "expired", "refunded", "stuck"];
|
|
345
|
+
var RFQ_REFUSAL_ERROR_CODES = [
|
|
346
|
+
"amount_side_unsupported",
|
|
347
|
+
"exact_out_unsupported",
|
|
348
|
+
"invalid_amount",
|
|
349
|
+
"invalid_payout_address",
|
|
350
|
+
"invalid_refund_address",
|
|
351
|
+
"invoice_amount_mismatch",
|
|
352
|
+
"invoice_cltv_too_large",
|
|
353
|
+
"invoice_malformed",
|
|
354
|
+
"invoice_missing_amount",
|
|
355
|
+
"invoice_missing_network",
|
|
356
|
+
"invoice_missing_payment_hash",
|
|
357
|
+
"invoice_missing_timestamp",
|
|
358
|
+
"invoice_mixed_case",
|
|
359
|
+
"invoice_sub_satoshi_amount",
|
|
360
|
+
"invoice_too_long",
|
|
361
|
+
"invoice_wrong_network"
|
|
362
|
+
];
|
|
363
|
+
var isRfqRefusalErrorCode = (value) => typeof value === "string" && RFQ_REFUSAL_ERROR_CODES.includes(value);
|
|
364
|
+
var REFUSAL_FIELDS = /* @__PURE__ */ new Set([
|
|
365
|
+
"amount",
|
|
366
|
+
"amount_side",
|
|
367
|
+
"profile.invoice",
|
|
368
|
+
"profile.payout_address",
|
|
369
|
+
"profile.refund_address"
|
|
370
|
+
]);
|
|
371
|
+
var REFUSAL_UNITS = /* @__PURE__ */ new Set(["blocks", "characters", "sats"]);
|
|
372
|
+
var safeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
373
|
+
var safeRefusalDetail = (detail) => {
|
|
374
|
+
if (!isRfqRefusalErrorCode(detail.errorCode)) return {};
|
|
375
|
+
return {
|
|
376
|
+
errorCode: detail.errorCode,
|
|
377
|
+
field: typeof detail.field === "string" && REFUSAL_FIELDS.has(detail.field) ? detail.field : void 0,
|
|
378
|
+
actual: safeInteger(detail.actual),
|
|
379
|
+
expected: safeInteger(detail.expected),
|
|
380
|
+
limit: safeInteger(detail.limit),
|
|
381
|
+
unit: typeof detail.unit === "string" && REFUSAL_UNITS.has(detail.unit) ? detail.unit : void 0
|
|
382
|
+
};
|
|
383
|
+
};
|
|
384
|
+
var refusalMessage = (reason, detail) => {
|
|
385
|
+
if (!detail.errorCode) return `solver refused: ${reason}`;
|
|
386
|
+
const where = detail.field ? ` at ${detail.field}` : "";
|
|
387
|
+
const unit = detail.unit ? ` ${detail.unit}` : "";
|
|
388
|
+
if (detail.actual !== void 0 && detail.limit !== void 0) {
|
|
389
|
+
return `solver refused: ${reason} (${detail.errorCode}${where}: ${detail.actual}${unit}, limit ${detail.limit})`;
|
|
390
|
+
}
|
|
391
|
+
if (detail.actual !== void 0 && detail.expected !== void 0) {
|
|
392
|
+
return `solver refused: ${reason} (${detail.errorCode}${where}: ${detail.actual}${unit}, expected ${detail.expected})`;
|
|
393
|
+
}
|
|
394
|
+
return `solver refused: ${reason} (${detail.errorCode}${where})`;
|
|
395
|
+
};
|
|
345
396
|
var SwapRefusal = class extends Error {
|
|
346
397
|
reason;
|
|
347
398
|
rfqId;
|
|
348
|
-
|
|
349
|
-
|
|
399
|
+
errorCode;
|
|
400
|
+
field;
|
|
401
|
+
actual;
|
|
402
|
+
expected;
|
|
403
|
+
limit;
|
|
404
|
+
unit;
|
|
405
|
+
constructor(reason, rfqId, detail = {}) {
|
|
406
|
+
const safeDetail = safeRefusalDetail(detail);
|
|
407
|
+
super(refusalMessage(reason, safeDetail));
|
|
350
408
|
this.name = "SwapRefusal";
|
|
351
409
|
this.reason = reason;
|
|
352
410
|
this.rfqId = rfqId;
|
|
411
|
+
this.errorCode = safeDetail.errorCode;
|
|
412
|
+
this.field = safeDetail.field;
|
|
413
|
+
this.actual = safeDetail.actual;
|
|
414
|
+
this.expected = safeDetail.expected;
|
|
415
|
+
this.limit = safeDetail.limit;
|
|
416
|
+
this.unit = safeDetail.unit;
|
|
353
417
|
}
|
|
354
418
|
};
|
|
355
419
|
var AddressMismatch = class extends Error {
|
|
@@ -517,7 +581,20 @@ var assertFundable = (input) => {
|
|
|
517
581
|
};
|
|
518
582
|
var expectQuote = (payload, rfqId, requestedPair) => {
|
|
519
583
|
const p = payload;
|
|
520
|
-
if (p?.type === "rfq_refusal")
|
|
584
|
+
if (p?.type === "rfq_refusal") {
|
|
585
|
+
throw new SwapRefusal(
|
|
586
|
+
p.reason ?? "unknown",
|
|
587
|
+
p.rfq_id ?? rfqId,
|
|
588
|
+
safeRefusalDetail({
|
|
589
|
+
errorCode: p.error_code,
|
|
590
|
+
field: p.field,
|
|
591
|
+
actual: p.actual,
|
|
592
|
+
expected: p.expected,
|
|
593
|
+
limit: p.limit,
|
|
594
|
+
unit: p.unit
|
|
595
|
+
})
|
|
596
|
+
);
|
|
597
|
+
}
|
|
521
598
|
if (p?.type !== "rfq_quote" || p.rfq_id !== rfqId) {
|
|
522
599
|
throw new Error(`unexpected reply: ${p?.type ?? "no payload"}`);
|
|
523
600
|
}
|
|
@@ -1301,6 +1378,8 @@ export {
|
|
|
1301
1378
|
ONCHAIN_SEND_PAIR,
|
|
1302
1379
|
ONCHAIN_RECEIVE_PAIR,
|
|
1303
1380
|
RFQ_TERMINAL_STATES,
|
|
1381
|
+
RFQ_REFUSAL_ERROR_CODES,
|
|
1382
|
+
isRfqRefusalErrorCode,
|
|
1304
1383
|
SwapRefusal,
|
|
1305
1384
|
AddressMismatch,
|
|
1306
1385
|
newRfqId,
|