Implement the logic for changing servers.
This commit is contained in:
parent
24f7433f1c
commit
ab5b34cbd8
|
@ -1,8 +1,15 @@
|
|||
package cash.z.ecc.android.sdk.block
|
||||
|
||||
import cash.z.ecc.android.sdk.exception.LightWalletException
|
||||
import cash.z.ecc.android.sdk.ext.tryCatchWith
|
||||
import cash.z.ecc.android.sdk.ext.tryWarn
|
||||
import cash.z.ecc.android.sdk.service.LightWalletService
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
import io.grpc.StatusRuntimeException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
|
@ -11,13 +18,20 @@ import kotlinx.coroutines.withContext
|
|||
* 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 lightWalletService the service used for requesting compact blocks
|
||||
* @property compactBlockStore responsible for persisting the compact blocks that are received
|
||||
*/
|
||||
open class CompactBlockDownloader(
|
||||
val lightwalletService: LightWalletService,
|
||||
val compactBlockStore: CompactBlockStore
|
||||
) {
|
||||
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
|
||||
|
@ -29,7 +43,7 @@ open class CompactBlockDownloader(
|
|||
* @return the number of blocks that were returned in the results from the lightwalletService.
|
||||
*/
|
||||
suspend fun downloadBlockRange(heightRange: IntRange): Int = withContext(IO) {
|
||||
val result = lightwalletService.getBlockRange(heightRange)
|
||||
val result = lightWalletService.getBlockRange(heightRange)
|
||||
compactBlockStore.write(result)
|
||||
result.size
|
||||
}
|
||||
|
@ -50,7 +64,7 @@ open class CompactBlockDownloader(
|
|||
* @return the latest block height.
|
||||
*/
|
||||
suspend fun getLatestBlockHeight() = withContext(IO) {
|
||||
lightwalletService.getLatestBlockHeight()
|
||||
lightWalletService.getLatestBlockHeight()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,14 +77,42 @@ open class CompactBlockDownloader(
|
|||
}
|
||||
|
||||
suspend fun getServerInfo(): Service.LightdInfo = withContext(IO) {
|
||||
lightwalletService.getServerInfo()
|
||||
lightWalletService.getServerInfo()
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
fun stop() {
|
||||
lightwalletService.shutdown()
|
||||
lightWalletService.shutdown()
|
||||
compactBlockStore.close()
|
||||
}
|
||||
|
||||
|
@ -79,7 +121,35 @@ open class CompactBlockDownloader(
|
|||
*
|
||||
* @return the full transaction info.
|
||||
*/
|
||||
fun fetchTransaction(txId: ByteArray) = lightwalletService.fetchTransaction(txId)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package cash.z.ecc.android.sdk.exception
|
||||
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
import io.grpc.Status
|
||||
import io.grpc.Status.Code.UNAVAILABLE
|
||||
|
||||
|
||||
/**
|
||||
* Marker for all custom exceptions from the SDK. Making it an interface would result in more typing
|
||||
|
@ -113,21 +117,40 @@ sealed class InitializerException(message: String, cause: Throwable? = null) :
|
|||
/**
|
||||
* Exceptions thrown while interacting with lightwalletd.
|
||||
*/
|
||||
sealed class LightwalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||
object InsecureConnection : LightwalletException("Error: attempted to connect to lightwalletd" +
|
||||
sealed class LightWalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||
object InsecureConnection : LightWalletException("Error: attempted to connect to lightwalletd" +
|
||||
" with an insecure connection! Plaintext connections are only allowed when the" +
|
||||
" resource value for 'R.bool.lightwalletd_allow_very_insecure_connections' is true" +
|
||||
" because this choice should be explicit.")
|
||||
class ConsensusBranchException(sdkBranch: String, lwdBranch: String) :
|
||||
LightwalletException(
|
||||
LightWalletException(
|
||||
"Error: the lightwalletd server is using a consensus branch" +
|
||||
" (branch: $lwdBranch) that does not match the transactions being created" +
|
||||
" (branch: $sdkBranch). This probably means the SDK and Server are on two" +
|
||||
" different chains, most likely because of a recent network upgrade (NU). Either" +
|
||||
" update the SDK to match lightwalletd or use a lightwalletd that matches the SDK."
|
||||
)
|
||||
|
||||
open class ChangeServerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
||||
class ChainInfoNotMatching(val propertyNames: String, val expectedInfo: Service.LightdInfo, val actualInfo: Service.LightdInfo) : ChangeServerException(
|
||||
"Server change error: the $propertyNames values did not match."
|
||||
)
|
||||
class StatusException(val status: Status, cause: Throwable? = null) : SdkException(status.toMessage(), cause) {
|
||||
companion object {
|
||||
private fun Status.toMessage(): String {
|
||||
return when(this.code) {
|
||||
UNAVAILABLE -> {
|
||||
"Error: the new server is unavailable. Verify that the host and port are correct. Failed with $this"
|
||||
}
|
||||
else -> "Changing servers failed with status $this"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Potentially user-facing exceptions thrown while encoding transactions.
|
||||
*/
|
||||
|
|
|
@ -2,7 +2,8 @@ package cash.z.ecc.android.sdk.service
|
|||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.sdk.R
|
||||
import cash.z.ecc.android.sdk.exception.LightwalletException
|
||||
import cash.z.ecc.android.sdk.annotation.OpenForTesting
|
||||
import cash.z.ecc.android.sdk.exception.LightWalletException
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.DEFAULT_LIGHTWALLETD_PORT
|
||||
import cash.z.ecc.android.sdk.ext.twig
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
||||
|
@ -18,18 +19,19 @@ import java.util.concurrent.TimeUnit
|
|||
* Implementation of LightwalletService using gRPC for requests to lightwalletd.
|
||||
*
|
||||
* @property channel the channel to use for communicating with the lightwalletd server.
|
||||
* @property singleRequestTimeoutSec the timeout to use for non-streaming requests. When a new stub is
|
||||
* created, it will use a deadline that is after the given duration from now.
|
||||
* @property streamingRequestTimeoutSec the timeout to use for streaming requests. When a new stub is
|
||||
* created for streaming requests, it will use a deadline that is after the given duration from now.
|
||||
* @property singleRequestTimeoutSec the timeout to use for non-streaming requests. When a new stub
|
||||
* is created, it will use a deadline that is after the given duration from now.
|
||||
* @property streamingRequestTimeoutSec the timeout to use for streaming requests. When a new stub
|
||||
* is created for streaming requests, it will use a deadline that is after the given duration from
|
||||
* now.
|
||||
*/
|
||||
@OpenForTesting
|
||||
class LightWalletGrpcService private constructor(
|
||||
var channel: ManagedChannel,
|
||||
private val singleRequestTimeoutSec: Long = 10L,
|
||||
private val streamingRequestTimeoutSec: Long = 90L
|
||||
) : LightWalletService {
|
||||
|
||||
//TODO: find a better way to do this, maybe change the constructor to keep the properties
|
||||
lateinit var connectionInfo: ConnectionInfo
|
||||
|
||||
/**
|
||||
|
@ -46,7 +48,8 @@ class LightWalletGrpcService private constructor(
|
|||
appContext: Context,
|
||||
host: String,
|
||||
port: Int = DEFAULT_LIGHTWALLETD_PORT,
|
||||
usePlaintext: Boolean = appContext.resources.getBoolean(R.bool.lightwalletd_allow_very_insecure_connections)
|
||||
usePlaintext: Boolean =
|
||||
appContext.resources.getBoolean(R.bool.lightwalletd_allow_very_insecure_connections)
|
||||
) : this(createDefaultChannel(appContext, host, port, usePlaintext)) {
|
||||
connectionInfo = ConnectionInfo(appContext.applicationContext, host, port, usePlaintext)
|
||||
}
|
||||
|
@ -57,17 +60,20 @@ class LightWalletGrpcService private constructor(
|
|||
if (heightRange.isEmpty()) return listOf()
|
||||
|
||||
channel.resetConnectBackoff()
|
||||
return channel.createStub(streamingRequestTimeoutSec).getBlockRange(heightRange.toBlockRange()).toList()
|
||||
return channel.createStub(streamingRequestTimeoutSec)
|
||||
.getBlockRange(heightRange.toBlockRange()).toList()
|
||||
}
|
||||
|
||||
override fun getLatestBlockHeight(): Int {
|
||||
channel.resetConnectBackoff()
|
||||
return channel.createStub(singleRequestTimeoutSec).getLatestBlock(Service.ChainSpec.newBuilder().build()).height.toInt()
|
||||
return channel.createStub(singleRequestTimeoutSec)
|
||||
.getLatestBlock(Service.ChainSpec.newBuilder().build()).height.toInt()
|
||||
}
|
||||
|
||||
override fun getServerInfo(): Service.LightdInfo {
|
||||
channel.resetConnectBackoff()
|
||||
return channel.createStub(singleRequestTimeoutSec).getLightdInfo(Service.Empty.newBuilder().build())
|
||||
return channel.createStub(singleRequestTimeoutSec)
|
||||
.getLightdInfo(Service.Empty.newBuilder().build())
|
||||
}
|
||||
|
||||
override fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse {
|
||||
|
@ -87,6 +93,7 @@ class LightWalletGrpcService private constructor(
|
|||
}
|
||||
|
||||
override fun shutdown() {
|
||||
twig("Shutting down channel")
|
||||
channel.shutdown()
|
||||
}
|
||||
|
||||
|
@ -94,7 +101,9 @@ class LightWalletGrpcService private constructor(
|
|||
if (txId.isEmpty()) return null
|
||||
|
||||
channel.resetConnectBackoff()
|
||||
return channel.createStub().getTransaction(Service.TxFilter.newBuilder().setHash(ByteString.copyFrom(txId)).build())
|
||||
return channel.createStub().getTransaction(
|
||||
Service.TxFilter.newBuilder().setHash(ByteString.copyFrom(txId)).build()
|
||||
)
|
||||
}
|
||||
|
||||
override fun getTAddressTransactions(
|
||||
|
@ -112,8 +121,9 @@ class LightWalletGrpcService private constructor(
|
|||
}
|
||||
|
||||
override fun reconnect() {
|
||||
twig("closing existing channel and then reconnecting to" +
|
||||
" ${connectionInfo.host}:${connectionInfo.port}?usePlaintext=${connectionInfo.usePlaintext}")
|
||||
twig("closing existing channel and then reconnecting to ${connectionInfo.host}:" +
|
||||
"${connectionInfo.port}?usePlaintext=${connectionInfo.usePlaintext}"
|
||||
)
|
||||
channel.shutdown()
|
||||
channel = createDefaultChannel(
|
||||
connectionInfo.appContext,
|
||||
|
@ -128,12 +138,12 @@ class LightWalletGrpcService private constructor(
|
|||
// Utilities
|
||||
//
|
||||
|
||||
private fun Channel.createStub(timeoutSec: Long = 60L): CompactTxStreamerGrpc.CompactTxStreamerBlockingStub =
|
||||
CompactTxStreamerGrpc
|
||||
.newBlockingStub(this)
|
||||
.withDeadlineAfter(timeoutSec, TimeUnit.SECONDS)
|
||||
private fun Channel.createStub(timeoutSec: Long = 60L) = CompactTxStreamerGrpc
|
||||
.newBlockingStub(this)
|
||||
.withDeadlineAfter(timeoutSec, TimeUnit.SECONDS)
|
||||
|
||||
private inline fun Int.toBlockHeight(): Service.BlockID = Service.BlockID.newBuilder().setHeight(this.toLong()).build()
|
||||
private inline fun Int.toBlockHeight(): Service.BlockID =
|
||||
Service.BlockID.newBuilder().setHeight(this.toLong()).build()
|
||||
|
||||
private inline fun IntRange.toBlockRange(): Service.BlockRange =
|
||||
Service.BlockRange.newBuilder()
|
||||
|
@ -177,7 +187,9 @@ class LightWalletGrpcService private constructor(
|
|||
.context(appContext)
|
||||
.apply {
|
||||
if (usePlaintext) {
|
||||
if (!appContext.resources.getBoolean(R.bool.lightwalletd_allow_very_insecure_connections)) throw LightwalletException.InsecureConnection
|
||||
if (!appContext.resources.getBoolean(
|
||||
R.bool.lightwalletd_allow_very_insecure_connections
|
||||
)) throw LightWalletException.InsecureConnection
|
||||
usePlaintext()
|
||||
} else {
|
||||
useTransportSecurity()
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package cash.z.ecc.android.sdk.service
|
||||
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
|
||||
|
@ -9,6 +8,14 @@ import cash.z.wallet.sdk.rpc.Service
|
|||
* calls because async concerns are handled at a higher level.
|
||||
*/
|
||||
interface LightWalletService {
|
||||
|
||||
/**
|
||||
* Fetch the details of a known transaction.
|
||||
*
|
||||
* @return the full transaction info.
|
||||
*/
|
||||
fun fetchTransaction(txId: ByteArray): Service.RawTransaction?
|
||||
|
||||
/**
|
||||
* Return the given range of blocks.
|
||||
*
|
||||
|
@ -46,13 +53,6 @@ interface LightWalletService {
|
|||
*/
|
||||
fun getServerInfo(): Service.LightdInfo
|
||||
|
||||
/**
|
||||
* Submit a raw transaction.
|
||||
*
|
||||
* @return the response from the server.
|
||||
*/
|
||||
fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse
|
||||
|
||||
/**
|
||||
* Gets all the transactions for a given t-address over the given range. In practice, this is
|
||||
* effectively the same as an RPC call to a node that's running an insight server. The data is
|
||||
|
@ -63,11 +63,10 @@ interface LightWalletService {
|
|||
fun getTAddressTransactions(tAddress: String, blockHeightRange: IntRange): List<Service.RawTransaction>
|
||||
|
||||
/**
|
||||
* Fetch the details of a known transaction.
|
||||
*
|
||||
* @return the full transaction info.
|
||||
* Reconnect to the same or a different server. This is useful when the connection is
|
||||
* unrecoverable. That might be time to switch to a mirror or just reconnect.
|
||||
*/
|
||||
fun fetchTransaction(txId: ByteArray): Service.RawTransaction?
|
||||
fun reconnect()
|
||||
|
||||
/**
|
||||
* Cleanup any connections when the service is shutting down and not going to be used again.
|
||||
|
@ -75,8 +74,10 @@ interface LightWalletService {
|
|||
fun shutdown()
|
||||
|
||||
/**
|
||||
* Reconnect to the same or a different server. This is useful when the connection is
|
||||
* unrecoverable. That might be time to switch to a mirror or just reconnect.
|
||||
* Submit a raw transaction.
|
||||
*
|
||||
* @return the response from the server.
|
||||
*/
|
||||
fun reconnect()
|
||||
fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue