UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

519 lines (517 loc) 23.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WocHeadersBulkListener = WocHeadersBulkListener; exports.WocHeadersBulkListener_test = WocHeadersBulkListener_test; exports.WocHeadersLiveListener = WocHeadersLiveListener; exports.WocHeadersLiveListener_test = WocHeadersLiveListener_test; const ws_1 = __importDefault(require("ws")); const WhatsOnChain_1 = require("../../../providers/WhatsOnChain"); const utilityHelpers_1 = require("../../../../utility/utilityHelpers"); async function getWhatsOnChainTipHeight(chain = 'main', apiKey) { const config = apiKey ? { apiKey } : {}; const woc = new WhatsOnChain_1.WhatsOnChain(chain, config); const chainInfo = await woc.getChainInfo(); return chainInfo.blocks; } /** * High speed WebSocket based based old block header listener * @param fromHeight * @param enqueue returns headers received from WebSocket service * @param error notifies of abnormal events, return false to close websocket, true to ignore the error. * @param stop an object with a stop property which gets set to a method to stop listener * @param chain 'test' | 'main' * @param idleWait how many milliseconds to timeout between completion checks. * @returns true on normal completion, false if should restart if no error received. */ async function WocHeadersBulkListener(fromHeight, toHeight, enqueue, error, stop, chain, logger = () => { }, idleWait = 5000) { //logger(`WocHeadersBulkListener from ${fromHeight} to ${toHeight} on ${chain} chain`) let done = false; let count = 0; let ping = 0; let ok = false; let wsIsOpen = false; function stopNow() { ok = true; if (wsIsOpen) { wsIsOpen = false; ws.close(); } else done = true; } stop.stop = stopNow; let webSocketUrl; switch (chain) { case 'test': case 'teratest': webSocketUrl = `wss://socket-v2-testnet.whatsonchain.com/websocket/blockheaders/history?from=${fromHeight}&to=${toHeight}`; break; case 'main': webSocketUrl = `wss://socket-v2.whatsonchain.com/websocket/blockheaders/history?from=${fromHeight}&to=${toHeight}`; break; case 'mock': throw new Error(`WocHeadersBulkListener does not support 'mock' chain.`); } function processData(rawData) { if (rawData.length === 0) { ping++; return; } const typicalrawDataAsOf20250710BufferThatParsesTo = `{ "channel": "blockHeaders", "pub": { "data": { "version": 635068416, "previousblockhash": "000000000000000005f5778823b824dedf49a503aac60ae153471a3e737d1b1b", "merkleroot": "617e2ce2c1348ec1b35ac727cad8bdec33b1b4b983bb00ba2583af4fe1e25ef7", "time": 1752133908, "bits": 403904562, "nonce": 1932754962, "hash": "00000000000000000670c88b898aadf911ecf4bc6ba4b71f9b319df47f022b8a", "height": 904889 } } }`; // Earlier data formats... // "{\"channel\":\"woc#c91c044b-66f5-4172-b509-82c11a41d7fc\",\"data\":{\"data\":{\"hash\":\"00000000000000000ce16106e2100b893f3da5989ef23cbac52eb568cf803dc1\",\"confirmations\":8,\"size\":3345869,\"height\":779890,\"version\":939515904,\"versionHex\":\"37ffe000\",\"merkleroot\":\"92e3d1d72d432c9216d296839eaa675b6ffb40ccf6e1a02229caf3d769dc0a5f\",\"txcount\":10757,\"time\":1676851109,\"mediantime\":1676849009,\"nonce\":4216995243,\"bits\":\"180e38cb\",\"difficulty\":77310268438.5808,\"chainwork\":\"00000000000000000000000000000000000000000142d5cf97c0ed497eae1066\",\"previousblockhash\":\"0000000000000000053d26b7b35a76bf2b672428bae591bc033ae99f9873697a\",\"nextblockhash\":\"000000000000000000e1a110376966033507953d50ade86eadef7226843f3349\"}}}" // Returned rawData may be a Buffer... const data = JSON.parse(rawData); switch (data.type || 0) { case 0: // last wocHeader has empty string for nextBlockHash // eslint-disable-next-line no-case-declarations if (!done) { const json = JSON.stringify(data); if (json === '{}') { ws.send('ping'); } else if (data.connect) { logger(json); } else if (data.pub) { //logger(json) const wocHeader = data.pub.data; if (wocHeader) { const header = (0, WhatsOnChain_1.convertWocToBlockHeaderHex)(wocHeader); enqueue(header); count++; if (header.height >= toHeight) { ok = true; done = true; ws.close(); } } } else { error(42, `unknown data ${json}`); } } break; case 3: break; // Unsubscribe "{\"type\":3,\"channel\":\"woc#c91c044b-66f5-4172-b509-82c11a41d7fc\",\"data\":{}}" case 5: break; // console.log('subscribed to a channel ' + data.channel) case 6: break; // subscribe "{\"type\":6,\"data\":{\"client\":\"c91c044b-66f5-4172-b509-82c11a41d7fc\",\"subs\":{\"woc#c91c044b-66f5-4172-b509-82c11a41d7fc\":{}},\"session\":\"d8693511-5bc8-4815-beb6-8ae1bdaaf3a6\"}}" case 7: // If you don't close after unsubscribe or end of headers, after a delay, this is received. // "{\"type\":7,\"data\":{\"code\":200,\"reason\":\"Data Delivered\"}}" error(data.data.code, JSON.stringify(data.data)); ws.close(); break; default: error(42, `unknown rawData ${rawData}`); ws.close(); } } const ws = new ws_1.default(webSocketUrl); ws.onopen = function (evt) { //logger(`WoC ${new Date().toISOString()} addHeaders from ${fromHeight} on ${chain}`) // This is required to trigger connect on server side. ws.send(JSON.stringify({})); wsIsOpen = true; }; ws.onerror = function (evt) { // if (evt.message.startsWith('getaddrinfo ENOTFOUND')) { let code = -1; switch (evt.message) { case 'Unexpected server response: 403': code = 403; break; case 'Unexpected server response: 404': code = 404; break; default: break; } const ignoreError = error(code, evt.message); if (!ignoreError) ws.close(); }; ws.onclose = function (ev) { // ok should be true if we got the last header, false otherwise. // error will have been called with anything abnormal... //logger(`WoC ${new Date().toISOString()} addHeaders input service closed`) done = true; }; ws.onmessage = function (ev) { // console.log("onmessage:", JSON.stringify(ev.data)); processData(ev.data); }; // Allow this many wait repetitions for first header, then expect the headers to keep coming const firstHeaderCycles = 12; let waitCycles = 0; while (!done) { const qlPreWait = count; await (0, utilityHelpers_1.wait)(idleWait); const qlPostWait = count; if (waitCycles > firstHeaderCycles && !done && qlPreWait === 0 && (qlPreWait === qlPostWait || ping > 0)) { // Web socket didn't close normally but we aren't getting any new headers // Assume a broken connection error(-2, 'unexpectedly went idle'); done = true; } ping = 0; waitCycles++; } //logger(`WoC ${new Date().toISOString()} ${count} headers added`) return ok; } /* v1 onmessage: "{\"type\":6,\"data\":{\"client\":\"c91c044b-66f5-4172-b509-82c11a41d7fc\",\"subs\":{\"woc#c91c044b-66f5-4172-b509-82c11a41d7fc\":{}},\"session\":\"d8693511-5bc8-4815-beb6-8ae1bdaaf3a6\"}}" onmessage: "{\"channel\":\"woc#c91c044b-66f5-4172-b509-82c11a41d7fc\",\"data\":{\"data\":{\"hash\":\"00000000000000000ce16106e2100b893f3da5989ef23cbac52eb568cf803dc1\",\"confirmations\":8,\"size\":3345869,\"height\":779890,\"version\":939515904,\"versionHex\":\"37ffe000\",\"merkleroot\":\"92e3d1d72d432c9216d296839eaa675b6ffb40ccf6e1a02229caf3d769dc0a5f\",\"txcount\":10757,\"time\":1676851109,\"mediantime\":1676849009,\"nonce\":4216995243,\"bits\":\"180e38cb\",\"difficulty\":77310268438.5808,\"chainwork\":\"00000000000000000000000000000000000000000142d5cf97c0ed497eae1066\",\"previousblockhash\":\"0000000000000000053d26b7b35a76bf2b672428bae591bc033ae99f9873697a\",\"nextblockhash\":\"000000000000000000e1a110376966033507953d50ade86eadef7226843f3349\"}}}" onmessage: "{\"channel\":\"woc#c91c044b-66f5-4172-b509-82c11a41d7fc\",\"data\":{\"data\":{\"hash\":\"000000000000000000e1a110376966033507953d50ade86eadef7226843f3349\",\"confirmations\":7,\"size\":615786,\"height\":779891,\"version\":809197568,\"versionHex\":\"303b6000\",\"merkleroot\":\"8f5df85a6fafe2d28c679ff1221c3ba331af8f361ba70752e700d2d6382e194e\",\"txcount\":1967,\"time\":1676851242,\"mediantime\":1676849070,\"nonce\":4059160917,\"bits\":\"180e4320\",\"difficulty\":77091494195.17546,\"chainwork\":\"00000000000000000000000000000000000000000142d5e18ad5b99177e0f584\",\"previousblockhash\":\"00000000000000000ce16106e2100b893f3da5989ef23cbac52eb568cf803dc1\",\"nextblockhash\":\"000000000000000002630b7972c23795326392d9c8b4b19aa1ca4753a3fb9ccd\"}}}" ... onmessage: "{\"channel\":\"woc#c91c044b-66f5-4172-b509-82c11a41d7fc\",\"data\":{\"data\":{\"hash\":\"00000000000000000af46a48dec514ff3a4ad9eb6710590ee0385778698241e7\",\"confirmations\":1,\"size\":5119462,\"height\":779897,\"version\":805298176,\"versionHex\":\"2fffe000\",\"merkleroot\":\"3f9b260f6802a3e218366dba3b478dd01703b1b71b12861f838409fbe47fcfba\",\"txcount\":11625,\"time\":1676856449,\"mediantime\":1676852429,\"nonce\":755767059,\"bits\":\"180e6ac7\",\"difficulty\":76263251756.43698,\"chainwork\":\"00000000000000000000000000000000000000000142d64c49fecded8e8be674\",\"previousblockhash\":\"000000000000000000f5b8d6cf1ec6632fd365427600d5acc4c558ce858a8b5a\",\"nextblockhash\":\"\"}}}" onmessage: "{\"type\":3,\"channel\":\"woc#c91c044b-66f5-4172-b509-82c11a41d7fc\",\"data\":{}}" onmessage: "{\"type\":7,\"data\":{\"code\":200,\"reason\":\"Data Delivered\"}}" */ /** v2 { "message": { "data": { "version": 872415232, "previousblockhash": "00000000000000000ea1f9ba0817a0f922ee227be306fd9097a4e76caf5ff411", "merkleroot": "dcd7efb3c39e8e2d597e4757b9a49c98f52f77a6df39d1d5936ac3abb2559944", "time": 1750182239, "bits": 403926191, "nonce": 1043732575, "hash": "0000000000000000032d09ca772ca5b3bc5b90a79a5bbcc4a05c99fb6d3b23d8", "height": 901658 } } } */ async function WocHeadersBulkListener_test() { try { const chains = ['main', 'test']; // eslint-disable-next-line prefer-const const stop = { stop: undefined }; for (const chain of chains) { const apiKey = undefined; const tipHeight = await getWhatsOnChainTipHeight(chain, apiKey); let errors = 0; while (errors === 0 && !(await WocHeadersBulkListener(tipHeight - 3, tipHeight, header => console.log(JSON.stringify(header)), (code, message) => { errors++; console.log(`code ${code} message ${message}`); return true; }, stop, chain, console.log.bind(console), 5000))) { console.log('restarting WoC addHeaders'); } } } catch (err) { console.log(err); } } /** * High speed WebSocket based based new block header listener * @param enqueue returns headers received from WebSocket service * @param error notifies of abnormal events, return false to close websocket, true to ignore the error. * @param stop an object with a stop property which gets set to a method to stop listener * @param chain 'test' | 'main' * @param idleWait without any input, after this many milliseconds, assume dead service and exit. * @returns true only if exit caused by `stop` */ async function WocHeadersLiveListener(enqueue, error, stop, chain, logger, idleWait = 100000) { let count = 0; let ok = false; let done = false; let wsIsOpen = false; let msecsWithoutPing = 0; function stopNow() { ok = true; if (wsIsOpen) { wsIsOpen = false; ws.close(); } else done = true; } stop.stop = stopNow; let webSocketUrl; switch (chain) { case 'test': case 'teratest': webSocketUrl = 'wss://socket-v2-testnet.whatsonchain.com/websocket/blockHeaders'; break; case 'main': webSocketUrl = 'wss://socket-v2.whatsonchain.com/websocket/blockHeaders'; break; case 'mock': throw new Error(`WocHeadersLiveListener does not support 'mock' chain.`); } function processData(rawData) { var _a, _b; if (rawData.length === 0) { // Ping return; } // rawData may be a Buffer... const data = JSON.parse(rawData); const json = JSON.stringify(data); switch (data.type || 0) { case 0: // "{\"channel\":\"woc#c91c044b-66f5-4172-b509-82c11a41d7fc\",\"data\":{\"data\":{\"hash\":\"00000000000000000ce16106e2100b893f3da5989ef23cbac52eb568cf803dc1\",\"confirmations\":8,\"size\":3345869,\"height\":779890,\"version\":939515904,\"versionHex\":\"37ffe000\",\"merkleroot\":\"92e3d1d72d432c9216d296839eaa675b6ffb40ccf6e1a02229caf3d769dc0a5f\",\"txcount\":10757,\"time\":1676851109,\"mediantime\":1676849009,\"nonce\":4216995243,\"bits\":\"180e38cb\",\"difficulty\":77310268438.5808,\"chainwork\":\"00000000000000000000000000000000000000000142d5cf97c0ed497eae1066\",\"previousblockhash\":\"0000000000000000053d26b7b35a76bf2b672428bae591bc033ae99f9873697a\",\"nextblockhash\":\"000000000000000000e1a110376966033507953d50ade86eadef7226843f3349\"}}}" // last wocHeader has empty string for nextBlockHash // eslint-disable-next-line no-case-declarations if (!done) { if (json === '{}') { ws.send('ping'); } else { //logger(JSON.stringify(data)) const wocHeader = ((_a = data.pub) === null || _a === void 0 ? void 0 : _a.data) || ((_b = data.data) === null || _b === void 0 ? void 0 : _b.data); if (wocHeader) { const header = (0, WhatsOnChain_1.convertWocToBlockHeaderHex)(wocHeader); enqueue(header); count++; } else { error(42, `unknown data ${json}`); } } } break; case 3: break; // Unsubscribe "{\"type\":3,\"channel\":\"woc#c91c044b-66f5-4172-b509-82c11a41d7fc\",\"data\":{}}" case 5: break; // console.log('subscribed to a channel ' + data.channel) case 6: break; // subscribe "{\"type\":6,\"data\":{\"client\":\"c91c044b-66f5-4172-b509-82c11a41d7fc\",\"subs\":{\"woc#c91c044b-66f5-4172-b509-82c11a41d7fc\":{}},\"session\":\"d8693511-5bc8-4815-beb6-8ae1bdaaf3a6\"}}" case 7: // If you don't close after unsubscribe or end of headers, after a delay, this is received. // "{\"type\":7,\"data\":{\"code\":200,\"reason\":\"Data Delivered\"}}" error(data.data.code, JSON.stringify(data.data)); ws.close(); break; default: error(42, `unknown data ${json}`); ws.close(); } } const ws = new ws_1.default(webSocketUrl); /* function startPing() { const pingInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send('ping'); } }, 30000); // Ping every 30 seconds } */ ws.onopen = function (evt) { //logger(`WoC ${new Date().toISOString()} listening for new headers`) // This is required to trigger connect on server side. ws.send(JSON.stringify({})); wsIsOpen = true; }; ws.onerror = function (evt) { // if (evt.message.startsWith('getaddrinfo ENOTFOUND')) { let code = -1; switch (evt.message) { case 'Unexpected server response: 403': code = 403; break; case 'Unexpected server response: 404': code = 404; break; default: break; } const ignoreError = error(code, evt.message); if (!ignoreError) ws.close(); }; ws.onclose = function (ev) { // ok should be true if we got the last header, false otherwise. // error will have been called with anything abnormal... //logger(`WoC ${new Date().toISOString()} listening for new headers service closed`) done = true; }; ws.onmessage = function (ev) { // console.log("onmessage:", JSON.stringify(ev.data)); processData(ev.data); msecsWithoutPing = 0; }; //startPing() const waitMsecs = 1000; let sinceLastSentPing = 0; while (!done) { await (0, utilityHelpers_1.wait)(waitMsecs); msecsWithoutPing += waitMsecs; sinceLastSentPing += waitMsecs; if (sinceLastSentPing > 10000) { ws.send('ping'); sinceLastSentPing = 0; } } return ok; } async function WocHeadersLiveListener_test() { try { const chain = 'main'; const stop = { stop: undefined }; let errors = 0; while (errors === 0 && !(await WocHeadersLiveListener(header => console.log(JSON.stringify(header)), (code, message) => { errors++; console.log(`code ${code} message ${message}`); return true; }, stop, chain, console.log.bind(console), 100000))) { console.log('restarting WoC addHeaders'); } } catch (err) { console.log(err); } } /** * v1 json data format: * { "hash": "0000000000000000010cb155a44577ff3541940ec4355c026dc2706fb7e261d6", "confirmations": 4, "size": 1229956, "height": 676421, "version": 541065216, "versionHex": "20400000", "merkleroot": "e2f64e87c0362314b65dc28b4367c41d904c22b891b652951779336ef735386b", "txcount": 654, "time": 1614534798, "mediantime": 1614527058, "nonce": 610133701, "bits": "180d359d", "difficulty": 83235621087.7299, "chainwork": "00000000000000000000000000000000000000000127d3854a414d8ec8f53190", "previousblockhash": "00000000000000000428ca33597675bd85f5a2ef464ba25e4db7b4a74d898d33", "nextblockhash": "00000000000000000cb40c1d850dece5cd3747e08a1e4066ef887251388c4ae6", "coinbaseTx": { "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff410345520a2f7461616c2e636f6d2f506c656173652070617920302e3520736174732f627974652c20696e666f407461616c2e636f6d6eead6b0ad91f2ce86be0400ffffffff0176674325000000001976a9148e9170be3f733a9773c907517fb9b786f1c884c688ac00000000", "txid": "13686a10870d23b4c94642bf0b78d6630e2640fc0de83bc30af835d96bb17482", "hash": "13686a10870d23b4c94642bf0b78d6630e2640fc0de83bc30af835d96bb17482", "size": 150, "version": 1, "locktime": 0, "vin": [ { "coinbase": "0345520a2f7461616c2e636f6d2f506c656173652070617920302e3520736174732f627974652c20696e666f407461616c2e636f6d6eead6b0ad91f2ce86be0400", "sequence": 4294967295 } ], "vout": [ { "value": 6.2517439, "n": 0, "scriptPubKey": { "asm": "OP_DUP OP_HASH160 8e9170be3f733a9773c907517fb9b786f1c884c6 OP_EQUALVERIFY OP_CHECKSIG", "hex": "76a9148e9170be3f733a9773c907517fb9b786f1c884c688ac", "reqSigs": 1, "type": "pubkeyhash", "addresses": [ "1DzqBck9oyCBzxJbbje2s15deZis6BeATi" ], "isTruncated": false } } ], "blockhash": "0000000000000000010cb155a44577ff3541940ec4355c026dc2706fb7e261d6", "confirmations": 4, "time": 1614534798, "blocktime": 1614534798 } } */ /** * v2 json data format: { "channel": "woc:blockHeader", "pub": { "data": { "hash": "000000000000000007405fd882e9d4bf3d7b2388010081fe443ee1e323d8a668", "confirmations": 1, "size": 368170789, "height": 892466, "version": 939524096, "versionHex": "38000000", "merkleroot": "b0ac43a40a42775d8f533a8eda00bed3257e12a5fca3c8cade47188e6b7aa382", "txcount": 9230, "time": 1744648860, "mediantime": 1744645401, "nonce": 2893086732, "bits": "181246e3", "difficulty": 60157618395.71893, "chainwork": "00000000000000000000000000000000000000000164d487adbc2b84d47d2831", "previousblockhash": "0000000000000000090fa4de4c971b0b3609acc1061b2020d22dc8d8bf72746a", "nextblockhash": "", "coinbaseTx": { "hex": "...", "txid": "1932aa0229b6b44be4b749b9e7279c02136374e2e77b4e890db761f8e74d2111", "hash": "1932aa0229b6b44be4b749b9e7279c02136374e2e77b4e890db761f8e74d2111", "size": 111, "version": 1, "locktime": 0, "vin": [ { "n": 0, "coinbase": "03329e0d2f7461616c2e636f6d2fb9df41e723a18d95e09d0200", "sequence": 4294967295 } ], "vout": [ { "value": 3.13276008, "n": 0, "scriptPubKey": { "asm": "OP_DUP OP_HASH160 522cf9e7626d9bd8729e5a1398ece40dad1b6a2f OP_EQUALVERIFY OP_CHECKSIG", "hex": "...", "reqSigs": 1, "type": "pubkeyhash", "addresses": [ "18VWHjMt4ixHddPPbs6righWTs3Sg2QNcn" ], "isTruncated": false }, "scripthash": "1981f116576960fab6ac9a71ba3c12e4f98c2d9b34532bdb40f2e8cc03337fa2" } ], "blockhash": "000000000000000007405fd882e9d4bf3d7b2388010081fe443ee1e323d8a668", "confirmations": 1, "time": 1744648860, "blocktime": 1744648860, // New properties in v2 "blockheight": 892466, "vincount": 1, "voutcount": 1, "voutvalue": 3.13276008 }, "totalFees": 0.007760080000000169 } } } */ //# sourceMappingURL=WhatsOnChainIngestorWs.js.map