UNPKG

@bsv/wallet-toolbox-client

Version:
270 lines 10.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ARC = void 0; const sdk_1 = require("@bsv/sdk"); const utilityHelpers_1 = require("../../utility/utilityHelpers"); const WalletError_1 = require("../../sdk/WalletError"); function defaultDeploymentId() { return `ts-sdk-${sdk_1.Utils.toHex((0, sdk_1.Random)(16))}`; } /** * Represents an ARC transaction broadcaster. */ class ARC { constructor(URL, config, name) { this.name = name !== null && name !== void 0 ? name : 'ARC'; this.URL = URL; if (typeof config === 'string') { this.apiKey = config; this.httpClient = (0, sdk_1.defaultHttpClient)(); this.deploymentId = defaultDeploymentId(); this.callbackToken = undefined; this.callbackUrl = undefined; } else { const configObj = config !== null && config !== void 0 ? config : {}; const { apiKey, deploymentId, httpClient, callbackToken, callbackUrl, headers } = configObj; this.apiKey = apiKey; this.httpClient = httpClient !== null && httpClient !== void 0 ? httpClient : (0, sdk_1.defaultHttpClient)(); this.deploymentId = deploymentId !== null && deploymentId !== void 0 ? deploymentId : defaultDeploymentId(); this.callbackToken = callbackToken; this.callbackUrl = callbackUrl; this.headers = headers; } } /** * Constructs a dictionary of the default & supplied request headers. */ requestHeaders() { const headers = { 'Content-Type': 'application/json', 'XDeployment-ID': this.deploymentId }; if (this.apiKey != null && this.apiKey !== '') { headers.Authorization = `Bearer ${this.apiKey}`; } if (this.callbackUrl != null && this.callbackUrl !== '') { headers['X-CallbackUrl'] = this.callbackUrl; } if (this.callbackToken != null && this.callbackToken !== '') { headers['X-CallbackToken'] = this.callbackToken; } if (this.headers != null) { for (const key in this.headers) { headers[key] = this.headers[key]; } } return headers; } /** * The ARC '/v1/tx' endpoint, as of 2025-02-17 supports all of the following hex string formats: * 1. Single serialized raw transaction. * 2. Single EF serialized raw transaction (untested). * 3. V1 serialized Beef (results returned reflect only the last transaction in the beef) * * The ARC '/v1/tx' endpoint, as of 2025-02-17 DOES NOT support the following hex string formats: * 1. V2 serialized Beef * * @param rawTx * @param txids * @returns */ async postRawTx(rawTx, txids) { let txid = sdk_1.Utils.toHex((0, utilityHelpers_1.doubleSha256BE)(sdk_1.Utils.toArray(rawTx, 'hex'))); if (txids) { txid = txids.slice(-1)[0]; } else { txids = [txid]; } const requestOptions = { method: 'POST', headers: this.requestHeaders(), data: { rawTx }, signal: AbortSignal.timeout(1000 * 30) // 30 seconds timeout, error.code will be 'ABORT_ERR' }; const r = { txid, status: 'success', notes: [] }; const url = `${this.URL}/v1/tx`; const nn = () => ({ name: this.name, when: new Date().toISOString() }); const nne = () => ({ ...nn(), rawTx, txids: txids.join(','), url }); try { const response = await this.httpClient.request(url, requestOptions); const { txid, extraInfo, txStatus, competingTxs } = response.data; const nnr = () => ({ txid, extraInfo, txStatus, competingTxs: competingTxs === null || competingTxs === void 0 ? void 0 : competingTxs.join(',') }); if (response.ok) { r.data = `${txStatus} ${extraInfo}`; if (r.txid !== txid) r.data += ` txid altered from ${r.txid} to ${txid}`; r.txid = txid; if (txStatus === 'DOUBLE_SPEND_ATTEMPTED' || txStatus === 'SEEN_IN_ORPHAN_MEMPOOL') { r.status = 'error'; r.doubleSpend = true; r.competingTxs = competingTxs; r.notes.push({ ...nne(), ...nnr(), what: 'postRawTxDoubleSpend' }); } else { r.notes.push({ ...nn(), ...nnr(), what: 'postRawTxSuccess' }); } } else if (typeof response === 'string') { r.notes.push({ ...nne(), what: 'postRawTxString', response }); r.status = 'error'; // response is not normally a string r.serviceError = true; } else { r.status = 'error'; // Treat unknown errors as service errors r.serviceError = true; const n = { ...nn(), ...nne(), ...nnr(), what: 'postRawTxError' }; const ed = {}; r.data = ed; const st = typeof response.status; if (st === 'number' || st === 'string') { n.status = response.status; ed.status = response.status.toString(); } else { n.status = st; ed.status = 'ERR_UNKNOWN'; } let d = response.data; if (d && typeof d === 'string') { n.data = response.data.slice(0, 128); try { d = JSON.parse(d); } catch (_a) { // Intentionally left empty } } else if (d && typeof d === 'object') { ed.more = d; ed.detail = d['detail']; if (typeof ed.detail !== 'string') ed.detail = undefined; if (ed.detail) { n.detail = ed.detail; } } r.notes.push(n); } } catch (eu) { const e = WalletError_1.WalletError.fromUnknown(eu); r.status = 'error'; r.serviceError = true; r.data = `${e.code} ${e.message}`; r.notes.push({ ...nne(), what: 'postRawTxCatch', code: e.code, description: e.description }); } return r; } /** * ARC does not natively support a postBeef end-point aware of multiple txids of interest in the Beef. * * It does process multiple new transactions, however, which allows results for all txids of interest * to be collected by the `/v1/tx/${txid}` endpoint. * * @param beef * @param txids * @returns */ async postBeef(beef, txids) { const r = { name: this.name, status: 'success', txidResults: [], notes: [] }; const nn = () => ({ name: this.name, when: new Date().toISOString() }); if (beef.version === sdk_1.BEEF_V2 && beef.txs.every(btx => !btx.isTxidOnly)) { beef.version = sdk_1.BEEF_V1; r.notes.push({ ...nn(), what: 'postBeefV2ToV1' }); } const beefHex = beef.toHex(); const prtr = await this.postRawTx(beefHex, txids); r.status = prtr.status; r.txidResults = [prtr]; // Since postRawTx only returns results for a single txid, // replicate the basic results any additional txids. // TODO: Temporary hack... for (const txid of txids) { if (prtr.txid === txid) continue; const tr = { txid, status: 'success', notes: [] }; // For the extra txids, go back to the service for confirmation... const dr = await this.getTxData(txid); if (dr.txid !== txid) { tr.status = 'error'; tr.data = 'internal error'; tr.notes.push({ ...nn(), what: 'postBeefGetTxDataInternal', txid, returnedTxid: dr.txid }); } else if (dr.txStatus === 'SEEN_ON_NETWORK' || dr.txStatus === 'STORED') { tr.data = dr.txStatus; tr.notes.push({ ...nn(), what: 'postBeefGetTxDataSuccess', txid, txStatus: dr.txStatus }); } else { tr.status = 'error'; tr.data = dr; tr.notes.push({ ...nn(), what: 'postBeefGetTxDataError', txid, txStatus: dr.txStatus }); } r.txidResults.push(tr); if (r.status === 'success' && tr.status === 'error') r.status = 'error'; } return r; } /** * This seems to only work for recently submitted txids...but that's all we need to complete postBeef! * @param txid * @returns */ async getTxData(txid) { const requestOptions = { method: 'GET', headers: this.requestHeaders() }; const response = await this.httpClient.request(`${this.URL}/v1/tx/${txid}`, requestOptions); return response.data; } } exports.ARC = ARC; //# sourceMappingURL=ARC.js.map