zcash-android-wallet-sdk/lightwallet-client-lib/src/main/java/co/electriccoin/lightwallet/client/internal/DarksideApi.kt

181 lines
6.1 KiB
Kotlin

package co.electriccoin.lightwallet.client.internal
import android.content.Context
import cash.z.wallet.sdk.internal.rpc.Darkside
import cash.z.wallet.sdk.internal.rpc.Darkside.DarksideTransactionsURL
import cash.z.wallet.sdk.internal.rpc.DarksideStreamerGrpc
import cash.z.wallet.sdk.internal.rpc.Service
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
import io.grpc.ManagedChannel
import io.grpc.stub.StreamObserver
import java.util.concurrent.TimeUnit
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/*
* This class is under the internal package, but is itself not restricted with internal visibility.
*
* This allows the class to be used for some automated tests in other modules.
*/
class DarksideApi private constructor(
private val channel: ManagedChannel,
private val singleRequestTimeout: Duration = 10.seconds
) {
companion object {
internal fun new(
channelFactory: ChannelFactory,
lightWalletEndpoint: LightWalletEndpoint
) = DarksideApi(channelFactory.newChannel(lightWalletEndpoint))
}
//
// Service APIs
//
fun reset(
saplingActivationHeight: BlockHeightUnsafe,
branchId: String = "e9ff75a6", // Canopy,
chainName: String = "darksidemainnet"
) = apply {
Darkside.DarksideMetaState.newBuilder()
.setBranchID(branchId)
.setChainName(chainName)
.setSaplingActivation(saplingActivationHeight.value.toInt())
.build().let { request ->
createStub().reset(request)
}
}
fun stageBlocks(url: String) = apply {
createStub().stageBlocks(url.toUrl())
}
fun stageTransactions(url: String, targetHeight: BlockHeightUnsafe) = apply {
createStub().stageTransactions(
DarksideTransactionsURL.newBuilder().setHeight(targetHeight.value).setUrl(url).build()
)
}
fun stageEmptyBlocks(
startHeight: BlockHeightUnsafe,
count: Int = 10,
nonce: Int = Random.nextInt()
) = apply {
createStub().stageBlocksCreate(
Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight.value).setCount(count)
.setNonce(nonce).build()
)
}
fun stageTransactions(txs: Iterator<Service.RawTransaction>?, tipHeight: BlockHeightUnsafe) {
if (txs == null) {
return
}
val response = EmptyResponse()
createStreamingStub().stageTransactionsStream(response).apply {
txs.forEach {
// apply the tipHeight because the passed in txs might not know their destination
// height (if they were created via SendTransaction)
onNext(
it.newBuilderForType().setData(it.data).setHeight(tipHeight.value).build()
)
}
onCompleted()
}
response.await()
}
fun applyBlocks(tipHeight: BlockHeightUnsafe) {
createStub().applyStaged(tipHeight.toHeight())
}
fun getSentTransactions(): MutableIterator<Service.RawTransaction>? {
return createStub().getIncomingTransactions(Service.Empty.newBuilder().build())
}
// fun setMetaState(
// branchId: String = "2bb40e60", // Blossom,
// chainName: String = "darkside",
// saplingActivationHeight: Int = 419200
// ): DarksideApi = apply {
// createStub().setMetaState(
// Darkside.DarksideMetaState.newBuilder()
// .setBranchID(branchId)
// .setChainName(chainName)
// .setSaplingActivation(saplingActivationHeight)
// .build()
// )
// }
// fun setLatestHeight(latestHeight: Int) = setState(latestHeight, reorgHeight)
//
// fun setReorgHeight(reorgHeight: Int)
// = setState(latestHeight.coerceAtLeast(reorgHeight), reorgHeight)
//
// fun setState(latestHeight: Int = -1, reorgHeight: Int = latestHeight): DarksideApi {
// this.latestHeight = latestHeight
// this.reorgHeight = reorgHeight
// // change this service to accept ints as heights, like everywhere else
// createStub().darksideSetState(
// Darkside.DarksideState.newBuilder()
// .setLatestHeight(latestHeight.toLong())
// .setReorgHeight(reorgHeight.toLong())
// .build()
// )
// return this
// }
private fun createStub(): DarksideStreamerGrpc.DarksideStreamerBlockingStub =
DarksideStreamerGrpc
.newBlockingStub(channel)
.withDeadlineAfter(singleRequestTimeout.inWholeSeconds, TimeUnit.SECONDS)
private fun createStreamingStub(): DarksideStreamerGrpc.DarksideStreamerStub =
DarksideStreamerGrpc
.newStub(channel)
.withDeadlineAfter(singleRequestTimeout.inWholeSeconds, TimeUnit.SECONDS)
private fun String.toUrl() = Darkside.DarksideBlocksURL.newBuilder().setUrl(this).build()
class EmptyResponse : StreamObserver<Service.Empty> {
companion object {
private val DEFAULT_DELAY = 20.milliseconds
}
var completed = false
var error: Throwable? = null
override fun onNext(value: Service.Empty?) {
// No implementation
}
override fun onError(t: Throwable?) {
error = t
completed = true
}
override fun onCompleted() {
completed = true
}
fun await() {
while (!completed) {
Thread.sleep(DEFAULT_DELAY.inWholeSeconds)
}
if (error != null) {
error("Server responded with an error: $error caused by ${error?.cause}")
}
}
}
}
private fun BlockHeightUnsafe.toHeight() =
Darkside.DarksideHeight.newBuilder().setHeight(this.value).build()
fun DarksideApi.Companion.new(
context: Context,
lightWalletEndpoint: LightWalletEndpoint
) = DarksideApi.new(AndroidChannelFactory(context), lightWalletEndpoint)