@arkade-os/swap 0.0.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/dist/nostr.cjs ADDED
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/nostr.ts
31
+ var nostr_exports = {};
32
+ __export(nostr_exports, {
33
+ RFQ_AD_KIND: () => RFQ_AD_KIND,
34
+ RFQ_DIRECTED_KIND: () => RFQ_DIRECTED_KIND,
35
+ RelayUnavailable: () => RelayUnavailable,
36
+ nostrRfqTransport: () => nostrRfqTransport
37
+ });
38
+ module.exports = __toCommonJS(nostr_exports);
39
+
40
+ // src/rfq.ts
41
+ var import_base5 = require("@scure/base");
42
+ var import_legacy2 = require("@noble/hashes/legacy.js");
43
+ var import_sdk3 = require("@arkade-os/sdk");
44
+
45
+ // src/onchainHtlc.ts
46
+ var import_base = require("@scure/base");
47
+ var import_sha2 = require("@noble/hashes/sha2.js");
48
+ var import_legacy = require("@noble/hashes/legacy.js");
49
+ var btc = __toESM(require("@scure/btc-signer"), 1);
50
+ var ONCHAIN_ORDER_MARGIN_SECONDS = 2 * 60 * 60;
51
+ var ONCHAIN_CLAIM_MARGIN_SECONDS = 90 * 60;
52
+ var ONCHAIN_DUST_SATS = BigInt(330);
53
+ var L1_NETWORKS = {
54
+ bitcoin: btc.NETWORK,
55
+ testnet: btc.TEST_NETWORK,
56
+ regtest: { ...btc.TEST_NETWORK, bech32: "bcrt" }
57
+ };
58
+
59
+ // src/secrets.ts
60
+ var import_base2 = require("@scure/base");
61
+ var import_sha22 = require("@noble/hashes/sha2.js");
62
+ var import_utils = require("@noble/hashes/utils.js");
63
+ var import_secp256k1 = require("@noble/curves/secp256k1.js");
64
+ var import_sdk = require("@arkade-os/sdk");
65
+
66
+ // src/claimPacket.ts
67
+ var import_base3 = require("@scure/base");
68
+ var import_secp256k12 = require("@noble/curves/secp256k1.js");
69
+ var import_hkdf = require("@noble/hashes/hkdf.js");
70
+ var import_sha23 = require("@noble/hashes/sha2.js");
71
+ var HKDF_INFO = new TextEncoder().encode("covclaimd/preimage/v1");
72
+
73
+ // src/lockupContract.ts
74
+ var import_base4 = require("@scure/base");
75
+ var import_sdk2 = require("@arkade-os/sdk");
76
+
77
+ // src/rfq.ts
78
+ var ARKADE_BTC = "arkade:BTC";
79
+ var LIGHTNING_BTC = "lightning:BTC";
80
+ var ONCHAIN_BTC = "onchain:BTC";
81
+ var rfqPair = (from, to) => `${from}->${to}`;
82
+ var LIGHTNING_SEND_PAIR = rfqPair(ARKADE_BTC, LIGHTNING_BTC);
83
+ var LIGHTNING_RECEIVE_PAIR = rfqPair(LIGHTNING_BTC, ARKADE_BTC);
84
+ var ONCHAIN_SEND_PAIR = rfqPair(ARKADE_BTC, ONCHAIN_BTC);
85
+ var ONCHAIN_RECEIVE_PAIR = rfqPair(ONCHAIN_BTC, ARKADE_BTC);
86
+ var SwapRefusal = class extends Error {
87
+ reason;
88
+ rfqId;
89
+ constructor(reason, rfqId) {
90
+ super(`solver refused: ${reason}`);
91
+ this.name = "SwapRefusal";
92
+ this.reason = reason;
93
+ this.rfqId = rfqId;
94
+ }
95
+ };
96
+ var MIN_HEADROOM_SECONDS = 90 * 60;
97
+ var MIN_CLAIM_WINDOW_SECONDS = 30 * 60;
98
+
99
+ // src/nostr.ts
100
+ var import_nostr_tools = require("nostr-tools");
101
+ var RFQ_DIRECTED_KIND = 24859;
102
+ var RFQ_AD_KIND = 38859;
103
+ var DEFAULT_TIMEOUT_MS = 3e4;
104
+ var RelayUnavailable = class extends Error {
105
+ reasons;
106
+ constructor(reasons) {
107
+ super(`lost every relay connection: ${reasons.join("; ") || "connection closed"}`);
108
+ this.name = "RelayUnavailable";
109
+ this.reasons = reasons;
110
+ }
111
+ };
112
+ var closeReasons = (raw) => raw.map((entry) => {
113
+ if (typeof entry === "string") return entry;
114
+ const { url, reason } = entry ?? {};
115
+ return url ? `${url}: ${reason ?? "closed"}` : reason ?? "closed";
116
+ });
117
+ var asQuote = (payload, rfqId) => {
118
+ const p = payload;
119
+ if (p?.type === "rfq_refusal") throw new SwapRefusal(p.reason ?? "unknown", p.rfq_id ?? rfqId);
120
+ if (p?.type !== "rfq_quote" || p.rfq_id !== rfqId) {
121
+ throw new Error(`unexpected reply: ${p?.type ?? "no payload"}`);
122
+ }
123
+ return payload;
124
+ };
125
+ var nostrRfqTransport = (options) => {
126
+ const relays = options.relays;
127
+ const solverPubkey = options.solverPubkey;
128
+ const secretKey = options.secretKey ?? (0, import_nostr_tools.generateSecretKey)();
129
+ const pubkey = (0, import_nostr_tools.getPublicKey)(secretKey);
130
+ const pool = options.pool ?? new import_nostr_tools.SimplePool();
131
+ const ownsPool = !options.pool;
132
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
133
+ const conversationKey = import_nostr_tools.nip44.v2.utils.getConversationKey(secretKey, solverPubkey);
134
+ let closed = false;
135
+ const waiters = /* @__PURE__ */ new Map();
136
+ const subscription = pool.subscribeMany(
137
+ relays,
138
+ { kinds: [RFQ_DIRECTED_KIND], "#p": [pubkey], authors: [solverPubkey] },
139
+ {
140
+ onevent(event) {
141
+ let payload;
142
+ try {
143
+ payload = JSON.parse(import_nostr_tools.nip44.v2.decrypt(event.content, conversationKey));
144
+ } catch {
145
+ return;
146
+ }
147
+ const rfqId = payload?.rfq_id;
148
+ if (!rfqId) return;
149
+ waiters.get(rfqId)?.resolve(payload);
150
+ },
151
+ // subscribeMany calls this once every relay has closed. Nothing will
152
+ // arrive after it, so failing now beats waiting out the timeout — and
153
+ // it names the real cause instead of implicating the solver.
154
+ onclose(reasons) {
155
+ if (closed) return;
156
+ const error = new RelayUnavailable(closeReasons(reasons));
157
+ for (const waiter of waiters.values()) waiter.reject(error);
158
+ waiters.clear();
159
+ }
160
+ }
161
+ );
162
+ const send = async (payload) => {
163
+ const event = (0, import_nostr_tools.finalizeEvent)(
164
+ {
165
+ kind: RFQ_DIRECTED_KIND,
166
+ created_at: Math.floor(Date.now() / 1e3),
167
+ tags: [["p", solverPubkey]],
168
+ content: import_nostr_tools.nip44.v2.encrypt(JSON.stringify(payload), conversationKey)
169
+ },
170
+ secretKey
171
+ );
172
+ const results = await Promise.allSettled(pool.publish(relays, event));
173
+ if (!results.some((r) => r.status === "fulfilled")) {
174
+ throw new Error("no relay accepted the RFQ message");
175
+ }
176
+ };
177
+ const awaitReply = (rfqId) => new Promise((resolve, reject) => {
178
+ const timer = setTimeout(() => {
179
+ waiters.delete(rfqId);
180
+ reject(new Error(`no solver reply within ${timeoutMs}ms`));
181
+ }, timeoutMs);
182
+ waiters.set(rfqId, {
183
+ resolve: (payload) => {
184
+ clearTimeout(timer);
185
+ waiters.delete(rfqId);
186
+ resolve(payload);
187
+ },
188
+ reject: (error) => {
189
+ clearTimeout(timer);
190
+ waiters.delete(rfqId);
191
+ reject(error);
192
+ }
193
+ });
194
+ });
195
+ return {
196
+ async requestQuote(payload) {
197
+ const rfqId = String(payload.rfq_id);
198
+ const reply = awaitReply(rfqId);
199
+ await send(payload);
200
+ return asQuote(await reply, rfqId);
201
+ },
202
+ async status(rfqId) {
203
+ const reply = awaitReply(rfqId);
204
+ await send({ v: 1, type: "rfq_status_request", rfq_id: rfqId });
205
+ const payload = await reply;
206
+ if (payload?.type !== "rfq_status") return null;
207
+ return payload;
208
+ },
209
+ async close() {
210
+ closed = true;
211
+ subscription.close();
212
+ waiters.clear();
213
+ if (ownsPool) pool.close(relays);
214
+ }
215
+ };
216
+ };
217
+ // Annotate the CommonJS export names for ESM import in node:
218
+ 0 && (module.exports = {
219
+ RFQ_AD_KIND,
220
+ RFQ_DIRECTED_KIND,
221
+ RelayUnavailable,
222
+ nostrRfqTransport
223
+ });
@@ -0,0 +1,99 @@
1
+ import { b as RfqTransport } from './rfq-CRgIOQ_y.cjs';
2
+ import { SimplePool } from 'nostr-tools';
3
+ import '@arkade-os/sdk';
4
+ import '@arkade-os/solver-discovery';
5
+
6
+ /**
7
+ * The Nostr RFQ transport — the PRODUCTION one (docs/rfq-protocol.md § 3.1 in
8
+ * arkade-os/lightning-swap-service).
9
+ *
10
+ * `rfq.ts` ships two other transports: `httpTransport`, and `relayTransport`
11
+ * which speaks the dev broker's `{op:"sub"|"event"}` framing. Neither is what a
12
+ * deployed solver listens on. This is, and it satisfies the same
13
+ * {@link RfqTransport} interface, so everything above the transport — the user
14
+ * flow, quote verification, the store — is identical whichever one it is handed.
15
+ *
16
+ * ## Why this is a separate entry point
17
+ *
18
+ * Imported as `@arkade-os/swap/nostr`, never from the package root, and
19
+ * `nostr-tools` is an OPTIONAL peer dependency. A consumer doing HTTP-only
20
+ * swaps should not pay for a Nostr library it never calls, and re-exporting
21
+ * this from `index.ts` would put `nostr-tools` in every consumer's module graph
22
+ * whether or not they resolve it. The subpath is what keeps that promise: the
23
+ * module is only loaded if it is imported.
24
+ *
25
+ * The consequence, stated plainly: importing this subpath without `nostr-tools`
26
+ * installed fails at resolution with a module-not-found. That is the intended
27
+ * failure — loud, at the import, naming the missing package — rather than a
28
+ * transport that silently degrades.
29
+ *
30
+ * A dynamic `await import()` inside the factory would also defer the cost, but
31
+ * it would make construction async. This transport opens its subscription
32
+ * eagerly, on purpose (see below), so an async factory would push a `await`
33
+ * into every caller for no benefit the subpath does not already give.
34
+ */
35
+
36
+ /**
37
+ * Directed RFQ traffic. Provisional in the spec; kept in one place.
38
+ *
39
+ * MUST stay inside NIP-01's ephemeral range (20000–29999) and MUST match the
40
+ * solver's `NOSTR_KIND_DIRECTED`. The two sides subscribe by `kinds`, so a
41
+ * mismatch is not an error either can report — they simply never see each
42
+ * other, and every request times out blaming the solver.
43
+ *
44
+ * Ephemeral because a negotiation is worthless once it is over: a request
45
+ * nobody answered inside the timeout, and a quote past its `valid_until`, help
46
+ * no one, while a retained copy is a permanent record of who negotiated what
47
+ * with whom. The cost is no store-and-forward — a request sent while the solver
48
+ * is disconnected is dropped rather than queued — which the timeout and the
49
+ * caller's retry cover.
50
+ */
51
+ declare const RFQ_DIRECTED_KIND = 24859;
52
+ /**
53
+ * Indicative solver advertisement — never binding; only a quote binds.
54
+ *
55
+ * Addressable (30000–39999) rather than ephemeral, deliberately: an ad is
56
+ * standing state a relay should keep the current version of, not a message in a
57
+ * negotiation. The two constants want OPPOSITE retention, and neither is a typo
58
+ * of the other.
59
+ */
60
+ declare const RFQ_AD_KIND = 38859;
61
+ /**
62
+ * Every relay dropped the subscription, so no reply can arrive on it.
63
+ *
64
+ * Distinct from a timeout on purpose. Both look like "no answer", but they mean
65
+ * opposite things: a timeout says the solver did not respond, while this says
66
+ * we were never in a position to hear it. Without the distinction the transport
67
+ * waits out the full timeout and then blames the solver for a failure on our
68
+ * own side of the wire.
69
+ */
70
+ declare class RelayUnavailable extends Error {
71
+ readonly reasons: string[];
72
+ constructor(reasons: string[]);
73
+ }
74
+ interface NostrRfqOptions {
75
+ /** Relay URLs from the solver's card. The rendezvous, not solver endpoints. */
76
+ relays: string[];
77
+ /** The card's `discovery_pubkey`, x-only hex — who we address. */
78
+ solverPubkey: string;
79
+ /**
80
+ * Transport key. Defaults to a FRESH key per transport, deliberately: the
81
+ * negotiation should not be linkable to a long-term identity, and nothing in
82
+ * the protocol needs a stable client key — the quote binds to the covenant,
83
+ * not to who asked for it.
84
+ */
85
+ secretKey?: Uint8Array;
86
+ /** Injectable for tests; a caller may also share one pool across swaps. */
87
+ pool?: SimplePool;
88
+ timeoutMs?: number;
89
+ }
90
+ /**
91
+ * Build an `RfqTransport` speaking kind-24859 directed traffic.
92
+ *
93
+ * Sends are fire-and-forget publishes; replies arrive on a single long-lived
94
+ * subscription filtered to this transport key, so a reply that arrives before
95
+ * the publish promise settles is not missed.
96
+ */
97
+ declare const nostrRfqTransport: (options: NostrRfqOptions) => RfqTransport;
98
+
99
+ export { type NostrRfqOptions, RFQ_AD_KIND, RFQ_DIRECTED_KIND, RelayUnavailable, nostrRfqTransport };
@@ -0,0 +1,99 @@
1
+ import { b as RfqTransport } from './rfq-CRgIOQ_y.js';
2
+ import { SimplePool } from 'nostr-tools';
3
+ import '@arkade-os/sdk';
4
+ import '@arkade-os/solver-discovery';
5
+
6
+ /**
7
+ * The Nostr RFQ transport — the PRODUCTION one (docs/rfq-protocol.md § 3.1 in
8
+ * arkade-os/lightning-swap-service).
9
+ *
10
+ * `rfq.ts` ships two other transports: `httpTransport`, and `relayTransport`
11
+ * which speaks the dev broker's `{op:"sub"|"event"}` framing. Neither is what a
12
+ * deployed solver listens on. This is, and it satisfies the same
13
+ * {@link RfqTransport} interface, so everything above the transport — the user
14
+ * flow, quote verification, the store — is identical whichever one it is handed.
15
+ *
16
+ * ## Why this is a separate entry point
17
+ *
18
+ * Imported as `@arkade-os/swap/nostr`, never from the package root, and
19
+ * `nostr-tools` is an OPTIONAL peer dependency. A consumer doing HTTP-only
20
+ * swaps should not pay for a Nostr library it never calls, and re-exporting
21
+ * this from `index.ts` would put `nostr-tools` in every consumer's module graph
22
+ * whether or not they resolve it. The subpath is what keeps that promise: the
23
+ * module is only loaded if it is imported.
24
+ *
25
+ * The consequence, stated plainly: importing this subpath without `nostr-tools`
26
+ * installed fails at resolution with a module-not-found. That is the intended
27
+ * failure — loud, at the import, naming the missing package — rather than a
28
+ * transport that silently degrades.
29
+ *
30
+ * A dynamic `await import()` inside the factory would also defer the cost, but
31
+ * it would make construction async. This transport opens its subscription
32
+ * eagerly, on purpose (see below), so an async factory would push a `await`
33
+ * into every caller for no benefit the subpath does not already give.
34
+ */
35
+
36
+ /**
37
+ * Directed RFQ traffic. Provisional in the spec; kept in one place.
38
+ *
39
+ * MUST stay inside NIP-01's ephemeral range (20000–29999) and MUST match the
40
+ * solver's `NOSTR_KIND_DIRECTED`. The two sides subscribe by `kinds`, so a
41
+ * mismatch is not an error either can report — they simply never see each
42
+ * other, and every request times out blaming the solver.
43
+ *
44
+ * Ephemeral because a negotiation is worthless once it is over: a request
45
+ * nobody answered inside the timeout, and a quote past its `valid_until`, help
46
+ * no one, while a retained copy is a permanent record of who negotiated what
47
+ * with whom. The cost is no store-and-forward — a request sent while the solver
48
+ * is disconnected is dropped rather than queued — which the timeout and the
49
+ * caller's retry cover.
50
+ */
51
+ declare const RFQ_DIRECTED_KIND = 24859;
52
+ /**
53
+ * Indicative solver advertisement — never binding; only a quote binds.
54
+ *
55
+ * Addressable (30000–39999) rather than ephemeral, deliberately: an ad is
56
+ * standing state a relay should keep the current version of, not a message in a
57
+ * negotiation. The two constants want OPPOSITE retention, and neither is a typo
58
+ * of the other.
59
+ */
60
+ declare const RFQ_AD_KIND = 38859;
61
+ /**
62
+ * Every relay dropped the subscription, so no reply can arrive on it.
63
+ *
64
+ * Distinct from a timeout on purpose. Both look like "no answer", but they mean
65
+ * opposite things: a timeout says the solver did not respond, while this says
66
+ * we were never in a position to hear it. Without the distinction the transport
67
+ * waits out the full timeout and then blames the solver for a failure on our
68
+ * own side of the wire.
69
+ */
70
+ declare class RelayUnavailable extends Error {
71
+ readonly reasons: string[];
72
+ constructor(reasons: string[]);
73
+ }
74
+ interface NostrRfqOptions {
75
+ /** Relay URLs from the solver's card. The rendezvous, not solver endpoints. */
76
+ relays: string[];
77
+ /** The card's `discovery_pubkey`, x-only hex — who we address. */
78
+ solverPubkey: string;
79
+ /**
80
+ * Transport key. Defaults to a FRESH key per transport, deliberately: the
81
+ * negotiation should not be linkable to a long-term identity, and nothing in
82
+ * the protocol needs a stable client key — the quote binds to the covenant,
83
+ * not to who asked for it.
84
+ */
85
+ secretKey?: Uint8Array;
86
+ /** Injectable for tests; a caller may also share one pool across swaps. */
87
+ pool?: SimplePool;
88
+ timeoutMs?: number;
89
+ }
90
+ /**
91
+ * Build an `RfqTransport` speaking kind-24859 directed traffic.
92
+ *
93
+ * Sends are fire-and-forget publishes; replies arrive on a single long-lived
94
+ * subscription filtered to this transport key, so a reply that arrives before
95
+ * the publish promise settles is not missed.
96
+ */
97
+ declare const nostrRfqTransport: (options: NostrRfqOptions) => RfqTransport;
98
+
99
+ export { type NostrRfqOptions, RFQ_AD_KIND, RFQ_DIRECTED_KIND, RelayUnavailable, nostrRfqTransport };
package/dist/nostr.js ADDED
@@ -0,0 +1,134 @@
1
+ import {
2
+ SwapRefusal
3
+ } from "./chunk-C5P7R7JT.js";
4
+
5
+ // src/nostr.ts
6
+ import {
7
+ finalizeEvent,
8
+ generateSecretKey,
9
+ getPublicKey,
10
+ nip44,
11
+ SimplePool
12
+ } from "nostr-tools";
13
+ var RFQ_DIRECTED_KIND = 24859;
14
+ var RFQ_AD_KIND = 38859;
15
+ var DEFAULT_TIMEOUT_MS = 3e4;
16
+ var RelayUnavailable = class extends Error {
17
+ reasons;
18
+ constructor(reasons) {
19
+ super(`lost every relay connection: ${reasons.join("; ") || "connection closed"}`);
20
+ this.name = "RelayUnavailable";
21
+ this.reasons = reasons;
22
+ }
23
+ };
24
+ var closeReasons = (raw) => raw.map((entry) => {
25
+ if (typeof entry === "string") return entry;
26
+ const { url, reason } = entry ?? {};
27
+ return url ? `${url}: ${reason ?? "closed"}` : reason ?? "closed";
28
+ });
29
+ var asQuote = (payload, rfqId) => {
30
+ const p = payload;
31
+ if (p?.type === "rfq_refusal") throw new SwapRefusal(p.reason ?? "unknown", p.rfq_id ?? rfqId);
32
+ if (p?.type !== "rfq_quote" || p.rfq_id !== rfqId) {
33
+ throw new Error(`unexpected reply: ${p?.type ?? "no payload"}`);
34
+ }
35
+ return payload;
36
+ };
37
+ var nostrRfqTransport = (options) => {
38
+ const relays = options.relays;
39
+ const solverPubkey = options.solverPubkey;
40
+ const secretKey = options.secretKey ?? generateSecretKey();
41
+ const pubkey = getPublicKey(secretKey);
42
+ const pool = options.pool ?? new SimplePool();
43
+ const ownsPool = !options.pool;
44
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
45
+ const conversationKey = nip44.v2.utils.getConversationKey(secretKey, solverPubkey);
46
+ let closed = false;
47
+ const waiters = /* @__PURE__ */ new Map();
48
+ const subscription = pool.subscribeMany(
49
+ relays,
50
+ { kinds: [RFQ_DIRECTED_KIND], "#p": [pubkey], authors: [solverPubkey] },
51
+ {
52
+ onevent(event) {
53
+ let payload;
54
+ try {
55
+ payload = JSON.parse(nip44.v2.decrypt(event.content, conversationKey));
56
+ } catch {
57
+ return;
58
+ }
59
+ const rfqId = payload?.rfq_id;
60
+ if (!rfqId) return;
61
+ waiters.get(rfqId)?.resolve(payload);
62
+ },
63
+ // subscribeMany calls this once every relay has closed. Nothing will
64
+ // arrive after it, so failing now beats waiting out the timeout — and
65
+ // it names the real cause instead of implicating the solver.
66
+ onclose(reasons) {
67
+ if (closed) return;
68
+ const error = new RelayUnavailable(closeReasons(reasons));
69
+ for (const waiter of waiters.values()) waiter.reject(error);
70
+ waiters.clear();
71
+ }
72
+ }
73
+ );
74
+ const send = async (payload) => {
75
+ const event = finalizeEvent(
76
+ {
77
+ kind: RFQ_DIRECTED_KIND,
78
+ created_at: Math.floor(Date.now() / 1e3),
79
+ tags: [["p", solverPubkey]],
80
+ content: nip44.v2.encrypt(JSON.stringify(payload), conversationKey)
81
+ },
82
+ secretKey
83
+ );
84
+ const results = await Promise.allSettled(pool.publish(relays, event));
85
+ if (!results.some((r) => r.status === "fulfilled")) {
86
+ throw new Error("no relay accepted the RFQ message");
87
+ }
88
+ };
89
+ const awaitReply = (rfqId) => new Promise((resolve, reject) => {
90
+ const timer = setTimeout(() => {
91
+ waiters.delete(rfqId);
92
+ reject(new Error(`no solver reply within ${timeoutMs}ms`));
93
+ }, timeoutMs);
94
+ waiters.set(rfqId, {
95
+ resolve: (payload) => {
96
+ clearTimeout(timer);
97
+ waiters.delete(rfqId);
98
+ resolve(payload);
99
+ },
100
+ reject: (error) => {
101
+ clearTimeout(timer);
102
+ waiters.delete(rfqId);
103
+ reject(error);
104
+ }
105
+ });
106
+ });
107
+ return {
108
+ async requestQuote(payload) {
109
+ const rfqId = String(payload.rfq_id);
110
+ const reply = awaitReply(rfqId);
111
+ await send(payload);
112
+ return asQuote(await reply, rfqId);
113
+ },
114
+ async status(rfqId) {
115
+ const reply = awaitReply(rfqId);
116
+ await send({ v: 1, type: "rfq_status_request", rfq_id: rfqId });
117
+ const payload = await reply;
118
+ if (payload?.type !== "rfq_status") return null;
119
+ return payload;
120
+ },
121
+ async close() {
122
+ closed = true;
123
+ subscription.close();
124
+ waiters.clear();
125
+ if (ownsPool) pool.close(relays);
126
+ }
127
+ };
128
+ };
129
+ export {
130
+ RFQ_AD_KIND,
131
+ RFQ_DIRECTED_KIND,
132
+ RelayUnavailable,
133
+ nostrRfqTransport
134
+ };