xytara 3.1.13 → 3.1.14
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/.env.example +8 -1
- package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
- package/RELEASE_NOTES.md +6 -4
- package/lib/a2a_rpc.js +280 -280
- package/lib/account_auth.js +585 -585
- package/lib/agent_identity.js +162 -162
- package/lib/bolt_adapter.js +187 -187
- package/lib/bolt_payment_front.js +224 -224
- package/lib/checkout_processor_adapter.js +217 -217
- package/lib/command_flow.js +722 -722
- package/lib/commerce_artifacts.js +591 -591
- package/lib/commerce_bridge.js +274 -274
- package/lib/commerce_cases.js +399 -399
- package/lib/commerce_client.js +7694 -7694
- package/lib/commerce_quotes.js +29 -0
- package/lib/commerce_runtime.js +322 -322
- package/lib/execution_dedupe.js +86 -86
- package/lib/external_credit_bridge.js +142 -142
- package/lib/http_transport.js +81 -81
- package/lib/l402_adapter.js +241 -241
- package/lib/l402_payment_front.js +265 -265
- package/lib/mpp_adapter.js +237 -237
- package/lib/payment_config.js +253 -253
- package/lib/payment_verification.js +357 -357
- package/lib/provider_payout_execution.js +155 -155
- package/lib/provider_registry.js +842 -842
- package/lib/rate_limiter.js +81 -81
- package/lib/runtime_state_store.js +491 -369
- package/lib/settlement_bsv_live.js +600 -600
- package/lib/settlement_bsv_teranode.js +117 -117
- package/lib/stripe_mpp_payment_front.js +258 -258
- package/lib/x402_facilitator.js +191 -191
- package/package.json +5 -3
- package/scripts/state_backup_offsite.js +196 -0
- package/scripts/verify_a2a_rpc.js +105 -105
- package/scripts/verify_agent_identity.js +153 -153
- package/scripts/verify_all.js +1 -1
- package/scripts/verify_bolt_adapter.js +128 -128
- package/scripts/verify_cdp_facilitator.js +110 -110
- package/scripts/verify_checkout_processor.js +109 -109
- package/scripts/verify_commerce_bridge.js +177 -177
- package/scripts/verify_execution_dedupe.js +189 -189
- package/scripts/verify_execution_surfacing.js +76 -76
- package/scripts/verify_final_activation.js +191 -191
- package/scripts/verify_l402_adapter.js +86 -86
- package/scripts/verify_mpp_adapter.js +128 -128
- package/scripts/verify_production_readiness.js +268 -268
- package/scripts/verify_rate_limiter.js +52 -52
- package/scripts/verify_refund_credit.js +106 -106
- package/scripts/verify_release_candidate.js +81 -81
- package/scripts/verify_runtime_state_guard.js +117 -0
- package/scripts/verify_service.js +15421 -15421
- package/scripts/verify_verify_execute_settle.js +208 -208
- package/scripts/verify_x402_facilitator.js +286 -286
- package/server.js +11976 -11974
package/lib/bolt_adapter.js
CHANGED
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
// Provider-agnostic BSV / UTXO (BOLT) adapter.
|
|
4
|
-
//
|
|
5
|
-
// Makes the BOLT lane a REAL on-chain rail: it verifies that a presented BSV
|
|
6
|
-
// transaction (txid) pays the operator's address for at least the required
|
|
7
|
-
// amount, by fetching the transaction from a configured BSV chain API. The txid
|
|
8
|
-
// is the proof of payment; replay is prevented by treating each txid as
|
|
9
|
-
// single-use. Engages only when env configures a provider; otherwise the caller
|
|
10
|
-
// preserves the existing bespoke token scheme. Dependency-free (node http/https).
|
|
11
|
-
//
|
|
12
|
-
// Env:
|
|
13
|
-
// XYTARA_BOLT_PROVIDER = "whatsonchain" | "generic" (unset => not configured)
|
|
14
|
-
// XYTARA_BOLT_PAYTO_ADDRESS (operator BSV address that must be paid)
|
|
15
|
-
// XYTARA_BOLT_PRICE_SATS (minimum amount, satoshis)
|
|
16
|
-
// XYTARA_BOLT_MIN_CONFIRMATIONS (default 0 => accept mempool)
|
|
17
|
-
// XYTARA_BOLT_NETWORK = "main" | "test" (default main)
|
|
18
|
-
// XYTARA_BOLT_ROOT_KEY | XYTARA_BOLT_SHARED_SECRET (HMAC key for the per-quote
|
|
19
|
-
// amount offset; recommended)
|
|
20
|
-
// whatsonchain: API base defaults to https://api.whatsonchain.com/v1/bsv/<network>
|
|
21
|
-
// generic: XYTARA_BOLT_API_URL (tx-by-hash base; "/tx/hash/<txid>" appended)
|
|
22
|
-
|
|
23
|
-
// REQUEST BINDING (production-safe) & LIMITATIONS:
|
|
24
|
-
// - FLAT-RATE lane: the base price is a flat XYTARA_BOLT_PRICE_SATS (BSV sats
|
|
25
|
-
// cannot be derived from a USD quote without a rate oracle). Use for flat-priced
|
|
26
|
-
// calls; do not expose high-value, variably-priced commands on this lane.
|
|
27
|
-
// - Per-quote binding: the front advertises a UNIQUE required amount
|
|
28
|
-
// (priceSats + an unpredictable HMAC-derived offset) per quote and asks the
|
|
29
|
-
// payer to pay it EXACTLY. verifyBsvPayment with options.requiredSats then
|
|
30
|
-
// requires paidSats === requiredSats, so a pre-existing/unrelated payment to
|
|
31
|
-
// the operator address cannot be reused. Each txid is also single-use.
|
|
32
|
-
// (When called without requiredSats the legacy ">= priceSats" check applies.)
|
|
33
|
-
|
|
34
|
-
const https = require("https");
|
|
35
|
-
const http = require("http");
|
|
36
|
-
const crypto = require("crypto");
|
|
37
|
-
const { URL } = require("url");
|
|
38
|
-
|
|
39
|
-
function env(name) {
|
|
40
|
-
const v = process.env[name];
|
|
41
|
-
return typeof v === "string" && v.trim() ? v.trim() : "";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function loadBoltConfig() {
|
|
45
|
-
const provider = env("XYTARA_BOLT_PROVIDER").toLowerCase();
|
|
46
|
-
if (provider !== "whatsonchain" && provider !== "generic") return { provider: null };
|
|
47
|
-
const network = (env("XYTARA_BOLT_NETWORK") || env("XYTARA_BSV_TERANODE_NETWORK") || "main")
|
|
48
|
-
.replace("mainnet", "main")
|
|
49
|
-
.replace("testnet", "test");
|
|
50
|
-
return {
|
|
51
|
-
provider,
|
|
52
|
-
network,
|
|
53
|
-
apiUrl: env("XYTARA_BOLT_API_URL") || (provider === "whatsonchain" ? `https://api.whatsonchain.com/v1/bsv/${network}` : ""),
|
|
54
|
-
payToAddress: env("XYTARA_BOLT_PAYTO_ADDRESS"),
|
|
55
|
-
priceSats: Number(env("XYTARA_BOLT_PRICE_SATS") || "") || null,
|
|
56
|
-
// Default to 1 confirmation so an unconfirmed (double-spendable) mempool tx is
|
|
57
|
-
// not accepted as proof of payment. An operator may explicitly set 0 to accept
|
|
58
|
-
// 0-conf at their own risk.
|
|
59
|
-
minConfirmations: env("XYTARA_BOLT_MIN_CONFIRMATIONS") === "" ? 1 : Math.max(0, Number(env("XYTARA_BOLT_MIN_CONFIRMATIONS")) || 0),
|
|
60
|
-
// Secret + window for the per-quote amount offset (request binding). A secret
|
|
61
|
-
// key is recommended so the offset is unpredictable; it falls back to the
|
|
62
|
-
// (public) payToAddress, which still binds via exact-amount + single-use.
|
|
63
|
-
bindingSecret: env("XYTARA_BOLT_ROOT_KEY") || env("XYTARA_BOLT_SHARED_SECRET") || (env("XYTARA_BOLT_PAYTO_ADDRESS") + "|" + network),
|
|
64
|
-
bindingWindowSats: Math.max(1, Number(env("XYTARA_BOLT_BINDING_WINDOW_SATS")) || 10000)
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function isConfigured(config) {
|
|
69
|
-
return Boolean(config && config.provider);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Deterministic unique required amount for a quote: base price + an HMAC-derived
|
|
73
|
-
// offset in [0, bindingWindowSats). Both challenge and verify derive the same
|
|
74
|
-
// value from the (stored, stable) quote, so the payer is told an exact amount and
|
|
75
|
-
// the tx must pay it precisely.
|
|
76
|
-
function computeRequiredSats(config, quote) {
|
|
77
|
-
const base = Number(config && config.priceSats) || 0;
|
|
78
|
-
if (!quote || !quote.quote_id) return base;
|
|
79
|
-
const mac = crypto.createHmac("sha256", String(config.bindingSecret || "")).update(String(quote.quote_id)).digest("hex");
|
|
80
|
-
const offset = parseInt(mac.slice(0, 8), 16) % Math.max(1, Number(config.bindingWindowSats) || 10000);
|
|
81
|
-
return base + offset;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function httpGetJson(urlStr, timeoutMs) {
|
|
85
|
-
return new Promise((resolve, reject) => {
|
|
86
|
-
let u;
|
|
87
|
-
try {
|
|
88
|
-
u = new URL(urlStr);
|
|
89
|
-
} catch (e) {
|
|
90
|
-
return reject(new Error("invalid_url"));
|
|
91
|
-
}
|
|
92
|
-
const lib = u.protocol === "http:" ? http : https;
|
|
93
|
-
const req = lib.request(
|
|
94
|
-
{ method: "GET", hostname: u.hostname, port: u.port || (u.protocol === "http:" ? 80 : 443), path: u.pathname + u.search, headers: { Accept: "application/json" } },
|
|
95
|
-
(res) => {
|
|
96
|
-
let d = "";
|
|
97
|
-
res.on("data", (c) => (d += c));
|
|
98
|
-
res.on("end", () => {
|
|
99
|
-
let j = null;
|
|
100
|
-
try {
|
|
101
|
-
j = d ? JSON.parse(d) : null;
|
|
102
|
-
} catch (_) {
|
|
103
|
-
j = null;
|
|
104
|
-
}
|
|
105
|
-
resolve({ status: res.statusCode, json: j, raw: d });
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
);
|
|
109
|
-
req.on("error", reject);
|
|
110
|
-
req.setTimeout(timeoutMs || 15000, () => req.destroy(new Error("bolt_api_timeout")));
|
|
111
|
-
req.end();
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Collect the payout addresses declared on a vout's scriptPubKey across the
|
|
116
|
-
// shapes different BSV APIs use.
|
|
117
|
-
function voutAddresses(vout) {
|
|
118
|
-
const spk = (vout && vout.scriptPubKey) || {};
|
|
119
|
-
if (Array.isArray(spk.addresses)) return spk.addresses;
|
|
120
|
-
if (spk.address) return [spk.address];
|
|
121
|
-
if (Array.isArray(vout && vout.addresses)) return vout.addresses;
|
|
122
|
-
return [];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Verify a BSV txid pays >= priceSats to payToAddress. Returns
|
|
126
|
-
// { ok, txid, paid_sats, confirmations, network } | { ok:false, reason, detail? }.
|
|
127
|
-
async function verifyBsvPayment(config, txid, fetchTxOverride, options) {
|
|
128
|
-
if (!isConfigured(config)) return { ok: false, reason: "bolt_provider_not_configured" };
|
|
129
|
-
if (!config.payToAddress || !config.priceSats) return { ok: false, reason: "bolt_payto_or_price_missing" };
|
|
130
|
-
if (!config.apiUrl) return { ok: false, reason: "bolt_api_url_missing" };
|
|
131
|
-
if (!/^[0-9a-fA-F]{64}$/.test(String(txid || ""))) return { ok: false, reason: "bolt_txid_invalid" };
|
|
132
|
-
const fetchTx = typeof fetchTxOverride === "function" ? fetchTxOverride : (u) => httpGetJson(u, 15000);
|
|
133
|
-
let res;
|
|
134
|
-
try {
|
|
135
|
-
res = await fetchTx(config.apiUrl.replace(/\/$/, "") + "/tx/hash/" + txid);
|
|
136
|
-
} catch (e) {
|
|
137
|
-
return { ok: false, reason: "bolt_api_unreachable", detail: e && e.message ? e.message : "request_error" };
|
|
138
|
-
}
|
|
139
|
-
if (!res.json || res.status >= 400) return { ok: false, reason: "bolt_tx_not_found", detail: `HTTP ${res.status}` };
|
|
140
|
-
const j = res.json;
|
|
141
|
-
const confirmations = Number(j.confirmations || 0);
|
|
142
|
-
if (config.minConfirmations > 0 && confirmations < config.minConfirmations) {
|
|
143
|
-
return { ok: false, reason: "bolt_insufficient_confirmations", detail: `${confirmations}/${config.minConfirmations}` };
|
|
144
|
-
}
|
|
145
|
-
const vouts = Array.isArray(j.vout) ? j.vout : [];
|
|
146
|
-
let paidSats = 0;
|
|
147
|
-
for (const o of vouts) {
|
|
148
|
-
if (voutAddresses(o).includes(config.payToAddress)) {
|
|
149
|
-
// BSV APIs report value in BSV; convert to satoshis.
|
|
150
|
-
paidSats += Math.round(Number(o.value || 0) * 1e8);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// Per-quote binding: when a unique required amount is supplied, the tx must pay
|
|
154
|
-
// it EXACTLY (not merely >=), so an unrelated payment to the address cannot be
|
|
155
|
-
// reused. Without it, fall back to the flat ">= priceSats" check.
|
|
156
|
-
const requiredSats = options && Number.isFinite(Number(options.requiredSats)) ? Math.round(Number(options.requiredSats)) : null;
|
|
157
|
-
if (requiredSats !== null) {
|
|
158
|
-
if (paidSats !== requiredSats) {
|
|
159
|
-
return { ok: false, reason: "bolt_amount_mismatch", detail: `${paidSats}/${requiredSats}` };
|
|
160
|
-
}
|
|
161
|
-
} else if (paidSats < config.priceSats) {
|
|
162
|
-
return { ok: false, reason: "bolt_amount_insufficient", detail: `${paidSats}/${config.priceSats}` };
|
|
163
|
-
}
|
|
164
|
-
return { ok: true, txid: String(txid), paid_sats: paidSats, confirmations, network: config.network, required_sats: requiredSats };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Parse "BOLT <txid>" (64-hex). Returns { txid } | null.
|
|
168
|
-
function parseBoltHeader(authorization) {
|
|
169
|
-
const m = String(authorization || "").match(/^BOLT\s+([0-9a-fA-F]{64})\s*$/);
|
|
170
|
-
return m ? { txid: m[1] } : null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// The challenge an unpaid caller needs: where to pay and how much. When a
|
|
174
|
-
// per-quote required amount is supplied, advertise that exact amount (the payer
|
|
175
|
-
// must pay it precisely) instead of the flat base price.
|
|
176
|
-
function buildBoltChallenge(config, options) {
|
|
177
|
-
const requiredSats = options && Number.isFinite(Number(options.requiredSats)) ? Math.round(Number(options.requiredSats)) : null;
|
|
178
|
-
return {
|
|
179
|
-
address: config.payToAddress,
|
|
180
|
-
amount_sats: requiredSats !== null ? requiredSats : config.priceSats,
|
|
181
|
-
exact: requiredSats !== null,
|
|
182
|
-
network: config.network,
|
|
183
|
-
min_confirmations: config.minConfirmations
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
module.exports = { loadBoltConfig, isConfigured, verifyBsvPayment, parseBoltHeader, buildBoltChallenge, computeRequiredSats };
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Provider-agnostic BSV / UTXO (BOLT) adapter.
|
|
4
|
+
//
|
|
5
|
+
// Makes the BOLT lane a REAL on-chain rail: it verifies that a presented BSV
|
|
6
|
+
// transaction (txid) pays the operator's address for at least the required
|
|
7
|
+
// amount, by fetching the transaction from a configured BSV chain API. The txid
|
|
8
|
+
// is the proof of payment; replay is prevented by treating each txid as
|
|
9
|
+
// single-use. Engages only when env configures a provider; otherwise the caller
|
|
10
|
+
// preserves the existing bespoke token scheme. Dependency-free (node http/https).
|
|
11
|
+
//
|
|
12
|
+
// Env:
|
|
13
|
+
// XYTARA_BOLT_PROVIDER = "whatsonchain" | "generic" (unset => not configured)
|
|
14
|
+
// XYTARA_BOLT_PAYTO_ADDRESS (operator BSV address that must be paid)
|
|
15
|
+
// XYTARA_BOLT_PRICE_SATS (minimum amount, satoshis)
|
|
16
|
+
// XYTARA_BOLT_MIN_CONFIRMATIONS (default 0 => accept mempool)
|
|
17
|
+
// XYTARA_BOLT_NETWORK = "main" | "test" (default main)
|
|
18
|
+
// XYTARA_BOLT_ROOT_KEY | XYTARA_BOLT_SHARED_SECRET (HMAC key for the per-quote
|
|
19
|
+
// amount offset; recommended)
|
|
20
|
+
// whatsonchain: API base defaults to https://api.whatsonchain.com/v1/bsv/<network>
|
|
21
|
+
// generic: XYTARA_BOLT_API_URL (tx-by-hash base; "/tx/hash/<txid>" appended)
|
|
22
|
+
|
|
23
|
+
// REQUEST BINDING (production-safe) & LIMITATIONS:
|
|
24
|
+
// - FLAT-RATE lane: the base price is a flat XYTARA_BOLT_PRICE_SATS (BSV sats
|
|
25
|
+
// cannot be derived from a USD quote without a rate oracle). Use for flat-priced
|
|
26
|
+
// calls; do not expose high-value, variably-priced commands on this lane.
|
|
27
|
+
// - Per-quote binding: the front advertises a UNIQUE required amount
|
|
28
|
+
// (priceSats + an unpredictable HMAC-derived offset) per quote and asks the
|
|
29
|
+
// payer to pay it EXACTLY. verifyBsvPayment with options.requiredSats then
|
|
30
|
+
// requires paidSats === requiredSats, so a pre-existing/unrelated payment to
|
|
31
|
+
// the operator address cannot be reused. Each txid is also single-use.
|
|
32
|
+
// (When called without requiredSats the legacy ">= priceSats" check applies.)
|
|
33
|
+
|
|
34
|
+
const https = require("https");
|
|
35
|
+
const http = require("http");
|
|
36
|
+
const crypto = require("crypto");
|
|
37
|
+
const { URL } = require("url");
|
|
38
|
+
|
|
39
|
+
function env(name) {
|
|
40
|
+
const v = process.env[name];
|
|
41
|
+
return typeof v === "string" && v.trim() ? v.trim() : "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadBoltConfig() {
|
|
45
|
+
const provider = env("XYTARA_BOLT_PROVIDER").toLowerCase();
|
|
46
|
+
if (provider !== "whatsonchain" && provider !== "generic") return { provider: null };
|
|
47
|
+
const network = (env("XYTARA_BOLT_NETWORK") || env("XYTARA_BSV_TERANODE_NETWORK") || "main")
|
|
48
|
+
.replace("mainnet", "main")
|
|
49
|
+
.replace("testnet", "test");
|
|
50
|
+
return {
|
|
51
|
+
provider,
|
|
52
|
+
network,
|
|
53
|
+
apiUrl: env("XYTARA_BOLT_API_URL") || (provider === "whatsonchain" ? `https://api.whatsonchain.com/v1/bsv/${network}` : ""),
|
|
54
|
+
payToAddress: env("XYTARA_BOLT_PAYTO_ADDRESS"),
|
|
55
|
+
priceSats: Number(env("XYTARA_BOLT_PRICE_SATS") || "") || null,
|
|
56
|
+
// Default to 1 confirmation so an unconfirmed (double-spendable) mempool tx is
|
|
57
|
+
// not accepted as proof of payment. An operator may explicitly set 0 to accept
|
|
58
|
+
// 0-conf at their own risk.
|
|
59
|
+
minConfirmations: env("XYTARA_BOLT_MIN_CONFIRMATIONS") === "" ? 1 : Math.max(0, Number(env("XYTARA_BOLT_MIN_CONFIRMATIONS")) || 0),
|
|
60
|
+
// Secret + window for the per-quote amount offset (request binding). A secret
|
|
61
|
+
// key is recommended so the offset is unpredictable; it falls back to the
|
|
62
|
+
// (public) payToAddress, which still binds via exact-amount + single-use.
|
|
63
|
+
bindingSecret: env("XYTARA_BOLT_ROOT_KEY") || env("XYTARA_BOLT_SHARED_SECRET") || (env("XYTARA_BOLT_PAYTO_ADDRESS") + "|" + network),
|
|
64
|
+
bindingWindowSats: Math.max(1, Number(env("XYTARA_BOLT_BINDING_WINDOW_SATS")) || 10000)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isConfigured(config) {
|
|
69
|
+
return Boolean(config && config.provider);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Deterministic unique required amount for a quote: base price + an HMAC-derived
|
|
73
|
+
// offset in [0, bindingWindowSats). Both challenge and verify derive the same
|
|
74
|
+
// value from the (stored, stable) quote, so the payer is told an exact amount and
|
|
75
|
+
// the tx must pay it precisely.
|
|
76
|
+
function computeRequiredSats(config, quote) {
|
|
77
|
+
const base = Number(config && config.priceSats) || 0;
|
|
78
|
+
if (!quote || !quote.quote_id) return base;
|
|
79
|
+
const mac = crypto.createHmac("sha256", String(config.bindingSecret || "")).update(String(quote.quote_id)).digest("hex");
|
|
80
|
+
const offset = parseInt(mac.slice(0, 8), 16) % Math.max(1, Number(config.bindingWindowSats) || 10000);
|
|
81
|
+
return base + offset;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function httpGetJson(urlStr, timeoutMs) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
let u;
|
|
87
|
+
try {
|
|
88
|
+
u = new URL(urlStr);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return reject(new Error("invalid_url"));
|
|
91
|
+
}
|
|
92
|
+
const lib = u.protocol === "http:" ? http : https;
|
|
93
|
+
const req = lib.request(
|
|
94
|
+
{ method: "GET", hostname: u.hostname, port: u.port || (u.protocol === "http:" ? 80 : 443), path: u.pathname + u.search, headers: { Accept: "application/json" } },
|
|
95
|
+
(res) => {
|
|
96
|
+
let d = "";
|
|
97
|
+
res.on("data", (c) => (d += c));
|
|
98
|
+
res.on("end", () => {
|
|
99
|
+
let j = null;
|
|
100
|
+
try {
|
|
101
|
+
j = d ? JSON.parse(d) : null;
|
|
102
|
+
} catch (_) {
|
|
103
|
+
j = null;
|
|
104
|
+
}
|
|
105
|
+
resolve({ status: res.statusCode, json: j, raw: d });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
req.on("error", reject);
|
|
110
|
+
req.setTimeout(timeoutMs || 15000, () => req.destroy(new Error("bolt_api_timeout")));
|
|
111
|
+
req.end();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Collect the payout addresses declared on a vout's scriptPubKey across the
|
|
116
|
+
// shapes different BSV APIs use.
|
|
117
|
+
function voutAddresses(vout) {
|
|
118
|
+
const spk = (vout && vout.scriptPubKey) || {};
|
|
119
|
+
if (Array.isArray(spk.addresses)) return spk.addresses;
|
|
120
|
+
if (spk.address) return [spk.address];
|
|
121
|
+
if (Array.isArray(vout && vout.addresses)) return vout.addresses;
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Verify a BSV txid pays >= priceSats to payToAddress. Returns
|
|
126
|
+
// { ok, txid, paid_sats, confirmations, network } | { ok:false, reason, detail? }.
|
|
127
|
+
async function verifyBsvPayment(config, txid, fetchTxOverride, options) {
|
|
128
|
+
if (!isConfigured(config)) return { ok: false, reason: "bolt_provider_not_configured" };
|
|
129
|
+
if (!config.payToAddress || !config.priceSats) return { ok: false, reason: "bolt_payto_or_price_missing" };
|
|
130
|
+
if (!config.apiUrl) return { ok: false, reason: "bolt_api_url_missing" };
|
|
131
|
+
if (!/^[0-9a-fA-F]{64}$/.test(String(txid || ""))) return { ok: false, reason: "bolt_txid_invalid" };
|
|
132
|
+
const fetchTx = typeof fetchTxOverride === "function" ? fetchTxOverride : (u) => httpGetJson(u, 15000);
|
|
133
|
+
let res;
|
|
134
|
+
try {
|
|
135
|
+
res = await fetchTx(config.apiUrl.replace(/\/$/, "") + "/tx/hash/" + txid);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return { ok: false, reason: "bolt_api_unreachable", detail: e && e.message ? e.message : "request_error" };
|
|
138
|
+
}
|
|
139
|
+
if (!res.json || res.status >= 400) return { ok: false, reason: "bolt_tx_not_found", detail: `HTTP ${res.status}` };
|
|
140
|
+
const j = res.json;
|
|
141
|
+
const confirmations = Number(j.confirmations || 0);
|
|
142
|
+
if (config.minConfirmations > 0 && confirmations < config.minConfirmations) {
|
|
143
|
+
return { ok: false, reason: "bolt_insufficient_confirmations", detail: `${confirmations}/${config.minConfirmations}` };
|
|
144
|
+
}
|
|
145
|
+
const vouts = Array.isArray(j.vout) ? j.vout : [];
|
|
146
|
+
let paidSats = 0;
|
|
147
|
+
for (const o of vouts) {
|
|
148
|
+
if (voutAddresses(o).includes(config.payToAddress)) {
|
|
149
|
+
// BSV APIs report value in BSV; convert to satoshis.
|
|
150
|
+
paidSats += Math.round(Number(o.value || 0) * 1e8);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Per-quote binding: when a unique required amount is supplied, the tx must pay
|
|
154
|
+
// it EXACTLY (not merely >=), so an unrelated payment to the address cannot be
|
|
155
|
+
// reused. Without it, fall back to the flat ">= priceSats" check.
|
|
156
|
+
const requiredSats = options && Number.isFinite(Number(options.requiredSats)) ? Math.round(Number(options.requiredSats)) : null;
|
|
157
|
+
if (requiredSats !== null) {
|
|
158
|
+
if (paidSats !== requiredSats) {
|
|
159
|
+
return { ok: false, reason: "bolt_amount_mismatch", detail: `${paidSats}/${requiredSats}` };
|
|
160
|
+
}
|
|
161
|
+
} else if (paidSats < config.priceSats) {
|
|
162
|
+
return { ok: false, reason: "bolt_amount_insufficient", detail: `${paidSats}/${config.priceSats}` };
|
|
163
|
+
}
|
|
164
|
+
return { ok: true, txid: String(txid), paid_sats: paidSats, confirmations, network: config.network, required_sats: requiredSats };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Parse "BOLT <txid>" (64-hex). Returns { txid } | null.
|
|
168
|
+
function parseBoltHeader(authorization) {
|
|
169
|
+
const m = String(authorization || "").match(/^BOLT\s+([0-9a-fA-F]{64})\s*$/);
|
|
170
|
+
return m ? { txid: m[1] } : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The challenge an unpaid caller needs: where to pay and how much. When a
|
|
174
|
+
// per-quote required amount is supplied, advertise that exact amount (the payer
|
|
175
|
+
// must pay it precisely) instead of the flat base price.
|
|
176
|
+
function buildBoltChallenge(config, options) {
|
|
177
|
+
const requiredSats = options && Number.isFinite(Number(options.requiredSats)) ? Math.round(Number(options.requiredSats)) : null;
|
|
178
|
+
return {
|
|
179
|
+
address: config.payToAddress,
|
|
180
|
+
amount_sats: requiredSats !== null ? requiredSats : config.priceSats,
|
|
181
|
+
exact: requiredSats !== null,
|
|
182
|
+
network: config.network,
|
|
183
|
+
min_confirmations: config.minConfirmations
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { loadBoltConfig, isConfigured, verifyBsvPayment, parseBoltHeader, buildBoltChallenge, computeRequiredSats };
|