From 5b2f4d4d808b9263159f6fc796cf24ef8c0f8877 Mon Sep 17 00:00:00 2001 From: Samuel Holmes Date: Wed, 3 Apr 2024 16:09:53 -0700 Subject: [PATCH] Add serverConfig to EngineInfo with fallback servers This infra is similar to accountbased currencies in that we maintain a list of server configs that can match with a specific key from initOptions. The only difference with this implementation is that there is no info-server support for this integration yet. These servers will only be used as fallback servers if WebSocket broadcasts fail. --- CHANGELOG.md | 2 + src/common/plugin/CurrencyPlugin.ts | 8 +- src/common/plugin/types.ts | 8 + src/common/utxobased/engine/ServerStates.ts | 165 ++++++++++++++---- .../utxobased/engine/UtxoEngineState.ts | 15 +- src/common/utxobased/engine/types.ts | 5 + src/common/utxobased/info/bitcoin.ts | 6 + src/common/utxobased/info/bitcoincash.ts | 6 + src/common/utxobased/info/dash.ts | 6 + src/common/utxobased/info/digibyte.ts | 6 + src/common/utxobased/info/dogecoin.ts | 6 + src/common/utxobased/info/groestlcoin.ts | 6 + src/common/utxobased/info/litecoin.ts | 6 + 13 files changed, 203 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c74c9d3c..302e6428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- added: New fallback server engine info, starting with NOWNodes blockbook servers + ## 2.5.4 (2024-01-26) - fixed: Check for NaN target totals in `subtractFee` style transactions diff --git a/src/common/plugin/CurrencyPlugin.ts b/src/common/plugin/CurrencyPlugin.ts index 4c46f44e..6c483339 100644 --- a/src/common/plugin/CurrencyPlugin.ts +++ b/src/common/plugin/CurrencyPlugin.ts @@ -7,7 +7,10 @@ import { EdgeWalletInfo } from 'edge-core-js/types' -import { asUtxoUserSettings } from '../utxobased/engine/types' +import { + asUtxoInitOptions, + asUtxoUserSettings +} from '../utxobased/engine/types' import { makeUtxoEngine } from '../utxobased/engine/UtxoEngine' import { makeCurrencyTools } from './CurrencyTools' import { makeEngineEmitter } from './EngineEmitter' @@ -19,7 +22,7 @@ export function makeCurrencyPlugin( pluginInfo: PluginInfo ): EdgeCurrencyPlugin { const { currencyInfo } = pluginInfo - const { io, log, pluginDisklet } = pluginOptions + const { io, log, pluginDisklet, initOptions } = pluginOptions const currencyTools = makeCurrencyTools(io, pluginInfo) const { defaultSettings, pluginId, currencyCode } = currencyInfo const pluginState = makePluginState({ @@ -43,6 +46,7 @@ export function makeCurrencyPlugin( pluginInfo, pluginDisklet, currencyTools, + initOptions: asUtxoInitOptions(initOptions), io, options: { ...pluginOptions, diff --git a/src/common/plugin/types.ts b/src/common/plugin/types.ts index 4884f66f..0b56561c 100644 --- a/src/common/plugin/types.ts +++ b/src/common/plugin/types.ts @@ -23,6 +23,7 @@ import { import * as wif from 'wif' import { asIUTXO, IProcessorTransaction, IUTXO } from '../utxobased/db/types' +import { UtxoInitOptions } from '../utxobased/engine/types' import { ScriptTemplates } from '../utxobased/info/scriptTemplates/types' import { UtxoPicker } from '../utxobased/keymanager/utxopicker' import { EngineEmitter } from './EngineEmitter' @@ -70,6 +71,7 @@ export interface EngineInfo { mempoolSpaceFeeInfoServer?: string defaultFeeInfo: FeeInfo scriptTemplates?: ScriptTemplates + serverConfigs?: ServerConfig[] // Codec Cleaners asBlockbookAddress?: Cleaner // Coin specific transaction handling @@ -79,6 +81,11 @@ export interface EngineInfo { ) => IProcessorTransaction } +export interface ServerConfig { + type: 'blockbook-nownode' + uris: string[] +} + /** * Coin Info */ @@ -196,6 +203,7 @@ export interface EngineConfig { pluginDisklet: Disklet currencyTools: EdgeCurrencyTools options: EngineOptions + initOptions: UtxoInitOptions io: EdgeIo pluginState: PluginState } diff --git a/src/common/utxobased/engine/ServerStates.ts b/src/common/utxobased/engine/ServerStates.ts index 9aaa9a5e..bce2c2d6 100644 --- a/src/common/utxobased/engine/ServerStates.ts +++ b/src/common/utxobased/engine/ServerStates.ts @@ -1,4 +1,4 @@ -import { EdgeLog, EdgeTransaction } from 'edge-core-js/types' +import { EdgeIo, EdgeLog, EdgeTransaction } from 'edge-core-js/types' import { parse } from 'uri-js' import { EngineEmitter, EngineEvent } from '../../plugin/EngineEmitter' @@ -7,12 +7,17 @@ import { PluginInfo } from '../../plugin/types' import { removeItem } from '../../plugin/utils' import { SafeWalletInfo } from '../keymanager/cleaners' import { Blockbook, makeBlockbook } from '../network/Blockbook' -import { SubscribeAddressResponse } from '../network/blockbookApi' +import { + asBlockbookResponse, + asBroadcastTxResponse, + SubscribeAddressResponse +} from '../network/blockbookApi' import Deferred from '../network/Deferred' import { WsTask } from '../network/Socket' import { SocketEmitter, SocketEvent } from '../network/SocketEmitter' import { pushUpdate, removeIdFromQueue } from '../network/socketQueue' import { MAX_CONNECTIONS, NEW_CONNECTIONS } from './constants' +import { UtxoInitOptions } from './types' interface ServerState { blockbook: Blockbook @@ -24,6 +29,8 @@ interface ServerState { interface ServerStateConfig { engineEmitter: EngineEmitter + initOptions: UtxoInitOptions + io: EdgeIo log: EdgeLog pluginInfo: PluginInfo pluginState: PluginState @@ -58,7 +65,16 @@ interface ServerStatesCache { } export function makeServerStates(config: ServerStateConfig): ServerStates { - const { engineEmitter, log, pluginInfo, pluginState, walletInfo } = config + const { + engineEmitter, + initOptions, + io, + log, + pluginInfo, + pluginState, + walletInfo + } = config + const { serverConfigs = [] } = pluginInfo.engineInfo log('Making server states') const serverStatesCache: ServerStatesCache = {} @@ -209,8 +225,8 @@ export function makeServerStates(config: ServerStateConfig): ServerStates { // Make new Blockbook instance const blockbook = makeBlockbook({ + asAddress: pluginInfo.engineInfo.asBlockbookAddress, connectionUri: uri, - socketEmitter, engineEmitter, log, onQueueSpaceCB: async (): Promise< @@ -229,8 +245,8 @@ export function makeServerStates(config: ServerStateConfig): ServerStates { } return task }, - walletId: walletInfo.id, - asAddress: pluginInfo.engineInfo.asBlockbookAddress + socketEmitter, + walletId: walletInfo.id }) // Make new ServerStates instance @@ -304,38 +320,117 @@ export function makeServerStates(config: ServerStateConfig): ServerStates { const broadcastTx = async (transaction: EdgeTransaction): Promise => { return await new Promise((resolve, reject) => { - const uris = Object.keys(serverStatesCache).filter(uri => { - const { blockbook } = serverStatesCache[uri] - if (blockbook == null) return false - return blockbook.isConnected - }) - if (uris == null || uris.length < 1) { - reject( - new Error('No available connections\nCheck your internet signal') - ) - } let resolved = false let bad = 0 - for (const uri of uris) { - const { blockbook } = serverStatesCache[uri] - if (blockbook == null) continue - blockbook - .broadcastTx(transaction) - .then(response => { - if (!resolved) { - resolved = true - resolve(response.result) - } - }) - .catch((e?: Error) => { - if (++bad === uris.length) { - const msg = e != null ? `With error ${e.message}` : '' - log.error( - `broadcastTx fail: ${JSON.stringify(transaction)}\n${msg}` - ) - reject(e) - } + + const wsUris = Object.keys(serverStatesCache).filter( + uri => serverStatesCache[uri].blockbook != null + ) + + // If there are no blockbook instances, reject the promise + if (wsUris.length < 1) { + reject(new Error('Unexpected error. Missing WebSocket connections.')) + // Exit early if there are blockbook instances + return + } + + // Determine if there are any connected blockbook instances + const isAnyBlockbookConnected = wsUris.some( + uri => serverStatesCache[uri].blockbook.isConnected + ) + + if (isAnyBlockbookConnected) { + for (const uri of wsUris) { + const { blockbook } = serverStatesCache[uri] + if (blockbook == null) continue + blockbook + .broadcastTx(transaction) + .then(response => { + if (!resolved) { + resolved = true + resolve(response.result) + } + }) + .catch((e?: Error) => { + if (++bad === wsUris.length) { + const msg = e != null ? `With error ${e.message}` : '' + log.error( + `broadcastTx fail: ${JSON.stringify(transaction)}\n${msg}` + ) + reject(e) + } + }) + } + } + + // Broadcast through any HTTP URI that may be configured, only if no + // blockbook instances are connected. + if (!isAnyBlockbookConnected) { + // This is for the future when we want to HTTP servers from the user + // settings: + // const httpUris = pluginState.getLocalServers(Infinity, [ + // /^http(?:s)?:/i + // ]) + + const { nowNodeApiKey } = initOptions + const nowNodeUris = serverConfigs + .filter(config => config.type === 'blockbook-nownode') + .map(config => config.uris) + .flat(1) + + // If there are no HTTP servers, reject the promise + if (nowNodeUris.length < 1) { + // If no HTTP servers are available, and we had no connected blockbook + // instances, reject the promise with a message indicating no + // available connections. It's clear we have some connection instances + // if we gotten to this point, but we just don't have any of those + // instances connected at this time. + reject( + new Error('No available connections. Check your internet signal.') + ) + return + } + + // If there is no key for the NowNode servers: + if (nowNodeApiKey == null) { + reject(new Error('Missing connection key for fallback servers.')) + return + } + + for (const uri of nowNodeUris) { + // HTTP Fallback + io.fetchCors(`${uri}/api/v2/sendtx/`, { + method: 'POST', + headers: { + 'api-key': nowNodeApiKey + }, + body: transaction.signedTx }) + .then(async response => { + if (!response.ok) { + throw new Error( + `Failed to broadcast transaction via Blockbook: HTTP ${response.status}` + ) + } + const json = await response.json() + return asBlockbookResponse(asBroadcastTxResponse)(json) + }) + .then(response => { + if (!resolved) { + resolved = true + resolve(response.result) + } + }) + .catch((e?: Error) => { + if (++bad === nowNodeUris.length) { + const msg = e != null ? `With error ${e.message}` : '' + log.error( + `broadcastTx fail: ${JSON.stringify(transaction)}\n${msg}` + ) + reject(e) + } + }) + } } }) } diff --git a/src/common/utxobased/engine/UtxoEngineState.ts b/src/common/utxobased/engine/UtxoEngineState.ts index 31dc4148..999e7b3a 100644 --- a/src/common/utxobased/engine/UtxoEngineState.ts +++ b/src/common/utxobased/engine/UtxoEngineState.ts @@ -95,13 +95,16 @@ export function makeUtxoEngineState( config: UtxoEngineStateConfig ): UtxoEngineState { const { + initOptions, + io, + options, + pluginState, pluginInfo, - walletInfo, - walletTools, - options: { emitter, log }, processor, - pluginState + walletInfo, + walletTools } = config + const { emitter, log } = options const { walletFormats } = walletInfo.keys @@ -178,6 +181,8 @@ export function makeUtxoEngineState( const serverStates = makeServerStates({ engineEmitter: emitter, + initOptions, + io, log, pluginInfo, pluginState, @@ -191,7 +196,7 @@ export function makeUtxoEngineState( emitter, taskCache, updateProgressRatio, - io: config.io, + io, log, serverStates, pluginState, diff --git a/src/common/utxobased/engine/types.ts b/src/common/utxobased/engine/types.ts index 2549f59f..ff1980f1 100644 --- a/src/common/utxobased/engine/types.ts +++ b/src/common/utxobased/engine/types.ts @@ -12,6 +12,11 @@ import { EdgeSpendInfo } from 'edge-core-js/types' import { asTxOptions } from '../../plugin/types' import { Input, Output } from '../keymanager/utxopicker/types' +export type UtxoInitOptions = ReturnType +export const asUtxoInitOptions = asObject({ + nowNodeApiKey: asOptional(asString) +}) + export const asUtxoUserSettings = asObject({ blockbookServers: asMaybe(asArray(asString), []), enableCustomServers: asMaybe(asBoolean, false) diff --git a/src/common/utxobased/info/bitcoin.ts b/src/common/utxobased/info/bitcoin.ts index 9a9ef6c6..09d71ce1 100644 --- a/src/common/utxobased/info/bitcoin.ts +++ b/src/common/utxobased/info/bitcoin.ts @@ -54,6 +54,12 @@ const currencyInfo: EdgeCurrencyInfo = { } const engineInfo: EngineInfo = { + serverConfigs: [ + { + type: 'blockbook-nownode', + uris: ['btcbook.nownodes.io'] + } + ], formats: ['bip49', 'bip84', 'bip44', 'bip32'], forks: ['bitcoincash', 'bitcoingold'], gapLimit: 25, diff --git a/src/common/utxobased/info/bitcoincash.ts b/src/common/utxobased/info/bitcoincash.ts index 7bfe1bbc..ea92c292 100644 --- a/src/common/utxobased/info/bitcoincash.ts +++ b/src/common/utxobased/info/bitcoincash.ts @@ -56,6 +56,12 @@ const currencyInfo: EdgeCurrencyInfo = { } const engineInfo: EngineInfo = { + serverConfigs: [ + { + type: 'blockbook-nownode', + uris: ['bchbook.nownodes.io'] + } + ], formats: ['bip44', 'bip32'], forks: [], // 'bitcoinsv' is currently disabled, so not included in the forks gapLimit: 10, diff --git a/src/common/utxobased/info/dash.ts b/src/common/utxobased/info/dash.ts index b369d256..4c767e2d 100644 --- a/src/common/utxobased/info/dash.ts +++ b/src/common/utxobased/info/dash.ts @@ -49,6 +49,12 @@ const currencyInfo: EdgeCurrencyInfo = { } const engineInfo: EngineInfo = { + serverConfigs: [ + { + type: 'blockbook-nownode', + uris: ['dashbook.nownodes.io'] + } + ], formats: ['bip44', 'bip32'], gapLimit: 10, defaultFee: 10000, diff --git a/src/common/utxobased/info/digibyte.ts b/src/common/utxobased/info/digibyte.ts index d1873ce8..462ce35c 100644 --- a/src/common/utxobased/info/digibyte.ts +++ b/src/common/utxobased/info/digibyte.ts @@ -40,6 +40,12 @@ const currencyInfo: EdgeCurrencyInfo = { } const engineInfo: EngineInfo = { + serverConfigs: [ + { + type: 'blockbook-nownode', + uris: ['dgbbook.nownodes.io'] + } + ], formats: ['bip49', 'bip84', 'bip44', 'bip32'], forks: [], gapLimit: 10, diff --git a/src/common/utxobased/info/dogecoin.ts b/src/common/utxobased/info/dogecoin.ts index 6550afb9..968d0cd8 100644 --- a/src/common/utxobased/info/dogecoin.ts +++ b/src/common/utxobased/info/dogecoin.ts @@ -45,6 +45,12 @@ const currencyInfo: EdgeCurrencyInfo = { } const engineInfo: EngineInfo = { + serverConfigs: [ + { + type: 'blockbook-nownode', + uris: ['https://dogebook.nownodes.io'] + } + ], formats: ['bip44', 'bip32'], gapLimit: 10, defaultFee: 1000, diff --git a/src/common/utxobased/info/groestlcoin.ts b/src/common/utxobased/info/groestlcoin.ts index 93a99353..25c89ee9 100644 --- a/src/common/utxobased/info/groestlcoin.ts +++ b/src/common/utxobased/info/groestlcoin.ts @@ -46,6 +46,12 @@ const currencyInfo: EdgeCurrencyInfo = { } const engineInfo: EngineInfo = { + serverConfigs: [ + { + type: 'blockbook-nownode', + uris: ['grsbook.nownodes.io'] + } + ], formats: ['bip49', 'bip84', 'bip44', 'bip32'], gapLimit: 10, defaultFee: 100000, diff --git a/src/common/utxobased/info/litecoin.ts b/src/common/utxobased/info/litecoin.ts index ce029d3b..119c38da 100644 --- a/src/common/utxobased/info/litecoin.ts +++ b/src/common/utxobased/info/litecoin.ts @@ -52,6 +52,12 @@ export const currencyInfo: EdgeCurrencyInfo = { } export const engineInfo: EngineInfo = { + serverConfigs: [ + { + type: 'blockbook-nownode', + uris: ['ltcbook.nownodes.io'] + } + ], formats: ['bip49', 'bip84', 'bip44', 'bip32'], gapLimit: 10, defaultFee: 50000,