You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
 
 

163 lines
5.8 KiB

package cash.z.ecc.android.sdk.internal.block
import cash.z.ecc.android.sdk.exception.LightWalletException
import cash.z.ecc.android.sdk.internal.ext.retryUpTo
import cash.z.ecc.android.sdk.internal.ext.tryWarn
import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.wallet.sdk.rpc.Service
import io.grpc.StatusRuntimeException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Serves as a source of compact blocks received from the light wallet server. Once started, it will use the given
* lightwallet service to request all the appropriate blocks and compact block store to persist them. By delegating to
* these dependencies, the downloader remains agnostic to the particular implementation of how to retrieve and store
* data; although, by default the SDK uses gRPC and SQL.
*
* @property lightWalletService the service used for requesting compact blocks
* @property compactBlockStore responsible for persisting the compact blocks that are received
*/
open class CompactBlockDownloader private constructor(val compactBlockStore: CompactBlockStore) {
lateinit var lightWalletService: LightWalletService
private set
constructor(
lightWalletService: LightWalletService,
compactBlockStore: CompactBlockStore
) : this(compactBlockStore) {
this.lightWalletService = lightWalletService
}
/**
* Requests the given range of blocks from the lightwalletService and then persists them to the
* compactBlockStore.
*
* @param heightRange the inclusive range of heights to request. For example 10..20 would
* request 11 blocks (including block 10 and block 20).
*
* @return the number of blocks that were returned in the results from the lightwalletService.
*/
suspend fun downloadBlockRange(heightRange: ClosedRange<BlockHeight>): Int = withContext(IO) {
val result = lightWalletService.getBlockRange(heightRange)
compactBlockStore.write(result)
}
/**
* Rewind the storage to the given height, usually to handle reorgs. Deletes all blocks above
* the given height.
*
* @param height the height to which the data will rewind.
*/
suspend fun rewindToHeight(height: BlockHeight) =
// TODO: cancel anything in flight
compactBlockStore.rewindTo(height)
/**
* Return the latest block height known by the lightwalletService.
*
* @return the latest block height.
*/
suspend fun getLatestBlockHeight() =
lightWalletService.getLatestBlockHeight()
/**
* Return the latest block height that has been persisted into the [CompactBlockStore].
*
* @return the latest block height that has been persisted.
*/
suspend fun getLastDownloadedHeight() =
compactBlockStore.getLatestHeight()
suspend fun getServerInfo(): Service.LightdInfo = withContext<Service.LightdInfo>(IO) {
lateinit var result: Service.LightdInfo
try {
result = lightWalletService.getServerInfo()
} catch (e: StatusRuntimeException) {
retryUpTo(6) {
twig("WARNING: reconnecting to service in response to failure (retry #${it + 1}): $e")
lightWalletService.reconnect()
result = lightWalletService.getServerInfo()
}
}
result
}
suspend fun changeService(
newService: LightWalletService,
errorHandler: (Throwable) -> Unit = { throw it }
) = withContext(IO) {
try {
val existing = lightWalletService.getServerInfo()
val new = newService.getServerInfo()
val nonMatching = existing.essentialPropertyDiff(new)
if (nonMatching.size > 0) {
errorHandler(
LightWalletException.ChangeServerException.ChainInfoNotMatching(
nonMatching.joinToString(),
existing,
new
)
)
}
gracefullyShutdown(lightWalletService)
lightWalletService = newService
} catch (s: StatusRuntimeException) {
errorHandler(LightWalletException.ChangeServerException.StatusException(s.status))
} catch (t: Throwable) {
errorHandler(t)
}
}
/**
* Stop this downloader and cleanup any resources being used.
*/
suspend fun stop() {
withContext(Dispatchers.IO) {
lightWalletService.shutdown()
}
compactBlockStore.close()
}
/**
* Fetch the details of a known transaction.
*
* @return the full transaction info.
*/
fun fetchTransaction(txId: ByteArray) = lightWalletService.fetchTransaction(txId)
//
// Convenience functions
//
private suspend fun CoroutineScope.gracefullyShutdown(service: LightWalletService) = launch {
delay(2_000L)
tryWarn("Warning: error while shutting down service") {
service.shutdown()
}
}
/**
* Return a list of critical properties that do not match.
*/
private fun Service.LightdInfo.essentialPropertyDiff(other: Service.LightdInfo) =
mutableListOf<String>().also {
if (!consensusBranchId.equals(other.consensusBranchId, true)) {
it.add("consensusBranchId")
}
if (saplingActivationHeight != other.saplingActivationHeight) {
it.add("saplingActivationHeight")
}
if (!chainName.equals(other.chainName, true)) {
it.add("chainName")
}
}
}