mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 18:25:50 +00:00
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:
+42
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -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
|
||||||
|
}
|
||||||
+22
@@ -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,
|
||||||
|
)
|
||||||
+4
@@ -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()
|
||||||
|
|
||||||
|
|||||||
+30
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -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,
|
||||||
|
|||||||
+57
@@ -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)
|
||||||
|
|
||||||
|
|||||||
+2
@@ -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"
|
||||||
|
|
||||||
|
|||||||
+2
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-5
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user