@arkade-os/swap 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +271 -12
- package/dist/{chunk-WGRU2DBF.js → chunk-6ZUS47GA.js} +15 -1
- package/dist/{chunk-ZDTRQZE2.js → chunk-TU4NGZDP.js} +59 -22
- package/dist/index.cjs +1150 -427
- package/dist/index.d.cts +292 -1139
- package/dist/index.d.ts +292 -1139
- package/dist/index.js +759 -87
- package/dist/nostr.d.cts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +1 -1
- package/dist/repositories/realm/index.cjs +49 -5
- package/dist/repositories/realm/index.d.cts +28 -6
- package/dist/repositories/realm/index.d.ts +28 -6
- package/dist/repositories/realm/index.js +50 -6
- package/dist/repositories/sqlite/index.cjs +45 -4
- package/dist/repositories/sqlite/index.d.cts +20 -7
- package/dist/repositories/sqlite/index.d.ts +20 -7
- package/dist/repositories/sqlite/index.js +46 -5
- package/dist/repository-BcZ9LXRP.d.ts +1862 -0
- package/dist/repository-Dso34L4D.d.cts +1862 -0
- package/dist/{rfq-DfT9dAss.d.cts → rfq-C-aq5LDJ.d.cts} +63 -2
- package/dist/{rfq-DfT9dAss.d.ts → rfq-C-aq5LDJ.d.ts} +63 -2
- package/package.json +2 -2
- package/dist/repository-BwnZ8N62.d.cts +0 -236
- package/dist/repository-BwnZ8N62.d.ts +0 -236
package/README.md
CHANGED
|
@@ -116,13 +116,22 @@ uncached discovery.
|
|
|
116
116
|
Neither subpath adds a dependency: they take the SDK's structural `SQLExecutor` / `RealmLike`
|
|
117
117
|
handles, so you pass the database you already opened.
|
|
118
118
|
|
|
119
|
+
All four carry both record types: asset swaps and the monitored RFQ swaps
|
|
120
|
+
(`saveRfqSwap` / `getRfqSwap` / `getAllRfqSwaps` / `removeRfqSwap`). Each keeps them in a store of their own — a
|
|
121
|
+
second object store on IndexedDB, an `…rfq_swaps` table on SQLite, the `ArkadeRfqSwap` class on
|
|
122
|
+
Realm — since the two record types have different keys and no consumer wants them interleaved.
|
|
123
|
+
|
|
119
124
|
**Records are stored whole.** The SQLite and Realm backends serialize each record to **JSON** in a
|
|
120
|
-
`data` column, with only `status` / `createdAt`
|
|
121
|
-
know about survives, which is what the `quote`-shaped
|
|
125
|
+
`data` column, with only `status` / `createdAt` (and an RFQ record's `state` / `updatedAt`) mapped
|
|
126
|
+
out for querying — so a field they do not know about survives, which is what the `quote`-shaped
|
|
127
|
+
extension in `MIGRATION.md` relies on. It is also what keeps an RFQ record's corridor `profile`
|
|
128
|
+
intact: `profile.hashlock` is a nested object holding the payment hash and any preimage material, and
|
|
129
|
+
a field-mapped backend is exactly what would lose it. JSON is
|
|
122
130
|
the boundary, though, and it is narrower than IndexedDB's structured clone: a `Date` in a
|
|
123
131
|
consumer-added field comes back an ISO **string**, a `Set` or `Map` comes back empty, and a `bigint`
|
|
124
|
-
makes `saveSwap` **throw**. `AssetSwap`
|
|
125
|
-
|
|
132
|
+
makes `saveSwap` **throw**. `AssetSwap` and `RfqSwapRecord` are both JSON-safe by design (amounts are
|
|
133
|
+
strings, binary is hex), and a corridor `profile` is plain JSON by the handler contract; keep your own
|
|
134
|
+
added fields — and any corridor profile you write — that way too.
|
|
126
135
|
|
|
127
136
|
### SQLite
|
|
128
137
|
|
|
@@ -169,9 +178,15 @@ const realm = await Realm.open({
|
|
|
169
178
|
const swaps = new RealmAssetSwapRepository(realm);
|
|
170
179
|
```
|
|
171
180
|
|
|
172
|
-
|
|
173
|
-
`ArkadeAssetSwapMarketsCache`. Unlike SQLite there is no prefix option
|
|
174
|
-
baked into the schema objects you register — so reconcile against your own
|
|
181
|
+
Four classes land in your Realm namespace: `ArkadeAssetSwap`, `ArkadeRfqSwap`,
|
|
182
|
+
`ArkadeAssetSwapScannedTxid`, `ArkadeAssetSwapMarketsCache`. Unlike SQLite there is no prefix option
|
|
183
|
+
— a Realm schema name is baked into the schema objects you register — so reconcile against your own
|
|
184
|
+
models by name.
|
|
185
|
+
|
|
186
|
+
`ArkadeRfqSwap` arrived after the other three. **If you already shipped them, add it and bump
|
|
187
|
+
`schemaVersion` again**: Realm creates schemas at open, so a config still listing three fails on the
|
|
188
|
+
first RFQ read rather than at open. SQLite needs nothing — its DDL runs `CREATE TABLE IF NOT EXISTS`
|
|
189
|
+
on every init, so the table appears on the next operation.
|
|
175
190
|
|
|
176
191
|
## Creating an offer
|
|
177
192
|
|
|
@@ -558,6 +573,103 @@ terminal: the lockup stays funded and watched, a solver claim still ends the swa
|
|
|
558
573
|
`pending`. The manager reports the same state when nothing is wired to act (`enableAutoActions:
|
|
559
574
|
false`, or no callbacks) and the window has passed.
|
|
560
575
|
|
|
576
|
+
**The two claim callbacks may be omitted.** `setCallbacks` accepts
|
|
577
|
+
`AvailableRfqSwapManagerCallbacks` — the full contract with `claimOnchain` and `claimLockup`
|
|
578
|
+
optional — so a consumer driving only lightning sends installs neither instead of stubbing them to
|
|
579
|
+
throw. Dispatch is already kind-gated, so neither is reachable there. `saveSwap` is optional too
|
|
580
|
+
(see below); `refundArkade` stays required.
|
|
581
|
+
|
|
582
|
+
`RfqSwapManagerCallbacks` itself is unchanged and still means "fully wired", so a helper taking one
|
|
583
|
+
and calling `claimOnchain` keeps its guarantee; only the parameter widens, which every existing
|
|
584
|
+
caller satisfies. What moves from compile time to runtime is bought back as a **block**: a kind
|
|
585
|
+
whose claim is missing reports `needs_counterparty` naming the gap, non-terminal and re-evaluated
|
|
586
|
+
every pass, lifted the moment `setCallbacks` supplies it. Not `failed` — `setCallbacks` is
|
|
587
|
+
installable late by design, and a terminal state would foreclose the late wiring this exists for.
|
|
588
|
+
A manager with *no* callbacks at all keeps today's manual mode on the L1 half: it reports
|
|
589
|
+
`claimable` and you act by hand.
|
|
590
|
+
|
|
591
|
+
**Take `arkadeRefunder` rather than assembling `refundArkade` by hand.** It composes the atomic
|
|
592
|
+
push and keeps the three rules the manager relies on structural instead of documented — an empty
|
|
593
|
+
lockup returns `null`, and both `RefundNotLocallyPossibleError` and `LockupNeedsRecoveryError`
|
|
594
|
+
propagate untouched.
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
manager.setCallbacks({
|
|
598
|
+
// `repository` is how it reaches `profile.signer`: the live swap the manager
|
|
599
|
+
// passes carries no descriptor, so the refund key is resolved by `rfqId`.
|
|
600
|
+
refundArkade: arkadeRefunder({ ark, indexer, wallet, repository }),
|
|
601
|
+
saveSwap,
|
|
602
|
+
});
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Keep the covenant on the swap (`request*`'s `script`, as a record's `lockup`): the refund is built
|
|
606
|
+
from it, and a swap carrying only `lockupPkScript` is refused rather than pushed.
|
|
607
|
+
|
|
608
|
+
### Let the manager own the records
|
|
609
|
+
|
|
610
|
+
Give `RfqSwapManager` a `repository` and it persists RFQ swaps itself — the restore loop, the
|
|
611
|
+
retention pass and every write, none of which a consumer has to compose:
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
const manager = new RfqSwapManager({
|
|
615
|
+
indexer,
|
|
616
|
+
contracts: await wallet.getContractManager(),
|
|
617
|
+
repository, // any AssetSwapRepository
|
|
618
|
+
});
|
|
619
|
+
manager.setCallbacks({ refundArkade, claimLockup });
|
|
620
|
+
|
|
621
|
+
// Rebuild what was stored: retention first, then each record's covenant from
|
|
622
|
+
// its own contract row, then `rebuildRfqSwap`. No caller input at all.
|
|
623
|
+
const { restored, failed, pruned } = await manager.restoreFromRepository();
|
|
624
|
+
await manager.start();
|
|
625
|
+
|
|
626
|
+
// A NEW swap arrives with the request-time half a live record cannot carry.
|
|
627
|
+
await manager.addSwap(swap, {
|
|
628
|
+
kind: "lightning_send",
|
|
629
|
+
lockupAddress: request.lockupAddress,
|
|
630
|
+
profile: rfqSecretsProfile(secrets, paymentHash),
|
|
631
|
+
fundingArkTxid,
|
|
632
|
+
amount,
|
|
633
|
+
});
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
That second argument is the whole point. Composing the write by hand runs into an **origin trap**:
|
|
637
|
+
`updateRfqSwapRecord(record, swap)` needs the record that does not exist yet, and
|
|
638
|
+
`createRfqSwapRecord(origin, swap)` needs request-time facts the live swap never carried — so a
|
|
639
|
+
swap's *first* record cannot be built from the swap alone. `addSwap`'s `origin` is where those
|
|
640
|
+
facts arrive, and the manager keeps them for the swap's life. Omit it and one of two things
|
|
641
|
+
happens: the store already holds a record, which *is* the origin, and it is read back; or it does
|
|
642
|
+
not, and you get `RfqSwapOriginRequired` at the door rather than an unwritable record a pass later.
|
|
643
|
+
An origin whose `kind` or `lockupAddress` is not this swap's is refused at that same door, for the
|
|
644
|
+
same reason: the write that would catch it happens a pass later, with the funding broadcast.
|
|
645
|
+
`start(swaps)` applies the same rule and is otherwise unchanged. Restored swaps carry their own.
|
|
646
|
+
|
|
647
|
+
`restoreFromRepository` returns three disjoint lists, and every stored record is in exactly one.
|
|
648
|
+
A record that cannot be rebuilt — no contract row (`LockupContractMissing`), covenant params that
|
|
649
|
+
do not derive the funded address, a corridor with no handler — lands in `failed` with its error and
|
|
650
|
+
stays in the store; it never strands the others and it is never silently dropped. `pruned` names
|
|
651
|
+
what retention removed: terminal and more than `RFQ_SWAP_RETENTION_SECONDS` past `updatedAt`, never
|
|
652
|
+
`needs_counterparty`. Retention runs first, so a retired record costs no contract lookup on its way
|
|
653
|
+
out; `pruneRetiredSwaps()` is public for a process that wants it on its own cadence. Pass
|
|
654
|
+
`{ params }` to take covenants from somewhere other than the contract store.
|
|
655
|
+
|
|
656
|
+
**Two sinks, and both gate.** With a repository wired the canonical `RfqSwapRecord` is written
|
|
657
|
+
first, then `saveSwap` if one is installed, and the pass counts as persisted only when both
|
|
658
|
+
succeeded — which is exactly today's rule for `saveSwap`, applied to whichever sinks exist. A
|
|
659
|
+
rejection from either leaves the record dirty and monitored, so waiters stay unsettled and a
|
|
660
|
+
terminal swap is not finalized until the write it claims lands. A failed canonical write skips
|
|
661
|
+
`saveSwap` entirely: projecting a state the record of record has just refused would put the
|
|
662
|
+
secondary sink ahead of the primary. If your `saveSwap` writes that same repository by hand, delete
|
|
663
|
+
the duplicate when you wire the dep — otherwise every pass writes twice — and keep the callback for
|
|
664
|
+
genuinely secondary sinks. With neither wired, state stays in memory and dies with the process.
|
|
665
|
+
|
|
666
|
+
**Terminal records name the transaction that ended them.** `RfqSwap.lockupSpendArkTxids` is
|
|
667
|
+
stamped from the chain read that resolved the swap — the solver's claim on a send leg, its reclaim
|
|
668
|
+
on a receive one, the trader's own claim when a receive settles. Nothing local produces those
|
|
669
|
+
transactions, so no other field can name them, and without the stamp the only way to find them is
|
|
670
|
+
another lockup read per terminal swap. Absent when the indexer named the checkpoint but not the ark
|
|
671
|
+
transaction: fewer txids beats a wrong one.
|
|
672
|
+
|
|
561
673
|
`preimageForSwapRecord` is the read path to wire, not a hand-rolled `contractPreimage` call: it
|
|
562
674
|
knows which of the record's fields are derivation inputs, and it verifies the result against
|
|
563
675
|
`paymentHash`. A caller that forgets to pass the salt gets a _wrong_ preimage from a wallet that can
|
|
@@ -642,6 +754,62 @@ through their stored `preimageHex` or their HD descriptor, and `DB_VERSION` is u
|
|
|
642
754
|
|
|
643
755
|
Notes from before 0.0.1, kept for consumers who tracked the branch.
|
|
644
756
|
|
|
757
|
+
- **`RfqSwapManager` can own its own persistence.** New optional
|
|
758
|
+
`RfqSwapManagerDeps.repository`, new `restoreFromRepository()` and `pruneRetiredSwaps()`, and
|
|
759
|
+
`addSwap(swap, origin?)` gains an optional second argument. Nothing narrows and nothing is
|
|
760
|
+
removed, so no existing caller changes: without a repository the manager persists through
|
|
761
|
+
`saveSwap` exactly as before. Two things to know if you wire it. `saveSwap` becomes a **second**
|
|
762
|
+
sink rather than the only one — it still gates waiters and finalization, so its semantics are
|
|
763
|
+
unchanged, but a callback that writes the same repository by hand now double-writes and should
|
|
764
|
+
drop the duplicate. And `addSwap` for a swap the store has never seen throws
|
|
765
|
+
`RfqSwapOriginRequired` unless you pass its origin, which is the only way its first record can be
|
|
766
|
+
written at all.
|
|
767
|
+
- **`saveSwap` is optional at installation**, alongside the two claims — the same
|
|
768
|
+
`AvailableRfqSwapManagerCallbacks` relaxation extended one field. Omit it with a repository wired
|
|
769
|
+
and the record store is the only sink; omit both and state is process-local, which is what a
|
|
770
|
+
manager with no callbacks already did.
|
|
771
|
+
- **`RfqSwap` and `RfqSwapRecord` gained `lockupSpendArkTxids?: string[]`** — the ark transactions
|
|
772
|
+
that spent the lockup, stamped by the manager from the chain read that ended the swap. Optional
|
|
773
|
+
and additive: no repository version bump, and a backend storing records whole already carries it.
|
|
774
|
+
- **`RfqSwapState`, `RFQ_SWAP_TERMINAL_STATES` and `isRfqSwapTerminal` moved to
|
|
775
|
+
`src/rfqSwapState.ts`** so the record layer can read them without importing the manager at
|
|
776
|
+
runtime. `swapManager.ts` re-exports all three and the package entry point is unchanged, so no
|
|
777
|
+
import path breaks.
|
|
778
|
+
- **`rfqSwapOriginOf(record)` is new** — a record's immutable half on its own. A record *is* an
|
|
779
|
+
origin plus manager state, so passing one where an origin is wanted type-checks and quietly
|
|
780
|
+
carries the old `failure`, `blockedReason` and `refundArkTxid` past `managerState`, which can
|
|
781
|
+
only set those fields and never clear them. Use this instead of spreading the record.
|
|
782
|
+
- **`arkadeRefunder({ ark, indexer, wallet, repository })` ships the `refundArkade` wiring** that
|
|
783
|
+
was prose in two places. New export, nothing removed.
|
|
784
|
+
- **`rfqSwapActivityInputs({ repository, indexer })` derives `SwapActivityInput[]` from the record
|
|
785
|
+
store** — the correlation helper `activity.ts` promised. `SwapActivityInput["kind"]` is now
|
|
786
|
+
`RfqSwapRecord["kind"]` rather than a literal union repeating it; source-compatible. Corridor
|
|
787
|
+
handlers gained an optional `activityTxids(profile)` so a leg's own claim txid comes from the
|
|
788
|
+
handler instead of a kind switch. The `indexer` is optional and consulted only for what a record
|
|
789
|
+
cannot answer: a record predating `fundingArkTxid`, and the counterparty's spend on a swap no
|
|
790
|
+
refund of ours ended. An unreachable indexer costs that record its extra txids, never a throw.
|
|
791
|
+
- **`setCallbacks` takes `AvailableRfqSwapManagerCallbacks`** — `RfqSwapManagerCallbacks` with the
|
|
792
|
+
two kind-gated claims optional. Nothing breaks: the strict interface is untouched and the widened
|
|
793
|
+
parameter accepts every existing caller. A consumer driving one kind stops stubbing the claims it
|
|
794
|
+
cannot reach; in exchange, a missing claim blocks at runtime (`needs_counterparty`, non-terminal)
|
|
795
|
+
instead of being unrepresentable.
|
|
796
|
+
- **The repository interface is at version `4`.** It gained
|
|
797
|
+
`getRfqSwap(rfqId): Promise<RfqSwapRecord | undefined>` — every backend is already keyed by
|
|
798
|
+
`rfqId`, so a consumer updating one record no longer scans them all. A miss returns `undefined`;
|
|
799
|
+
retention prunes terminal records, so absence is ordinary. All four in-tree backends implement it
|
|
800
|
+
and `DB_VERSION` is unchanged; a custom implementor adds the two-line read and bumps its own
|
|
801
|
+
`version` to `4`.
|
|
802
|
+
- **`RfqSwapOrigin` gained `fundingArkTxid?`** — the ark transaction that funded the lockup. It is
|
|
803
|
+
origin, not manager state: the caller broadcasts the funding and knows the txid, while the manager
|
|
804
|
+
watches the lockup by script and never learns it. Optional and stored whole, so no migration.
|
|
805
|
+
Consumers stashing it in `profile` should move it: `profile` is merged as
|
|
806
|
+
`{ ...profile, ...handler.project(swap) }` on every write, so a key a corridor also projects is
|
|
807
|
+
silently overwritten.
|
|
808
|
+
- **`readLockupFate` names the spends it observed.** `claimed` and `returned` now carry
|
|
809
|
+
`spends: readonly LockupSpend[]`, one per spent lockup output, with the `checkpointTxid` that
|
|
810
|
+
`spentBy` names and the `arkTxid` that rode it. History correlation wants `arkTxid`; the
|
|
811
|
+
checkpoint txid is the wrong value to correlate on alone. `unknown` and `open` claim no spend.
|
|
812
|
+
|
|
645
813
|
- **Every derived address changed again, in both corridors — the unilateral ladder was re-spaced.**
|
|
646
814
|
`unilateralRefundDelay` now sits **level with** `claimDelay` instead of one 512s step above it,
|
|
647
815
|
and `unilateralRefundWithoutReceiverDelay` sits `SOLO_REFUND_HEADROOM_SECONDS` (4096s, newly
|
|
@@ -721,10 +889,100 @@ scanned? })` — the server key is required because a spend is classified by reb
|
|
|
721
889
|
- **A spend that cannot be classified is no longer restored as `fulfilled`.** It leaves the funding
|
|
722
890
|
txid unanswered so a later scan decides it. Records are never written on a guess.
|
|
723
891
|
- **`AssetSwap` gained `signingDescriptor?`**, and `preimageHex` now means "P that cannot be
|
|
724
|
-
re-derived" — caller-supplied, or minted for a static descriptor.
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
892
|
+
re-derived" — caller-supplied, or minted for a static descriptor. A field-mapped backend must
|
|
893
|
+
persist the record whole: silently dropping `preimageHex` leaves a static swap permanently
|
|
894
|
+
unclaimable.
|
|
895
|
+
- **The repository interface is at version `3`.** It gained `saveRfqSwap` / `getAllRfqSwaps` /
|
|
896
|
+
`removeRfqSwap` for monitored RFQ swaps, and the IndexedDB backend a matching `rfqSwaps` object
|
|
897
|
+
store at `DB_VERSION` 2. Version `2` was the shape 0.0.5 released — swaps, scan cursor, markets,
|
|
898
|
+
with `preimageSaltHex` on the swap record — and `DB_VERSION` was 1 there, so this is the database's
|
|
899
|
+
first version increase. The bump is deliberate: an implementor must acknowledge the new methods
|
|
900
|
+
rather than silently satisfy an older shape. Existing databases upgrade in place: the new store is
|
|
901
|
+
added and the three original ones are untouched. **`DB_VERSION` 2 is a one-way door** — a browser
|
|
902
|
+
whose database has upgraded cannot be rolled back to 0.0.5, which opens it at version 1 and fails
|
|
903
|
+
`VersionError` across the whole swap store, not just the RFQ half. Store RFQ records whole for the
|
|
904
|
+
same reason as above: what is in one is what nothing else can recover — the manager's own state,
|
|
905
|
+
and, inside the corridor's `profile`, its keys and its gates.
|
|
906
|
+
- **An RFQ record's keys live in its corridor's `profile`, under two keys.** `profile.signer` holds
|
|
907
|
+
`signingDescriptor` — which wallet key signs this leg, on any corridor. `profile.hashlock` holds
|
|
908
|
+
`paymentHash` (the covenant binds `hash160` of it, which is one-way) plus, **only on legs we
|
|
909
|
+
claim**, `preimageHex` or `preimageSaltHex`. The record's own half — `kind`, `lockupAddress`,
|
|
910
|
+
`amount`, the manager's state — recovers nothing on its own, so a backend that drops either nested
|
|
911
|
+
object loses the signer or the claim secret exactly as one dropping `preimageHex` used to. Two keys
|
|
912
|
+
rather than one because a hashlock belongs to a corridor and a signer does not: a corridor that
|
|
913
|
+
settles without a preimage still has a leg to sign and refund.
|
|
914
|
+
|
|
915
|
+
```ts
|
|
916
|
+
// In. One call per leg, whatever that leg's provisioning produced — never
|
|
917
|
+
// hand-mapped: copying `signingDescriptor` and `preimageHex` across by hand
|
|
918
|
+
// drops the salt a static wallet's P derives from, and the swap is
|
|
919
|
+
// unclaimable with nothing to say so until claim time.
|
|
920
|
+
const record = createRfqSwapRecord(
|
|
921
|
+
{
|
|
922
|
+
kind: "lightning_receive",
|
|
923
|
+
lockupAddress: result.address,
|
|
924
|
+
profile: {
|
|
925
|
+
...rfqSecretsProfile(result.secrets, result.treeParams.paymentHash),
|
|
926
|
+
expectedAmount: result.expectedAmount,
|
|
927
|
+
payoutAddress: result.payoutAddress,
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
swap,
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
// Out, and WHICH reader depends on the leg. The refund signer, on any leg:
|
|
934
|
+
const sender = await senderIdentityForSwapRecord(wallet, rfqSignerOf(record)!);
|
|
935
|
+
// P, only where we claim — `lightning_receive`, `onchain_send`:
|
|
936
|
+
const claim = rfqClaimSecretOf(record);
|
|
937
|
+
if (claim) await preimageForSwapRecord(wallet, claim); // hash-checked
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
- **`lightning_send` has a payment hash and no preimage**, so `rfqClaimSecretOf` answers `undefined`
|
|
941
|
+
for it. P belongs to the payee and its descriptor is a *refund* key from `provisionRefundKey`.
|
|
942
|
+
Wiring the claim helper to all three legs does not degrade gracefully: the salted arm derives
|
|
943
|
+
*some* P off the refund descriptor and the payment-hash check rejects it, so a correct record reads
|
|
944
|
+
as corrupt. That leg's reader is `rfqSignerOf`.
|
|
945
|
+
- **Non-hashlock corridors carry no `profile.hashlock` at all** — no `paymentHash`, no preimage
|
|
946
|
+
material, no placeholder; the key is simply absent, which is why `rfqSecretsProfile` takes the
|
|
947
|
+
payment hash as an optional second argument. They still write `profile.signer` if their leg is one
|
|
948
|
+
this wallet signs. The three corridors shipping today all lock to a preimage, but that is a fact
|
|
949
|
+
about them and not about RFQ. A corridor needing more than one descriptor — a co-signed leg, a
|
|
950
|
+
second key for an L1 half — extends `profile.signer` rather than fabricating a hashlock.
|
|
951
|
+
- **Both readers answer `undefined` only for "this corridor has no such half", and throw on a half
|
|
952
|
+
that is there and unusable.** Neither ever hands back a partial projection:
|
|
953
|
+
`preimageForSwapRecord` verifies only when the projection carries a `paymentHash`, so one missing
|
|
954
|
+
its hash would claim with an *unverified* preimage instead of failing. A thrown
|
|
955
|
+
`PreimageNotRecoverableError("malformed-record")` is a storage bug, not a protocol state — treating
|
|
956
|
+
it as "no preimage available" and falling back to a refund reads the two as the same thing.
|
|
957
|
+
- **An RFQ record stores no covenant.** The tree lives in the lockup's contract row, written before
|
|
958
|
+
the address could be funded and keyed by the script its params derive — a key `createContract`
|
|
959
|
+
refuses to write unless they reproduce it. So the rebuild takes the params from the caller:
|
|
960
|
+
|
|
961
|
+
```ts
|
|
962
|
+
const params = await lockupContractParams(
|
|
963
|
+
await wallet.getContractManager(),
|
|
964
|
+
record.lockupAddress,
|
|
965
|
+
);
|
|
966
|
+
const swap = rebuildRfqSwap(record, params);
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
`lockupContractParams` throws `LockupContractMissing` when this wallet has no row for the lockup —
|
|
970
|
+
a cleared contract store, or a record from elsewhere. A consumer that would rather not depend on
|
|
971
|
+
the contract store can keep its own copy of
|
|
972
|
+
`VHTLCV2ContractHandler.serializeParams(script.options)` and pass that instead; either way the
|
|
973
|
+
params are checked against the record's `lockupAddress` before a swap is handed back, so the wrong
|
|
974
|
+
row fails at restore rather than at refund time. **Superseded** for a consumer that wires
|
|
975
|
+
`RfqSwapManagerDeps.repository`: `restoreFromRepository()` is this loop, over every stored
|
|
976
|
+
record, with retention in front of it.
|
|
977
|
+
|
|
978
|
+
- **Pruning is the consumer's unless the manager holds the repository.** `shouldRetainRfqSwap(record,
|
|
979
|
+
now)` answers whether a record is still worth keeping — live swaps and `needs_counterparty`
|
|
980
|
+
always, terminal ones for `RFQ_SWAP_RETENTION_SECONDS` (30 days) after `updatedAt`. Sweep with it
|
|
981
|
+
at boot and pass the rejects to `removeRfqSwap`; skip it and a hot wallet's `rfqSwaps` store grows
|
|
982
|
+
without bound. `now` is **unix seconds**, the unit `RfqSwap.updatedAt` carries — `Date.now()` would
|
|
983
|
+
retire every terminal record after ~43 minutes. **Superseded** for a consumer that wires
|
|
984
|
+
`RfqSwapManagerDeps.repository`: `pruneRetiredSwaps()` is that sweep, and
|
|
985
|
+
`restoreFromRepository()` runs it first.
|
|
728
986
|
- **A write that gates something irreversible throws; one that follows it does not.**
|
|
729
987
|
`addAssetSwap` and `updateAssetSwap` throw on a failed read or write — nothing irreversible may
|
|
730
988
|
happen until the record is durable, which is why `cancelOffer` writes its `cancelling` marker
|
|
@@ -754,4 +1012,5 @@ scanned? })` — the server key is required because a spend is classified by reb
|
|
|
754
1012
|
receive swap monitored with nothing wired to claim it expires quietly, and a compile error is the
|
|
755
1013
|
right way to learn a corridor was added. A caller with only send swaps can satisfy it with a stub
|
|
756
1014
|
that throws. `RfqSwapActionName` gains `"claimLockup"`, so an exhaustive `switch` over it needs a
|
|
757
|
-
new arm.
|
|
1015
|
+
new arm. **Superseded:** such a caller now installs `AvailableRfqSwapManagerCallbacks` and omits
|
|
1016
|
+
both — see above.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// src/repository.ts
|
|
2
2
|
var marketsCacheKey = (network, registry) => `arkade-intents-markets-${network}-${registry}`;
|
|
3
3
|
var InMemoryAssetSwapRepository = class {
|
|
4
|
-
version =
|
|
4
|
+
version = 4;
|
|
5
5
|
swaps = /* @__PURE__ */ new Map();
|
|
6
|
+
rfqSwaps = /* @__PURE__ */ new Map();
|
|
6
7
|
scanned = /* @__PURE__ */ new Set();
|
|
7
8
|
markets = /* @__PURE__ */ new Map();
|
|
8
9
|
async saveSwap(swap) {
|
|
@@ -11,6 +12,18 @@ var InMemoryAssetSwapRepository = class {
|
|
|
11
12
|
async getAllSwaps() {
|
|
12
13
|
return [...this.swaps.values()];
|
|
13
14
|
}
|
|
15
|
+
async saveRfqSwap(record) {
|
|
16
|
+
this.rfqSwaps.set(record.rfqId, record);
|
|
17
|
+
}
|
|
18
|
+
async getRfqSwap(rfqId) {
|
|
19
|
+
return this.rfqSwaps.get(rfqId);
|
|
20
|
+
}
|
|
21
|
+
async getAllRfqSwaps() {
|
|
22
|
+
return [...this.rfqSwaps.values()];
|
|
23
|
+
}
|
|
24
|
+
async removeRfqSwap(rfqId) {
|
|
25
|
+
this.rfqSwaps.delete(rfqId);
|
|
26
|
+
}
|
|
14
27
|
async getScannedTxids() {
|
|
15
28
|
return new Set(this.scanned);
|
|
16
29
|
}
|
|
@@ -25,6 +38,7 @@ var InMemoryAssetSwapRepository = class {
|
|
|
25
38
|
}
|
|
26
39
|
async clear() {
|
|
27
40
|
this.swaps.clear();
|
|
41
|
+
this.rfqSwaps.clear();
|
|
28
42
|
this.scanned.clear();
|
|
29
43
|
this.markets.clear();
|
|
30
44
|
}
|
|
@@ -25,6 +25,11 @@ function onchainHtlcScript(params, network) {
|
|
|
25
25
|
`refundLocktime must be a positive unix timestamp, got ${params.refundLocktime}`
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
|
+
if (!Object.hasOwn(L1_NETWORKS, network)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`unknown L1 network '${String(network)}' \u2014 expected one of ${Object.keys(L1_NETWORKS).join(", ")}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
28
33
|
const h160 = h160FromPaymentHash(params.paymentHash);
|
|
29
34
|
const claim = btc.Script.encode([
|
|
30
35
|
"SIZE",
|
|
@@ -249,10 +254,27 @@ async function sealWithEntropy(input, ephemeralKey, nonce) {
|
|
|
249
254
|
|
|
250
255
|
// src/lockupContract.ts
|
|
251
256
|
import { hex as hex2 } from "@scure/base";
|
|
252
|
-
import {
|
|
257
|
+
import {
|
|
258
|
+
ArkAddress,
|
|
259
|
+
VHTLCV2ContractHandler
|
|
260
|
+
} from "@arkade-os/sdk";
|
|
253
261
|
var SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
|
|
254
262
|
var SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
|
|
255
263
|
var SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
|
|
264
|
+
var LockupContractMissing = class extends Error {
|
|
265
|
+
/** The lockup whose row is absent. */
|
|
266
|
+
address;
|
|
267
|
+
/** Its pkScript hex — the key the row would have been under. */
|
|
268
|
+
script;
|
|
269
|
+
constructor(address, script) {
|
|
270
|
+
super(
|
|
271
|
+
`no contract row for lockup ${address} (script ${script}); its covenant cannot be rebuilt from this wallet's contract store`
|
|
272
|
+
);
|
|
273
|
+
this.name = "LockupContractMissing";
|
|
274
|
+
this.address = address;
|
|
275
|
+
this.script = script;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
256
278
|
var LockupRegistrationFailed = class extends Error {
|
|
257
279
|
/** The lockup address that was never registered — never fund it: nothing
|
|
258
280
|
* is watching it. */
|
|
@@ -281,12 +303,18 @@ async function registerLockupContract(contracts, script, address) {
|
|
|
281
303
|
throw new LockupRegistrationFailed(script, address, error);
|
|
282
304
|
}
|
|
283
305
|
}
|
|
306
|
+
async function lockupContractParams(contracts, lockupAddress) {
|
|
307
|
+
const script = hex2.encode(ArkAddress.decode(lockupAddress).pkScript);
|
|
308
|
+
const [row] = await contracts.getContracts({ script });
|
|
309
|
+
if (!row) throw new LockupContractMissing(lockupAddress, script);
|
|
310
|
+
return row.params;
|
|
311
|
+
}
|
|
284
312
|
|
|
285
313
|
// src/rfq.ts
|
|
286
314
|
import { hex as hex3 } from "@scure/base";
|
|
287
315
|
import { ripemd160 as ripemd1602 } from "@noble/hashes/legacy.js";
|
|
288
316
|
import {
|
|
289
|
-
ArkAddress,
|
|
317
|
+
ArkAddress as ArkAddress2,
|
|
290
318
|
RestArkProvider,
|
|
291
319
|
VHTLC,
|
|
292
320
|
getNetwork,
|
|
@@ -649,7 +677,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
649
677
|
}
|
|
650
678
|
const serverPubkey = xOnly(hex3.decode(info.signerPubkey), "ark signer key");
|
|
651
679
|
const network = getNetwork(info.network);
|
|
652
|
-
const
|
|
680
|
+
const treeParams = {
|
|
653
681
|
solverPubkey: xOnly(hex3.decode(quote.solver_pubkey), "solver key"),
|
|
654
682
|
refundLocktime: quote.refund_locktime,
|
|
655
683
|
serverPubkey,
|
|
@@ -661,8 +689,9 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
661
689
|
),
|
|
662
690
|
senderPubkey,
|
|
663
691
|
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
664
|
-
refundPkScript:
|
|
665
|
-
}
|
|
692
|
+
refundPkScript: ArkAddress2.decode(refundAddress).pkScript
|
|
693
|
+
};
|
|
694
|
+
const script = lightningSendVtxoScript(treeParams);
|
|
666
695
|
const address = script.address(network.hrp, serverPubkey).encode();
|
|
667
696
|
verifyLockupAddress(quote, address);
|
|
668
697
|
assertFundable({
|
|
@@ -682,7 +711,8 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
682
711
|
script,
|
|
683
712
|
refundAddress,
|
|
684
713
|
senderPubkey,
|
|
685
|
-
secrets
|
|
714
|
+
secrets,
|
|
715
|
+
treeParams
|
|
686
716
|
};
|
|
687
717
|
}
|
|
688
718
|
var offerTermsFromQuote = (quote, assets) => {
|
|
@@ -756,25 +786,25 @@ function deriveOnchainSend(input) {
|
|
|
756
786
|
emulatorPubkey: input.emulatorPubkey,
|
|
757
787
|
senderPubkey: input.senderPubkey,
|
|
758
788
|
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
759
|
-
refundPkScript:
|
|
789
|
+
refundPkScript: ArkAddress2.decode(input.refundAddress).pkScript
|
|
760
790
|
});
|
|
761
791
|
const address = script.address(input.hrp, input.serverPubkey).encode();
|
|
762
792
|
verifyLockupAddress(quote, address);
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
input.l1Network
|
|
771
|
-
);
|
|
793
|
+
const htlcParams = {
|
|
794
|
+
paymentHash: input.paymentHash,
|
|
795
|
+
claimKey: input.payoutPubkey,
|
|
796
|
+
refundKey: xOnly(hex3.decode(htlcPubkey), "solver L1 htlc key"),
|
|
797
|
+
refundLocktime: htlcLocktime
|
|
798
|
+
};
|
|
799
|
+
const htlc = onchainHtlcScript(htlcParams, input.l1Network);
|
|
772
800
|
if (htlc.address !== htlcAddress) throw new AddressMismatch(htlc.address, htlcAddress);
|
|
773
801
|
return {
|
|
774
802
|
address,
|
|
775
803
|
swapPkScript: script.pkScript,
|
|
776
804
|
script,
|
|
777
805
|
htlc,
|
|
806
|
+
htlcParams,
|
|
807
|
+
l1Network: input.l1Network,
|
|
778
808
|
refundLocktime,
|
|
779
809
|
htlcLocktime,
|
|
780
810
|
minConfirmations
|
|
@@ -844,6 +874,9 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
844
874
|
script: derived.script,
|
|
845
875
|
refundAddress,
|
|
846
876
|
htlc: derived.htlc,
|
|
877
|
+
htlcParams: derived.htlcParams,
|
|
878
|
+
l1Network: derived.l1Network,
|
|
879
|
+
minConfirmations: derived.minConfirmations,
|
|
847
880
|
senderPubkey,
|
|
848
881
|
secrets
|
|
849
882
|
};
|
|
@@ -950,7 +983,7 @@ function deriveLightningReceive(input) {
|
|
|
950
983
|
if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
|
|
951
984
|
throw new Error("lightning-receive quote is missing a binding field");
|
|
952
985
|
}
|
|
953
|
-
const
|
|
986
|
+
const treeParams = {
|
|
954
987
|
solverPubkey: xOnly(hex3.decode(quote.solver_pubkey), "solver key"),
|
|
955
988
|
refundLocktime,
|
|
956
989
|
serverPubkey: input.serverPubkey,
|
|
@@ -959,11 +992,12 @@ function deriveLightningReceive(input) {
|
|
|
959
992
|
emulatorPubkey: input.emulatorPubkey,
|
|
960
993
|
solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
|
|
961
994
|
payoutPubkey: input.payoutPubkey,
|
|
962
|
-
payoutPkScript:
|
|
963
|
-
}
|
|
995
|
+
payoutPkScript: ArkAddress2.decode(input.payoutAddress).pkScript
|
|
996
|
+
};
|
|
997
|
+
const script = receiveVtxoScript(treeParams);
|
|
964
998
|
const address = script.address(input.hrp, input.serverPubkey).encode();
|
|
965
999
|
verifyLockupAddress(quote, address);
|
|
966
|
-
return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime };
|
|
1000
|
+
return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime, treeParams };
|
|
967
1001
|
}
|
|
968
1002
|
async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
|
|
969
1003
|
const rfqId = params.rfqId ?? newRfqId();
|
|
@@ -1035,7 +1069,8 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
1035
1069
|
script: derived.script,
|
|
1036
1070
|
payoutAddress,
|
|
1037
1071
|
payoutPubkey,
|
|
1038
|
-
secrets
|
|
1072
|
+
secrets,
|
|
1073
|
+
treeParams: derived.treeParams
|
|
1039
1074
|
};
|
|
1040
1075
|
}
|
|
1041
1076
|
function deriveOnchainReceive(input) {
|
|
@@ -1059,7 +1094,7 @@ function deriveOnchainReceive(input) {
|
|
|
1059
1094
|
emulatorPubkey: input.emulatorPubkey,
|
|
1060
1095
|
solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
|
|
1061
1096
|
payoutPubkey: input.payoutPubkey,
|
|
1062
|
-
payoutPkScript:
|
|
1097
|
+
payoutPkScript: ArkAddress2.decode(input.payoutAddress).pkScript
|
|
1063
1098
|
});
|
|
1064
1099
|
const address = script.address(input.hrp, input.serverPubkey).encode();
|
|
1065
1100
|
verifyLockupAddress(quote, address);
|
|
@@ -1179,8 +1214,10 @@ export {
|
|
|
1179
1214
|
SWAP_LOCKUP_CONTRACT_TYPE,
|
|
1180
1215
|
SWAP_LOCKUP_CONTRACT_LABEL,
|
|
1181
1216
|
SWAP_LOCKUP_CONTRACT_KIND,
|
|
1217
|
+
LockupContractMissing,
|
|
1182
1218
|
LockupRegistrationFailed,
|
|
1183
1219
|
registerLockupContract,
|
|
1220
|
+
lockupContractParams,
|
|
1184
1221
|
ARKADE_BTC,
|
|
1185
1222
|
LIGHTNING_BTC,
|
|
1186
1223
|
ONCHAIN_BTC,
|