UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

254 lines (225 loc) 8.4 kB
import { Beef, defaultHttpClient, HexString, HttpClient, Utils } from '@bsv/sdk' import { convertProofToMerklePath, doubleSha256BE, sdk, wait } from '../../index.client' import { ReqHistoryNote } from '../../sdk' export interface BitailsConfig { /** Authentication token for BitTails API */ apiKey?: string /** The HTTP client used to make requests to the API. */ httpClient?: HttpClient } /** * */ export class Bitails { readonly chain: sdk.Chain readonly apiKey: string readonly URL: string readonly httpClient: HttpClient constructor(chain: sdk.Chain = 'main', config: BitailsConfig = {}) { const { apiKey, httpClient } = config this.chain = chain this.URL = chain === 'main' ? `https://api.bitails.io/` : `https://test-api.bitails.io/` this.httpClient = httpClient ?? defaultHttpClient() this.apiKey = apiKey ?? '' } getHttpHeaders(): Record<string, string> { const headers: Record<string, string> = { Accept: 'application/json' } if (typeof this.apiKey === 'string' && this.apiKey.trim() !== '') { headers.Authorization = this.apiKey } return headers } /** * Bitails does not natively support a postBeef end-point aware of multiple txids of interest in the Beef. * * Send rawTx in `txids` order from beef. * * @param beef * @param txids * @returns */ async postBeef(beef: Beef, txids: string[]): Promise<sdk.PostBeefResult> { const nn = () => ({ name: 'BitailsPostBeef', when: new Date().toISOString() }) const nne = () => ({ ...nn(), beef: beef.toHex(), txids: txids.join(',') }) const note: ReqHistoryNote = { ...nn(), what: 'postBeef' } const raws: string[] = [] for (const txid of txids) { const rawTx = Utils.toHex(beef.findTxid(txid)!.rawTx!) raws.push(rawTx) } const r = await this.postRaws(raws, txids) r.notes!.unshift(note) if (r.status !== 'success') r.notes!.push({ ...nne(), what: 'postBeefError' }) else r.notes!.push({ ...nn(), what: 'postBeefSuccess' }) return r } /** * @param raws Array of raw transactions to broadcast as hex strings * @param txids Array of txids for transactions in raws for which results are requested, remaining raws are supporting only. * @returns */ async postRaws(raws: HexString[], txids?: string[]): Promise<sdk.PostBeefResult> { const r: sdk.PostBeefResult = { name: 'BitailsPostRaws', status: 'success', txidResults: [], notes: [] } const rawTxids: string[] = [] for (const raw of raws) { const txid = Utils.toHex(doubleSha256BE(Utils.toArray(raw, 'hex'))) // Results aren't always identified by txid. rawTxids.push(txid) if (!txids || txids.indexOf(txid) >= 0) { r.txidResults.push({ txid, status: 'success', notes: [] }) } } const headers = this.getHttpHeaders() headers['Content-Type'] = 'application/json' //headers['Accept'] = 'text/json' const data = { raws: raws } const requestOptions = { method: 'POST', headers, data } const url = `${this.URL}tx/broadcast/multi` const nn = () => ({ name: 'BitailsPostRawTx', when: new Date().toISOString() }) const nne = () => ({ ...nn(), raws: raws.join(','), txids: r.txidResults.map(r => r.txid).join(','), url }) try { const response = await this.httpClient.request<BitailsPostRawsResult[]>(url, requestOptions) if (response.ok) { // status: 201, statusText: 'Created' const btrs: BitailsPostRawsResult[] = response.data if (btrs.length !== raws.length) { r.status = 'error' r.notes!.push({ ...nne(), what: 'postRawsErrorResultsCount' }) } else { // Check that each response result has a txid that matches corresponding rawTxids let i = -1 for (const btr of btrs) { i++ if (!btr.txid) { btr.txid = rawTxids[i] r.notes!.push({ ...nn(), what: 'postRawsResultMissingTxids', i, rawsTxid: rawTxids[i] }) } else if (btr.txid !== rawTxids[i]) { r.status = 'error' r.notes!.push({ ...nn(), what: 'postRawsResultTxids', i, txid: btr.txid, rawsTxid: rawTxids[i] }) } } if (r.status === 'success') { // btrs has correct number of results and each one has expected txid. // focus on results for requested txids for (const rt of r.txidResults) { const btr = btrs.find(btr => btr.txid! === rt.txid)! const txid = rt.txid if (btr.error) { // code: -25, message: 'missing-inputs' // code: -27, message: 'already-in-mempool' const { code, message } = btr.error if (code === -27) { rt.notes!.push({ ...nne(), what: 'postRawsSuccessAlreadyInMempool' }) } else { rt.status = 'error' if (code === -25) { rt.doubleSpend = true // this is a possible double spend attempt rt.competingTxs = undefined // not provided with any data for this. rt.notes!.push({ ...nne(), what: 'postRawsErrorMissingInputs' }) } else if ((btr['code'] as string) === 'ECONNRESET') { rt.notes!.push({ ...nne(), what: 'postRawsErrorECONNRESET', txid, message }) } else { rt.notes!.push({ ...nne(), what: 'postRawsError', txid, code, message }) } } } else { rt.notes!.push({ ...nn(), what: 'postRawsSuccess' }) } if (rt.status !== 'success' && r.status === 'success') r.status = 'error' } } } } else { r.status = 'error' const n: ReqHistoryNote = { ...nne(), what: 'postRawsError' } r.notes!.push(n) } } catch (eu: unknown) { r.status = 'error' const e = sdk.WalletError.fromUnknown(eu) const { code, description } = e r.notes!.push({ ...nne(), what: 'postRawsCatch', code, description }) } return r } /** * * @param txid * @param services * @returns */ async getMerklePath(txid: string, services: sdk.WalletServices): Promise<sdk.GetMerklePathResult> { const r: sdk.GetMerklePathResult = { name: 'BitailsTsc', notes: [] } const url = `${this.URL}tx/${txid}/proof/tsc` const nn = () => ({ name: 'BitailsProofTsc', when: new Date().toISOString(), txid, url }) const headers = this.getHttpHeaders() const requestOptions = { method: 'GET', headers } try { const response = await this.httpClient.request<BitailsMerkleProof>(url, requestOptions) const nne = () => ({ ...nn(), txid, url, status: response.status, statusText: response.statusText }) if (response.status === 404 && response.statusText === 'Not Found') { r.notes!.push({ ...nn(), what: 'getMerklePathNotFound' }) } else if (!response.ok || response.status !== 200 || response.statusText !== 'OK') { r.notes!.push({ ...nne(), what: 'getMerklePathBadStatus' }) } else if (!response.data) { r.notes!.push({ ...nne(), what: 'getMerklePathNoData' }) } else { const p = response.data const header = await services.hashToHeader(p.target) if (header) { const proof = { index: p.index, nodes: p.nodes, height: header.height } r.merklePath = convertProofToMerklePath(txid, proof) r.header = header r.notes!.push({ ...nne(), what: 'getMerklePathSuccess' }) } else { r.notes!.push({ ...nne(), what: 'getMerklePathNoHeader', target: p.target }) } } } catch (eu: unknown) { const e = sdk.WalletError.fromUnknown(eu) const { code, description } = e r.notes!.push({ ...nn(), what: 'getMerklePathCatch', code, description }) r.error = e } return r } } interface BitailsPostRawsResult { txid?: string error?: { code: number message: string } } export interface BitailsMerkleProof { index: number txOrId: string target: string nodes: string[] }