UNPKG

@bsv/overlay-express

Version:
196 lines 8.65 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ArcadeProvider = void 0; exports.isTerminalArcStatus = isTerminalArcStatus; const sdk_1 = require("@bsv/sdk"); const TERMINAL_STATUSES = new Set([ 'DOUBLE_SPEND_ATTEMPTED', 'REJECTED', 'INVALID', 'MALFORMED', 'MINED_IN_STALE_BLOCK' ]); const SUCCESS_STATUSES = new Set([ 'RECEIVED', 'SENT_TO_NETWORK', 'ANNOUNCED_TO_NETWORK', 'ACCEPTED_BY_NETWORK', 'SEEN_ON_NETWORK', 'STORED', 'MINED', 'IMMUTABLE' ]); function isTerminalArcStatus(status, extraInfo) { const statusText = typeof status === 'string' ? status.toUpperCase() : ''; const extraText = typeof extraInfo === 'string' ? extraInfo.toUpperCase() : ''; return TERMINAL_STATUSES.has(statusText) || statusText.includes('ORPHAN') || extraText.includes('ORPHAN'); } function trimBaseUrl(url) { let base = url; while (base.endsWith('/')) base = base.slice(0, -1); return base; } function parseDescription(data, fallback) { var _a, _b, _c, _d, _e, _f; if (typeof data === 'string') return data; if (typeof data === 'object' && data !== null) { const d = data; return (_f = (_c = (_b = (_a = d.detail) !== null && _a !== void 0 ? _a : d.reason) !== null && _b !== void 0 ? _b : d.error) !== null && _c !== void 0 ? _c : `${(_d = d.txStatus) !== null && _d !== void 0 ? _d : ''} ${(_e = d.extraInfo) !== null && _e !== void 0 ? _e : ''}`.trim()) !== null && _f !== void 0 ? _f : fallback; } return fallback; } function rawTxForArcade(tx) { try { return tx.toHexEF(); } catch (error) { if (error instanceof Error && error.message === 'All inputs must have source transactions when serializing to EF format') { return tx.toHex(); } throw error; } } /** * Broadcaster/proof client for bsv-blockchain/arcade. * * Arcade exposes transaction propagation at `/tx` and status/proofs at * `/tx/{txid}`. It returns terminal validation and double-spend statuses in the * body, so callers must classify those as failed broadcasts even when HTTP * accepted the request. */ class ArcadeProvider { constructor(url, config = {}) { var _a; this.config = config; this.name = 'Arcade'; this.baseUrl = trimBaseUrl(url); this.fetcher = (_a = config.fetch) !== null && _a !== void 0 ? _a : fetch; } async broadcast(tx) { var _a, _b, _c, _d; const rawTx = rawTxForArcade(tx); try { const response = await this.fetcher(`${this.baseUrl}/tx`, { method: 'POST', headers: this.requestHeaders(), body: JSON.stringify({ rawTx }) }); const data = await this.readResponse(response); if (!response.ok) { const failure = { status: 'error', code: (_a = data === null || data === void 0 ? void 0 : data.txStatus) !== null && _a !== void 0 ? _a : String(response.status), txid: data === null || data === void 0 ? void 0 : data.txid, description: parseDescription(data, response.statusText) }; const more = { provider: this.name, httpStatus: response.status, terminal: response.status === 400 || isTerminalArcStatus(data === null || data === void 0 ? void 0 : data.txStatus, data === null || data === void 0 ? void 0 : data.extraInfo), response: data }; if ((data === null || data === void 0 ? void 0 : data.competingTxs) !== undefined) { more.competingTxs = data.competingTxs; } failure.more = more; return failure; } const txStatus = data === null || data === void 0 ? void 0 : data.txStatus; if (isTerminalArcStatus(txStatus, data === null || data === void 0 ? void 0 : data.extraInfo)) { const failure = { status: 'error', code: txStatus !== null && txStatus !== void 0 ? txStatus : 'UNKNOWN', txid: data === null || data === void 0 ? void 0 : data.txid, description: `${txStatus !== null && txStatus !== void 0 ? txStatus : ''} ${(_b = data === null || data === void 0 ? void 0 : data.extraInfo) !== null && _b !== void 0 ? _b : ''}`.trim() }; const more = { provider: this.name, terminal: true, response: data }; if ((data === null || data === void 0 ? void 0 : data.competingTxs) !== undefined) { more.competingTxs = data.competingTxs; } failure.more = more; return failure; } const statusText = typeof txStatus === 'string' ? txStatus.toUpperCase() : undefined; return { status: 'success', txid: (_c = data === null || data === void 0 ? void 0 : data.txid) !== null && _c !== void 0 ? _c : tx.id('hex'), message: `${txStatus !== null && txStatus !== void 0 ? txStatus : (SUCCESS_STATUSES.has(statusText !== null && statusText !== void 0 ? statusText : '') ? statusText !== null && statusText !== void 0 ? statusText : '' : response.statusText)} ${(_d = data === null || data === void 0 ? void 0 : data.extraInfo) !== null && _d !== void 0 ? _d : ''}`.trim(), competingTxs: data === null || data === void 0 ? void 0 : data.competingTxs }; } catch (error) { return { status: 'error', code: '500', description: error instanceof Error ? error.message : 'Internal Server Error', more: { provider: this.name, terminal: false } }; } } async fetchMerkleProof(txid) { var _a; const response = await this.fetcher(`${this.baseUrl}/tx/${txid}`, { method: 'GET', headers: this.requestHeaders({ accept: 'application/json' }) }); if (response.status === 404) return undefined; const data = await this.readResponse(response); if (!response.ok) { throw new Error(`Arcade proof lookup failed for ${txid}: ${response.status} ${parseDescription(data, response.statusText)}`); } const mined = (data === null || data === void 0 ? void 0 : data.txStatus) === 'MINED' || (data === null || data === void 0 ? void 0 : data.txStatus) === 'IMMUTABLE'; if (!mined || (data === null || data === void 0 ? void 0 : data.merklePath) === undefined || data.merklePath === '') { return undefined; } const merklePath = sdk_1.MerklePath.fromHex(data.merklePath); return { txid, merklePath, blockHeight: (_a = data.blockHeight) !== null && _a !== void 0 ? _a : merklePath.blockHeight, blockHash: data.blockHash, merkleRoot: merklePath.computeRoot(txid) }; } requestHeaders(options = {}) { var _a; const headers = { Accept: (_a = options.accept) !== null && _a !== void 0 ? _a : 'application/json', 'Content-Type': 'application/json' }; if (this.config.deploymentId !== undefined && this.config.deploymentId !== '') { headers['XDeployment-ID'] = this.config.deploymentId; } if (this.config.apiKey !== undefined && this.config.apiKey !== '') { headers.Authorization = `Bearer ${this.config.apiKey}`; } if (this.config.callbackUrl !== undefined && this.config.callbackUrl !== '') { headers['X-CallbackUrl'] = this.config.callbackUrl; } if (this.config.callbackToken !== undefined && this.config.callbackToken !== '') { headers['X-CallbackToken'] = this.config.callbackToken; } if (this.config.headers !== undefined) { for (const [key, value] of Object.entries(this.config.headers)) { headers[key] = value; } } return headers; } async readResponse(response) { const text = await response.text(); if (text === '') return undefined; try { return JSON.parse(text); } catch { return text; } } } exports.ArcadeProvider = ArcadeProvider; //# sourceMappingURL=ArcadeProvider.js.map