Skip to content

Commit

Permalink
Add serverConfig to EngineInfo with fallback servers
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
samholmes committed Apr 5, 2024
1 parent aeced0f commit 0990871
Show file tree
Hide file tree
Showing 13 changed files with 203 additions and 42 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/common/plugin/CurrencyPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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({
Expand All @@ -43,6 +46,7 @@ export function makeCurrencyPlugin(
pluginInfo,
pluginDisklet,
currencyTools,
initOptions: asUtxoInitOptions(initOptions),
io,
options: {
...pluginOptions,
Expand Down
8 changes: 8 additions & 0 deletions src/common/plugin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -70,6 +71,7 @@ export interface EngineInfo {
mempoolSpaceFeeInfoServer?: string
defaultFeeInfo: FeeInfo
scriptTemplates?: ScriptTemplates
serverConfigs?: ServerConfig[]
// Codec Cleaners
asBlockbookAddress?: Cleaner<string>
// Coin specific transaction handling
Expand All @@ -79,6 +81,11 @@ export interface EngineInfo {
) => IProcessorTransaction
}

export interface ServerConfig {
type: 'blockbook-nownode'
uris: string[]
}

/**
* Coin Info
*/
Expand Down Expand Up @@ -196,6 +203,7 @@ export interface EngineConfig {
pluginDisklet: Disklet
currencyTools: EdgeCurrencyTools
options: EngineOptions
initOptions: UtxoInitOptions
io: EdgeIo
pluginState: PluginState
}
Expand Down
165 changes: 130 additions & 35 deletions src/common/utxobased/engine/ServerStates.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand All @@ -24,6 +29,8 @@ interface ServerState {

interface ServerStateConfig {
engineEmitter: EngineEmitter
initOptions: UtxoInitOptions
io: EdgeIo
log: EdgeLog
pluginInfo: PluginInfo
pluginState: PluginState
Expand Down Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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<
Expand All @@ -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
Expand Down Expand Up @@ -304,38 +320,117 @@ export function makeServerStates(config: ServerStateConfig): ServerStates {

const broadcastTx = async (transaction: EdgeTransaction): Promise<string> => {
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)
}
})
}
}
})
}
Expand Down
15 changes: 10 additions & 5 deletions src/common/utxobased/engine/UtxoEngineState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -178,6 +181,8 @@ export function makeUtxoEngineState(

const serverStates = makeServerStates({
engineEmitter: emitter,
initOptions,
io,
log,
pluginInfo,
pluginState,
Expand All @@ -191,7 +196,7 @@ export function makeUtxoEngineState(
emitter,
taskCache,
updateProgressRatio,
io: config.io,
io,
log,
serverStates,
pluginState,
Expand Down
5 changes: 5 additions & 0 deletions src/common/utxobased/engine/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof asUtxoInitOptions>
export const asUtxoInitOptions = asObject({
nowNodeApiKey: asOptional(asString)
})

export const asUtxoUserSettings = asObject({
blockbookServers: asMaybe(asArray(asString), []),
enableCustomServers: asMaybe(asBoolean, false)
Expand Down
6 changes: 6 additions & 0 deletions src/common/utxobased/info/bitcoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/common/utxobased/info/bitcoincash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/common/utxobased/info/dash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/common/utxobased/info/digibyte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 0990871

Please sign in to comment.