feat: Bitcoin API client, balance reading, and node health checks

Adds a mempool.space-style REST client (RealBitcoinApi, with the same
retry-on-429 pattern as TronGridApi), a polling AssetBalance implementation,
and BitcoinNodeHealthStateTester so the Networks screen can show live
health for Bitcoin nodes (mirrors TronNodeHealthStateTester's rationale:
mempool.space speaks plain REST, not JSON-RPC, so the existing Ethereum/
Substrate testers can't be reused).

Wires Chain.Asset.Type.BitcoinNative through the asset-type mappers and
TypeBasedAssetSourceRegistry, and BitcoinAssetsModule into AssetsModule -
transfers/history stay on the Unsupported stubs until send support lands.
This commit is contained in:
2026-07-12 14:59:05 -07:00
parent db1476f119
commit 69b311cb47
17 changed files with 336 additions and 7 deletions
@@ -0,0 +1,42 @@
package io.novafoundation.nova.feature_wallet_impl.data.network.bitcoin
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import kotlinx.coroutines.delay
import retrofit2.HttpException
import java.math.BigInteger
interface BitcoinApi {
suspend fun fetchNativeBalance(baseUrl: String, address: String): Balance
}
class RealBitcoinApi(
private val retrofitApi: RetrofitBitcoinApi
) : BitcoinApi {
override suspend fun fetchNativeBalance(baseUrl: String, address: String): Balance {
val stats = retryOn429 { retrofitApi.getAddress(addressUrl(baseUrl, address)) }.chainStats
?: return BigInteger.ZERO
val funded = stats.fundedTxoSum ?: 0L
val spent = stats.spentTxoSum ?: 0L
return (funded - spent).toBigInteger().coerceAtLeast(BigInteger.ZERO)
}
private fun addressUrl(baseUrl: String, address: String): String {
return "${baseUrl.trimEnd('/')}/address/$address"
}
private suspend fun <T> retryOn429(maxAttempts: Int = 4, block: suspend () -> T): T {
repeat(maxAttempts - 1) { attempt ->
try {
return block()
} catch (e: HttpException) {
if (e.code() != 429) throw e
delay(1_000L * (attempt + 1))
}
}
return block()
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_wallet_impl.data.network.bitcoin
import io.novafoundation.nova.common.data.network.UserAgent
import io.novafoundation.nova.feature_wallet_impl.data.network.bitcoin.model.BitcoinAddressResponse
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Url
interface RetrofitBitcoinApi {
@GET
@Headers(UserAgent.NOVA)
suspend fun getAddress(@Url url: String): BitcoinAddressResponse
}
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_wallet_impl.data.network.bitcoin.model
import com.google.gson.annotations.SerializedName
/**
* Response shape of mempool.space's `GET /address/{address}`.
*
* `chainStats` reflects only confirmed on-chain activity; `mempoolStats` (unconfirmed) is deliberately not
* used for balance - matching the exchange's proven 2-confirmation-required posture for this same API.
*/
class BitcoinAddressResponse(
@SerializedName("chain_stats")
val chainStats: BitcoinAddressStats? = null,
)
class BitcoinAddressStats(
@SerializedName("funded_txo_sum")
val fundedTxoSum: Long? = null,
@SerializedName("spent_txo_sum")
val spentTxoSum: Long? = null,
)
@@ -31,6 +31,7 @@ class TypeBasedAssetSourceRegistry(
private val equilibriumAssetSource: Lazy<AssetSource>, private val equilibriumAssetSource: Lazy<AssetSource>,
private val tronNativeSource: Lazy<AssetSource>, private val tronNativeSource: Lazy<AssetSource>,
private val trc20Source: Lazy<AssetSource>, private val trc20Source: Lazy<AssetSource>,
private val bitcoinNativeSource: Lazy<AssetSource>,
private val unsupportedBalanceSource: AssetSource, private val unsupportedBalanceSource: AssetSource,
private val nativeAssetEventDetector: NativeAssetEventDetector, private val nativeAssetEventDetector: NativeAssetEventDetector,
@@ -49,6 +50,7 @@ class TypeBasedAssetSourceRegistry(
is Chain.Asset.Type.Equilibrium -> equilibriumAssetSource.get() is Chain.Asset.Type.Equilibrium -> equilibriumAssetSource.get()
is Chain.Asset.Type.TronNative -> tronNativeSource.get() is Chain.Asset.Type.TronNative -> tronNativeSource.get()
is Chain.Asset.Type.Trc20 -> trc20Source.get() is Chain.Asset.Type.Trc20 -> trc20Source.get()
is Chain.Asset.Type.BitcoinNative -> bitcoinNativeSource.get()
Chain.Asset.Type.Unsupported -> unsupportedBalanceSource Chain.Asset.Type.Unsupported -> unsupportedBalanceSource
} }
} }
@@ -63,6 +65,7 @@ class TypeBasedAssetSourceRegistry(
add(equilibriumAssetSource.get()) add(equilibriumAssetSource.get())
add(tronNativeSource.get()) add(tronNativeSource.get())
add(trc20Source.get()) add(trc20Source.get())
add(bitcoinNativeSource.get())
} }
} }
@@ -72,6 +75,7 @@ class TypeBasedAssetSourceRegistry(
Chain.Asset.Type.EvmNative, Chain.Asset.Type.EvmNative,
is Chain.Asset.Type.TronNative, is Chain.Asset.Type.TronNative,
is Chain.Asset.Type.Trc20, is Chain.Asset.Type.Trc20,
is Chain.Asset.Type.BitcoinNative,
Chain.Asset.Type.Unsupported -> UnsupportedEventDetector() Chain.Asset.Type.Unsupported -> UnsupportedEventDetector()
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.bitcoinNative
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
private const val BITCOIN_BALANCE_POLLING_INTERVAL_MS = 30_000L
/**
* mempool.space is a plain REST API with no push/subscription mechanism, same as TronGrid - see
* `TronBalancePolling.pollingBalanceFlow`'s doc for the full rationale this mirrors.
*/
internal fun pollingBalanceFlow(
intervalMs: Long = BITCOIN_BALANCE_POLLING_INTERVAL_MS,
fetch: suspend () -> Balance
): Flow<Balance> = flow {
var lastEmitted: Balance? = null
while (true) {
val latest = fetch()
if (latest != lastEmitted) {
lastEmitted = latest
emit(latest)
}
delay(intervalMs)
}
}
@@ -0,0 +1,83 @@
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.bitcoinNative
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
import io.novafoundation.nova.feature_wallet_api.data.cache.updateNonLockableAsset
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.ChainAssetBalance
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdatePoint
import io.novafoundation.nova.feature_wallet_impl.data.network.bitcoin.BitcoinApi
import io.novafoundation.nova.runtime.ext.addressOf
import io.novafoundation.nova.runtime.ext.requireMempoolSpaceBaseUrl
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import java.math.BigInteger
/**
* Native BTC balance on a Bitcoin-based chain. Read-only (Phase 4): fetches via a mempool.space-style REST API
* and polls for updates, since that API has no push/subscription mechanism. No transfer/history support here -
* see `BitcoinAssetsModule` for how this is paired with `UnsupportedAssetTransfers`/`UnsupportedAssetHistory`.
*/
class BitcoinNativeAssetBalance(
private val assetCache: AssetCache,
private val bitcoinApi: BitcoinApi,
) : AssetBalance {
override suspend fun startSyncingBalanceLocks(
metaAccount: MetaAccount,
chain: Chain,
chainAsset: Chain.Asset,
accountId: AccountId,
subscriptionBuilder: SharedRequestsBuilder
): Flow<*> {
// Bitcoin native balance does not support locks
return emptyFlow<Nothing>()
}
override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean {
return true
}
override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger {
// Bitcoin does not have an existential deposit concept (UTXO dust limit is enforced at the transfer level, not here)
return BigInteger.ZERO
}
override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
val balance = bitcoinApi.fetchNativeBalance(chain.requireMempoolSpaceBaseUrl(), chain.addressOf(accountId))
return ChainAssetBalance.fromFree(chainAsset, balance)
}
override suspend fun subscribeAccountBalanceUpdatePoint(
chain: Chain,
chainAsset: Chain.Asset,
accountId: AccountId,
): Flow<TransferableBalanceUpdatePoint> {
// Not on the critical sync path (mirrors TronNativeAssetBalance/EvmNativeAssetBalance) - out of scope for Phase 4.
TODO("Not yet implemented")
}
override suspend fun startSyncingBalance(
chain: Chain,
chainAsset: Chain.Asset,
metaAccount: MetaAccount,
accountId: AccountId,
subscriptionBuilder: SharedRequestsBuilder
): Flow<BalanceSyncUpdate> {
val baseUrl = chain.requireMempoolSpaceBaseUrl()
val address = chain.addressOf(accountId)
return pollingBalanceFlow { bitcoinApi.fetchNativeBalance(baseUrl, address) }
.map { balance ->
assetCache.updateNonLockableAsset(metaAccount.id, chainAsset, balance)
BalanceSyncUpdate.NoCause
}
}
}
@@ -22,6 +22,7 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets
EvmNativeAssetsModule::class, EvmNativeAssetsModule::class,
EquilibriumAssetsModule::class, EquilibriumAssetsModule::class,
TronAssetsModule::class, TronAssetsModule::class,
BitcoinAssetsModule::class,
UnsupportedAssetsModule::class UnsupportedAssetsModule::class
] ]
) )
@@ -38,6 +39,7 @@ class AssetsModule {
@EquilibriumAsset equilibrium: Lazy<AssetSource>, @EquilibriumAsset equilibrium: Lazy<AssetSource>,
@TronNativeAssets tronNative: Lazy<AssetSource>, @TronNativeAssets tronNative: Lazy<AssetSource>,
@Trc20Assets trc20: Lazy<AssetSource>, @Trc20Assets trc20: Lazy<AssetSource>,
@BitcoinNativeAssets bitcoinNative: Lazy<AssetSource>,
@UnsupportedAssets unsupported: AssetSource, @UnsupportedAssets unsupported: AssetSource,
nativeAssetEventDetector: NativeAssetEventDetector, nativeAssetEventDetector: NativeAssetEventDetector,
@@ -53,6 +55,7 @@ class AssetsModule {
equilibriumAssetSource = equilibrium, equilibriumAssetSource = equilibrium,
tronNativeSource = tronNative, tronNativeSource = tronNative,
trc20Source = trc20, trc20Source = trc20,
bitcoinNativeSource = bitcoinNative,
unsupportedBalanceSource = unsupported, unsupportedBalanceSource = unsupported,
nativeAssetEventDetector = nativeAssetEventDetector, nativeAssetEventDetector = nativeAssetEventDetector,
@@ -0,0 +1,57 @@
package io.novafoundation.nova.feature_wallet_impl.di.modules
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSource
import io.novafoundation.nova.feature_wallet_impl.data.network.bitcoin.BitcoinApi
import io.novafoundation.nova.feature_wallet_impl.data.network.bitcoin.RealBitcoinApi
import io.novafoundation.nova.feature_wallet_impl.data.network.bitcoin.RetrofitBitcoinApi
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.StaticAssetSource
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.bitcoinNative.BitcoinNativeAssetBalance
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.UnsupportedAssetHistory
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.UnsupportedAssetTransfers
import javax.inject.Qualifier
@Qualifier
annotation class BitcoinNativeAssets
/**
* Bitcoin support - Phase 4, read-only.
*
* Only `balance` is implemented for real; `transfers`/`history` reuse the same `Unsupported*` stubs the rest of
* the app uses for asset types with no send/history support yet (see `TronAssetsModule` for the identical
* Phase-1 precedent this mirrors). Send support is separate, later work.
*/
@Module
class BitcoinAssetsModule {
@Provides
@FeatureScope
fun provideRetrofitBitcoinApi(
networkApiCreator: NetworkApiCreator
): RetrofitBitcoinApi = networkApiCreator.create(RetrofitBitcoinApi::class.java)
@Provides
@FeatureScope
fun provideBitcoinApi(retrofitBitcoinApi: RetrofitBitcoinApi): BitcoinApi = RealBitcoinApi(retrofitBitcoinApi)
@Provides
@FeatureScope
fun provideBitcoinNativeBalance(assetCache: AssetCache, bitcoinApi: BitcoinApi) = BitcoinNativeAssetBalance(assetCache, bitcoinApi)
@Provides
@BitcoinNativeAssets
@FeatureScope
fun provideBitcoinNativeAssetSource(
bitcoinNativeAssetBalance: BitcoinNativeAssetBalance,
unsupportedAssetTransfers: UnsupportedAssetTransfers,
unsupportedAssetHistory: UnsupportedAssetHistory,
): AssetSource = StaticAssetSource(
transfers = unsupportedAssetTransfers,
balance = bitcoinNativeAssetBalance,
history = unsupportedAssetHistory
)
}
@@ -37,6 +37,7 @@ import io.novafoundation.nova.runtime.multiNetwork.runtime.types.BaseTypeSynchro
import io.novafoundation.nova.runtime.multiNetwork.runtime.types.TypesFetcher import io.novafoundation.nova.runtime.multiNetwork.runtime.types.TypesFetcher
import io.novasama.substrate_sdk_android.wsrpc.SocketService import io.novasama.substrate_sdk_android.wsrpc.SocketService
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import org.web3j.protocol.http.HttpService import org.web3j.protocol.http.HttpService
import javax.inject.Provider import javax.inject.Provider
@@ -160,12 +161,14 @@ class ChainRegistryModule {
socketProvider: Provider<SocketService>, socketProvider: Provider<SocketService>,
bulkRetriever: BulkRetriever, bulkRetriever: BulkRetriever,
connectionSecrets: ConnectionSecrets, connectionSecrets: ConnectionSecrets,
web3ApiFactory: Web3ApiFactory web3ApiFactory: Web3ApiFactory,
httpClient: OkHttpClient,
) = NodeHealthStateTesterFactory( ) = NodeHealthStateTesterFactory(
socketProvider, socketProvider,
connectionSecrets, connectionSecrets,
bulkRetriever, bulkRetriever,
web3ApiFactory web3ApiFactory,
httpClient
) )
@Provides @Provides
@@ -10,11 +10,14 @@ import io.novafoundation.nova.core_db.dao.ChainAssetDao
import io.novafoundation.nova.core_db.dao.ChainDao import io.novafoundation.nova.core_db.dao.ChainDao
import io.novafoundation.nova.core_db.dao.StorageDao import io.novafoundation.nova.core_db.dao.StorageDao
import io.novasama.substrate_sdk_android.wsrpc.SocketService import io.novasama.substrate_sdk_android.wsrpc.SocketService
import okhttp3.OkHttpClient
interface RuntimeDependencies { interface RuntimeDependencies {
fun networkApiCreator(): NetworkApiCreator fun networkApiCreator(): NetworkApiCreator
fun okHttpClient(): OkHttpClient
fun socketServiceCreator(): SocketService fun socketServiceCreator(): SocketService
fun gson(): Gson fun gson(): Gson
@@ -560,6 +560,18 @@ fun Chain.requireTronGridBaseUrl(): String {
} }
} }
/**
* The mempool.space-style REST API base url for a Bitcoin-based chain - same rationale as [requireTronGridBaseUrl]:
* Bitcoin has no JSON-RPC/WS node concept at all, so the configured `nodes` entry directly *is* the REST API base url.
*/
fun Chain.requireMempoolSpaceBaseUrl(): String {
require(isBitcoinBased) { "Chain $id is not Bitcoin-based" }
return requireNotNull(nodes.nodes.minByOrNull { it.orderId }?.unformattedUrl) {
"No mempool.space-style node configured for chain $id"
}
}
fun Chain.Asset.requireEquilibrium(): Type.Equilibrium { fun Chain.Asset.requireEquilibrium(): Type.Equilibrium {
require(type is Type.Equilibrium) require(type is Type.Equilibrium)
@@ -12,6 +12,8 @@ const val ASSET_EVM_NATIVE = "evmNative"
const val ASSET_TRON_NATIVE = "tronNative" const val ASSET_TRON_NATIVE = "tronNative"
const val ASSET_TRC20 = "trc20" const val ASSET_TRC20 = "trc20"
const val ASSET_BITCOIN_NATIVE = "bitcoinNative"
const val ASSET_EQUILIBRIUM = "equilibrium" const val ASSET_EQUILIBRIUM = "equilibrium"
const val ASSET_EQUILIBRIUM_ON_CHAIN_ID = "assetId" const val ASSET_EQUILIBRIUM_ON_CHAIN_ID = "assetId"
@@ -69,6 +69,8 @@ fun mapChainAssetTypeToRaw(type: Chain.Asset.Type): Pair<String, Map<String, Any
ASSET_EQUILIBRIUM_ON_CHAIN_ID to type.id.toString() ASSET_EQUILIBRIUM_ON_CHAIN_ID to type.id.toString()
) )
Chain.Asset.Type.BitcoinNative -> ASSET_BITCOIN_NATIVE to null
Chain.Asset.Type.Unsupported -> ASSET_UNSUPPORTED to null Chain.Asset.Type.Unsupported -> ASSET_UNSUPPORTED to null
} }
@@ -95,6 +95,8 @@ private fun mapChainAssetTypeFromRaw(type: String?, typeExtras: Map<String, Any?
ASSET_EQUILIBRIUM -> Chain.Asset.Type.Equilibrium((typeExtras!![ASSET_EQUILIBRIUM_ON_CHAIN_ID] as String).toBigInteger()) ASSET_EQUILIBRIUM -> Chain.Asset.Type.Equilibrium((typeExtras!![ASSET_EQUILIBRIUM_ON_CHAIN_ID] as String).toBigInteger())
ASSET_BITCOIN_NATIVE -> Chain.Asset.Type.BitcoinNative
else -> Chain.Asset.Type.Unsupported else -> Chain.Asset.Type.Unsupported
} }
} }
@@ -139,6 +139,12 @@ data class Chain(
val contractAddress: String val contractAddress: String
) : Type() ) : Type()
/**
* Native BTC balance on a Bitcoin-based chain.
* Balance is fetched from a mempool.space-style REST API rather than JSON-RPC.
*/
object BitcoinNative : Type()
object Unsupported : Type() object Unsupported : Type()
} }
@@ -0,0 +1,37 @@
package io.novafoundation.nova.runtime.multiNetwork.connection.node.healthState
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import kotlin.time.ExperimentalTime
import kotlin.time.measureTime
/**
* mempool.space speaks a plain REST API, not Ethereum JSON-RPC - see [TronNodeHealthStateTester]'s doc for the
* identical rationale this mirrors. `GET /blocks/tip/height` needs no account/address context and is cheap on
* mempool.space's side, making it a good generic liveness ping.
*/
class BitcoinNodeHealthStateTester(
private val node: Chain.Node,
private val httpClient: OkHttpClient,
) : NodeHealthStateTester {
@OptIn(ExperimentalTime::class)
override suspend fun testNodeHealthState(): Result<Long> = withContext(Dispatchers.IO) {
runCatching {
val request = Request.Builder()
.url("${node.unformattedUrl.trimEnd('/')}/blocks/tip/height")
.build()
val duration = measureTime {
httpClient.newCall(request).execute().use { response ->
check(response.isSuccessful) { "HTTP ${response.code}" }
}
}
duration.inWholeMilliseconds
}
}
}
@@ -5,6 +5,7 @@ import io.novafoundation.nova.runtime.ethereum.Web3ApiFactory
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionSecrets import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionSecrets
import io.novasama.substrate_sdk_android.wsrpc.SocketService import io.novasama.substrate_sdk_android.wsrpc.SocketService
import okhttp3.OkHttpClient
import javax.inject.Provider import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -12,15 +13,21 @@ class NodeHealthStateTesterFactory(
private val socketServiceProvider: Provider<SocketService>, private val socketServiceProvider: Provider<SocketService>,
private val connectionSecrets: ConnectionSecrets, private val connectionSecrets: ConnectionSecrets,
private val bulkRetriever: BulkRetriever, private val bulkRetriever: BulkRetriever,
private val web3ApiFactory: Web3ApiFactory private val web3ApiFactory: Web3ApiFactory,
private val httpClient: OkHttpClient,
) { ) {
fun create(chain: Chain, node: Chain.Node, coroutineScope: CoroutineScope): NodeHealthStateTester { fun create(chain: Chain, node: Chain.Node, coroutineScope: CoroutineScope): NodeHealthStateTester {
val nodeIsSupported = chain.nodes.nodes.any { it.unformattedUrl == node.unformattedUrl } val nodeIsSupported = chain.nodes.nodes.any { it.unformattedUrl == node.unformattedUrl }
require(nodeIsSupported) require(nodeIsSupported)
return if (chain.hasSubstrateRuntime) { return when {
SubstrateNodeHealthStateTester( chain.isBitcoinBased -> BitcoinNodeHealthStateTester(
node = node,
httpClient = httpClient
)
chain.hasSubstrateRuntime -> SubstrateNodeHealthStateTester(
chain = chain, chain = chain,
socketService = socketServiceProvider.get(), socketService = socketServiceProvider.get(),
connectionSecrets = connectionSecrets, connectionSecrets = connectionSecrets,
@@ -28,8 +35,8 @@ class NodeHealthStateTesterFactory(
node = node, node = node,
coroutineScope = coroutineScope coroutineScope = coroutineScope
) )
} else {
EthereumNodeHealthStateTester( else -> EthereumNodeHealthStateTester(
socketService = socketServiceProvider.get(), socketService = socketServiceProvider.get(),
connectionSecrets = connectionSecrets, connectionSecrets = connectionSecrets,
node = node, node = node,