@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.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
LIGHTNING_BTC,
|
|
6
6
|
LIGHTNING_RECEIVE_PAIR,
|
|
7
7
|
LIGHTNING_SEND_PAIR,
|
|
8
|
+
LockupContractMissing,
|
|
8
9
|
LockupRegistrationFailed,
|
|
9
10
|
MAX_MIN_CONFIRMATIONS,
|
|
10
11
|
MIN_CLAIM_WINDOW_SECONDS,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
SWAP_LOCKUP_CONTRACT_LABEL,
|
|
23
24
|
SWAP_LOCKUP_CONTRACT_TYPE,
|
|
24
25
|
SwapRefusal,
|
|
26
|
+
arkadeAssetLeg,
|
|
25
27
|
arkadeSwapRequest,
|
|
26
28
|
assertFundable,
|
|
27
29
|
assertReceivable,
|
|
@@ -38,6 +40,7 @@ import {
|
|
|
38
40
|
lightningReceiveRequest,
|
|
39
41
|
lightningSendRequest,
|
|
40
42
|
lightningSendVtxoScript,
|
|
43
|
+
lockupContractParams,
|
|
41
44
|
newPreimage,
|
|
42
45
|
newRfqId,
|
|
43
46
|
offerTermsFromQuote,
|
|
@@ -59,11 +62,11 @@ import {
|
|
|
59
62
|
unilateralRefundWithoutReceiverDelay,
|
|
60
63
|
verifyLockupAddress,
|
|
61
64
|
verifyReceiveInvoice
|
|
62
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-TU4NGZDP.js";
|
|
63
66
|
import {
|
|
64
67
|
InMemoryAssetSwapRepository,
|
|
65
68
|
marketsCacheKey
|
|
66
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-6ZUS47GA.js";
|
|
67
70
|
|
|
68
71
|
// src/offer.ts
|
|
69
72
|
import { hex as hex2 } from "@scure/base";
|
|
@@ -695,60 +698,50 @@ var validatePlan = (plan, giveBalance, dust) => {
|
|
|
695
698
|
};
|
|
696
699
|
|
|
697
700
|
// src/indexedDbRepository.ts
|
|
698
|
-
import {
|
|
701
|
+
import {
|
|
702
|
+
awaitTransaction,
|
|
703
|
+
createManagedConnection,
|
|
704
|
+
promisifyRequest
|
|
705
|
+
} from "@arkade-os/sdk";
|
|
699
706
|
var DEFAULT_DB_NAME = "arkade-intents";
|
|
700
|
-
var DB_VERSION =
|
|
707
|
+
var DB_VERSION = 2;
|
|
701
708
|
var STORE_SWAPS = "swaps";
|
|
709
|
+
var STORE_RFQ_SWAPS = "rfqSwaps";
|
|
702
710
|
var STORE_SCANNED = "scannedTxids";
|
|
703
711
|
var STORE_MARKETS = "markets";
|
|
704
712
|
var STORES = [
|
|
705
713
|
[STORE_SWAPS, { keyPath: "id" }],
|
|
714
|
+
// v2. Separate from `swaps` rather than sharing it: the two record types
|
|
715
|
+
// have different keys and no consumer wants them interleaved.
|
|
716
|
+
[STORE_RFQ_SWAPS, { keyPath: "rfqId" }],
|
|
706
717
|
[STORE_SCANNED],
|
|
707
718
|
[STORE_MARKETS]
|
|
708
719
|
];
|
|
709
|
-
function initDatabase(db) {
|
|
720
|
+
function initDatabase(db, oldVersion, transaction) {
|
|
721
|
+
void oldVersion;
|
|
722
|
+
void transaction;
|
|
710
723
|
for (const [name, options] of STORES) {
|
|
711
724
|
if (!db.objectStoreNames.contains(name)) db.createObjectStore(name, options);
|
|
712
725
|
}
|
|
713
726
|
}
|
|
714
|
-
var request = (req) => new Promise((resolve, reject) => {
|
|
715
|
-
req.onsuccess = () => resolve(req.result);
|
|
716
|
-
req.onerror = () => reject(req.error);
|
|
717
|
-
});
|
|
718
|
-
var txDone = (tx) => new Promise((resolve, reject) => {
|
|
719
|
-
tx.oncomplete = () => resolve();
|
|
720
|
-
tx.onerror = () => reject(tx.error);
|
|
721
|
-
tx.onabort = () => reject(tx.error);
|
|
722
|
-
});
|
|
723
727
|
var IndexedDbAssetSwapRepository = class {
|
|
728
|
+
version = 4;
|
|
729
|
+
connection;
|
|
724
730
|
constructor(dbName = DEFAULT_DB_NAME) {
|
|
725
|
-
this.
|
|
726
|
-
}
|
|
727
|
-
dbName;
|
|
728
|
-
version = 2;
|
|
729
|
-
// the promise, not the resolved database: openDatabase bumps a refcount on
|
|
730
|
-
// every call including cache hits, while dispose closes once, so two
|
|
731
|
-
// concurrent first calls would strand the refcount above zero and leak the
|
|
732
|
-
// connection for the process lifetime. Cleared on failure so a failed open
|
|
733
|
-
// can be retried rather than cached forever.
|
|
734
|
-
dbPromise = null;
|
|
731
|
+
this.connection = createManagedConnection(dbName, DB_VERSION, initDatabase);
|
|
732
|
+
}
|
|
735
733
|
ensureDb() {
|
|
736
|
-
return this.
|
|
737
|
-
(err) => {
|
|
738
|
-
this.dbPromise = null;
|
|
739
|
-
throw err;
|
|
740
|
-
}
|
|
741
|
-
);
|
|
734
|
+
return this.connection.get();
|
|
742
735
|
}
|
|
743
736
|
async readStore(name) {
|
|
744
737
|
return (await this.ensureDb()).transaction([name], "readonly").objectStore(name);
|
|
745
738
|
}
|
|
746
739
|
/** Every write in one place, so none of them can forget to await the
|
|
747
740
|
* commit. Requests need no individual await: a failed one aborts the
|
|
748
|
-
* transaction, which `
|
|
741
|
+
* transaction, which `awaitTransaction` reports. */
|
|
749
742
|
async write(name, apply) {
|
|
750
743
|
const tx = (await this.ensureDb()).transaction([name], "readwrite");
|
|
751
|
-
const done =
|
|
744
|
+
const done = awaitTransaction(tx);
|
|
752
745
|
apply(tx.objectStore(name));
|
|
753
746
|
await done;
|
|
754
747
|
}
|
|
@@ -758,10 +751,26 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
758
751
|
});
|
|
759
752
|
}
|
|
760
753
|
async getAllSwaps() {
|
|
761
|
-
return
|
|
754
|
+
return promisifyRequest((await this.readStore(STORE_SWAPS)).getAll());
|
|
755
|
+
}
|
|
756
|
+
async saveRfqSwap(record) {
|
|
757
|
+
await this.write(STORE_RFQ_SWAPS, (store) => {
|
|
758
|
+
store.put(record);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
async getRfqSwap(rfqId) {
|
|
762
|
+
return promisifyRequest((await this.readStore(STORE_RFQ_SWAPS)).get(rfqId));
|
|
763
|
+
}
|
|
764
|
+
async getAllRfqSwaps() {
|
|
765
|
+
return promisifyRequest((await this.readStore(STORE_RFQ_SWAPS)).getAll());
|
|
766
|
+
}
|
|
767
|
+
async removeRfqSwap(rfqId) {
|
|
768
|
+
await this.write(STORE_RFQ_SWAPS, (store) => {
|
|
769
|
+
store.delete(rfqId);
|
|
770
|
+
});
|
|
762
771
|
}
|
|
763
772
|
async getScannedTxids() {
|
|
764
|
-
const keys = await
|
|
773
|
+
const keys = await promisifyRequest((await this.readStore(STORE_SCANNED)).getAllKeys());
|
|
765
774
|
return new Set(keys);
|
|
766
775
|
}
|
|
767
776
|
async markTxidsScanned(txids) {
|
|
@@ -771,7 +780,7 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
771
780
|
}
|
|
772
781
|
async getCachedMarkets(network, registry) {
|
|
773
782
|
const store = await this.readStore(STORE_MARKETS);
|
|
774
|
-
return
|
|
783
|
+
return promisifyRequest(store.get(marketsCacheKey(network, registry)));
|
|
775
784
|
}
|
|
776
785
|
async saveCachedMarkets(network, registry, entry) {
|
|
777
786
|
await this.write(STORE_MARKETS, (store) => {
|
|
@@ -784,19 +793,353 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
784
793
|
async clear() {
|
|
785
794
|
const stores = STORES.map(([name]) => name);
|
|
786
795
|
const tx = (await this.ensureDb()).transaction(stores, "readwrite");
|
|
787
|
-
const done =
|
|
796
|
+
const done = awaitTransaction(tx);
|
|
788
797
|
for (const name of stores) tx.objectStore(name).clear();
|
|
789
798
|
await done;
|
|
790
799
|
}
|
|
791
800
|
async [Symbol.asyncDispose]() {
|
|
792
|
-
|
|
793
|
-
await closeDatabase(this.dbName);
|
|
794
|
-
this.dbPromise = null;
|
|
801
|
+
await this.connection[Symbol.asyncDispose]();
|
|
795
802
|
}
|
|
796
803
|
};
|
|
797
804
|
|
|
805
|
+
// src/rfqCorridors.ts
|
|
806
|
+
import { hex as hex3 } from "@scure/base";
|
|
807
|
+
|
|
808
|
+
// src/rfqCorridor.ts
|
|
809
|
+
var RfqCorridorRegistry = class {
|
|
810
|
+
handlers = /* @__PURE__ */ new Map();
|
|
811
|
+
register(handler) {
|
|
812
|
+
if (this.handlers.has(handler.kind)) {
|
|
813
|
+
throw new Error(
|
|
814
|
+
`RFQ corridor handler for kind '${handler.kind}' is already registered`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
this.handlers.set(handler.kind, handler);
|
|
818
|
+
}
|
|
819
|
+
/** Takes a bare `string`, deliberately, where {@link RfqCorridorHandler.kind}
|
|
820
|
+
* is the manager's union: a lookup key comes off a record a backend handed
|
|
821
|
+
* back, and that it typechecks as a `kind` is a claim about the type, not
|
|
822
|
+
* about what was stored. Narrowing this would read as a check already made. */
|
|
823
|
+
get(kind) {
|
|
824
|
+
return this.handlers.get(kind);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* The handler for a kind, or a loud failure.
|
|
828
|
+
*
|
|
829
|
+
* A record whose corridor is not registered cannot be restored, and
|
|
830
|
+
* guessing would monitor a swap with no idea how to drive it. Names the
|
|
831
|
+
* registered kinds so a missing `register()` call is obvious.
|
|
832
|
+
*/
|
|
833
|
+
getOrThrow(kind) {
|
|
834
|
+
const handler = this.get(kind);
|
|
835
|
+
if (!handler) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
`no RFQ corridor handler registered for kind '${kind}'; registered: ${this.registeredKinds().join(", ") || "none"}`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return handler;
|
|
841
|
+
}
|
|
842
|
+
has(kind) {
|
|
843
|
+
return this.handlers.has(kind);
|
|
844
|
+
}
|
|
845
|
+
registeredKinds() {
|
|
846
|
+
return [...this.handlers.keys()];
|
|
847
|
+
}
|
|
848
|
+
/** Test seam, mirroring the contract registry's. */
|
|
849
|
+
unregister(kind) {
|
|
850
|
+
return this.handlers.delete(kind);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
var rfqCorridorHandlers = new RfqCorridorRegistry();
|
|
854
|
+
|
|
855
|
+
// src/rfqProfileParts.ts
|
|
856
|
+
var rfqSecretsProfile = (secrets, paymentHash) => {
|
|
857
|
+
const { signingDescriptor, ...preimage } = swapSecretsToRecord(secrets);
|
|
858
|
+
return {
|
|
859
|
+
signer: { signingDescriptor },
|
|
860
|
+
...paymentHash ? { hashlock: { paymentHash, ...preimage } } : {}
|
|
861
|
+
};
|
|
862
|
+
};
|
|
863
|
+
var parseHex32 = (value, field) => {
|
|
864
|
+
if (typeof value !== "string" || !/^[0-9a-fA-F]{64}$/.test(value)) {
|
|
865
|
+
throw new Error(`${field} must be 32 bytes of hex, got ${JSON.stringify(value)}`);
|
|
866
|
+
}
|
|
867
|
+
return value.toLowerCase();
|
|
868
|
+
};
|
|
869
|
+
var parseSigner = (value) => {
|
|
870
|
+
const signingDescriptor = value?.signingDescriptor;
|
|
871
|
+
if (typeof signingDescriptor !== "string" || signingDescriptor.length === 0) {
|
|
872
|
+
throw new Error(
|
|
873
|
+
`rfq profile.signer carries no signingDescriptor (got ${JSON.stringify(signingDescriptor)})`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
return { signingDescriptor };
|
|
877
|
+
};
|
|
878
|
+
var parseHashlock = (value) => {
|
|
879
|
+
const raw = value ?? {};
|
|
880
|
+
const hashlock = {
|
|
881
|
+
paymentHash: parseHex32(raw.paymentHash, "rfq profile.hashlock.paymentHash")
|
|
882
|
+
};
|
|
883
|
+
if (raw.preimageHex !== void 0) {
|
|
884
|
+
hashlock.preimageHex = parseHex32(raw.preimageHex, "rfq profile.hashlock.preimageHex");
|
|
885
|
+
}
|
|
886
|
+
if (raw.preimageSaltHex !== void 0) {
|
|
887
|
+
hashlock.preimageSaltHex = parseHex32(
|
|
888
|
+
raw.preimageSaltHex,
|
|
889
|
+
"rfq profile.hashlock.preimageSaltHex"
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
return hashlock;
|
|
893
|
+
};
|
|
894
|
+
var hydrateHashlock = (profile) => {
|
|
895
|
+
try {
|
|
896
|
+
return { paymentHash: parseHashlock(profile.hashlock).paymentHash };
|
|
897
|
+
} catch (cause) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`rfq record carries no usable hashlock; it cannot verify a preimage: ${String(cause)}`,
|
|
900
|
+
{ cause }
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
var rfqSignerOf = (record) => {
|
|
905
|
+
const signer = record.profile.signer;
|
|
906
|
+
if (signer === void 0) return void 0;
|
|
907
|
+
return parseSigner(signer);
|
|
908
|
+
};
|
|
909
|
+
var rfqClaimSecretOf = (record) => {
|
|
910
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
911
|
+
if (!handler.claimSecret) return void 0;
|
|
912
|
+
try {
|
|
913
|
+
const claim = handler.claimSecret(record.profile);
|
|
914
|
+
return { ...parseSigner(claim), ...parseHashlock(claim) };
|
|
915
|
+
} catch (cause) {
|
|
916
|
+
throw new PreimageNotRecoverableError(
|
|
917
|
+
"malformed-record",
|
|
918
|
+
`this ${record.kind} record's claim secret is unreadable: ${String(cause)}`,
|
|
919
|
+
{ cause }
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
// src/rfqCorridors.ts
|
|
925
|
+
var LightningSendCorridor = {
|
|
926
|
+
kind: "lightning_send",
|
|
927
|
+
project: () => ({}),
|
|
928
|
+
hydrate: (profile) => hydrateHashlock(profile)
|
|
929
|
+
// No `claimSecret`, deliberately: `provisionRefundKey` mints no preimage at
|
|
930
|
+
// all, and deriving one off the refund descriptor would fail the payment
|
|
931
|
+
// hash check — a `hash-mismatch` on a swap that was never broken.
|
|
932
|
+
};
|
|
933
|
+
var LightningReceiveCorridor = {
|
|
934
|
+
kind: "lightning_receive",
|
|
935
|
+
// What the manager holds: the amount gate, and its own claim once it lands.
|
|
936
|
+
// `payoutAddress` comes from the request result and never changes, so the
|
|
937
|
+
// caller writes it once and this leaves it alone.
|
|
938
|
+
project: (swap) => {
|
|
939
|
+
const receive = swap;
|
|
940
|
+
return {
|
|
941
|
+
expectedAmount: receive.expectedAmount,
|
|
942
|
+
...receive.claimArkTxid ? { claimArkTxid: receive.claimArkTxid } : {}
|
|
943
|
+
};
|
|
944
|
+
},
|
|
945
|
+
hydrate(profile) {
|
|
946
|
+
if (typeof profile.expectedAmount !== "number" || !Number.isFinite(profile.expectedAmount)) {
|
|
947
|
+
throw new Error("lightning_receive record carries no expectedAmount; it cannot claim");
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
...hydrateHashlock(profile),
|
|
951
|
+
expectedAmount: profile.expectedAmount,
|
|
952
|
+
...profile.claimArkTxid ? { claimArkTxid: profile.claimArkTxid } : {}
|
|
953
|
+
};
|
|
954
|
+
},
|
|
955
|
+
// We are the claimant here, so the preimage material on the hashlock is
|
|
956
|
+
// ours to use.
|
|
957
|
+
claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
|
|
958
|
+
activityTxids: (profile) => profile.claimArkTxid ? [profile.claimArkTxid] : []
|
|
959
|
+
};
|
|
960
|
+
function onchainSendProfile(result) {
|
|
961
|
+
return {
|
|
962
|
+
claimKey: hex3.encode(result.htlcParams.claimKey),
|
|
963
|
+
refundKey: hex3.encode(result.htlcParams.refundKey),
|
|
964
|
+
htlcLocktime: result.htlcParams.refundLocktime,
|
|
965
|
+
network: result.l1Network,
|
|
966
|
+
htlcAddress: result.htlc.address,
|
|
967
|
+
minConfirmations: result.minConfirmations
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
var OnchainSendCorridor = {
|
|
971
|
+
kind: "onchain_send",
|
|
972
|
+
// The L1 keys and the network are only in the request result — nothing on
|
|
973
|
+
// `OnchainSendSwap` carries them — so the caller writes them once. What the
|
|
974
|
+
// manager learns as it drives the swap is the fill and our own claim.
|
|
975
|
+
project: (swap) => {
|
|
976
|
+
const send = swap;
|
|
977
|
+
return {
|
|
978
|
+
...send.funding ? { funding: send.funding } : {},
|
|
979
|
+
...send.claimTxid ? { claimTxid: send.claimTxid } : {}
|
|
980
|
+
};
|
|
981
|
+
},
|
|
982
|
+
hydrate(profile) {
|
|
983
|
+
const { paymentHash } = hydrateHashlock(profile);
|
|
984
|
+
if (!profile.claimKey || !profile.refundKey) {
|
|
985
|
+
throw new Error(
|
|
986
|
+
"onchain_send record carries no L1 keys; its HTLC cannot be rebuilt and its refund window would pass unwatched"
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
if (!Number.isInteger(profile.minConfirmations) || profile.minConfirmations < 1) {
|
|
990
|
+
throw new Error(
|
|
991
|
+
`onchain_send record carries no usable minConfirmations (${String(profile.minConfirmations)}); the confirmation gate cannot be checked \u2014 refusing to restore a swap that would claim an unconfirmed fill`
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
const htlc = onchainHtlcScript(
|
|
995
|
+
{
|
|
996
|
+
// From the profile, not the covenant: the lockup commits to
|
|
997
|
+
// `hash160(P)`, and the HTLC needs `sha256(P)`. One P
|
|
998
|
+
// unlocks both legs, but only one of the two hashes of it
|
|
999
|
+
// is recoverable here.
|
|
1000
|
+
paymentHash,
|
|
1001
|
+
claimKey: hex3.decode(profile.claimKey),
|
|
1002
|
+
refundKey: hex3.decode(profile.refundKey),
|
|
1003
|
+
refundLocktime: profile.htlcLocktime
|
|
1004
|
+
},
|
|
1005
|
+
profile.network
|
|
1006
|
+
);
|
|
1007
|
+
if (htlc.address !== profile.htlcAddress) {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`onchain_send record's L1 inputs derive ${htlc.address}, but the fill was expected at ${String(profile.htlcAddress)} \u2014 these are not this swap's`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
paymentHash,
|
|
1014
|
+
htlc,
|
|
1015
|
+
minConfirmations: profile.minConfirmations,
|
|
1016
|
+
...profile.funding ? { funding: profile.funding } : {},
|
|
1017
|
+
...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
|
|
1018
|
+
};
|
|
1019
|
+
},
|
|
1020
|
+
// The trader claims the L1 HTLC with P, so this leg's preimage material is
|
|
1021
|
+
// ours.
|
|
1022
|
+
claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
|
|
1023
|
+
// Our own L1 claim only. `funding` is the SOLVER's fill into the HTLC —
|
|
1024
|
+
// not a transaction of ours, so grouping it would claim a row this wallet
|
|
1025
|
+
// never made.
|
|
1026
|
+
activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
|
|
1027
|
+
};
|
|
1028
|
+
rfqCorridorHandlers.register(LightningSendCorridor);
|
|
1029
|
+
rfqCorridorHandlers.register(LightningReceiveCorridor);
|
|
1030
|
+
rfqCorridorHandlers.register(OnchainSendCorridor);
|
|
1031
|
+
|
|
1032
|
+
// src/rfqRecord.ts
|
|
1033
|
+
import { ArkAddress as ArkAddress2, VHTLCV2ContractHandler } from "@arkade-os/sdk";
|
|
1034
|
+
import { hex as hex4 } from "@scure/base";
|
|
1035
|
+
|
|
1036
|
+
// src/rfqSwapState.ts
|
|
1037
|
+
var RFQ_SWAP_TERMINAL_STATES = ["settled", "refunded", "failed"];
|
|
1038
|
+
var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
|
|
1039
|
+
|
|
1040
|
+
// src/rfqRecord.ts
|
|
1041
|
+
var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
|
|
1042
|
+
var managerState = (swap) => ({
|
|
1043
|
+
rfqId: swap.rfqId,
|
|
1044
|
+
state: swap.state,
|
|
1045
|
+
createdAt: swap.createdAt,
|
|
1046
|
+
updatedAt: swap.updatedAt,
|
|
1047
|
+
...swap.refundArkTxid ? { refundArkTxid: swap.refundArkTxid } : {},
|
|
1048
|
+
...swap.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...swap.lockupSpendArkTxids] } : {},
|
|
1049
|
+
...swap.failure ? { failure: swap.failure } : {},
|
|
1050
|
+
...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
|
|
1051
|
+
});
|
|
1052
|
+
function assertSameSwap(origin, swap) {
|
|
1053
|
+
if (origin.kind !== swap.kind) {
|
|
1054
|
+
throw new Error(
|
|
1055
|
+
`rfq swap record is a ${origin.kind} origin paired with a ${swap.kind} swap`
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
const funded = hex4.encode(ArkAddress2.decode(origin.lockupAddress).pkScript);
|
|
1059
|
+
const watched = hex4.encode(swap.lockupPkScript);
|
|
1060
|
+
if (funded !== watched) {
|
|
1061
|
+
throw new Error(
|
|
1062
|
+
`rfq swap record's lockup address holds ${funded}, but the swap watches ${watched} \u2014 these are not the same swap`
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function createRfqSwapRecord(origin, swap) {
|
|
1067
|
+
assertSameSwap(origin, swap);
|
|
1068
|
+
const handler = rfqCorridorHandlers.getOrThrow(origin.kind);
|
|
1069
|
+
return {
|
|
1070
|
+
...origin,
|
|
1071
|
+
...managerState(swap),
|
|
1072
|
+
// The caller wrote what only the request result knows; the handler adds
|
|
1073
|
+
// what the live swap already carries.
|
|
1074
|
+
profile: { ...origin.profile, ...handler.project(swap) }
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function updateRfqSwapRecord(record, swap) {
|
|
1078
|
+
assertSameSwap(record, swap);
|
|
1079
|
+
const {
|
|
1080
|
+
refundArkTxid: _refundArkTxid,
|
|
1081
|
+
lockupSpendArkTxids: _lockupSpendArkTxids,
|
|
1082
|
+
failure: _failure,
|
|
1083
|
+
blockedReason: _blockedReason,
|
|
1084
|
+
...origin
|
|
1085
|
+
} = record;
|
|
1086
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
1087
|
+
return {
|
|
1088
|
+
...origin,
|
|
1089
|
+
...managerState(swap),
|
|
1090
|
+
profile: { ...record.profile, ...handler.project(swap) }
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
function rfqSwapOriginOf(record) {
|
|
1094
|
+
return {
|
|
1095
|
+
kind: record.kind,
|
|
1096
|
+
lockupAddress: record.lockupAddress,
|
|
1097
|
+
profile: { ...record.profile },
|
|
1098
|
+
...record.amount !== void 0 ? { amount: record.amount } : {},
|
|
1099
|
+
...record.fundingArkTxid ? { fundingArkTxid: record.fundingArkTxid } : {}
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function lockupScript(params, lockupAddress) {
|
|
1103
|
+
const script = VHTLCV2ContractHandler.createScript(params);
|
|
1104
|
+
const funded = ArkAddress2.decode(lockupAddress).pkScript;
|
|
1105
|
+
if (hex4.encode(funded) !== hex4.encode(script.pkScript)) {
|
|
1106
|
+
throw new Error(
|
|
1107
|
+
`rfq swap covenant params derive ${hex4.encode(script.pkScript)}, but the record's lockup address holds ${hex4.encode(funded)} \u2014 these params are not this swap's`
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
return script;
|
|
1111
|
+
}
|
|
1112
|
+
function rebuildRfqSwap(record, params) {
|
|
1113
|
+
const script = lockupScript(params, record.lockupAddress);
|
|
1114
|
+
const common = {
|
|
1115
|
+
rfqId: record.rfqId,
|
|
1116
|
+
state: record.state,
|
|
1117
|
+
lockupPkScript: script.pkScript,
|
|
1118
|
+
lockup: { script, address: record.lockupAddress },
|
|
1119
|
+
// From the covenant, which binds it: the record's own copy would be a
|
|
1120
|
+
// second source for the deadline the refund is gated on.
|
|
1121
|
+
refundLocktime: Number(script.options.refundLocktime),
|
|
1122
|
+
createdAt: record.createdAt,
|
|
1123
|
+
updatedAt: record.updatedAt,
|
|
1124
|
+
...record.refundArkTxid ? { refundArkTxid: record.refundArkTxid } : {},
|
|
1125
|
+
...record.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...record.lockupSpendArkTxids] } : {},
|
|
1126
|
+
...record.failure ? { failure: record.failure } : {},
|
|
1127
|
+
...record.blockedReason ? { blockedReason: record.blockedReason } : {}
|
|
1128
|
+
};
|
|
1129
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
1130
|
+
return {
|
|
1131
|
+
...common,
|
|
1132
|
+
kind: record.kind,
|
|
1133
|
+
...handler.hydrate(record.profile, { lockup: script })
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function shouldRetainRfqSwap(record, now) {
|
|
1137
|
+
if (!isRfqSwapTerminal(record.state)) return true;
|
|
1138
|
+
return now - record.updatedAt < RFQ_SWAP_RETENTION_SECONDS;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
798
1141
|
// src/restore.ts
|
|
799
|
-
import { base64, hex as
|
|
1142
|
+
import { base64, hex as hex5 } from "@scure/base";
|
|
800
1143
|
import {
|
|
801
1144
|
Extension,
|
|
802
1145
|
Transaction,
|
|
@@ -832,7 +1175,7 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
|
832
1175
|
let leaves;
|
|
833
1176
|
try {
|
|
834
1177
|
const script = offerVtxoScript(offer, serverPubkey);
|
|
835
|
-
if (
|
|
1178
|
+
if (hex5.encode(script.pkScript) !== hex5.encode(offer.swapPkScript)) return "indeterminate";
|
|
836
1179
|
leaves = {
|
|
837
1180
|
cancel: script.functionByName("cancel")?.leafScript,
|
|
838
1181
|
fulfill: script.functionByName("fulfill")?.leafScript
|
|
@@ -843,11 +1186,11 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
|
843
1186
|
for (let i = 0; i < spendTx.inputsLength; i++) {
|
|
844
1187
|
const input = spendTx.getInput(i);
|
|
845
1188
|
if (!input.txid || input.index !== deposit.vout) continue;
|
|
846
|
-
if (
|
|
1189
|
+
if (hex5.encode(input.txid) !== deposit.txid) continue;
|
|
847
1190
|
for (const leaf of input.tapLeafScript ?? []) {
|
|
848
|
-
const spent =
|
|
849
|
-
if (leaves.cancel && spent ===
|
|
850
|
-
if (leaves.fulfill && spent ===
|
|
1191
|
+
const spent = hex5.encode(scriptFromTapLeafScript(leaf));
|
|
1192
|
+
if (leaves.cancel && spent === hex5.encode(leaves.cancel)) return "cancelled";
|
|
1193
|
+
if (leaves.fulfill && spent === hex5.encode(leaves.fulfill)) return "fulfilled";
|
|
851
1194
|
}
|
|
852
1195
|
}
|
|
853
1196
|
return "indeterminate";
|
|
@@ -882,13 +1225,13 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
882
1225
|
found.push({
|
|
883
1226
|
fundingTx,
|
|
884
1227
|
offer: decodeOffer(payload),
|
|
885
|
-
offerHex:
|
|
1228
|
+
offerHex: hex5.encode(payload)
|
|
886
1229
|
});
|
|
887
1230
|
} catch {
|
|
888
1231
|
}
|
|
889
1232
|
}
|
|
890
1233
|
if (found.length === 0) return { restored: [], scannedTxids: fetchedTxids };
|
|
891
|
-
const scripts = [...new Set(found.map((f) =>
|
|
1234
|
+
const scripts = [...new Set(found.map((f) => hex5.encode(f.offer.swapPkScript)))];
|
|
892
1235
|
const { vtxos } = await indexer.getVtxos({ scripts });
|
|
893
1236
|
const vtxoByScriptAndTxid = new Map(vtxos.map((v) => [`${v.script}:${v.txid}`, v]));
|
|
894
1237
|
const txByAnyId = /* @__PURE__ */ new Map();
|
|
@@ -900,7 +1243,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
900
1243
|
const spendTxids = /* @__PURE__ */ new Set();
|
|
901
1244
|
for (const { fundingTx, offer } of found) {
|
|
902
1245
|
const vtxo = vtxoByScriptAndTxid.get(
|
|
903
|
-
`${
|
|
1246
|
+
`${hex5.encode(offer.swapPkScript)}:${fundingTx.redeemTxid}`
|
|
904
1247
|
);
|
|
905
1248
|
if (vtxo?.virtualStatus.state !== "spent") continue;
|
|
906
1249
|
for (const txid of spendTxidsOf(vtxo)) spendTxids.add(txid);
|
|
@@ -909,7 +1252,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
909
1252
|
const restored = [];
|
|
910
1253
|
const unresolved = /* @__PURE__ */ new Set();
|
|
911
1254
|
for (const { fundingTx, offer, offerHex } of found) {
|
|
912
|
-
const swapPkScript =
|
|
1255
|
+
const swapPkScript = hex5.encode(offer.swapPkScript);
|
|
913
1256
|
const vtxo = vtxoByScriptAndTxid.get(`${swapPkScript}:${fundingTx.redeemTxid}`);
|
|
914
1257
|
if (!vtxo) {
|
|
915
1258
|
unresolved.add(fundingTx.redeemTxid);
|
|
@@ -966,9 +1309,9 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
966
1309
|
}
|
|
967
1310
|
|
|
968
1311
|
// src/watch.ts
|
|
969
|
-
import { base64 as base642, hex as
|
|
1312
|
+
import { base64 as base642, hex as hex6 } from "@scure/base";
|
|
970
1313
|
import {
|
|
971
|
-
ArkAddress as
|
|
1314
|
+
ArkAddress as ArkAddress3,
|
|
972
1315
|
RestIndexerProvider as RestIndexerProvider3,
|
|
973
1316
|
Transaction as Transaction2
|
|
974
1317
|
} from "@arkade-os/sdk";
|
|
@@ -991,7 +1334,7 @@ async function watchOfferSwaps({
|
|
|
991
1334
|
onUpdate
|
|
992
1335
|
}) {
|
|
993
1336
|
const manager = await wallet.getContractManager();
|
|
994
|
-
const serverPubkey =
|
|
1337
|
+
const serverPubkey = ArkAddress3.decode(await wallet.getAddress()).serverPubKey;
|
|
995
1338
|
const indexer = new RestIndexerProvider3(arkServerUrl);
|
|
996
1339
|
let queue = Promise.resolve();
|
|
997
1340
|
const enqueue = (task) => {
|
|
@@ -1005,7 +1348,7 @@ async function watchOfferSwaps({
|
|
|
1005
1348
|
if (candidates.length === 0) return "indeterminate";
|
|
1006
1349
|
const { txs } = await indexer.getVirtualTxs(candidates);
|
|
1007
1350
|
return classifyDepositSpend(
|
|
1008
|
-
decodeOffer(
|
|
1351
|
+
decodeOffer(hex6.decode(swap.offerHex)),
|
|
1009
1352
|
serverPubkey,
|
|
1010
1353
|
txs.map((psbt) => Transaction2.fromPSBT(base642.decode(psbt))),
|
|
1011
1354
|
{ txid: vtxo.txid, vout: vtxo.vout }
|
|
@@ -1049,7 +1392,7 @@ async function watchOfferSwaps({
|
|
|
1049
1392
|
}
|
|
1050
1393
|
|
|
1051
1394
|
// src/claim.ts
|
|
1052
|
-
import { hex as
|
|
1395
|
+
import { hex as hex8 } from "@scure/base";
|
|
1053
1396
|
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
1054
1397
|
import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
|
|
1055
1398
|
import {
|
|
@@ -1059,7 +1402,7 @@ import {
|
|
|
1059
1402
|
} from "@arkade-os/sdk";
|
|
1060
1403
|
|
|
1061
1404
|
// src/refund.ts
|
|
1062
|
-
import { base64 as base643, hex as
|
|
1405
|
+
import { base64 as base643, hex as hex7 } from "@scure/base";
|
|
1063
1406
|
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
1064
1407
|
import {
|
|
1065
1408
|
CSVMultisigTapscript,
|
|
@@ -1116,7 +1459,7 @@ var LockupNeedsRecoveryError = class extends Error {
|
|
|
1116
1459
|
}
|
|
1117
1460
|
};
|
|
1118
1461
|
async function findLockupVtxos(indexer, swapPkScript) {
|
|
1119
|
-
const scripts = [
|
|
1462
|
+
const scripts = [hex7.encode(swapPkScript)];
|
|
1120
1463
|
const [spendable, recoverable] = await Promise.all([
|
|
1121
1464
|
indexer.getVtxos({ scripts, spendableOnly: true }),
|
|
1122
1465
|
indexer.getVtxos({ scripts, recoverableOnly: true })
|
|
@@ -1141,23 +1484,28 @@ async function findLockupVtxos(indexer, swapPkScript) {
|
|
|
1141
1484
|
}
|
|
1142
1485
|
return out;
|
|
1143
1486
|
}
|
|
1144
|
-
var hashesTo = (candidate, paymentHash) =>
|
|
1487
|
+
var hashesTo = (candidate, paymentHash) => hex7.encode(sha2562(candidate)) === paymentHash;
|
|
1145
1488
|
var candidateWitnessItems = (tx, inputIndex) => [
|
|
1146
1489
|
...getArkPsbtFields(tx, inputIndex, ConditionWitness).flat(),
|
|
1147
1490
|
...tx.getInput(inputIndex).finalScriptWitness ?? []
|
|
1148
1491
|
];
|
|
1149
1492
|
async function readLockupFate(indexer, input) {
|
|
1150
|
-
const { vtxos } = await indexer.getVtxos({ scripts: [
|
|
1493
|
+
const { vtxos } = await indexer.getVtxos({ scripts: [hex7.encode(input.swapPkScript)] });
|
|
1151
1494
|
const all = vtxos ?? [];
|
|
1152
1495
|
if (all.length === 0) return { fate: "unknown" };
|
|
1153
|
-
const spentBy = /* @__PURE__ */ new
|
|
1496
|
+
const spentBy = /* @__PURE__ */ new Map();
|
|
1154
1497
|
let everySpendNamed = true;
|
|
1155
1498
|
for (const vtxo of all) {
|
|
1156
1499
|
if (!vtxo.isSpent && !vtxo.spentBy && !vtxo.settledBy) return { fate: "open" };
|
|
1157
|
-
if (vtxo.spentBy)
|
|
1500
|
+
if (vtxo.spentBy)
|
|
1501
|
+
spentBy.set(vtxo.spentBy, {
|
|
1502
|
+
checkpointTxid: vtxo.spentBy,
|
|
1503
|
+
arkTxid: vtxo.arkTxId
|
|
1504
|
+
});
|
|
1158
1505
|
else everySpendNamed = false;
|
|
1159
1506
|
}
|
|
1160
|
-
const
|
|
1507
|
+
const spends = [...spentBy.values()];
|
|
1508
|
+
const { txs } = await indexer.getVirtualTxs([...spentBy.keys()]);
|
|
1161
1509
|
const observed = /* @__PURE__ */ new Set();
|
|
1162
1510
|
for (const raw of txs) {
|
|
1163
1511
|
let tx;
|
|
@@ -1170,16 +1518,16 @@ async function readLockupFate(indexer, input) {
|
|
|
1170
1518
|
for (let i = 0; i < tx.inputsLength; i++) {
|
|
1171
1519
|
const spent = tx.getInput(i);
|
|
1172
1520
|
if (!spent.txid) continue;
|
|
1173
|
-
const txid =
|
|
1521
|
+
const txid = hex7.encode(spent.txid);
|
|
1174
1522
|
if (!all.some((vtxo) => vtxo.txid === txid && vtxo.vout === spent.index)) continue;
|
|
1175
1523
|
for (const candidate of candidateWitnessItems(tx, i)) {
|
|
1176
1524
|
if (hashesTo(candidate, input.paymentHash)) {
|
|
1177
|
-
return { fate: "claimed", preimage: candidate };
|
|
1525
|
+
return { fate: "claimed", preimage: candidate, spends };
|
|
1178
1526
|
}
|
|
1179
1527
|
}
|
|
1180
1528
|
}
|
|
1181
1529
|
}
|
|
1182
|
-
return everySpendNamed && observed.size === spentBy.size ? { fate: "returned" } : { fate: "unknown" };
|
|
1530
|
+
return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
|
|
1183
1531
|
}
|
|
1184
1532
|
async function pushRefundWithoutReceiver(ark, input) {
|
|
1185
1533
|
if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
|
|
@@ -1199,7 +1547,7 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
1199
1547
|
const info = await ark.getInfo();
|
|
1200
1548
|
let serverUnrollScript;
|
|
1201
1549
|
try {
|
|
1202
|
-
serverUnrollScript = CSVMultisigTapscript.decode(
|
|
1550
|
+
serverUnrollScript = CSVMultisigTapscript.decode(hex7.decode(info.checkpointTapscript));
|
|
1203
1551
|
} catch {
|
|
1204
1552
|
throw new Error("invalid checkpointTapscript from the Arkade server");
|
|
1205
1553
|
}
|
|
@@ -1310,13 +1658,13 @@ async function pushClaim(ark, input) {
|
|
|
1310
1658
|
}
|
|
1311
1659
|
}
|
|
1312
1660
|
const committed = input.script.options.preimageHash;
|
|
1313
|
-
if (
|
|
1661
|
+
if (hex8.encode(ripemd160(sha2563(input.preimage))) !== hex8.encode(committed)) {
|
|
1314
1662
|
throw new Error("preimage does not match the covenant's payment hash");
|
|
1315
1663
|
}
|
|
1316
1664
|
const info = await ark.getInfo();
|
|
1317
1665
|
let serverUnrollScript;
|
|
1318
1666
|
try {
|
|
1319
|
-
serverUnrollScript = CSVMultisigTapscript2.decode(
|
|
1667
|
+
serverUnrollScript = CSVMultisigTapscript2.decode(hex8.decode(info.checkpointTapscript));
|
|
1320
1668
|
} catch {
|
|
1321
1669
|
throw new Error("invalid checkpointTapscript from the Arkade server");
|
|
1322
1670
|
}
|
|
@@ -1411,10 +1759,31 @@ async function senderIdentityForSwapRecord(wallet, record) {
|
|
|
1411
1759
|
}
|
|
1412
1760
|
}
|
|
1413
1761
|
|
|
1762
|
+
// src/arkadeRefunder.ts
|
|
1763
|
+
function arkadeRefunder(deps) {
|
|
1764
|
+
return async (swap) => {
|
|
1765
|
+
const script = swap.lockup?.script;
|
|
1766
|
+
if (!script) {
|
|
1767
|
+
throw new Error(
|
|
1768
|
+
`swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
const vtxos = await findLockupVtxos(deps.indexer, swap.lockupPkScript);
|
|
1772
|
+
if (vtxos.length === 0) return null;
|
|
1773
|
+
const record = await deps.repository.getRfqSwap(swap.rfqId);
|
|
1774
|
+
if (!record) {
|
|
1775
|
+
throw new RefundNotLocallyPossibleError(
|
|
1776
|
+
"no-secrets",
|
|
1777
|
+
`no stored record for ${swap.rfqId}; the descriptor that signs its refund lives there`
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
|
|
1781
|
+
return pushRefundWithoutReceiver(deps.ark, { script, sender, vtxos });
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1414
1785
|
// src/swapManager.ts
|
|
1415
|
-
import { hex as
|
|
1416
|
-
var RFQ_SWAP_TERMINAL_STATES = ["settled", "refunded", "failed"];
|
|
1417
|
-
var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
|
|
1786
|
+
import { hex as hex9 } from "@scure/base";
|
|
1418
1787
|
function nextOnchainAction(input) {
|
|
1419
1788
|
switch (input.phase.phase) {
|
|
1420
1789
|
case "unfunded":
|
|
@@ -1430,6 +1799,17 @@ function nextOnchainAction(input) {
|
|
|
1430
1799
|
return input.htlcLocktime - input.now >= ONCHAIN_CLAIM_MARGIN_SECONDS ? "claim" : "claim_window_closed";
|
|
1431
1800
|
}
|
|
1432
1801
|
}
|
|
1802
|
+
var RfqSwapOriginRequired = class extends Error {
|
|
1803
|
+
/** The swap that could not be admitted. */
|
|
1804
|
+
rfqId;
|
|
1805
|
+
constructor(rfqId) {
|
|
1806
|
+
super(
|
|
1807
|
+
`rfq swap ${rfqId} has no stored record and no origin was supplied; pass the request-time origin as addSwap's second argument so its first record can be written`
|
|
1808
|
+
);
|
|
1809
|
+
this.name = "RfqSwapOriginRequired";
|
|
1810
|
+
this.rfqId = rfqId;
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1433
1813
|
var notify = (listeners, call) => {
|
|
1434
1814
|
for (const listener of listeners) {
|
|
1435
1815
|
try {
|
|
@@ -1507,7 +1887,20 @@ var RfqSwapManager = class {
|
|
|
1507
1887
|
* answers instead of throwing "not found". Cleared by {@link removeSwap}. */
|
|
1508
1888
|
finished = /* @__PURE__ */ new Map();
|
|
1509
1889
|
waiters = /* @__PURE__ */ new Map();
|
|
1510
|
-
/**
|
|
1890
|
+
/**
|
|
1891
|
+
* The request-time origin of each swap the manager may have to CREATE a
|
|
1892
|
+
* record for, by rfqId.
|
|
1893
|
+
*
|
|
1894
|
+
* Needed only for a swap the store has never seen: once a record exists,
|
|
1895
|
+
* `updateRfqSwapRecord` carries the origin half through and the map is
|
|
1896
|
+
* redundant. It is kept anyway for the swap's whole life, so a record the
|
|
1897
|
+
* store loses between passes is rewritten rather than lost — and dropped
|
|
1898
|
+
* by {@link removeSwap} and by retention, which are the two places a swap
|
|
1899
|
+
* stops being this manager's business.
|
|
1900
|
+
*/
|
|
1901
|
+
origins = /* @__PURE__ */ new Map();
|
|
1902
|
+
/** Records changed during the current pass, flushed to the repository and
|
|
1903
|
+
* through `saveSwap`. */
|
|
1511
1904
|
dirty = /* @__PURE__ */ new Set();
|
|
1512
1905
|
/** Race guard: one action at a time per swap. */
|
|
1513
1906
|
inProgress = /* @__PURE__ */ new Set();
|
|
@@ -1529,7 +1922,9 @@ var RfqSwapManager = class {
|
|
|
1529
1922
|
this.actionExecutedListeners.add(config.events.onActionExecuted);
|
|
1530
1923
|
}
|
|
1531
1924
|
}
|
|
1532
|
-
/** Wire the money-moving half. Without it the manager only watches.
|
|
1925
|
+
/** Wire the money-moving half. Without it the manager only watches. The
|
|
1926
|
+
* two claims may be omitted for a consumer that drives no kind reaching
|
|
1927
|
+
* them — see {@link AvailableRfqSwapManagerCallbacks}. */
|
|
1533
1928
|
setCallbacks(callbacks) {
|
|
1534
1929
|
this.callbacks = callbacks;
|
|
1535
1930
|
}
|
|
@@ -1549,6 +1944,116 @@ var RfqSwapManager = class {
|
|
|
1549
1944
|
this.actionExecutedListeners.add(listener);
|
|
1550
1945
|
return () => this.actionExecutedListeners.delete(listener);
|
|
1551
1946
|
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Rebuild every stored swap and take over monitoring them.
|
|
1949
|
+
*
|
|
1950
|
+
* The composition a consumer otherwise writes by hand, and the one place
|
|
1951
|
+
* all four pieces meet: retention decides what to keep
|
|
1952
|
+
* (`shouldRetainRfqSwap`), the lockup's contract row supplies the covenant
|
|
1953
|
+
* (`lockupContractParams`), `rebuildRfqSwap` turns a record back into a
|
|
1954
|
+
* live swap, and each rebuilt swap arrives with its own origin, so nothing
|
|
1955
|
+
* is asked of the caller.
|
|
1956
|
+
*
|
|
1957
|
+
* **Deliberately not part of {@link start}.** A consumer that wants to
|
|
1958
|
+
* look at its records — count them, show them, prune and stop — is not
|
|
1959
|
+
* forced to start driving money to do it. Call this first and `start()`
|
|
1960
|
+
* after; a manager already running polls the restored swaps at once.
|
|
1961
|
+
*
|
|
1962
|
+
* Retention runs BEFORE the rebuild, so a record past
|
|
1963
|
+
* `RFQ_SWAP_RETENTION_SECONDS` costs no contract lookup on its way to being
|
|
1964
|
+
* dropped.
|
|
1965
|
+
*
|
|
1966
|
+
* **A record that cannot be rebuilt is reported, never swallowed and never
|
|
1967
|
+
* fatal.** `rebuildRfqSwap` throws by design when the covenant params do
|
|
1968
|
+
* not derive the funded address, and `lockupContractParams` throws
|
|
1969
|
+
* `LockupContractMissing` when the wallet has no row for the lockup — both
|
|
1970
|
+
* say something true about that one record, and neither is a reason to
|
|
1971
|
+
* strand the others. They come back in {@link RfqRestoreResult.failed}.
|
|
1972
|
+
*/
|
|
1973
|
+
async restoreFromRepository(options = {}) {
|
|
1974
|
+
const repository = this.requireRepository("restoreFromRepository");
|
|
1975
|
+
const params = options.params ?? this.paramsFromContracts();
|
|
1976
|
+
const records = await repository.getAllRfqSwaps();
|
|
1977
|
+
const pruned = await this.dropRetired(repository, records);
|
|
1978
|
+
const retired = new Set(pruned);
|
|
1979
|
+
const restored = [];
|
|
1980
|
+
const failed = [];
|
|
1981
|
+
for (const record of records) {
|
|
1982
|
+
if (retired.has(record.rfqId)) continue;
|
|
1983
|
+
let swap;
|
|
1984
|
+
try {
|
|
1985
|
+
swap = rebuildRfqSwap(record, await params(record));
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
failed.push({
|
|
1988
|
+
rfqId: record.rfqId,
|
|
1989
|
+
error: error instanceof Error ? error : new Error(errorMessage(error))
|
|
1990
|
+
});
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
this.origins.set(record.rfqId, rfqSwapOriginOf(record));
|
|
1994
|
+
if (isRfqSwapTerminal(swap.state)) this.finished.set(swap.rfqId, swap);
|
|
1995
|
+
else this.track(swap);
|
|
1996
|
+
restored.push(swap);
|
|
1997
|
+
}
|
|
1998
|
+
if (this.running) {
|
|
1999
|
+
await Promise.allSettled(
|
|
2000
|
+
restored.filter((swap) => this.monitored.has(swap.rfqId)).map((swap) => this.pollSwap(swap))
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
return { restored, failed, pruned };
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Drop stored records that are terminal and past
|
|
2007
|
+
* `RFQ_SWAP_RETENTION_SECONDS`, and return their ids.
|
|
2008
|
+
*
|
|
2009
|
+
* `needs_counterparty` is never dropped, however old: the money is still at
|
|
2010
|
+
* the lockup and the counterparty's move still ends the swap. That rule
|
|
2011
|
+
* lives in `shouldRetainRfqSwap`, which this defers to rather than
|
|
2012
|
+
* restating.
|
|
2013
|
+
*
|
|
2014
|
+
* Public because retention is a caller's cadence, not the manager's: a
|
|
2015
|
+
* long-lived process wants it on a timer of its own, a mobile app wants it
|
|
2016
|
+
* at boot. {@link restoreFromRepository} runs it first, so the boot path
|
|
2017
|
+
* needs no separate call.
|
|
2018
|
+
*/
|
|
2019
|
+
async pruneRetiredSwaps() {
|
|
2020
|
+
const repository = this.deps.repository;
|
|
2021
|
+
if (!repository) return [];
|
|
2022
|
+
return this.dropRetired(repository, await repository.getAllRfqSwaps());
|
|
2023
|
+
}
|
|
2024
|
+
async dropRetired(repository, records) {
|
|
2025
|
+
const now = this.config.now();
|
|
2026
|
+
const dropped = [];
|
|
2027
|
+
for (const record of records) {
|
|
2028
|
+
if (shouldRetainRfqSwap(record, now)) continue;
|
|
2029
|
+
await repository.removeRfqSwap(record.rfqId);
|
|
2030
|
+
this.finished.delete(record.rfqId);
|
|
2031
|
+
if (!this.monitored.has(record.rfqId)) this.origins.delete(record.rfqId);
|
|
2032
|
+
dropped.push(record.rfqId);
|
|
2033
|
+
}
|
|
2034
|
+
return dropped;
|
|
2035
|
+
}
|
|
2036
|
+
requireRepository(method) {
|
|
2037
|
+
const repository = this.deps.repository;
|
|
2038
|
+
if (!repository) {
|
|
2039
|
+
throw new Error(
|
|
2040
|
+
`${method} needs a record store; pass one as RfqSwapManagerDeps.repository`
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
return repository;
|
|
2044
|
+
}
|
|
2045
|
+
/** The default covenant source: the wallet's own contract row for each
|
|
2046
|
+
* lockup, which is where registration put it before the address could be
|
|
2047
|
+
* funded. */
|
|
2048
|
+
paramsFromContracts() {
|
|
2049
|
+
const contracts = this.deps.contracts;
|
|
2050
|
+
if (!contracts) {
|
|
2051
|
+
throw new Error(
|
|
2052
|
+
"restoreFromRepository needs the covenant parameters: wire RfqSwapManagerDeps.contracts so each lockup's contract row can be read, or pass options.params"
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
return (record) => lockupContractParams(contracts, record.lockupAddress);
|
|
2056
|
+
}
|
|
1552
2057
|
/**
|
|
1553
2058
|
* Load records and begin monitoring. Runs one pass immediately — a caller
|
|
1554
2059
|
* resuming after a restart may be well past a deadline already — then
|
|
@@ -1558,8 +2063,17 @@ var RfqSwapManager = class {
|
|
|
1558
2063
|
* Calling it again while running loads the records and returns rather than
|
|
1559
2064
|
* re-arming — dropping them silently would strand a funded swap on a
|
|
1560
2065
|
* caller's harmless double-start.
|
|
2066
|
+
*
|
|
2067
|
+
* The signature is unchanged with a {@link RfqSwapManagerDeps.repository}
|
|
2068
|
+
* wired; the origins are resolved from the store instead, by the same rule
|
|
2069
|
+
* {@link addSwap} applies. A swap the store has never seen throws
|
|
2070
|
+
* {@link RfqSwapOriginRequired} — hand that one to `addSwap` with its
|
|
2071
|
+
* origin, or restore the whole set with {@link restoreFromRepository},
|
|
2072
|
+
* which needs no caller input at all. Every swap is checked before any is
|
|
2073
|
+
* tracked, so a bad one in the list does not leave a half-loaded manager.
|
|
1561
2074
|
*/
|
|
1562
2075
|
async start(swaps = []) {
|
|
2076
|
+
for (const swap of swaps) await this.admit(swap);
|
|
1563
2077
|
for (const swap of swaps) {
|
|
1564
2078
|
if (isRfqSwapTerminal(swap.state)) this.finished.set(swap.rfqId, swap);
|
|
1565
2079
|
else this.track(swap);
|
|
@@ -1591,9 +2105,22 @@ var RfqSwapManager = class {
|
|
|
1591
2105
|
this.unsubscribeContracts?.();
|
|
1592
2106
|
this.unsubscribeContracts = null;
|
|
1593
2107
|
}
|
|
1594
|
-
/**
|
|
1595
|
-
*
|
|
1596
|
-
|
|
2108
|
+
/**
|
|
2109
|
+
* Begin monitoring a swap. Polled immediately when the manager is running,
|
|
2110
|
+
* so a just-funded swap does not wait out a whole interval.
|
|
2111
|
+
*
|
|
2112
|
+
* `origin` is the request-time half a live swap cannot carry — the corridor,
|
|
2113
|
+
* the funded address, the corridor's profile, the funding txid — and it is
|
|
2114
|
+
* what lets the manager write this swap's FIRST record. Supply it whenever
|
|
2115
|
+
* a {@link RfqSwapManagerDeps.repository} is wired and the swap is new. It
|
|
2116
|
+
* may be omitted for a swap the store already holds a record for, which is
|
|
2117
|
+
* then read to confirm it; omitting it for one the store has never seen
|
|
2118
|
+
* throws {@link RfqSwapOriginRequired} rather than admitting a swap whose
|
|
2119
|
+
* record could never be written. With no repository wired the parameter is
|
|
2120
|
+
* inert.
|
|
2121
|
+
*/
|
|
2122
|
+
async addSwap(swap, origin) {
|
|
2123
|
+
await this.admit(swap, origin);
|
|
1597
2124
|
if (isRfqSwapTerminal(swap.state)) {
|
|
1598
2125
|
this.finished.set(swap.rfqId, swap);
|
|
1599
2126
|
return;
|
|
@@ -1601,6 +2128,29 @@ var RfqSwapManager = class {
|
|
|
1601
2128
|
this.track(swap);
|
|
1602
2129
|
if (this.running) await this.pollSwap(swap);
|
|
1603
2130
|
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Settle where this swap's record will come from, before it is monitored.
|
|
2133
|
+
*
|
|
2134
|
+
* Three ways it can be answered, in order: the caller passed an origin, one
|
|
2135
|
+
* is already remembered from an earlier `addSwap`, or the store holds a
|
|
2136
|
+
* record — which is the origin, already written. Only the last costs a read,
|
|
2137
|
+
* and only when the first two are absent.
|
|
2138
|
+
*/
|
|
2139
|
+
async admit(swap, origin) {
|
|
2140
|
+
if (origin) {
|
|
2141
|
+
assertSameSwap(origin, swap);
|
|
2142
|
+
this.origins.set(swap.rfqId, origin);
|
|
2143
|
+
if (this.deps.repository && !isRfqSwapTerminal(swap.state)) {
|
|
2144
|
+
this.dirty.add(swap.rfqId);
|
|
2145
|
+
}
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
const repository = this.deps.repository;
|
|
2149
|
+
if (!repository || this.origins.has(swap.rfqId)) return;
|
|
2150
|
+
const stored = await repository.getRfqSwap(swap.rfqId);
|
|
2151
|
+
if (!stored) throw new RfqSwapOriginRequired(swap.rfqId);
|
|
2152
|
+
this.origins.set(swap.rfqId, rfqSwapOriginOf(stored));
|
|
2153
|
+
}
|
|
1604
2154
|
/** Forget a swap entirely, monitored or finished.
|
|
1605
2155
|
*
|
|
1606
2156
|
* Its contract row is left alone: registration is a wallet-level fact about
|
|
@@ -1618,6 +2168,7 @@ var RfqSwapManager = class {
|
|
|
1618
2168
|
}
|
|
1619
2169
|
this.waiters.delete(rfqId);
|
|
1620
2170
|
this.dirty.delete(rfqId);
|
|
2171
|
+
this.origins.delete(rfqId);
|
|
1621
2172
|
}
|
|
1622
2173
|
/** Every swap still being monitored. */
|
|
1623
2174
|
async getPendingSwaps() {
|
|
@@ -1680,7 +2231,7 @@ var RfqSwapManager = class {
|
|
|
1680
2231
|
// ── internals ────────────────────────────────────────────────────────────
|
|
1681
2232
|
track(swap) {
|
|
1682
2233
|
this.monitored.set(swap.rfqId, swap);
|
|
1683
|
-
this.byLockupScript.set(
|
|
2234
|
+
this.byLockupScript.set(hex9.encode(swap.lockupPkScript), swap);
|
|
1684
2235
|
}
|
|
1685
2236
|
/** Drops the swap from BOTH indexes. The event index is the one that stops
|
|
1686
2237
|
* a late event finding a swap that is gone; `pollSwap`'s own
|
|
@@ -1689,7 +2240,7 @@ var RfqSwapManager = class {
|
|
|
1689
2240
|
* them from silently re-driving a cancelled swap. */
|
|
1690
2241
|
untrack(rfqId) {
|
|
1691
2242
|
const swap = this.monitored.get(rfqId);
|
|
1692
|
-
if (swap) this.byLockupScript.delete(
|
|
2243
|
+
if (swap) this.byLockupScript.delete(hex9.encode(swap.lockupPkScript));
|
|
1693
2244
|
this.monitored.delete(rfqId);
|
|
1694
2245
|
this.refundRefused.delete(rfqId);
|
|
1695
2246
|
this.lastClaimError.delete(rfqId);
|
|
@@ -1751,7 +2302,7 @@ var RfqSwapManager = class {
|
|
|
1751
2302
|
if (!lockup) {
|
|
1752
2303
|
try {
|
|
1753
2304
|
const [existing] = await contracts.getContracts({
|
|
1754
|
-
script:
|
|
2305
|
+
script: hex9.encode(swap.lockupPkScript)
|
|
1755
2306
|
});
|
|
1756
2307
|
if (existing) {
|
|
1757
2308
|
this.registered.set(swap.rfqId, true);
|
|
@@ -1770,13 +2321,13 @@ var RfqSwapManager = class {
|
|
|
1770
2321
|
);
|
|
1771
2322
|
return;
|
|
1772
2323
|
}
|
|
1773
|
-
const script =
|
|
1774
|
-
if (script !==
|
|
2324
|
+
const script = hex9.encode(lockup.script.pkScript);
|
|
2325
|
+
if (script !== hex9.encode(swap.lockupPkScript)) {
|
|
1775
2326
|
this.registered.set(swap.rfqId, false);
|
|
1776
2327
|
this.emitFailed(
|
|
1777
2328
|
swap,
|
|
1778
2329
|
new Error(
|
|
1779
|
-
`swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${
|
|
2330
|
+
`swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${hex9.encode(swap.lockupPkScript)}`
|
|
1780
2331
|
)
|
|
1781
2332
|
);
|
|
1782
2333
|
return;
|
|
@@ -1795,7 +2346,7 @@ var RfqSwapManager = class {
|
|
|
1795
2346
|
* script for its whole life. Best-effort — the swap is over either way. */
|
|
1796
2347
|
retireContract(swap) {
|
|
1797
2348
|
if (!this.deps.contracts || !this.registered.get(swap.rfqId)) return;
|
|
1798
|
-
void this.deps.contracts.setContractWatchState(
|
|
2349
|
+
void this.deps.contracts.setContractWatchState(hex9.encode(swap.lockupPkScript), "retained").catch((error) => this.emitFailed(swap, error));
|
|
1799
2350
|
}
|
|
1800
2351
|
arm() {
|
|
1801
2352
|
if (!this.running) return;
|
|
@@ -1833,6 +2384,7 @@ var RfqSwapManager = class {
|
|
|
1833
2384
|
fate = { fate: "unknown" };
|
|
1834
2385
|
}
|
|
1835
2386
|
if (fate.fate === "claimed" || fate.fate === "returned") {
|
|
2387
|
+
this.stampLockupSpends(swap, fate.spends);
|
|
1836
2388
|
this.setState(swap, fate.fate === "claimed" ? "settled" : "refunded");
|
|
1837
2389
|
return;
|
|
1838
2390
|
}
|
|
@@ -1917,10 +2469,10 @@ var RfqSwapManager = class {
|
|
|
1917
2469
|
);
|
|
1918
2470
|
}
|
|
1919
2471
|
}
|
|
1920
|
-
if (!this.callbacks) {
|
|
2472
|
+
if (!this.callbacks || !this.callbacks.claimLockup) {
|
|
1921
2473
|
return this.block(
|
|
1922
2474
|
swap,
|
|
1923
|
-
"no callbacks are wired, so this wallet cannot claim the lockup"
|
|
2475
|
+
this.callbacks ? "no claimLockup callback is wired, so this wallet cannot claim the lockup" : "no callbacks are wired, so this wallet cannot claim the lockup"
|
|
1924
2476
|
);
|
|
1925
2477
|
}
|
|
1926
2478
|
this.setState(swap, "claimable");
|
|
@@ -1970,8 +2522,15 @@ var RfqSwapManager = class {
|
|
|
1970
2522
|
});
|
|
1971
2523
|
if (action === "claim" && phase.phase === "claimable") {
|
|
1972
2524
|
if (swap.claimTxid) return "continue";
|
|
2525
|
+
if (this.callbacks && !this.callbacks.claimOnchain) {
|
|
2526
|
+
this.block(
|
|
2527
|
+
swap,
|
|
2528
|
+
"no claimOnchain callback is wired, so this wallet cannot claim the L1 fill"
|
|
2529
|
+
);
|
|
2530
|
+
return "handled";
|
|
2531
|
+
}
|
|
1973
2532
|
this.setOnchainState(swap, "claimable");
|
|
1974
|
-
if (!this.config.enableAutoActions || !this.callbacks) return "handled";
|
|
2533
|
+
if (!this.config.enableAutoActions || !this.callbacks?.claimOnchain) return "handled";
|
|
1975
2534
|
try {
|
|
1976
2535
|
const { txid } = await this.callbacks.claimOnchain(swap, phase.utxo);
|
|
1977
2536
|
swap.claimTxid = txid;
|
|
@@ -2084,6 +2643,24 @@ var RfqSwapManager = class {
|
|
|
2084
2643
|
if (swap.state !== "needs_counterparty") return;
|
|
2085
2644
|
this.setState(swap, traderClaimTxid(swap) ? "claimed" : "pending");
|
|
2086
2645
|
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Record which ark transactions ended the lockup.
|
|
2648
|
+
*
|
|
2649
|
+
* Only the ones the indexer actually named: `LockupSpend.arkTxid` is
|
|
2650
|
+
* optional, and a checkpoint txid is not what history correlates on — a
|
|
2651
|
+
* record carrying one would name a transaction the wallet's own activity
|
|
2652
|
+
* never shows. Fewer txids is the right failure here.
|
|
2653
|
+
*
|
|
2654
|
+
* Assigned rather than merged: the fate is one read of the whole lockup,
|
|
2655
|
+
* so it is the complete answer for this swap, and a swap only reaches a
|
|
2656
|
+
* verdict once.
|
|
2657
|
+
*/
|
|
2658
|
+
stampLockupSpends(swap, spends) {
|
|
2659
|
+
const arkTxids = spends.map((spend) => spend.arkTxid).filter((txid) => txid !== void 0);
|
|
2660
|
+
if (arkTxids.length === 0) return;
|
|
2661
|
+
swap.lockupSpendArkTxids = arkTxids;
|
|
2662
|
+
this.touch(swap);
|
|
2663
|
+
}
|
|
2087
2664
|
touch(swap) {
|
|
2088
2665
|
swap.updatedAt = this.config.now();
|
|
2089
2666
|
this.dirty.add(swap.rfqId);
|
|
@@ -2109,9 +2686,29 @@ var RfqSwapManager = class {
|
|
|
2109
2686
|
emitAction(swap, action) {
|
|
2110
2687
|
notify(this.actionExecutedListeners, (listener) => listener(swap, action));
|
|
2111
2688
|
}
|
|
2112
|
-
/**
|
|
2689
|
+
/**
|
|
2690
|
+
* Flush a changed record to every sink that is wired, and say whether all
|
|
2691
|
+
* of them took it.
|
|
2692
|
+
*
|
|
2693
|
+
* Up to two writes, in this order: the canonical `RfqSwapRecord` to
|
|
2694
|
+
* {@link RfqSwapManagerDeps.repository}, then
|
|
2695
|
+
* {@link RfqSwapManagerCallbacks.saveSwap}. Both gate — a rejection from
|
|
2696
|
+
* either leaves the record dirty and monitored, so waiters stay unsettled
|
|
2697
|
+
* and a terminal swap is not finalized until the write it claims lands.
|
|
2698
|
+
* That is exactly today's rule for `saveSwap`, applied to whichever sinks
|
|
2699
|
+
* exist; wiring the repository does not weaken it.
|
|
2700
|
+
*
|
|
2701
|
+
* The canonical write goes FIRST and a failure there skips the second.
|
|
2702
|
+
* `saveSwap` is a projection of the record, and projecting a state the
|
|
2703
|
+
* record of record has just refused would leave the secondary sink ahead
|
|
2704
|
+
* of the primary — the one ordering that survives no restart.
|
|
2705
|
+
*
|
|
2706
|
+
* With neither wired the state is process-local, which is what a manager
|
|
2707
|
+
* with no callbacks has always done.
|
|
2708
|
+
*/
|
|
2113
2709
|
async save(swap) {
|
|
2114
|
-
if (!this.
|
|
2710
|
+
if (!await this.saveRecord(swap)) return false;
|
|
2711
|
+
if (!this.callbacks?.saveSwap) return true;
|
|
2115
2712
|
try {
|
|
2116
2713
|
await this.callbacks.saveSwap(swap);
|
|
2117
2714
|
return true;
|
|
@@ -2120,6 +2717,25 @@ var RfqSwapManager = class {
|
|
|
2120
2717
|
return false;
|
|
2121
2718
|
}
|
|
2122
2719
|
}
|
|
2720
|
+
/** The canonical write. True when there is no repository to write to. */
|
|
2721
|
+
async saveRecord(swap) {
|
|
2722
|
+
const repository = this.deps.repository;
|
|
2723
|
+
if (!repository) return true;
|
|
2724
|
+
try {
|
|
2725
|
+
const stored = await repository.getRfqSwap(swap.rfqId);
|
|
2726
|
+
const record = stored ? updateRfqSwapRecord(stored, swap) : createRfqSwapRecord(this.originOrThrow(swap), swap);
|
|
2727
|
+
await repository.saveRfqSwap(record);
|
|
2728
|
+
return true;
|
|
2729
|
+
} catch (error) {
|
|
2730
|
+
this.emitFailed(swap, error);
|
|
2731
|
+
return false;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
originOrThrow(swap) {
|
|
2735
|
+
const origin = this.origins.get(swap.rfqId);
|
|
2736
|
+
if (!origin) throw new RfqSwapOriginRequired(swap.rfqId);
|
|
2737
|
+
return origin;
|
|
2738
|
+
}
|
|
2123
2739
|
/**
|
|
2124
2740
|
* Drop a terminal swap from monitoring and report it exactly once.
|
|
2125
2741
|
*
|
|
@@ -2179,6 +2795,10 @@ var errorMessage = (error) => error instanceof Error ? error.message : String(er
|
|
|
2179
2795
|
var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
|
|
2180
2796
|
|
|
2181
2797
|
// src/activity.ts
|
|
2798
|
+
import {
|
|
2799
|
+
ArkAddress as ArkAddress4
|
|
2800
|
+
} from "@arkade-os/sdk";
|
|
2801
|
+
import { hex as hex10 } from "@scure/base";
|
|
2182
2802
|
var LABELS = {
|
|
2183
2803
|
lightning_send: "Lightning send",
|
|
2184
2804
|
lightning_receive: "Lightning receive",
|
|
@@ -2231,6 +2851,44 @@ function swapActivityResolver(deps) {
|
|
|
2231
2851
|
}
|
|
2232
2852
|
};
|
|
2233
2853
|
}
|
|
2854
|
+
async function rfqSwapActivityInputs(deps) {
|
|
2855
|
+
const records = await deps.repository.getAllRfqSwaps();
|
|
2856
|
+
return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
|
|
2857
|
+
}
|
|
2858
|
+
async function activityInputOf(record, indexer) {
|
|
2859
|
+
const txids = /* @__PURE__ */ new Set();
|
|
2860
|
+
if (record.fundingArkTxid) txids.add(record.fundingArkTxid);
|
|
2861
|
+
if (record.refundArkTxid) txids.add(record.refundArkTxid);
|
|
2862
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
2863
|
+
for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
|
|
2864
|
+
for (const txid of record.lockupSpendArkTxids ?? []) txids.add(txid);
|
|
2865
|
+
const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundArkTxid && !record.lockupSpendArkTxids?.length;
|
|
2866
|
+
if (indexer && (!record.fundingArkTxid || spendUnknown)) {
|
|
2867
|
+
for (const txid of await lockupTxids(indexer, record, !record.fundingArkTxid)) {
|
|
2868
|
+
txids.add(txid);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
return { rfqId: record.rfqId, kind: record.kind, state: record.state, txids: [...txids] };
|
|
2872
|
+
}
|
|
2873
|
+
async function lockupTxids(indexer, record, wantFunding) {
|
|
2874
|
+
let script;
|
|
2875
|
+
try {
|
|
2876
|
+
script = hex10.encode(ArkAddress4.decode(record.lockupAddress).pkScript);
|
|
2877
|
+
} catch {
|
|
2878
|
+
return [];
|
|
2879
|
+
}
|
|
2880
|
+
try {
|
|
2881
|
+
const { vtxos } = await indexer.getVtxos({ scripts: [script] });
|
|
2882
|
+
const out = [];
|
|
2883
|
+
for (const vtxo of vtxos ?? []) {
|
|
2884
|
+
if (wantFunding) out.push(vtxo.txid);
|
|
2885
|
+
if (vtxo.arkTxId) out.push(vtxo.arkTxId);
|
|
2886
|
+
}
|
|
2887
|
+
return out;
|
|
2888
|
+
} catch {
|
|
2889
|
+
return [];
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2234
2892
|
export {
|
|
2235
2893
|
ARKADE_ASSET,
|
|
2236
2894
|
ARKADE_BTC,
|
|
@@ -2242,6 +2900,7 @@ export {
|
|
|
2242
2900
|
LIGHTNING_RECEIVE_PAIR,
|
|
2243
2901
|
LIGHTNING_SEND_PAIR,
|
|
2244
2902
|
LockupAmountMismatchError,
|
|
2903
|
+
LockupContractMissing,
|
|
2245
2904
|
LockupNeedsRecoveryError,
|
|
2246
2905
|
LockupRegistrationFailed,
|
|
2247
2906
|
MAX_MIN_CONFIRMATIONS,
|
|
@@ -2259,16 +2918,20 @@ export {
|
|
|
2259
2918
|
QUOTE_OPTIONS,
|
|
2260
2919
|
REFUND_MTP_LAG_SECONDS,
|
|
2261
2920
|
RFQ_RESOLVED_STATES,
|
|
2921
|
+
RFQ_SWAP_RETENTION_SECONDS,
|
|
2262
2922
|
RFQ_SWAP_TERMINAL_STATES,
|
|
2263
2923
|
RFQ_TERMINAL_STATES,
|
|
2264
2924
|
RefundNotLocallyPossibleError,
|
|
2265
2925
|
RfqSwapManager,
|
|
2926
|
+
RfqSwapOriginRequired,
|
|
2266
2927
|
SOLO_REFUND_HEADROOM_SECONDS,
|
|
2267
2928
|
SWAP_LOCKUP_CONTRACT_KIND,
|
|
2268
2929
|
SWAP_LOCKUP_CONTRACT_LABEL,
|
|
2269
2930
|
SWAP_LOCKUP_CONTRACT_TYPE,
|
|
2270
2931
|
SwapRefusal,
|
|
2271
2932
|
addAssetSwap,
|
|
2933
|
+
arkadeAssetLeg,
|
|
2934
|
+
arkadeRefunder,
|
|
2272
2935
|
arkadeSwapRequest,
|
|
2273
2936
|
assertFundable,
|
|
2274
2937
|
assertReceivable,
|
|
@@ -2284,6 +2947,7 @@ export {
|
|
|
2284
2947
|
classifyOnchainHtlc,
|
|
2285
2948
|
classifySpend,
|
|
2286
2949
|
createOffer,
|
|
2950
|
+
createRfqSwapRecord,
|
|
2287
2951
|
decodeOffer,
|
|
2288
2952
|
deriveLightningReceive,
|
|
2289
2953
|
deriveOnchainReceive,
|
|
@@ -2301,6 +2965,7 @@ export {
|
|
|
2301
2965
|
lightningReceiveRequest,
|
|
2302
2966
|
lightningSendRequest,
|
|
2303
2967
|
lightningSendVtxoScript,
|
|
2968
|
+
lockupContractParams,
|
|
2304
2969
|
makeCachedFeedFetch,
|
|
2305
2970
|
newPreimage,
|
|
2306
2971
|
newRfqId,
|
|
@@ -2309,12 +2974,14 @@ export {
|
|
|
2309
2974
|
offerVtxoScript,
|
|
2310
2975
|
onchainHtlcScript,
|
|
2311
2976
|
onchainReceiveRequest,
|
|
2977
|
+
onchainSendProfile,
|
|
2312
2978
|
onchainSendRequest,
|
|
2313
2979
|
paymentHashOf,
|
|
2314
2980
|
preimageForSwapRecord,
|
|
2315
2981
|
pushClaim,
|
|
2316
2982
|
pushRefundWithoutReceiver,
|
|
2317
2983
|
readLockupFate,
|
|
2984
|
+
rebuildRfqSwap,
|
|
2318
2985
|
receiveVtxoScript,
|
|
2319
2986
|
refundIfUnresolved,
|
|
2320
2987
|
registerLockupContract,
|
|
@@ -2325,9 +2992,15 @@ export {
|
|
|
2325
2992
|
requestOnchainSend,
|
|
2326
2993
|
restoreAssetSwaps,
|
|
2327
2994
|
retireSettledOfferContracts,
|
|
2995
|
+
rfqClaimSecretOf,
|
|
2328
2996
|
rfqPair,
|
|
2997
|
+
rfqSecretsProfile,
|
|
2998
|
+
rfqSignerOf,
|
|
2999
|
+
rfqSwapActivityInputs,
|
|
3000
|
+
rfqSwapOriginOf,
|
|
2329
3001
|
sealClaimPacket,
|
|
2330
3002
|
senderIdentityForSwapRecord,
|
|
3003
|
+
shouldRetainRfqSwap,
|
|
2331
3004
|
spendTxidsOf,
|
|
2332
3005
|
spendUpdate,
|
|
2333
3006
|
swapActivityResolver,
|
|
@@ -2338,6 +3011,7 @@ export {
|
|
|
2338
3011
|
unilateralRefundWithoutReceiverDelay,
|
|
2339
3012
|
updateAssetSwap,
|
|
2340
3013
|
updateAssetSwapBestEffort,
|
|
3014
|
+
updateRfqSwapRecord,
|
|
2341
3015
|
validatePlan,
|
|
2342
3016
|
verifyLockupAddress,
|
|
2343
3017
|
verifyReceiveInvoice,
|