[#615] Refactor lightwalletd client
* [#615] Refactor lightwalletd client This moves the lightwalletd client code to a separate Gradle module, but does not yet do much clean up the public API (e.g. hiding generated GRPC objects). That will be a followon change via #673 I’ve prototyped a safer version of the API that would be implemented for #673 for two specific calls: obtaining the current block height and obtaining the light wallet info. These were the easiest endpoints to update and they provide a useful exploration for the future changes needed. * Fix benchmarking for networking module - Moved to fixture and build type check components to the new networking module, so it's accessible from all needed parts of the projects - Changed fixture return type to fit to all needed usages * Align with previous review comment * Fix wrong merge * Add basic sanity test - To satisfy tests requirements on emulators.wtf service * Remove grpc dependency from sdk-lib module * Repackage cash.z.wallet.sdk.rpc to cash.z.wallet.sdk.internal.rpc * Fix BuildConfig path * Update demo-app paths to rpc * Fix broken grpc services locations - Our aim here is to change only the local services location (package name), the server location can't be changed due to backward compatibility. * Enhance GRPC Response model * Adopt enhanced GRPC response model - Adopted in a few endpoints * Enhance Connection failure type * Add simple fixture test * Refactor fetchTransaction() to work with Response - Refactored this server call to adopt our new Response mechanism - GrpsStatusResolver.resolveFailureFromStatus() now accepts Throwable input - Added Response.Failure.toThrowable() to satisfy subsequent server calls results processing - A few other minor refactoring changes * Remove commented out log * Unify return type of collection returning methods - Switched fetchUtxos() to return Sequence instead of List - Added a check on empty tAddress input * fetchUtxos returns flow - Switched fetchUtxos() to return Flow of Service.GetAddressUtxosReply - Internally it calls getAddressUtxosStream() instead of getAddressUtxos() from GRPC layer * Update unsafe suffix documentation * Address several minor change requests * Remove code parameter - Removed from the locally triggered failures with default codes. * Rename local variable * Switch from empty response to exception - Our server calls now rather throw IllegalArgumentException than return an empty response - Removed commented out log message - Updated javadocs * Update proto files - Plus related api changes adoption * Unify new clients instances name * AGP 7.4.0 changes - packagingOptions -> androidComponents in sdk-lib and lightwallet-client-lib modules * SDK version bump * Response resolver tests * Release build upload timeout increase * Release build upload timeout increase fix * Revert timeout - As Github has some infrastructure troubles and we need to wait * Add migrations documentation * Sort packaging filters * Remove private field from public documentation * Hide private variables * Remove package from Android Manifest * Throw exception instead of custom error - This step unify our approach to validation on client side across all server calls * Replace setAddresses index const with number * Fix indentation in proto file --------- Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
parent
c2d0c2bc58
commit
c0a2c11418
|
@ -49,6 +49,7 @@ captures/
|
||||||
.idea/tasks.xml
|
.idea/tasks.xml
|
||||||
.idea/vcs.xml
|
.idea/vcs.xml
|
||||||
.idea/workspace.xml
|
.idea/workspace.xml
|
||||||
|
.idea/protoeditor.xml
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
# Keystore files
|
# Keystore files
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name=":sdk-lib:connectedAndroidTest" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
|
<configuration default="false" name=":sdk-lib:connectedAndroidTest" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
|
||||||
<module name="zcash-android-sdk.sdk-lib" />
|
<module name="zcash-android-sdk.sdk-lib.androidTest" />
|
||||||
<option name="TESTING_TYPE" value="0" />
|
<option name="TESTING_TYPE" value="0" />
|
||||||
<option name="METHOD_NAME" value="" />
|
<option name="METHOD_NAME" value="" />
|
||||||
<option name="CLASS_NAME" value="" />
|
<option name="CLASS_NAME" value="" />
|
||||||
|
@ -8,13 +8,14 @@
|
||||||
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
|
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
|
||||||
<option name="EXTRA_OPTIONS" value="" />
|
<option name="EXTRA_OPTIONS" value="" />
|
||||||
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
|
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
|
||||||
|
<option name="RETENTION_ENABLED" value="No" />
|
||||||
|
<option name="RETENTION_MAX_SNAPSHOTS" value="2" />
|
||||||
|
<option name="RETENTION_COMPRESS_SNAPSHOTS" value="false" />
|
||||||
<option name="CLEAR_LOGCAT" value="false" />
|
<option name="CLEAR_LOGCAT" value="false" />
|
||||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
||||||
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
|
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
|
||||||
<option name="FORCE_STOP_RUNNING_APP" value="true" />
|
|
||||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||||
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="2147483645" />
|
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="2147483645" />
|
||||||
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="api-9130115880275692386-873230" />
|
|
||||||
<option name="DEBUGGER_TYPE" value="Auto" />
|
<option name="DEBUGGER_TYPE" value="Auto" />
|
||||||
<Auto>
|
<Auto>
|
||||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||||
|
@ -42,7 +43,7 @@
|
||||||
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
||||||
<option name="STARTUP_PROFILING_ENABLED" value="false" />
|
<option name="STARTUP_PROFILING_ENABLED" value="false" />
|
||||||
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
||||||
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
|
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Callstack Sample" />
|
||||||
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
|
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
|
||||||
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
|
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
|
||||||
</Profilers>
|
</Profilers>
|
||||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,6 +1,16 @@
|
||||||
Change Log
|
Change Log
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
## 1.13.0-beta01
|
||||||
|
### Changed
|
||||||
|
- The SDK's internal networking has been refactored to a separate Gradle module `lightwallet-client-lib` (and
|
||||||
|
therefore a separate artifact) which is a transitive dependency of the Zcash Android SDK.
|
||||||
|
- The `z.cash.ecc.android.sdk.model.LightWalletEndpoint` class has been moved to `co.electriccoin.lightwallet.client.model.LightWalletEndpoint`
|
||||||
|
- The new networking module now provides a `BlockingLightWalletClient` for blocking calls and a
|
||||||
|
`CoroutineLightWalletClient` for asynchronous calls.
|
||||||
|
- Most unary calls respond with the new `Response` class and its subclasses. Streaming calls will be updated
|
||||||
|
with the Response class later.
|
||||||
|
|
||||||
## 1.12.0-beta01
|
## 1.12.0-beta01
|
||||||
### Changed
|
### Changed
|
||||||
- `TransactionOverview`, `Transaction.Sent`, and `Transaction.Received` have `minedHeight` as a nullable field now. This fixes a potential crash when fetching transactions when a transaction is in the mempool
|
- `TransactionOverview`, `Transaction.Sent`, and `Transaction.Received` have `minedHeight` as a nullable field now. This fixes a potential crash when fetching transactions when a transaction is in the mempool
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
Troubleshooting Migrations
|
Troubleshooting Migrations
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
Migration to Version 1.13
|
||||||
|
---------------------------------
|
||||||
|
Update usages of `z.cash.ecc.android.sdk.model.LightWalletEndpoint` to `co.electriccoin.lightwallet.client.model.LightWalletEndpoint`.
|
||||||
|
|
||||||
|
SDK clients should avoid using generated GRPC objects, as these are an internal implementation detail and are in process of being removed from the public API. Any clients using GRPC objects will find these have been repackaged from `cash.z.wallet.sdk.rpc` to `cash.z.wallet.sdk.internal.rpc` to signal they are not a public API.
|
||||||
|
|
||||||
|
Migration to Version 1.12
|
||||||
|
---------------------------------
|
||||||
|
`TransactionOverview`, `Transaction.Sent`, and `Transaction.Received` have been updated to reflect that `minedHeight` is nullable.
|
||||||
|
|
||||||
Migration to Version 1.11
|
Migration to Version 1.11
|
||||||
---------------------------------
|
---------------------------------
|
||||||
The way the SDK is initialized has changed. The `Initializer` object has been removed and `Synchronizer.new` now takes a longer parameter list which includes the parameters previously passed to `Initializer`.
|
The way the SDK is initialized has changed. The `Initializer` object has been removed and `Synchronizer.new` now takes a longer parameter list which includes the parameters previously passed to `Initializer`.
|
||||||
|
|
|
@ -23,6 +23,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(projects.lightwalletClientLib)
|
||||||
implementation(projects.sdkLib)
|
implementation(projects.sdkLib)
|
||||||
implementation(libs.kotlin.stdlib)
|
implementation(libs.kotlin.stdlib)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
|
|
@ -6,8 +6,8 @@ package cash.z.ecc.android.sdk.darkside // package cash.z.ecc.android.sdk.integr
|
||||||
// import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
// import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
||||||
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
|
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
|
||||||
// import cash.z.ecc.android.sdk.util.SimpleMnemonics
|
// import cash.z.ecc.android.sdk.util.SimpleMnemonics
|
||||||
// import cash.z.wallet.sdk.rpc.CompactFormats
|
// import cash.z.wallet.sdk.internal.rpc.CompactFormats
|
||||||
// import cash.z.wallet.sdk.rpc.Service
|
// import cash.z.wallet.sdk.internal.rpc.Service
|
||||||
// import io.grpc.*
|
// import io.grpc.*
|
||||||
// import kotlinx.coroutines.delay
|
// import kotlinx.coroutines.delay
|
||||||
// import kotlinx.coroutines.runBlocking
|
// import kotlinx.coroutines.runBlocking
|
||||||
|
|
|
@ -7,7 +7,7 @@ package cash.z.ecc.android.sdk.darkside.reorgs // package cash.z.ecc.android.sdk
|
||||||
// import cash.z.ecc.android.sdk.test.ScopedTest
|
// import cash.z.ecc.android.sdk.test.ScopedTest
|
||||||
// import cash.z.ecc.android.sdk.ext.import
|
// import cash.z.ecc.android.sdk.ext.import
|
||||||
// import cash.z.ecc.android.sdk.internal.twig
|
// import cash.z.ecc.android.sdk.internal.twig
|
||||||
// import cash.z.ecc.android.sdk.darkside.test.DarksideApi
|
// import co.electriccoin.lightwallet.client.internal.DarksideApi
|
||||||
// import io.grpc.StatusRuntimeException
|
// import io.grpc.StatusRuntimeException
|
||||||
// import kotlinx.coroutines.delay
|
// import kotlinx.coroutines.delay
|
||||||
// import kotlinx.coroutines.flow.filter
|
// import kotlinx.coroutines.flow.filter
|
||||||
|
@ -106,7 +106,7 @@ package cash.z.ecc.android.sdk.darkside.reorgs // package cash.z.ecc.android.sdk
|
||||||
// lightwalletd.getBlockRange(height..height).first()
|
// lightwalletd.getBlockRange(height..height).first()
|
||||||
//
|
//
|
||||||
// private val lightwalletd
|
// private val lightwalletd
|
||||||
// get() = (synchronizer as SdkSynchronizer).processor.downloader.lightwalletService
|
// get() = (synchronizer as SdkSynchronizer).processor.downloader.lightWalletClient
|
||||||
//
|
//
|
||||||
// companion object {
|
// companion object {
|
||||||
// private const val port = 9067
|
// private const val port = 9067
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
package cash.z.ecc.android.sdk.darkside.test
|
package cash.z.ecc.android.sdk.darkside.test
|
||||||
|
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.Darkside
|
import cash.z.ecc.android.sdk.model.Darkside
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.internal.DarksideApi
|
||||||
|
import co.electriccoin.lightwallet.client.internal.new
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import io.grpc.StatusRuntimeException
|
import io.grpc.StatusRuntimeException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
@ -54,13 +58,16 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
|
||||||
try {
|
try {
|
||||||
twig("entering the darkside")
|
twig("entering the darkside")
|
||||||
initiate()
|
initiate()
|
||||||
synchronizer.getServerInfo().apply {
|
|
||||||
assertTrue(
|
// In the future, we may want to have the SDK internally verify being on the darkside by matching the network type
|
||||||
"Error: not on the darkside",
|
|
||||||
vendor.contains("dark", true)
|
// synchronizer.getServerInfo().apply {
|
||||||
or chainName.contains("dark", true)
|
// assertTrue(
|
||||||
)
|
// "Error: not on the darkside",
|
||||||
}
|
// vendor.contains("dark", true)
|
||||||
|
// or chainName.contains("dark", true)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
twig("darkside initiation complete!")
|
twig("darkside initiation complete!")
|
||||||
} catch (error: StatusRuntimeException) {
|
} catch (error: StatusRuntimeException) {
|
||||||
Assert.fail(
|
Assert.fail(
|
||||||
|
@ -76,9 +83,8 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
|
||||||
*/
|
*/
|
||||||
fun initiate() {
|
fun initiate() {
|
||||||
twig("*************** INITIALIZING TEST COORDINATOR (ONLY ONCE) ***********************")
|
twig("*************** INITIALIZING TEST COORDINATOR (ONLY ONCE) ***********************")
|
||||||
val channel = synchronizer.channel
|
darkside = DarksideApi.new(ApplicationProvider.getApplicationContext(), LightWalletEndpoint.Darkside)
|
||||||
darkside = DarksideApi(channel)
|
darkside.reset(BlockHeightUnsafe(wallet.network.saplingActivationHeight.value))
|
||||||
darkside.reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun triggerSmallReorg() {
|
// fun triggerSmallReorg() {
|
||||||
|
@ -243,30 +249,30 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
|
||||||
tipHeight: BlockHeight = startHeight + 100
|
tipHeight: BlockHeight = startHeight + 100
|
||||||
): DarksideChainMaker = apply {
|
): DarksideChainMaker = apply {
|
||||||
darkside
|
darkside
|
||||||
.reset(startHeight)
|
.reset(BlockHeightUnsafe(startHeight.value))
|
||||||
.stageBlocks(blocksUrl)
|
.stageBlocks(blocksUrl)
|
||||||
applyTipHeight(tipHeight)
|
applyTipHeight(tipHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stageTransaction(url: String, targetHeight: BlockHeight): DarksideChainMaker = apply {
|
fun stageTransaction(url: String, targetHeight: BlockHeight): DarksideChainMaker = apply {
|
||||||
darkside.stageTransactions(url, targetHeight)
|
darkside.stageTransactions(url, BlockHeightUnsafe(targetHeight.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stageTransactions(targetHeight: BlockHeight, vararg urls: String): DarksideChainMaker = apply {
|
fun stageTransactions(targetHeight: BlockHeight, vararg urls: String): DarksideChainMaker = apply {
|
||||||
urls.forEach {
|
urls.forEach {
|
||||||
darkside.stageTransactions(it, targetHeight)
|
darkside.stageTransactions(it, BlockHeightUnsafe(targetHeight.value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stageEmptyBlocks(startHeight: BlockHeight, count: Int = 10): DarksideChainMaker = apply {
|
fun stageEmptyBlocks(startHeight: BlockHeight, count: Int = 10): DarksideChainMaker = apply {
|
||||||
darkside.stageEmptyBlocks(startHeight, count)
|
darkside.stageEmptyBlocks(BlockHeightUnsafe(startHeight.value), count)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stageEmptyBlock() = stageEmptyBlocks(lastTipHeight!! + 1, 1)
|
fun stageEmptyBlock() = stageEmptyBlocks(lastTipHeight!! + 1, 1)
|
||||||
|
|
||||||
fun applyTipHeight(tipHeight: BlockHeight): DarksideChainMaker = apply {
|
fun applyTipHeight(tipHeight: BlockHeight): DarksideChainMaker = apply {
|
||||||
twig("applying tip height of $tipHeight")
|
twig("applying tip height of $tipHeight")
|
||||||
darkside.applyBlocks(tipHeight)
|
darkside.applyBlocks(BlockHeightUnsafe(tipHeight.value))
|
||||||
lastTipHeight = tipHeight
|
lastTipHeight = tipHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,7 +283,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
|
||||||
*/
|
*/
|
||||||
fun makeSimpleChain() {
|
fun makeSimpleChain() {
|
||||||
darkside
|
darkside
|
||||||
.reset(DEFAULT_START_HEIGHT)
|
.reset(BlockHeightUnsafe(DEFAULT_START_HEIGHT.value))
|
||||||
.stageBlocks("https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-incoming/blocks.txt")
|
.stageBlocks("https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-incoming/blocks.txt")
|
||||||
applyTipHeight(DEFAULT_START_HEIGHT + 100)
|
applyTipHeight(DEFAULT_START_HEIGHT + 100)
|
||||||
}
|
}
|
||||||
|
@ -285,13 +291,13 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
|
||||||
fun advanceBy(numEmptyBlocks: Int) {
|
fun advanceBy(numEmptyBlocks: Int) {
|
||||||
val nextBlock = lastTipHeight!! + 1
|
val nextBlock = lastTipHeight!! + 1
|
||||||
twig("adding $numEmptyBlocks empty blocks to the chain starting at $nextBlock")
|
twig("adding $numEmptyBlocks empty blocks to the chain starting at $nextBlock")
|
||||||
darkside.stageEmptyBlocks(nextBlock, numEmptyBlocks)
|
darkside.stageEmptyBlocks(BlockHeightUnsafe(nextBlock.value), numEmptyBlocks)
|
||||||
applyTipHeight(nextBlock + numEmptyBlocks)
|
applyTipHeight(nextBlock + numEmptyBlocks)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyPendingTransactions(targetHeight: BlockHeight = lastTipHeight!! + 1) {
|
fun applyPendingTransactions(targetHeight: BlockHeight = lastTipHeight!! + 1) {
|
||||||
stageEmptyBlocks(lastTipHeight!! + 1, (targetHeight.value - lastTipHeight!!.value).toInt())
|
stageEmptyBlocks(lastTipHeight!! + 1, (targetHeight.value - lastTipHeight!!.value).toInt())
|
||||||
darkside.stageTransactions(darkside.getSentTransactions()?.iterator(), targetHeight)
|
darkside.stageTransactions(darkside.getSentTransactions()?.iterator(), BlockHeightUnsafe(targetHeight.value))
|
||||||
applyTipHeight(targetHeight)
|
applyTipHeight(targetHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,17 +6,16 @@ import cash.z.ecc.android.bip39.toSeed
|
||||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.Darkside
|
import cash.z.ecc.android.sdk.model.Darkside
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.isPending
|
import cash.z.ecc.android.sdk.model.isPending
|
||||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
@ -72,10 +71,7 @@ class TestWallet(
|
||||||
endpoint,
|
endpoint,
|
||||||
seed,
|
seed,
|
||||||
startHeight
|
startHeight
|
||||||
)
|
) as SdkSynchronizer
|
||||||
as
|
|
||||||
SdkSynchronizer
|
|
||||||
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
|
|
||||||
|
|
||||||
val available get() = synchronizer.saplingBalances.value?.available
|
val available get() = synchronizer.saplingBalances.value?.available
|
||||||
val unifiedAddress =
|
val unifiedAddress =
|
||||||
|
|
|
@ -7,16 +7,18 @@ import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
||||||
import cash.z.ecc.android.sdk.ext.toHex
|
import cash.z.ecc.android.sdk.ext.toHex
|
||||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.Mainnet
|
import cash.z.ecc.android.sdk.model.Mainnet
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||||
import cash.z.ecc.android.sdk.model.isFailure
|
import cash.z.ecc.android.sdk.model.isFailure
|
||||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||||
|
import co.electriccoin.lightwallet.client.BlockingLightWalletClient
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.new
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
|
@ -63,7 +65,8 @@ class SampleCodeTest {
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////
|
||||||
// Get Address
|
// Get Address
|
||||||
@Test fun getAddress() = runBlocking {
|
@Test
|
||||||
|
fun getAddress() = runBlocking {
|
||||||
val address = synchronizer.getUnifiedAddress()
|
val address = synchronizer.getUnifiedAddress()
|
||||||
assertFalse(address.isBlank())
|
assertFalse(address.isBlank())
|
||||||
log("Address: $address")
|
log("Address: $address")
|
||||||
|
@ -71,25 +74,37 @@ class SampleCodeTest {
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////
|
||||||
// Derive address from Extended Full Viewing Key
|
// Derive address from Extended Full Viewing Key
|
||||||
@Test fun getAddressFromViewingKey() {
|
@Test
|
||||||
|
fun getAddressFromViewingKey() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////
|
||||||
// Query latest block height
|
// Query latest block height
|
||||||
@Test fun getLatestBlockHeightTest() {
|
@Test
|
||||||
val lightwalletService = LightWalletGrpcService.new(context, lightwalletdHost)
|
fun getLatestBlockHeightTest() {
|
||||||
log("Latest Block: ${lightwalletService.getLatestBlockHeight()}")
|
val lightwalletClient = BlockingLightWalletClient.new(context, lightwalletdHost)
|
||||||
|
log("Latest Block: ${lightwalletClient.getLatestBlockHeight()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////
|
||||||
// Download compact block range
|
// Download compact block range
|
||||||
@Test fun getBlockRange() {
|
@Test
|
||||||
val blockRange = BlockHeight.new(ZcashNetwork.Mainnet, 500_000)..BlockHeight.new(
|
fun getBlockRange() {
|
||||||
ZcashNetwork.Mainnet,
|
val blockRange = BlockHeightUnsafe(
|
||||||
500_009
|
BlockHeight.new(
|
||||||
|
ZcashNetwork.Mainnet,
|
||||||
|
500_000
|
||||||
|
).value
|
||||||
|
)..BlockHeightUnsafe(
|
||||||
|
(
|
||||||
|
BlockHeight.new(
|
||||||
|
ZcashNetwork.Mainnet,
|
||||||
|
500_009
|
||||||
|
).value
|
||||||
|
)
|
||||||
)
|
)
|
||||||
val lightwalletService = LightWalletGrpcService.new(context, lightwalletdHost)
|
val lightwalletClient = BlockingLightWalletClient.new(context, lightwalletdHost)
|
||||||
val blocks = lightwalletService.getBlockRange(blockRange)
|
val blocks = lightwalletClient.getBlockRange(blockRange)
|
||||||
assertEquals(blockRange.endInclusive.value - blockRange.start.value, blocks.count())
|
assertEquals(blockRange.endInclusive.value - blockRange.start.value, blocks.count())
|
||||||
|
|
||||||
blocks.forEachIndexed { i, block ->
|
blocks.forEachIndexed { i, block ->
|
||||||
|
@ -99,12 +114,14 @@ class SampleCodeTest {
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////
|
||||||
// Query account outgoing transactions
|
// Query account outgoing transactions
|
||||||
@Test fun queryOutgoingTransactions() {
|
@Test
|
||||||
|
fun queryOutgoingTransactions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////
|
||||||
// Query account incoming transactions
|
// Query account incoming transactions
|
||||||
@Test fun queryIncomingTransactions() {
|
@Test
|
||||||
|
fun queryIncomingTransactions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// // ///////////////////////////////////////////////////
|
// // ///////////////////////////////////////////////////
|
||||||
|
@ -126,7 +143,8 @@ class SampleCodeTest {
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////
|
||||||
// Create a signed transaction (with memo) and broadcast
|
// Create a signed transaction (with memo) and broadcast
|
||||||
@Test fun submitTransaction() = runBlocking {
|
@Test
|
||||||
|
fun submitTransaction() = runBlocking {
|
||||||
val amount = 0.123.convertZecToZatoshi()
|
val amount = 0.123.convertZecToZatoshi()
|
||||||
val address = "ztestsapling1tklsjr0wyw0d58f3p7wufvrj2cyfv6q6caumyueadq8qvqt8lda6v6tpx474rfru9y6u75u7qnw"
|
val address = "ztestsapling1tklsjr0wyw0d58f3p7wufvrj2cyfv6q6caumyueadq8qvqt8lda6v6tpx474rfru9y6u75u7qnw"
|
||||||
val memo = "Test Transaction"
|
val memo = "Test Transaction"
|
||||||
|
|
|
@ -23,11 +23,11 @@ import kotlinx.coroutines.launch
|
||||||
abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
|
abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Since the lightWalletService is not a component that apps typically use, directly, we provide
|
* Since the lightwalletClient is not a component that apps typically use, directly, we provide
|
||||||
* this from one place. Everything that can be done with the service can/should be done with the
|
* this from one place. Everything that can be done with the service can/should be done with the
|
||||||
* synchronizer because it wraps the service.
|
* synchronizer because it wraps the service.
|
||||||
*/
|
*/
|
||||||
val lightWalletService get() = mainActivity()?.lightWalletService
|
val lightWalletClient get() = mainActivity()?.lightwalletClient
|
||||||
|
|
||||||
// contains view information provided by the user
|
// contains view information provided by the user
|
||||||
val sharedViewModel: SharedViewModel by activityViewModels()
|
val sharedViewModel: SharedViewModel by activityViewModels()
|
||||||
|
|
|
@ -21,12 +21,12 @@ import androidx.navigation.ui.setupActionBarWithNavController
|
||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletService
|
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.BlockingLightWalletClient
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.new
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ class MainActivity :
|
||||||
* this object because it would utilize the synchronizer, instead, which exposes APIs that
|
* this object because it would utilize the synchronizer, instead, which exposes APIs that
|
||||||
* automatically sync with the server.
|
* automatically sync with the server.
|
||||||
*/
|
*/
|
||||||
var lightWalletService: LightWalletService? = null
|
var lightwalletClient: BlockingLightWalletClient? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -81,7 +81,7 @@ class MainActivity :
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
lightWalletService?.shutdown()
|
lightwalletClient?.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
@ -127,11 +127,11 @@ class MainActivity :
|
||||||
//
|
//
|
||||||
|
|
||||||
private fun initService() {
|
private fun initService() {
|
||||||
if (lightWalletService != null) {
|
if (lightwalletClient != null) {
|
||||||
lightWalletService?.shutdown()
|
lightwalletClient?.shutdown()
|
||||||
}
|
}
|
||||||
val network = ZcashNetwork.fromResources(applicationContext)
|
val network = ZcashNetwork.fromResources(applicationContext)
|
||||||
lightWalletService = LightWalletGrpcService.new(
|
lightwalletClient = BlockingLightWalletClient.new(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
LightWalletEndpoint.defaultForNetwork(network)
|
LightWalletEndpoint.defaultForNetwork(network)
|
||||||
)
|
)
|
||||||
|
@ -174,7 +174,6 @@ class MainActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDrawerOpened(drawerView: View) {
|
override fun onDrawerOpened(drawerView: View) {
|
||||||
twig("Drawer opened.")
|
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,14 @@ import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.android.bip39.toSeed
|
import cash.z.ecc.android.bip39.toSeed
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||||
import cash.z.ecc.android.sdk.ext.BenchmarkingExt
|
|
||||||
import cash.z.ecc.android.sdk.ext.onFirst
|
import cash.z.ecc.android.sdk.ext.onFirst
|
||||||
import cash.z.ecc.android.sdk.fixture.BlockRangeFixture
|
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.ext.BenchmarkingExt
|
||||||
|
import co.electriccoin.lightwallet.client.fixture.BlockRangeFixture
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -78,7 +78,7 @@ class SharedViewModel(application: Application) : AndroidViewModel(application)
|
||||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||||
seed = seedBytes,
|
seed = seedBytes,
|
||||||
birthday = if (BenchmarkingExt.isBenchmarking()) {
|
birthday = if (BenchmarkingExt.isBenchmarking()) {
|
||||||
BlockRangeFixture.new().start
|
BlockHeight.new(ZcashNetwork.Mainnet, BlockRangeFixture.new().start)
|
||||||
} else {
|
} else {
|
||||||
birthdayHeight.value
|
birthdayHeight.value
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@ import cash.z.ecc.android.sdk.demoapp.model.PersistableWallet
|
||||||
import cash.z.ecc.android.sdk.demoapp.util.Twig
|
import cash.z.ecc.android.sdk.demoapp.util.Twig
|
||||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||||
import cash.z.ecc.android.sdk.ext.onFirst
|
import cash.z.ecc.android.sdk.ext.onFirst
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
|
@ -15,10 +15,11 @@ import cash.z.ecc.android.sdk.demoapp.util.withCommas
|
||||||
import cash.z.ecc.android.sdk.ext.toHex
|
import cash.z.ecc.android.sdk.ext.toHex
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a compact block from the lightwalletd service and displays basic information about it.
|
* Retrieves a compact block from the lightwalletd server and displays basic information about it.
|
||||||
* This demonstrates the basic ability to connect to the server, request a compact block and parse
|
* This demonstrates the basic ability to connect to the server, request a compact block and parse
|
||||||
* the response.
|
* the response.
|
||||||
*/
|
*/
|
||||||
|
@ -26,7 +27,11 @@ class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
|
||||||
|
|
||||||
private fun setBlockHeight(blockHeight: BlockHeight) {
|
private fun setBlockHeight(blockHeight: BlockHeight) {
|
||||||
val blocks =
|
val blocks =
|
||||||
lightWalletService?.getBlockRange(blockHeight..blockHeight)
|
lightWalletClient?.getBlockRange(
|
||||||
|
BlockHeightUnsafe(blockHeight.value)..BlockHeightUnsafe(
|
||||||
|
blockHeight.value
|
||||||
|
)
|
||||||
|
)
|
||||||
val block = blocks?.firstOrNull()
|
val block = blocks?.firstOrNull()
|
||||||
binding.textInfo.visibility = View.VISIBLE
|
binding.textInfo.visibility = View.VISIBLE
|
||||||
binding.textInfo.text = HtmlCompat.fromHtml(
|
binding.textInfo.text = HtmlCompat.fromHtml(
|
||||||
|
|
|
@ -14,10 +14,11 @@ import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime
|
||||||
import cash.z.ecc.android.sdk.demoapp.util.withCommas
|
import cash.z.ecc.android.sdk.demoapp.util.withCommas
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a range of compact block from the lightwalletd service and displays basic information
|
* Retrieves a range of compact block from the lightwalletd server and displays basic information
|
||||||
* about them. This demonstrates the basic ability to connect to the server, request a range of
|
* about them. This demonstrates the basic ability to connect to the server, request a range of
|
||||||
* compact block and parse the response. This could be augmented to display metadata about certain
|
* compact block and parse the response. This could be augmented to display metadata about certain
|
||||||
* block ranges for instance, to find the block with the most shielded transactions in a range.
|
* block ranges for instance, to find the block with the most shielded transactions in a range.
|
||||||
|
@ -27,7 +28,11 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
|
||||||
private fun setBlockRange(blockRange: ClosedRange<BlockHeight>) {
|
private fun setBlockRange(blockRange: ClosedRange<BlockHeight>) {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val blocks =
|
val blocks =
|
||||||
lightWalletService?.getBlockRange(blockRange)
|
lightWalletClient?.getBlockRange(
|
||||||
|
BlockHeightUnsafe(blockRange.start.value)..BlockHeightUnsafe(
|
||||||
|
blockRange.endInclusive.value
|
||||||
|
)
|
||||||
|
)
|
||||||
val fetchDelta = System.currentTimeMillis() - start
|
val fetchDelta = System.currentTimeMillis() - start
|
||||||
|
|
||||||
// Note: This is a demo so we won't worry about iterating efficiently over these blocks
|
// Note: This is a demo so we won't worry about iterating efficiently over these blocks
|
||||||
|
@ -95,7 +100,12 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
|
||||||
setText(R.string.loading)
|
setText(R.string.loading)
|
||||||
binding.textInfo.setText(R.string.loading)
|
binding.textInfo.setText(R.string.loading)
|
||||||
post {
|
post {
|
||||||
setBlockRange(BlockHeight.new(network, start)..BlockHeight.new(network, end))
|
setBlockRange(
|
||||||
|
BlockHeight.new(network, start)..BlockHeight.new(
|
||||||
|
network,
|
||||||
|
end
|
||||||
|
)
|
||||||
|
)
|
||||||
isEnabled = true
|
isEnabled = true
|
||||||
setText(R.string.apply)
|
setText(R.string.apply)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ class GetLatestHeightFragment : BaseDemoFragment<FragmentGetLatestHeightBinding>
|
||||||
private fun displayLatestHeight() {
|
private fun displayLatestHeight() {
|
||||||
// note: this is a blocking call, a real app wouldn't do this on the main thread
|
// note: this is a blocking call, a real app wouldn't do this on the main thread
|
||||||
// instead, a production app would leverage the synchronizer like in the other demos
|
// instead, a production app would leverage the synchronizer like in the other demos
|
||||||
binding.textInfo.text = lightWalletService?.getLatestBlockHeight().toString()
|
binding.textInfo.text = lightWalletClient?.getLatestBlockHeight().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -21,6 +21,7 @@ import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
@ -84,12 +85,12 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
||||||
?: getUxtoEndHeight(requireApplicationContext()).value
|
?: getUxtoEndHeight(requireApplicationContext()).value
|
||||||
var allStart = now
|
var allStart = now
|
||||||
twig("loading transactions in range $startToUse..$endToUse")
|
twig("loading transactions in range $startToUse..$endToUse")
|
||||||
val txids = lightWalletService?.getTAddressTransactions(
|
val txids = lightWalletClient?.getTAddressTransactions(
|
||||||
addressToUse,
|
addressToUse,
|
||||||
BlockHeight.new(network, startToUse)..BlockHeight.new(network, endToUse)
|
BlockHeightUnsafe(startToUse)..BlockHeightUnsafe(endToUse)
|
||||||
)
|
)
|
||||||
var delta = now - allStart
|
var delta = now - allStart
|
||||||
updateStatus("found ${txids?.size} transactions in ${delta}ms.", false)
|
updateStatus("found ${txids?.toList()?.size} transactions in ${delta}ms.", false)
|
||||||
|
|
||||||
txids?.map {
|
txids?.map {
|
||||||
// Disabled during migration to newer SDK version; this appears to have been
|
// Disabled during migration to newer SDK version; this appears to have been
|
||||||
|
|
|
@ -2,8 +2,8 @@ package cash.z.ecc.android.sdk.demoapp.util
|
||||||
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.tracing.Trace
|
import androidx.tracing.Trace
|
||||||
import cash.z.ecc.android.sdk.ext.BenchmarkingExt
|
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
|
import co.electriccoin.lightwallet.client.ext.BenchmarkingExt
|
||||||
|
|
||||||
interface BenchmarkTrace {
|
interface BenchmarkTrace {
|
||||||
fun checkMainThread() {
|
fun checkMainThread() {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import android.content.Context
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import cash.z.ecc.android.sdk.demoapp.MainActivity
|
import cash.z.ecc.android.sdk.demoapp.MainActivity
|
||||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
import cash.z.wallet.sdk.internal.rpc.CompactFormats
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy extensions to make demo life easier.
|
* Lazy extensions to make demo life easier.
|
||||||
|
|
|
@ -7,18 +7,47 @@ Thankfully, the only thing an app developer has to be concerned with is the foll
|
||||||
|
|
||||||
![SDK Diagram Developer Perspective](assets/sdk_dev_pov_final.png?raw=true "SDK Diagram Dev PoV")
|
![SDK Diagram Developer Perspective](assets/sdk_dev_pov_final.png?raw=true "SDK Diagram Dev PoV")
|
||||||
|
|
||||||
# Components
|
# Modules
|
||||||
|
The SDK is broken down into several logical components, implemented as Gradle modules. At a high level, the modularization is:
|
||||||
|
|
||||||
| Component | Summary |
|
* sdk-lib — Compiles all of the modules together for the SDK.
|
||||||
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
* lightwallet-client-lib — Provides a set of Kotlin APIs for interacting with lightwalletd over the network.
|
||||||
| **LightWalletService** | Service used for requesting compact blocks |
|
* darkside-test-lib — Contains integration tests for the SDK, running against a localhost lightwalletd instance running in darkside mode. This is not run as part of the SDK test suite, because it requires some manual setup to enable.
|
||||||
| **CompactBlockStore** | Stores compact blocks that have been downloaded from the `LightWalletService` |
|
* demo-app — Contains a primitive demo application to exercise the SDK.
|
||||||
| **CompactBlockProcessor** | Validates and scans the compact blocks in the `CompactBlockStore` for transaction details |
|
|
||||||
| **OutboundTransactionManager** | Creates, Submits and manages transactions for spending funds |
|
|
||||||
| **DerivationTool** | Utilities for deriving keys and addresses |
|
|
||||||
| **RustBackend** | Wraps and simplifies the rust library and exposes its functionality to the Kotlin SDK |
|
|
||||||
|
|
||||||
# Checkpoints
|
```mermaid
|
||||||
|
flowchart TB;
|
||||||
|
lightwalletClientLib[[lightwallet-client-lib]] --> sdkLib[[sdk-lib]];
|
||||||
|
sdkLib[[sdk-lib]] --> demoApp[[demo-app]];
|
||||||
|
sdkLib[[sdk-lib]] --> darksideTestLib[[darkside-test-lib]];
|
||||||
|
```
|
||||||
|
|
||||||
|
# Data model
|
||||||
|
Before diving into some of the module specifics, it is helpful to provide some context on the data model representations as data flows between the different modules of this repository. There are multiple data representations, including:
|
||||||
|
|
||||||
|
1. Network — The wire representation for calls to and from the Lightwalletd server. These are generated by `protoc` at compile time. These are not generally a public API.
|
||||||
|
2. Unsafe — The representation provided as the output from `lightwallet-client-lib`. These values are not necessarily validated, hence the smurf naming with the suffix `Unsafe`. These are not generally a public API for clients of the SDK.
|
||||||
|
3. SDK — Objects exposed as a public API for clients of the SDK.
|
||||||
|
|
||||||
|
# lightwallet-client-lib
|
||||||
|
This library is a work-in-progress.
|
||||||
|
|
||||||
|
This is generally not considered part of the public API, and much of the internals do not guarantee API stability. Internally, the implementation uses GRPC although over time that should be hidden from clients.
|
||||||
|
|
||||||
|
# sdk-lib
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
| Component | Summary |
|
||||||
|
|--------------------------------|-------------------------------------------------------------------------------------------|
|
||||||
|
| **LightWalletClient** | Component used for requesting compact blocks |
|
||||||
|
| **CompactBlockStore** | Stores compact blocks that have been downloaded from the `LightWalletClient` |
|
||||||
|
| **CompactBlockProcessor** | Validates and scans the compact blocks in the `CompactBlockStore` for transaction details |
|
||||||
|
| **OutboundTransactionManager** | Creates, Submits and manages transactions for spending funds |
|
||||||
|
| **DerivationTool** | Utilities for deriving keys and addresses |
|
||||||
|
| **RustBackend** | Wraps and simplifies the rust library and exposes its functionality to the Kotlin SDK |
|
||||||
|
|
||||||
|
## Checkpoints
|
||||||
To improve the speed of syncing with the Zcash network, the SDK contains a series of embedded checkpoints. These should be updated periodically, as new transactions are added to the network. Checkpoints are stored under the [sdk-lib's assets](../sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint) directory as JSON files. Checkpoints for both mainnet and testnet are bundled into the SDK.
|
To improve the speed of syncing with the Zcash network, the SDK contains a series of embedded checkpoints. These should be updated periodically, as new transactions are added to the network. Checkpoints are stored under the [sdk-lib's assets](../sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint) directory as JSON files. Checkpoints for both mainnet and testnet are bundled into the SDK.
|
||||||
|
|
||||||
To update the checkpoints, see [Checkmate](https://github.com/zcash-hackworks/checkmate).
|
To update the checkpoints, see [Checkmate](https://github.com/zcash-hackworks/checkmate).
|
||||||
|
|
|
@ -22,7 +22,7 @@ ZCASH_ASCII_GPG_KEY=
|
||||||
# Configures whether release is an unstable snapshot, therefore published to the snapshot repository.
|
# Configures whether release is an unstable snapshot, therefore published to the snapshot repository.
|
||||||
IS_SNAPSHOT=true
|
IS_SNAPSHOT=true
|
||||||
|
|
||||||
LIBRARY_VERSION=1.12.0-beta01
|
LIBRARY_VERSION=1.13.0-beta01
|
||||||
|
|
||||||
# Kotlin compiler warnings can be considered errors, failing the build.
|
# Kotlin compiler warnings can be considered errors, failing the build.
|
||||||
ZCASH_IS_TREAT_WARNINGS_AS_ERRORS=true
|
ZCASH_IS_TREAT_WARNINGS_AS_ERRORS=true
|
||||||
|
@ -116,6 +116,7 @@ ANDROIDX_UI_AUTOMATOR_VERSION=2.3.0-alpha02
|
||||||
BIP39_VERSION=1.0.4
|
BIP39_VERSION=1.0.4
|
||||||
COROUTINES_OKHTTP=1.0
|
COROUTINES_OKHTTP=1.0
|
||||||
GOOGLE_MATERIAL_VERSION=1.7.0
|
GOOGLE_MATERIAL_VERSION=1.7.0
|
||||||
|
GRPC_KOTLIN_VERSION=1.3.0
|
||||||
GRPC_VERSION=1.52.1
|
GRPC_VERSION=1.52.1
|
||||||
GSON_VERSION=2.9.0
|
GSON_VERSION=2.9.0
|
||||||
GUAVA_VERSION=31.1-android
|
GUAVA_VERSION=31.1-android
|
||||||
|
|
|
@ -0,0 +1,240 @@
|
||||||
|
import com.google.protobuf.gradle.generateProtoTasks
|
||||||
|
import com.google.protobuf.gradle.id
|
||||||
|
import com.google.protobuf.gradle.plugins
|
||||||
|
import com.google.protobuf.gradle.proto
|
||||||
|
import com.google.protobuf.gradle.protobuf
|
||||||
|
import com.google.protobuf.gradle.protoc
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("zcash-sdk.android-conventions")
|
||||||
|
|
||||||
|
id("org.jetbrains.dokka")
|
||||||
|
id("com.google.protobuf")
|
||||||
|
|
||||||
|
id("wtf.emulator.gradle")
|
||||||
|
id("zcash-sdk.emulator-wtf-conventions")
|
||||||
|
|
||||||
|
id("maven-publish")
|
||||||
|
id("signing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publishing information
|
||||||
|
val publicationVariant = "release"
|
||||||
|
val myVersion = project.property("LIBRARY_VERSION").toString()
|
||||||
|
val myArtifactId = "client"
|
||||||
|
val isSnapshot = project.property("IS_SNAPSHOT").toString().toBoolean()
|
||||||
|
project.group = "co.electriccoin.lightwallet"
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
register<MavenPublication>("release") {
|
||||||
|
artifactId = myArtifactId
|
||||||
|
version = if (isSnapshot) {
|
||||||
|
"$myVersion-SNAPSHOT"
|
||||||
|
} else {
|
||||||
|
myVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
from(components[publicationVariant])
|
||||||
|
}
|
||||||
|
|
||||||
|
pom {
|
||||||
|
name.set("Zcash Light Wallet Client")
|
||||||
|
description.set("Client API for connecting to the Light Wallet server.")
|
||||||
|
url.set("https://github.com/zcash/zcash-android-wallet-sdk/")
|
||||||
|
inceptionYear.set("2022")
|
||||||
|
scm {
|
||||||
|
url.set("https://github.com/zcash/zcash-android-wallet-sdk/")
|
||||||
|
connection.set("scm:git:git://github.com/zcash/zcash-android-wallet-sdk.git")
|
||||||
|
developerConnection.set("scm:git:ssh://git@github.com/zcash/zcash-android-wallet-sdk.git")
|
||||||
|
}
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id.set("zcash")
|
||||||
|
name.set("Zcash")
|
||||||
|
url.set("https://github.com/zcash/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name.set("The MIT License")
|
||||||
|
url.set("http://opensource.org/licenses/MIT")
|
||||||
|
distribution.set("repo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
val mavenUrl = if (isSnapshot) {
|
||||||
|
project.property("ZCASH_MAVEN_PUBLISH_SNAPSHOT_URL").toString()
|
||||||
|
} else {
|
||||||
|
project.property("ZCASH_MAVEN_PUBLISH_RELEASE_URL").toString()
|
||||||
|
}
|
||||||
|
val mavenPublishUsername = project.property("ZCASH_MAVEN_PUBLISH_USERNAME").toString()
|
||||||
|
val mavenPublishPassword = project.property("ZCASH_MAVEN_PUBLISH_PASSWORD").toString()
|
||||||
|
|
||||||
|
mavenLocal {
|
||||||
|
name = "MavenLocal"
|
||||||
|
}
|
||||||
|
maven(mavenUrl) {
|
||||||
|
name = "MavenCentral"
|
||||||
|
credentials {
|
||||||
|
username = mavenPublishUsername
|
||||||
|
password = mavenPublishPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "co.electriccoin.lightwallet.client"
|
||||||
|
useLibrary("android.test.runner")
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
consumerProguardFiles("proguard-consumer.txt")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
getByName("debug").apply {
|
||||||
|
// test builds exceed the dex limit because they pull in large test libraries
|
||||||
|
multiDexEnabled = true
|
||||||
|
isMinifyEnabled = false
|
||||||
|
}
|
||||||
|
getByName("release").apply {
|
||||||
|
multiDexEnabled = false
|
||||||
|
isMinifyEnabled = project.property("IS_MINIFY_SDK_ENABLED").toString().toBoolean()
|
||||||
|
proguardFiles.addAll(
|
||||||
|
listOf(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
File("proguard-project.txt")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
create("benchmark") {
|
||||||
|
// We provide the extra benchmark build type just for benchmarking purposes
|
||||||
|
initWith(buildTypes.getByName("release"))
|
||||||
|
matchingFallbacks += listOf("release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets.getByName("main") {
|
||||||
|
proto { srcDir("src/main/proto") }
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
|
baseline = File("lint-baseline.xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
singleVariant(publicationVariant) {
|
||||||
|
withSourcesJar()
|
||||||
|
withJavadocJar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
onVariants { variant ->
|
||||||
|
if (variant.name.toLowerCase(Locale.US).contains("release")) {
|
||||||
|
variant.packaging.resources.excludes.addAll(
|
||||||
|
listOf(
|
||||||
|
"META-INF/ASL2.0",
|
||||||
|
"META-INF/DEPENDENCIES",
|
||||||
|
"META-INF/LICENSE",
|
||||||
|
"META-INF/LICENSE-notice.md",
|
||||||
|
"META-INF/LICENSE.md",
|
||||||
|
"META-INF/LICENSE.txt",
|
||||||
|
"META-INF/NOTICE",
|
||||||
|
"META-INF/NOTICE.txt",
|
||||||
|
"META-INF/license.txt",
|
||||||
|
"META-INF/notice.txt"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.dokkaHtml.configure {
|
||||||
|
dokkaSourceSets {
|
||||||
|
configureEach {
|
||||||
|
outputDirectory.set(file("build/docs/rtd"))
|
||||||
|
displayName.set("Lightwallet Client")
|
||||||
|
includes.from("packages.md")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = libs.protoc.compiler.get().asCoordinateString()
|
||||||
|
}
|
||||||
|
plugins {
|
||||||
|
id("java") {
|
||||||
|
artifact = libs.protoc.gen.java.get().asCoordinateString()
|
||||||
|
}
|
||||||
|
id("grpc") {
|
||||||
|
artifact = libs.protoc.gen.java.get().asCoordinateString()
|
||||||
|
}
|
||||||
|
id("grpckt") {
|
||||||
|
artifact = libs.protoc.gen.kotlin.get().asCoordinateString() + ":jdk8@jar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generateProtoTasks {
|
||||||
|
all().forEach {
|
||||||
|
it.plugins {
|
||||||
|
id("java") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
id("grpc") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
id("grpckt") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.builtins {
|
||||||
|
id("kotlin") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.androidx.annotation)
|
||||||
|
implementation(libs.kotlin.stdlib)
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
|
||||||
|
// TODO [#673]: Make `implementation` https://github.com/zcash/zcash-android-wallet-sdk/issues/673
|
||||||
|
api(libs.bundles.grpc)
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
testImplementation(libs.kotlin.reflect)
|
||||||
|
testImplementation(libs.kotlin.test)
|
||||||
|
testImplementation(libs.grpc.testing)
|
||||||
|
|
||||||
|
androidTestImplementation(libs.androidx.multidex)
|
||||||
|
androidTestImplementation(libs.androidx.test.runner)
|
||||||
|
androidTestImplementation(libs.androidx.test.junit)
|
||||||
|
androidTestImplementation(libs.androidx.test.core)
|
||||||
|
androidTestImplementation(libs.coroutines.okhttp)
|
||||||
|
androidTestImplementation(libs.kotlin.test)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
getByName("preBuild").dependsOn(create("bugfixTask") {
|
||||||
|
doFirst {
|
||||||
|
mkdir("build/extracted-include-protos/main")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MinimalExternalModuleDependency.asCoordinateString() =
|
||||||
|
"${module.group}:${module.name}:${versionConstraint.displayName}"
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<issues format="5" by="lint 4.2.1" client="gradle" variant="all" version="4.2.1">
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="DefaultLocale"
|
||||||
|
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
||||||
|
errorLine1=" (t.message?.toLowerCase()?.contains(unlessContains.toLowerCase()) == true)"
|
||||||
|
errorLine2=" ~~~~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/java/cash/z/ecc/android/sdk/ext/Ext.kt"
|
||||||
|
line="29"
|
||||||
|
column="29"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="DefaultLocale"
|
||||||
|
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
||||||
|
errorLine1=" (t.message?.toLowerCase()?.contains(unlessContains.toLowerCase()) == true)"
|
||||||
|
errorLine2=" ~~~~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/java/cash/z/ecc/android/sdk/ext/Ext.kt"
|
||||||
|
line="29"
|
||||||
|
column="68"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="DefaultLocale"
|
||||||
|
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
||||||
|
errorLine1=" (t.message?.toLowerCase()?.contains(ifContains.toLowerCase()) == false)"
|
||||||
|
errorLine2=" ~~~~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/java/cash/z/ecc/android/sdk/ext/Ext.kt"
|
||||||
|
line="33"
|
||||||
|
column="33"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="DefaultLocale"
|
||||||
|
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
||||||
|
errorLine1=" (t.message?.toLowerCase()?.contains(ifContains.toLowerCase()) == false)"
|
||||||
|
errorLine2=" ~~~~~~~~~~~">
|
||||||
|
<location
|
||||||
|
file="src/main/java/cash/z/ecc/android/sdk/ext/Ext.kt"
|
||||||
|
line="33"
|
||||||
|
column="68"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
</issues>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<lint>
|
||||||
|
|
||||||
|
</lint>
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Module lightwallet-client-lib
|
||||||
|
|
||||||
|
Light Wallet Client SDK.
|
||||||
|
|
||||||
|
# Package co.electriccoin.lightwallet.client.model
|
||||||
|
|
||||||
|
Integration tests designed to be executed with darksidewalletd.
|
|
@ -0,0 +1,22 @@
|
||||||
|
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# https://github.com/grpc/grpc-java/blob/master/android/proguard-rules.txt
|
||||||
|
-keepclassmembers class io.grpc.okhttp.OkHttpChannelBuilder {
|
||||||
|
io.grpc.okhttp.OkHttpChannelBuilder forTarget(java.lang.String);
|
||||||
|
io.grpc.okhttp.OkHttpChannelBuilder scheduledExecutorService(java.util.concurrent.ScheduledExecutorService);
|
||||||
|
io.grpc.okhttp.OkHttpChannelBuilder sslSocketFactory(javax.net.ssl.SSLSocketFactory);
|
||||||
|
io.grpc.okhttp.OkHttpChannelBuilder transportExecutor(java.util.concurrent.Executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prevent OKHttp from causing warnings for consumers of the SDK
|
||||||
|
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||||
|
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||||
|
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||||
|
-dontwarn org.conscrypt.Conscrypt
|
||||||
|
-dontwarn org.conscrypt.Conscrypt$Version
|
||||||
|
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||||
|
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||||
|
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||||
|
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
|
@ -0,0 +1,18 @@
|
||||||
|
# This improves obfuscation and moves non-public classes to their own namespace.
|
||||||
|
-repackageclasses 'co.electriccoin.lightwallet.client.internal'
|
||||||
|
|
||||||
|
# This makes it easier to autocomplete methods in an IDE using this obfuscated library.
|
||||||
|
-keepparameternames
|
||||||
|
|
||||||
|
# The ProGuard manual recommends keeping these attributes for libraries.
|
||||||
|
-keepattributes EnclosingMethod,InnerClasses,Signature,Exceptions,*Annotation*
|
||||||
|
|
||||||
|
# Ensure that stacktraces are reversible.
|
||||||
|
-renamesourcefileattribute SourceFile
|
||||||
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# Keep the public interface of the library.
|
||||||
|
# Some of these will need to be tuned in the future, as they shouldn't ALL be considered part of the
|
||||||
|
# public API. Much of this will be improved by further repackaging of the classes.
|
||||||
|
-keep public class co.electriccoin.lightwallet.client.model.* { public protected *; }
|
||||||
|
-keep public class co.electriccoin.lightwallet.client.* { public protected *; }
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- For code coverage -->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<application android:name="androidx.multidex.MultiDexApplication" />
|
||||||
|
</manifest>
|
|
@ -0,0 +1,22 @@
|
||||||
|
package co.electriccoin.lightwallet.client.ext
|
||||||
|
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import co.electriccoin.lightwallet.client.BuildConfig
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class BenchmarkingExtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SmallTest
|
||||||
|
fun check_build_config() {
|
||||||
|
val benchmarkType = "benchmark" // $NON-NLS
|
||||||
|
|
||||||
|
if (BuildConfig.BUILD_TYPE.contains(benchmarkType)) {
|
||||||
|
assertTrue(BenchmarkingExt.isBenchmarking())
|
||||||
|
} else {
|
||||||
|
assertFalse(BenchmarkingExt.isBenchmarking())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package co.electriccoin.lightwallet.client.fixture
|
||||||
|
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class BlockRangeFixtureTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SmallTest
|
||||||
|
fun compare_default_values() {
|
||||||
|
BlockRangeFixture.new().also {
|
||||||
|
assertEquals(BlockRangeFixture.BLOCK_HEIGHT_LOWER_BOUND, it.start)
|
||||||
|
assertEquals(BlockRangeFixture.BLOCK_HEIGHT_UPPER_BOUND, it.endInclusive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package co.electriccoin.lightwallet.client.fixture
|
||||||
|
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
|
||||||
|
object StatusExceptionFixture {
|
||||||
|
|
||||||
|
fun new(
|
||||||
|
status: Status
|
||||||
|
): StatusRuntimeException {
|
||||||
|
return StatusRuntimeException(status)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package co.electriccoin.lightwallet.client.internal
|
||||||
|
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import cash.z.ecc.android.sdk.test.getAppContext
|
||||||
|
import kotlin.test.Ignore
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class ChannelFactoryTest {
|
||||||
|
|
||||||
|
private val channelFactory = AndroidChannelFactory(getAppContext())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SmallTest
|
||||||
|
@Ignore("Finish the test once we can reach the needed model classes.")
|
||||||
|
fun new_channel_sanity_test() {
|
||||||
|
// TODO [#897]: Prepare new model classes module
|
||||||
|
// TODO [#897]: https://github.com/zcash/zcash-android-wallet-sdk/issues/897
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package co.electriccoin.lightwallet.client.internal
|
||||||
|
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import co.electriccoin.lightwallet.client.fixture.StatusExceptionFixture
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
|
import io.grpc.Status
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class GrpcStatusResolverTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SmallTest
|
||||||
|
fun resolve_explicitly_caught_server_error_test() {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus<Unit>(
|
||||||
|
StatusExceptionFixture.new(Status.NOT_FOUND)
|
||||||
|
).also { resolvedResponse ->
|
||||||
|
assertTrue(resolvedResponse is Response.Failure.Server.NotFound<Unit>)
|
||||||
|
assertEquals(resolvedResponse.code, Status.NOT_FOUND.code.value())
|
||||||
|
assertNull(resolvedResponse.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SmallTest
|
||||||
|
fun resolve_explicitly_caught_error_client_test() {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus<Unit>(
|
||||||
|
StatusExceptionFixture.new(Status.CANCELLED)
|
||||||
|
).also { resolvedResponse ->
|
||||||
|
assertTrue(resolvedResponse is Response.Failure.Client.Canceled<Unit>)
|
||||||
|
assertEquals(resolvedResponse.code, Status.CANCELLED.code.value())
|
||||||
|
assertNull(resolvedResponse.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SmallTest
|
||||||
|
fun resolve_other_test() {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus<Unit>(
|
||||||
|
StatusExceptionFixture.new(Status.INTERNAL)
|
||||||
|
).also { resolvedResponse ->
|
||||||
|
assertTrue(resolvedResponse is Response.Failure.Server.Other<Unit>)
|
||||||
|
assertEquals(resolvedResponse.code, Status.INTERNAL.code.value())
|
||||||
|
assertNull(resolvedResponse.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SmallTest
|
||||||
|
fun resolve_unexpected_error_test() {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus<Unit>(
|
||||||
|
IllegalArgumentException("This should not come into the resolver.")
|
||||||
|
).also { resolvedResponse ->
|
||||||
|
assertTrue(resolvedResponse is Response.Failure.Server.Unknown<Unit>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package cash.z.ecc.android.sdk.test
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
|
||||||
|
fun getAppContext(): Context = ApplicationProvider.getApplicationContext()
|
||||||
|
|
||||||
|
fun getStringResource(@StringRes resId: Int) = getAppContext().getString(resId)
|
||||||
|
|
||||||
|
fun getStringResourceWithArgs(@StringRes resId: Int, vararg formatArgs: String) =
|
||||||
|
getAppContext().getString(resId, *formatArgs)
|
|
@ -0,0 +1,4 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
</manifest>
|
|
@ -0,0 +1,103 @@
|
||||||
|
package co.electriccoin.lightwallet.client
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.CompactFormats
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.Service
|
||||||
|
import co.electriccoin.lightwallet.client.internal.AndroidChannelFactory
|
||||||
|
import co.electriccoin.lightwallet.client.internal.BlockingLightWalletClientImpl
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpointInfoUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.RawTransactionUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
|
import co.electriccoin.lightwallet.client.model.SendResponseUnsafe
|
||||||
|
|
||||||
|
// TODO [895]: Let remaining server calls adopt new response result
|
||||||
|
// TODO [895]: https://github.com/zcash/zcash-android-wallet-sdk/issues/895
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client for interacting with lightwalletd.
|
||||||
|
*/
|
||||||
|
interface BlockingLightWalletClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the full transaction info.
|
||||||
|
*/
|
||||||
|
fun fetchTransaction(txId: ByteArray): Response<RawTransactionUnsafe>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tAddress the transparent address to use.
|
||||||
|
* @param startHeight the starting height to use.
|
||||||
|
*
|
||||||
|
* @return the UTXOs for the given address from the [startHeight].
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException when empty argument provided
|
||||||
|
*/
|
||||||
|
fun fetchUtxos(
|
||||||
|
tAddress: String,
|
||||||
|
startHeight: BlockHeightUnsafe
|
||||||
|
): Sequence<Service.GetAddressUtxosReply>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param heightRange the inclusive range to fetch. For instance if 1..5 is given, then every
|
||||||
|
* block in that range will be fetched, including 1 and 5.
|
||||||
|
*
|
||||||
|
* @return a sequence of compact blocks for the given range
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException when empty argument provided
|
||||||
|
*/
|
||||||
|
fun getBlockRange(heightRange: ClosedRange<BlockHeightUnsafe>): Sequence<CompactFormats.CompactBlock>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the latest block height known to the service.
|
||||||
|
*/
|
||||||
|
fun getLatestBlockHeight(): Response<BlockHeightUnsafe>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return basic server information.
|
||||||
|
*/
|
||||||
|
fun getServerInfo(): Response<LightWalletEndpointInfoUnsafe>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* indexed and responses are fairly quick.
|
||||||
|
*
|
||||||
|
* @return a sequence of transactions that correspond to the given address for the given range.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException when empty argument provided
|
||||||
|
*/
|
||||||
|
fun getTAddressTransactions(
|
||||||
|
tAddress: String,
|
||||||
|
blockHeightRange: ClosedRange<BlockHeightUnsafe>
|
||||||
|
): Sequence<Service.RawTransaction>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 reconnect()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup any connections when the service is shutting down and not going to be used again.
|
||||||
|
*/
|
||||||
|
fun shutdown()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a raw transaction.
|
||||||
|
*
|
||||||
|
* @return the response from the server.
|
||||||
|
*/
|
||||||
|
fun submitTransaction(spendTransaction: ByteArray): Response<SendResponseUnsafe>
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A new client specifically for Android devices.
|
||||||
|
*/
|
||||||
|
fun BlockingLightWalletClient.Companion.new(
|
||||||
|
context: Context,
|
||||||
|
lightWalletEndpoint: LightWalletEndpoint
|
||||||
|
): BlockingLightWalletClient =
|
||||||
|
BlockingLightWalletClientImpl.new(AndroidChannelFactory(context), lightWalletEndpoint)
|
|
@ -0,0 +1,104 @@
|
||||||
|
package co.electriccoin.lightwallet.client
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.CompactFormats
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.Service
|
||||||
|
import co.electriccoin.lightwallet.client.internal.AndroidChannelFactory
|
||||||
|
import co.electriccoin.lightwallet.client.internal.CoroutineLightWalletClientImpl
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpointInfoUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.RawTransactionUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
|
import co.electriccoin.lightwallet.client.model.SendResponseUnsafe
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
// TODO [895]: Let remaining server calls adopt new response result
|
||||||
|
// TODO [895]: https://github.com/zcash/zcash-android-wallet-sdk/issues/895
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client for interacting with lightwalletd.
|
||||||
|
*/
|
||||||
|
interface CoroutineLightWalletClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the full transaction info.
|
||||||
|
*/
|
||||||
|
suspend fun fetchTransaction(txId: ByteArray): Response<RawTransactionUnsafe>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tAddress the transparent address to use.
|
||||||
|
* @param startHeight the starting height to use.
|
||||||
|
*
|
||||||
|
* @return a flow of UTXOs for the given address from the [startHeight].
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException when empty argument provided
|
||||||
|
*/
|
||||||
|
suspend fun fetchUtxos(
|
||||||
|
tAddress: String,
|
||||||
|
startHeight: BlockHeightUnsafe
|
||||||
|
): Flow<Service.GetAddressUtxosReply>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param heightRange the inclusive range to fetch. For instance if 1..5 is given, then every
|
||||||
|
* block in that range will be fetched, including 1 and 5.
|
||||||
|
*
|
||||||
|
* @return a flow of compact blocks for the given range
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException when empty argument provided
|
||||||
|
*/
|
||||||
|
fun getBlockRange(heightRange: ClosedRange<BlockHeightUnsafe>): Flow<CompactFormats.CompactBlock>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the latest block height known to the service.
|
||||||
|
*/
|
||||||
|
suspend fun getLatestBlockHeight(): Response<BlockHeightUnsafe>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return useful server details.
|
||||||
|
*/
|
||||||
|
suspend fun getServerInfo(): Response<LightWalletEndpointInfoUnsafe>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* indexed and responses are fairly quick.
|
||||||
|
*
|
||||||
|
* @return a flow of transactions that correspond to the given address for the given range.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException when empty argument provided
|
||||||
|
*/
|
||||||
|
fun getTAddressTransactions(
|
||||||
|
tAddress: String,
|
||||||
|
blockHeightRange: ClosedRange<BlockHeightUnsafe>
|
||||||
|
): Flow<Service.RawTransaction>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 reconnect()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup any connections when the service is shutting down and not going to be used again.
|
||||||
|
*/
|
||||||
|
fun shutdown()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a raw transaction.
|
||||||
|
*
|
||||||
|
* @return the response from the server.
|
||||||
|
*/
|
||||||
|
suspend fun submitTransaction(spendTransaction: ByteArray): Response<SendResponseUnsafe>
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A new client specifically for Android devices.
|
||||||
|
*/
|
||||||
|
fun CoroutineLightWalletClient.Companion.new(
|
||||||
|
context: Context,
|
||||||
|
lightWalletEndpoint: LightWalletEndpoint
|
||||||
|
): CoroutineLightWalletClient =
|
||||||
|
CoroutineLightWalletClientImpl.new(AndroidChannelFactory(context), lightWalletEndpoint)
|
|
@ -1,6 +1,6 @@
|
||||||
package cash.z.ecc.android.sdk.ext
|
package co.electriccoin.lightwallet.client.ext
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.BuildConfig
|
import co.electriccoin.lightwallet.client.BuildConfig
|
||||||
|
|
||||||
object BenchmarkingExt {
|
object BenchmarkingExt {
|
||||||
private const val TARGET_BUILD_TYPE = "benchmark" // NON-NLS
|
private const val TARGET_BUILD_TYPE = "benchmark" // NON-NLS
|
|
@ -0,0 +1,25 @@
|
||||||
|
package co.electriccoin.lightwallet.client.fixture
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
|
||||||
|
object BlockRangeFixture {
|
||||||
|
|
||||||
|
// Be aware that changing these bounds values in a broader range may result in a timeout reached in
|
||||||
|
// SyncBlockchainBenchmark. So if changing these, don't forget to align also the test timeout in
|
||||||
|
// waitForBalanceScreen() appropriately.
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
internal val BLOCK_HEIGHT_LOWER_BOUND = 1730001L
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
internal val BLOCK_HEIGHT_UPPER_BOUND = 1730100L
|
||||||
|
|
||||||
|
fun new(
|
||||||
|
lowerBound: Long = BLOCK_HEIGHT_LOWER_BOUND,
|
||||||
|
upperBound: Long = BLOCK_HEIGHT_UPPER_BOUND
|
||||||
|
): ClosedRange<Long> {
|
||||||
|
return lowerBound..upperBound
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package co.electriccoin.lightwallet.client.internal
|
||||||
|
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides conversion from API statuses to our predefined Server or Client error classes.
|
||||||
|
*/
|
||||||
|
interface ApiStatusResolver {
|
||||||
|
fun <T> resolveFailureFromStatus(throwable: Throwable): Response.Failure<T>
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
package co.electriccoin.lightwallet.client.internal
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.CompactFormats
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.CompactTxStreamerGrpc
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.Service
|
||||||
|
import co.electriccoin.lightwallet.client.BlockingLightWalletClient
|
||||||
|
import co.electriccoin.lightwallet.client.ext.BenchmarkingExt
|
||||||
|
import co.electriccoin.lightwallet.client.fixture.BlockRangeFixture
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpointInfoUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.RawTransactionUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
|
import co.electriccoin.lightwallet.client.model.SendResponseUnsafe
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import io.grpc.Channel
|
||||||
|
import io.grpc.ConnectivityState
|
||||||
|
import io.grpc.ManagedChannel
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of BlockingLightWalletClient using gRPC for requests to lightwalletd.
|
||||||
|
*
|
||||||
|
* @property singleRequestTimeout 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 streamingRequestTimeout 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.
|
||||||
|
*/
|
||||||
|
internal class BlockingLightWalletClientImpl private constructor(
|
||||||
|
private val channelFactory: ChannelFactory,
|
||||||
|
private val lightWalletEndpoint: LightWalletEndpoint,
|
||||||
|
private val singleRequestTimeout: Duration = 10.seconds,
|
||||||
|
private val streamingRequestTimeout: Duration = 90.seconds
|
||||||
|
) : BlockingLightWalletClient {
|
||||||
|
|
||||||
|
private var channel = channelFactory.newChannel(lightWalletEndpoint)
|
||||||
|
|
||||||
|
override fun getBlockRange(heightRange: ClosedRange<BlockHeightUnsafe>): Sequence<CompactFormats.CompactBlock> {
|
||||||
|
require(!heightRange.isEmpty()) {
|
||||||
|
"${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} range: $heightRange." // NON-NLS
|
||||||
|
}
|
||||||
|
|
||||||
|
return requireChannel().createStub(streamingRequestTimeout)
|
||||||
|
.getBlockRange(heightRange.toBlockRange()).iterator().asSequence()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLatestBlockHeight(): Response<BlockHeightUnsafe> {
|
||||||
|
return try {
|
||||||
|
if (BenchmarkingExt.isBenchmarking()) {
|
||||||
|
// We inject a benchmark test blocks range at this point to process only a restricted range of blocks
|
||||||
|
// for a more reliable benchmark results.
|
||||||
|
Response.Success(BlockHeightUnsafe(BlockRangeFixture.new().endInclusive))
|
||||||
|
} else {
|
||||||
|
val response = requireChannel().createStub(singleRequestTimeout)
|
||||||
|
.getLatestBlock(Service.ChainSpec.newBuilder().build())
|
||||||
|
|
||||||
|
val blockHeight = BlockHeightUnsafe(response.height)
|
||||||
|
|
||||||
|
Response.Success(blockHeight)
|
||||||
|
}
|
||||||
|
} catch (e: StatusRuntimeException) {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("SwallowedException")
|
||||||
|
override fun getServerInfo(): Response<LightWalletEndpointInfoUnsafe> {
|
||||||
|
return try {
|
||||||
|
val lightdInfo = requireChannel().createStub(singleRequestTimeout)
|
||||||
|
.getLightdInfo(Service.Empty.newBuilder().build())
|
||||||
|
|
||||||
|
val lightwalletEndpointInfo = LightWalletEndpointInfoUnsafe.new(lightdInfo)
|
||||||
|
|
||||||
|
Response.Success(lightwalletEndpointInfo)
|
||||||
|
} catch (e: StatusRuntimeException) {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun submitTransaction(spendTransaction: ByteArray): Response<SendResponseUnsafe> {
|
||||||
|
require(spendTransaction.isNotEmpty()) {
|
||||||
|
"${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} Failed to submit transaction because it was empty, so " +
|
||||||
|
"this request was ignored on the client-side." // NON-NLS
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val request =
|
||||||
|
Service.RawTransaction.newBuilder().setData(ByteString.copyFrom(spendTransaction))
|
||||||
|
.build()
|
||||||
|
val response = requireChannel().createStub().sendTransaction(request)
|
||||||
|
|
||||||
|
val sendResponse = SendResponseUnsafe.new(response)
|
||||||
|
|
||||||
|
Response.Success(sendResponse)
|
||||||
|
} catch (e: StatusRuntimeException) {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchTransaction(txId: ByteArray): Response<RawTransactionUnsafe> {
|
||||||
|
require(txId.isNotEmpty()) {
|
||||||
|
"${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} Failed to start fetching the transaction with null " +
|
||||||
|
"transaction ID, so this request was ignored on the client-side." // NON-NLS
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val request = Service.TxFilter.newBuilder().setHash(ByteString.copyFrom(txId)).build()
|
||||||
|
|
||||||
|
val response = requireChannel().createStub().getTransaction(request)
|
||||||
|
|
||||||
|
val transactionResponse = RawTransactionUnsafe.new(response)
|
||||||
|
|
||||||
|
Response.Success(transactionResponse)
|
||||||
|
} catch (e: StatusRuntimeException) {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchUtxos(
|
||||||
|
tAddress: String,
|
||||||
|
startHeight: BlockHeightUnsafe
|
||||||
|
): Sequence<Service.GetAddressUtxosReply> {
|
||||||
|
require(tAddress.isNotBlank()) {
|
||||||
|
"${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} address: $tAddress." // NON-NLS
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = requireChannel().createStub().getAddressUtxos(
|
||||||
|
Service.GetAddressUtxosArg.newBuilder().setAddresses(0, tAddress)
|
||||||
|
.setStartHeight(startHeight.value).build()
|
||||||
|
)
|
||||||
|
return result.addressUtxosList.asSequence()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTAddressTransactions(
|
||||||
|
tAddress: String,
|
||||||
|
blockHeightRange: ClosedRange<BlockHeightUnsafe>
|
||||||
|
): Sequence<Service.RawTransaction> {
|
||||||
|
require(!blockHeightRange.isEmpty() && tAddress.isNotBlank()) {
|
||||||
|
"${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} range: $blockHeightRange, address: $tAddress." // NON-NLS
|
||||||
|
}
|
||||||
|
|
||||||
|
return requireChannel().createStub().getTaddressTxids(
|
||||||
|
Service.TransparentAddressBlockFilter.newBuilder().setAddress(tAddress)
|
||||||
|
.setRange(blockHeightRange.toBlockRange()).build()
|
||||||
|
).iterator().asSequence()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shutdown() {
|
||||||
|
channel.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reconnect() {
|
||||||
|
channel.shutdown()
|
||||||
|
channel = channelFactory.newChannel(lightWalletEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// These make the implementation of BlockingLightWalletClientImpl not thread-safe.
|
||||||
|
private var stateCount = 0
|
||||||
|
private var state: ConnectivityState? = null
|
||||||
|
private fun requireChannel(): ManagedChannel {
|
||||||
|
state = channel.getState(false).let { new ->
|
||||||
|
if (state == new) stateCount++ else stateCount = 0
|
||||||
|
new
|
||||||
|
}
|
||||||
|
channel.resetConnectBackoff()
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun new(
|
||||||
|
channelFactory: ChannelFactory,
|
||||||
|
lightWalletEndpoint: LightWalletEndpoint
|
||||||
|
): BlockingLightWalletClient {
|
||||||
|
return BlockingLightWalletClientImpl(channelFactory, lightWalletEndpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Channel.createStub(timeoutSec: Duration = 60.seconds) =
|
||||||
|
CompactTxStreamerGrpc.newBlockingStub(this)
|
||||||
|
.withDeadlineAfter(timeoutSec.inWholeSeconds, TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
private fun BlockHeightUnsafe.toBlockHeight(): Service.BlockID =
|
||||||
|
Service.BlockID.newBuilder().setHeight(value).build()
|
||||||
|
|
||||||
|
private fun ClosedRange<BlockHeightUnsafe>.toBlockRange(): Service.BlockRange =
|
||||||
|
Service.BlockRange.newBuilder()
|
||||||
|
.setStart(start.toBlockHeight())
|
||||||
|
.setEnd(endInclusive.toBlockHeight())
|
||||||
|
.build()
|
|
@ -0,0 +1,30 @@
|
||||||
|
package co.electriccoin.lightwallet.client.internal
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import io.grpc.ManagedChannel
|
||||||
|
import io.grpc.android.AndroidChannelBuilder
|
||||||
|
|
||||||
|
internal interface ChannelFactory {
|
||||||
|
fun newChannel(endpoint: LightWalletEndpoint): ManagedChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AndroidChannelFactory(context: android.content.Context) : ChannelFactory {
|
||||||
|
private val context = context.applicationContext
|
||||||
|
|
||||||
|
override fun newChannel(endpoint: LightWalletEndpoint): ManagedChannel {
|
||||||
|
return AndroidChannelBuilder
|
||||||
|
.forAddress(endpoint.host, endpoint.port)
|
||||||
|
.context(context)
|
||||||
|
.enableFullStreamDecompression()
|
||||||
|
.apply {
|
||||||
|
if (endpoint.isSecure) {
|
||||||
|
useTransportSecurity()
|
||||||
|
} else {
|
||||||
|
Log.w(Constants.LOG_TAG, "WARNING Using plaintext connection")
|
||||||
|
usePlaintext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package co.electriccoin.lightwallet.client.internal
|
||||||
|
|
||||||
|
internal object Constants {
|
||||||
|
const val LOG_TAG = "LightWalletClient" // NON-NLS
|
||||||
|
const val ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE = "Illegal argument provided - can't be empty:" // NON-NLS
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
package co.electriccoin.lightwallet.client.internal
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.CompactFormats
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.CompactTxStreamerGrpcKt
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.Service
|
||||||
|
import co.electriccoin.lightwallet.client.CoroutineLightWalletClient
|
||||||
|
import co.electriccoin.lightwallet.client.ext.BenchmarkingExt
|
||||||
|
import co.electriccoin.lightwallet.client.fixture.BlockRangeFixture
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpointInfoUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.RawTransactionUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
|
import co.electriccoin.lightwallet.client.model.SendResponseUnsafe
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import io.grpc.CallOptions
|
||||||
|
import io.grpc.Channel
|
||||||
|
import io.grpc.ConnectivityState
|
||||||
|
import io.grpc.ManagedChannel
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of CoroutineLightWalletClient using gRPC for requests to lightwalletd.
|
||||||
|
*
|
||||||
|
* @property singleRequestTimeout 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 streamingRequestTimeout 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.
|
||||||
|
*/
|
||||||
|
internal class CoroutineLightWalletClientImpl private constructor(
|
||||||
|
private val channelFactory: ChannelFactory,
|
||||||
|
private val lightWalletEndpoint: LightWalletEndpoint,
|
||||||
|
private val singleRequestTimeout: Duration = 10.seconds,
|
||||||
|
private val streamingRequestTimeout: Duration = 90.seconds
|
||||||
|
) : CoroutineLightWalletClient {
|
||||||
|
|
||||||
|
private var channel = channelFactory.newChannel(lightWalletEndpoint)
|
||||||
|
|
||||||
|
override fun getBlockRange(heightRange: ClosedRange<BlockHeightUnsafe>): Flow<CompactFormats.CompactBlock> {
|
||||||
|
require(!heightRange.isEmpty()) {
|
||||||
|
"${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} range: $heightRange." // NON-NLS
|
||||||
|
}
|
||||||
|
|
||||||
|
return requireChannel().createStub(streamingRequestTimeout)
|
||||||
|
.getBlockRange(heightRange.toBlockRange())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getLatestBlockHeight(): Response<BlockHeightUnsafe> {
|
||||||
|
return try {
|
||||||
|
if (BenchmarkingExt.isBenchmarking()) {
|
||||||
|
// We inject a benchmark test blocks range at this point to process only a restricted range of blocks
|
||||||
|
// for a more reliable benchmark results.
|
||||||
|
Response.Success(BlockHeightUnsafe(BlockRangeFixture.new().endInclusive))
|
||||||
|
} else {
|
||||||
|
val response = requireChannel().createStub(singleRequestTimeout)
|
||||||
|
.getLatestBlock(Service.ChainSpec.newBuilder().build())
|
||||||
|
|
||||||
|
val blockHeight = BlockHeightUnsafe(response.height)
|
||||||
|
|
||||||
|
Response.Success(blockHeight)
|
||||||
|
}
|
||||||
|
} catch (e: StatusRuntimeException) {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("SwallowedException")
|
||||||
|
override suspend fun getServerInfo(): Response<LightWalletEndpointInfoUnsafe> {
|
||||||
|
return try {
|
||||||
|
val lightdInfo = requireChannel().createStub(singleRequestTimeout)
|
||||||
|
.getLightdInfo(Service.Empty.newBuilder().build())
|
||||||
|
|
||||||
|
val lightwalletEndpointInfo = LightWalletEndpointInfoUnsafe.new(lightdInfo)
|
||||||
|
|
||||||
|
Response.Success(lightwalletEndpointInfo)
|
||||||
|
} catch (e: StatusRuntimeException) {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun submitTransaction(spendTransaction: ByteArray): Response<SendResponseUnsafe> {
|
||||||
|
require(spendTransaction.isNotEmpty()) {
|
||||||
|
"${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} Failed to submit transaction because it was empty, so " +
|
||||||
|
"this request was ignored on the client-side." // NON-NLS
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val request =
|
||||||
|
Service.RawTransaction.newBuilder().setData(ByteString.copyFrom(spendTransaction))
|
||||||
|
.build()
|
||||||
|
val response = requireChannel().createStub().sendTransaction(request)
|
||||||
|
|
||||||
|
val sendResponse = SendResponseUnsafe.new(response)
|
||||||
|
|
||||||
|
Response.Success(sendResponse)
|
||||||
|
} catch (e: StatusRuntimeException) {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchTransaction(txId: ByteArray): Response<RawTransactionUnsafe> {
|
||||||
|
require(txId.isNotEmpty()) {
|
||||||
|
"${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} Failed to start fetching the transaction with null " +
|
||||||
|
"transaction ID, so this request was ignored on the client-side." // NON-NLS
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val request = Service.TxFilter.newBuilder().setHash(ByteString.copyFrom(txId)).build()
|
||||||
|
|
||||||
|
val response = requireChannel().createStub().getTransaction(request)
|
||||||
|
|
||||||
|
val transactionResponse = RawTransactionUnsafe.new(response)
|
||||||
|
|
||||||
|
Response.Success(transactionResponse)
|
||||||
|
} catch (e: StatusRuntimeException) {
|
||||||
|
GrpcStatusResolver.resolveFailureFromStatus(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchUtxos(
|
||||||
|
tAddress: String,
|
||||||
|
startHeight: BlockHeightUnsafe
|
||||||
|
): Flow<Service.GetAddressUtxosReply> {
|
||||||
|
require(tAddress.isNotBlank()) {
|
||||||
|
"${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} address: $tAddress." // NON-NLS
|
||||||
|
}
|
||||||
|
return requireChannel().createStub().getAddressUtxosStream(
|
||||||
|
Service.GetAddressUtxosArg.newBuilder().setAddresses(0, tAddress)
|
||||||
|
.setStartHeight(startHeight.value).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTAddressTransactions(
|
||||||
|
tAddress: String,
|
||||||
|
blockHeightRange: ClosedRange<BlockHeightUnsafe>
|
||||||
|
): Flow<Service.RawTransaction> {
|
||||||
|
require(!blockHeightRange.isEmpty() && tAddress.isNotBlank()) {
|
||||||
|
"${Constants.ILLEGAL_ARGUMENT_EXCEPTION_MESSAGE} range: $blockHeightRange, address: $tAddress." // NON-NLS
|
||||||
|
}
|
||||||
|
return requireChannel().createStub().getTaddressTxids(
|
||||||
|
Service.TransparentAddressBlockFilter.newBuilder().setAddress(tAddress)
|
||||||
|
.setRange(blockHeightRange.toBlockRange()).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shutdown() {
|
||||||
|
channel.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reconnect() {
|
||||||
|
channel.shutdown()
|
||||||
|
channel = channelFactory.newChannel(lightWalletEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// These make the CoroutineLightWalletClientImpl not thread safe. In the long-term, we should
|
||||||
|
// consider making it thread safe.
|
||||||
|
private var stateCount = 0
|
||||||
|
private var state: ConnectivityState? = null
|
||||||
|
private fun requireChannel(): ManagedChannel {
|
||||||
|
state = channel.getState(false).let { new ->
|
||||||
|
if (state == new) stateCount++ else stateCount = 0
|
||||||
|
new
|
||||||
|
}
|
||||||
|
channel.resetConnectBackoff()
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun new(
|
||||||
|
channelFactory: ChannelFactory,
|
||||||
|
lightWalletEndpoint: LightWalletEndpoint
|
||||||
|
): CoroutineLightWalletClientImpl {
|
||||||
|
return CoroutineLightWalletClientImpl(channelFactory, lightWalletEndpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Channel.createStub(timeoutSec: Duration = 60.seconds) =
|
||||||
|
CompactTxStreamerGrpcKt.CompactTxStreamerCoroutineStub(this, CallOptions.DEFAULT)
|
||||||
|
.withDeadlineAfter(timeoutSec.inWholeSeconds, TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
private fun BlockHeightUnsafe.toBlockHeight(): Service.BlockID =
|
||||||
|
Service.BlockID.newBuilder().setHeight(value).build()
|
||||||
|
|
||||||
|
private fun ClosedRange<BlockHeightUnsafe>.toBlockRange(): Service.BlockRange =
|
||||||
|
Service.BlockRange.newBuilder()
|
||||||
|
.setStart(start.toBlockHeight())
|
||||||
|
.setEnd(endInclusive.toBlockHeight())
|
||||||
|
.build()
|
|
@ -1,47 +1,46 @@
|
||||||
package cash.z.ecc.android.sdk.darkside.test
|
package co.electriccoin.lightwallet.client.internal
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
import cash.z.wallet.sdk.internal.rpc.Darkside
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.wallet.sdk.internal.rpc.Darkside.DarksideTransactionsURL
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.wallet.sdk.internal.rpc.DarksideStreamerGrpc
|
||||||
import cash.z.ecc.android.sdk.model.Darkside
|
import cash.z.wallet.sdk.internal.rpc.Service
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import cash.z.wallet.sdk.rpc.Darkside
|
|
||||||
import cash.z.wallet.sdk.rpc.Darkside.DarksideTransactionsURL
|
|
||||||
import cash.z.wallet.sdk.rpc.DarksideStreamerGrpc
|
|
||||||
import cash.z.wallet.sdk.rpc.Service
|
|
||||||
import io.grpc.ManagedChannel
|
import io.grpc.ManagedChannel
|
||||||
import io.grpc.stub.StreamObserver
|
import io.grpc.stub.StreamObserver
|
||||||
import java.lang.RuntimeException
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class DarksideApi(
|
/*
|
||||||
|
* 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 channel: ManagedChannel,
|
||||||
private val singleRequestTimeoutSec: Long = 10L
|
private val singleRequestTimeout: Duration = 10.seconds
|
||||||
) {
|
) {
|
||||||
|
|
||||||
constructor(
|
companion object {
|
||||||
appContext: Context,
|
internal fun new(
|
||||||
lightWalletEndpoint: LightWalletEndpoint
|
channelFactory: ChannelFactory,
|
||||||
) : this(
|
lightWalletEndpoint: LightWalletEndpoint
|
||||||
LightWalletGrpcService.createDefaultChannel(
|
) = DarksideApi(channelFactory.newChannel(lightWalletEndpoint))
|
||||||
appContext,
|
}
|
||||||
lightWalletEndpoint
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Service APIs
|
// Service APIs
|
||||||
//
|
//
|
||||||
|
|
||||||
fun reset(
|
fun reset(
|
||||||
saplingActivationHeight: BlockHeight = ZcashNetwork.Mainnet.saplingActivationHeight,
|
saplingActivationHeight: BlockHeightUnsafe,
|
||||||
branchId: String = "e9ff75a6", // Canopy,
|
branchId: String = "e9ff75a6", // Canopy,
|
||||||
chainName: String = "darkside${ZcashNetwork.Mainnet.networkName}"
|
chainName: String = "darksidemainnet"
|
||||||
) = apply {
|
) = apply {
|
||||||
twig("resetting darksidewalletd with saplingActivation=$saplingActivationHeight branchId=$branchId chainName=$chainName")
|
|
||||||
Darkside.DarksideMetaState.newBuilder()
|
Darkside.DarksideMetaState.newBuilder()
|
||||||
.setBranchID(branchId)
|
.setBranchID(branchId)
|
||||||
.setChainName(chainName)
|
.setChainName(chainName)
|
||||||
|
@ -52,51 +51,49 @@ class DarksideApi(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stageBlocks(url: String) = apply {
|
fun stageBlocks(url: String) = apply {
|
||||||
twig("staging blocks url=$url")
|
|
||||||
createStub().stageBlocks(url.toUrl())
|
createStub().stageBlocks(url.toUrl())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stageTransactions(url: String, targetHeight: BlockHeight) = apply {
|
fun stageTransactions(url: String, targetHeight: BlockHeightUnsafe) = apply {
|
||||||
twig("staging transaction at height=$targetHeight from url=$url")
|
|
||||||
createStub().stageTransactions(
|
createStub().stageTransactions(
|
||||||
DarksideTransactionsURL.newBuilder().setHeight(targetHeight.value).setUrl(url).build()
|
DarksideTransactionsURL.newBuilder().setHeight(targetHeight.value.toInt()).setUrl(url).build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stageEmptyBlocks(startHeight: BlockHeight, count: Int = 10, nonce: Int = Random.nextInt()) = apply {
|
fun stageEmptyBlocks(
|
||||||
twig("staging $count empty blocks starting at $startHeight with nonce $nonce")
|
startHeight: BlockHeightUnsafe,
|
||||||
|
count: Int = 10,
|
||||||
|
nonce: Int = Random.nextInt()
|
||||||
|
) = apply {
|
||||||
createStub().stageBlocksCreate(
|
createStub().stageBlocksCreate(
|
||||||
Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight.value).setCount(count).setNonce(nonce).build()
|
Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight.value.toInt()).setCount(count)
|
||||||
|
.setNonce(nonce).build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stageTransactions(txs: Iterator<Service.RawTransaction>?, tipHeight: BlockHeight) {
|
fun stageTransactions(txs: Iterator<Service.RawTransaction>?, tipHeight: BlockHeightUnsafe) {
|
||||||
if (txs == null) {
|
if (txs == null) {
|
||||||
twig("no transactions to stage")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
twig("staging transaction at height=$tipHeight")
|
|
||||||
val response = EmptyResponse()
|
val response = EmptyResponse()
|
||||||
createStreamingStub().stageTransactionsStream(response).apply {
|
createStreamingStub().stageTransactionsStream(response).apply {
|
||||||
txs.forEach {
|
txs.forEach {
|
||||||
twig("stageTransactions: onNext calling!!!")
|
// apply the tipHeight because the passed in txs might not know their destination
|
||||||
onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.value).build()) // apply the tipHeight because the passed in txs might not know their destination height (if they were created via SendTransaction)
|
// height (if they were created via SendTransaction)
|
||||||
twig("stageTransactions: onNext called")
|
onNext(
|
||||||
|
it.newBuilderForType().setData(it.data).setHeight(tipHeight.value).build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
twig("stageTransactions: onCompleted calling!!!")
|
|
||||||
onCompleted()
|
onCompleted()
|
||||||
twig("stageTransactions: onCompleted called")
|
|
||||||
}
|
}
|
||||||
response.await()
|
response.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyBlocks(tipHeight: BlockHeight) {
|
fun applyBlocks(tipHeight: BlockHeightUnsafe) {
|
||||||
twig("applying blocks up to tipHeight=$tipHeight")
|
|
||||||
createStub().applyStaged(tipHeight.toHeight())
|
createStub().applyStaged(tipHeight.toHeight())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSentTransactions(): MutableIterator<Service.RawTransaction>? {
|
fun getSentTransactions(): MutableIterator<Service.RawTransaction>? {
|
||||||
twig("grabbing sent transactions...")
|
|
||||||
return createStub().getIncomingTransactions(Service.Empty.newBuilder().build())
|
return createStub().getIncomingTransactions(Service.Empty.newBuilder().build())
|
||||||
}
|
}
|
||||||
// fun setMetaState(
|
// fun setMetaState(
|
||||||
|
@ -121,7 +118,7 @@ class DarksideApi(
|
||||||
// fun setState(latestHeight: Int = -1, reorgHeight: Int = latestHeight): DarksideApi {
|
// fun setState(latestHeight: Int = -1, reorgHeight: Int = latestHeight): DarksideApi {
|
||||||
// this.latestHeight = latestHeight
|
// this.latestHeight = latestHeight
|
||||||
// this.reorgHeight = reorgHeight
|
// this.reorgHeight = reorgHeight
|
||||||
// // TODO: change this service to accept ints as heights, like everywhere else
|
// // change this service to accept ints as heights, like everywhere else
|
||||||
// createStub().darksideSetState(
|
// createStub().darksideSetState(
|
||||||
// Darkside.DarksideState.newBuilder()
|
// Darkside.DarksideState.newBuilder()
|
||||||
// .setLatestHeight(latestHeight.toLong())
|
// .setLatestHeight(latestHeight.toLong())
|
||||||
|
@ -134,40 +131,50 @@ class DarksideApi(
|
||||||
private fun createStub(): DarksideStreamerGrpc.DarksideStreamerBlockingStub =
|
private fun createStub(): DarksideStreamerGrpc.DarksideStreamerBlockingStub =
|
||||||
DarksideStreamerGrpc
|
DarksideStreamerGrpc
|
||||||
.newBlockingStub(channel)
|
.newBlockingStub(channel)
|
||||||
.withDeadlineAfter(singleRequestTimeoutSec, TimeUnit.SECONDS)
|
.withDeadlineAfter(singleRequestTimeout.inWholeSeconds, TimeUnit.SECONDS)
|
||||||
|
|
||||||
private fun createStreamingStub(): DarksideStreamerGrpc.DarksideStreamerStub =
|
private fun createStreamingStub(): DarksideStreamerGrpc.DarksideStreamerStub =
|
||||||
DarksideStreamerGrpc
|
DarksideStreamerGrpc
|
||||||
.newStub(channel)
|
.newStub(channel)
|
||||||
.withDeadlineAfter(singleRequestTimeoutSec, TimeUnit.SECONDS)
|
.withDeadlineAfter(singleRequestTimeout.inWholeSeconds, TimeUnit.SECONDS)
|
||||||
|
|
||||||
private fun String.toUrl() = Darkside.DarksideBlocksURL.newBuilder().setUrl(this).build()
|
private fun String.toUrl() = Darkside.DarksideBlocksURL.newBuilder().setUrl(this).build()
|
||||||
private fun BlockHeight.toHeight() = Darkside.DarksideHeight.newBuilder().setHeight(this.value).build()
|
|
||||||
|
|
||||||
class EmptyResponse : StreamObserver<Service.Empty> {
|
class EmptyResponse : StreamObserver<Service.Empty> {
|
||||||
|
companion object {
|
||||||
|
private val DEFAULT_DELAY = 20.milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
var completed = false
|
var completed = false
|
||||||
var error: Throwable? = null
|
var error: Throwable? = null
|
||||||
override fun onNext(value: Service.Empty?) {
|
override fun onNext(value: Service.Empty?) {
|
||||||
twig("<><><><><><><><> EMPTY RESPONSE: ONNEXT CALLED!!!!")
|
// No implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(t: Throwable?) {
|
override fun onError(t: Throwable?) {
|
||||||
twig("<><><><><><><><> EMPTY RESPONSE: ONERROR CALLED!!!!")
|
|
||||||
error = t
|
error = t
|
||||||
completed = true
|
completed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCompleted() {
|
override fun onCompleted() {
|
||||||
twig("<><><><><><><><> EMPTY RESPONSE: ONCOMPLETED CALLED!!!")
|
|
||||||
completed = true
|
completed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun await() {
|
fun await() {
|
||||||
while (!completed) {
|
while (!completed) {
|
||||||
twig("awaiting server response...")
|
Thread.sleep(DEFAULT_DELAY.inWholeSeconds)
|
||||||
Thread.sleep(20L)
|
}
|
||||||
|
if (error != null) {
|
||||||
|
error("Server responded with an error: $error caused by ${error?.cause}")
|
||||||
}
|
}
|
||||||
if (error != null) throw RuntimeException("Server responded with an error: $error caused by ${error?.cause}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun BlockHeightUnsafe.toHeight() =
|
||||||
|
Darkside.DarksideHeight.newBuilder().setHeight(this.value.toInt()).build()
|
||||||
|
|
||||||
|
fun DarksideApi.Companion.new(
|
||||||
|
context: Context,
|
||||||
|
lightWalletEndpoint: LightWalletEndpoint
|
||||||
|
) = DarksideApi.new(AndroidChannelFactory(context), lightWalletEndpoint)
|
|
@ -0,0 +1,67 @@
|
||||||
|
package co.electriccoin.lightwallet.client.internal
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
|
import io.grpc.Status
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides conversion from GRPC Status to our predefined Server or Client error classes.
|
||||||
|
*/
|
||||||
|
object GrpcStatusResolver : ApiStatusResolver {
|
||||||
|
|
||||||
|
override fun <T> resolveFailureFromStatus(throwable: Throwable): Response.Failure<T> {
|
||||||
|
val status = Status.fromThrowable(throwable)
|
||||||
|
Log.w(Constants.LOG_TAG, "Networking error: ${status.code}: ${status.description}")
|
||||||
|
|
||||||
|
return when (status.code) {
|
||||||
|
Status.Code.ABORTED -> {
|
||||||
|
Response.Failure.Server.Aborted(
|
||||||
|
code = status.code.value(),
|
||||||
|
description = status.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Status.Code.CANCELLED -> {
|
||||||
|
Response.Failure.Client.Canceled(
|
||||||
|
code = status.code.value(),
|
||||||
|
description = status.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Status.Code.DEADLINE_EXCEEDED -> {
|
||||||
|
Response.Failure.Server.DeadlineExceeded(
|
||||||
|
code = status.code.value(),
|
||||||
|
description = status.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Status.Code.NOT_FOUND -> {
|
||||||
|
Response.Failure.Server.NotFound(
|
||||||
|
code = status.code.value(),
|
||||||
|
description = status.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Status.Code.PERMISSION_DENIED -> {
|
||||||
|
Response.Failure.Server.PermissionDenied(
|
||||||
|
code = status.code.value(),
|
||||||
|
description = status.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Status.Code.UNAVAILABLE -> {
|
||||||
|
Response.Failure.Server.Unavailable(
|
||||||
|
code = status.code.value(),
|
||||||
|
description = status.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Status.Code.UNKNOWN -> {
|
||||||
|
Response.Failure.Server.Unknown(
|
||||||
|
code = status.code.value(),
|
||||||
|
description = status.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Response.Failure.Server.Other(
|
||||||
|
code = status.code.value(),
|
||||||
|
description = status.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package co.electriccoin.lightwallet.client.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Block Height that has come from the Light Wallet server.
|
||||||
|
*
|
||||||
|
* It is marked as "unsafe" because it is not guaranteed to be valid.
|
||||||
|
*/
|
||||||
|
data class BlockHeightUnsafe(val value: Long) : Comparable<BlockHeightUnsafe> {
|
||||||
|
init {
|
||||||
|
require(UINT_RANGE.contains(value)) { "Height $value is outside of allowed range $UINT_RANGE" }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: BlockHeightUnsafe): Int = value.compareTo(other.value)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val UINT_RANGE = 0.toLong()..UInt.MAX_VALUE.toLong()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package cash.z.ecc.android.sdk.model
|
package co.electriccoin.lightwallet.client.model
|
||||||
|
|
||||||
data class LightWalletEndpoint(val host: String, val port: Int, val isSecure: Boolean) {
|
data class LightWalletEndpoint(val host: String, val port: Int, val isSecure: Boolean) {
|
||||||
companion object
|
companion object
|
|
@ -0,0 +1,23 @@
|
||||||
|
package co.electriccoin.lightwallet.client.model
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.Service
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lightwalletd endpoint information, which has come from the Light Wallet server.
|
||||||
|
*
|
||||||
|
* It is marked as "unsafe" because it is not guaranteed to be valid.
|
||||||
|
*/
|
||||||
|
data class LightWalletEndpointInfoUnsafe(
|
||||||
|
val chainName: String,
|
||||||
|
val consensusBranchId: String,
|
||||||
|
val blockHeightUnsafe: BlockHeightUnsafe
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
internal fun new(lightdInfo: Service.LightdInfo) =
|
||||||
|
LightWalletEndpointInfoUnsafe(
|
||||||
|
lightdInfo.chainName,
|
||||||
|
lightdInfo.consensusBranchId,
|
||||||
|
BlockHeightUnsafe(lightdInfo.blockHeight)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package co.electriccoin.lightwallet.client.model
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.Service.RawTransaction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RawTransaction contains the complete transaction data, which has come from the Light Wallet server.
|
||||||
|
*
|
||||||
|
* It is marked as "unsafe" because it is not guaranteed to be valid.
|
||||||
|
*/
|
||||||
|
class RawTransactionUnsafe(val height: BlockHeightUnsafe, val data: ByteArray) {
|
||||||
|
companion object {
|
||||||
|
fun new(rawTransaction: RawTransaction) = RawTransactionUnsafe(
|
||||||
|
BlockHeightUnsafe(rawTransaction.height),
|
||||||
|
rawTransaction.data.toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package co.electriccoin.lightwallet.client.model
|
||||||
|
|
||||||
|
internal const val CONNECTION_ERROR_CODE = 3100
|
||||||
|
internal const val CONNECTION_ERROR_DESCRIPTION = "Missing internet connection." // NON-NLS
|
||||||
|
|
||||||
|
sealed class Response<T> {
|
||||||
|
data class Success<T>(val result: T) : Response<T>()
|
||||||
|
|
||||||
|
sealed class Failure<T>(
|
||||||
|
open val code: Int,
|
||||||
|
open val description: String?
|
||||||
|
) : Response<T>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this function to convert Failure into Throwable object.
|
||||||
|
*/
|
||||||
|
fun toThrowable() =
|
||||||
|
Throwable("Communication failure with details: $code${description?.let{": $it"} ?: "."}") // NON-NLS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client was not able to communicate with the server.
|
||||||
|
*/
|
||||||
|
class Connection<T>(
|
||||||
|
override val description: String? = CONNECTION_ERROR_DESCRIPTION
|
||||||
|
) : Failure<T>(CONNECTION_ERROR_CODE, description) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Connection Error(code='$code', description='$description')" // NON-NLS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server did respond and returned an error.
|
||||||
|
*/
|
||||||
|
sealed class Server<T>(
|
||||||
|
override val code: Int,
|
||||||
|
override val description: String?
|
||||||
|
) : Failure<T>(code, description) {
|
||||||
|
/**
|
||||||
|
* The operation was aborted, typically due to a concurrency issue like sequencer check failures,
|
||||||
|
* transaction aborts, etc.
|
||||||
|
*/
|
||||||
|
class Aborted<T>(code: Int, description: String?) : Server<T>(code, description)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deadline expired before operation could complete. For operations that change the state of the system,
|
||||||
|
* this error may be returned even if the operation has completed successfully. For example, a
|
||||||
|
* successful response from a server could have been delayed long enough for the deadline to expire.
|
||||||
|
*/
|
||||||
|
class DeadlineExceeded<T>(code: Int, description: String?) : Server<T>(code, description)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some requested entity (e.g., file or directory) was not found.
|
||||||
|
*/
|
||||||
|
class NotFound<T>(code: Int, description: String?) : Server<T>(code, description)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This state covers errors like ALREADY_EXISTS, FAILED_PRECONDITION, DATA_LOSS, INTERNAL, INVALID_ARGUMENT,
|
||||||
|
* OUT_OF_RANGE, RESOURCE_EXHAUSTED, UNAUTHENTICATED or UNIMPLEMENTED. You find about these errors in
|
||||||
|
* {@link io.grpc.Status.Code}.
|
||||||
|
*/
|
||||||
|
class Other<T>(code: Int, description: String?) : Server<T>(code, description)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The caller does not have permission to execute the specified operation.
|
||||||
|
*/
|
||||||
|
class PermissionDenied<T>(code: Int, description: String?) : Server<T>(code, description)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service is currently unavailable. This is a most likely a transient condition and may be
|
||||||
|
* corrected by retrying with a backoff. Note that it is not always safe to retry non-idempotent operations.
|
||||||
|
*/
|
||||||
|
class Unavailable<T>(code: Int, description: String?) : Server<T>(code, description)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unknown error. An example of where this error may be returned is if a Status value received from
|
||||||
|
* another address space belongs to an error-space that is not known in this address space. Also errors
|
||||||
|
* raised by APIs that do not return enough error information may be converted to this error.
|
||||||
|
*/
|
||||||
|
class Unknown<T>(code: Int, description: String?) : Server<T>(code, description)
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Server Error(code='$code', description='$description')" // NON-NLS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A failure occurred on the client, such as parsing a response failed.
|
||||||
|
*/
|
||||||
|
sealed class Client<T>(
|
||||||
|
override val code: Int,
|
||||||
|
override val description: String?
|
||||||
|
) : Failure<T>(code, description) {
|
||||||
|
/**
|
||||||
|
* The operation was cancelled (typically by the caller).
|
||||||
|
*/
|
||||||
|
class Canceled<T>(code: Int, description: String?) : Client<T>(code, description)
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Client Error(code='$code', description='$description')" // NON-NLS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package co.electriccoin.lightwallet.client.model
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.Service
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SendResponse encodes an error code and a string. It is currently used only by SendTransaction(). If error code
|
||||||
|
* is zero, the operation was successful; if non-zero, it and the message specify the failure. It has come from the
|
||||||
|
* Light Wallet server.
|
||||||
|
*
|
||||||
|
* It is marked as "unsafe" because it is not guaranteed to be valid.
|
||||||
|
*/
|
||||||
|
data class SendResponseUnsafe(
|
||||||
|
val code: Int,
|
||||||
|
val message: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
internal fun new(sendResponse: Service.SendResponse) =
|
||||||
|
SendResponseUnsafe(
|
||||||
|
code = sendResponse.errorCode,
|
||||||
|
message = sendResponse.errorMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright (c) 2019-2020 The Zcash developers
|
||||||
|
// Distributed under the MIT software license, see the accompanying
|
||||||
|
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
||||||
|
|
||||||
|
syntax = "proto3";
|
||||||
|
package cash.z.wallet.sdk.rpc;
|
||||||
|
option java_package = "cash.z.wallet.sdk.internal.rpc";
|
||||||
|
option go_package = "lightwalletd/walletrpc";
|
||||||
|
option swift_prefix = "";
|
||||||
|
// Remember that proto3 fields are all optional. A field that is not present will be set to its zero value.
|
||||||
|
// bytes fields of hashes are in canonical little-endian format.
|
||||||
|
|
||||||
|
// CompactBlock is a packaging of ONLY the data from a block that's needed to:
|
||||||
|
// 1. Detect a payment to your shielded Sapling address
|
||||||
|
// 2. Detect a spend of your shielded Sapling notes
|
||||||
|
// 3. Update your witnesses to generate new Sapling spend proofs.
|
||||||
|
message CompactBlock {
|
||||||
|
uint32 protoVersion = 1; // the version of this wire format, for storage
|
||||||
|
uint64 height = 2; // the height of this block
|
||||||
|
bytes hash = 3; // the ID (hash) of this block, same as in block explorers
|
||||||
|
bytes prevHash = 4; // the ID (hash) of this block's predecessor
|
||||||
|
uint32 time = 5; // Unix epoch time when the block was mined
|
||||||
|
bytes header = 6; // (hash, prevHash, and time) OR (full header)
|
||||||
|
repeated CompactTx vtx = 7; // zero or more compact transactions from this block
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompactTx contains the minimum information for a wallet to know if this transaction
|
||||||
|
// is relevant to it (either pays to it or spends from it) via shielded elements
|
||||||
|
// only. This message will not encode a transparent-to-transparent transaction.
|
||||||
|
message CompactTx {
|
||||||
|
uint64 index = 1; // the index within the full block
|
||||||
|
bytes hash = 2; // the ID (hash) of this transaction, same as in block explorers
|
||||||
|
|
||||||
|
// The transaction fee: present if server can provide. In the case of a
|
||||||
|
// stateless server and a transaction with transparent inputs, this will be
|
||||||
|
// unset because the calculation requires reference to prior transactions.
|
||||||
|
// in a pure-Sapling context, the fee will be calculable as:
|
||||||
|
// valueBalance + (sum(vPubNew) - sum(vPubOld) - sum(tOut))
|
||||||
|
uint32 fee = 3;
|
||||||
|
|
||||||
|
repeated CompactSaplingSpend spends = 4; // inputs
|
||||||
|
repeated CompactSaplingOutput outputs = 5; // outputs
|
||||||
|
repeated CompactOrchardAction actions = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompactSaplingSpend is a Sapling Spend Description as described in 7.3 of the Zcash
|
||||||
|
// protocol specification.
|
||||||
|
message CompactSaplingSpend {
|
||||||
|
bytes nf = 1; // nullifier (see the Zcash protocol specification)
|
||||||
|
}
|
||||||
|
|
||||||
|
// output is a Sapling Output Description as described in section 7.4 of the
|
||||||
|
// Zcash protocol spec. Total size is 948.
|
||||||
|
message CompactSaplingOutput {
|
||||||
|
bytes cmu = 1; // note commitment u-coordinate
|
||||||
|
bytes epk = 2; // ephemeral public key
|
||||||
|
bytes ciphertext = 3; // first 52 bytes of ciphertext
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/zcash/zips/blob/main/zip-0225.rst#orchard-action-description-orchardaction
|
||||||
|
// (but not all fields are needed)
|
||||||
|
message CompactOrchardAction {
|
||||||
|
bytes nullifier = 1; // [32] The nullifier of the input note
|
||||||
|
bytes cmx = 2; // [32] The x-coordinate of the note commitment for the output note
|
||||||
|
bytes ephemeralKey = 3; // [32] An encoding of an ephemeral Pallas public key
|
||||||
|
bytes ciphertext = 4; // [52] The note plaintext component of the encCiphertext field
|
||||||
|
}
|
|
@ -4,7 +4,8 @@
|
||||||
|
|
||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
package cash.z.wallet.sdk.rpc;
|
package cash.z.wallet.sdk.rpc;
|
||||||
option go_package = ".;walletrpc";
|
option java_package = "cash.z.wallet.sdk.internal.rpc";
|
||||||
|
option go_package = "lightwalletd/walletrpc";
|
||||||
option swift_prefix = "";
|
option swift_prefix = "";
|
||||||
import "service.proto";
|
import "service.proto";
|
||||||
|
|
||||||
|
@ -29,16 +30,16 @@ message DarksideBlocksURL {
|
||||||
// of hex-encoded transactions, one per line, that are to be associated
|
// of hex-encoded transactions, one per line, that are to be associated
|
||||||
// with the given height (fake-mined into the block at that height)
|
// with the given height (fake-mined into the block at that height)
|
||||||
message DarksideTransactionsURL {
|
message DarksideTransactionsURL {
|
||||||
int64 height = 1;
|
int32 height = 1;
|
||||||
string url = 2;
|
string url = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DarksideHeight {
|
message DarksideHeight {
|
||||||
int64 height = 1;
|
int32 height = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DarksideEmptyBlocks {
|
message DarksideEmptyBlocks {
|
||||||
int64 height = 1;
|
int32 height = 1;
|
||||||
int32 nonce = 2;
|
int32 nonce = 2;
|
||||||
int32 count = 3;
|
int32 count = 3;
|
||||||
}
|
}
|
||||||
|
@ -114,4 +115,20 @@ service DarksideStreamer {
|
||||||
|
|
||||||
// Clear the incoming transaction pool.
|
// Clear the incoming transaction pool.
|
||||||
rpc ClearIncomingTransactions(Empty) returns (Empty) {}
|
rpc ClearIncomingTransactions(Empty) returns (Empty) {}
|
||||||
|
|
||||||
|
// Add a GetAddressUtxosReply entry to be returned by GetAddressUtxos().
|
||||||
|
// There is no staging or applying for these, very simple.
|
||||||
|
rpc AddAddressUtxo(GetAddressUtxosReply) returns (Empty) {}
|
||||||
|
|
||||||
|
// Clear the list of GetAddressUtxos entries (can't fail)
|
||||||
|
rpc ClearAddressUtxo(Empty) returns (Empty) {}
|
||||||
|
|
||||||
|
// Adds a GetTreeState to the tree state cache
|
||||||
|
rpc AddTreeState(TreeState) returns (Empty) {}
|
||||||
|
|
||||||
|
// Removes a GetTreeState for the given height from cache if present (can't fail)
|
||||||
|
rpc RemoveTreeState(BlockID) returns (Empty) {}
|
||||||
|
|
||||||
|
// Clear the list of GetTreeStates entries (can't fail)
|
||||||
|
rpc ClearAllTreeStates(Empty) returns (Empty) {}
|
||||||
}
|
}
|
|
@ -4,7 +4,8 @@
|
||||||
|
|
||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
package cash.z.wallet.sdk.rpc;
|
package cash.z.wallet.sdk.rpc;
|
||||||
option go_package = ".;walletrpc";
|
option java_package = "cash.z.wallet.sdk.internal.rpc";
|
||||||
|
option go_package = "lightwalletd/walletrpc";
|
||||||
option swift_prefix = "";
|
option swift_prefix = "";
|
||||||
import "compact_formats.proto";
|
import "compact_formats.proto";
|
||||||
|
|
||||||
|
@ -32,7 +33,8 @@ message TxFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawTransaction contains the complete transaction data. It also optionally includes
|
// RawTransaction contains the complete transaction data. It also optionally includes
|
||||||
// the block height in which the transaction was included.
|
// the block height in which the transaction was included, or, when returned
|
||||||
|
// by GetMempoolStream(), the latest block height.
|
||||||
message RawTransaction {
|
message RawTransaction {
|
||||||
bytes data = 1; // exact data returned by Zcash 'getrawtransaction'
|
bytes data = 1; // exact data returned by Zcash 'getrawtransaction'
|
||||||
uint64 height = 2; // height that the transaction was mined (or -1)
|
uint64 height = 2; // height that the transaction was mined (or -1)
|
||||||
|
@ -109,19 +111,23 @@ message Exclude {
|
||||||
|
|
||||||
// The TreeState is derived from the Zcash z_gettreestate rpc.
|
// The TreeState is derived from the Zcash z_gettreestate rpc.
|
||||||
message TreeState {
|
message TreeState {
|
||||||
string network = 1; // "main" or "test"
|
string network = 1; // "main" or "test"
|
||||||
uint64 height = 2;
|
uint64 height = 2; // block height
|
||||||
string hash = 3; // block id
|
string hash = 3; // block id
|
||||||
uint32 time = 4; // Unix epoch time when the block was mined
|
uint32 time = 4; // Unix epoch time when the block was mined
|
||||||
string tree = 5; // sapling commitment tree state
|
string saplingTree = 5; // sapling commitment tree state
|
||||||
|
string orchardTree = 6; // orchard commitment tree state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Results are sorted by height, which makes it easy to issue another
|
||||||
|
// request that picks up from where the previous left off.
|
||||||
message GetAddressUtxosArg {
|
message GetAddressUtxosArg {
|
||||||
string address = 1;
|
repeated string addresses = 1;
|
||||||
uint64 startHeight = 2;
|
uint64 startHeight = 2;
|
||||||
uint32 maxEntries = 3; // zero means unlimited
|
uint32 maxEntries = 3; // zero means unlimited
|
||||||
}
|
}
|
||||||
message GetAddressUtxosReply {
|
message GetAddressUtxosReply {
|
||||||
|
string address = 6;
|
||||||
bytes txid = 1;
|
bytes txid = 1;
|
||||||
int32 index = 2;
|
int32 index = 2;
|
||||||
bytes script = 3;
|
bytes script = 3;
|
||||||
|
@ -161,17 +167,22 @@ service CompactTxStreamer {
|
||||||
// in the exclude list that don't exist in the mempool are ignored.
|
// in the exclude list that don't exist in the mempool are ignored.
|
||||||
rpc GetMempoolTx(Exclude) returns (stream CompactTx) {}
|
rpc GetMempoolTx(Exclude) returns (stream CompactTx) {}
|
||||||
|
|
||||||
|
// Return a stream of current Mempool transactions. This will keep the output stream open while
|
||||||
|
// there are mempool transactions. It will close the returned stream when a new block is mined.
|
||||||
|
rpc GetMempoolStream(Empty) returns (stream RawTransaction) {}
|
||||||
|
|
||||||
// GetTreeState returns the note commitment tree state corresponding to the given block.
|
// GetTreeState returns the note commitment tree state corresponding to the given block.
|
||||||
// See section 3.7 of the Zcash protocol specification. It returns several other useful
|
// See section 3.7 of the Zcash protocol specification. It returns several other useful
|
||||||
// values also (even though they can be obtained using GetBlock).
|
// values also (even though they can be obtained using GetBlock).
|
||||||
// The block can be specified by either height or hash.
|
// The block can be specified by either height or hash.
|
||||||
rpc GetTreeState(BlockID) returns (TreeState) {}
|
rpc GetTreeState(BlockID) returns (TreeState) {}
|
||||||
|
rpc GetLatestTreeState(Empty) returns (TreeState) {}
|
||||||
|
|
||||||
rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {}
|
rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {}
|
||||||
rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {}
|
rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {}
|
||||||
|
|
||||||
// Return information about this lightwalletd instance and the blockchain
|
// Return information about this lightwalletd instance and the blockchain
|
||||||
rpc GetLightdInfo(Empty) returns (LightdInfo) {}
|
rpc GetLightdInfo(Empty) returns (LightdInfo) {}
|
||||||
// Testing-only
|
// Testing-only, requires lightwalletd --ping-very-insecure (do not enable in production)
|
||||||
rpc Ping(Duration) returns (PingResponse) {}
|
rpc Ping(Duration) returns (PingResponse) {}
|
||||||
}
|
}
|
|
@ -1,10 +1,5 @@
|
||||||
import com.google.protobuf.gradle.generateProtoTasks
|
|
||||||
import com.google.protobuf.gradle.id
|
|
||||||
import com.google.protobuf.gradle.plugins
|
|
||||||
import com.google.protobuf.gradle.proto
|
|
||||||
import com.google.protobuf.gradle.protobuf
|
|
||||||
import com.google.protobuf.gradle.protoc
|
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.library")
|
id("com.android.library")
|
||||||
|
@ -14,7 +9,6 @@ plugins {
|
||||||
id("com.google.devtools.ksp")
|
id("com.google.devtools.ksp")
|
||||||
id("org.jetbrains.kotlin.plugin.allopen")
|
id("org.jetbrains.kotlin.plugin.allopen")
|
||||||
id("org.jetbrains.dokka")
|
id("org.jetbrains.dokka")
|
||||||
id("com.google.protobuf")
|
|
||||||
id("org.mozilla.rust-android-gradle.rust-android")
|
id("org.mozilla.rust-android-gradle.rust-android")
|
||||||
|
|
||||||
id("wtf.emulator.gradle")
|
id("wtf.emulator.gradle")
|
||||||
|
@ -154,11 +148,6 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets.getByName("main") {
|
|
||||||
java.srcDir("build/generated/source/grpc")
|
|
||||||
proto { srcDir("src/main/proto") }
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
// Tricky: fix: By default, the kotlin_module name will not include the version (in classes.jar/META-INF).
|
// Tricky: fix: By default, the kotlin_module name will not include the version (in classes.jar/META-INF).
|
||||||
// Instead it has a colon, which breaks compilation on Windows. This is one way to set it explicitly to the
|
// Instead it has a colon, which breaks compilation on Windows. This is one way to set it explicitly to the
|
||||||
|
@ -166,23 +155,6 @@ android {
|
||||||
freeCompilerArgs += listOf("-module-name", "$myArtifactId-${myVersion}_release")
|
freeCompilerArgs += listOf("-module-name", "$myArtifactId-${myVersion}_release")
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
resources.excludes.addAll(
|
|
||||||
listOf(
|
|
||||||
"META-INF/DEPENDENCIES",
|
|
||||||
"META-INF/LICENSE",
|
|
||||||
"META-INF/LICENSE.txt",
|
|
||||||
"META-INF/license.txt",
|
|
||||||
"META-INF/NOTICE",
|
|
||||||
"META-INF/NOTICE.txt",
|
|
||||||
"META-INF/notice.txt",
|
|
||||||
"META-INF/ASL2.0",
|
|
||||||
"META-INF/LICENSE.md",
|
|
||||||
"META-INF/LICENSE-notice.md"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
baseline = File("lint-baseline.xml")
|
baseline = File("lint-baseline.xml")
|
||||||
}
|
}
|
||||||
|
@ -195,6 +167,27 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
onVariants { variant ->
|
||||||
|
if (variant.name.toLowerCase(Locale.US).contains("release")) {
|
||||||
|
variant.packaging.resources.excludes.addAll(
|
||||||
|
listOf(
|
||||||
|
"META-INF/ASL2.0",
|
||||||
|
"META-INF/DEPENDENCIES",
|
||||||
|
"META-INF/LICENSE",
|
||||||
|
"META-INF/LICENSE-notice.md",
|
||||||
|
"META-INF/LICENSE.md",
|
||||||
|
"META-INF/LICENSE.txt",
|
||||||
|
"META-INF/NOTICE",
|
||||||
|
"META-INF/NOTICE.txt",
|
||||||
|
"META-INF/license.txt",
|
||||||
|
"META-INF/notice.txt"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
allOpen {
|
allOpen {
|
||||||
// marker for classes that we want to be able to extend in debug builds for testing purposes
|
// marker for classes that we want to be able to extend in debug builds for testing purposes
|
||||||
annotation("cash.z.ecc.android.sdk.annotation.OpenClass")
|
annotation("cash.z.ecc.android.sdk.annotation.OpenClass")
|
||||||
|
@ -210,30 +203,6 @@ tasks.dokkaHtml.configure {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protobuf {
|
|
||||||
//generatedFilesBaseDir = "$projectDir/src/generated/source/grpc"
|
|
||||||
protoc { artifact = libs.protoc.get().asCoordinateString() }
|
|
||||||
plugins {
|
|
||||||
id("grpc") {
|
|
||||||
artifact = libs.grpc.protoc.get().asCoordinateString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
generateProtoTasks {
|
|
||||||
all().forEach { task ->
|
|
||||||
task.builtins {
|
|
||||||
id("java") {
|
|
||||||
option("lite")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
task.plugins {
|
|
||||||
id("grpc") {
|
|
||||||
option("lite")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cargo {
|
cargo {
|
||||||
module = "."
|
module = "."
|
||||||
libname = "zcashwalletsdk"
|
libname = "zcashwalletsdk"
|
||||||
|
@ -255,6 +224,8 @@ cargo {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
api(projects.lightwalletClientLib)
|
||||||
|
|
||||||
implementation(libs.androidx.annotation)
|
implementation(libs.androidx.annotation)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
|
||||||
|
@ -278,10 +249,6 @@ dependencies {
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
|
||||||
// grpc-java
|
|
||||||
implementation(libs.bundles.grpc)
|
|
||||||
compileOnly(libs.javax.annotation)
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Locked Versions
|
// Locked Versions
|
||||||
// these should be checked regularly and removed when possible
|
// these should be checked regularly and removed when possible
|
||||||
|
@ -296,7 +263,6 @@ dependencies {
|
||||||
testImplementation(libs.kotlin.reflect)
|
testImplementation(libs.kotlin.reflect)
|
||||||
testImplementation(libs.kotlin.test)
|
testImplementation(libs.kotlin.test)
|
||||||
testImplementation(libs.bundles.junit)
|
testImplementation(libs.bundles.junit)
|
||||||
testImplementation(libs.grpc.testing)
|
|
||||||
|
|
||||||
// NOTE: androidTests will use JUnit4, while src/test/java tests will leverage Junit5
|
// NOTE: androidTests will use JUnit4, while src/test/java tests will leverage Junit5
|
||||||
// Attempting to use JUnit5 via https://github.com/mannodermaus/android-junit5 was painful. The plugin configuration
|
// Attempting to use JUnit5 via https://github.com/mannodermaus/android-junit5 was painful. The plugin configuration
|
||||||
|
@ -318,12 +284,6 @@ dependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
getByName("preBuild").dependsOn(create("bugfixTask") {
|
|
||||||
doFirst {
|
|
||||||
mkdir("build/extracted-include-protos/main")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The Mozilla Rust Gradle plugin caches the native build data under the "target" directory,
|
* The Mozilla Rust Gradle plugin caches the native build data under the "target" directory,
|
||||||
* which does not normally get deleted during a clean. The following task and dependency solves
|
* which does not normally get deleted during a clean. The following task and dependency solves
|
||||||
|
|
|
@ -8,29 +8,32 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||||
import cash.z.ecc.android.sdk.ext.onFirst
|
import cash.z.ecc.android.sdk.ext.onFirst
|
||||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.isSubmitSuccess
|
import cash.z.ecc.android.sdk.model.isSubmitSuccess
|
||||||
import cash.z.ecc.android.sdk.test.ScopedTest
|
import cash.z.ecc.android.sdk.test.ScopedTest
|
||||||
import cash.z.ecc.android.sdk.tool.CheckpointTool
|
import cash.z.ecc.android.sdk.tool.CheckpointTool
|
||||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||||
|
import co.electriccoin.lightwallet.client.BlockingLightWalletClient
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
|
import co.electriccoin.lightwallet.client.new
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.BeforeClass
|
import org.junit.BeforeClass
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class TestnetIntegrationTest : ScopedTest() {
|
class TestnetIntegrationTest : ScopedTest() {
|
||||||
|
|
||||||
|
@ -40,12 +43,13 @@ class TestnetIntegrationTest : ScopedTest() {
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test is broken")
|
@Ignore("This test is broken")
|
||||||
fun testLatestBlockTest() {
|
fun testLatestBlockTest() {
|
||||||
val service = LightWalletGrpcService.new(
|
val service = BlockingLightWalletClient.new(
|
||||||
context,
|
context,
|
||||||
lightWalletEndpoint
|
lightWalletEndpoint
|
||||||
)
|
)
|
||||||
val height = service.getLatestBlockHeight()
|
val height = service.getLatestBlockHeight()
|
||||||
assertTrue(height > saplingActivation)
|
assertTrue(height is Response.Success<BlockHeightUnsafe>)
|
||||||
|
assertTrue((height as Response.Success<BlockHeightUnsafe>).result.value > saplingActivation.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -81,8 +85,7 @@ class TestnetIntegrationTest : ScopedTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
assertTrue(
|
assertTrue(
|
||||||
availableBalance!!.value > 0,
|
availableBalance!!.value > 0
|
||||||
"No funds available when we expected a balance greater than zero!"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,33 +6,24 @@ import androidx.test.filters.SdkSuppress
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import cash.z.ecc.android.sdk.annotation.MaintainedTest
|
import cash.z.ecc.android.sdk.annotation.MaintainedTest
|
||||||
import cash.z.ecc.android.sdk.annotation.TestPurpose
|
import cash.z.ecc.android.sdk.annotation.TestPurpose
|
||||||
import cash.z.ecc.android.sdk.exception.LightWalletException.ChangeServerException.ChainInfoNotMatching
|
|
||||||
import cash.z.ecc.android.sdk.exception.LightWalletException.ChangeServerException.StatusException
|
|
||||||
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
|
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
|
||||||
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletService
|
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.Mainnet
|
import cash.z.ecc.android.sdk.model.Mainnet
|
||||||
import cash.z.ecc.android.sdk.model.Testnet
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.test.ScopedTest
|
import cash.z.ecc.android.sdk.test.ScopedTest
|
||||||
import kotlinx.coroutines.delay
|
import co.electriccoin.lightwallet.client.BlockingLightWalletClient
|
||||||
import kotlinx.coroutines.launch
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
import kotlinx.coroutines.runBlocking
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
|
import co.electriccoin.lightwallet.client.new
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertNotNull
|
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
import org.mockito.Spy
|
|
||||||
|
|
||||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
|
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
|
||||||
@MaintainedTest(TestPurpose.REGRESSION)
|
@MaintainedTest(TestPurpose.REGRESSION)
|
||||||
|
@ -48,17 +39,16 @@ class ChangeServiceTest : ScopedTest() {
|
||||||
lateinit var mockBlockStore: CompactBlockRepository
|
lateinit var mockBlockStore: CompactBlockRepository
|
||||||
var mockCloseable: AutoCloseable? = null
|
var mockCloseable: AutoCloseable? = null
|
||||||
|
|
||||||
@Spy
|
val service = BlockingLightWalletClient.new(context, lightWalletEndpoint)
|
||||||
val service = LightWalletGrpcService.new(context, lightWalletEndpoint)
|
|
||||||
|
|
||||||
lateinit var downloader: CompactBlockDownloader
|
lateinit var downloader: CompactBlockDownloader
|
||||||
lateinit var otherService: LightWalletService
|
lateinit var otherService: BlockingLightWalletClient
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
initMocks()
|
initMocks()
|
||||||
downloader = CompactBlockDownloader(service, mockBlockStore)
|
downloader = CompactBlockDownloader(service, mockBlockStore)
|
||||||
otherService = LightWalletGrpcService.new(context, eccEndpoint)
|
otherService = BlockingLightWalletClient.new(context, eccEndpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -79,99 +69,8 @@ class ChangeServiceTest : ScopedTest() {
|
||||||
twig(it)
|
twig(it)
|
||||||
}.getOrElse { return }
|
}.getOrElse { return }
|
||||||
|
|
||||||
assertTrue(result > network.saplingActivationHeight)
|
assertTrue(result is Response.Success<BlockHeightUnsafe>)
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
assertTrue((result as Response.Success<BlockHeightUnsafe>).result.value > network.saplingActivationHeight.value)
|
||||||
fun testCleanSwitch() = runBlocking {
|
|
||||||
// Test the result, only if there is no server communication problem.
|
|
||||||
val result = runCatching {
|
|
||||||
downloader.changeService(otherService)
|
|
||||||
return@runCatching downloader.downloadBlockRange(
|
|
||||||
BlockHeight.new(network, 900_000)..BlockHeight.new(network, 901_000)
|
|
||||||
)
|
|
||||||
}.onFailure {
|
|
||||||
twig(it)
|
|
||||||
}.getOrElse { return@runBlocking }
|
|
||||||
|
|
||||||
assertEquals(1_001, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repeatedly connect to servers and download a range of blocks. Switch part way through and
|
|
||||||
* verify that the servers change over, even while actively downloading.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
@Ignore("This test is broken")
|
|
||||||
fun testSwitchWhileActive() = runBlocking {
|
|
||||||
val start = BlockHeight.new(ZcashNetwork.Mainnet, 900_000)
|
|
||||||
val count = 5
|
|
||||||
val differentiators = mutableListOf<String>()
|
|
||||||
var initialValue = downloader.getServerInfo().buildUser
|
|
||||||
val job = testScope.launch {
|
|
||||||
repeat(count) {
|
|
||||||
differentiators.add(downloader.getServerInfo().buildUser)
|
|
||||||
twig("downloading from ${differentiators.last()}")
|
|
||||||
downloader.downloadBlockRange(start..(start + 100 * it))
|
|
||||||
delay(10L)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delay(30)
|
|
||||||
testScope.launch {
|
|
||||||
downloader.changeService(otherService)
|
|
||||||
}
|
|
||||||
job.join()
|
|
||||||
assertTrue(differentiators.count { it == initialValue } < differentiators.size)
|
|
||||||
assertEquals(count, differentiators.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testSwitchToInvalidServer() = runBlocking {
|
|
||||||
var caughtException: Throwable? = null
|
|
||||||
|
|
||||||
downloader.changeService(LightWalletGrpcService.new(context, LightWalletEndpoint("invalid.lightwalletd", 9087, true))) {
|
|
||||||
caughtException = it
|
|
||||||
}
|
|
||||||
|
|
||||||
// the test can continue only if there is no server communication problem
|
|
||||||
if (caughtException is StatusException) {
|
|
||||||
twig("Server communication problem while testing.")
|
|
||||||
return@runBlocking
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNotNull("Using an invalid host should generate an exception.", caughtException)
|
|
||||||
assertTrue(
|
|
||||||
"Exception was of the wrong type.",
|
|
||||||
caughtException is StatusException
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testSwitchToTestnetFails() = runBlocking {
|
|
||||||
var caughtException: Throwable? = null
|
|
||||||
|
|
||||||
downloader.changeService(LightWalletGrpcService.new(context, LightWalletEndpoint.Testnet)) {
|
|
||||||
caughtException = it
|
|
||||||
}
|
|
||||||
|
|
||||||
// the test can continue only if there is no server communication problem
|
|
||||||
if (caughtException is StatusException) {
|
|
||||||
twig("Server communication problem while testing.")
|
|
||||||
return@runBlocking
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNotNull("Using an invalid host should generate an exception.", caughtException)
|
|
||||||
assertTrue(
|
|
||||||
"Exception was of the wrong type. Expected ${ChainInfoNotMatching::class.simpleName} but was ${caughtException!!::class.simpleName}",
|
|
||||||
caughtException is ChainInfoNotMatching
|
|
||||||
)
|
|
||||||
(caughtException as ChainInfoNotMatching).propertyNames.let { props ->
|
|
||||||
arrayOf("saplingActivationHeight", "chainName").forEach {
|
|
||||||
assertTrue(
|
|
||||||
"$it should be a non-matching property but properties were [$props]",
|
|
||||||
props.contains(it, true)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
|
@ -9,7 +9,6 @@ import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.db.commonDatabaseBuilder
|
import cash.z.ecc.android.sdk.internal.db.commonDatabaseBuilder
|
||||||
import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDb
|
import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDb
|
||||||
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletService
|
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||||
|
@ -21,6 +20,7 @@ import cash.z.ecc.android.sdk.test.ScopedTest
|
||||||
import cash.z.ecc.android.sdk.test.getAppContext
|
import cash.z.ecc.android.sdk.test.getAppContext
|
||||||
import cash.z.ecc.fixture.DatabaseNameFixture
|
import cash.z.ecc.fixture.DatabaseNameFixture
|
||||||
import cash.z.ecc.fixture.DatabasePathFixture
|
import cash.z.ecc.fixture.DatabasePathFixture
|
||||||
|
import co.electriccoin.lightwallet.client.BlockingLightWalletClient
|
||||||
import com.nhaarman.mockitokotlin2.any
|
import com.nhaarman.mockitokotlin2.any
|
||||||
import com.nhaarman.mockitokotlin2.stub
|
import com.nhaarman.mockitokotlin2.stub
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
@ -45,7 +45,7 @@ class PersistentTransactionManagerTest : ScopedTest() {
|
||||||
internal lateinit var mockEncoder: TransactionEncoder
|
internal lateinit var mockEncoder: TransactionEncoder
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
lateinit var mockService: LightWalletService
|
lateinit var mockService: BlockingLightWalletClient
|
||||||
|
|
||||||
private val pendingDbFile = File(
|
private val pendingDbFile = File(
|
||||||
DatabasePathFixture.new(),
|
DatabasePathFixture.new(),
|
||||||
|
|
|
@ -9,11 +9,11 @@ import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
|
||||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||||
import cash.z.ecc.android.sdk.test.readFileLinesInFlow
|
import cash.z.ecc.android.sdk.test.readFileLinesInFlow
|
||||||
import cash.z.ecc.android.sdk.tool.CheckpointTool
|
import cash.z.ecc.android.sdk.tool.CheckpointTool
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
|
@ -7,9 +7,9 @@ import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
|
@ -6,17 +6,16 @@ import cash.z.ecc.android.bip39.toSeed
|
||||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.Testnet
|
import cash.z.ecc.android.sdk.model.Testnet
|
||||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.isPending
|
import cash.z.ecc.android.sdk.model.isPending
|
||||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
@ -73,7 +72,6 @@ class TestWallet(
|
||||||
seed = seed,
|
seed = seed,
|
||||||
startHeight
|
startHeight
|
||||||
) as SdkSynchronizer
|
) as SdkSynchronizer
|
||||||
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
|
|
||||||
|
|
||||||
val available get() = synchronizer.saplingBalances.value?.available
|
val available get() = synchronizer.saplingBalances.value?.available
|
||||||
val unifiedAddress =
|
val unifiedAddress =
|
||||||
|
|
|
@ -3,12 +3,15 @@ package cash.z.ecc.android.sdk.util
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
import cash.z.ecc.android.sdk.internal.model.from
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.Mainnet
|
import cash.z.ecc.android.sdk.model.Mainnet
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.BlockingLightWalletClient
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.new
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
@ -16,7 +19,7 @@ class TransactionCounterUtil {
|
||||||
|
|
||||||
private val network = ZcashNetwork.Mainnet
|
private val network = ZcashNetwork.Mainnet
|
||||||
private val context = InstrumentationRegistry.getInstrumentation().context
|
private val context = InstrumentationRegistry.getInstrumentation().context
|
||||||
private val service = LightWalletGrpcService.new(context, LightWalletEndpoint.Mainnet)
|
private val service = BlockingLightWalletClient.new(context, LightWalletEndpoint.Mainnet)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Twig.plant(TroubleshootingTwig())
|
Twig.plant(TroubleshootingTwig())
|
||||||
|
@ -27,9 +30,16 @@ class TransactionCounterUtil {
|
||||||
fun testBlockSize() {
|
fun testBlockSize() {
|
||||||
val sizes = mutableMapOf<Int, Int>()
|
val sizes = mutableMapOf<Int, Int>()
|
||||||
service.getBlockRange(
|
service.getBlockRange(
|
||||||
BlockHeight.new(ZcashNetwork.Mainnet, 900_000)..BlockHeight.new(
|
BlockHeightUnsafe.from(
|
||||||
ZcashNetwork.Mainnet,
|
BlockHeight.new(
|
||||||
910_000
|
ZcashNetwork.Mainnet,
|
||||||
|
900_000
|
||||||
|
)
|
||||||
|
)..BlockHeightUnsafe.from(
|
||||||
|
BlockHeight.new(
|
||||||
|
ZcashNetwork.Mainnet,
|
||||||
|
910_000
|
||||||
|
)
|
||||||
)
|
)
|
||||||
).forEach { b ->
|
).forEach { b ->
|
||||||
twig("h: ${b.header.size()}")
|
twig("h: ${b.header.size()}")
|
||||||
|
@ -47,9 +57,16 @@ class TransactionCounterUtil {
|
||||||
var totalOutputs = 0
|
var totalOutputs = 0
|
||||||
var totalTxs = 0
|
var totalTxs = 0
|
||||||
service.getBlockRange(
|
service.getBlockRange(
|
||||||
BlockHeight.new(ZcashNetwork.Mainnet, 900_000)..BlockHeight.new(
|
BlockHeightUnsafe.from(
|
||||||
ZcashNetwork.Mainnet,
|
BlockHeight.new(
|
||||||
950_000
|
ZcashNetwork.Mainnet,
|
||||||
|
900_000
|
||||||
|
)
|
||||||
|
)..BlockHeightUnsafe.from(
|
||||||
|
BlockHeight.new(
|
||||||
|
ZcashNetwork.Mainnet,
|
||||||
|
950_000
|
||||||
|
)
|
||||||
)
|
)
|
||||||
).forEach { b ->
|
).forEach { b ->
|
||||||
b.header.size()
|
b.header.size()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<!-- Required for downloading sapling params. -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -31,8 +31,6 @@ import cash.z.ecc.android.sdk.internal.isEmpty
|
||||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||||
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
||||||
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletService
|
|
||||||
import cash.z.ecc.android.sdk.internal.transaction.OutboundTransactionManager
|
import cash.z.ecc.android.sdk.internal.transaction.OutboundTransactionManager
|
||||||
import cash.z.ecc.android.sdk.internal.transaction.PersistentTransactionManager
|
import cash.z.ecc.android.sdk.internal.transaction.PersistentTransactionManager
|
||||||
import cash.z.ecc.android.sdk.internal.transaction.TransactionEncoder
|
import cash.z.ecc.android.sdk.internal.transaction.TransactionEncoder
|
||||||
|
@ -42,7 +40,6 @@ import cash.z.ecc.android.sdk.internal.twigTask
|
||||||
import cash.z.ecc.android.sdk.jni.RustBackend
|
import cash.z.ecc.android.sdk.jni.RustBackend
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.PendingTransaction
|
import cash.z.ecc.android.sdk.model.PendingTransaction
|
||||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||||
|
@ -62,8 +59,9 @@ import cash.z.ecc.android.sdk.type.AddressType.Transparent
|
||||||
import cash.z.ecc.android.sdk.type.AddressType.Unified
|
import cash.z.ecc.android.sdk.type.AddressType.Unified
|
||||||
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
||||||
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
|
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
|
||||||
import cash.z.wallet.sdk.rpc.Service
|
import co.electriccoin.lightwallet.client.BlockingLightWalletClient
|
||||||
import io.grpc.ManagedChannel
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.new
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -189,15 +187,6 @@ class SdkSynchronizer private constructor(
|
||||||
|
|
||||||
var coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
var coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
|
|
||||||
/**
|
|
||||||
* The channel that this Synchronizer uses to communicate with lightwalletd. In most cases, this
|
|
||||||
* should not be needed or used. Instead, APIs should be added to the synchronizer to
|
|
||||||
* enable the desired behavior. In the rare case, such as testing, it can be helpful to share
|
|
||||||
* the underlying channel to connect to the same service, and use other APIs
|
|
||||||
* (such as darksidewalletd) because channels are heavyweight.
|
|
||||||
*/
|
|
||||||
val channel: ManagedChannel get() = (processor.downloader.lightWalletService as LightWalletGrpcService).channel
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Balances
|
// Balances
|
||||||
//
|
//
|
||||||
|
@ -335,13 +324,6 @@ class SdkSynchronizer private constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function that exposes the underlying server information, like its name and
|
|
||||||
* consensus branch id. Most wallets should already have a different source of truth for the
|
|
||||||
* server(s) with which they operate.
|
|
||||||
*/
|
|
||||||
override suspend fun getServerInfo(): Service.LightdInfo = processor.downloader.getServerInfo()
|
|
||||||
|
|
||||||
override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight =
|
override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight =
|
||||||
processor.getNearestRewindHeight(height)
|
processor.getNearestRewindHeight(height)
|
||||||
|
|
||||||
|
@ -737,7 +719,7 @@ class SdkSynchronizer private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun validateConsensusBranch(): ConsensusMatchType {
|
override suspend fun validateConsensusBranch(): ConsensusMatchType {
|
||||||
val serverBranchId = tryNull { processor.downloader.getServerInfo().consensusBranchId }
|
val serverBranchId = tryNull { processor.downloader.getServerInfo()?.consensusBranchId }
|
||||||
val sdkBranchId = tryNull {
|
val sdkBranchId = tryNull {
|
||||||
(txManager as PersistentTransactionManager).encoder.getConsensusBranchId()
|
(txManager as PersistentTransactionManager).encoder.getConsensusBranchId()
|
||||||
}
|
}
|
||||||
|
@ -792,8 +774,8 @@ internal object DefaultSynchronizerFactory {
|
||||||
cacheDbFile
|
cacheDbFile
|
||||||
)
|
)
|
||||||
|
|
||||||
fun defaultService(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletService =
|
fun defaultService(context: Context, lightWalletEndpoint: LightWalletEndpoint): BlockingLightWalletClient =
|
||||||
LightWalletGrpcService.new(context, lightWalletEndpoint)
|
BlockingLightWalletClient.new(context, lightWalletEndpoint)
|
||||||
|
|
||||||
internal fun defaultEncoder(
|
internal fun defaultEncoder(
|
||||||
rustBackend: RustBackend,
|
rustBackend: RustBackend,
|
||||||
|
@ -802,7 +784,7 @@ internal object DefaultSynchronizerFactory {
|
||||||
): TransactionEncoder = WalletTransactionEncoder(rustBackend, saplingParamTool, repository)
|
): TransactionEncoder = WalletTransactionEncoder(rustBackend, saplingParamTool, repository)
|
||||||
|
|
||||||
fun defaultDownloader(
|
fun defaultDownloader(
|
||||||
service: LightWalletService,
|
service: BlockingLightWalletClient,
|
||||||
blockStore: CompactBlockRepository
|
blockStore: CompactBlockRepository
|
||||||
): CompactBlockDownloader = CompactBlockDownloader(service, blockStore)
|
): CompactBlockDownloader = CompactBlockDownloader(service, blockStore)
|
||||||
|
|
||||||
|
@ -811,7 +793,7 @@ internal object DefaultSynchronizerFactory {
|
||||||
zcashNetwork: ZcashNetwork,
|
zcashNetwork: ZcashNetwork,
|
||||||
alias: String,
|
alias: String,
|
||||||
encoder: TransactionEncoder,
|
encoder: TransactionEncoder,
|
||||||
service: LightWalletService
|
service: BlockingLightWalletClient
|
||||||
): OutboundTransactionManager {
|
): OutboundTransactionManager {
|
||||||
val databaseFile = DatabaseCoordinator.getInstance(context).pendingTransactionsDbFile(
|
val databaseFile = DatabaseCoordinator.getInstance(context).pendingTransactionsDbFile(
|
||||||
zcashNetwork,
|
zcashNetwork,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
||||||
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.ecc.android.sdk.model.PendingTransaction
|
import cash.z.ecc.android.sdk.model.PendingTransaction
|
||||||
import cash.z.ecc.android.sdk.model.Transaction
|
import cash.z.ecc.android.sdk.model.Transaction
|
||||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||||
|
@ -20,7 +19,7 @@ import cash.z.ecc.android.sdk.tool.CheckpointTool
|
||||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||||
import cash.z.ecc.android.sdk.type.AddressType
|
import cash.z.ecc.android.sdk.type.AddressType
|
||||||
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
||||||
import cash.z.wallet.sdk.rpc.Service
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -277,13 +276,6 @@ interface Synchronizer {
|
||||||
*/
|
*/
|
||||||
suspend fun validateAddress(address: String): AddressType
|
suspend fun validateAddress(address: String): AddressType
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function that exposes the underlying server information, like its name and
|
|
||||||
* consensus branch id. Most wallets should already have a different source of truth for the
|
|
||||||
* server(s) with which they operate and thereby not need this function.
|
|
||||||
*/
|
|
||||||
suspend fun getServerInfo(): Service.LightdInfo
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download all UTXOs for the given address and store any new ones in the database.
|
* Download all UTXOs for the given address and store any new ones in the database.
|
||||||
*
|
*
|
||||||
|
|
|
@ -14,12 +14,10 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Validating
|
||||||
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
|
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
|
||||||
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDecryptError
|
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDecryptError
|
||||||
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDownloadError
|
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDownloadError
|
||||||
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.MismatchedBranch
|
|
||||||
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.MismatchedNetwork
|
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.MismatchedNetwork
|
||||||
import cash.z.ecc.android.sdk.exception.InitializeException
|
import cash.z.ecc.android.sdk.exception.InitializeException
|
||||||
import cash.z.ecc.android.sdk.exception.RustLayerException
|
import cash.z.ecc.android.sdk.exception.RustLayerException
|
||||||
import cash.z.ecc.android.sdk.ext.BatchMetrics
|
import cash.z.ecc.android.sdk.ext.BatchMetrics
|
||||||
import cash.z.ecc.android.sdk.ext.BenchmarkingExt
|
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.DOWNLOAD_BATCH_SIZE
|
import cash.z.ecc.android.sdk.ext.ZcashSdk.DOWNLOAD_BATCH_SIZE
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.MAX_BACKOFF_INTERVAL
|
import cash.z.ecc.android.sdk.ext.ZcashSdk.MAX_BACKOFF_INTERVAL
|
||||||
|
@ -28,13 +26,14 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk.POLL_INTERVAL
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.RETRIES
|
import cash.z.ecc.android.sdk.ext.ZcashSdk.RETRIES
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.REWIND_DISTANCE
|
import cash.z.ecc.android.sdk.ext.ZcashSdk.REWIND_DISTANCE
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.SCAN_BATCH_SIZE
|
import cash.z.ecc.android.sdk.ext.ZcashSdk.SCAN_BATCH_SIZE
|
||||||
import cash.z.ecc.android.sdk.fixture.BlockRangeFixture
|
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
|
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
|
||||||
import cash.z.ecc.android.sdk.internal.ext.retryUpTo
|
import cash.z.ecc.android.sdk.internal.ext.retryUpTo
|
||||||
import cash.z.ecc.android.sdk.internal.ext.retryWithBackoff
|
import cash.z.ecc.android.sdk.internal.ext.retryWithBackoff
|
||||||
import cash.z.ecc.android.sdk.internal.ext.toHexReversed
|
import cash.z.ecc.android.sdk.internal.ext.toHexReversed
|
||||||
import cash.z.ecc.android.sdk.internal.isEmpty
|
import cash.z.ecc.android.sdk.internal.isEmpty
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.from
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.toBlockHeight
|
||||||
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.internal.twigTask
|
import cash.z.ecc.android.sdk.internal.twigTask
|
||||||
|
@ -45,8 +44,13 @@ import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||||
import cash.z.wallet.sdk.rpc.Service
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import io.grpc.StatusRuntimeException
|
import cash.z.wallet.sdk.internal.rpc.Service
|
||||||
|
import co.electriccoin.lightwallet.client.ext.BenchmarkingExt
|
||||||
|
import co.electriccoin.lightwallet.client.fixture.BlockRangeFixture
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpointInfoUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
@ -216,8 +220,9 @@ class CompactBlockProcessor internal constructor(
|
||||||
delay(napTime)
|
delay(napTime)
|
||||||
}
|
}
|
||||||
BlockProcessingResult.NoBlocksToProcess, BlockProcessingResult.FailedEnhance -> {
|
BlockProcessingResult.NoBlocksToProcess, BlockProcessingResult.FailedEnhance -> {
|
||||||
val noWorkDone = currentInfo.lastDownloadRange?.isEmpty()
|
val noWorkDone =
|
||||||
?: true && currentInfo.lastScanRange?.isEmpty() ?: true
|
currentInfo.lastDownloadRange?.isEmpty() ?: true &&
|
||||||
|
currentInfo.lastScanRange?.isEmpty() ?: true
|
||||||
val summary = if (noWorkDone) {
|
val summary = if (noWorkDone) {
|
||||||
"Nothing to process: no new blocks to download or scan"
|
"Nothing to process: no new blocks to download or scan"
|
||||||
} else {
|
} else {
|
||||||
|
@ -284,7 +289,7 @@ class CompactBlockProcessor internal constructor(
|
||||||
if (!updateRanges()) {
|
if (!updateRanges()) {
|
||||||
twig("Disconnection detected! Attempting to reconnect!")
|
twig("Disconnection detected! Attempting to reconnect!")
|
||||||
setState(Disconnected)
|
setState(Disconnected)
|
||||||
downloader.lightWalletService.reconnect()
|
downloader.lightWalletClient.reconnect()
|
||||||
BlockProcessingResult.Reconnecting
|
BlockProcessingResult.Reconnecting
|
||||||
} else if (currentInfo.lastDownloadRange.isEmpty() && currentInfo.lastScanRange.isEmpty()) {
|
} else if (currentInfo.lastDownloadRange.isEmpty() && currentInfo.lastScanRange.isEmpty()) {
|
||||||
setState(Scanned(currentInfo.lastScanRange))
|
setState(Scanned(currentInfo.lastScanRange))
|
||||||
|
@ -293,7 +298,12 @@ class CompactBlockProcessor internal constructor(
|
||||||
if (BenchmarkingExt.isBenchmarking()) {
|
if (BenchmarkingExt.isBenchmarking()) {
|
||||||
// We inject a benchmark test blocks range at this point to process only a restricted range of blocks
|
// We inject a benchmark test blocks range at this point to process only a restricted range of blocks
|
||||||
// for a more reliable benchmark results.
|
// for a more reliable benchmark results.
|
||||||
val benchmarkBlockRange = BlockRangeFixture.new()
|
val benchmarkBlockRange = BlockRangeFixture.new().let {
|
||||||
|
// Convert range of Longs to range of BlockHeights
|
||||||
|
BlockHeight.new(ZcashNetwork.Mainnet, it.start)..(
|
||||||
|
BlockHeight.new(ZcashNetwork.Mainnet, it.endInclusive)
|
||||||
|
)
|
||||||
|
}
|
||||||
downloadNewBlocks(benchmarkBlockRange)
|
downloadNewBlocks(benchmarkBlockRange)
|
||||||
val error = validateAndScanNewBlocks(benchmarkBlockRange)
|
val error = validateAndScanNewBlocks(benchmarkBlockRange)
|
||||||
if (error != BlockProcessingResult.Success) {
|
if (error != BlockProcessingResult.Success) {
|
||||||
|
@ -329,57 +339,65 @@ class CompactBlockProcessor internal constructor(
|
||||||
* @return true when the update succeeds.
|
* @return true when the update succeeds.
|
||||||
*/
|
*/
|
||||||
private suspend fun updateRanges(): Boolean = withContext(IO) {
|
private suspend fun updateRanges(): Boolean = withContext(IO) {
|
||||||
try {
|
// This fetches the latest height each time this method is called, which can be very inefficient
|
||||||
// TODO [#683]: rethink this and make it easier to understand what's happening. Can we reduce this
|
// when downloading all of the blocks from the server
|
||||||
// so that we only work with actual changing info rather than periodic snapshots? Do we need
|
val networkBlockHeight = run {
|
||||||
// to calculate these derived values every time?
|
val networkBlockHeightUnsafe =
|
||||||
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
|
when (val response = downloader.getLatestBlockHeight()) {
|
||||||
ProcessorInfo(
|
is Response.Success -> response.result
|
||||||
networkBlockHeight = downloader.getLatestBlockHeight(),
|
else -> null
|
||||||
lastScannedHeight = getLastScannedHeight(),
|
}
|
||||||
lastDownloadedHeight = getLastDownloadedHeight()?.let {
|
|
||||||
|
runCatching { networkBlockHeightUnsafe?.toBlockHeight(network) }.getOrNull()
|
||||||
|
} ?: return@withContext false
|
||||||
|
|
||||||
|
// TODO [#683]: rethink this and make it easier to understand what's happening. Can we reduce this
|
||||||
|
// so that we only work with actual changing info rather than periodic snapshots? Do we need
|
||||||
|
// to calculate these derived values every time?
|
||||||
|
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
|
||||||
|
ProcessorInfo(
|
||||||
|
networkBlockHeight = networkBlockHeight,
|
||||||
|
lastScannedHeight = getLastScannedHeight(),
|
||||||
|
lastDownloadedHeight = getLastDownloadedHeight()?.let {
|
||||||
|
BlockHeight.new(
|
||||||
|
network,
|
||||||
|
max(
|
||||||
|
it.value,
|
||||||
|
lowerBoundHeight.value - 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
lastDownloadRange = null,
|
||||||
|
lastScanRange = null
|
||||||
|
).let { initialInfo ->
|
||||||
|
updateProgress(
|
||||||
|
networkBlockHeight = initialInfo.networkBlockHeight,
|
||||||
|
lastScannedHeight = initialInfo.lastScannedHeight,
|
||||||
|
lastDownloadedHeight = initialInfo.lastDownloadedHeight,
|
||||||
|
lastScanRange = if (
|
||||||
|
initialInfo.lastScannedHeight != null &&
|
||||||
|
initialInfo.networkBlockHeight != null
|
||||||
|
) {
|
||||||
|
initialInfo.lastScannedHeight + 1..initialInfo.networkBlockHeight
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
lastDownloadRange = if (initialInfo.networkBlockHeight != null) {
|
||||||
BlockHeight.new(
|
BlockHeight.new(
|
||||||
network,
|
network,
|
||||||
max(
|
buildList {
|
||||||
it.value,
|
add(network.saplingActivationHeight.value)
|
||||||
lowerBoundHeight.value - 1
|
initialInfo.lastDownloadedHeight?.let { add(it.value + 1) }
|
||||||
)
|
initialInfo.lastScannedHeight?.let { add(it.value + 1) }
|
||||||
)
|
}.max()
|
||||||
},
|
)..initialInfo.networkBlockHeight
|
||||||
lastDownloadRange = null,
|
} else {
|
||||||
lastScanRange = null
|
null
|
||||||
).let { initialInfo ->
|
}
|
||||||
updateProgress(
|
)
|
||||||
networkBlockHeight = initialInfo.networkBlockHeight,
|
|
||||||
lastScannedHeight = initialInfo.lastScannedHeight,
|
|
||||||
lastDownloadedHeight = initialInfo.lastDownloadedHeight,
|
|
||||||
lastScanRange = if (
|
|
||||||
initialInfo.lastScannedHeight != null &&
|
|
||||||
initialInfo.networkBlockHeight != null
|
|
||||||
) {
|
|
||||||
initialInfo.lastScannedHeight + 1..initialInfo.networkBlockHeight
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
lastDownloadRange = if (initialInfo.networkBlockHeight != null) {
|
|
||||||
BlockHeight.new(
|
|
||||||
network,
|
|
||||||
buildList {
|
|
||||||
add(network.saplingActivationHeight.value)
|
|
||||||
initialInfo.lastDownloadedHeight?.let { add(it.value + 1) }
|
|
||||||
initialInfo.lastScannedHeight?.let { add(it.value + 1) }
|
|
||||||
}.max()
|
|
||||||
)..initialInfo.networkBlockHeight
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} catch (t: StatusRuntimeException) {
|
|
||||||
twig("Warning: failed to update ranges due to $t caused by ${t.cause}")
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -451,49 +469,65 @@ class CompactBlockProcessor internal constructor(
|
||||||
private suspend fun enhanceHelper(id: Long, rawTransactionId: ByteArray, minedHeight: BlockHeight) {
|
private suspend fun enhanceHelper(id: Long, rawTransactionId: ByteArray, minedHeight: BlockHeight) {
|
||||||
twig("START: enhancing transaction (id:$id block:$minedHeight)")
|
twig("START: enhancing transaction (id:$id block:$minedHeight)")
|
||||||
|
|
||||||
runCatching {
|
when (val response = downloader.fetchTransaction(rawTransactionId)) {
|
||||||
downloader.fetchTransaction(rawTransactionId)
|
is Response.Success -> {
|
||||||
}.onSuccess { tx ->
|
|
||||||
tx?.let {
|
|
||||||
runCatching {
|
runCatching {
|
||||||
twig("decrypting and storing transaction (id:$id block:$minedHeight)")
|
twig("decrypting and storing transaction (id:$id block:$minedHeight)")
|
||||||
rustBackend.decryptAndStoreTransaction(it.data.toByteArray())
|
rustBackend.decryptAndStoreTransaction(response.result.data)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
twig("DONE: enhancing transaction (id:$id block:$minedHeight)")
|
twig("DONE: enhancing transaction (id:$id block:$minedHeight)")
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
onProcessorError(EnhanceTxDecryptError(minedHeight, error))
|
onProcessorError(EnhanceTxDecryptError(minedHeight, error))
|
||||||
}
|
}
|
||||||
} ?: twig("no transaction found. Nothing to enhance. This probably shouldn't happen.")
|
}
|
||||||
}.onFailure { error ->
|
is Response.Failure -> {
|
||||||
onProcessorError(EnhanceTxDownloadError(minedHeight, error))
|
onProcessorError(EnhanceTxDownloadError(minedHeight, response.toThrowable()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm that the wallet data is properly setup for use.
|
* Confirm that the wallet data is properly setup for use.
|
||||||
*/
|
*/
|
||||||
|
// Need to refactor this to be less ugly and more testable
|
||||||
|
@Suppress("NestedBlockDepth")
|
||||||
private suspend fun verifySetup() {
|
private suspend fun verifySetup() {
|
||||||
// verify that the data is initialized
|
// verify that the data is initialized
|
||||||
var error = when {
|
val error = if (!repository.isInitialized()) {
|
||||||
!repository.isInitialized() -> CompactBlockProcessorException.Uninitialized
|
CompactBlockProcessorException.Uninitialized
|
||||||
repository.getAccountCount() == 0 -> CompactBlockProcessorException.NoAccount
|
} else if (repository.getAccountCount() == 0) {
|
||||||
else -> {
|
CompactBlockProcessorException.NoAccount
|
||||||
// verify that the server is correct
|
} else {
|
||||||
downloader.getServerInfo().let { info ->
|
// verify that the server is correct
|
||||||
val clientBranch =
|
|
||||||
"%x".format(rustBackend.getBranchIdForHeight(BlockHeight(info.blockHeight)))
|
// How do we handle network connection issues?
|
||||||
|
|
||||||
|
downloader.getServerInfo()?.let { info ->
|
||||||
|
val serverBlockHeight =
|
||||||
|
runCatching { info.blockHeightUnsafe.toBlockHeight(network) }.getOrNull()
|
||||||
|
|
||||||
|
if (null == serverBlockHeight) {
|
||||||
|
// TODO Better signal network connection issue
|
||||||
|
CompactBlockProcessorException.BadBlockHeight(info.blockHeightUnsafe)
|
||||||
|
} else {
|
||||||
|
val clientBranch = "%x".format(
|
||||||
|
Locale.ROOT,
|
||||||
|
rustBackend.getBranchIdForHeight(serverBlockHeight)
|
||||||
|
)
|
||||||
val network = rustBackend.network.networkName
|
val network = rustBackend.network.networkName
|
||||||
when {
|
|
||||||
!info.matchingNetwork(network) -> MismatchedNetwork(
|
if (!clientBranch.equals(info.consensusBranchId, true)) {
|
||||||
|
MismatchedNetwork(
|
||||||
clientNetwork = network,
|
clientNetwork = network,
|
||||||
serverNetwork = info.chainName
|
serverNetwork = info.chainName
|
||||||
)
|
)
|
||||||
!info.matchingConsensusBranchId(clientBranch) -> MismatchedBranch(
|
} else if (!info.matchingNetwork(network)) {
|
||||||
clientBranch = clientBranch,
|
MismatchedNetwork(
|
||||||
serverBranch = info.consensusBranchId,
|
clientNetwork = network,
|
||||||
networkName = network
|
serverNetwork = info.chainName
|
||||||
)
|
)
|
||||||
else -> null
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -540,7 +574,10 @@ class CompactBlockProcessor internal constructor(
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
try {
|
try {
|
||||||
retryUpTo(3) {
|
retryUpTo(3) {
|
||||||
val result = downloader.lightWalletService.fetchUtxos(tAddress, startHeight)
|
val result = downloader.lightWalletClient.fetchUtxos(
|
||||||
|
tAddress,
|
||||||
|
BlockHeightUnsafe.from(startHeight)
|
||||||
|
)
|
||||||
count = processUtxoResult(result, tAddress, startHeight)
|
count = processUtxoResult(result, tAddress, startHeight)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
@ -561,7 +598,7 @@ class CompactBlockProcessor internal constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun processUtxoResult(
|
internal suspend fun processUtxoResult(
|
||||||
result: List<Service.GetAddressUtxosReply>,
|
result: Sequence<Service.GetAddressUtxosReply>,
|
||||||
tAddress: String,
|
tAddress: String,
|
||||||
startHeight: BlockHeight
|
startHeight: BlockHeight
|
||||||
): Int = withContext(IO) {
|
): Int = withContext(IO) {
|
||||||
|
@ -601,7 +638,7 @@ class CompactBlockProcessor internal constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// return the number of UTXOs that were downloaded
|
// return the number of UTXOs that were downloaded
|
||||||
result.size - skipped
|
result.count() - skipped
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -854,12 +891,12 @@ class CompactBlockProcessor internal constructor(
|
||||||
|
|
||||||
if (alsoClearBlockCache) {
|
if (alsoClearBlockCache) {
|
||||||
twig(
|
twig(
|
||||||
"Also clearing block cache back to $targetHeight. These rewound blocks will " +
|
"Also clearing block cache back to $targetHeight. These rewound blocks will download " +
|
||||||
"download in the next scheduled scan"
|
"in the next scheduled scan"
|
||||||
)
|
)
|
||||||
downloader.rewindToHeight(targetHeight)
|
downloader.rewindToHeight(targetHeight)
|
||||||
// communicate that the wallet is no longer synced because it might remain this way for 20+
|
// communicate that the wallet is no longer synced because it might remain this way for 20+ second
|
||||||
// seconds because we only download on 20s time boundaries so we can't trigger any immediate action
|
// because we only download on 20s time boundaries so we can't trigger any immediate action
|
||||||
setState(Downloading)
|
setState(Downloading)
|
||||||
if (null == currentNetworkBlockHeight) {
|
if (null == currentNetworkBlockHeight) {
|
||||||
updateProgress(
|
updateProgress(
|
||||||
|
@ -1256,21 +1293,6 @@ class CompactBlockProcessor internal constructor(
|
||||||
// Helper Extensions
|
// Helper Extensions
|
||||||
//
|
//
|
||||||
|
|
||||||
private fun Service.LightdInfo.matchingConsensusBranchId(clientBranch: String): Boolean {
|
|
||||||
return consensusBranchId.equals(clientBranch, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Service.LightdInfo.matchingNetwork(network: String): Boolean {
|
|
||||||
fun String.toId() = lowercase(Locale.US).run {
|
|
||||||
when {
|
|
||||||
contains("main") -> "mainnet"
|
|
||||||
contains("test") -> "testnet"
|
|
||||||
else -> this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chainName.toId() == network.toId()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the mutex in great detail just in case we need it for troubleshooting deadlock.
|
* Log the mutex in great detail just in case we need it for troubleshooting deadlock.
|
||||||
*/
|
*/
|
||||||
|
@ -1285,6 +1307,17 @@ class CompactBlockProcessor internal constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun LightWalletEndpointInfoUnsafe.matchingNetwork(network: String): Boolean {
|
||||||
|
fun String.toId() = lowercase(Locale.ROOT).run {
|
||||||
|
when {
|
||||||
|
contains("main") -> "mainnet"
|
||||||
|
contains("test") -> "testnet"
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chainName.toId() == network.toId()
|
||||||
|
}
|
||||||
|
|
||||||
private fun max(a: BlockHeight?, b: BlockHeight) = if (null == a) {
|
private fun max(a: BlockHeight?, b: BlockHeight) = if (null == a) {
|
||||||
b
|
b
|
||||||
} else if (a.value > b.value) {
|
} else if (a.value > b.value) {
|
||||||
|
|
|
@ -4,7 +4,8 @@ import cash.z.ecc.android.sdk.internal.SaplingParameters
|
||||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.wallet.sdk.rpc.Service
|
import cash.z.wallet.sdk.internal.rpc.Service
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
import io.grpc.Status
|
import io.grpc.Status
|
||||||
import io.grpc.Status.Code.UNAVAILABLE
|
import io.grpc.Status.Code.UNAVAILABLE
|
||||||
|
|
||||||
|
@ -138,6 +139,9 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
|
||||||
"Incompatible server: this client expects a server following consensus branch $clientBranch on $networkName " +
|
"Incompatible server: this client expects a server following consensus branch $clientBranch on $networkName " +
|
||||||
"but it was $serverBranch! Try updating the client or switching servers."
|
"but it was $serverBranch! Try updating the client or switching servers."
|
||||||
)
|
)
|
||||||
|
class BadBlockHeight(serverBlockHeight: BlockHeightUnsafe) : CompactBlockProcessorException(
|
||||||
|
"The server returned a block height of $serverBlockHeight which is not valid."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
package cash.z.ecc.android.sdk.fixture
|
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|
||||||
|
|
||||||
object BlockRangeFixture {
|
|
||||||
|
|
||||||
// Be aware that changing these bounds values in a broader range may result in a timeout reached in
|
|
||||||
// SyncBlockchainBenchmark. So if changing these, don't forget to align also the test timeout in
|
|
||||||
// waitForBalanceScreen() appropriately.
|
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
private val BLOCK_HEIGHT_LOWER_BOUND = BlockHeight.new(ZcashNetwork.Mainnet, 1730001L)
|
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
private val BLOCK_HEIGHT_UPPER_BOUND = BlockHeight.new(ZcashNetwork.Mainnet, 1730100L)
|
|
||||||
|
|
||||||
fun new(
|
|
||||||
lowerBound: BlockHeight = BLOCK_HEIGHT_LOWER_BOUND,
|
|
||||||
upperBound: BlockHeight = BLOCK_HEIGHT_UPPER_BOUND
|
|
||||||
): ClosedRange<BlockHeight> {
|
|
||||||
return lowerBound..upperBound
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +1,52 @@
|
||||||
package cash.z.ecc.android.sdk.internal.block
|
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.retryUpTo
|
||||||
import cash.z.ecc.android.sdk.internal.ext.tryWarn
|
import cash.z.ecc.android.sdk.internal.model.from
|
||||||
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletService
|
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.wallet.sdk.rpc.Service
|
import co.electriccoin.lightwallet.client.BlockingLightWalletClient
|
||||||
import io.grpc.StatusRuntimeException
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpointInfoUnsafe
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serves as a source of compact blocks received from the light wallet server. Once started, it will use the given
|
* 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
|
* lightWallet client 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
|
* 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.
|
* data; although, by default the SDK uses gRPC and SQL.
|
||||||
*
|
*
|
||||||
* @property lightWalletService the service used for requesting compact blocks
|
* @property lightWalletClient the client used for requesting compact blocks
|
||||||
* @property compactBlockRepository responsible for persisting the compact blocks that are received
|
* @property compactBlockStore responsible for persisting the compact blocks that are received
|
||||||
*/
|
*/
|
||||||
open class CompactBlockDownloader private constructor(val compactBlockRepository: CompactBlockRepository) {
|
open class CompactBlockDownloader private constructor(val compactBlockRepository: CompactBlockRepository) {
|
||||||
|
|
||||||
lateinit var lightWalletService: LightWalletService
|
lateinit var lightWalletClient: BlockingLightWalletClient
|
||||||
private set
|
private set
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
lightWalletService: LightWalletService,
|
lightWalletClient: BlockingLightWalletClient,
|
||||||
compactBlockRepository: CompactBlockRepository
|
compactBlockRepository: CompactBlockRepository
|
||||||
) : this(compactBlockRepository) {
|
) : this(compactBlockRepository) {
|
||||||
this.lightWalletService = lightWalletService
|
this.lightWalletClient = lightWalletClient
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests the given range of blocks from the lightwalletService and then persists them to the
|
* Requests the given range of blocks from the lightWalletClient and then persists them to the
|
||||||
* compactBlockStore.
|
* compactBlockStore.
|
||||||
*
|
*
|
||||||
* @param heightRange the inclusive range of heights to request. For example 10..20 would
|
* @param heightRange the inclusive range of heights to request. For example 10..20 would
|
||||||
* request 11 blocks (including block 10 and block 20).
|
* request 11 blocks (including block 10 and block 20).
|
||||||
*
|
*
|
||||||
* @return the number of blocks that were returned in the results from the lightwalletService.
|
* @return the number of blocks that were returned in the results from the lightWalletClient.
|
||||||
*/
|
*/
|
||||||
suspend fun downloadBlockRange(heightRange: ClosedRange<BlockHeight>): Int = withContext(IO) {
|
suspend fun downloadBlockRange(heightRange: ClosedRange<BlockHeight>): Int = withContext(IO) {
|
||||||
val result = lightWalletService.getBlockRange(heightRange)
|
val result = lightWalletClient.getBlockRange(
|
||||||
|
BlockHeightUnsafe.from(heightRange.start)..BlockHeightUnsafe.from(heightRange.endInclusive)
|
||||||
|
)
|
||||||
compactBlockRepository.write(result)
|
compactBlockRepository.write(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,12 +62,12 @@ open class CompactBlockDownloader private constructor(val compactBlockRepository
|
||||||
compactBlockRepository.rewindTo(height)
|
compactBlockRepository.rewindTo(height)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the latest block height known by the lightwalletService.
|
* Return the latest block height known by the lightWalletClient.
|
||||||
*
|
*
|
||||||
* @return the latest block height.
|
* @return the latest block height.
|
||||||
*/
|
*/
|
||||||
suspend fun getLatestBlockHeight() =
|
suspend fun getLatestBlockHeight() =
|
||||||
lightWalletService.getLatestBlockHeight()
|
lightWalletClient.getLatestBlockHeight()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the latest block height that has been persisted into the [CompactBlockRepository].
|
* Return the latest block height that has been persisted into the [CompactBlockRepository].
|
||||||
|
@ -77,47 +77,18 @@ open class CompactBlockDownloader private constructor(val compactBlockRepository
|
||||||
suspend fun getLastDownloadedHeight() =
|
suspend fun getLastDownloadedHeight() =
|
||||||
compactBlockRepository.getLatestHeight()
|
compactBlockRepository.getLatestHeight()
|
||||||
|
|
||||||
suspend fun getServerInfo(): Service.LightdInfo = withContext<Service.LightdInfo>(IO) {
|
suspend fun getServerInfo(): LightWalletEndpointInfoUnsafe? = withContext(IO) {
|
||||||
lateinit var result: Service.LightdInfo
|
retryUpTo(GET_SERVER_INFO_RETRIES) {
|
||||||
try {
|
when (val response = lightWalletClient.getServerInfo()) {
|
||||||
result = lightWalletService.getServerInfo()
|
is Response.Success -> return@withContext response.result
|
||||||
} catch (e: StatusRuntimeException) {
|
else -> {
|
||||||
retryUpTo(GET_SERVER_INFO_RETRIES) {
|
lightWalletClient.reconnect()
|
||||||
twig("WARNING: reconnecting to service in response to failure (retry #${it + 1}): $e")
|
twig("WARNING: reconnecting to server in response to failure (retry #${it + 1})")
|
||||||
lightWalletService.reconnect()
|
}
|
||||||
result = lightWalletService.getServerInfo()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun changeService(
|
null
|
||||||
newService: LightWalletService,
|
|
||||||
errorHandler: (Throwable) -> Unit = { throw it }
|
|
||||||
) = withContext(IO) {
|
|
||||||
@Suppress("TooGenericExceptionCaught")
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -125,7 +96,7 @@ open class CompactBlockDownloader private constructor(val compactBlockRepository
|
||||||
*/
|
*/
|
||||||
suspend fun stop() {
|
suspend fun stop() {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
lightWalletService.shutdown()
|
lightWalletClient.shutdown()
|
||||||
}
|
}
|
||||||
compactBlockRepository.close()
|
compactBlockRepository.close()
|
||||||
}
|
}
|
||||||
|
@ -135,35 +106,7 @@ open class CompactBlockDownloader private constructor(val compactBlockRepository
|
||||||
*
|
*
|
||||||
* @return the full transaction info.
|
* @return the full transaction info.
|
||||||
*/
|
*/
|
||||||
fun fetchTransaction(txId: ByteArray) = lightWalletService.fetchTransaction(txId)
|
fun fetchTransaction(txId: ByteArray) = lightWalletClient.fetchTransaction(txId)
|
||||||
|
|
||||||
//
|
|
||||||
// Convenience functions
|
|
||||||
//
|
|
||||||
|
|
||||||
private suspend fun CoroutineScope.gracefullyShutdown(service: LightWalletService) = launch {
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val GET_SERVER_INFO_RETRIES = 6
|
private const val GET_SERVER_INFO_RETRIES = 6
|
||||||
|
|
|
@ -8,7 +8,7 @@ import cash.z.ecc.android.sdk.internal.db.commonDatabaseBuilder
|
||||||
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
import cash.z.wallet.sdk.internal.rpc.CompactFormats
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
|
@ -42,32 +42,6 @@ suspend inline fun retryUpTo(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the given block and if it fails, retry up to [retries] more times, using thread sleep
|
|
||||||
* instead of suspending. If none of the retries succeed then throw the final error. This function
|
|
||||||
* is intended to be called with no parameters, i.e., it is designed to use its defaults.
|
|
||||||
*
|
|
||||||
* @param retries the number of times to retry. Typically, this should be low.
|
|
||||||
* @param sleepTime the amount of time to sleep in between retries. Typically, this should be an
|
|
||||||
* amount of time that is hard to perceive.
|
|
||||||
* @param block the block of logic to try.
|
|
||||||
*/
|
|
||||||
inline fun retrySimple(retries: Int = 2, sleepTime: Long = 20L, block: (Int) -> Unit) {
|
|
||||||
var failedAttempts = 0
|
|
||||||
while (failedAttempts <= retries) {
|
|
||||||
@Suppress("TooGenericExceptionCaught")
|
|
||||||
try {
|
|
||||||
block(failedAttempts)
|
|
||||||
return
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
failedAttempts++
|
|
||||||
if (failedAttempts > retries) throw t
|
|
||||||
twig("failed due to $t simply retrying ($failedAttempts/$retries) in ${sleepTime}ms...")
|
|
||||||
Thread.sleep(sleepTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the given block and if it fails, retry with an exponential backoff.
|
* Execute the given block and if it fails, retry with an exponential backoff.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
|
|
||||||
|
internal fun BlockHeightUnsafe.Companion.from(blockHeight: BlockHeight) =
|
||||||
|
BlockHeightUnsafe(blockHeight.value)
|
||||||
|
|
||||||
|
internal fun BlockHeightUnsafe.toBlockHeight(zcashNetwork: ZcashNetwork) =
|
||||||
|
BlockHeight.new(zcashNetwork, value)
|
|
@ -1,7 +1,7 @@
|
||||||
package cash.z.ecc.android.sdk.internal.repository
|
package cash.z.ecc.android.sdk.internal.repository
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
import cash.z.wallet.sdk.internal.rpc.CompactFormats
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for storing compact blocks.
|
* Interface for storing compact blocks.
|
||||||
|
|
|
@ -1,210 +0,0 @@
|
||||||
package cash.z.ecc.android.sdk.internal.service
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import cash.z.ecc.android.sdk.annotation.OpenForTesting
|
|
||||||
import cash.z.ecc.android.sdk.ext.BenchmarkingExt
|
|
||||||
import cash.z.ecc.android.sdk.fixture.BlockRangeFixture
|
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
|
||||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
||||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
|
||||||
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc
|
|
||||||
import cash.z.wallet.sdk.rpc.Service
|
|
||||||
import com.google.protobuf.ByteString
|
|
||||||
import io.grpc.Channel
|
|
||||||
import io.grpc.ConnectivityState
|
|
||||||
import io.grpc.ManagedChannel
|
|
||||||
import io.grpc.android.AndroidChannelBuilder
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of LightwalletService using gRPC for requests to lightwalletd.
|
|
||||||
*
|
|
||||||
* @property channel the channel to use for communicating with the lightwalletd server.
|
|
||||||
* @property singleRequestTimeout 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 streamingRequestTimeout 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(
|
|
||||||
context: Context,
|
|
||||||
private val lightWalletEndpoint: LightWalletEndpoint,
|
|
||||||
var channel: ManagedChannel,
|
|
||||||
private val singleRequestTimeout: Duration = 10.seconds,
|
|
||||||
private val streamingRequestTimeout: Duration = 90.seconds
|
|
||||||
) : LightWalletService {
|
|
||||||
|
|
||||||
private val applicationContext = context.applicationContext
|
|
||||||
|
|
||||||
/* LightWalletService implementation */
|
|
||||||
|
|
||||||
override fun getBlockRange(heightRange: ClosedRange<BlockHeight>): Sequence<CompactFormats.CompactBlock> {
|
|
||||||
if (heightRange.isEmpty()) {
|
|
||||||
return emptySequence()
|
|
||||||
}
|
|
||||||
|
|
||||||
return requireChannel().createStub(streamingRequestTimeout)
|
|
||||||
.getBlockRange(heightRange.toBlockRange()).iterator().asSequence()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLatestBlockHeight(): BlockHeight {
|
|
||||||
return if (BenchmarkingExt.isBenchmarking()) {
|
|
||||||
// We inject a benchmark test blocks range at this point to process only a restricted range of blocks
|
|
||||||
// for a more reliable benchmark results.
|
|
||||||
BlockRangeFixture.new().endInclusive
|
|
||||||
} else {
|
|
||||||
BlockHeight(
|
|
||||||
requireChannel().createStub(singleRequestTimeout)
|
|
||||||
.getLatestBlock(Service.ChainSpec.newBuilder().build()).height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getServerInfo(): Service.LightdInfo {
|
|
||||||
return requireChannel().createStub(singleRequestTimeout)
|
|
||||||
.getLightdInfo(Service.Empty.newBuilder().build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse {
|
|
||||||
if (spendTransaction.isEmpty()) {
|
|
||||||
return Service.SendResponse.newBuilder()
|
|
||||||
.setErrorCode(EMPTY_TRANSACTION_ERROR_CODE)
|
|
||||||
.setErrorMessage(EMPTY_TRANSACTION_ERROR_MESSAGE)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
val request =
|
|
||||||
Service.RawTransaction.newBuilder().setData(ByteString.copyFrom(spendTransaction))
|
|
||||||
.build()
|
|
||||||
return requireChannel().createStub().sendTransaction(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shutdown() {
|
|
||||||
twig("Shutting down channel")
|
|
||||||
channel.shutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchTransaction(txId: ByteArray): Service.RawTransaction? {
|
|
||||||
if (txId.isEmpty()) return null
|
|
||||||
|
|
||||||
return requireChannel().createStub().getTransaction(
|
|
||||||
Service.TxFilter.newBuilder().setHash(ByteString.copyFrom(txId)).build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchUtxos(
|
|
||||||
tAddress: String,
|
|
||||||
startHeight: BlockHeight
|
|
||||||
): List<Service.GetAddressUtxosReply> {
|
|
||||||
val result = requireChannel().createStub().getAddressUtxos(
|
|
||||||
Service.GetAddressUtxosArg.newBuilder().setAddress(tAddress)
|
|
||||||
.setStartHeight(startHeight.value).build()
|
|
||||||
)
|
|
||||||
return result.addressUtxosList
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTAddressTransactions(
|
|
||||||
tAddress: String,
|
|
||||||
blockHeightRange: ClosedRange<BlockHeight>
|
|
||||||
): List<Service.RawTransaction> {
|
|
||||||
if (blockHeightRange.isEmpty() || tAddress.isBlank()) return listOf()
|
|
||||||
|
|
||||||
val result = requireChannel().createStub().getTaddressTxids(
|
|
||||||
Service.TransparentAddressBlockFilter.newBuilder().setAddress(tAddress)
|
|
||||||
.setRange(blockHeightRange.toBlockRange()).build()
|
|
||||||
)
|
|
||||||
return result.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun reconnect() {
|
|
||||||
twig("closing existing channel and then reconnecting")
|
|
||||||
channel.shutdown()
|
|
||||||
channel = createDefaultChannel(applicationContext, lightWalletEndpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test code
|
|
||||||
internal var stateCount = 0
|
|
||||||
internal var state: ConnectivityState? = null
|
|
||||||
private fun requireChannel(): ManagedChannel {
|
|
||||||
state = channel.getState(false).let { new ->
|
|
||||||
if (state == new) stateCount++ else stateCount = 0
|
|
||||||
new
|
|
||||||
}
|
|
||||||
channel.resetConnectBackoff()
|
|
||||||
twig(
|
|
||||||
"getting channel isShutdown: ${channel.isShutdown} " +
|
|
||||||
"isTerminated: ${channel.isTerminated} " +
|
|
||||||
"getState: $state stateCount: $stateCount",
|
|
||||||
-1
|
|
||||||
)
|
|
||||||
return channel
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val EMPTY_TRANSACTION_ERROR_CODE = 3000
|
|
||||||
private const val EMPTY_TRANSACTION_ERROR_MESSAGE = "ERROR: failed to submit transaction because it was" +
|
|
||||||
" empty so this request was ignored on the client-side."
|
|
||||||
|
|
||||||
fun new(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletGrpcService {
|
|
||||||
val channel = createDefaultChannel(context, lightWalletEndpoint)
|
|
||||||
|
|
||||||
return LightWalletGrpcService(context, lightWalletEndpoint, channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function for creating the default channel to be used for all connections. It
|
|
||||||
* is important that this channel can handle transitioning from WiFi to Cellular connections
|
|
||||||
* and is properly setup to support TLS, when required.
|
|
||||||
*/
|
|
||||||
fun createDefaultChannel(
|
|
||||||
appContext: Context,
|
|
||||||
lightWalletEndpoint: LightWalletEndpoint
|
|
||||||
): ManagedChannel {
|
|
||||||
twig(
|
|
||||||
"Creating channel that will connect to " +
|
|
||||||
"${lightWalletEndpoint.host}:${lightWalletEndpoint.port}" +
|
|
||||||
"/?usePlaintext=${!lightWalletEndpoint.isSecure}"
|
|
||||||
)
|
|
||||||
return AndroidChannelBuilder
|
|
||||||
.forAddress(lightWalletEndpoint.host, lightWalletEndpoint.port)
|
|
||||||
.context(appContext)
|
|
||||||
.enableFullStreamDecompression()
|
|
||||||
.apply {
|
|
||||||
if (lightWalletEndpoint.isSecure) {
|
|
||||||
useTransportSecurity()
|
|
||||||
} else {
|
|
||||||
twig("WARNING: Using insecure channel")
|
|
||||||
usePlaintext()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Channel.createStub(timeoutSec: Duration = 60.seconds) = CompactTxStreamerGrpc
|
|
||||||
.newBlockingStub(this)
|
|
||||||
.withDeadlineAfter(timeoutSec.inWholeSeconds, TimeUnit.SECONDS)
|
|
||||||
|
|
||||||
private fun BlockHeight.toBlockHeight(): Service.BlockID =
|
|
||||||
Service.BlockID.newBuilder().setHeight(value).build()
|
|
||||||
|
|
||||||
private fun ClosedRange<BlockHeight>.toBlockRange(): Service.BlockRange =
|
|
||||||
Service.BlockRange.newBuilder()
|
|
||||||
.setStart(start.toBlockHeight())
|
|
||||||
.setEnd(endInclusive.toBlockHeight())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function effectively parses streaming responses. Each call to next(), on the iterators
|
|
||||||
* returned from grpc, triggers a network call.
|
|
||||||
*/
|
|
||||||
private fun <T> Iterator<T>.toList(): List<T> =
|
|
||||||
mutableListOf<T>().apply {
|
|
||||||
while (hasNext()) {
|
|
||||||
this@apply += next()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
package cash.z.ecc.android.sdk.internal.service
|
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
|
||||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
|
||||||
import cash.z.wallet.sdk.rpc.Service
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for interacting with lightwalletd. Implementers of this service should make blocking
|
|
||||||
* 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?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch all UTXOs for the given address, going back to the start height.
|
|
||||||
*
|
|
||||||
* @param tAddress the transparent address to use.
|
|
||||||
* @param startHeight the starting height to use.
|
|
||||||
*
|
|
||||||
* @return the UTXOs for the given address from the startHeight.
|
|
||||||
*/
|
|
||||||
fun fetchUtxos(tAddress: String, startHeight: BlockHeight): List<Service.GetAddressUtxosReply>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the given range of blocks.
|
|
||||||
*
|
|
||||||
* @param heightRange the inclusive range to fetch. For instance if 1..5 is given, then every
|
|
||||||
* block in that range will be fetched, including 1 and 5.
|
|
||||||
*
|
|
||||||
* @return a list of compact blocks for the given range
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fun getBlockRange(heightRange: ClosedRange<BlockHeight>): Sequence<CompactFormats.CompactBlock>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the latest block height known to the service.
|
|
||||||
*
|
|
||||||
* @return the latest block height known to the service.
|
|
||||||
*/
|
|
||||||
fun getLatestBlockHeight(): BlockHeight
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return basic information about the server such as:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* {
|
|
||||||
* "version": "0.2.1",
|
|
||||||
* "vendor": "ECC LightWalletD",
|
|
||||||
* "taddrSupport": true,
|
|
||||||
* "chainName": "main",
|
|
||||||
* "saplingActivationHeight": 419200,
|
|
||||||
* "consensusBranchId": "2bb40e60",
|
|
||||||
* "blockHeight": 861272
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @return useful server details.
|
|
||||||
*/
|
|
||||||
fun getServerInfo(): Service.LightdInfo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* indexed and responses are fairly quick.
|
|
||||||
*
|
|
||||||
* @return a list of transactions that correspond to the given address for the given range.
|
|
||||||
*/
|
|
||||||
fun getTAddressTransactions(
|
|
||||||
tAddress: String,
|
|
||||||
blockHeightRange: ClosedRange<BlockHeight>
|
|
||||||
): List<Service.RawTransaction>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 reconnect()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup any connections when the service is shutting down and not going to be used again.
|
|
||||||
*/
|
|
||||||
fun shutdown()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit a raw transaction.
|
|
||||||
*
|
|
||||||
* @return the response from the server.
|
|
||||||
*/
|
|
||||||
fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse
|
|
||||||
}
|
|
|
@ -11,7 +11,6 @@ import cash.z.ecc.android.sdk.internal.db.pending.isCancelled
|
||||||
import cash.z.ecc.android.sdk.internal.db.pending.isFailedEncoding
|
import cash.z.ecc.android.sdk.internal.db.pending.isFailedEncoding
|
||||||
import cash.z.ecc.android.sdk.internal.db.pending.isSubmitted
|
import cash.z.ecc.android.sdk.internal.db.pending.isSubmitted
|
||||||
import cash.z.ecc.android.sdk.internal.db.pending.recipient
|
import cash.z.ecc.android.sdk.internal.db.pending.recipient
|
||||||
import cash.z.ecc.android.sdk.internal.service.LightWalletService
|
|
||||||
import cash.z.ecc.android.sdk.internal.twig
|
import cash.z.ecc.android.sdk.internal.twig
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
@ -20,6 +19,8 @@ import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.BlockingLightWalletClient
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -46,7 +47,7 @@ internal class PersistentTransactionManager(
|
||||||
db: PendingTransactionDb,
|
db: PendingTransactionDb,
|
||||||
private val zcashNetwork: ZcashNetwork,
|
private val zcashNetwork: ZcashNetwork,
|
||||||
internal val encoder: TransactionEncoder,
|
internal val encoder: TransactionEncoder,
|
||||||
private val service: LightWalletService
|
private val service: BlockingLightWalletClient
|
||||||
) : OutboundTransactionManager {
|
) : OutboundTransactionManager {
|
||||||
|
|
||||||
private val daoMutex = Mutex()
|
private val daoMutex = Mutex()
|
||||||
|
@ -214,16 +215,24 @@ internal class PersistentTransactionManager(
|
||||||
)
|
)
|
||||||
else -> {
|
else -> {
|
||||||
twig("submitting transaction with memo: ${tx.memo} amount: ${tx.value}", -1)
|
twig("submitting transaction with memo: ${tx.memo} amount: ${tx.value}", -1)
|
||||||
val response = service.submitTransaction(tx.raw)
|
when (val response = service.submitTransaction(tx.raw)) {
|
||||||
val error = response.errorCode < 0
|
is Response.Success -> {
|
||||||
twig(
|
twig("SUCCESS: submit transaction completed with response: ${response.result}")
|
||||||
"${if (error) "FAILURE! " else "SUCCESS!"} submit transaction completed with" +
|
safeUpdate("updating submitted transaction (hadError: false)", -1) {
|
||||||
" response: ${response.errorCode}: ${response.errorMessage}"
|
updateError(tx.id, null, response.result.code)
|
||||||
)
|
updateSubmitAttempts(tx.id, max(1, tx.submitAttempts + 1))
|
||||||
|
}
|
||||||
safeUpdate("updating submitted transaction (hadError: $error)", -1) {
|
}
|
||||||
updateError(tx.id, if (error) response.errorMessage else null, response.errorCode)
|
is Response.Failure -> {
|
||||||
updateSubmitAttempts(tx.id, max(1, tx.submitAttempts + 1))
|
twig(
|
||||||
|
"FAILURE! submit transaction completed with response: ${response.code}: ${response
|
||||||
|
.description}"
|
||||||
|
)
|
||||||
|
safeUpdate("updating submitted transaction (hadError: true)", -1) {
|
||||||
|
updateError(tx.id, response.description, response.code)
|
||||||
|
updateSubmitAttempts(tx.id, max(1, tx.submitAttempts + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -355,7 +364,7 @@ internal class PersistentTransactionManager(
|
||||||
appContext: Context,
|
appContext: Context,
|
||||||
zcashNetwork: ZcashNetwork,
|
zcashNetwork: ZcashNetwork,
|
||||||
encoder: TransactionEncoder,
|
encoder: TransactionEncoder,
|
||||||
service: LightWalletService,
|
service: BlockingLightWalletClient,
|
||||||
databaseFile: File
|
databaseFile: File
|
||||||
) = PersistentTransactionManager(
|
) = PersistentTransactionManager(
|
||||||
commonDatabaseBuilder(
|
commonDatabaseBuilder(
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
package cash.z.ecc.android.sdk.model
|
package cash.z.ecc.android.sdk.model
|
||||||
|
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This is a set of extension functions currently, because we expect them to change in the future.
|
* This is a set of extension functions currently, because we expect them to change in the future.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
syntax = "proto3";
|
|
||||||
package cash.z.wallet.sdk.rpc;
|
|
||||||
option go_package = "walletrpc";
|
|
||||||
option swift_prefix = "";
|
|
||||||
// Remember that proto3 fields are all optional. A field that is not present will be set to its zero value.
|
|
||||||
// bytes fields of hashes are in canonical little-endian format.
|
|
||||||
|
|
||||||
// CompactBlock is a packaging of ONLY the data from a block that's needed to:
|
|
||||||
// 1. Detect a payment to your shielded Sapling address
|
|
||||||
// 2. Detect a spend of your shielded Sapling notes
|
|
||||||
// 3. Update your witnesses to generate new Sapling spend proofs.
|
|
||||||
message CompactBlock {
|
|
||||||
uint32 protoVersion = 1; // the version of this wire format, for storage
|
|
||||||
uint64 height = 2; // the height of this block
|
|
||||||
bytes hash = 3;
|
|
||||||
bytes prevHash = 4;
|
|
||||||
uint32 time = 5;
|
|
||||||
bytes header = 6; // (hash, prevHash, and time) OR (full header)
|
|
||||||
repeated CompactTx vtx = 7; // compact transactions from this block
|
|
||||||
}
|
|
||||||
|
|
||||||
message CompactTx {
|
|
||||||
// Index and hash will allow the receiver to call out to chain
|
|
||||||
// explorers or other data structures to retrieve more information
|
|
||||||
// about this transaction.
|
|
||||||
uint64 index = 1;
|
|
||||||
bytes hash = 2;
|
|
||||||
|
|
||||||
// The transaction fee: present if server can provide. In the case of a
|
|
||||||
// stateless server and a transaction with transparent inputs, this will be
|
|
||||||
// unset because the calculation requires reference to prior transactions.
|
|
||||||
// in a pure-Sapling context, the fee will be calculable as:
|
|
||||||
// valueBalance + (sum(vPubNew) - sum(vPubOld) - sum(tOut))
|
|
||||||
uint32 fee = 3;
|
|
||||||
|
|
||||||
repeated CompactSpend spends = 4;
|
|
||||||
repeated CompactOutput outputs = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CompactSpend {
|
|
||||||
bytes nf = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CompactOutput {
|
|
||||||
bytes cmu = 1;
|
|
||||||
bytes epk = 2;
|
|
||||||
bytes ciphertext = 3;
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
package cash.z.ecc.android.sdk.model
|
package cash.z.ecc.android.sdk.model
|
||||||
|
|
||||||
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,8 @@ dependencyResolutionManagement {
|
||||||
val coroutinesOkhttpVersion = extra["COROUTINES_OKHTTP"].toString()
|
val coroutinesOkhttpVersion = extra["COROUTINES_OKHTTP"].toString()
|
||||||
val flankVersion = extra["FLANK_VERSION"].toString()
|
val flankVersion = extra["FLANK_VERSION"].toString()
|
||||||
val googleMaterialVersion = extra["GOOGLE_MATERIAL_VERSION"].toString()
|
val googleMaterialVersion = extra["GOOGLE_MATERIAL_VERSION"].toString()
|
||||||
val grpcVersion = extra["GRPC_VERSION"].toString()
|
val grpcJavaVersion = extra["GRPC_VERSION"].toString()
|
||||||
|
val grpcKotlinVersion = extra["GRPC_KOTLIN_VERSION"].toString()
|
||||||
val gsonVersion = extra["GSON_VERSION"].toString()
|
val gsonVersion = extra["GSON_VERSION"].toString()
|
||||||
val guavaVersion = extra["GUAVA_VERSION"].toString()
|
val guavaVersion = extra["GUAVA_VERSION"].toString()
|
||||||
val javaVersion = extra["ANDROID_JVM_TARGET"].toString()
|
val javaVersion = extra["ANDROID_JVM_TARGET"].toString()
|
||||||
|
@ -113,7 +114,7 @@ dependencyResolutionManagement {
|
||||||
|
|
||||||
// Standalone versions
|
// Standalone versions
|
||||||
version("flank", flankVersion)
|
version("flank", flankVersion)
|
||||||
version("grpc", grpcVersion)
|
version("grpc", grpcJavaVersion)
|
||||||
version("java", javaVersion)
|
version("java", javaVersion)
|
||||||
version("kotlin", kotlinVersion)
|
version("kotlin", kotlinVersion)
|
||||||
version("protoc", protocVersion)
|
version("protoc", protocVersion)
|
||||||
|
@ -125,8 +126,9 @@ dependencyResolutionManagement {
|
||||||
library("gradle-plugin-rust", "org.mozilla.rust-android-gradle:plugin:$rustGradlePluginVersion")
|
library("gradle-plugin-rust", "org.mozilla.rust-android-gradle:plugin:$rustGradlePluginVersion")
|
||||||
|
|
||||||
// Special cases used by the grpc gradle plugin
|
// Special cases used by the grpc gradle plugin
|
||||||
library("grpc-protoc", "io.grpc:protoc-gen-grpc-java:$grpcVersion")
|
library("protoc-compiler", "com.google.protobuf:protoc:$protocVersion")
|
||||||
library("protoc", "com.google.protobuf:protoc:$protocVersion")
|
library("protoc-gen-java", "io.grpc:protoc-gen-grpc-java:$grpcJavaVersion")
|
||||||
|
library("protoc-gen-kotlin", "io.grpc:protoc-gen-grpc-kotlin:$grpcKotlinVersion")
|
||||||
|
|
||||||
// Libraries
|
// Libraries
|
||||||
library("androidx-annotation", "androidx.annotation:annotation:$androidxAnnotationVersion")
|
library("androidx-annotation", "androidx.annotation:annotation:$androidxAnnotationVersion")
|
||||||
|
@ -149,10 +151,12 @@ dependencyResolutionManagement {
|
||||||
library("androidx-sqlite-framework", "androidx.sqlite:sqlite-framework:${androidxDatabaseVersion}")
|
library("androidx-sqlite-framework", "androidx.sqlite:sqlite-framework:${androidxDatabaseVersion}")
|
||||||
library("androidx-viewmodel-compose", "androidx.lifecycle:lifecycle-viewmodel-compose:$androidxLifecycleVersion")
|
library("androidx-viewmodel-compose", "androidx.lifecycle:lifecycle-viewmodel-compose:$androidxLifecycleVersion")
|
||||||
library("bip39", "cash.z.ecc.android:kotlin-bip39:$bip39Version")
|
library("bip39", "cash.z.ecc.android:kotlin-bip39:$bip39Version")
|
||||||
library("grpc-android", "io.grpc:grpc-android:$grpcVersion")
|
library("grpc-android", "io.grpc:grpc-android:$grpcJavaVersion")
|
||||||
library("grpc-okhttp", "io.grpc:grpc-okhttp:$grpcVersion")
|
library("grpc-kotlin", "com.google.protobuf:protobuf-kotlin-lite:$protocVersion")
|
||||||
library("grpc-protobuf", "io.grpc:grpc-protobuf-lite:$grpcVersion")
|
library("grpc-kotlin-stub", "io.grpc:grpc-kotlin-stub:$grpcKotlinVersion")
|
||||||
library("grpc-stub", "io.grpc:grpc-stub:$grpcVersion")
|
library("grpc-okhttp", "io.grpc:grpc-okhttp:$grpcJavaVersion")
|
||||||
|
library("grpc-protobuf", "io.grpc:grpc-protobuf-lite:$grpcJavaVersion")
|
||||||
|
library("grpc-stub", "io.grpc:grpc-stub:$grpcJavaVersion")
|
||||||
library("gson", "com.google.code.gson:gson:$gsonVersion")
|
library("gson", "com.google.code.gson:gson:$gsonVersion")
|
||||||
library("guava", "com.google.guava:guava:$guavaVersion")
|
library("guava", "com.google.guava:guava:$guavaVersion")
|
||||||
library("javax-annotation", "javax.annotation:javax.annotation-api:$javaxAnnotationVersion")
|
library("javax-annotation", "javax.annotation:javax.annotation-api:$javaxAnnotationVersion")
|
||||||
|
@ -189,7 +193,7 @@ dependencyResolutionManagement {
|
||||||
library("androidx-tracing", "androidx.tracing:tracing:$androidxTracingVersion")
|
library("androidx-tracing", "androidx.tracing:tracing:$androidxTracingVersion")
|
||||||
library("androidx-uiAutomator", "androidx.test.uiautomator:uiautomator:$androidxUiAutomatorVersion")
|
library("androidx-uiAutomator", "androidx.test.uiautomator:uiautomator:$androidxUiAutomatorVersion")
|
||||||
library("coroutines-okhttp", "ru.gildor.coroutines:kotlin-coroutines-okhttp:$coroutinesOkhttpVersion")
|
library("coroutines-okhttp", "ru.gildor.coroutines:kotlin-coroutines-okhttp:$coroutinesOkhttpVersion")
|
||||||
library("grpc-testing", "io.grpc:grpc-testing:$grpcVersion")
|
library("grpc-testing", "io.grpc:grpc-testing:$grpcJavaVersion")
|
||||||
library("junit-api", "org.junit.jupiter:junit-jupiter-api:$junitVersion")
|
library("junit-api", "org.junit.jupiter:junit-jupiter-api:$junitVersion")
|
||||||
library("junit-engine", "org.junit.jupiter:junit-jupiter-engine:$junitVersion")
|
library("junit-engine", "org.junit.jupiter:junit-jupiter-engine:$junitVersion")
|
||||||
library("junit-migration", "org.junit.jupiter:junit-jupiter-migrationsupport:$junitVersion")
|
library("junit-migration", "org.junit.jupiter:junit-jupiter-migrationsupport:$junitVersion")
|
||||||
|
@ -203,8 +207,10 @@ dependencyResolutionManagement {
|
||||||
bundle(
|
bundle(
|
||||||
"grpc",
|
"grpc",
|
||||||
listOf(
|
listOf(
|
||||||
"grpc-okhttp",
|
|
||||||
"grpc-android",
|
"grpc-android",
|
||||||
|
"grpc-kotlin",
|
||||||
|
"grpc-kotlin-stub",
|
||||||
|
"grpc-okhttp",
|
||||||
"grpc-protobuf",
|
"grpc-protobuf",
|
||||||
"grpc-stub"
|
"grpc-stub"
|
||||||
)
|
)
|
||||||
|
@ -260,6 +266,7 @@ rootProject.name = "zcash-android-sdk"
|
||||||
includeBuild("build-conventions")
|
includeBuild("build-conventions")
|
||||||
|
|
||||||
include("darkside-test-lib")
|
include("darkside-test-lib")
|
||||||
include("sdk-lib")
|
|
||||||
include("demo-app")
|
include("demo-app")
|
||||||
include("demo-app-benchmark-test")
|
include("demo-app-benchmark-test")
|
||||||
|
include("lightwallet-client-lib")
|
||||||
|
include("sdk-lib")
|
Loading…
Reference in New Issue