@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
260 lines • 11.3 kB
JavaScript
;
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