@satora/swap 0.2.0 → 0.3.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 +4 -0
- package/dist/{bitcoin-reader-esplora-GRUP7RAG.js → bitcoin-reader-esplora-HV6CANAA.js} +2 -2
- package/dist/{chunk-J35RZBDW.js → chunk-EIWELJJK.js} +25 -9
- package/dist/chunk-EIWELJJK.js.map +1 -0
- package/dist/index.cjs +737 -208
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +301 -87
- package/dist/index.d.ts +301 -87
- package/dist/index.js +720 -199
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-J35RZBDW.js.map +0 -1
- /package/dist/{bitcoin-reader-esplora-GRUP7RAG.js.map → bitcoin-reader-esplora-HV6CANAA.js.map} +0 -0
package/README.md
CHANGED
|
@@ -16,6 +16,10 @@ New code should import `@satora/swap`.
|
|
|
16
16
|
npm install @satora/swap
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Compatibility headers
|
|
20
|
+
|
|
21
|
+
API requests include `X-Lendaswap-Client: <sdk-name>/<semver>` and `x-satora-server-version: <server-api-semver>`. The server-version header declares the server/API version this SDK was built against; the API logs it for diagnostics and does not enforce compatibility yet.
|
|
22
|
+
|
|
19
23
|
## Supported swaps
|
|
20
24
|
|
|
21
25
|
| Direction | Source | Target |
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
DEFAULT_ESPLORA_URLS,
|
|
3
3
|
esploraReader,
|
|
4
4
|
htlcFactsFromEsploraTxs
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-EIWELJJK.js";
|
|
6
6
|
export {
|
|
7
7
|
DEFAULT_ESPLORA_URLS,
|
|
8
8
|
esploraReader,
|
|
9
9
|
htlcFactsFromEsploraTxs
|
|
10
10
|
};
|
|
11
|
-
//# sourceMappingURL=bitcoin-reader-esplora-
|
|
11
|
+
//# sourceMappingURL=bitcoin-reader-esplora-HV6CANAA.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/contracts/bitcoin-reader-esplora.ts
|
|
2
|
-
function htlcFactsFromEsploraTxs(txs, address) {
|
|
2
|
+
function htlcFactsFromEsploraTxs(txs, address, opts) {
|
|
3
3
|
for (const tx of txs) {
|
|
4
4
|
const spend = tx.vin.find(
|
|
5
5
|
(vin) => vin.prevout?.scriptpubkey_address === address
|
|
@@ -17,8 +17,15 @@ function htlcFactsFromEsploraTxs(txs, address) {
|
|
|
17
17
|
);
|
|
18
18
|
if (funding) {
|
|
19
19
|
const fundedSats = funding.vout.filter((vout) => vout.scriptpubkey_address === address).reduce((sum, vout) => sum + (vout.value ?? 0), 0);
|
|
20
|
+
const minConf = opts?.minConfirmations ?? 0;
|
|
21
|
+
let deepEnough;
|
|
22
|
+
if (funding.status?.confirmed) {
|
|
23
|
+
deepEnough = minConf <= 1 || opts?.tipHeight !== void 0 && funding.status.block_height !== void 0 && opts.tipHeight - funding.status.block_height + 1 >= minConf;
|
|
24
|
+
} else {
|
|
25
|
+
deepEnough = minConf === 0;
|
|
26
|
+
}
|
|
20
27
|
return {
|
|
21
|
-
funding:
|
|
28
|
+
funding: deepEnough ? "confirmed" : "mempool",
|
|
22
29
|
fundedSats
|
|
23
30
|
};
|
|
24
31
|
}
|
|
@@ -28,13 +35,15 @@ var DEFAULT_ESPLORA_URLS = [
|
|
|
28
35
|
"https://mempool.space/api",
|
|
29
36
|
"https://blockstream.info/api"
|
|
30
37
|
];
|
|
31
|
-
function esploraReader(esploraUrls, fetchImpl = fetch) {
|
|
38
|
+
function esploraReader(esploraUrls, fetchImpl = fetch, policy) {
|
|
32
39
|
const bases = (Array.isArray(esploraUrls) ? esploraUrls : [esploraUrls]).map(
|
|
33
40
|
(url) => url.replace(/\/+$/, "")
|
|
34
41
|
);
|
|
42
|
+
const minConf = policy?.minConfirmations ?? 0;
|
|
35
43
|
let start = 0;
|
|
36
44
|
return {
|
|
37
|
-
async getHtlcFacts(address) {
|
|
45
|
+
async getHtlcFacts(address, minConfirmations) {
|
|
46
|
+
const required = minConfirmations ?? minConf;
|
|
38
47
|
const from = start++ % bases.length;
|
|
39
48
|
let lastError;
|
|
40
49
|
for (let i = 0; i < bases.length; i++) {
|
|
@@ -44,10 +53,17 @@ function esploraReader(esploraUrls, fetchImpl = fetch) {
|
|
|
44
53
|
`${base}/address/${encodeURIComponent(address)}/txs`
|
|
45
54
|
);
|
|
46
55
|
if (!res.ok) throw new Error(`esplora ${res.status} at ${base}`);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
const txs = await res.json();
|
|
57
|
+
let tipHeight;
|
|
58
|
+
if (required > 1) {
|
|
59
|
+
const tip = await fetchImpl(`${base}/blocks/tip/height`);
|
|
60
|
+
if (!tip.ok) throw new Error(`esplora ${tip.status} at ${base}`);
|
|
61
|
+
tipHeight = Number(await tip.text());
|
|
62
|
+
}
|
|
63
|
+
return htlcFactsFromEsploraTxs(txs, address, {
|
|
64
|
+
minConfirmations: required,
|
|
65
|
+
tipHeight
|
|
66
|
+
});
|
|
51
67
|
} catch (error) {
|
|
52
68
|
lastError = error;
|
|
53
69
|
}
|
|
@@ -62,4 +78,4 @@ export {
|
|
|
62
78
|
DEFAULT_ESPLORA_URLS,
|
|
63
79
|
esploraReader
|
|
64
80
|
};
|
|
65
|
-
//# sourceMappingURL=chunk-
|
|
81
|
+
//# sourceMappingURL=chunk-EIWELJJK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/contracts/bitcoin-reader-esplora.ts"],"sourcesContent":["/**\n * A esplora-backed {@link BitcoinChainReader} — the concrete chain source the\n * {@link BitcoinContractManager} uses in production.\n *\n * Kept separate from the manager so the manager stays free of any HTTP/esplora\n * dependency (and unit-testable against a fake reader). A single\n * `GET /address/{addr}/txs` yields both the funding state and the spending\n * input's witness, so no extra round-trips are needed.\n */\n\nimport type { BitcoinHtlcFacts } from \"./bitcoin.js\";\nimport type { BitcoinChainReader } from \"./bitcoin-manager.js\";\n\n/** The slice of esplora's tx JSON we read. */\ntype EsploraTx = {\n vin: Array<{\n witness?: string[];\n prevout?: { scriptpubkey_address?: string } | null;\n }>;\n vout: Array<{ scriptpubkey_address?: string; value?: number }>;\n status?: { confirmed?: boolean; block_height?: number };\n};\n\n/**\n * When a funding tx counts as `confirmed` for observation purposes.\n *\n * `minConfirmations: 0` (the default) accepts an UNCONFIRMED funding, so a swap\n * claims as soon as the funding hits the mempool instead of waiting ~10min for\n * a block. This TRUSTS the funder not to double-spend: claiming publishes the\n * preimage, so a funder who then replaces its funding tx could take both legs.\n * Our own server therefore broadcasts HTLC fundings without signalling RBF.\n * Note that a non-signalling tx is still replaceable by full-RBF miners — set\n * `1` (or more) for a policy that doesn't rely on the funder's good behaviour;\n * values above 1 make the reader fetch the tip height to compute depth.\n */\nexport type BitcoinConfirmationPolicy = {\n minConfirmations?: number;\n};\n\n/**\n * Reduce an address's esplora tx history to HTLC facts. If a tx spends an output\n * at the address, that's the resolving spend (its witness classifies claim vs\n * refund); otherwise a tx paying the address is the funding, gated by the\n * confirmation policy (see {@link BitcoinConfirmationPolicy}).\n */\nexport function htlcFactsFromEsploraTxs(\n txs: EsploraTx[],\n address: string,\n opts?: BitcoinConfirmationPolicy & { tipHeight?: number },\n): BitcoinHtlcFacts {\n for (const tx of txs) {\n const spend = tx.vin.find(\n (vin) => vin.prevout?.scriptpubkey_address === address,\n );\n if (spend)\n return {\n funding: \"confirmed\",\n fundedSats: 0, // already spent — the amount no longer matters\n spendWitness: spend.witness ?? [],\n };\n }\n const funding = txs.find((tx) =>\n tx.vout.some((vout) => vout.scriptpubkey_address === address),\n );\n if (funding) {\n const fundedSats = funding.vout\n .filter((vout) => vout.scriptpubkey_address === address)\n .reduce((sum, vout) => sum + (vout.value ?? 0), 0);\n const minConf = opts?.minConfirmations ?? 0;\n let deepEnough: boolean;\n if (funding.status?.confirmed) {\n deepEnough =\n minConf <= 1 ||\n (opts?.tipHeight !== undefined &&\n funding.status.block_height !== undefined &&\n opts.tipHeight - funding.status.block_height + 1 >= minConf);\n } else {\n deepEnough = minConf === 0;\n }\n return {\n funding: deepEnough ? \"confirmed\" : \"mempool\",\n fundedSats,\n };\n }\n return { funding: \"absent\", fundedSats: 0 };\n}\n\n/**\n * Public esplora endpoints tried in rotation (mainnet). blockstream.info runs the\n * reference esplora, so it's API-compatible with mempool.space; spreading calls\n * across both halves the per-provider load and survives one being throttled.\n */\nexport const DEFAULT_ESPLORA_URLS = [\n \"https://mempool.space/api\",\n \"https://blockstream.info/api\",\n];\n\n/**\n * Build a {@link BitcoinChainReader} over one or more esplora REST endpoints.\n * With several, each call starts at a rotating endpoint (to spread load across\n * providers) and fails over to the rest on error — so a throttled or failing\n * provider doesn't stall tracking.\n */\nexport function esploraReader(\n esploraUrls: string | string[],\n fetchImpl: typeof fetch = fetch,\n policy?: BitcoinConfirmationPolicy,\n): BitcoinChainReader {\n const bases = (Array.isArray(esploraUrls) ? esploraUrls : [esploraUrls]).map(\n (url) => url.replace(/\\/+$/, \"\"),\n );\n const minConf = policy?.minConfirmations ?? 0;\n let start = 0;\n return {\n async getHtlcFacts(address, minConfirmations) {\n const required = minConfirmations ?? minConf;\n const from = start++ % bases.length; // rotate the primary to spread load\n let lastError: unknown;\n for (let i = 0; i < bases.length; i++) {\n const base = bases[(from + i) % bases.length];\n try {\n const res = await fetchImpl(\n `${base}/address/${encodeURIComponent(address)}/txs`,\n );\n if (!res.ok) throw new Error(`esplora ${res.status} at ${base}`);\n const txs = (await res.json()) as EsploraTx[];\n // Depth policies beyond 1 need the tip to compute confirmations;\n // 0 and 1 read straight off the tx's confirmed flag.\n let tipHeight: number | undefined;\n if (required > 1) {\n const tip = await fetchImpl(`${base}/blocks/tip/height`);\n if (!tip.ok) throw new Error(`esplora ${tip.status} at ${base}`);\n tipHeight = Number(await tip.text());\n }\n return htlcFactsFromEsploraTxs(txs, address, {\n minConfirmations: required,\n tipHeight,\n });\n } catch (error) {\n lastError = error;\n }\n }\n throw lastError ?? new Error(\"no esplora endpoints configured\");\n },\n };\n}\n"],"mappings":";AA6CO,SAAS,wBACd,KACA,SACA,MACkB;AAClB,aAAW,MAAM,KAAK;AACpB,UAAM,QAAQ,GAAG,IAAI;AAAA,MACnB,CAAC,QAAQ,IAAI,SAAS,yBAAyB;AAAA,IACjD;AACA,QAAI;AACF,aAAO;AAAA,QACL,SAAS;AAAA,QACT,YAAY;AAAA;AAAA,QACZ,cAAc,MAAM,WAAW,CAAC;AAAA,MAClC;AAAA,EACJ;AACA,QAAM,UAAU,IAAI;AAAA,IAAK,CAAC,OACxB,GAAG,KAAK,KAAK,CAAC,SAAS,KAAK,yBAAyB,OAAO;AAAA,EAC9D;AACA,MAAI,SAAS;AACX,UAAM,aAAa,QAAQ,KACxB,OAAO,CAAC,SAAS,KAAK,yBAAyB,OAAO,EACtD,OAAO,CAAC,KAAK,SAAS,OAAO,KAAK,SAAS,IAAI,CAAC;AACnD,UAAM,UAAU,MAAM,oBAAoB;AAC1C,QAAI;AACJ,QAAI,QAAQ,QAAQ,WAAW;AAC7B,mBACE,WAAW,KACV,MAAM,cAAc,UACnB,QAAQ,OAAO,iBAAiB,UAChC,KAAK,YAAY,QAAQ,OAAO,eAAe,KAAK;AAAA,IAC1D,OAAO;AACL,mBAAa,YAAY;AAAA,IAC3B;AACA,WAAO;AAAA,MACL,SAAS,aAAa,cAAc;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,SAAS,UAAU,YAAY,EAAE;AAC5C;AAOO,IAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AACF;AAQO,SAAS,cACd,aACA,YAA0B,OAC1B,QACoB;AACpB,QAAM,SAAS,MAAM,QAAQ,WAAW,IAAI,cAAc,CAAC,WAAW,GAAG;AAAA,IACvE,CAAC,QAAQ,IAAI,QAAQ,QAAQ,EAAE;AAAA,EACjC;AACA,QAAM,UAAU,QAAQ,oBAAoB;AAC5C,MAAI,QAAQ;AACZ,SAAO;AAAA,IACL,MAAM,aAAa,SAAS,kBAAkB;AAC5C,YAAM,WAAW,oBAAoB;AACrC,YAAM,OAAO,UAAU,MAAM;AAC7B,UAAI;AACJ,eAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAM,OAAO,OAAO,OAAO,KAAK,MAAM,MAAM;AAC5C,YAAI;AACF,gBAAM,MAAM,MAAM;AAAA,YAChB,GAAG,IAAI,YAAY,mBAAmB,OAAO,CAAC;AAAA,UAChD;AACA,cAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,WAAW,IAAI,MAAM,OAAO,IAAI,EAAE;AAC/D,gBAAM,MAAO,MAAM,IAAI,KAAK;AAG5B,cAAI;AACJ,cAAI,WAAW,GAAG;AAChB,kBAAM,MAAM,MAAM,UAAU,GAAG,IAAI,oBAAoB;AACvD,gBAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,WAAW,IAAI,MAAM,OAAO,IAAI,EAAE;AAC/D,wBAAY,OAAO,MAAM,IAAI,KAAK,CAAC;AAAA,UACrC;AACA,iBAAO,wBAAwB,KAAK,SAAS;AAAA,YAC3C,kBAAkB;AAAA,YAClB;AAAA,UACF,CAAC;AAAA,QACH,SAAS,OAAO;AACd,sBAAY;AAAA,QACd;AAAA,MACF;AACA,YAAM,aAAa,IAAI,MAAM,iCAAiC;AAAA,IAChE;AAAA,EACF;AACF;","names":[]}
|