@arkade-os/swap 0.1.0-rc.0 → 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -7
- package/dist/{chunk-AM3NNUMR.js → chunk-OEKSNYJW.js} +101 -31
- package/dist/{chunk-6ZUS47GA.js → chunk-U7VWWTL6.js} +15 -1
- package/dist/index.cjs +1277 -164
- package/dist/index.d.cts +528 -14
- package/dist/index.d.ts +528 -14
- package/dist/index.js +1121 -109
- package/dist/node/index.cjs +322 -0
- package/dist/node/index.d.cts +1055 -0
- package/dist/node/index.d.ts +1055 -0
- package/dist/node/index.js +295 -0
- 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 +50 -2
- package/dist/repositories/realm/index.d.cts +53 -8
- package/dist/repositories/realm/index.d.ts +53 -8
- package/dist/repositories/realm/index.js +49 -3
- package/dist/repositories/sqlite/index.cjs +42 -1
- package/dist/repositories/sqlite/index.d.cts +8 -3
- package/dist/repositories/sqlite/index.d.ts +8 -3
- package/dist/repositories/sqlite/index.js +43 -2
- package/dist/{repository-BWP1UstE.d.cts → repository-DoR-ahHc.d.ts} +1004 -32
- package/dist/{repository-DLpH41ZO.d.ts → repository-oeW8KZo1.d.cts} +1004 -32
- package/dist/{rfq-CzCGjICq.d.cts → rfq-D0KGjmnn.d.cts} +30 -3
- package/dist/{rfq-CzCGjICq.d.ts → rfq-D0KGjmnn.d.ts} +30 -3
- package/package.json +20 -5
package/README.md
CHANGED
|
@@ -144,9 +144,25 @@ uncached discovery.
|
|
|
144
144
|
| `IndexedDbAssetSwapRepository` | `@arkade-os/swap` | the browser (or a polyfilled IndexedDB) |
|
|
145
145
|
| `SQLiteAssetSwapRepository` | `@arkade-os/swap/repositories/sqlite` | React Native, over your SQLite driver |
|
|
146
146
|
| `RealmAssetSwapRepository` | `@arkade-os/swap/repositories/realm` | React Native, over your Realm instance |
|
|
147
|
+
| `nodeSwapRepository()` | `@arkade-os/swap/node` | Node — file-backed SQLite, opened for you |
|
|
147
148
|
|
|
148
|
-
Neither subpath adds a dependency: they take the SDK's structural `SQLExecutor` /
|
|
149
|
-
handles, so you pass the database you already opened.
|
|
149
|
+
Neither React Native subpath adds a dependency: they take the SDK's structural `SQLExecutor` /
|
|
150
|
+
`RealmLike` handles, so you pass the database you already opened.
|
|
151
|
+
|
|
152
|
+
`@arkade-os/swap/node` is the exception, and the only entry point that imports `node:` builtins —
|
|
153
|
+
which is why it is a separate subpath rather than something the main entry falls back to. It opens
|
|
154
|
+
the database itself, under the platform config directory (XDG / `~/Library/Application Support` /
|
|
155
|
+
`%APPDATA%`) at `arkade/swaps/swaps-<network>.sqlite`, and it is the one backend whose disposal
|
|
156
|
+
closes a connection:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { nodeSwapRepository } from "@arkade-os/swap/node";
|
|
160
|
+
|
|
161
|
+
await using swaps = nodeSwapRepository({ network: "mainnet" }); // or { path } to choose the file
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Every other backend's `[Symbol.asyncDispose]` is a no-op, because you opened the handle and it is
|
|
165
|
+
yours to close. This one opened it, so it closes it.
|
|
150
166
|
|
|
151
167
|
All four carry both record types: asset swaps and the monitored RFQ swaps
|
|
152
168
|
(`saveRfqSwap` / `getRfqSwap` / `getAllRfqSwaps` / `removeRfqSwap`). Each keeps them in a store of their own — a
|
|
@@ -210,15 +226,17 @@ const realm = await Realm.open({
|
|
|
210
226
|
const swaps = new RealmAssetSwapRepository(realm);
|
|
211
227
|
```
|
|
212
228
|
|
|
213
|
-
|
|
229
|
+
Five classes land in your Realm namespace: `ArkadeAssetSwap`, `ArkadeRfqSwap`, `ArkadeSwapRecord`,
|
|
214
230
|
`ArkadeAssetSwapScannedTxid`, `ArkadeAssetSwapMarketsCache`. Unlike SQLite there is no prefix option
|
|
215
231
|
— a Realm schema name is baked into the schema objects you register — so reconcile against your own
|
|
216
232
|
models by name.
|
|
217
233
|
|
|
218
|
-
`ArkadeRfqSwap` arrived after the
|
|
219
|
-
`schemaVersion` again**: Realm creates schemas at open, so a
|
|
220
|
-
|
|
221
|
-
|
|
234
|
+
`ArkadeRfqSwap` and `ArkadeSwapRecord` arrived after the first three. **If you already shipped an
|
|
235
|
+
earlier set, add the new ones and bump `schemaVersion` again**: Realm creates schemas at open, so a
|
|
236
|
+
config listing fewer fails on the first read of the missing one rather than at open. Spreading
|
|
237
|
+
`AssetSwapRealmSchemas` rather than listing names by hand is what keeps that from happening again.
|
|
238
|
+
SQLite needs nothing — its DDL runs `CREATE TABLE IF NOT EXISTS` on every init, so a new table
|
|
239
|
+
appears on the next operation.
|
|
222
240
|
|
|
223
241
|
## Creating an offer
|
|
224
242
|
|
|
@@ -9,6 +9,7 @@ var MAX_MIN_CONFIRMATIONS = 6;
|
|
|
9
9
|
var LOCKTIME_THRESHOLD = 5e8;
|
|
10
10
|
var ONCHAIN_SECONDS_PER_BLOCK = 600;
|
|
11
11
|
var ONCHAIN_DUST_SATS = BigInt(330);
|
|
12
|
+
var ONCHAIN_CLAIM_VSIZE = 152;
|
|
12
13
|
var newPreimage = () => crypto.getRandomValues(new Uint8Array(32));
|
|
13
14
|
var paymentHashOf = (preimage) => hex.encode(sha256(preimage));
|
|
14
15
|
var h160FromPaymentHash = (paymentHash) => ripemd160(hex.decode(paymentHash));
|
|
@@ -17,6 +18,7 @@ var L1_NETWORKS = {
|
|
|
17
18
|
testnet: btc.TEST_NETWORK,
|
|
18
19
|
regtest: { ...btc.TEST_NETWORK, bech32: "bcrt" }
|
|
19
20
|
};
|
|
21
|
+
var l1ScriptForAddress = (address, network) => btc.OutScript.encode(btc.Address(L1_NETWORKS[network]).decode(address));
|
|
20
22
|
function onchainHtlcScript(params, network) {
|
|
21
23
|
if (params.claimKey.length !== 32 || params.refundKey.length !== 32) {
|
|
22
24
|
throw new Error("claimKey and refundKey must be 32-byte x-only keys");
|
|
@@ -199,7 +201,11 @@ async function classifyOnchainHtlc(chain, input) {
|
|
|
199
201
|
const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
|
|
200
202
|
if (!best) {
|
|
201
203
|
if (!input.funding) return { phase: "unfunded" };
|
|
202
|
-
const spend = await chain.getSpendingTx(
|
|
204
|
+
const spend = await chain.getSpendingTx(
|
|
205
|
+
input.funding.txid,
|
|
206
|
+
input.funding.vout,
|
|
207
|
+
input.htlc.pkScript
|
|
208
|
+
);
|
|
203
209
|
if (!spend) return { phase: "unfunded" };
|
|
204
210
|
const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
|
|
205
211
|
const txid = btc.Transaction.fromRaw(hex.decode(spend.txHex), {
|
|
@@ -445,6 +451,10 @@ var assertFundable = (input) => {
|
|
|
445
451
|
const fail = (reason, message) => {
|
|
446
452
|
throw gateError(reason, message);
|
|
447
453
|
};
|
|
454
|
+
if (input.quote.valid_until === void 0) {
|
|
455
|
+
fail("quote_malformed", "quote carries no valid_until");
|
|
456
|
+
}
|
|
457
|
+
assertFinite(input.quote.valid_until, "quote_malformed", "quote valid_until");
|
|
448
458
|
if (input.invoiceExpiresAt !== void 0 && input.now >= input.invoiceExpiresAt) {
|
|
449
459
|
fail("invoice_expired", "invoice expired");
|
|
450
460
|
}
|
|
@@ -453,6 +463,43 @@ var assertFundable = (input) => {
|
|
|
453
463
|
if (input.quote.refund_locktime !== void 0 && input.quote.refund_locktime - input.now < MIN_HEADROOM_SECONDS) {
|
|
454
464
|
fail("insufficient_headroom", "refund deadline headroom below 90 minutes");
|
|
455
465
|
}
|
|
466
|
+
if (input.maxFee) {
|
|
467
|
+
const { bps, sats, referenceRate } = input.maxFee;
|
|
468
|
+
if (bps === void 0 && sats === void 0) {
|
|
469
|
+
fail("max_fee_unbounded", "maxFee names neither bps nor sats");
|
|
470
|
+
}
|
|
471
|
+
if (bps !== void 0 && (!Number.isInteger(bps) || bps < 0 || bps > 1e4)) {
|
|
472
|
+
fail("max_fee_out_of_range", `maxFee.bps must be an integer in 0..10000, got ${bps}`);
|
|
473
|
+
}
|
|
474
|
+
if (sats !== void 0 && (!Number.isInteger(sats) || sats < 0)) {
|
|
475
|
+
fail("max_fee_out_of_range", `maxFee.sats must be a non-negative integer, got ${sats}`);
|
|
476
|
+
}
|
|
477
|
+
const legs = input.quote.pair.split("->");
|
|
478
|
+
const assetOf = (leg) => leg.slice(leg.indexOf(":") + 1);
|
|
479
|
+
const sameAsset = legs.length === 2 && assetOf(legs[0]) === assetOf(legs[1]);
|
|
480
|
+
if (!sameAsset && referenceRate === void 0) {
|
|
481
|
+
fail(
|
|
482
|
+
"fee_gate_unavailable",
|
|
483
|
+
`maxFee cannot gate ${input.quote.pair}: its legs name different assets, so from_amount - to_amount is not a fee. Supply maxFee.referenceRate (to-units per from-unit) from a source of your OWN \u2014 reading it off the solver's published feed would check the solver against its own number`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (!sameAsset && (!Number.isFinite(referenceRate) || referenceRate <= 0)) {
|
|
487
|
+
fail(
|
|
488
|
+
"max_fee_out_of_range",
|
|
489
|
+
`maxFee.referenceRate must be a positive finite number, got ${referenceRate}`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const fee = sameAsset ? input.quote.from_amount - input.quote.to_amount : Math.ceil(
|
|
493
|
+
(input.quote.from_amount * referenceRate - input.quote.to_amount) / referenceRate
|
|
494
|
+
);
|
|
495
|
+
const allowed = Math.max(
|
|
496
|
+
sats ?? 0,
|
|
497
|
+
Math.floor(input.quote.from_amount * (bps ?? 0) / 1e4)
|
|
498
|
+
);
|
|
499
|
+
if (fee > allowed) {
|
|
500
|
+
fail("fee_too_high", `fee ${fee} exceeds the ${allowed} this client allows`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
456
503
|
if (input.onchain) {
|
|
457
504
|
const { htlcLocktime, minConfirmations, direction } = input.onchain;
|
|
458
505
|
if (!Number.isInteger(minConfirmations) || minConfirmations < 1 || minConfirmations > MAX_MIN_CONFIRMATIONS) {
|
|
@@ -649,6 +696,43 @@ function lightningSendContract(params) {
|
|
|
649
696
|
}
|
|
650
697
|
});
|
|
651
698
|
}
|
|
699
|
+
function deriveLightningSend(input) {
|
|
700
|
+
const { quote } = input;
|
|
701
|
+
if (quote.refund_locktime === void 0) {
|
|
702
|
+
throw new Error("lightning-send quote is missing refund_locktime");
|
|
703
|
+
}
|
|
704
|
+
const receiverPkScriptHex = quote.profile?.receiver_pk_script;
|
|
705
|
+
if (receiverPkScriptHex === void 0) {
|
|
706
|
+
throw new Error("lightning-send quote is missing profile.receiver_pk_script");
|
|
707
|
+
}
|
|
708
|
+
const contractParams = {
|
|
709
|
+
solverPubkey: toXOnly(hex3.decode(quote.solver_pubkey), "solver key"),
|
|
710
|
+
refundLocktime: quote.refund_locktime,
|
|
711
|
+
operatorPubkey: input.operatorPubkey,
|
|
712
|
+
paymentHash: input.paymentHash,
|
|
713
|
+
claimDelay: input.claimDelay,
|
|
714
|
+
emulatorPubkey: input.emulatorPubkey,
|
|
715
|
+
senderPubkey: input.senderPubkey,
|
|
716
|
+
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
717
|
+
refundPkScript: input.refundPkScript
|
|
718
|
+
};
|
|
719
|
+
const matched = matchQuotedLockup(
|
|
720
|
+
quote,
|
|
721
|
+
input.hrp,
|
|
722
|
+
input.operatorPubkey,
|
|
723
|
+
(legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
|
|
724
|
+
);
|
|
725
|
+
return {
|
|
726
|
+
address: matched.address,
|
|
727
|
+
swapPkScript: matched.script.pkScript,
|
|
728
|
+
script: matched.script,
|
|
729
|
+
contractParams: {
|
|
730
|
+
...contractParams,
|
|
731
|
+
...matched.legacy !== void 0 && { legacy: matched.legacy }
|
|
732
|
+
},
|
|
733
|
+
refundLocktime: quote.refund_locktime
|
|
734
|
+
};
|
|
735
|
+
}
|
|
652
736
|
async function requestLightningSend(wallet, transport, params) {
|
|
653
737
|
const rfqId = params.rfqId ?? newRfqId();
|
|
654
738
|
const secrets = await provisionRefundKey(wallet);
|
|
@@ -658,13 +742,6 @@ async function requestLightningSend(wallet, transport, params) {
|
|
|
658
742
|
const quote = await transport.requestQuote(
|
|
659
743
|
lightningSendRequest({ rfqId, invoice: params.invoice.raw, refundAddress, senderPubkey })
|
|
660
744
|
);
|
|
661
|
-
if (quote.refund_locktime === void 0) {
|
|
662
|
-
throw new Error("lightning-send quote is missing refund_locktime");
|
|
663
|
-
}
|
|
664
|
-
const receiverPkScriptHex = quote.profile?.receiver_pk_script;
|
|
665
|
-
if (receiverPkScriptHex === void 0) {
|
|
666
|
-
throw new Error("lightning-send quote is missing profile.receiver_pk_script");
|
|
667
|
-
}
|
|
668
745
|
if (quote.to_amount !== params.invoice.amountSats) {
|
|
669
746
|
throw new Error(
|
|
670
747
|
`quote to_amount ${quote.to_amount} does not match the invoice's ${params.invoice.amountSats}`
|
|
@@ -677,32 +754,20 @@ async function requestLightningSend(wallet, transport, params) {
|
|
|
677
754
|
}
|
|
678
755
|
const operatorPubkey = toXOnly(hex3.decode(info.signerPubkey), "ark signer key");
|
|
679
756
|
const network = networkFromArkadeInfo(info);
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
refundLocktime: quote.refund_locktime,
|
|
683
|
-
operatorPubkey,
|
|
757
|
+
const derived = deriveLightningSend({
|
|
758
|
+
quote,
|
|
684
759
|
paymentHash: params.invoice.paymentHash,
|
|
685
|
-
|
|
760
|
+
senderPubkey,
|
|
761
|
+
refundPkScript: secrets.pkScript,
|
|
762
|
+
operatorPubkey,
|
|
686
763
|
emulatorPubkey: toXOnly(
|
|
687
764
|
hex3.decode(resolveEmulatorPubkey(network, params.emulatorPubkey)),
|
|
688
765
|
"emulator signer key"
|
|
689
766
|
),
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
};
|
|
694
|
-
const matched = matchQuotedLockup(
|
|
695
|
-
quote,
|
|
696
|
-
network.hrp,
|
|
697
|
-
operatorPubkey,
|
|
698
|
-
(legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
|
|
699
|
-
);
|
|
700
|
-
const script = matched.script;
|
|
701
|
-
const address = matched.address;
|
|
702
|
-
const matchedContractParams = {
|
|
703
|
-
...contractParams,
|
|
704
|
-
...matched.legacy !== void 0 && { legacy: matched.legacy }
|
|
705
|
-
};
|
|
767
|
+
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
768
|
+
hrp: network.hrp
|
|
769
|
+
});
|
|
770
|
+
const { address, script, contractParams } = derived;
|
|
706
771
|
assertFundable({
|
|
707
772
|
quote,
|
|
708
773
|
invoiceExpiresAt: params.invoice.expiresAt,
|
|
@@ -716,12 +781,12 @@ async function requestLightningSend(wallet, transport, params) {
|
|
|
716
781
|
// What the lockup must carry: the quote's `from_amount` — the invoice
|
|
717
782
|
// PLUS the corridor's fee, never the bare invoice amount.
|
|
718
783
|
fundAmount: quote.from_amount,
|
|
719
|
-
swapPkScript:
|
|
784
|
+
swapPkScript: derived.swapPkScript,
|
|
720
785
|
script,
|
|
721
786
|
refundAddress,
|
|
722
787
|
senderPubkey,
|
|
723
788
|
secrets,
|
|
724
|
-
contractParams
|
|
789
|
+
contractParams
|
|
725
790
|
};
|
|
726
791
|
}
|
|
727
792
|
var offerTermsFromQuote = (quote, assets) => {
|
|
@@ -848,6 +913,7 @@ async function requestOnchainSend(wallet, transport, params) {
|
|
|
848
913
|
amountSide: params.amountSide
|
|
849
914
|
})
|
|
850
915
|
);
|
|
916
|
+
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
851
917
|
const network = networkFromArkadeInfo(info);
|
|
852
918
|
const derived = deriveOnchainSend({
|
|
853
919
|
quote,
|
|
@@ -1234,8 +1300,11 @@ export {
|
|
|
1234
1300
|
LOCKTIME_THRESHOLD,
|
|
1235
1301
|
ONCHAIN_SECONDS_PER_BLOCK,
|
|
1236
1302
|
ONCHAIN_DUST_SATS,
|
|
1303
|
+
ONCHAIN_CLAIM_VSIZE,
|
|
1237
1304
|
newPreimage,
|
|
1238
1305
|
paymentHashOf,
|
|
1306
|
+
L1_NETWORKS,
|
|
1307
|
+
l1ScriptForAddress,
|
|
1239
1308
|
onchainHtlcScript,
|
|
1240
1309
|
buildHtlcClaim,
|
|
1241
1310
|
buildHtlcRefund,
|
|
@@ -1281,6 +1350,7 @@ export {
|
|
|
1281
1350
|
lightningSendContract,
|
|
1282
1351
|
requestLightningSend,
|
|
1283
1352
|
offerTermsFromQuote,
|
|
1353
|
+
l1NetworkFromArk,
|
|
1284
1354
|
onchainSendRequest,
|
|
1285
1355
|
lightningReceiveRequest,
|
|
1286
1356
|
onchainReceiveRequest,
|
|
@@ -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 = 5;
|
|
5
5
|
swaps = /* @__PURE__ */ new Map();
|
|
6
|
+
records = /* @__PURE__ */ new Map();
|
|
6
7
|
rfqSwaps = /* @__PURE__ */ new Map();
|
|
7
8
|
scanned = /* @__PURE__ */ new Set();
|
|
8
9
|
markets = /* @__PURE__ */ new Map();
|
|
@@ -24,6 +25,18 @@ var InMemoryAssetSwapRepository = class {
|
|
|
24
25
|
async removeRfqSwap(rfqId) {
|
|
25
26
|
this.rfqSwaps.delete(rfqId);
|
|
26
27
|
}
|
|
28
|
+
async saveSwapRecord(record) {
|
|
29
|
+
this.records.set(record.id, record);
|
|
30
|
+
}
|
|
31
|
+
async getSwapRecord(id) {
|
|
32
|
+
return this.records.get(id);
|
|
33
|
+
}
|
|
34
|
+
async getAllSwapRecords() {
|
|
35
|
+
return [...this.records.values()];
|
|
36
|
+
}
|
|
37
|
+
async removeSwapRecord(id) {
|
|
38
|
+
this.records.delete(id);
|
|
39
|
+
}
|
|
27
40
|
async getScannedTxids() {
|
|
28
41
|
return new Set(this.scanned);
|
|
29
42
|
}
|
|
@@ -39,6 +52,7 @@ var InMemoryAssetSwapRepository = class {
|
|
|
39
52
|
async clear() {
|
|
40
53
|
this.swaps.clear();
|
|
41
54
|
this.rfqSwaps.clear();
|
|
55
|
+
this.records.clear();
|
|
42
56
|
this.scanned.clear();
|
|
43
57
|
this.markets.clear();
|
|
44
58
|
}
|