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
|
@@ -1,600 +1,600 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const crypto = require("crypto");
|
|
4
|
-
const { buildFetchOptionsForPurpose } = require("./network_transport");
|
|
5
|
-
|
|
6
|
-
const LIVE_SETTLEMENT_MODE_ALIASES = new Map([
|
|
7
|
-
["live", "arc"],
|
|
8
|
-
["arc", "arc"],
|
|
9
|
-
["observe", "observe"],
|
|
10
|
-
["observer", "observe"],
|
|
11
|
-
["mock", "mock"]
|
|
12
|
-
]);
|
|
13
|
-
|
|
14
|
-
function firstEnv(...names) {
|
|
15
|
-
for (const name of names) {
|
|
16
|
-
const value = process.env[name];
|
|
17
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
18
|
-
return value.trim();
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function sha256Hex(value) {
|
|
25
|
-
return crypto.createHash("sha256").update(String(value || "")).digest("hex");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function loadBsvSettlementRuntimeConfig() {
|
|
29
|
-
const modeInput = String(
|
|
30
|
-
firstEnv(
|
|
31
|
-
"XYTARA_BSV_SETTLEMENT_RUNTIME_MODE",
|
|
32
|
-
"XYTARA_BSV_TERANODE_SETTLEMENT_RUNTIME_MODE"
|
|
33
|
-
) || "mock"
|
|
34
|
-
).trim().toLowerCase() || "mock";
|
|
35
|
-
const mode = LIVE_SETTLEMENT_MODE_ALIASES.get(modeInput) || modeInput;
|
|
36
|
-
const requiredConfirmations = Math.max(
|
|
37
|
-
1,
|
|
38
|
-
Number(firstEnv("XYTARA_BSV_REQUIRED_CONFIRMATIONS", "XYTARA_BSV_TERANODE_REQUIRED_CONFIRMATIONS") || 1) || 1
|
|
39
|
-
);
|
|
40
|
-
const network = String(firstEnv("XYTARA_BSV_NETWORK", "XYTARA_BSV_TERANODE_NETWORK") || "mainnet").trim() || "mainnet";
|
|
41
|
-
const observerNetwork = network === "mainnet" ? "main" : network === "testnet" ? "test" : network;
|
|
42
|
-
const observerUrl = String(
|
|
43
|
-
firstEnv("XYTARA_BSV_OBSERVER_URL", "XYTARA_BSV_TERANODE_OBSERVER_URL")
|
|
44
|
-
|| `https://api.whatsonchain.com/v1/bsv/${observerNetwork}`
|
|
45
|
-
).trim() || null;
|
|
46
|
-
return {
|
|
47
|
-
mode,
|
|
48
|
-
rpc_url: firstEnv("XYTARA_BSV_ARC_RPC_URL", "XYTARA_BSV_ARC_URL", "XYTARA_BSV_TERANODE_RPC_URL"),
|
|
49
|
-
rpc_api_key: firstEnv("XYTARA_BSV_ARC_RPC_API_KEY", "XYTARA_BSV_ARC_API_KEY", "XYTARA_BSV_TERANODE_RPC_API_KEY"),
|
|
50
|
-
rpc_auth_header: String(firstEnv("XYTARA_BSV_ARC_RPC_AUTH_HEADER", "XYTARA_BSV_ARC_AUTH_HEADER", "XYTARA_BSV_TERANODE_RPC_AUTH_HEADER") || "authorization").trim() || "authorization",
|
|
51
|
-
rpc_auth_scheme: String(firstEnv("XYTARA_BSV_ARC_RPC_AUTH_SCHEME", "XYTARA_BSV_ARC_AUTH_SCHEME", "XYTARA_BSV_TERANODE_RPC_AUTH_SCHEME") || "").trim(),
|
|
52
|
-
observer_url: observerUrl,
|
|
53
|
-
network,
|
|
54
|
-
required_confirmations: requiredConfirmations
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function clone(value) {
|
|
59
|
-
return JSON.parse(JSON.stringify(value));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function isLiveSettlementRuntimeMode(mode) {
|
|
63
|
-
return String(mode || "").trim().toLowerCase() !== "mock";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function buildRuntimeAuthHeaders(config) {
|
|
67
|
-
if (!config || !config.rpc_api_key) return {};
|
|
68
|
-
const headerName = config.rpc_auth_header || "authorization";
|
|
69
|
-
const scheme = String(config.rpc_auth_scheme || "").trim();
|
|
70
|
-
return {
|
|
71
|
-
[headerName]: scheme ? `${scheme} ${config.rpc_api_key}` : config.rpc_api_key
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function extractSubmittedTransactionId(payload) {
|
|
76
|
-
if (!payload || typeof payload !== "object") return null;
|
|
77
|
-
return payload.txid || payload.txId || payload.transactionId || payload.data && payload.data.txid || null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function submitArcRawTransaction(config, rawTx) {
|
|
81
|
-
const response = await fetch(config.rpc_url, {
|
|
82
|
-
method: "POST",
|
|
83
|
-
headers: {
|
|
84
|
-
"content-type": "application/json",
|
|
85
|
-
...buildRuntimeAuthHeaders(config)
|
|
86
|
-
},
|
|
87
|
-
body: JSON.stringify({ rawTx }),
|
|
88
|
-
...buildFetchOptionsForPurpose("settlement")
|
|
89
|
-
});
|
|
90
|
-
const text = await response.text();
|
|
91
|
-
let parsed = null;
|
|
92
|
-
try {
|
|
93
|
-
parsed = text ? JSON.parse(text) : null;
|
|
94
|
-
} catch (_) {
|
|
95
|
-
parsed = null;
|
|
96
|
-
}
|
|
97
|
-
if (!response.ok) {
|
|
98
|
-
return {
|
|
99
|
-
ok: false,
|
|
100
|
-
reason: "settlement_submit_failed",
|
|
101
|
-
status: response.status,
|
|
102
|
-
body: parsed || text || null
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
const txid = extractSubmittedTransactionId(parsed);
|
|
106
|
-
if (!txid) {
|
|
107
|
-
return {
|
|
108
|
-
ok: false,
|
|
109
|
-
reason: "settlement_submit_missing_txid",
|
|
110
|
-
status: response.status,
|
|
111
|
-
body: parsed || text || null
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
ok: true,
|
|
116
|
-
txid,
|
|
117
|
-
provider_ref: parsed && typeof parsed === "object" ? parsed.requestId || parsed.request_id || null : null,
|
|
118
|
-
response: parsed || text || null
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function normalizeArcStatusUrl(config, txid) {
|
|
123
|
-
const rpcUrl = String(config && config.rpc_url || "").replace(/\/$/, "");
|
|
124
|
-
if (!rpcUrl) return null;
|
|
125
|
-
if (rpcUrl.endsWith("/tx")) return `${rpcUrl}/${encodeURIComponent(txid)}`;
|
|
126
|
-
if (rpcUrl.includes("/tx/")) return rpcUrl;
|
|
127
|
-
return `${rpcUrl}/tx/${encodeURIComponent(txid)}`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function fetchArcTransactionStatus(config, txid) {
|
|
131
|
-
const statusUrl = normalizeArcStatusUrl(config, txid);
|
|
132
|
-
if (!statusUrl) {
|
|
133
|
-
return { ok: false, reason: "settlement_transport_missing" };
|
|
134
|
-
}
|
|
135
|
-
const response = await fetch(statusUrl, {
|
|
136
|
-
headers: buildRuntimeAuthHeaders(config),
|
|
137
|
-
...buildFetchOptionsForPurpose("settlement")
|
|
138
|
-
});
|
|
139
|
-
const text = await response.text();
|
|
140
|
-
let parsed = null;
|
|
141
|
-
try {
|
|
142
|
-
parsed = text ? JSON.parse(text) : null;
|
|
143
|
-
} catch (_) {
|
|
144
|
-
parsed = null;
|
|
145
|
-
}
|
|
146
|
-
if (!response.ok) {
|
|
147
|
-
return {
|
|
148
|
-
ok: false,
|
|
149
|
-
reason: response.status === 404 ? "tx_not_found" : "arc_status_failed",
|
|
150
|
-
status: response.status,
|
|
151
|
-
body: parsed || text || null
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
return {
|
|
155
|
-
ok: true,
|
|
156
|
-
payload: parsed || null
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function fetchObserverTransactionStatus(config, txid) {
|
|
161
|
-
const observerBaseUrl = String(config && config.observer_url || "").replace(/\/$/, "");
|
|
162
|
-
if (!observerBaseUrl) {
|
|
163
|
-
return { ok: false, reason: "settlement_observer_missing" };
|
|
164
|
-
}
|
|
165
|
-
const response = await fetch(`${observerBaseUrl}/tx/hash/${encodeURIComponent(txid)}`, buildFetchOptionsForPurpose("settlement"));
|
|
166
|
-
const text = await response.text();
|
|
167
|
-
let parsed = null;
|
|
168
|
-
try {
|
|
169
|
-
parsed = text ? JSON.parse(text) : null;
|
|
170
|
-
} catch (_) {
|
|
171
|
-
parsed = null;
|
|
172
|
-
}
|
|
173
|
-
if (!response.ok) {
|
|
174
|
-
return {
|
|
175
|
-
ok: false,
|
|
176
|
-
reason: response.status === 404 ? "tx_not_found" : "observer_status_failed",
|
|
177
|
-
status: response.status,
|
|
178
|
-
body: parsed || text || null
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const confirmations = Number(
|
|
182
|
-
parsed && (
|
|
183
|
-
parsed.confirmations
|
|
184
|
-
|| parsed.confirmationCount
|
|
185
|
-
|| parsed.confirmation_count
|
|
186
|
-
|| parsed.data && (parsed.data.confirmations || parsed.data.confirmationCount || parsed.data.confirmation_count)
|
|
187
|
-
)
|
|
188
|
-
);
|
|
189
|
-
const blockhash = parsed && (parsed.blockhash || parsed.blockHash || parsed.merkleRoot || parsed.data && (parsed.data.blockhash || parsed.data.blockHash)) || null;
|
|
190
|
-
return {
|
|
191
|
-
ok: true,
|
|
192
|
-
confirmations: Number.isFinite(confirmations) && confirmations >= 0 ? confirmations : 0,
|
|
193
|
-
blockhash,
|
|
194
|
-
payload: parsed || null
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function buildSettlementLiveView(record) {
|
|
199
|
-
if (!record) return null;
|
|
200
|
-
return {
|
|
201
|
-
settlement_id: record.settlement_id,
|
|
202
|
-
status: record.status,
|
|
203
|
-
finality_status: record.finality_status,
|
|
204
|
-
txid: record.txid,
|
|
205
|
-
network: record.network,
|
|
206
|
-
rail: record.rail,
|
|
207
|
-
confirmation_count: record.confirmation_count,
|
|
208
|
-
required_confirmations: record.required_confirmations,
|
|
209
|
-
raw_tx_present: Boolean(record.raw_tx),
|
|
210
|
-
submitted_at_iso: record.submitted_at_iso,
|
|
211
|
-
observed_at_iso: record.observed_at_iso,
|
|
212
|
-
confirmed_at_iso: record.confirmed_at_iso,
|
|
213
|
-
finalized_at_iso: record.finalized_at_iso,
|
|
214
|
-
next_refresh_at_iso: record.next_refresh_at_iso || null,
|
|
215
|
-
refresh_count: record.refresh_count,
|
|
216
|
-
runtime_mode: record.runtime_mode
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function updateArtifactsForSettlementRecord(state, record) {
|
|
221
|
-
if (!record) return;
|
|
222
|
-
const linked = state.transactions.get(record.transaction_id);
|
|
223
|
-
if (linked) {
|
|
224
|
-
const settlementLive = buildSettlementLiveView(record);
|
|
225
|
-
const verified = record.finality_status === "confirmed" || record.finality_status === "finalized";
|
|
226
|
-
const transaction = {
|
|
227
|
-
...linked.transaction,
|
|
228
|
-
settlement: {
|
|
229
|
-
...(linked.transaction.settlement || {}),
|
|
230
|
-
status: record.status,
|
|
231
|
-
verified,
|
|
232
|
-
txid: record.txid || (linked.transaction.settlement && linked.transaction.settlement.txid) || null,
|
|
233
|
-
live: settlementLive
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
const receipt = {
|
|
237
|
-
...linked.receipt,
|
|
238
|
-
settlement: {
|
|
239
|
-
...(linked.receipt.settlement || {}),
|
|
240
|
-
status: record.status,
|
|
241
|
-
verified,
|
|
242
|
-
txid: record.txid || (linked.receipt.settlement && linked.receipt.settlement.txid) || null,
|
|
243
|
-
live: settlementLive
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
const next = {
|
|
247
|
-
...linked,
|
|
248
|
-
transaction,
|
|
249
|
-
receipt
|
|
250
|
-
};
|
|
251
|
-
state.transactions.set(record.transaction_id, next);
|
|
252
|
-
state.receipts.set(receipt.receipt_id, next);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (Array.isArray(state.paymentLedger)) {
|
|
256
|
-
state.paymentLedger = state.paymentLedger.map((entry) => {
|
|
257
|
-
if (!entry || entry.payment_id !== record.payment_id) return entry;
|
|
258
|
-
return {
|
|
259
|
-
...entry,
|
|
260
|
-
settlement: {
|
|
261
|
-
...(entry.settlement || {}),
|
|
262
|
-
status: record.status,
|
|
263
|
-
verified: record.finality_status === "confirmed" || record.finality_status === "finalized",
|
|
264
|
-
txid: record.txid || (entry.settlement && entry.settlement.txid) || null,
|
|
265
|
-
live: buildSettlementLiveView(record)
|
|
266
|
-
},
|
|
267
|
-
live_settlement: buildSettlementLiveView(record)
|
|
268
|
-
};
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function createBsvSettlementRecord(state, transaction, receipt, paymentRecord) {
|
|
274
|
-
if (!transaction || !transaction.settlement || transaction.settlement.rail !== "bsv_teranode") return null;
|
|
275
|
-
const config = loadBsvSettlementRuntimeConfig();
|
|
276
|
-
const settlementId = transaction.settlement && transaction.settlement.settlement_id
|
|
277
|
-
? transaction.settlement.settlement_id
|
|
278
|
-
: `settlement_${crypto.randomBytes(6).toString("hex")}`;
|
|
279
|
-
const existing = state.bsvSettlementRecords.get(settlementId);
|
|
280
|
-
if (existing) return existing;
|
|
281
|
-
const record = {
|
|
282
|
-
settlement_id: settlementId,
|
|
283
|
-
transaction_id: transaction.transaction_id,
|
|
284
|
-
receipt_id: receipt && receipt.receipt_id ? receipt.receipt_id : null,
|
|
285
|
-
payment_id: paymentRecord && paymentRecord.payment_id ? paymentRecord.payment_id : null,
|
|
286
|
-
account_id: transaction.account_id || null,
|
|
287
|
-
quote_id: transaction.quote_id || null,
|
|
288
|
-
amount_minor: transaction.amount_minor,
|
|
289
|
-
currency: transaction.currency,
|
|
290
|
-
rail: "bsv_teranode",
|
|
291
|
-
network: transaction.settlement && transaction.settlement.network ? transaction.settlement.network : config.network,
|
|
292
|
-
runtime_mode: config.mode,
|
|
293
|
-
status: "ready_for_submission",
|
|
294
|
-
finality_status: "pending_submission",
|
|
295
|
-
confirmation_count: 0,
|
|
296
|
-
required_confirmations: config.required_confirmations,
|
|
297
|
-
txid: transaction.settlement && transaction.settlement.txid ? transaction.settlement.txid : null,
|
|
298
|
-
raw_tx: transaction.settlement && transaction.settlement.raw_tx ? transaction.settlement.raw_tx : null,
|
|
299
|
-
provider_ref: null,
|
|
300
|
-
settlement_reference:
|
|
301
|
-
transaction.settlement && transaction.settlement.binding_hash
|
|
302
|
-
? transaction.settlement.binding_hash
|
|
303
|
-
: null,
|
|
304
|
-
merchant_address:
|
|
305
|
-
transaction.settlement && transaction.settlement.hints
|
|
306
|
-
? transaction.settlement.hints.merchant_address || null
|
|
307
|
-
: null,
|
|
308
|
-
merchant_paymail:
|
|
309
|
-
transaction.settlement && transaction.settlement.hints
|
|
310
|
-
? transaction.settlement.hints.merchant_paymail || null
|
|
311
|
-
: null,
|
|
312
|
-
created_at_iso: new Date().toISOString(),
|
|
313
|
-
submitted_at_iso: null,
|
|
314
|
-
observed_at_iso: null,
|
|
315
|
-
confirmed_at_iso: null,
|
|
316
|
-
finalized_at_iso: null,
|
|
317
|
-
next_refresh_at_iso: null,
|
|
318
|
-
refresh_count: 0,
|
|
319
|
-
last_error: null
|
|
320
|
-
};
|
|
321
|
-
state.bsvSettlementRecords.set(settlementId, record);
|
|
322
|
-
updateArtifactsForSettlementRecord(state, record);
|
|
323
|
-
return record;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function listBsvSettlementRecords(state, filters = {}) {
|
|
327
|
-
const records = Array.from(state.bsvSettlementRecords.values());
|
|
328
|
-
let items = records;
|
|
329
|
-
if (filters.status) items = items.filter((item) => item.status === filters.status);
|
|
330
|
-
if (filters.account_id) items = items.filter((item) => item.account_id === filters.account_id);
|
|
331
|
-
if (filters.transaction_id) items = items.filter((item) => item.transaction_id === filters.transaction_id);
|
|
332
|
-
if (filters.payment_id) items = items.filter((item) => item.payment_id === filters.payment_id);
|
|
333
|
-
return items;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function getBsvSettlementRecord(state, settlementId) {
|
|
337
|
-
return state.bsvSettlementRecords.get(settlementId) || null;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function normalizeOptionalString(value) {
|
|
341
|
-
if (typeof value !== "string") return null;
|
|
342
|
-
const normalized = value.trim();
|
|
343
|
-
return normalized ? normalized : null;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function normalizeTxid(value) {
|
|
347
|
-
const txid = normalizeOptionalString(value);
|
|
348
|
-
if (!txid) return null;
|
|
349
|
-
const lowered = txid.toLowerCase();
|
|
350
|
-
return /^[a-f0-9]{64}$/.test(lowered) ? lowered : null;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function normalizeRawTransaction(value) {
|
|
354
|
-
const rawTx = normalizeOptionalString(value);
|
|
355
|
-
if (!rawTx) return null;
|
|
356
|
-
if (rawTx.length < 20 || rawTx.length % 2 !== 0) return null;
|
|
357
|
-
return /^[a-f0-9]+$/i.test(rawTx) ? rawTx.toLowerCase() : null;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function updateBsvSettlementSubmissionMaterial(state, settlementId, payload = {}) {
|
|
361
|
-
const body = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
362
|
-
const record = getBsvSettlementRecord(state, settlementId);
|
|
363
|
-
if (!record) {
|
|
364
|
-
return { ok: false, reason: "settlement_not_found" };
|
|
365
|
-
}
|
|
366
|
-
if (record.status !== "ready_for_submission" && record.status !== "failed") {
|
|
367
|
-
return {
|
|
368
|
-
ok: false,
|
|
369
|
-
reason: "settlement_material_update_not_allowed",
|
|
370
|
-
settlement: clone(record)
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const txidInput = body.txid || body.txId || body.transaction_id || body.transactionId;
|
|
375
|
-
const rawTxInput = body.raw_tx || body.rawTx || body.raw_transaction || body.rawTransaction;
|
|
376
|
-
const txid = normalizeTxid(txidInput);
|
|
377
|
-
const rawTx = normalizeRawTransaction(rawTxInput);
|
|
378
|
-
if (txidInput && !txid) {
|
|
379
|
-
return { ok: false, reason: "settlement_txid_invalid", settlement: clone(record) };
|
|
380
|
-
}
|
|
381
|
-
if (rawTxInput && !rawTx) {
|
|
382
|
-
return { ok: false, reason: "settlement_raw_tx_invalid", settlement: clone(record) };
|
|
383
|
-
}
|
|
384
|
-
if (!txid && !rawTx) {
|
|
385
|
-
return { ok: false, reason: "settlement_submission_material_required", settlement: clone(record) };
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const next = {
|
|
389
|
-
...record,
|
|
390
|
-
txid: txid || record.txid || null,
|
|
391
|
-
raw_tx: rawTx || record.raw_tx || null,
|
|
392
|
-
provider_ref: normalizeOptionalString(body.provider_ref || body.providerRef) || record.provider_ref || null,
|
|
393
|
-
network: normalizeOptionalString(body.network) || record.network,
|
|
394
|
-
runtime_mode: loadBsvSettlementRuntimeConfig().mode,
|
|
395
|
-
finality_status: "pending_submission",
|
|
396
|
-
last_error: null,
|
|
397
|
-
submission_material_updated_at_iso: new Date().toISOString()
|
|
398
|
-
};
|
|
399
|
-
state.bsvSettlementRecords.set(settlementId, next);
|
|
400
|
-
updateArtifactsForSettlementRecord(state, next);
|
|
401
|
-
return { ok: true, settlement: clone(next) };
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
async function submitBsvSettlementRecord(state, settlementId) {
|
|
405
|
-
const record = getBsvSettlementRecord(state, settlementId);
|
|
406
|
-
if (!record) {
|
|
407
|
-
return { ok: false, reason: "settlement_not_found" };
|
|
408
|
-
}
|
|
409
|
-
if (record.status !== "ready_for_submission" && record.status !== "failed") {
|
|
410
|
-
return { ok: true, settlement: clone(record) };
|
|
411
|
-
}
|
|
412
|
-
const config = loadBsvSettlementRuntimeConfig();
|
|
413
|
-
const submittedAtIso = new Date().toISOString();
|
|
414
|
-
const nextRefreshAtIso = new Date(Date.now() + 1500).toISOString();
|
|
415
|
-
if (config.mode === "mock") {
|
|
416
|
-
const txid = sha256Hex(`${record.settlement_id}:${record.settlement_reference}:${record.network}`).slice(0, 64);
|
|
417
|
-
const next = {
|
|
418
|
-
...record,
|
|
419
|
-
runtime_mode: config.mode,
|
|
420
|
-
status: "submitted",
|
|
421
|
-
finality_status: "submitted",
|
|
422
|
-
txid,
|
|
423
|
-
provider_ref: `bsv_teranode_submit_${record.settlement_id}`,
|
|
424
|
-
submitted_at_iso: submittedAtIso,
|
|
425
|
-
next_refresh_at_iso: nextRefreshAtIso,
|
|
426
|
-
last_error: null
|
|
427
|
-
};
|
|
428
|
-
state.bsvSettlementRecords.set(settlementId, next);
|
|
429
|
-
updateArtifactsForSettlementRecord(state, next);
|
|
430
|
-
return { ok: true, settlement: clone(next) };
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
let txid = record.txid || null;
|
|
434
|
-
let providerRef = record.provider_ref || null;
|
|
435
|
-
if (!txid && record.raw_tx && config.mode === "arc") {
|
|
436
|
-
const submitResult = await submitArcRawTransaction(config, record.raw_tx);
|
|
437
|
-
if (!submitResult.ok) {
|
|
438
|
-
const failed = {
|
|
439
|
-
...record,
|
|
440
|
-
runtime_mode: config.mode,
|
|
441
|
-
status: "failed",
|
|
442
|
-
finality_status: "submission_failed",
|
|
443
|
-
last_error: submitResult.reason
|
|
444
|
-
};
|
|
445
|
-
state.bsvSettlementRecords.set(settlementId, failed);
|
|
446
|
-
updateArtifactsForSettlementRecord(state, failed);
|
|
447
|
-
return { ok: false, reason: submitResult.reason, settlement: clone(failed), details: submitResult.body || null };
|
|
448
|
-
}
|
|
449
|
-
txid = submitResult.txid;
|
|
450
|
-
providerRef = submitResult.provider_ref || `arc_submit_${record.settlement_id}`;
|
|
451
|
-
}
|
|
452
|
-
if (!txid) {
|
|
453
|
-
return {
|
|
454
|
-
ok: false,
|
|
455
|
-
reason: config.mode === "observe" ? "settlement_txid_missing_for_observe_mode" : "settlement_submission_material_missing"
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
const next = {
|
|
459
|
-
...record,
|
|
460
|
-
runtime_mode: config.mode,
|
|
461
|
-
status: "submitted",
|
|
462
|
-
finality_status: "submitted",
|
|
463
|
-
txid,
|
|
464
|
-
provider_ref: providerRef || `bsv_teranode_submit_${record.settlement_id}`,
|
|
465
|
-
submitted_at_iso: submittedAtIso,
|
|
466
|
-
next_refresh_at_iso: nextRefreshAtIso,
|
|
467
|
-
last_error: null
|
|
468
|
-
};
|
|
469
|
-
state.bsvSettlementRecords.set(settlementId, next);
|
|
470
|
-
updateArtifactsForSettlementRecord(state, next);
|
|
471
|
-
return { ok: true, settlement: clone(next) };
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
async function refreshBsvSettlementRecord(state, settlementId) {
|
|
475
|
-
const record = getBsvSettlementRecord(state, settlementId);
|
|
476
|
-
if (!record) {
|
|
477
|
-
return { ok: false, reason: "settlement_not_found" };
|
|
478
|
-
}
|
|
479
|
-
if (!record.txid) {
|
|
480
|
-
return { ok: false, reason: "settlement_not_submitted" };
|
|
481
|
-
}
|
|
482
|
-
const config = loadBsvSettlementRuntimeConfig();
|
|
483
|
-
if (config.mode === "mock") {
|
|
484
|
-
const refreshCount = (record.refresh_count || 0) + 1;
|
|
485
|
-
const confirmationCount = Math.min(record.required_confirmations + 1, record.confirmation_count + 1);
|
|
486
|
-
let status = "observed";
|
|
487
|
-
let finalityStatus = "observed";
|
|
488
|
-
let observedAtIso = record.observed_at_iso;
|
|
489
|
-
let confirmedAtIso = record.confirmed_at_iso;
|
|
490
|
-
let finalizedAtIso = record.finalized_at_iso;
|
|
491
|
-
const nowIso = new Date().toISOString();
|
|
492
|
-
|
|
493
|
-
if (!observedAtIso) observedAtIso = nowIso;
|
|
494
|
-
if (confirmationCount >= record.required_confirmations) {
|
|
495
|
-
status = "confirmed";
|
|
496
|
-
finalityStatus = "confirmed";
|
|
497
|
-
confirmedAtIso = confirmedAtIso || nowIso;
|
|
498
|
-
}
|
|
499
|
-
if (confirmationCount > record.required_confirmations) {
|
|
500
|
-
status = "finalized";
|
|
501
|
-
finalityStatus = "finalized";
|
|
502
|
-
finalizedAtIso = finalizedAtIso || nowIso;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const next = {
|
|
506
|
-
...record,
|
|
507
|
-
refresh_count: refreshCount,
|
|
508
|
-
confirmation_count: confirmationCount,
|
|
509
|
-
status,
|
|
510
|
-
finality_status: finalityStatus,
|
|
511
|
-
observed_at_iso: observedAtIso,
|
|
512
|
-
confirmed_at_iso: confirmedAtIso,
|
|
513
|
-
finalized_at_iso: finalizedAtIso,
|
|
514
|
-
next_refresh_at_iso: status === "finalized" ? null : new Date(Date.now() + 1500).toISOString()
|
|
515
|
-
};
|
|
516
|
-
state.bsvSettlementRecords.set(settlementId, next);
|
|
517
|
-
updateArtifactsForSettlementRecord(state, next);
|
|
518
|
-
return { ok: true, settlement: clone(next) };
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const refreshCount = (record.refresh_count || 0) + 1;
|
|
522
|
-
let status = "submitted";
|
|
523
|
-
let finalityStatus = "submitted";
|
|
524
|
-
let observedAtIso = record.observed_at_iso;
|
|
525
|
-
let confirmedAtIso = record.confirmed_at_iso;
|
|
526
|
-
let finalizedAtIso = record.finalized_at_iso;
|
|
527
|
-
let confirmationCount = Number(record.confirmation_count || 0);
|
|
528
|
-
const nowIso = new Date().toISOString();
|
|
529
|
-
|
|
530
|
-
const arcStatus = config.mode === "arc"
|
|
531
|
-
? await fetchArcTransactionStatus(config, record.txid)
|
|
532
|
-
: { ok: false, reason: "arc_status_not_requested" };
|
|
533
|
-
const observerStatus = await fetchObserverTransactionStatus(config, record.txid);
|
|
534
|
-
if (arcStatus.ok || observerStatus.ok) {
|
|
535
|
-
observedAtIso = observedAtIso || nowIso;
|
|
536
|
-
status = "observed";
|
|
537
|
-
finalityStatus = "observed";
|
|
538
|
-
}
|
|
539
|
-
if (observerStatus.ok) {
|
|
540
|
-
confirmationCount = observerStatus.confirmations;
|
|
541
|
-
}
|
|
542
|
-
if (confirmationCount >= record.required_confirmations) {
|
|
543
|
-
status = "confirmed";
|
|
544
|
-
finalityStatus = "confirmed";
|
|
545
|
-
confirmedAtIso = confirmedAtIso || nowIso;
|
|
546
|
-
}
|
|
547
|
-
if (confirmationCount > record.required_confirmations) {
|
|
548
|
-
status = "finalized";
|
|
549
|
-
finalityStatus = "finalized";
|
|
550
|
-
finalizedAtIso = finalizedAtIso || nowIso;
|
|
551
|
-
}
|
|
552
|
-
const next = {
|
|
553
|
-
...record,
|
|
554
|
-
runtime_mode: config.mode,
|
|
555
|
-
refresh_count: refreshCount,
|
|
556
|
-
confirmation_count: confirmationCount,
|
|
557
|
-
status,
|
|
558
|
-
finality_status: finalityStatus,
|
|
559
|
-
observed_at_iso: observedAtIso,
|
|
560
|
-
confirmed_at_iso: confirmedAtIso,
|
|
561
|
-
finalized_at_iso: finalizedAtIso,
|
|
562
|
-
next_refresh_at_iso: status === "finalized" ? null : new Date(Date.now() + 1500).toISOString(),
|
|
563
|
-
last_error: observerStatus.ok || arcStatus.ok
|
|
564
|
-
? null
|
|
565
|
-
: observerStatus.reason || arcStatus.reason || record.last_error
|
|
566
|
-
};
|
|
567
|
-
state.bsvSettlementRecords.set(settlementId, next);
|
|
568
|
-
updateArtifactsForSettlementRecord(state, next);
|
|
569
|
-
return { ok: true, settlement: clone(next) };
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function summarizeBsvSettlementRecords(state) {
|
|
573
|
-
const records = Array.from(state.bsvSettlementRecords.values());
|
|
574
|
-
return {
|
|
575
|
-
settlement_count: records.length,
|
|
576
|
-
status_counts: records.reduce((acc, item) => {
|
|
577
|
-
const key = item && item.status ? item.status : "unknown";
|
|
578
|
-
acc[key] = (acc[key] || 0) + 1;
|
|
579
|
-
return acc;
|
|
580
|
-
}, {}),
|
|
581
|
-
finality_counts: records.reduce((acc, item) => {
|
|
582
|
-
const key = item && item.finality_status ? item.finality_status : "unknown";
|
|
583
|
-
acc[key] = (acc[key] || 0) + 1;
|
|
584
|
-
return acc;
|
|
585
|
-
}, {})
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
module.exports = {
|
|
590
|
-
buildSettlementLiveView,
|
|
591
|
-
createBsvSettlementRecord,
|
|
592
|
-
getBsvSettlementRecord,
|
|
593
|
-
listBsvSettlementRecords,
|
|
594
|
-
loadBsvSettlementRuntimeConfig,
|
|
595
|
-
refreshBsvSettlementRecord,
|
|
596
|
-
submitBsvSettlementRecord,
|
|
597
|
-
summarizeBsvSettlementRecords,
|
|
598
|
-
updateBsvSettlementSubmissionMaterial,
|
|
599
|
-
updateArtifactsForSettlementRecord
|
|
600
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const { buildFetchOptionsForPurpose } = require("./network_transport");
|
|
5
|
+
|
|
6
|
+
const LIVE_SETTLEMENT_MODE_ALIASES = new Map([
|
|
7
|
+
["live", "arc"],
|
|
8
|
+
["arc", "arc"],
|
|
9
|
+
["observe", "observe"],
|
|
10
|
+
["observer", "observe"],
|
|
11
|
+
["mock", "mock"]
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function firstEnv(...names) {
|
|
15
|
+
for (const name of names) {
|
|
16
|
+
const value = process.env[name];
|
|
17
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
18
|
+
return value.trim();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sha256Hex(value) {
|
|
25
|
+
return crypto.createHash("sha256").update(String(value || "")).digest("hex");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadBsvSettlementRuntimeConfig() {
|
|
29
|
+
const modeInput = String(
|
|
30
|
+
firstEnv(
|
|
31
|
+
"XYTARA_BSV_SETTLEMENT_RUNTIME_MODE",
|
|
32
|
+
"XYTARA_BSV_TERANODE_SETTLEMENT_RUNTIME_MODE"
|
|
33
|
+
) || "mock"
|
|
34
|
+
).trim().toLowerCase() || "mock";
|
|
35
|
+
const mode = LIVE_SETTLEMENT_MODE_ALIASES.get(modeInput) || modeInput;
|
|
36
|
+
const requiredConfirmations = Math.max(
|
|
37
|
+
1,
|
|
38
|
+
Number(firstEnv("XYTARA_BSV_REQUIRED_CONFIRMATIONS", "XYTARA_BSV_TERANODE_REQUIRED_CONFIRMATIONS") || 1) || 1
|
|
39
|
+
);
|
|
40
|
+
const network = String(firstEnv("XYTARA_BSV_NETWORK", "XYTARA_BSV_TERANODE_NETWORK") || "mainnet").trim() || "mainnet";
|
|
41
|
+
const observerNetwork = network === "mainnet" ? "main" : network === "testnet" ? "test" : network;
|
|
42
|
+
const observerUrl = String(
|
|
43
|
+
firstEnv("XYTARA_BSV_OBSERVER_URL", "XYTARA_BSV_TERANODE_OBSERVER_URL")
|
|
44
|
+
|| `https://api.whatsonchain.com/v1/bsv/${observerNetwork}`
|
|
45
|
+
).trim() || null;
|
|
46
|
+
return {
|
|
47
|
+
mode,
|
|
48
|
+
rpc_url: firstEnv("XYTARA_BSV_ARC_RPC_URL", "XYTARA_BSV_ARC_URL", "XYTARA_BSV_TERANODE_RPC_URL"),
|
|
49
|
+
rpc_api_key: firstEnv("XYTARA_BSV_ARC_RPC_API_KEY", "XYTARA_BSV_ARC_API_KEY", "XYTARA_BSV_TERANODE_RPC_API_KEY"),
|
|
50
|
+
rpc_auth_header: String(firstEnv("XYTARA_BSV_ARC_RPC_AUTH_HEADER", "XYTARA_BSV_ARC_AUTH_HEADER", "XYTARA_BSV_TERANODE_RPC_AUTH_HEADER") || "authorization").trim() || "authorization",
|
|
51
|
+
rpc_auth_scheme: String(firstEnv("XYTARA_BSV_ARC_RPC_AUTH_SCHEME", "XYTARA_BSV_ARC_AUTH_SCHEME", "XYTARA_BSV_TERANODE_RPC_AUTH_SCHEME") || "").trim(),
|
|
52
|
+
observer_url: observerUrl,
|
|
53
|
+
network,
|
|
54
|
+
required_confirmations: requiredConfirmations
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function clone(value) {
|
|
59
|
+
return JSON.parse(JSON.stringify(value));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isLiveSettlementRuntimeMode(mode) {
|
|
63
|
+
return String(mode || "").trim().toLowerCase() !== "mock";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildRuntimeAuthHeaders(config) {
|
|
67
|
+
if (!config || !config.rpc_api_key) return {};
|
|
68
|
+
const headerName = config.rpc_auth_header || "authorization";
|
|
69
|
+
const scheme = String(config.rpc_auth_scheme || "").trim();
|
|
70
|
+
return {
|
|
71
|
+
[headerName]: scheme ? `${scheme} ${config.rpc_api_key}` : config.rpc_api_key
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function extractSubmittedTransactionId(payload) {
|
|
76
|
+
if (!payload || typeof payload !== "object") return null;
|
|
77
|
+
return payload.txid || payload.txId || payload.transactionId || payload.data && payload.data.txid || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function submitArcRawTransaction(config, rawTx) {
|
|
81
|
+
const response = await fetch(config.rpc_url, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
"content-type": "application/json",
|
|
85
|
+
...buildRuntimeAuthHeaders(config)
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify({ rawTx }),
|
|
88
|
+
...buildFetchOptionsForPurpose("settlement")
|
|
89
|
+
});
|
|
90
|
+
const text = await response.text();
|
|
91
|
+
let parsed = null;
|
|
92
|
+
try {
|
|
93
|
+
parsed = text ? JSON.parse(text) : null;
|
|
94
|
+
} catch (_) {
|
|
95
|
+
parsed = null;
|
|
96
|
+
}
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
reason: "settlement_submit_failed",
|
|
101
|
+
status: response.status,
|
|
102
|
+
body: parsed || text || null
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const txid = extractSubmittedTransactionId(parsed);
|
|
106
|
+
if (!txid) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
reason: "settlement_submit_missing_txid",
|
|
110
|
+
status: response.status,
|
|
111
|
+
body: parsed || text || null
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
txid,
|
|
117
|
+
provider_ref: parsed && typeof parsed === "object" ? parsed.requestId || parsed.request_id || null : null,
|
|
118
|
+
response: parsed || text || null
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeArcStatusUrl(config, txid) {
|
|
123
|
+
const rpcUrl = String(config && config.rpc_url || "").replace(/\/$/, "");
|
|
124
|
+
if (!rpcUrl) return null;
|
|
125
|
+
if (rpcUrl.endsWith("/tx")) return `${rpcUrl}/${encodeURIComponent(txid)}`;
|
|
126
|
+
if (rpcUrl.includes("/tx/")) return rpcUrl;
|
|
127
|
+
return `${rpcUrl}/tx/${encodeURIComponent(txid)}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function fetchArcTransactionStatus(config, txid) {
|
|
131
|
+
const statusUrl = normalizeArcStatusUrl(config, txid);
|
|
132
|
+
if (!statusUrl) {
|
|
133
|
+
return { ok: false, reason: "settlement_transport_missing" };
|
|
134
|
+
}
|
|
135
|
+
const response = await fetch(statusUrl, {
|
|
136
|
+
headers: buildRuntimeAuthHeaders(config),
|
|
137
|
+
...buildFetchOptionsForPurpose("settlement")
|
|
138
|
+
});
|
|
139
|
+
const text = await response.text();
|
|
140
|
+
let parsed = null;
|
|
141
|
+
try {
|
|
142
|
+
parsed = text ? JSON.parse(text) : null;
|
|
143
|
+
} catch (_) {
|
|
144
|
+
parsed = null;
|
|
145
|
+
}
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
reason: response.status === 404 ? "tx_not_found" : "arc_status_failed",
|
|
150
|
+
status: response.status,
|
|
151
|
+
body: parsed || text || null
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
payload: parsed || null
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function fetchObserverTransactionStatus(config, txid) {
|
|
161
|
+
const observerBaseUrl = String(config && config.observer_url || "").replace(/\/$/, "");
|
|
162
|
+
if (!observerBaseUrl) {
|
|
163
|
+
return { ok: false, reason: "settlement_observer_missing" };
|
|
164
|
+
}
|
|
165
|
+
const response = await fetch(`${observerBaseUrl}/tx/hash/${encodeURIComponent(txid)}`, buildFetchOptionsForPurpose("settlement"));
|
|
166
|
+
const text = await response.text();
|
|
167
|
+
let parsed = null;
|
|
168
|
+
try {
|
|
169
|
+
parsed = text ? JSON.parse(text) : null;
|
|
170
|
+
} catch (_) {
|
|
171
|
+
parsed = null;
|
|
172
|
+
}
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
reason: response.status === 404 ? "tx_not_found" : "observer_status_failed",
|
|
177
|
+
status: response.status,
|
|
178
|
+
body: parsed || text || null
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const confirmations = Number(
|
|
182
|
+
parsed && (
|
|
183
|
+
parsed.confirmations
|
|
184
|
+
|| parsed.confirmationCount
|
|
185
|
+
|| parsed.confirmation_count
|
|
186
|
+
|| parsed.data && (parsed.data.confirmations || parsed.data.confirmationCount || parsed.data.confirmation_count)
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
const blockhash = parsed && (parsed.blockhash || parsed.blockHash || parsed.merkleRoot || parsed.data && (parsed.data.blockhash || parsed.data.blockHash)) || null;
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
confirmations: Number.isFinite(confirmations) && confirmations >= 0 ? confirmations : 0,
|
|
193
|
+
blockhash,
|
|
194
|
+
payload: parsed || null
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildSettlementLiveView(record) {
|
|
199
|
+
if (!record) return null;
|
|
200
|
+
return {
|
|
201
|
+
settlement_id: record.settlement_id,
|
|
202
|
+
status: record.status,
|
|
203
|
+
finality_status: record.finality_status,
|
|
204
|
+
txid: record.txid,
|
|
205
|
+
network: record.network,
|
|
206
|
+
rail: record.rail,
|
|
207
|
+
confirmation_count: record.confirmation_count,
|
|
208
|
+
required_confirmations: record.required_confirmations,
|
|
209
|
+
raw_tx_present: Boolean(record.raw_tx),
|
|
210
|
+
submitted_at_iso: record.submitted_at_iso,
|
|
211
|
+
observed_at_iso: record.observed_at_iso,
|
|
212
|
+
confirmed_at_iso: record.confirmed_at_iso,
|
|
213
|
+
finalized_at_iso: record.finalized_at_iso,
|
|
214
|
+
next_refresh_at_iso: record.next_refresh_at_iso || null,
|
|
215
|
+
refresh_count: record.refresh_count,
|
|
216
|
+
runtime_mode: record.runtime_mode
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function updateArtifactsForSettlementRecord(state, record) {
|
|
221
|
+
if (!record) return;
|
|
222
|
+
const linked = state.transactions.get(record.transaction_id);
|
|
223
|
+
if (linked) {
|
|
224
|
+
const settlementLive = buildSettlementLiveView(record);
|
|
225
|
+
const verified = record.finality_status === "confirmed" || record.finality_status === "finalized";
|
|
226
|
+
const transaction = {
|
|
227
|
+
...linked.transaction,
|
|
228
|
+
settlement: {
|
|
229
|
+
...(linked.transaction.settlement || {}),
|
|
230
|
+
status: record.status,
|
|
231
|
+
verified,
|
|
232
|
+
txid: record.txid || (linked.transaction.settlement && linked.transaction.settlement.txid) || null,
|
|
233
|
+
live: settlementLive
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const receipt = {
|
|
237
|
+
...linked.receipt,
|
|
238
|
+
settlement: {
|
|
239
|
+
...(linked.receipt.settlement || {}),
|
|
240
|
+
status: record.status,
|
|
241
|
+
verified,
|
|
242
|
+
txid: record.txid || (linked.receipt.settlement && linked.receipt.settlement.txid) || null,
|
|
243
|
+
live: settlementLive
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const next = {
|
|
247
|
+
...linked,
|
|
248
|
+
transaction,
|
|
249
|
+
receipt
|
|
250
|
+
};
|
|
251
|
+
state.transactions.set(record.transaction_id, next);
|
|
252
|
+
state.receipts.set(receipt.receipt_id, next);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (Array.isArray(state.paymentLedger)) {
|
|
256
|
+
state.paymentLedger = state.paymentLedger.map((entry) => {
|
|
257
|
+
if (!entry || entry.payment_id !== record.payment_id) return entry;
|
|
258
|
+
return {
|
|
259
|
+
...entry,
|
|
260
|
+
settlement: {
|
|
261
|
+
...(entry.settlement || {}),
|
|
262
|
+
status: record.status,
|
|
263
|
+
verified: record.finality_status === "confirmed" || record.finality_status === "finalized",
|
|
264
|
+
txid: record.txid || (entry.settlement && entry.settlement.txid) || null,
|
|
265
|
+
live: buildSettlementLiveView(record)
|
|
266
|
+
},
|
|
267
|
+
live_settlement: buildSettlementLiveView(record)
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function createBsvSettlementRecord(state, transaction, receipt, paymentRecord) {
|
|
274
|
+
if (!transaction || !transaction.settlement || transaction.settlement.rail !== "bsv_teranode") return null;
|
|
275
|
+
const config = loadBsvSettlementRuntimeConfig();
|
|
276
|
+
const settlementId = transaction.settlement && transaction.settlement.settlement_id
|
|
277
|
+
? transaction.settlement.settlement_id
|
|
278
|
+
: `settlement_${crypto.randomBytes(6).toString("hex")}`;
|
|
279
|
+
const existing = state.bsvSettlementRecords.get(settlementId);
|
|
280
|
+
if (existing) return existing;
|
|
281
|
+
const record = {
|
|
282
|
+
settlement_id: settlementId,
|
|
283
|
+
transaction_id: transaction.transaction_id,
|
|
284
|
+
receipt_id: receipt && receipt.receipt_id ? receipt.receipt_id : null,
|
|
285
|
+
payment_id: paymentRecord && paymentRecord.payment_id ? paymentRecord.payment_id : null,
|
|
286
|
+
account_id: transaction.account_id || null,
|
|
287
|
+
quote_id: transaction.quote_id || null,
|
|
288
|
+
amount_minor: transaction.amount_minor,
|
|
289
|
+
currency: transaction.currency,
|
|
290
|
+
rail: "bsv_teranode",
|
|
291
|
+
network: transaction.settlement && transaction.settlement.network ? transaction.settlement.network : config.network,
|
|
292
|
+
runtime_mode: config.mode,
|
|
293
|
+
status: "ready_for_submission",
|
|
294
|
+
finality_status: "pending_submission",
|
|
295
|
+
confirmation_count: 0,
|
|
296
|
+
required_confirmations: config.required_confirmations,
|
|
297
|
+
txid: transaction.settlement && transaction.settlement.txid ? transaction.settlement.txid : null,
|
|
298
|
+
raw_tx: transaction.settlement && transaction.settlement.raw_tx ? transaction.settlement.raw_tx : null,
|
|
299
|
+
provider_ref: null,
|
|
300
|
+
settlement_reference:
|
|
301
|
+
transaction.settlement && transaction.settlement.binding_hash
|
|
302
|
+
? transaction.settlement.binding_hash
|
|
303
|
+
: null,
|
|
304
|
+
merchant_address:
|
|
305
|
+
transaction.settlement && transaction.settlement.hints
|
|
306
|
+
? transaction.settlement.hints.merchant_address || null
|
|
307
|
+
: null,
|
|
308
|
+
merchant_paymail:
|
|
309
|
+
transaction.settlement && transaction.settlement.hints
|
|
310
|
+
? transaction.settlement.hints.merchant_paymail || null
|
|
311
|
+
: null,
|
|
312
|
+
created_at_iso: new Date().toISOString(),
|
|
313
|
+
submitted_at_iso: null,
|
|
314
|
+
observed_at_iso: null,
|
|
315
|
+
confirmed_at_iso: null,
|
|
316
|
+
finalized_at_iso: null,
|
|
317
|
+
next_refresh_at_iso: null,
|
|
318
|
+
refresh_count: 0,
|
|
319
|
+
last_error: null
|
|
320
|
+
};
|
|
321
|
+
state.bsvSettlementRecords.set(settlementId, record);
|
|
322
|
+
updateArtifactsForSettlementRecord(state, record);
|
|
323
|
+
return record;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function listBsvSettlementRecords(state, filters = {}) {
|
|
327
|
+
const records = Array.from(state.bsvSettlementRecords.values());
|
|
328
|
+
let items = records;
|
|
329
|
+
if (filters.status) items = items.filter((item) => item.status === filters.status);
|
|
330
|
+
if (filters.account_id) items = items.filter((item) => item.account_id === filters.account_id);
|
|
331
|
+
if (filters.transaction_id) items = items.filter((item) => item.transaction_id === filters.transaction_id);
|
|
332
|
+
if (filters.payment_id) items = items.filter((item) => item.payment_id === filters.payment_id);
|
|
333
|
+
return items;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function getBsvSettlementRecord(state, settlementId) {
|
|
337
|
+
return state.bsvSettlementRecords.get(settlementId) || null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function normalizeOptionalString(value) {
|
|
341
|
+
if (typeof value !== "string") return null;
|
|
342
|
+
const normalized = value.trim();
|
|
343
|
+
return normalized ? normalized : null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function normalizeTxid(value) {
|
|
347
|
+
const txid = normalizeOptionalString(value);
|
|
348
|
+
if (!txid) return null;
|
|
349
|
+
const lowered = txid.toLowerCase();
|
|
350
|
+
return /^[a-f0-9]{64}$/.test(lowered) ? lowered : null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function normalizeRawTransaction(value) {
|
|
354
|
+
const rawTx = normalizeOptionalString(value);
|
|
355
|
+
if (!rawTx) return null;
|
|
356
|
+
if (rawTx.length < 20 || rawTx.length % 2 !== 0) return null;
|
|
357
|
+
return /^[a-f0-9]+$/i.test(rawTx) ? rawTx.toLowerCase() : null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function updateBsvSettlementSubmissionMaterial(state, settlementId, payload = {}) {
|
|
361
|
+
const body = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
362
|
+
const record = getBsvSettlementRecord(state, settlementId);
|
|
363
|
+
if (!record) {
|
|
364
|
+
return { ok: false, reason: "settlement_not_found" };
|
|
365
|
+
}
|
|
366
|
+
if (record.status !== "ready_for_submission" && record.status !== "failed") {
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
reason: "settlement_material_update_not_allowed",
|
|
370
|
+
settlement: clone(record)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const txidInput = body.txid || body.txId || body.transaction_id || body.transactionId;
|
|
375
|
+
const rawTxInput = body.raw_tx || body.rawTx || body.raw_transaction || body.rawTransaction;
|
|
376
|
+
const txid = normalizeTxid(txidInput);
|
|
377
|
+
const rawTx = normalizeRawTransaction(rawTxInput);
|
|
378
|
+
if (txidInput && !txid) {
|
|
379
|
+
return { ok: false, reason: "settlement_txid_invalid", settlement: clone(record) };
|
|
380
|
+
}
|
|
381
|
+
if (rawTxInput && !rawTx) {
|
|
382
|
+
return { ok: false, reason: "settlement_raw_tx_invalid", settlement: clone(record) };
|
|
383
|
+
}
|
|
384
|
+
if (!txid && !rawTx) {
|
|
385
|
+
return { ok: false, reason: "settlement_submission_material_required", settlement: clone(record) };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const next = {
|
|
389
|
+
...record,
|
|
390
|
+
txid: txid || record.txid || null,
|
|
391
|
+
raw_tx: rawTx || record.raw_tx || null,
|
|
392
|
+
provider_ref: normalizeOptionalString(body.provider_ref || body.providerRef) || record.provider_ref || null,
|
|
393
|
+
network: normalizeOptionalString(body.network) || record.network,
|
|
394
|
+
runtime_mode: loadBsvSettlementRuntimeConfig().mode,
|
|
395
|
+
finality_status: "pending_submission",
|
|
396
|
+
last_error: null,
|
|
397
|
+
submission_material_updated_at_iso: new Date().toISOString()
|
|
398
|
+
};
|
|
399
|
+
state.bsvSettlementRecords.set(settlementId, next);
|
|
400
|
+
updateArtifactsForSettlementRecord(state, next);
|
|
401
|
+
return { ok: true, settlement: clone(next) };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function submitBsvSettlementRecord(state, settlementId) {
|
|
405
|
+
const record = getBsvSettlementRecord(state, settlementId);
|
|
406
|
+
if (!record) {
|
|
407
|
+
return { ok: false, reason: "settlement_not_found" };
|
|
408
|
+
}
|
|
409
|
+
if (record.status !== "ready_for_submission" && record.status !== "failed") {
|
|
410
|
+
return { ok: true, settlement: clone(record) };
|
|
411
|
+
}
|
|
412
|
+
const config = loadBsvSettlementRuntimeConfig();
|
|
413
|
+
const submittedAtIso = new Date().toISOString();
|
|
414
|
+
const nextRefreshAtIso = new Date(Date.now() + 1500).toISOString();
|
|
415
|
+
if (config.mode === "mock") {
|
|
416
|
+
const txid = sha256Hex(`${record.settlement_id}:${record.settlement_reference}:${record.network}`).slice(0, 64);
|
|
417
|
+
const next = {
|
|
418
|
+
...record,
|
|
419
|
+
runtime_mode: config.mode,
|
|
420
|
+
status: "submitted",
|
|
421
|
+
finality_status: "submitted",
|
|
422
|
+
txid,
|
|
423
|
+
provider_ref: `bsv_teranode_submit_${record.settlement_id}`,
|
|
424
|
+
submitted_at_iso: submittedAtIso,
|
|
425
|
+
next_refresh_at_iso: nextRefreshAtIso,
|
|
426
|
+
last_error: null
|
|
427
|
+
};
|
|
428
|
+
state.bsvSettlementRecords.set(settlementId, next);
|
|
429
|
+
updateArtifactsForSettlementRecord(state, next);
|
|
430
|
+
return { ok: true, settlement: clone(next) };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let txid = record.txid || null;
|
|
434
|
+
let providerRef = record.provider_ref || null;
|
|
435
|
+
if (!txid && record.raw_tx && config.mode === "arc") {
|
|
436
|
+
const submitResult = await submitArcRawTransaction(config, record.raw_tx);
|
|
437
|
+
if (!submitResult.ok) {
|
|
438
|
+
const failed = {
|
|
439
|
+
...record,
|
|
440
|
+
runtime_mode: config.mode,
|
|
441
|
+
status: "failed",
|
|
442
|
+
finality_status: "submission_failed",
|
|
443
|
+
last_error: submitResult.reason
|
|
444
|
+
};
|
|
445
|
+
state.bsvSettlementRecords.set(settlementId, failed);
|
|
446
|
+
updateArtifactsForSettlementRecord(state, failed);
|
|
447
|
+
return { ok: false, reason: submitResult.reason, settlement: clone(failed), details: submitResult.body || null };
|
|
448
|
+
}
|
|
449
|
+
txid = submitResult.txid;
|
|
450
|
+
providerRef = submitResult.provider_ref || `arc_submit_${record.settlement_id}`;
|
|
451
|
+
}
|
|
452
|
+
if (!txid) {
|
|
453
|
+
return {
|
|
454
|
+
ok: false,
|
|
455
|
+
reason: config.mode === "observe" ? "settlement_txid_missing_for_observe_mode" : "settlement_submission_material_missing"
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const next = {
|
|
459
|
+
...record,
|
|
460
|
+
runtime_mode: config.mode,
|
|
461
|
+
status: "submitted",
|
|
462
|
+
finality_status: "submitted",
|
|
463
|
+
txid,
|
|
464
|
+
provider_ref: providerRef || `bsv_teranode_submit_${record.settlement_id}`,
|
|
465
|
+
submitted_at_iso: submittedAtIso,
|
|
466
|
+
next_refresh_at_iso: nextRefreshAtIso,
|
|
467
|
+
last_error: null
|
|
468
|
+
};
|
|
469
|
+
state.bsvSettlementRecords.set(settlementId, next);
|
|
470
|
+
updateArtifactsForSettlementRecord(state, next);
|
|
471
|
+
return { ok: true, settlement: clone(next) };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function refreshBsvSettlementRecord(state, settlementId) {
|
|
475
|
+
const record = getBsvSettlementRecord(state, settlementId);
|
|
476
|
+
if (!record) {
|
|
477
|
+
return { ok: false, reason: "settlement_not_found" };
|
|
478
|
+
}
|
|
479
|
+
if (!record.txid) {
|
|
480
|
+
return { ok: false, reason: "settlement_not_submitted" };
|
|
481
|
+
}
|
|
482
|
+
const config = loadBsvSettlementRuntimeConfig();
|
|
483
|
+
if (config.mode === "mock") {
|
|
484
|
+
const refreshCount = (record.refresh_count || 0) + 1;
|
|
485
|
+
const confirmationCount = Math.min(record.required_confirmations + 1, record.confirmation_count + 1);
|
|
486
|
+
let status = "observed";
|
|
487
|
+
let finalityStatus = "observed";
|
|
488
|
+
let observedAtIso = record.observed_at_iso;
|
|
489
|
+
let confirmedAtIso = record.confirmed_at_iso;
|
|
490
|
+
let finalizedAtIso = record.finalized_at_iso;
|
|
491
|
+
const nowIso = new Date().toISOString();
|
|
492
|
+
|
|
493
|
+
if (!observedAtIso) observedAtIso = nowIso;
|
|
494
|
+
if (confirmationCount >= record.required_confirmations) {
|
|
495
|
+
status = "confirmed";
|
|
496
|
+
finalityStatus = "confirmed";
|
|
497
|
+
confirmedAtIso = confirmedAtIso || nowIso;
|
|
498
|
+
}
|
|
499
|
+
if (confirmationCount > record.required_confirmations) {
|
|
500
|
+
status = "finalized";
|
|
501
|
+
finalityStatus = "finalized";
|
|
502
|
+
finalizedAtIso = finalizedAtIso || nowIso;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const next = {
|
|
506
|
+
...record,
|
|
507
|
+
refresh_count: refreshCount,
|
|
508
|
+
confirmation_count: confirmationCount,
|
|
509
|
+
status,
|
|
510
|
+
finality_status: finalityStatus,
|
|
511
|
+
observed_at_iso: observedAtIso,
|
|
512
|
+
confirmed_at_iso: confirmedAtIso,
|
|
513
|
+
finalized_at_iso: finalizedAtIso,
|
|
514
|
+
next_refresh_at_iso: status === "finalized" ? null : new Date(Date.now() + 1500).toISOString()
|
|
515
|
+
};
|
|
516
|
+
state.bsvSettlementRecords.set(settlementId, next);
|
|
517
|
+
updateArtifactsForSettlementRecord(state, next);
|
|
518
|
+
return { ok: true, settlement: clone(next) };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const refreshCount = (record.refresh_count || 0) + 1;
|
|
522
|
+
let status = "submitted";
|
|
523
|
+
let finalityStatus = "submitted";
|
|
524
|
+
let observedAtIso = record.observed_at_iso;
|
|
525
|
+
let confirmedAtIso = record.confirmed_at_iso;
|
|
526
|
+
let finalizedAtIso = record.finalized_at_iso;
|
|
527
|
+
let confirmationCount = Number(record.confirmation_count || 0);
|
|
528
|
+
const nowIso = new Date().toISOString();
|
|
529
|
+
|
|
530
|
+
const arcStatus = config.mode === "arc"
|
|
531
|
+
? await fetchArcTransactionStatus(config, record.txid)
|
|
532
|
+
: { ok: false, reason: "arc_status_not_requested" };
|
|
533
|
+
const observerStatus = await fetchObserverTransactionStatus(config, record.txid);
|
|
534
|
+
if (arcStatus.ok || observerStatus.ok) {
|
|
535
|
+
observedAtIso = observedAtIso || nowIso;
|
|
536
|
+
status = "observed";
|
|
537
|
+
finalityStatus = "observed";
|
|
538
|
+
}
|
|
539
|
+
if (observerStatus.ok) {
|
|
540
|
+
confirmationCount = observerStatus.confirmations;
|
|
541
|
+
}
|
|
542
|
+
if (confirmationCount >= record.required_confirmations) {
|
|
543
|
+
status = "confirmed";
|
|
544
|
+
finalityStatus = "confirmed";
|
|
545
|
+
confirmedAtIso = confirmedAtIso || nowIso;
|
|
546
|
+
}
|
|
547
|
+
if (confirmationCount > record.required_confirmations) {
|
|
548
|
+
status = "finalized";
|
|
549
|
+
finalityStatus = "finalized";
|
|
550
|
+
finalizedAtIso = finalizedAtIso || nowIso;
|
|
551
|
+
}
|
|
552
|
+
const next = {
|
|
553
|
+
...record,
|
|
554
|
+
runtime_mode: config.mode,
|
|
555
|
+
refresh_count: refreshCount,
|
|
556
|
+
confirmation_count: confirmationCount,
|
|
557
|
+
status,
|
|
558
|
+
finality_status: finalityStatus,
|
|
559
|
+
observed_at_iso: observedAtIso,
|
|
560
|
+
confirmed_at_iso: confirmedAtIso,
|
|
561
|
+
finalized_at_iso: finalizedAtIso,
|
|
562
|
+
next_refresh_at_iso: status === "finalized" ? null : new Date(Date.now() + 1500).toISOString(),
|
|
563
|
+
last_error: observerStatus.ok || arcStatus.ok
|
|
564
|
+
? null
|
|
565
|
+
: observerStatus.reason || arcStatus.reason || record.last_error
|
|
566
|
+
};
|
|
567
|
+
state.bsvSettlementRecords.set(settlementId, next);
|
|
568
|
+
updateArtifactsForSettlementRecord(state, next);
|
|
569
|
+
return { ok: true, settlement: clone(next) };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function summarizeBsvSettlementRecords(state) {
|
|
573
|
+
const records = Array.from(state.bsvSettlementRecords.values());
|
|
574
|
+
return {
|
|
575
|
+
settlement_count: records.length,
|
|
576
|
+
status_counts: records.reduce((acc, item) => {
|
|
577
|
+
const key = item && item.status ? item.status : "unknown";
|
|
578
|
+
acc[key] = (acc[key] || 0) + 1;
|
|
579
|
+
return acc;
|
|
580
|
+
}, {}),
|
|
581
|
+
finality_counts: records.reduce((acc, item) => {
|
|
582
|
+
const key = item && item.finality_status ? item.finality_status : "unknown";
|
|
583
|
+
acc[key] = (acc[key] || 0) + 1;
|
|
584
|
+
return acc;
|
|
585
|
+
}, {})
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
module.exports = {
|
|
590
|
+
buildSettlementLiveView,
|
|
591
|
+
createBsvSettlementRecord,
|
|
592
|
+
getBsvSettlementRecord,
|
|
593
|
+
listBsvSettlementRecords,
|
|
594
|
+
loadBsvSettlementRuntimeConfig,
|
|
595
|
+
refreshBsvSettlementRecord,
|
|
596
|
+
submitBsvSettlementRecord,
|
|
597
|
+
summarizeBsvSettlementRecords,
|
|
598
|
+
updateBsvSettlementSubmissionMaterial,
|
|
599
|
+
updateArtifactsForSettlementRecord
|
|
600
|
+
};
|