xytara 3.1.13 → 3.1.15

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.
Files changed (68) hide show
  1. package/.env.example +38 -4
  2. package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
  3. package/REAL_PAYMENT_SETUP.md +14 -7
  4. package/RELEASE_NOTES.md +9 -4
  5. package/fixtures/catalog.js +22 -0
  6. package/lib/a2a_rpc.js +287 -280
  7. package/lib/account_auth.js +585 -585
  8. package/lib/agent_identity.js +162 -162
  9. package/lib/asyncapi_contract.js +3 -3
  10. package/lib/bolt_adapter.js +187 -187
  11. package/lib/bolt_payment_front.js +224 -224
  12. package/lib/buyer_event_log.js +215 -0
  13. package/lib/buyer_safety_policy.js +132 -0
  14. package/lib/checkout_processor_adapter.js +217 -217
  15. package/lib/command_flow.js +911 -722
  16. package/lib/commerce_artifacts.js +591 -591
  17. package/lib/commerce_bridge.js +274 -274
  18. package/lib/commerce_cases.js +399 -399
  19. package/lib/commerce_client.js +7694 -7694
  20. package/lib/commerce_quotes.js +29 -0
  21. package/lib/commerce_runtime.js +322 -322
  22. package/lib/execution_dedupe.js +86 -86
  23. package/lib/external_credit_bridge.js +142 -142
  24. package/lib/http_transport.js +99 -81
  25. package/lib/l402_adapter.js +241 -241
  26. package/lib/l402_payment_front.js +265 -265
  27. package/lib/mpp_adapter.js +237 -237
  28. package/lib/openapi_contract.js +4 -0
  29. package/lib/payment_config.js +257 -253
  30. package/lib/payment_verification.js +357 -357
  31. package/lib/provider_payout_execution.js +155 -155
  32. package/lib/provider_registry.js +852 -842
  33. package/lib/rate_limiter.js +81 -81
  34. package/lib/runtime_state_store.js +495 -369
  35. package/lib/settlement_bsv_live.js +600 -600
  36. package/lib/settlement_bsv_teranode.js +117 -117
  37. package/lib/stripe_mpp_payment_front.js +258 -258
  38. package/lib/x402_facilitator.js +198 -191
  39. package/lib/x402_lane_contract.js +5 -3
  40. package/lib/x402_settlement_recovery.js +175 -0
  41. package/package.json +9 -3
  42. package/scripts/buyer_events_daily_report.js +79 -0
  43. package/scripts/state_backup_offsite.js +196 -0
  44. package/scripts/verify_a2a_rpc.js +131 -105
  45. package/scripts/verify_agent_identity.js +153 -153
  46. package/scripts/verify_all.js +1 -1
  47. package/scripts/verify_bolt_adapter.js +128 -128
  48. package/scripts/verify_buyer_event_log.js +123 -0
  49. package/scripts/verify_buyer_safety.js +141 -0
  50. package/scripts/verify_cdp_facilitator.js +110 -110
  51. package/scripts/verify_checkout_processor.js +109 -109
  52. package/scripts/verify_commerce_bridge.js +177 -177
  53. package/scripts/verify_execution_dedupe.js +189 -189
  54. package/scripts/verify_execution_surfacing.js +76 -76
  55. package/scripts/verify_final_activation.js +191 -191
  56. package/scripts/verify_l402_adapter.js +86 -86
  57. package/scripts/verify_lifecycle_spend_binding.js +88 -0
  58. package/scripts/verify_mpp_adapter.js +128 -128
  59. package/scripts/verify_production_readiness.js +268 -268
  60. package/scripts/verify_rate_limiter.js +52 -52
  61. package/scripts/verify_refund_credit.js +106 -106
  62. package/scripts/verify_release_candidate.js +81 -81
  63. package/scripts/verify_runtime_state_guard.js +117 -0
  64. package/scripts/verify_service.js +15421 -15421
  65. package/scripts/verify_verify_execute_settle.js +209 -208
  66. package/scripts/verify_x402_facilitator.js +307 -286
  67. package/scripts/verify_x402_settlement_recovery.js +153 -0
  68. package/server.js +12242 -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
+ };