UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

260 lines 11.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TaskArcadeSSE = void 0; const sdk_1 = require("@bsv/sdk"); const types_1 = require("../../sdk/types"); const EntityProvenTxReq_1 = require("../../storage/schema/entities/EntityProvenTxReq"); const EntityProvenTx_1 = require("../../storage/schema/entities/EntityProvenTx"); const ArcSSEClient_1 = require("../../services/providers/ArcSSEClient"); const WalletMonitorTask_1 = require("./WalletMonitorTask"); /** * Monitor task that receives transaction status updates from Arcade via SSE * and processes them — including fetching merkle proofs directly from Arcade * when transactions are MINED. */ class TaskArcadeSSE extends WalletMonitorTask_1.WalletMonitorTask { constructor(monitor) { super(monitor, TaskArcadeSSE.taskName); this.sseClient = null; this.pendingEvents = []; } async asyncSetup() { var _a, _b, _c, _d, _e; const callbackToken = this.monitor.options.callbackToken; if (!callbackToken) { console.log('[TaskArcadeSSE] no callbackToken configured — SSE disabled'); return; } const arcUrl = (_a = this.monitor.services.options) === null || _a === void 0 ? void 0 : _a.arcUrl; if (!arcUrl) { console.log('[TaskArcadeSSE] no arcUrl configured — SSE disabled'); return; } const EventSourceClass = this.monitor.options.EventSourceClass; if (!EventSourceClass) { console.log('[TaskArcadeSSE] no EventSourceClass provided — SSE disabled'); return; } let lastEventId; try { lastEventId = await ((_c = (_b = this.monitor.options).loadLastSSEEventId) === null || _c === void 0 ? void 0 : _c.call(_b)); console.log(`[TaskArcadeSSE] loaded persisted lastEventId: ${lastEventId !== null && lastEventId !== void 0 ? lastEventId : '(none)'}`); } catch (e) { console.log(`[TaskArcadeSSE] failed to load lastEventId: ${e}`); } const arcApiKey = (_e = (_d = this.monitor.services.options) === null || _d === void 0 ? void 0 : _d.arcConfig) === null || _e === void 0 ? void 0 : _e.apiKey; console.log(`[TaskArcadeSSE] setting up — arcUrl=${arcUrl} token=${callbackToken.substring(0, 8)}...`); this.sseClient = new ArcSSEClient_1.ArcSSEClient({ baseUrl: arcUrl, callbackToken, arcApiKey, lastEventId, EventSourceClass, onEvent: event => { this.pendingEvents.push(event); }, onError: err => { console.log(`[TaskArcadeSSE] error: ${err.message}`); }, onLastEventIdChanged: (id) => { var _a, _b; (_b = (_a = this.monitor.options).saveLastSSEEventId) === null || _b === void 0 ? void 0 : _b.call(_a, id).catch(e => { console.log(`[TaskArcadeSSE] failed to persist lastEventId: ${e}`); }); } }); this.sseClient.connect(); } trigger(_nowMsecsSinceEpoch) { return { run: this.pendingEvents.length > 0 }; } async runTask() { const events = this.pendingEvents.splice(0); if (events.length === 0) return ''; let log = ''; for (const event of events) { log += await this.processStatusEvent(event); } return log; } async fetchNow() { if (!this.sseClient) return 0; return await this.sseClient.fetchEvents(); } async processStatusEvent(event) { let log = `SSE: txid=${event.txid} status=${event.txStatus}\n`; const reqs = await this.storage.findProvenTxReqs({ partial: { txid: event.txid } }); if (reqs.length === 0) { log += ` No matching ProvenTxReq\n`; return log; } for (const reqApi of reqs) { const req = new EntityProvenTxReq_1.EntityProvenTxReq(reqApi); if (types_1.ProvenTxReqTerminalStatus.includes(req.status)) { log += ` req ${req.id} already terminal: ${req.status}\n`; continue; } const note = { when: new Date().toISOString(), what: 'arcSSE', arcStatus: event.txStatus }; switch (event.txStatus) { case 'SENT_TO_NETWORK': case 'ACCEPTED_BY_NETWORK': case 'SEEN_ON_NETWORK': { if (['unsent', 'sending', 'callback'].includes(req.status)) { req.status = 'unmined'; req.addHistoryNote(note); await req.updateStorageDynamicProperties(this.storage); const ids = req.notify.transactionIds; if (ids) { await this.storage.runAsStorageProvider(async (sp) => { await sp.updateTransactionsStatus(ids, 'unproven'); }); } log += ` req ${req.id} => unmined\n`; } break; } case 'MINED': case 'IMMUTABLE': { req.addHistoryNote(note); await req.updateStorageDynamicProperties(this.storage); // Fetch proof directly from Arcade and complete the transaction log += await this.fetchProofFromArcade(req); break; } case 'DOUBLE_SPEND_ATTEMPTED': { req.status = 'doubleSpend'; req.addHistoryNote(note); await req.updateStorageDynamicProperties(this.storage); const ids = req.notify.transactionIds; if (ids) { await this.storage.runAsStorageProvider(async (sp) => { await sp.updateTransactionsStatus(ids, 'failed'); }); } log += ` req ${req.id} => doubleSpend\n`; break; } case 'REJECTED': { req.status = 'invalid'; req.addHistoryNote(note); await req.updateStorageDynamicProperties(this.storage); const ids = req.notify.transactionIds; if (ids) { await this.storage.runAsStorageProvider(async (sp) => { await sp.updateTransactionsStatus(ids, 'failed'); }); } log += ` req ${req.id} => invalid\n`; break; } default: log += ` req ${req.id} unhandled status: ${event.txStatus}\n`; break; } } this.monitor.callOnTransactionStatusChanged(event.txid, event.txStatus); return log; } /** * Fetch the merklePath from Arcade's GET /tx/{txid} endpoint and * create a ProvenTx record, completing the transaction. */ async fetchProofFromArcade(req) { var _a, _b, _c; const arcUrl = (_a = this.monitor.services.options) === null || _a === void 0 ? void 0 : _a.arcUrl; const txid = req.txid; let log = ` req ${req.id} MINED/IMMUTABLE — fetching proof from Arcade\n`; try { const fetchHeaders = {}; const apiKey = (_c = (_b = this.monitor.services.options) === null || _b === void 0 ? void 0 : _b.arcConfig) === null || _c === void 0 ? void 0 : _c.apiKey; if (apiKey) { fetchHeaders['Authorization'] = `Bearer ${apiKey}`; } const response = await fetch(`${arcUrl}/tx/${txid}`, { headers: fetchHeaders }); if (!response.ok) { log += ` Arcade GET /tx/${txid} returned ${response.status}\n`; return log; } const data = await response.json(); console.log(`[TaskArcadeSSE] GET /tx/${txid}:`, JSON.stringify(data)); if (!data.merklePath) { log += ` No merklePath in response (status=${data.txStatus})\n`; return log; } // Parse the merklePath hex from Arcade const merklePath = sdk_1.MerklePath.fromHex(data.merklePath); const merkleRoot = merklePath.computeRoot(txid); // Find the leaf to get the tx index const leaf = merklePath.path[0].find(l => l.txid === true && l.hash === txid); if (!leaf) { log += ` merklePath does not contain leaf for txid\n`; return log; } const blockHash = data.blockHash || ''; const height = data.blockHeight || merklePath.blockHeight; // Create ProvenTx entity const now = new Date(); const ptx = new EntityProvenTx_1.EntityProvenTx({ created_at: now, updated_at: now, provenTxId: 0, txid, height, index: leaf.offset, merklePath: merklePath.toBinary(), rawTx: req.rawTx, merkleRoot, blockHash }); // Persist via the same path as TaskCheckForProofs await req.refreshFromStorage(this.storage); const { provenTxReqId, status, attempts, history } = req.toApi(); const r = await this.storage.runAsStorageProvider(async (sp) => { return await sp.updateProvenTxReqWithNewProvenTx({ provenTxReqId, status, txid, attempts, history, index: leaf.offset, height, blockHash, merklePath: merklePath.toBinary(), merkleRoot }); }); req.status = r.status; req.apiHistory = r.history; req.provenTxId = r.provenTxId; req.notified = true; this.monitor.callOnProvenTransaction({ txid, txIndex: leaf.offset, blockHeight: height, blockHash, merklePath: merklePath.toBinary(), merkleRoot }); log += ` proved at height ${height}, index ${leaf.offset} => ${r.status}\n`; } catch (err) { const msg = err instanceof Error ? err.message : String(err); log += ` error fetching proof: ${msg}\n`; req.addHistoryNote({ when: new Date().toISOString(), what: 'arcProofError', error: msg }); await req.updateStorageDynamicProperties(this.storage); } return log; } } exports.TaskArcadeSSE = TaskArcadeSSE; TaskArcadeSSE.taskName = 'ArcadeSSE'; //# sourceMappingURL=TaskArcSSE.js.map