@bsv/wallet-toolbox-client
Version:
Client only Wallet Storage
514 lines • 21.6 kB
JavaScript
;
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';
this.targetCurrencies = ['USD', 'GBP', 'EUR'];
this.chain = typeof optionsOrChain === 'string' ? optionsOrChain : optionsOrChain.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);
//prettier-ignore
this.getMerklePathServices = new ServiceCollection_1.ServiceCollection('getMerklePath')
.add({ name: 'WhatsOnChain', service: this.whatsonchain.getMerklePath.bind(this.whatsonchain) })
.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) })
.add({ name: 'Bitails', service: this.bitails.postBeef.bind(this.bitails) })
.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: 'ChaintracksService', service: exchangeRates_1.updateChaintracksFiatExchangeRates })
.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) {
const rates = await this.updateFiatExchangeRates(this.options.fiatExchangeRates, this.options.fiatUpdateMsecs);
this.options.fiatExchangeRates = rates;
base || (base = 'USD');
const rate = rates.rates[currency] / rates.rates[base];
return rate;
}
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) {
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: []
};
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);
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);
}
return r0;
}
async getScriptHashHistory(hash, useNext) {
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: []
};
for (let tries = 0; tries < services.count; tries++) {
const stc = services.serviceToCall;
try {
const r = await stc.service(hash);
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();
}
return r0;
}
/**
*
* @param beef
* @param chain
* @returns
*/
async postBeef(beef, txids) {
let rs = [];
const services = this.postBeefServices;
const stcs = services.allServicesToCall;
switch (this.postBeefMode) {
case 'UntilSuccess':
{
for (const stc of stcs) {
const r = await callService(stc);
rs.push(r);
if (r.status === 'success')
break;
if (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;
}
return rs;
async function callService(stc) {
const r = await stc.service(beef, txids);
if (r.status === 'success') {
services.addServiceCallSuccess(stc);
}
else {
if (r.error) {
services.addServiceCallError(stc, r.error);
}
else {
services.addServiceCallFailure(stc);
}
}
return r;
}
}
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);
if (!header)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('hash', `valid blockhash '${hash}' on mined chain ${this.chain}`);
return header;
};
return this.invokeChaintracksWithRetry(method);
}
async getMerklePath(txid, useNext) {
const services = this.getMerklePathServices;
if (useNext)
services.next();
const r0 = { notes: [] };
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) {
// 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;
}
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(rates, updateMsecs) {
updateMsecs || (updateMsecs = 1000 * 60 * 15);
const freshnessDate = new Date(Date.now() - updateMsecs);
if (rates) {
// Check if the rate we know is stale enough to update.
updateMsecs || (updateMsecs = 1000 * 60 * 15);
if (rates.timestamp > freshnessDate)
return rates;
}
// Make sure we always start with the first service listed (chaintracks aggregator)
const services = this.updateFiatExchangeRateServices.clone();
let r0;
for (let tries = 0; tries < services.count; tries++) {
const stc = services.serviceToCall;
try {
const r = await stc.service(this.targetCurrencies, this.options);
if (this.targetCurrencies.every(c => typeof r.rates[c] === 'number')) {
services.addServiceCallSuccess(stc);
r0 = r;
break;
}
else {
services.addServiceCallFailure(stc);
}
}
catch (eu) {
const e = WalletError_1.WalletError.fromUnknown(eu);
services.addServiceCallError(stc, e);
}
services.next();
}
if (!r0) {
console.error('Failed to update fiat exchange rates.');
if (!rates)
throw new WERR_errors_1.WERR_INTERNAL();
return rates;
}
return r0;
}
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.writeUInt32BE(header.version);
writer.writeReverse((0, utilityHelpers_noBuffer_1.asArray)(header.previousHash));
writer.writeReverse((0, utilityHelpers_noBuffer_1.asArray)(header.merkleRoot));
writer.writeUInt32BE(header.time);
writer.writeUInt32BE(header.bits);
writer.writeUInt32BE(header.nonce);
const r = writer.toArray();
return r;
}
//# sourceMappingURL=Services.js.map