@bsv/wallet-toolbox-client
Version:
Client only Wallet Storage
680 lines • 29.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WhatsOnChain = exports.WhatsOnChainNoServices = void 0;
exports.convertWocToBlockHeaderHex = convertWocToBlockHeaderHex;
exports.getWhatsOnChainBlockHeaderByHash = getWhatsOnChainBlockHeaderByHash;
const sdk_1 = require("@bsv/sdk");
const tscProofToMerklePath_1 = require("../../utility/tscProofToMerklePath");
const SdkWhatsOnChain_1 = __importDefault(require("./SdkWhatsOnChain"));
const WERR_errors_1 = require("../../sdk/WERR_errors");
const WalletError_1 = require("../../sdk/WalletError");
const utilityHelpers_1 = require("../../utility/utilityHelpers");
const utilityHelpers_noBuffer_1 = require("../../utility/utilityHelpers.noBuffer");
const Services_1 = require("../Services");
const validationHelpers_1 = require("../../sdk/validationHelpers");
class WhatsOnChainNoServices extends SdkWhatsOnChain_1.default {
constructor(chain = 'main', config = {}) {
super(chain, config);
}
/**
* POST
* https://api.whatsonchain.com/v1/bsv/main/txs/status
* Content-Type: application/json
* data: "{\"txids\":[\"6815f8014db74eab8b7f75925c68929597f1d97efa970109d990824c25e5e62b\"]}"
*
* result for a mined txid:
* [{
* "txid":"294cd1ebd5689fdee03509f92c32184c0f52f037d4046af250229b97e0c8f1aa",
* "blockhash":"000000000000000004b5ce6670f2ff27354a1e87d0a01bf61f3307f4ccd358b5",
* "blockheight":612251,
* "blocktime":1575841517,
* "confirmations":278272
* }]
*
* result for a valid recent txid:
* [{"txid":"6815f8014db74eab8b7f75925c68929597f1d97efa970109d990824c25e5e62b"}]
*
* result for an unknown txid:
* [{"txid":"6815f8014db74eab8b7f75925c68929597f1d97efa970109d990824c25e5e62c","error":"unknown"}]
*/
async getStatusForTxids(txids) {
const r = {
name: 'WoC',
status: 'error',
error: undefined,
results: []
};
const requestOptions = {
method: 'POST',
headers: this.getHttpHeaders(),
data: { txids }
};
const url = `${this.URL}/txs/status`;
try {
const response = await this.httpClient.request(url, requestOptions);
if (!response.data || !response.ok || response.status !== 200)
throw new WERR_errors_1.WERR_INVALID_OPERATION(`Unable to get status for txids at this timei.`);
const data = response.data;
for (const txid of txids) {
const d = data.find(d => d.txid === txid);
if (!d || d.error === 'unknown')
r.results.push({ txid, status: 'unknown', depth: undefined });
else if (d.error !== undefined) {
console.log(`WhatsOnChain getStatusForTxids unexpected error ${d.error} ${txid}`);
r.results.push({ txid, status: 'unknown', depth: undefined });
}
else if (d.confirmations === undefined)
r.results.push({ txid, status: 'known', depth: 0 });
else
r.results.push({ txid, status: 'mined', depth: d.confirmations });
}
r.status = 'success';
}
catch (eu) {
const e = WalletError_1.WalletError.fromUnknown(eu);
r.error = e;
}
return r;
}
/**
* 2025-02-16 throwing internal server error 500.
* @param txid
* @returns
*/
async getTxPropagation(txid) {
const requestOptions = {
method: 'GET',
headers: this.getHttpHeaders()
};
const response = await this.httpClient.request(`${this.URL}/tx/hash/${txid}/propagation`, requestOptions);
// response.statusText is often, but not always 'OK' on success...
if (!response.data || !response.ok || response.status !== 200)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('txid', `valid transaction. '${txid}' response ${response.statusText}`);
return 0;
}
/**
* May return undefined for unmined transactions that are in the mempool.
* @param txid
* @returns raw transaction as hex string or undefined if txid not found in mined block.
*/
async getRawTx(txid) {
const headers = this.getHttpHeaders();
headers['Cache-Control'] = 'no-cache';
const requestOptions = {
method: 'GET',
headers
};
const url = `${this.URL}/tx/${txid}/hex`;
for (let retry = 0; retry < 2; retry++) {
const response = await this.httpClient.request(url, requestOptions);
if (response.statusText === 'Too Many Requests' && retry < 2) {
await (0, utilityHelpers_1.wait)(2000);
continue;
}
if (response.status === 404 && response.statusText === 'Not Found')
return undefined;
// response.statusText is often, but not always 'OK' on success...
if (!response.data || !response.ok || response.status !== 200)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('txid', `valid transaction. '${txid}' response ${response.statusText}`);
return response.data;
}
throw new WERR_errors_1.WERR_INTERNAL();
}
async getRawTxResult(txid) {
const r = { name: 'WoC', txid: (0, utilityHelpers_noBuffer_1.asString)(txid) };
try {
const rawTxHex = await this.getRawTx(txid);
if (rawTxHex)
r.rawTx = (0, utilityHelpers_noBuffer_1.asArray)(rawTxHex);
}
catch (err) {
r.error = WalletError_1.WalletError.fromUnknown(err);
}
return r;
}
/**
* WhatsOnChain 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, txids) {
const r = {
name: 'WoC',
status: 'success',
txidResults: [],
notes: []
};
let delay = false;
const nn = () => ({ name: 'WoCpostBeef', when: new Date().toISOString() });
const nne = () => ({ ...nn(), beef: beef.toHex(), txids: txids.join(',') });
for (const txid of txids) {
const rawTx = sdk_1.Utils.toHex(beef.findTxid(txid).rawTx);
if (delay) {
// For multiple txids, give WoC time to propagate each one.
await (0, utilityHelpers_1.wait)(3000);
}
delay = true;
const tr = await this.postRawTx(rawTx);
if (txid !== tr.txid) {
tr.notes.push({ ...nne(), what: 'postRawTxTxidChanged', txid, trTxid: tr.txid });
}
r.txidResults.push(tr);
if (r.status === 'success' && tr.status !== 'success')
r.status = 'error';
}
if (r.status === 'success') {
r.notes.push({ ...nn(), what: 'postBeefSuccess' });
}
else {
r.notes.push({ ...nne(), what: 'postBeefError' });
}
return r;
}
/**
* @param rawTx raw transaction to broadcast as hex string
* @returns txid returned by transaction processor of transaction broadcast
*/
async postRawTx(rawTx) {
let txid = sdk_1.Utils.toHex((0, utilityHelpers_1.doubleSha256BE)(sdk_1.Utils.toArray(rawTx, 'hex')));
const r = {
txid,
status: 'success',
notes: []
};
const headers = this.getHttpHeaders();
headers['Content-Type'] = 'application/json';
headers['Accept'] = 'text/plain';
const requestOptions = {
method: 'POST',
headers,
data: { txhex: rawTx }
};
const url = `${this.URL}/tx/raw`;
const nn = () => ({ name: 'WoCpostRawTx', when: new Date().toISOString() });
const nne = () => ({ ...nn(), rawTx, txid, url });
const retryLimit = 5;
for (let retry = 0; retry < retryLimit; retry++) {
try {
const response = await this.httpClient.request(url, requestOptions);
if (response.statusText === 'Too Many Requests' && retry < 2) {
r.notes.push({ ...nn(), what: 'postRawTxRateLimit' });
await (0, utilityHelpers_1.wait)(2000);
continue;
}
if (response.ok) {
const txid = response.data;
r.notes.push({ ...nn(), what: 'postRawTxSuccess' });
}
else if (response.data === 'unexpected response code 500: Transaction already in the mempool') {
r.notes.push({ ...nne(), what: 'postRawTxSuccessAlreadyInMempool' });
}
else {
r.status = 'error';
if (response.data === 'unexpected response code 500: 258: txn-mempool-conflict') {
r.doubleSpend = true; // this is a possible double spend attempt
r.competingTxs = undefined; // not provided with any data for this.
r.notes.push({ ...nne(), what: 'postRawTxErrorMempoolConflict' });
}
else if (response.data === 'unexpected response code 500: Missing inputs') {
r.doubleSpend = true; // this is a possible double spend attempt
r.competingTxs = undefined; // not provided with any data for this.
r.notes.push({ ...nne(), what: 'postRawTxErrorMissingInputs' });
}
else {
const n = {
...nne(),
what: 'postRawTxError'
};
if (typeof response.data === 'string') {
n.data = response.data.slice(0, 128);
r.data = response.data;
}
else {
r.data = '';
}
if (typeof response.statusText === 'string') {
n.statusText = response.statusText.slice(0, 128);
r.data += `,${response.statusText}`;
}
if (typeof response.status === 'string') {
n.status = response.status.slice(0, 128);
r.data += `,${response.status}`;
}
if (typeof response.status === 'number') {
n.status = response.status;
r.data += `,${response.status}`;
}
r.notes.push(n);
}
}
}
catch (eu) {
r.status = 'error';
const e = WalletError_1.WalletError.fromUnknown(eu);
r.notes.push({
...nne(),
what: 'postRawTxCatch',
code: e.code,
description: e.description
});
r.serviceError = true;
r.data = `${e.code} ${e.description}`;
}
return r;
}
r.status = 'error';
r.serviceError = true;
r.notes.push({
...nne(),
what: 'postRawTxRetryLimit',
retryLimit
});
return r;
}
async updateBsvExchangeRate(rate, updateMsecs) {
if (rate) {
// Check if the rate we know is stale enough to update.
updateMsecs || (updateMsecs = 1000 * 60 * 15);
if (new Date(Date.now() - updateMsecs) < rate.timestamp)
return rate;
}
const requestOptions = {
method: 'GET',
headers: this.getHttpHeaders()
};
for (let retry = 0; retry < 2; retry++) {
const response = await this.httpClient.request(`${this.URL}/exchangerate`, requestOptions);
if (response.statusText === 'Too Many Requests' && retry < 2) {
await (0, utilityHelpers_1.wait)(2000);
continue;
}
// response.statusText is often, but not always 'OK' on success...
if (!response.data || !response.ok || response.status !== 200)
throw new WERR_errors_1.WERR_INVALID_OPERATION(`WoC exchangerate response ${response.statusText}`);
const wocrate = response.data;
if (wocrate.currency !== 'USD')
wocrate.rate = NaN;
const newRate = {
timestamp: new Date(),
base: 'USD',
rate: wocrate.rate
};
return newRate;
}
throw new WERR_errors_1.WERR_INTERNAL();
}
async getUtxoStatus(output, outputFormat, outpoint) {
const r = {
name: 'WoC',
status: 'error',
error: new WERR_errors_1.WERR_INTERNAL(),
details: []
};
for (let retry = 0;; retry++) {
let url = '';
try {
const scriptHash = (0, Services_1.validateScriptHash)(output, outputFormat);
const requestOptions = {
method: 'GET',
headers: this.getHttpHeaders()
};
const response = await this.httpClient.request(`${this.URL}/script/${scriptHash}/unspent/all`, requestOptions);
if (response.statusText === 'Too Many Requests' && retry < 2) {
await (0, utilityHelpers_1.wait)(2000);
continue;
}
// response.statusText is often, but not always 'OK' on success...
if (!response.data || !response.ok || response.status !== 200)
throw new WERR_errors_1.WERR_INVALID_OPERATION(`WoC getUtxoStatus response ${response.statusText}`);
const data = response.data;
if (data.script !== scriptHash || !Array.isArray(data.result)) {
throw new WERR_errors_1.WERR_INTERNAL('data. is not an array');
}
if (data.result.length === 0) {
r.status = 'success';
r.error = undefined;
r.isUtxo = false;
}
else {
r.status = 'success';
r.error = undefined;
for (const s of data.result) {
r.details.push({
txid: s.tx_hash,
satoshis: s.value,
height: s.height,
index: s.tx_pos
});
}
if (outpoint) {
const { txid, vout } = (0, validationHelpers_1.parseWalletOutpoint)(outpoint);
r.isUtxo = r.details.find(d => d.txid === txid && d.index === vout) !== undefined;
}
else
r.isUtxo = r.details.length > 0;
}
return r;
}
catch (eu) {
const e = WalletError_1.WalletError.fromUnknown(eu);
if (e.code !== 'ECONNRESET' || retry > 2) {
r.error = new WERR_errors_1.WERR_INTERNAL(`service failure: ${url}, error: ${JSON.stringify(WalletError_1.WalletError.fromUnknown(eu))}`);
return r;
}
}
}
}
async getScriptHashConfirmedHistory(hash) {
const r = {
name: 'WoC',
status: 'error',
error: undefined,
history: []
};
// reverse hash from LE to BE for Woc
hash = sdk_1.Utils.toHex(sdk_1.Utils.toArray(hash, 'hex').reverse());
const url = `${this.URL}/script/${hash}/confirmed/history`;
for (let retry = 0;; retry++) {
try {
const requestOptions = {
method: 'GET',
headers: this.getHttpHeaders()
};
const response = await this.httpClient.request(url, requestOptions);
if (response.statusText === 'Too Many Requests' && retry < 2) {
await (0, utilityHelpers_1.wait)(2000);
continue;
}
if (!response.ok && response.status === 404) {
// There is no history for this script hash...
r.status = 'success';
return r;
}
// response.statusText is often, but not always 'OK' on success...
if (!response.data || !response.ok || response.status !== 200) {
r.error = new WERR_errors_1.WERR_BAD_REQUEST(`WoC getScriptHashConfirmedHistory response ${response.ok} ${response.status} ${response.statusText}`);
return r;
}
if (response.data.error) {
r.error = new WERR_errors_1.WERR_BAD_REQUEST(`WoC getScriptHashConfirmedHistory error ${response.data.error}`);
return r;
}
r.history = response.data.result.map(d => ({ txid: d.tx_hash, height: d.height }));
r.status = 'success';
return r;
}
catch (eu) {
const e = WalletError_1.WalletError.fromUnknown(eu);
if (e.code !== 'ECONNRESET' || retry > 2) {
r.error = new WERR_errors_1.WERR_INTERNAL(`WoC getScriptHashConfirmedHistory service failure: ${url}, error: ${JSON.stringify(WalletError_1.WalletError.fromUnknown(eu))}`);
return r;
}
}
}
return r;
}
async getScriptHashUnconfirmedHistory(hash) {
const r = {
name: 'WoC',
status: 'error',
error: undefined,
history: []
};
// reverse hash from LE to BE for Woc
hash = sdk_1.Utils.toHex(sdk_1.Utils.toArray(hash, 'hex').reverse());
const url = `${this.URL}/script/${hash}/unconfirmed/history`;
for (let retry = 0;; retry++) {
try {
const requestOptions = {
method: 'GET',
headers: this.getHttpHeaders()
};
const response = await this.httpClient.request(url, requestOptions);
if (response.statusText === 'Too Many Requests' && retry < 2) {
await (0, utilityHelpers_1.wait)(2000);
continue;
}
if (!response.ok && response.status === 404) {
// There is no history for this script hash...
r.status = 'success';
return r;
}
// response.statusText is often, but not always 'OK' on success...
if (!response.data || !response.ok || response.status !== 200) {
r.error = new WERR_errors_1.WERR_BAD_REQUEST(`WoC getScriptHashUnconfirmedHistory response ${response.ok} ${response.status} ${response.statusText}`);
return r;
}
if (response.data.error) {
r.error = new WERR_errors_1.WERR_BAD_REQUEST(`WoC getScriptHashUnconfirmedHistory error ${response.data.error}`);
return r;
}
r.history = response.data.result.map(d => ({ txid: d.tx_hash, height: d.height }));
r.status = 'success';
return r;
}
catch (eu) {
const e = WalletError_1.WalletError.fromUnknown(eu);
if (e.code !== 'ECONNRESET' || retry > 2) {
r.error = new WERR_errors_1.WERR_INTERNAL(`WoC getScriptHashUnconfirmedHistory service failure: ${url}, error: ${JSON.stringify(WalletError_1.WalletError.fromUnknown(eu))}`);
return r;
}
}
}
return r;
}
async getScriptHashHistory(hash) {
const r1 = await this.getScriptHashConfirmedHistory(hash);
if (r1.error || r1.status !== 'success')
return r1;
const r2 = await this.getScriptHashUnconfirmedHistory(hash);
if (r2.error || r2.status !== 'success')
return r2;
r1.history = r1.history.concat(r2.history);
return r1;
}
/**
{
"hash": "000000000000000004a288072ebb35e37233f419918f9783d499979cb6ac33eb",
"confirmations": 328433,
"size": 14421,
"height": 575045,
"version": 536928256,
"versionHex": "2000e000",
"merkleroot": "4ebcba09addd720991d03473f39dce4b9a72cc164e505cd446687a54df9b1585",
"time": 1553416668,
"mediantime": 1553414858,
"nonce": 87914848,
"bits": "180997ee",
"difficulty": 114608607557.4425,
"chainwork": "000000000000000000000000000000000000000000ddf5d385546872bab7dc01",
"previousblockhash": "00000000000000000988156c7075dc9147a5b62922f1310862e8b9000d46dd9b",
"nextblockhash": "00000000000000000112b36a37c10235fa0c991f680bc5482ba9692e0ae697db",
"nTx": 0,
"num_tx": 5
}
*/
async getBlockHeaderByHash(hash) {
const headers = this.getHttpHeaders();
const requestOptions = {
method: 'GET',
headers
};
const url = `${this.URL}/block/${hash}/header`;
for (let retry = 0; retry < 2; retry++) {
const response = await this.httpClient.request(url, requestOptions);
if (response.statusText === 'Too Many Requests' && retry < 2) {
await (0, utilityHelpers_1.wait)(2000);
continue;
}
if (response.status === 404 && response.statusText === 'Not Found')
return undefined;
// response.statusText is often, but not always 'OK' on success...
if (!response.data || !response.ok || response.status !== 200)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('hash', `valid block hash. '${hash}' response ${response.statusText}`);
const header = convertWocToBlockHeaderHex(response.data);
return header;
}
throw new WERR_errors_1.WERR_INTERNAL();
}
async getChainInfo() {
const headers = this.getHttpHeaders();
const requestOptions = {
method: 'GET',
headers
};
const url = `${this.URL}/chain/info`;
for (let retry = 0; retry < 2; retry++) {
const response = await this.httpClient.request(url, requestOptions);
if (response.statusText === 'Too Many Requests' && retry < 2) {
await (0, utilityHelpers_1.wait)(2000);
continue;
}
// response.statusText is often, but not always 'OK' on success...
if (!response.data || !response.ok || response.status !== 200)
throw new WERR_errors_1.WERR_INVALID_PARAMETER('hash', `valid block hash. '${url}' response ${response.statusText}`);
return response.data;
}
throw new WERR_errors_1.WERR_INTERNAL();
}
}
exports.WhatsOnChainNoServices = WhatsOnChainNoServices;
/**
*
*/
class WhatsOnChain extends WhatsOnChainNoServices {
constructor(chain = 'main', config = {}, services) {
super(chain, config);
this.services = services || new Services_1.Services(chain);
}
/**
* @param txid
* @returns
*/
async getMerklePath(txid, services) {
const r = { name: 'WoCTsc', notes: [] };
const headers = this.getHttpHeaders();
const requestOptions = {
method: 'GET',
headers
};
for (let retry = 0; retry < 2; retry++) {
try {
const response = await this.httpClient.request(`${this.URL}/tx/${txid}/proof/tsc`, requestOptions);
if (response.statusText === 'Too Many Requests' && retry < 2) {
r.notes.push({
what: 'getMerklePathRetry',
name: r.name,
status: response.status,
statusText: response.statusText
});
await (0, utilityHelpers_1.wait)(2000);
continue;
}
if (response.status === 404 && response.statusText === 'Not Found') {
r.notes.push({
what: 'getMerklePathNotFound',
name: r.name,
status: response.status,
statusText: response.statusText
});
return r;
}
// response.statusText is often, but not always 'OK' on success...
if (!response.ok || response.status !== 200) {
r.notes.push({
what: 'getMerklePathBadStatus',
name: r.name,
status: response.status,
statusText: response.statusText
});
throw new WERR_errors_1.WERR_INVALID_PARAMETER('txid', `valid transaction. '${txid}' response ${response.statusText}`);
}
if (!response.data) {
// Unmined, proof not yet available.
r.notes.push({
what: 'getMerklePathNoData',
name: r.name,
status: response.status,
statusText: response.statusText
});
return r;
}
if (!Array.isArray(response.data))
response.data = [response.data];
if (response.data.length != 1)
return r;
const p = response.data[0];
const header = await services.hashToHeader(p.target);
if (header) {
const proof = {
index: p.index,
nodes: p.nodes,
height: header.height
};
r.merklePath = (0, tscProofToMerklePath_1.convertProofToMerklePath)(txid, proof);
r.header = header;
r.notes.push({
what: 'getMerklePathSuccess',
name: r.name,
status: response.status,
statusText: response.statusText
});
}
else {
r.notes.push({
what: 'getMerklePathNoHeader',
target: p.target,
name: r.name,
status: response.status,
statusText: response.statusText
});
throw new WERR_errors_1.WERR_INVALID_PARAMETER('blockhash', 'a valid on-chain block hash');
}
}
catch (eu) {
const e = WalletError_1.WalletError.fromUnknown(eu);
r.notes.push({
what: 'getMerklePathError',
name: r.name,
code: e.code,
description: e.description
});
r.error = e;
}
return r;
}
r.notes.push({ what: 'getMerklePathInternal', name: r.name });
throw new WERR_errors_1.WERR_INTERNAL();
}
}
exports.WhatsOnChain = WhatsOnChain;
function convertWocToBlockHeaderHex(woc) {
const bits = typeof woc.bits === 'string' ? parseInt(woc.bits, 16) : woc.bits;
if (!woc.previousblockhash) {
woc.previousblockhash = '0000000000000000000000000000000000000000000000000000000000000000'; // genesis
}
return {
version: woc.version,
previousHash: woc.previousblockhash,
merkleRoot: woc.merkleroot,
time: woc.time,
bits,
nonce: woc.nonce,
hash: woc.hash,
height: woc.height
};
}
async function getWhatsOnChainBlockHeaderByHash(hash, chain = 'main', apiKey) {
const config = apiKey ? { apiKey } : {};
const woc = new WhatsOnChain(chain, config);
const header = await woc.getBlockHeaderByHash(hash);
return header;
}
//# sourceMappingURL=WhatsOnChain.js.map