UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

660 lines 28.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Services = void 0; exports.validateScriptHash = validateScriptHash; exports.toBinaryBaseBlockHeader = toBinaryBaseBlockHeader; const sdk_1 = require("@bsv/sdk"); const ServiceCollection_1 = require("./ServiceCollection"); const createDefaultWalletServicesOptions_1 = require("./createDefaultWalletServicesOptions"); const WhatsOnChain_1 = require("./providers/WhatsOnChain"); const exchangeRates_1 = require("./providers/exchangeRates"); const ARC_1 = require("./providers/ARC"); const Bitails_1 = require("./providers/Bitails"); const getBeefForTxid_1 = require("./providers/getBeefForTxid"); const WERR_errors_1 = require("../sdk/WERR_errors"); const ChaintracksChainTracker_1 = require("./chaintracker/ChaintracksChainTracker"); const WalletError_1 = require("../sdk/WalletError"); const utilityHelpers_1 = require("../utility/utilityHelpers"); const utilityHelpers_noBuffer_1 = require("../utility/utilityHelpers.noBuffer"); class Services { static createDefaultOptions(chain) { return (0, createDefaultWalletServicesOptions_1.createDefaultWalletServicesOptions)(chain); } constructor(optionsOrChain) { this.postBeefMode = 'UntilSuccess'; /** * Soft timeout used for each provider call in `UntilSuccess` mode. * This bounds request latency when a provider hangs before failover. */ this.postBeefUntilSuccessSoftTimeoutMs = 5000; /** * Additional soft-timeout budget (ms) per KiB of serialized Beef payload. * Helps avoid false timeout failover on legitimately large submissions. */ this.postBeefUntilSuccessSoftTimeoutPerKbMs = 50; /** * Upper bound for adaptive soft-timeout in `UntilSuccess` mode. */ this.postBeefUntilSuccessSoftTimeoutMaxMs = 30000; this.chain = typeof optionsOrChain === 'string' ? optionsOrChain : optionsOrChain.chain; if (this.chain === 'mock') { throw new WERR_errors_1.WERR_INVALID_PARAMETER('chain', `'main', 'test', or 'teratest'. Use MockServices for 'mock' chain.`); } this.options = typeof optionsOrChain === 'string' ? Services.createDefaultOptions(this.chain) : optionsOrChain; this.whatsonchain = new WhatsOnChain_1.WhatsOnChain(this.chain, { apiKey: this.options.whatsOnChainApiKey }, this); this.arcTaal = new ARC_1.ARC(this.options.arcUrl, this.options.arcConfig, 'arcTaal'); if (this.options.arcGorillaPoolUrl) { this.arcGorillaPool = new ARC_1.ARC(this.options.arcGorillaPoolUrl, this.options.arcGorillaPoolConfig, 'arcGorillaPool'); } this.bitails = new Bitails_1.Bitails(this.chain, { apiKey: this.options.bitailsApiKey }); const hasBitails = this.chain === 'main' || this.chain === 'test'; //prettier-ignore this.getMerklePathServices = new ServiceCollection_1.ServiceCollection('getMerklePath') .add({ name: 'WhatsOnChain', service: this.whatsonchain.getMerklePath.bind(this.whatsonchain) }); if (hasBitails) { this.getMerklePathServices.add({ name: 'Bitails', service: this.bitails.getMerklePath.bind(this.bitails) }); } //prettier-ignore this.getRawTxServices = new ServiceCollection_1.ServiceCollection('getRawTx') .add({ name: 'WhatsOnChain', service: this.whatsonchain.getRawTxResult.bind(this.whatsonchain) }); this.postBeefServices = new ServiceCollection_1.ServiceCollection('postBeef'); if (this.arcGorillaPool) { //prettier-ignore this.postBeefServices.add({ name: 'GorillaPoolArcBeef', service: this.arcGorillaPool.postBeef.bind(this.arcGorillaPool) }); } //prettier-ignore this.postBeefServices .add({ name: 'TaalArcBeef', service: this.arcTaal.postBeef.bind(this.arcTaal) }); if (hasBitails) { this.postBeefServices.add({ name: 'Bitails', service: this.bitails.postBeef.bind(this.bitails) }); } //prettier-ignore this.postBeefServices .add({ name: 'WhatsOnChain', service: this.whatsonchain.postBeef.bind(this.whatsonchain) }); //prettier-ignore this.getUtxoStatusServices = new ServiceCollection_1.ServiceCollection('getUtxoStatus') .add({ name: 'WhatsOnChain', service: this.whatsonchain.getUtxoStatus.bind(this.whatsonchain) }); //prettier-ignore this.getStatusForTxidsServices = new ServiceCollection_1.ServiceCollection('getStatusForTxids') .add({ name: 'WhatsOnChain', service: this.whatsonchain.getStatusForTxids.bind(this.whatsonchain) }); //prettier-ignore this.getScriptHashHistoryServices = new ServiceCollection_1.ServiceCollection('getScriptHashHistory') .add({ name: 'WhatsOnChain', service: this.whatsonchain.getScriptHashHistory.bind(this.whatsonchain) }); //prettier-ignore this.updateFiatExchangeRateServices = new ServiceCollection_1.ServiceCollection('updateFiatExchangeRate') .add({ name: 'exchangeratesapi', service: exchangeRates_1.updateExchangeratesapi }); } getServicesCallHistory(reset) { return { version: 2, getMerklePath: this.getMerklePathServices.getServiceCallHistory(reset), getRawTx: this.getRawTxServices.getServiceCallHistory(reset), postBeef: this.postBeefServices.getServiceCallHistory(reset), getUtxoStatus: this.getUtxoStatusServices.getServiceCallHistory(reset), getStatusForTxids: this.getStatusForTxidsServices.getServiceCallHistory(reset), getScriptHashHistory: this.getScriptHashHistoryServices.getServiceCallHistory(reset), updateFiatExchangeRates: this.updateFiatExchangeRateServices.getServiceCallHistory(reset) }; } async getChainTracker() { if (!this.options.chaintracks) throw new WERR_errors_1.WERR_INVALID_PARAMETER('options.chaintracks', `valid to enable 'getChainTracker' service.`); return new ChaintracksChainTracker_1.ChaintracksChainTracker(this.chain, this.options.chaintracks); } async getBsvExchangeRate() { this.options.bsvExchangeRate = await this.whatsonchain.updateBsvExchangeRate(this.options.bsvExchangeRate, this.options.bsvUpdateMsecs); return this.options.bsvExchangeRate.rate; } async getFiatExchangeRate(currency, base) { var _a, _b; base || (base = 'USD'); if (currency === base) return 1; const required = base === 'USD' ? [currency] : [currency, base]; await this.updateFiatExchangeRates(required, this.options.fiatUpdateMsecs); const rates = this.options.fiatExchangeRates; const c = (_a = rates.rates) === null || _a === void 0 ? void 0 : _a[currency]; const b = (_b = rates.rates) === null || _b === void 0 ? void 0 : _b[base]; if (typeof c !== 'number') { throw new WERR_errors_1.WERR_INVALID_PARAMETER('currency', `valid fiat currency '${currency}' with an exchange rate.`); } if (typeof b !== 'number') { throw new WERR_errors_1.WERR_INVALID_PARAMETER('base', `valid fiat currency '${base}' with an exchange rate.`); } return c / b; } async getFiatExchangeRates(targetCurrencies) { var _a; await this.updateFiatExchangeRates(targetCurrencies, this.options.fiatUpdateMsecs); const stored = this.options.fiatExchangeRates; const rates = {}; for (const c of targetCurrencies) { const v = (_a = stored.rates) === null || _a === void 0 ? void 0 : _a[c]; if (typeof v === 'number') { rates[c] = v; } } return { timestamp: stored.timestamp, base: 'USD', rates, rateTimestamps: stored.rateTimestamps }; } get getProofsCount() { return this.getMerklePathServices.count; } get getRawTxsCount() { return this.getRawTxServices.count; } get postBeefServicesCount() { return this.postBeefServices.count; } get getUtxoStatsCount() { return this.getUtxoStatusServices.count; } async getStatusForTxids(txids, useNext) { const services = this.getStatusForTxidsServices; if (useNext) services.next(); let r0 = { name: '<noservices>', status: 'error', error: new WERR_errors_1.WERR_INTERNAL('No services available.'), results: [] }; for (let tries = 0; tries < services.count; tries++) { const stc = services.serviceToCall; try { const r = await stc.service(txids); if (r.status === 'success') { services.addServiceCallSuccess(stc); r0 = r; break; } else { if (r.error) services.addServiceCallError(stc, r.error); else services.addServiceCallFailure(stc); } } catch (eu) { const e = WalletError_1.WalletError.fromUnknown(eu); services.addServiceCallError(stc, e); } services.next(); } return r0; } /** * @param script Output script to be hashed for `getUtxoStatus` default `outputFormat` * @returns script hash in 'hashLE' format, which is the default. */ hashOutputScript(script) { const hash = sdk_1.Utils.toHex((0, utilityHelpers_1.sha256Hash)(sdk_1.Utils.toArray(script, 'hex'))); return hash; } async isUtxo(output) { if (!output.lockingScript) { throw new WERR_errors_1.WERR_INVALID_PARAMETER('output.lockingScript', 'validated by storage provider validateOutputScript.'); } const hash = this.hashOutputScript(sdk_1.Utils.toHex(output.lockingScript)); const or = await this.getUtxoStatus(hash, undefined, `${output.txid}.${output.vout}`); return or.isUtxo === true; } async getUtxoStatus(output, outputFormat, outpoint, useNext, logger) { const services = this.getUtxoStatusServices; if (useNext) services.next(); let r0 = { name: '<noservices>', status: 'error', error: new WERR_errors_1.WERR_INTERNAL('No services available.'), details: [] }; logger === null || logger === void 0 ? void 0 : logger.group(`services getUtxoStatus`); for (let retry = 0; retry < 2; retry++) { for (let tries = 0; tries < services.count; tries++) { const stc = services.serviceToCall; try { const r = await stc.service(output, outputFormat, outpoint); logger === null || logger === void 0 ? void 0 : logger.log(`${stc.providerName} status ${r.status}`); if (r.status === 'success') { services.addServiceCallSuccess(stc); r0 = r; break; } else { if (r.error) services.addServiceCallError(stc, r.error); else services.addServiceCallFailure(stc); } } catch (eu) { const e = WalletError_1.WalletError.fromUnknown(eu); services.addServiceCallError(stc, e); } services.next(); } if (r0.status === 'success') break; await (0, utilityHelpers_1.wait)(2000); } logger === null || logger === void 0 ? void 0 : logger.groupEnd(); return r0; } async getScriptHashHistory(hash, useNext, logger) { const services = this.getScriptHashHistoryServices; if (useNext) services.next(); let r0 = { name: '<noservices>', status: 'error', error: new WERR_errors_1.WERR_INTERNAL('No services available.'), history: [] }; logger === null || logger === void 0 ? void 0 : logger.group(`services getScriptHashHistory`); for (let tries = 0; tries < services.count; tries++) { const stc = services.serviceToCall; try { const r = await stc.service(hash); logger === null || logger === void 0 ? void 0 : logger.log(`${stc.providerName} status ${r.status}`); if (r.status === 'success') { r0 = r; break; } else { if (r.error) services.addServiceCallError(stc, r.error); else services.addServiceCallFailure(stc); } } catch (eu) { const e = WalletError_1.WalletError.fromUnknown(eu); services.addServiceCallError(stc, e); } services.next(); } logger === null || logger === void 0 ? void 0 : logger.groupEnd(); return r0; } /** * * @param beef * @param chain * @returns */ async postBeef(beef, txids, logger) { var _a; let rs = []; const services = this.postBeefServices; const stcs = services.allServicesToCall; const softTimeoutMs = this.getPostBeefSoftTimeoutMs(beef); logger === null || logger === void 0 ? void 0 : logger.group(`services postBeef`); switch (this.postBeefMode) { case 'UntilSuccess': { for (const stc of stcs) { const r = await callService(stc, softTimeoutMs); logger === null || logger === void 0 ? void 0 : logger.log(`${stc.providerName} status ${r.status}`); rs.push(r); if (r.status === 'success') break; const softTimedOut = ((_a = r.notes) === null || _a === void 0 ? void 0 : _a.some(n => n.what === 'postBeefServiceTimeout')) === true; if (!softTimedOut && r.txidResults && r.txidResults.every(txr => txr.serviceError)) { // move this service to the end of the list this.postBeefServices.moveServiceToLast(stc); } } } break; case 'PromiseAll': { rs = await Promise.all(stcs.map(async (stc) => { const r = await callService(stc); return r; })); } break; } logger === null || logger === void 0 ? void 0 : logger.groupEnd(); return rs; async function callService(stc, timeoutMs) { const callPromise = stc.service(beef, txids); let r; if (!timeoutMs || timeoutMs <= 0) { r = await callPromise; } else { let timeoutHandle; const timeoutPromise = new Promise(resolve => { timeoutHandle = setTimeout(() => resolve(makeServiceTimeoutResult(stc.providerName, txids, timeoutMs)), timeoutMs); }); r = await Promise.race([callPromise, timeoutPromise]); if (timeoutHandle) clearTimeout(timeoutHandle); // Avoid unhandled rejection after timeout race wins. void callPromise.catch(() => undefined); } if (r.status === 'success') { services.addServiceCallSuccess(stc); } else { if (r.error) { services.addServiceCallError(stc, r.error); } else { services.addServiceCallFailure(stc); } } return r; } function makeServiceTimeoutResult(providerName, txids, timeoutMs) { return { name: providerName, status: 'error', txidResults: txids.map(txid => ({ txid, status: 'error', serviceError: true, data: { detail: `timeout after ${timeoutMs}ms` } })), notes: [{ when: new Date().toISOString(), what: 'postBeefServiceTimeout', providerName, timeoutMs }] }; } } getPostBeefSoftTimeoutMs(beef) { const baseMs = Math.max(0, this.postBeefUntilSuccessSoftTimeoutMs); const perKbMs = Math.max(0, this.postBeefUntilSuccessSoftTimeoutPerKbMs); const maxMs = Math.max(baseMs, this.postBeefUntilSuccessSoftTimeoutMaxMs); if (perKbMs <= 0) return Math.min(baseMs, maxMs); const beefBytes = beef.toBinary().length; const extraMs = Math.ceil((beefBytes / 1024) * perKbMs); return Math.min(maxMs, baseMs + extraMs); } async getRawTx(txid, useNext) { const services = this.getRawTxServices; if (useNext) services.next(); const r0 = { txid }; for (let tries = 0; tries < services.count; tries++) { const stc = services.serviceToCall; try { const r = await stc.service(txid, this.chain); if (r.rawTx) { const hash = (0, utilityHelpers_noBuffer_1.asString)((0, utilityHelpers_1.doubleSha256BE)(r.rawTx)); // Confirm transaction hash matches txid if (hash === (0, utilityHelpers_noBuffer_1.asString)(txid)) { // If we have a match, call it done. r0.rawTx = r.rawTx; r0.name = r.name; r0.error = undefined; services.addServiceCallSuccess(stc); break; } r.error = new WERR_errors_1.WERR_INTERNAL(`computed txid ${hash} doesn't match requested value ${txid}`); r.rawTx = undefined; } if (r.error) services.addServiceCallError(stc, r.error); else if (!r.rawTx) services.addServiceCallSuccess(stc, `not found`); else services.addServiceCallFailure(stc); if (r.error && !r0.error && !r0.rawTx) // If we have an error and didn't before... r0.error = r.error; } catch (eu) { const e = WalletError_1.WalletError.fromUnknown(eu); services.addServiceCallError(stc, e); } services.next(); } return r0; } async invokeChaintracksWithRetry(method) { if (!this.options.chaintracks) throw new WERR_errors_1.WERR_INVALID_PARAMETER('options.chaintracks', 'valid for this service operation.'); for (let retry = 0; retry < 3; retry++) { try { const r = await method(); return r; } catch (eu) { const e = WalletError_1.WalletError.fromUnknown(eu); if (e.code != 'ECONNRESET') throw eu; } } throw new WERR_errors_1.WERR_INVALID_OPERATION('hashToHeader service unavailable'); } async getHeaderForHeight(height) { const method = async () => { const header = await this.options.chaintracks.findHeaderForHeight(height); if (!header) throw new WERR_errors_1.WERR_INVALID_PARAMETER('hash', `valid height '${height}' on mined chain ${this.chain}`); return toBinaryBaseBlockHeader(header); }; return this.invokeChaintracksWithRetry(method); } async getHeight() { const method = async () => { return await this.options.chaintracks.currentHeight(); }; return this.invokeChaintracksWithRetry(method); } async hashToHeader(hash) { const method = async () => { const header = await this.options.chaintracks.findHeaderForBlockHash(hash); return header; }; let header = await this.invokeChaintracksWithRetry(method); if (!header) { header = await this.whatsonchain.getBlockHeaderByHash(hash); } if (!header) throw new WERR_errors_1.WERR_INVALID_PARAMETER('hash', `valid blockhash '${hash}' on mined chain ${this.chain}`); return header; } async getMerklePath(txid, useNext, logger) { const services = this.getMerklePathServices; if (useNext) services.next(); const r0 = { notes: [] }; logger === null || logger === void 0 ? void 0 : logger.group(`services getMerklePath`); for (let tries = 0; tries < services.count; tries++) { const stc = services.serviceToCall; try { const r = await stc.service(txid, this); if (r.notes) r0.notes.push(...r.notes); if (!r0.name) r0.name = r.name; if (r.merklePath) { logger === null || logger === void 0 ? void 0 : logger.log(`${stc.providerName} has merklePath`); // If we have a proof, call it done. r0.merklePath = r.merklePath; r0.header = r.header; r0.name = r.name; r0.error = undefined; services.addServiceCallSuccess(stc); break; } else { logger === null || logger === void 0 ? void 0 : logger.log(`${stc.providerName} no merklePath`); } if (r.error) services.addServiceCallError(stc, r.error); else services.addServiceCallFailure(stc); if (r.error && !r0.error) { // If we have an error and didn't before... r0.error = r.error; } } catch (eu) { const e = WalletError_1.WalletError.fromUnknown(eu); services.addServiceCallError(stc, e); } services.next(); } return r0; } async updateFiatExchangeRates(targetCurrencies, updateMsecs) { var _a, _b, _c, _d, _e; updateMsecs || (updateMsecs = 1000 * 60 * 60 * 24); const freshnessDate = new Date(Date.now() - updateMsecs); const stored = this.options.fiatExchangeRates; const storedRates = (_a = stored.rates) !== null && _a !== void 0 ? _a : {}; const toFetch = []; for (const c of targetCurrencies) { if (c === 'USD') { if (typeof storedRates.USD !== 'number') { storedRates.USD = 1; } continue; } const v = storedRates[c]; const ts = (_c = (_b = stored.rateTimestamps) === null || _b === void 0 ? void 0 : _b[c]) !== null && _c !== void 0 ? _c : stored.timestamp; const fresh = typeof v === 'number' && ts instanceof Date && ts > freshnessDate; if (!fresh) { toFetch.push(c); } } if (toFetch.length === 0) { this.options.fiatExchangeRates = { timestamp: stored.timestamp, base: stored.base, rates: storedRates, rateTimestamps: stored.rateTimestamps }; return this.options.fiatExchangeRates; } const services = this.updateFiatExchangeRateServices.clone(); let fetched; for (let tries = 0; tries < services.count; tries++) { const stc = services.serviceToCall; try { const r = await stc.service(toFetch, this.options); if (toFetch.every(c => { var _a; return c === 'USD' || typeof ((_a = r.rates) === null || _a === void 0 ? void 0 : _a[c]) === 'number'; })) { services.addServiceCallSuccess(stc); fetched = r; break; } else { services.addServiceCallFailure(stc); } } catch (eu) { const e = WalletError_1.WalletError.fromUnknown(eu); services.addServiceCallError(stc, e); } services.next(); } if (!fetched) { if (stored && Object.keys(storedRates).length > 0) { return stored; } throw new WERR_errors_1.WERR_INTERNAL(); } const nextRates = { ...storedRates }; const nextTimestamps = { ...((_d = stored.rateTimestamps) !== null && _d !== void 0 ? _d : {}) }; for (const c of toFetch) { const v = (_e = fetched.rates) === null || _e === void 0 ? void 0 : _e[c]; if (typeof v === 'number') { nextRates[c] = v; nextTimestamps[c] = fetched.timestamp; } } const nextTimestamp = new Date(Math.max(stored.timestamp instanceof Date ? stored.timestamp.getTime() : new Date(stored.timestamp).getTime(), fetched.timestamp.getTime())); this.options.fiatExchangeRates = { timestamp: nextTimestamp, base: stored.base, rates: nextRates, rateTimestamps: nextTimestamps }; return this.options.fiatExchangeRates; } async nLockTimeIsFinal(tx) { const MAXINT = 0xffffffff; const BLOCK_LIMIT = 500000000; let nLockTime; if (typeof tx === 'number') nLockTime = tx; else { if (typeof tx === 'string') { tx = sdk_1.Transaction.fromHex(tx); } else if (Array.isArray(tx)) { tx = sdk_1.Transaction.fromBinary(tx); } if (tx instanceof sdk_1.Transaction) { if (tx.inputs.every(i => i.sequence === MAXINT)) { return true; } nLockTime = tx.lockTime; } else { throw new WERR_errors_1.WERR_INTERNAL('Should be either @bsv/sdk Transaction or babbage-bsv Transaction'); } } if (nLockTime >= BLOCK_LIMIT) { const limit = Math.floor(Date.now() / 1000); return nLockTime < limit; } const height = await this.getHeight(); return nLockTime < height; } async getBeefForTxid(txid) { const beef = await (0, getBeefForTxid_1.getBeefForTxid)(this, txid); return beef; } } exports.Services = Services; function validateScriptHash(output, outputFormat) { let b = (0, utilityHelpers_noBuffer_1.asArray)(output); if (!outputFormat) { if (b.length === 32) outputFormat = 'hashLE'; else outputFormat = 'script'; } switch (outputFormat) { case 'hashBE': break; case 'hashLE': b = b.reverse(); break; case 'script': b = (0, utilityHelpers_1.sha256Hash)(b).reverse(); break; default: throw new WERR_errors_1.WERR_INVALID_PARAMETER('outputFormat', `not be ${outputFormat}`); } return (0, utilityHelpers_noBuffer_1.asString)(b); } /** * Serializes a block header as an 80 byte array. * The exact serialized format is defined in the Bitcoin White Paper * such that computing a double sha256 hash of the array computes * the block hash for the header. * @returns 80 byte array * @publicbody */ function toBinaryBaseBlockHeader(header) { const writer = new sdk_1.Utils.Writer(); writer.writeUInt32LE(header.version); writer.writeReverse((0, utilityHelpers_noBuffer_1.asArray)(header.previousHash)); writer.writeReverse((0, utilityHelpers_noBuffer_1.asArray)(header.merkleRoot)); writer.writeUInt32LE(header.time); writer.writeUInt32LE(header.bits); writer.writeUInt32LE(header.nonce); const r = writer.toArray(); return r; } //# sourceMappingURL=Services.js.map