Initial commit

This commit is contained in:
fekt
2022-06-30 01:53:12 -04:00
commit 0131016214
703 changed files with 33394 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cash.z.ecc.android.sdk">
<!-- For code coverage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:name="androidx.multidex.MultiDexApplication" />
</manifest>

View File

@@ -0,0 +1,7 @@
{
"network": "main",
"height": 1290000,
"hash": "00000000014836c3cbc011276cbd3702a76a1fea7eb2c0c2c257321220376450",
"time": 1624075741,
"saplingTree": "01accf4fc3dc4233bbe757f94e0d4cd23b4aa2e6ac472601f4f53ca4dc86a8a05901fae977171a6103a0338990e073ffe50e29fc8bf0400dcd3378ebfe7a146ed1481300014f7b33dd5159ac66f2670b7db8925065e7154e0199ff7ee7559b276ba56ad1200173e9881f21357e54027a4275114f0f6ad4ca17143554182f63c77f3288a23a20011d65465ab942440e200d429ef892452b4b05c5b21e9a6e6d968a719c67b5e85b000000000000000150926c74975e2d8ff095defb75a4a6d9f17007e87a74230a65a3265d8f45032900012ffde6dccbef68b60cd7b4e7a8fe7989f5954fa4bacad01b247d16b9bfa5084000000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
}

View File

@@ -0,0 +1,7 @@
{
"network": "main",
"height": 1290000,
"hash": "00000000014836c3cbc011276cbd3702a76a1fea7eb2c0c2c257321220376450",
"time": 1624075741,
"saplingTree": "01accf4fc3dc4233bbe757f94e0d4cd23b4aa2e6ac472601f4f53ca4dc86a8a05901fae977171a6103a0338990e073ffe50e29fc8bf0400dcd3378ebfe7a146ed1481300014f7b33dd5159ac66f2670b7db8925065e7154e0199ff7ee7559b276ba56ad1200173e9881f21357e54027a4275114f0f6ad4ca17143554182f63c77f3288a23a20011d65465ab942440e200d429ef892452b4b05c5b21e9a6e6d968a719c67b5e85b000000000000000150926c74975e2d8ff095defb75a4a6d9f17007e87a74230a65a3265d8f45032900012ffde6dccbef68b60cd7b4e7a8fe7989f5954fa4bacad01b247d16b9bfa5084000000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
}

View File

@@ -0,0 +1,7 @@
{
"network": "main",
"height": 1300000,
"hash": "00000000027222bdbcf9c5f807f851f97312ac6e0dbbc2b93f2be21a69c59d44",
"time": 1624830312,
"saplingTree": "01f5a97e2679a2bb9103caf37b825f92fcd73fff836234844dfcf1815394522b2c01526587b9b9e8aeb0eb572d81fec1f5127b8278ba0f57e451bd6b796596940a2213000131c7ff90fafff6159b8fb6544a2bcbba6c102903158fce8f9a9d3c6654abb23300013555cb7f4f79badeaca9bf2dca5a8704f0929053d50e95c03002f9a4d5286c3a01ad3557e11c1607ec888dc84f5f8899c3c79fb1f50b613946452ec7dd5e53763c0001c4583f4482b949390dba355fc8fa63019c83acd644ddd633cb50211d236f870600000001088da0d78eefd0c222507927e403b972d0890d0c31e08b02268fbe39ac4a6e170001edf82d4e2b4893ea2028ca8c5149e50a4c358b856d73f2de2b9a22034fa78f22012ffde6dccbef68b60cd7b4e7a8fe7989f5954fa4bacad01b247d16b9bfa5084000000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
}

View File

@@ -0,0 +1,103 @@
package cash.z.ecc.android.sdk
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class AssetTest {
@Test
@SmallTest
fun validate_mainnet_assets() {
val network = ZcashNetwork.Mainnet
val assets = listAssets(network)
assertFilesExist(assets)
assertFilenames(assets)
assertFileContents(network, assets)
}
@Test
@SmallTest
fun validate_testnet_assets() {
val network = ZcashNetwork.Testnet
val assets = listAssets(network)
assertFilesExist(assets)
assertFilenames(assets)
assertFileContents(network, assets)
}
private fun assertFilesExist(files: Array<String>?) {
assertFalse(files.isNullOrEmpty())
}
private fun assertFilenames(files: Array<String>?) {
files?.forEach {
val split = it.split('.')
assertEquals(2, split.size)
val intString = split.first()
val extensionString = split.last()
// Will throw exception if cannot be parsed
intString.toInt()
assertEquals("json", extensionString)
}
}
private fun assertFileContents(network: ZcashNetwork, files: Array<String>?) {
files?.map { filename ->
val filePath = "${WalletBirthdayTool.birthdayDirectory(network)}/$filename"
ApplicationProvider.getApplicationContext<Context>().assets.open(filePath)
.use { inputSteam ->
inputSteam.bufferedReader().use { bufferedReader ->
val slurped = bufferedReader.readText()
JsonFile(JSONObject(slurped), filename)
}
}
}?.forEach {
val jsonObject = it.jsonObject
assertTrue(jsonObject.has("network"))
assertTrue(jsonObject.has("height"))
assertTrue(jsonObject.has("hash"))
assertTrue(jsonObject.has("time"))
assertTrue(jsonObject.has("saplingTree"))
val expectedNetworkName = when (network) {
ZcashNetwork.Mainnet -> "main"
ZcashNetwork.Testnet -> "test"
}
assertEquals("File: ${it.filename}", expectedNetworkName, jsonObject.getString("network"))
assertEquals(
"File: ${it.filename}",
WalletBirthdayTool.birthdayHeight(it.filename),
jsonObject.getInt("height")
)
// In the future, additional validation of the JSON can be added
}
}
private data class JsonFile(val jsonObject: JSONObject, val filename: String)
companion object {
fun listAssets(network: ZcashNetwork) = runBlocking {
WalletBirthdayTool.listBirthdayDirectoryContents(
ApplicationProvider.getApplicationContext<Context>(),
WalletBirthdayTool.birthdayDirectory(network)
)
}
}
}

View File

@@ -0,0 +1,98 @@
package cash.z.ecc.android.sdk
class InitializerTest {
// lateinit var initializer: Initializer
//
// @After
// fun cleanUp() {
// // don't leave databases sitting around after this test is run
// if (::initializer.isInitialized) initializer.erase()
// }
//
// @Test
// fun testInit() {
// val height = 980000
//
// initializer = Initializer(context) { config ->
// config.importedWalletBirthday(height)
// config.setViewingKeys(
// "zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v",
// "zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7"
// )
// config.alias = "VkInitTest1"
// }
// assertEquals(height, initializer.birthday.height)
// initializer.erase()
// }
//
// @Test
// fun testErase() {
// val alias = "VkInitTest2"
// initializer = Initializer(context) { config ->
// config.importedWalletBirthday(1_419_900)
// config.setViewingKeys(
// "zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v",
// "zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7"
// )
// config.alias = alias
// }
//
// assertTrue("Failed to erase initializer", Initializer.erase(context, alias))
// assertFalse("Expected false when erasing nothing.", Initializer.erase(context))
// }
//
// @Test(expected = InitializerException.MissingDefaultBirthdayException::class)
// fun testMissingBirthday() {
// val config = Initializer.Config { config ->
// config.setViewingKeys("vk1")
// }
// config.validate()
// }
//
// @Test(expected = InitializerException.InvalidBirthdayHeightException::class)
// fun testOutOfBoundsBirthday() {
// val config = Initializer.Config { config ->
// config.setViewingKeys("vk1")
// config.setBirthdayHeight(ZcashSdk.SAPLING_ACTIVATION_HEIGHT - 1)
// }
// config.validate()
// }
//
// @Test
// fun testImportedWalletUsesSaplingActivation() {
// initializer = Initializer(context) { config ->
// config.setViewingKeys("vk1")
// config.importWallet(ByteArray(32))
// }
// assertEquals("Incorrect height used for import.", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)
// }
//
// @Test
// fun testDefaultToOldestHeight_true() {
// initializer = Initializer(context) { config ->
// config.setViewingKeys("vk1")
// config.setBirthdayHeight(null, true)
// }
// assertEquals("Height should equal sapling activation height when defaultToOldestHeight is true", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)
// }
//
// @Test
// fun testDefaultToOldestHeight_false() {
// val initialHeight = 750_000
// initializer = Initializer(context) { config ->
// config.setViewingKeys("vk1")
// config.setBirthdayHeight(initialHeight, false)
// }
// val h = initializer.birthday.height
// assertNotEquals("Height should not equal sapling activation height when defaultToOldestHeight is false", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, h)
// assertTrue("expected $h to be higher", h >= initialHeight)
// }
//
// companion object {
// private val context = InstrumentationRegistry.getInstrumentation().context
// init {
// Twig.plant(TroubleshootingTwig())
// }
// }
}

View File

@@ -0,0 +1,33 @@
package cash.z.ecc.android.sdk
import cash.z.ecc.android.sdk.integration.SanityTest
import cash.z.ecc.android.sdk.integration.SmokeTest
import cash.z.ecc.android.sdk.integration.service.ChangeServiceTest
import cash.z.ecc.android.sdk.internal.transaction.PersistentTransactionManagerTest
import cash.z.ecc.android.sdk.jni.BranchIdTest
import cash.z.ecc.android.sdk.jni.TransparentTest
import org.junit.runner.RunWith
import org.junit.runners.Suite
/**
* Suite of tests to run before submitting a pull request.
*
* For now, these are just the tests that are known to be recently updated and that pass. In the
* near future this suite will contain only fast running tests that can be used to quickly validate
* that a PR hasn't broken anything major.
*/
@RunWith(Suite::class)
@Suite.SuiteClasses(
// Fast tests that only run locally and don't require darksidewalletd or lightwalletd
BranchIdTest::class,
TransparentTest::class,
PersistentTransactionManagerTest::class,
// potentially exclude because these are long-running (and hit external srvcs)
SanityTest::class,
// potentially exclude because these hit external services
ChangeServiceTest::class,
SmokeTest::class
)
class PullRequestSuite

View File

@@ -0,0 +1,29 @@
package cash.z.ecc.android.sdk.annotation
enum class TestPurpose {
/**
* These tests are explicitly designed to preserve behavior that we do not want to lose after
* major upgrades or refactors. It is acceptable for these test to run long and require
* additional infrastructure.
*/
REGRESSION,
/**
* These tests are designed to be run against new pull requests and generally before any changes
* are committed. It is not ideal for these tests to run long.
*/
COMMIT,
/**
* These tests require a running instance of [darksidewalletd](https://github.com/zcash/lightwalletd/blob/master/docs/darksidewalletd.md).
*/
DARKSIDE,
}
/**
* Signals that this test is explicitly intended to be maintained and run regularly in order to
* achieve the given purpose. Eventually, we will run all such tests nightly.
*/
@Target(AnnotationTarget.CLASS)
annotation class MaintainedTest(vararg val purpose: TestPurpose)

View File

@@ -0,0 +1,53 @@
package cash.z.ecc.android.sdk.ext
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.android.sdk.util.SimpleMnemonics
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import ru.gildor.coroutines.okhttp.await
fun Initializer.Config.seedPhrase(seedPhrase: String, network: ZcashNetwork) {
runBlocking { setSeed(SimpleMnemonics().toSeed(seedPhrase.toCharArray()), network) }
}
object BlockExplorer {
suspend fun fetchLatestHeight(): Int {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://api.blockchair.com/zcash/blocks?limit=1")
.build()
val result = client.newCall(request).await()
val body = result.body?.string()
return JSONObject(body).getJSONArray("data").getJSONObject(0).getInt("id")
}
}
object Transactions {
val outbound = arrayOf(
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/c9e35e6ff444b071d63bf9bab6480409d6361760445c8a28d24179adb35c2495.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/72a29d7db511025da969418880b749f7fc0fc910cdb06f52193b5fa5c0401d9d.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/ff6ea36765dc29793775c7aa71de19fca039c5b5b873a0497866e9c4bc48af01.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/34e507cab780546f980176f3ff2695cd404917508c7e5ee18cc1d2ff3858cb08.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/6edf869063eccff3345676b0fed9f1aa6988fb2524e3d9ca7420a13cfadcd76c.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/de97394ae220c28a33ba78b944e82dabec8cb404a4407650b134b3d5950358c0.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/4eaa902279f8380914baf5bcc470d8b7c11d84fda809f67f517a7cb48912b87b.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/73c5edf8ffba774d99155121ccf07e67fbcf14284458f7e732751fea60d3bcbc.txt"
)
val inbound = arrayOf(
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/8f064d23c66dc36e32445e5f3b50e0f32ac3ddb78cff21fb521eb6c19c07c99a.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/d2e7be14bbb308f9d4d68de424d622cbf774226d01cd63cc6f155fafd5cd212c.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e6566be3a4f9a80035dab8e1d97e40832a639e3ea938fb7972ea2f8482ff51ce.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e9527891b5d43d1ac72f2c0a3ac18a33dc5a0529aec04fa600616ed35f8123f8.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/4dcc95dd0a2f1f51bd64bb9f729b423c6de1690664a1b6614c75925e781662f7.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/75f2cdd2ff6a94535326abb5d9e663d53cbfa5f31ebb24b4d7e420e9440d41a2.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/7690c8ec740c1be3c50e2aedae8bf907ac81141ae8b6a134c1811706c73f49a6.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf.txt"
)
}

View File

@@ -0,0 +1,133 @@
package cash.z.ecc.android.sdk.integration
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.ext.BlockExplorer
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
/**
* This test is intended to run to make sure that basic things are functional and pinpoint what is
* not working. It was originally developed after a major refactor to find what broke.
*/
@MaintainedTest(TestPurpose.COMMIT)
@RunWith(Parameterized::class)
class SanityTest(
private val wallet: TestWallet,
private val extfvk: String,
private val extpub: String,
private val birthday: Int
) {
val networkName = wallet.networkName
val name = "$networkName wallet"
@Test
fun testNotPlaintext() {
val message =
"is using plaintext. This will cause problems for the test. Ensure that the `lightwalletd_allow_very_insecure_connections` resource value is false"
assertFalse("$name $message", wallet.service.connectionInfo.usePlaintext)
}
@Test
fun testFilePaths() {
assertEquals(
"$name has invalid DataDB file",
"/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_${networkName}_Data.db",
wallet.initializer.rustBackend.pathDataDb
)
assertEquals(
"$name has invalid CacheDB file",
"/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_${networkName}_Cache.db",
wallet.initializer.rustBackend.pathCacheDb
)
assertEquals(
"$name has invalid CacheDB params dir",
"/data/user/0/cash.z.ecc.android.sdk.test/cache/params",
wallet.initializer.rustBackend.pathParamsDir
)
}
@Test
fun testBirthday() {
assertEquals(
"$name has invalid birthday height",
birthday,
wallet.initializer.birthday.height
)
}
@Test
fun testViewingKeys() {
assertEquals(
"$name has invalid extfvk",
extfvk,
wallet.initializer.viewingKeys[0].extfvk
)
assertEquals(
"$name has invalid extpub",
extpub,
wallet.initializer.viewingKeys[0].extpub
)
}
@Test
fun testServerConnection() {
assertEquals(
"$name has an invalid server connection",
"$networkName.lightwalletd.com:9067?usePlaintext=false",
wallet.connectionInfo
)
}
@Test
fun testLatestHeight() = runBlocking {
if (wallet.networkName == "mainnet") {
val expectedHeight = BlockExplorer.fetchLatestHeight()
// fetch height directly because the synchronizer hasn't started, yet
val downloaderHeight = wallet.service.getLatestBlockHeight()
val info = wallet.connectionInfo
assertTrue(
"$info\n ${wallet.networkName} Lightwalletd is too far behind. Downloader height $downloaderHeight is more than 10 blocks behind block explorer height $expectedHeight",
expectedHeight - 10 < downloaderHeight
)
}
}
@Test
fun testSingleBlockDownload() = runBlocking {
// fetch block directly because the synchronizer hasn't started, yet
val height = 1_000_000
val block = wallet.service.getBlockRange(height..height)[0]
assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height.toInt() == height)
}
companion object {
@JvmStatic
@Parameterized.Parameters
fun wallets() = listOf(
// Testnet wallet
arrayOf(
TestWallet(TestWallet.Backups.SAMPLE_WALLET),
"zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063",
"0234965f30c8611253d035f44e68d4e2ce82150e8665c95f41ccbaf916b16c69d8",
1320000
),
// Mainnet wallet
arrayOf(
TestWallet(TestWallet.Backups.SAMPLE_WALLET, ZcashNetwork.Mainnet),
"zxviews1q0hxkupsqqqqpqzsffgrk2smjuccedua7zswf5e3rgtv3ga9nhvhjug670egshd6me53r5n083s2m9mf4va4z7t39ltd3wr7hawnjcw09eu85q0ammsg0tsgx24p4ma0uvr4p8ltx5laum2slh2whc23ctwlnxme9w4dw92kalwk5u4wyem8dynknvvqvs68ktvm8qh7nx9zg22xfc77acv8hk3qqll9k3x4v2fa26puu2939ea7hy4hh60ywma69xtqhcy4037ne8g2sg8sq",
"031c6355641237643317e2d338f5e8734c57e8aa8ce960ee22283cf2d76bef73be",
1000000
)
)
}
}

View File

@@ -0,0 +1,62 @@
package cash.z.ecc.android.sdk.integration
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
/**
* This test is intended to run to make sure that basic things are functional and pinpoint what is
* not working. It was originally developed after a major refactor to find what broke.
*/
@MaintainedTest(TestPurpose.COMMIT)
@MediumTest
class SmokeTest {
@Test
fun testNotPlaintext() {
val service =
wallet.synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService
Assert.assertFalse(
"Wallet is using plaintext. This will cause problems for the test. Ensure that the `lightwalletd_allow_very_insecure_connections` resource value is false",
service.connectionInfo.usePlaintext
)
}
@Test
fun testFilePaths() {
Assert.assertEquals("Invalid DataDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Data.db", wallet.initializer.rustBackend.pathDataDb)
Assert.assertEquals("Invalid CacheDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Cache.db", wallet.initializer.rustBackend.pathCacheDb)
Assert.assertEquals("Invalid CacheDB params dir", "/data/user/0/cash.z.ecc.android.sdk.test/cache/params", wallet.initializer.rustBackend.pathParamsDir)
}
@Test
fun testBirthday() {
Assert.assertEquals("Invalid birthday height", 1_320_000, wallet.initializer.birthday.height)
}
@Test
fun testViewingKeys() {
Assert.assertEquals("Invalid extfvk", "zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063", wallet.initializer.viewingKeys[0].extfvk)
Assert.assertEquals("Invalid extpub", "0234965f30c8611253d035f44e68d4e2ce82150e8665c95f41ccbaf916b16c69d8", wallet.initializer.viewingKeys[0].extpub)
}
// This test takes an extremely long time
// Does its runtime grow over time based on growth of the blockchain?
@Test
@LargeTest
@Ignore("This test is extremely slow and times out before the timeout given")
fun testSync() = runBlocking<Unit> {
wallet.sync(300_000L)
}
companion object {
val wallet = TestWallet(TestWallet.Backups.SAMPLE_WALLET)
}
}

View File

@@ -0,0 +1,144 @@
package cash.z.wallet.sdk.integration
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.onFirst
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
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.model.Zatoshi
import cash.z.ecc.android.sdk.test.ScopedTest
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import java.util.concurrent.CountDownLatch
class TestnetIntegrationTest : ScopedTest() {
var stopWatch = CountDownLatch(1)
val saplingActivation = synchronizer.network.saplingActivationHeight
@Test
@Ignore("This test is broken")
fun testLatestBlockTest() {
val service = LightWalletGrpcService(
context,
host
)
val height = service.getLatestBlockHeight()
assertTrue(height > saplingActivation)
}
@Test
fun testLoadBirthday() {
val (height, hash, time, tree) = runBlocking {
WalletBirthdayTool.loadNearest(
context,
synchronizer.network,
saplingActivation + 1
)
}
assertEquals(saplingActivation, height)
}
@Test
@Ignore("This test is broken")
fun getAddress() = runBlocking {
assertEquals(address, synchronizer.getAddress())
}
// This is an extremely slow test; it is disabled so that we can get CI set up
@Test
@LargeTest
@Ignore("This test is extremely slow")
fun testBalance() = runBlocking {
var availableBalance: Zatoshi? = null
synchronizer.saplingBalances.onFirst {
availableBalance = it?.available
}
synchronizer.status.filter { it == SYNCED }.onFirst {
delay(100)
}
assertTrue(
"No funds available when we expected a balance greater than zero!",
availableBalance!!.value > 0
)
}
@Test
@Ignore("This test is broken")
fun testSpend() = runBlocking {
var success = false
synchronizer.saplingBalances.filterNotNull().onEach {
success = sendFunds()
}.first()
log("asserting $success")
assertTrue(success)
}
private suspend fun sendFunds(): Boolean {
val spendingKey = DerivationTool.deriveSpendingKeys(seed, synchronizer.network)[0]
log("sending to address")
synchronizer.sendToAddress(
spendingKey,
ZcashSdk.MINERS_FEE,
toAddress,
"first mainnet tx from the SDK"
).filter { it?.isSubmitSuccess() == true }.onFirst {
log("DONE SENDING!!!")
}
log("returning true from sendFunds")
return true
}
fun log(message: String) {
twig("\n---\n[TESTLOG]: $message\n---\n")
}
companion object {
init { Twig.plant(TroubleshootingTwig()) }
const val host = "lightwalletd.testnet.z.cash"
private const val birthdayHeight = 963150
private const val targetHeight = 663250
private const val seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
val seed = "cash.z.ecc.android.sdk.integration.IntegrationTest.seed.value.64bytes".toByteArray()
val address = "zs1m30y59wxut4zk9w24d6ujrdnfnl42hpy0ugvhgyhr8s0guszutqhdj05c7j472dndjstulph74m"
val toAddress = "zs1vp7kvlqr4n9gpehztr76lcn6skkss9p8keqs3nv8avkdtjrcctrvmk9a7u494kluv756jeee5k0"
private val context = InstrumentationRegistry.getInstrumentation().context
private val initializer = runBlocking {
Initializer.new(context) { config ->
config.setNetwork(ZcashNetwork.Testnet, host)
runBlocking { config.importWallet(seed, birthdayHeight, ZcashNetwork.Testnet) }
}
}
private lateinit var synchronizer: Synchronizer
@JvmStatic
@BeforeClass
fun startUp() {
synchronizer = Synchronizer.newBlocking(initializer)
synchronizer.start(classScope)
}
}
}

View File

@@ -0,0 +1,138 @@
package cash.z.ecc.android.sdk.integration.service
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.annotation.MaintainedTest
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.CompactBlockStore
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.test.ScopedTest
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.Spy
@MaintainedTest(TestPurpose.REGRESSION)
@RunWith(AndroidJUnit4::class)
@SmallTest
class ChangeServiceTest : ScopedTest() {
val network = ZcashNetwork.Mainnet
@Mock
lateinit var mockBlockStore: CompactBlockStore
var mockCloseable: AutoCloseable? = null
@Spy
val service = LightWalletGrpcService(context, network)
lateinit var downloader: CompactBlockDownloader
lateinit var otherService: LightWalletService
@Before
fun setup() {
initMocks()
downloader = CompactBlockDownloader(service, mockBlockStore)
otherService = LightWalletGrpcService(context, "lightwalletd.electriccoin.co")
}
@After
fun tearDown() {
mockCloseable?.close()
}
private fun initMocks() {
mockCloseable = MockitoAnnotations.openMocks(this)
}
@Test
fun testSanityCheck() {
val result = service.getLatestBlockHeight()
assertTrue(result > network.saplingActivationHeight)
}
@Test
fun testCleanSwitch() = runBlocking {
downloader.changeService(otherService)
val result = downloader.downloadBlockRange(900_000..901_000)
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 = 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(context, "invalid.lightwalletd")) {
caughtException = it
}
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(context, ZcashNetwork.Testnet)) {
caughtException = it
}
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)
)
}
}
}
}

View File

@@ -0,0 +1,81 @@
package cash.z.ecc.android.sdk.internal
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.ext.ZcashSdk
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
@RunWith(AndroidJUnit4::class)
class SaplingParamToolTest {
val context: Context = InstrumentationRegistry.getInstrumentation().context
val cacheDir = "${context.cacheDir.absolutePath}/params"
@Before
fun setup() {
// clear the param files
runBlocking { SaplingParamTool.clear(cacheDir) }
}
@Test
@Ignore("This test is broken")
fun testFilesExists() = runBlocking {
// Given
SaplingParamTool.fetchParams(cacheDir)
// When
val result = SaplingParamTool.validate(cacheDir)
// Then
Assert.assertFalse(result)
}
@Test
fun testOnlySpendFileExits() = runBlocking {
// Given
SaplingParamTool.fetchParams(cacheDir)
File("$cacheDir/${ZcashSdk.OUTPUT_PARAM_FILE_NAME}").delete()
// When
val result = SaplingParamTool.validate(cacheDir)
// Then
Assert.assertFalse("Validation should fail when the spend params are missing", result)
}
@Test
fun testOnlyOutputOFileExits() = runBlocking {
// Given
SaplingParamTool.fetchParams(cacheDir)
File("$cacheDir/${ZcashSdk.SPEND_PARAM_FILE_NAME}").delete()
// When
val result = SaplingParamTool.validate(cacheDir)
// Then
Assert.assertFalse("Validation should fail when the spend params are missing", result)
}
@Test
fun testInsufficientDeviceStorage() = runBlocking {
// Given
SaplingParamTool.fetchParams(cacheDir)
Assert.assertFalse("insufficient storage", false)
}
@Test
fun testSufficientDeviceStorageForOnlyOneFile() = runBlocking {
SaplingParamTool.fetchParams(cacheDir)
Assert.assertFalse("insufficient storage", false)
}
}

View File

@@ -0,0 +1,30 @@
package cash.z.ecc.android.sdk.internal
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.fixture.WalletBirthdayFixture
import cash.z.ecc.fixture.toJson
import org.junit.Assert.assertEquals
import org.junit.Test
class WalletBirthdayTest {
@Test
@SmallTest
fun deserialize() {
val fixtureBirthday = WalletBirthdayFixture.new()
val deserialized = WalletBirthday.from(fixtureBirthday.toJson())
assertEquals(fixtureBirthday, deserialized)
}
@Test
@SmallTest
fun epoch_seconds_as_long_that_would_overflow_int() {
val jsonString = WalletBirthdayFixture.new(time = Long.MAX_VALUE).toJson()
WalletBirthday.from(jsonString).also {
assertEquals(Long.MAX_VALUE, it.time)
}
}
}

View File

@@ -0,0 +1,122 @@
package cash.z.ecc.android.sdk.internal.transaction
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.isCancelled
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.test.ScopedTest
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.stub
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@MaintainedTest(TestPurpose.REGRESSION)
@RunWith(AndroidJUnit4::class)
@SmallTest
class PersistentTransactionManagerTest : ScopedTest() {
@Mock lateinit var mockEncoder: TransactionEncoder
@Mock lateinit var mockService: LightWalletService
val pendingDbName = "PersistentTxMgrTest_Pending.db"
val dataDbName = "PersistentTxMgrTest_Data.db"
private lateinit var manager: OutboundTransactionManager
@Before
fun setup() {
initMocks()
deleteDb()
manager = PersistentTransactionManager(context, mockEncoder, mockService, pendingDbName)
}
private fun deleteDb() {
context.getDatabasePath(pendingDbName).delete()
}
private fun initMocks() {
MockitoAnnotations.initMocks(this)
mockEncoder.stub {
onBlocking {
createTransaction(any(), any(), any(), any(), any())
}.thenAnswer { invocation ->
runBlocking {
delay(200)
EncodedTransaction(byteArrayOf(1, 2, 3), byteArrayOf(8, 9), 5_000_000)
}
}
}
}
@Test
fun testCancellation_RaceCondition() = runBlocking {
val tx = manager.initSpend(Zatoshi(1234), "taddr", "memo-good", 0)
val txFlow = manager.monitorById(tx.id)
// encode TX
testScope.launch {
twig("ENCODE: start"); manager.encode("fookey", tx); twig("ENCODE: end")
}
// then cancel it before it is done encoding
testScope.launch {
delay(100)
twig("CANCEL: start"); manager.cancel(tx.id); twig("CANCEL: end")
}
txFlow.drop(2).onEach {
twig("found tx: $it")
assertTrue("Expected the encoded tx to be cancelled but it wasn't", it.isCancelled())
twig("found it to be successfully cancelled")
testScope.cancel()
}.launchIn(testScope).join()
}
@Test
fun testCancel() = runBlocking {
var tx = manager.initSpend(Zatoshi(1234), "a", "b", 0)
assertFalse(tx.isCancelled())
manager.cancel(tx.id)
tx = manager.findById(tx.id)!!
assertTrue("Transaction was not cancelled", tx.isCancelled())
}
@Test
fun testAbort() = runBlocking {
var tx: PendingTransaction? = manager.initSpend(Zatoshi(1234), "a", "b", 0)
assertNotNull(tx)
manager.abort(tx!!)
tx = manager.findById(tx.id)
assertNull("Transaction was not removed from the DB", tx)
}
companion object {
@BeforeClass
fun init() {
Twig.plant(TroubleshootingTwig())
}
}
}

View File

@@ -0,0 +1,64 @@
package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
/**
* This test is intended to run to make sure that branch ID logic works across all target devices.
*/
@MaintainedTest(TestPurpose.REGRESSION)
@RunWith(Parameterized::class)
class BranchIdTest(
private val networkName: String,
private val height: Int,
private val branchId: Long,
private val branchHex: String,
private val rustBackend: RustBackendWelding
) {
@Test
fun testBranchId_Hex() {
val branchId = rustBackend.getBranchIdForHeight(height)
val clientBranch = "%x".format(branchId)
assertEquals("Invalid branch Id Hex value for $networkName at height $height on ${rustBackend.network.networkName}", branchHex, clientBranch)
}
@Test
fun testBranchId_Numeric() {
val actual = rustBackend.getBranchIdForHeight(height)
assertEquals("Invalid branch ID for $networkName at height $height on ${rustBackend.network.networkName}", branchId, actual)
}
companion object {
@JvmStatic
@Parameterized.Parameters
fun wallets(): List<Array<Any>> {
// init values don't matter for this test because we're just checking branchIds, which
// is an abnormal use of the SDK because this really should run at the rust level
// However, due to quirks on certain devices, we created this test at the Android level,
// as a sanity check
val testnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Testnet) }
val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet) }
return listOf(
// Mainnet Cases
arrayOf("Sapling", 419_200, 1991772603L, "76b809bb", mainnetBackend),
arrayOf("Blossom", 653_600, 733220448L, "2bb40e60", mainnetBackend),
arrayOf("Heartwood", 903_000, 4122551051L, "f5b9230b", mainnetBackend),
arrayOf("Canopy", 1_046_400, 3925833126L, "e9ff75a6", mainnetBackend),
// Testnet Cases
arrayOf("Sapling", 280_000, 1991772603L, "76b809bb", testnetBackend),
arrayOf("Blossom", 584_000, 733220448L, "2bb40e60", testnetBackend),
arrayOf("Heartwood", 903_800, 4122551051L, "f5b9230b", testnetBackend),
arrayOf("Canopy", 1_028_500, 3925833126L, "e9ff75a6", testnetBackend)
)
}
}
}

View File

@@ -0,0 +1,88 @@
package cash.z.ecc.android.sdk.jni
import androidx.test.filters.SmallTest
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@MaintainedTest(TestPurpose.REGRESSION)
@RunWith(Parameterized::class)
@SmallTest
class TransparentTest(val expected: Expected, val network: ZcashNetwork) {
@Test
fun deriveTransparentSecretKeyTest() = runBlocking {
assertEquals(expected.tskCompressed, DerivationTool.deriveTransparentSecretKey(SEED, network = network))
}
@Test
fun deriveTransparentAddressTest() = runBlocking {
assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddress(SEED, network = network))
}
@Test
fun deriveTransparentAddressFromSecretKeyTest() = runBlocking {
val pk = DerivationTool.deriveTransparentSecretKey(SEED, network = network)
assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddressFromPrivateKey(pk, network = network))
}
@Test
fun deriveUnifiedViewingKeysFromSeedTest() = runBlocking {
val uvks = DerivationTool.deriveUnifiedViewingKeys(SEED, network = network)
assertEquals(1, uvks.size)
val uvk = uvks.first()
assertEquals(expected.zAddr, DerivationTool.deriveShieldedAddress(uvk.extfvk, network = network))
assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddressFromPublicKey(uvk.extpub, network = network))
}
companion object {
const val PHRASE = "deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"
val MNEMONIC = MnemonicCode(PHRASE)
val SEED = MNEMONIC.toSeed()
object ExpectedMainnet : Expected {
override val tAddr = "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4"
override val zAddr = "zs1yc4sgtfwwzz6xfsy2xsradzr6m4aypgxhfw2vcn3hatrh5ryqsr08sgpemlg39vdh9kfupx20py"
override val tskCompressed = "L4BvDC33yLjMRxipZvdiUmdYeRfZmR8viziwsVwe72zJdGbiJPv2"
override val tpk = "03b1d7fb28d17c125b504d06b1530097e0a3c76ada184237e3bc0925041230a5af"
}
object ExpectedTestnet : Expected {
override val tAddr = "tm9v3KTsjXK8XWSqiwFjic6Vda6eHY9Mjjq"
override val zAddr = "ztestsapling1wn3tw9w5rs55x5yl586gtk72e8hcfdq8zsnjzcu8p7ghm8lrx54axc74mvm335q7lmy3g0sqje6"
override val tskCompressed = "KzVugoXxR7AtTMdR5sdJtHxCNvMzQ4H196k7ATv4nnjoummsRC9G"
override val tpk = "03b1d7fb28d17c125b504d06b1530097e0a3c76ada184237e3bc0925041230a5af"
}
@BeforeClass
@JvmStatic
fun startup() {
Twig.plant(TroubleshootingTwig(formatter = { "@TWIG $it" }))
}
@JvmStatic
@Parameterized.Parameters
fun data() = listOf(
arrayOf(ExpectedTestnet, ZcashNetwork.Testnet),
arrayOf(ExpectedMainnet, ZcashNetwork.Mainnet)
)
}
interface Expected {
val tAddr: String
val zAddr: String
val tskCompressed: String
val tpk: String
}
}

View File

@@ -0,0 +1,39 @@
package cash.z.ecc.android.sdk.sample
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
/**
* Samples related to shielding funds.
*/
class ShieldFundsSample {
val SEED_PHRASE = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" // \"//\"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"//"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"
/**
* This test will construct a t2z transaction. It is safe to run this repeatedly, because
* nothing is submitted to the network (because the keys don't match the address so the encoding
* fails). Originally, it's intent is just to exercise the code and troubleshoot any issues but
* then it became clear that this would be a cool Sample Test and PoC for writing a SimpleWallet
* class.
*/
@Test
@Ignore("This test is broken")
fun constructT2Z() = runBlocking {
Twig.sprout("ShieldFundsSample")
val wallet = TestWallet(TestWallet.Backups.DEV_WALLET, ZcashNetwork.Mainnet)
Assert.assertEquals("foo", "${wallet.shieldedAddress} ${wallet.transparentAddress}")
// wallet.shieldFunds()
Twig.clip("ShieldFundsSample")
Assert.assertEquals(Zatoshi(5), wallet.synchronizer.saplingBalances.value?.available)
}
}

View File

@@ -0,0 +1,236 @@
package cash.z.ecc.android.sdk.sample
import androidx.test.filters.LargeTest
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.ZcashNetwork.Testnet
import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
/**
* Sample tests are used to demonstrate functionality. This one attempts to setup a scenario where
* one wallet shields funds and the other restores from the blockchain. Ultimately, they should have
* the same data.
*/
class TransparentRestoreSample {
val TX_VALUE = Zatoshi(ZcashSdk.MINERS_FEE.value / 2)
// val walletA = SimpleWallet(SEED_PHRASE, "WalletA")
// the wallet that only restores what everyone else did
// val walletB = SimpleWallet(SEED_PHRASE, "WalletB")
// // the wallet that sends Z2T transactions
//
// // sandbox wallet
// val walletSandbox = SimpleWallet(SEED_PHRASE, "WalletC")
// val walletZ2T = SimpleWallet(SEED_PHRASE, "WalletZ2T")
// val externalTransparentAddress =
// DerivationTool.deriveTransparentAddress(Mnemonics.MnemonicCode(RANDOM_PHRASE).toSeed(), Testnet)
// @Test
fun sendZ2Texternal() = runBlocking {
twig("Syncing WalletExt")
val extWallet = TestWallet(TestWallet.Backups.ALICE, alias = "WalletE")
extWallet.sync()
// extWallet.send(542, walletSandbox.transparentAddress, "External funds memo is lost, though")
delay(1000)
twig("Done sending funds to external address (Z->T COMPLETE!)")
}
// @Test
fun sendZ2T() = runBlocking {
// walletSandbox.sync()
// walletZ2T.send(543, externalTransparentAddress, "External funds memo is lost, though")
delay(1000)
twig("Done sending funds to external address (Z->T COMPLETE!)")
}
// @Test
fun autoShield() = runBlocking<Unit> {
val wallet = TestWallet(TestWallet.Backups.SAMPLE_WALLET, alias = "WalletC")
wallet.sync()
twig("Done syncing wallet!")
val tbalance = wallet.transparentBalance()
val address = wallet.transparentAddress
twig("t-avail: ${tbalance.available} t-total: ${tbalance.total}")
Assert.assertTrue("Not enough funds to run sample. Expected some Zatoshi but found ${tbalance.available}. Try adding funds to $address", tbalance.available.value > 0)
twig("Shielding available transparent funds!")
// wallet.shieldFunds()
}
// @Test
fun cli() = runBlocking<Unit> {
// val wallet = SimpleWallet(SEED_PHRASE, "WalletCli")
// wallet.sync()
// wallet.rewindToHeight(1343500).join(45_000)
val wallet = TestWallet(TestWallet.Backups.SAMPLE_WALLET, alias = "WalletC")
// wallet.sync().rewindToHeight(1339178).join(10000)
wallet.sync().rewindToHeight(1339178).send(
"ztestsapling17zazsl8rryl8kjaqxnr2r29rw9d9a2mud37ugapm0s8gmyv0ue43h9lqwmhdsp3nu9dazeqfs6l",
"is send broken?"
).join(5)
}
// This test is extremely slow and doesn't assert anything, so the benefit of this test is unclear
// It is disabled to allow moving forward with configuring CI.
@Test
@LargeTest
@Ignore("This test is extremely slow")
fun kris() = runBlocking<Unit> {
val wallet0 = TestWallet(TestWallet.Backups.SAMPLE_WALLET.seedPhrase, "tmpabc", Testnet, startHeight = 1330190)
// val wallet1 = SimpleWallet(WALLET0_PHRASE, "Wallet1")
wallet0.sync() // .shieldFunds()
// .send(amount = 1543L, memo = "")
.join()
// wallet1.sync().join(5_000L)
}
/*
*/
/**
* Sanity check that the wallet has enough funds for the test
*/
// @Test
fun hasFunds() = runBlocking<Unit> {
val walletSandbox = TestWallet(TestWallet.Backups.SAMPLE_WALLET.seedPhrase, "WalletC", Testnet, startHeight = 1330190)
// val job = walletA.walletScope.launch {
// twig("Syncing WalletA")
// walletA.sync()
// }
twig("Syncing WalletSandbox")
walletSandbox.sync()
// job.join()
delay(500)
twig("Done syncing both wallets!")
// val value = walletA.available
// val address = walletA.shieldedAddress
// Assert.assertTrue("Not enough funds to run sample. Expected at least $TX_VALUE Zatoshi but found $value. Try adding funds to $address", value >= TX_VALUE)
// send z->t
// walletA.send(TX_VALUE, walletA.transparentAddress, "${TransparentRestoreSample::class.java.simpleName} z->t")
walletSandbox.rewindToHeight(1339178)
twig("Done REWINDING!")
twig("T-ADDR (for the win!): ${walletSandbox.transparentAddress}")
delay(500)
// walletB.sync()
// rewind database B to height then rescan
}
// // when startHeight is null, it will use the latest checkpoint
// class SimpleWallet(
// seedPhrase: String,
// alias: String = ZcashSdk.DEFAULT_ALIAS,
// startHeight: Int? = null
// ) {
// val walletScope = CoroutineScope(
// SupervisorJob() + newFixedThreadPoolContext(3, this.javaClass.simpleName)
// )
// private val context = InstrumentationRegistry.getInstrumentation().context
// private val seed: ByteArray = Mnemonics.MnemonicCode(seedPhrase).toSeed()
// private val shieldedSpendingKey = DerivationTool.deriveSpendingKeys(seed, Testnet)[0]
// private val transparentSecretKey = DerivationTool.deriveTransparentSecretKey(seed, Testnet)
// private val host = "lightwalletd.testnet.electriccoin.co"
// private val initializer = Initializer(context) { config ->
// config.importWallet(seed, startHeight)
// config.setNetwork(Testnet, host)
// config.alias = alias
// }
//
// val synchronizer = Synchronizer(initializer)
// val available get() = synchronizer.latestBalance.availableZatoshi
// val shieldedAddress = DerivationTool.deriveShieldedAddress(seed, Testnet)
// val transparentAddress = DerivationTool.deriveTransparentAddress(seed, Testnet)
// val birthdayHeight get() = synchronizer.latestBirthdayHeight
//
// suspend fun transparentBalance(): WalletBalance {
// synchronizer.refreshUtxos(transparentAddress, synchronizer.latestBirthdayHeight)
// return synchronizer.getTransparentBalance(transparentAddress)
// }
//
// suspend fun sync(): SimpleWallet {
// if (!synchronizer.isStarted) {
// twig("Starting sync")
// synchronizer.start(walletScope)
// } else {
// twig("Awaiting next SYNCED status")
// }
//
// // block until synced
// synchronizer.status.first { it == SYNCED }
// twig("Synced!")
// return this
// }
//
// suspend fun send(address: String = transparentAddress, memo: String = "", amount: Long = 500L): SimpleWallet {
// synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo)
// .takeWhile { it.isPending() }
// .collect {
// twig("Updated transaction: $it")
// }
// return this
// }
//
// suspend fun rewindToHeight(height: Int): SimpleWallet {
// synchronizer.rewindToHeight(height, false)
// return this
// }
//
// suspend fun shieldFunds(): SimpleWallet {
// twig("checking $transparentAddress for transactions!")
// synchronizer.refreshUtxos(transparentAddress, 935000).let { count ->
// twig("FOUND $count new UTXOs")
// }
//
// synchronizer.getTransparentBalance(transparentAddress).let { walletBalance ->
// twig("FOUND utxo balance of total: ${walletBalance.totalZatoshi} available: ${walletBalance.availableZatoshi}")
//
// if (walletBalance.availableZatoshi > 0L) {
// synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey)
// .onCompletion { twig("done shielding funds") }
// .catch { twig("Failed with $it") }
// .collect()
// }
// }
//
// return this
// }
//
// suspend fun join(timeout: Long? = null): SimpleWallet {
// // block until stopped
// twig("Staying alive until synchronizer is stopped!")
// if (timeout != null) {
// twig("Scheduling a stop in ${timeout}ms")
// walletScope.launch {
// delay(timeout)
// synchronizer.stop()
// }
// }
// synchronizer.status.first { it == Synchronizer.Status.STOPPED }
// twig("Stopped!")
// return this
// }
//
// companion object {
// init {
// Twig.plant(TroubleshootingTwig())
// }
// }
// }
}

View File

@@ -0,0 +1,91 @@
package cash.z.ecc.android.sdk.test
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
import java.util.concurrent.TimeoutException
open class ScopedTest(val defaultTimeout: Long = 2000L) {
protected lateinit var testScope: CoroutineScope
// if an androidTest doesn't need a context, then maybe it should be a unit test instead?!
val context: Context = InstrumentationRegistry.getInstrumentation().context
@Before
fun start() {
twig("===================== TEST STARTED ==================================")
testScope = CoroutineScope(
Job(classScope.coroutineContext[Job]!!) + newFixedThreadPoolContext(
5,
this.javaClass.simpleName
)
)
}
@After
fun end() = runBlocking<Unit> {
twig("======================= TEST CANCELLING =============================")
testScope.cancel()
testScope.coroutineContext[Job]?.join()
twig("======================= TEST ENDED ==================================")
}
fun timeout(duration: Long, block: suspend () -> Unit) = timeoutWith(testScope, duration, block)
companion object {
@JvmStatic
lateinit var classScope: CoroutineScope
init {
Twig.plant(TroubleshootingTwig())
twig("================================================================ INIT")
}
@BeforeClass
@JvmStatic
fun createScope() {
twig("======================= CLASS STARTED ===============================")
classScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(2, this.javaClass.simpleName)
)
}
@AfterClass
@JvmStatic
fun destroyScope() = runBlocking<Unit> {
twig("======================= CLASS CANCELLING ============================")
classScope.cancel()
classScope.coroutineContext[Job]?.join()
twig("======================= CLASS ENDED =================================")
}
@JvmStatic
fun timeoutWith(scope: CoroutineScope, duration: Long, block: suspend () -> Unit) {
scope.launch {
delay(duration)
val message = "ERROR: Test timed out after ${duration}ms"
twig(message)
throw TimeoutException(message)
}.let { selfDestruction ->
scope.launch {
block()
selfDestruction.cancel()
}
}
}
}
}

View File

@@ -0,0 +1,57 @@
package cash.z.ecc.android.sdk.tool
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool.IS_FALLBACK_ON_FAILURE
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class WalletBirthdayToolTest {
@Test
@SmallTest
fun birthday_height_from_filename() {
assertEquals(123, WalletBirthdayTool.birthdayHeight("123.json"))
}
@Test
@SmallTest
fun load_latest_birthday() {
// Using a separate directory, so that we don't have to keep updating this test each time
// mainnet or testnet changes
val directory = "co.electriccoin.zcash/checkpoint/goodnet"
val context = ApplicationProvider.getApplicationContext<Context>()
val birthday = runBlocking {
WalletBirthdayTool.getFirstValidWalletBirthday(
context,
directory,
listOf("1300000.json", "1290000.json")
)
}
assertEquals(1300000, birthday.height)
}
@Test
@SmallTest
fun load_latest_birthday_fallback_on_bad_json() {
if (!IS_FALLBACK_ON_FAILURE) {
return
}
val directory = "co.electriccoin.zcash/checkpoint/badnet"
val context = ApplicationProvider.getApplicationContext<Context>()
val birthday = runBlocking {
WalletBirthdayTool.getFirstValidWalletBirthday(
context,
directory,
listOf("1300000.json", "1290000.json")
)
}
assertEquals(1290000, birthday.height)
}
}

View File

@@ -0,0 +1,58 @@
package cash.z.ecc.android.sdk.util
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import okio.buffer
import okio.source
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.IOException
@ExperimentalCoroutinesApi
class AddressGeneratorUtil {
private val context = InstrumentationRegistry.getInstrumentation().context
private val mnemonics = SimpleMnemonics()
@Test
fun printMnemonic() {
mnemonics.apply {
val mnemonicPhrase = String(nextMnemonic())
println("example mnemonic: $mnemonicPhrase")
assertEquals(24, mnemonicPhrase.split(" ").size)
}
}
@Test
fun generateAddresses() = runBlocking {
readLines()
.map { seedPhrase ->
mnemonics.toSeed(seedPhrase.toCharArray())
}.map { seed ->
DerivationTool.deriveShieldedAddress(seed, ZcashNetwork.Mainnet)
}.collect { address ->
println("xrxrx2\t$address")
assertTrue(address.startsWith("zs1"))
}
}
@Throws(IOException::class)
fun readLines() = flow<String> {
val seedFile = javaClass.getResourceAsStream("/utils/seeds.txt")!!
seedFile.source().buffer().use { source ->
var line: String? = source.readUtf8Line()
while (line != null) {
emit(line)
line = source.readUtf8Line()
}
}
}
}

View File

@@ -0,0 +1,196 @@
package cash.z.ecc.android.sdk.util
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import okio.buffer
import okio.source
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import java.io.IOException
/**
* A tool for checking transactions since the given birthday and printing balances. This was useful for the Zcon1 app to
* ensure that we loaded all the pokerchips correctly.
*/
@ExperimentalCoroutinesApi
class BalancePrinterUtil {
private val network = ZcashNetwork.Mainnet
private val downloadBatchSize = 9_000
private val birthdayHeight = 523240
private val mnemonics = SimpleMnemonics()
private val context = InstrumentationRegistry.getInstrumentation().context
private val alias = "BalanceUtil"
// private val caceDbPath = Initializer.cacheDbPath(context, alias)
//
// private val downloader = CompactBlockDownloader(
// LightWalletGrpcService(context, host, port),
// CompactBlockDbStore(context, caceDbPath)
// )
// private val processor = CompactBlockProcessor(downloader)
// private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName)
private lateinit var birthday: WalletBirthday
private var synchronizer: Synchronizer? = null
@Before
fun setup() {
Twig.plant(TroubleshootingTwig())
cacheBlocks()
birthday = runBlocking { WalletBirthdayTool.loadNearest(context, network, birthdayHeight) }
}
private fun cacheBlocks() = runBlocking {
// twig("downloading compact blocks...")
// val latestBlockHeight = downloader.getLatestBlockHeight()
// val lastDownloaded = downloader.getLastDownloadedHeight()
// val blockRange = (Math.max(birthday, lastDownloaded))..latestBlockHeight
// downloadNewBlocks(blockRange)
// val error = validateNewBlocks(blockRange)
// twig("validation completed with result $error")
// assertEquals(-1, error)
}
private suspend fun deleteDb(dbName: String) {
context.getDatabasePath(dbName).absoluteFile.deleteSuspend()
}
@Test
@Ignore("This test is broken")
fun printBalances() = runBlocking {
readLines()
.map { seedPhrase ->
twig("checking balance for: $seedPhrase")
mnemonics.toSeed(seedPhrase.toCharArray())
}.collect { seed ->
// TODO: clear the dataDb but leave the cacheDb
val initializer = Initializer.new(context) { config ->
runBlocking { config.importWallet(seed, birthdayHeight, network) }
config.setNetwork(network)
config.alias = alias
}
/*
what I need to do right now
- for each seed
- I can reuse the cache of blocks... so just like get the cache once
- I need to scan into a new database
- I don't really need a new rustbackend
- I definitely don't need a new grpc connection
- can I just use a processor and point it to a different DB?
+ so yeah, I think I need to use the processor directly right here and just swap out its pieces
- perhaps create a new initializer and use that to configure the processor?
- or maybe just set the data destination for the processor
- I might need to consider how state is impacting this design
- can we be more stateless and thereby improve the flexibility of this code?!!!
*/
synchronizer?.stop()
synchronizer = Synchronizer.new(initializer).apply {
start()
}
// deleteDb(dataDbPath)
// initWallet(seed)
// twig("scanning blocks for seed <$seed>")
// // rustBackend.scanBlocks()
// twig("done scanning blocks for seed $seed")
// // val total = rustBackend.getBalance(0)
// twig("found total: $total")
// // val available = rustBackend.getVerifiedBalance(0)
// twig("found available: $available")
// twig("xrxrx2\t$seed\t$total\t$available")
// println("xrxrx2\t$seed\t$total\t$available")
}
}
// @Test
// fun printBalances() = runBlocking {
// readLines().collect { seed ->
// deleteDb(dataDbName)
// initWallet(seed)
// twig("scanning blocks for seed <$seed>")
// rustBackend.scanBlocks()
// twig("done scanning blocks for seed $seed")
// val total = rustBackend.getBalance(0)
// twig("found total: $total")
// val available = rustBackend.getVerifiedBalance(0)
// twig("found available: $available")
// twig("xrxrx2\t$seed\t$total\t$available")
// println("xrxrx2\t$seed\t$total\t$available")
// }
// Thread.sleep(5000)
// assertEquals("foo", "bar")
// }
@Throws(IOException::class)
fun readLines() = flow<String> {
val seedFile = javaClass.getResourceAsStream("/utils/seeds.txt")!!
seedFile.source().buffer().use { source ->
var line: String? = source.readUtf8Line()
while (line != null) {
emit(line)
line = source.readUtf8Line()
}
}
}
// private fun initWallet(seed: String): Wallet {
// val spendingKeyProvider = Delegates.notNull<String>()
// return Wallet(
// context,
// rustBackend,
// SampleSeedProvider(seed),
// spendingKeyProvider,
// Wallet.loadBirthdayFromAssets(context, birthday)
// ).apply {
// runCatching {
// initialize()
// }
// }
// }
private fun downloadNewBlocks(range: IntRange) = runBlocking {
Twig.sprout("downloading")
twig("downloading blocks in range $range")
var downloadedBlockHeight = range.start
val count = range.last - range.first + 1
val batches =
(count / downloadBatchSize + (if (count.rem(downloadBatchSize) == 0) 0 else 1))
twig("found $count missing blocks, downloading in $batches batches of $downloadBatchSize...")
for (i in 1..batches) {
val end = Math.min(range.first + (i * downloadBatchSize), range.last + 1)
val batchRange = downloadedBlockHeight until end
twig("downloaded $batchRange (batch $i of $batches)") {
// downloader.downloadBlockRange(batchRange)
}
downloadedBlockHeight = end
}
Twig.clip("downloading")
}
// private fun validateNewBlocks(range: IntRange?): Int {
// // val dummyWallet = initWallet("dummySeed")
// Twig.sprout("validating")
// twig("validating blocks in range $range")
// // val result = rustBackend.validateCombinedChain()
// Twig.clip("validating")
// return result
// }
}

View File

@@ -0,0 +1,142 @@
package cash.z.ecc.android.sdk.util
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
/**
* A tool for validating an existing database and testing reorgs.
*/
@ExperimentalCoroutinesApi
class DataDbScannerUtil {
private val context = InstrumentationRegistry.getInstrumentation().context
private val host = "lightd-main.zecwallet.co"
private val port = 443
private val alias = "ScannerUtil"
// private val mnemonics = SimpleMnemonics()
// private val caceDbPath = Initializer.cacheDbPath(context, alias)
// private val downloader = CompactBlockDownloader(
// LightWalletGrpcService(context, host, port),
// CompactBlockDbStore(context, caceDbPath)
// )
// private val processor = CompactBlockProcessor(downloader)
// private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName)
private val birthdayHeight = 600_000
private lateinit var synchronizer: Synchronizer
@Before
fun setup() {
Twig.plant(TroubleshootingTwig())
// cacheBlocks()
}
private fun cacheBlocks() = runBlocking {
// twig("downloading compact blocks...")
// val latestBlockHeight = downloader.getLatestBlockHeight()
// val lastDownloaded = downloader.getLastDownloadedHeight()
// val blockRange = (Math.max(birthday, lastDownloaded))..latestBlockHeight
// downloadNewBlocks(blockRange)
// val error = validateNewBlocks(blockRange)
// twig("validation completed with result $error")
// assertEquals(-1, error)
}
private fun deleteDb(dbName: String) {
context.getDatabasePath(dbName).absoluteFile.delete()
}
@Test
@Ignore("This test is broken")
fun scanExistingDb() {
synchronizer = run {
val initializer = runBlocking {
Initializer.new(context) {
it.setBirthdayHeight(
birthdayHeight
)
}
}
val synchronizer = runBlocking {
Synchronizer.new(
initializer
)
}
synchronizer
}
println("sync!")
synchronizer.start()
val scope = (synchronizer as SdkSynchronizer).coroutineScope
scope.launch {
synchronizer.status.collect { status ->
// when (status) {
println("received status of $status")
// }
}
}
println("going to sleep!")
Thread.sleep(125000)
println("I'm back and I'm out!")
synchronizer.stop()
}
//
// @Test
// fun printBalances() = runBlocking {
// readLines()
// .map { seedPhrase ->
// twig("checking balance for: $seedPhrase")
// mnemonics.toSeed(seedPhrase.toCharArray())
// }.collect { seed ->
// initializer.import(seed, birthday, clearDataDb = true, clearCacheDb = false)
// /*
// what I need to do right now
// - for each seed
// - I can reuse the cache of blocks... so just like get the cache once
// - I need to scan into a new database
// - I don't really need a new rustbackend
// - I definitely don't need a new grpc connection
// - can I just use a processor and point it to a different DB?
// + so yeah, I think I need to use the processor directly right here and just swap out its pieces
// - perhaps create a new initializer and use that to configure the processor?
// - or maybe just set the data destination for the processor
// - I might need to consider how state is impacting this design
// - can we be more stateless and thereby improve the flexibility of this code?!!!
// */
// synchronizer?.stop()
// synchronizer = Synchronizer(context, initializer)
//
// // deleteDb(dataDbPath)
// // initWallet(seed)
// // twig("scanning blocks for seed <$seed>")
// //// rustBackend.scanBlocks()
// // twig("done scanning blocks for seed $seed")
// //// val total = rustBackend.getBalance(0)
// // twig("found total: $total")
// //// val available = rustBackend.getVerifiedBalance(0)
// // twig("found available: $available")
// // twig("xrxrx2\t$seed\t$total\t$available")
// // println("xrxrx2\t$seed\t$total\t$available")
// }
//
// Thread.sleep(5000)
// assertEquals("foo", "bar")
// }
}

View File

@@ -0,0 +1,20 @@
package cash.z.ecc.android.sdk.util
import cash.z.android.plugin.MnemonicPlugin
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.Mnemonics.WordCount
import cash.z.ecc.android.bip39.toEntropy
import cash.z.ecc.android.bip39.toSeed
import java.util.Locale
class SimpleMnemonics : MnemonicPlugin {
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars
override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> = MnemonicCode(entropy).words
override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed()
override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words
}

View File

@@ -0,0 +1,175 @@
package cash.z.ecc.android.sdk.util
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.isPending
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.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeoutException
/**
* A simple wallet that connects to testnet for integration testing. The intention is that it is
* easy to drive and nice to use.
*/
class TestWallet(
val seedPhrase: String,
val alias: String = "TestWallet",
val network: ZcashNetwork = ZcashNetwork.Testnet,
val host: String = network.defaultHost,
startHeight: Int? = null,
val port: Int = network.defaultPort
) {
constructor(
backup: Backups,
network: ZcashNetwork = ZcashNetwork.Testnet,
alias: String = "TestWallet"
) : this(
backup.seedPhrase,
network = network,
startHeight = if (network == ZcashNetwork.Mainnet) backup.mainnetBirthday else backup.testnetBirthday,
alias = alias
)
val walletScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(3, this.javaClass.simpleName)
)
// Although runBlocking isn't great, this usage is OK because this is only used within the
// automated tests
private val context = InstrumentationRegistry.getInstrumentation().context
private val seed: ByteArray = Mnemonics.MnemonicCode(seedPhrase).toSeed()
private val shieldedSpendingKey =
runBlocking { DerivationTool.deriveSpendingKeys(seed, network = network)[0] }
private val transparentSecretKey =
runBlocking { DerivationTool.deriveTransparentSecretKey(seed, network = network) }
val initializer = runBlocking {
Initializer.new(context) { config ->
runBlocking { config.importWallet(seed, startHeight, network, host, alias = alias) }
}
}
val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(initializer) as SdkSynchronizer
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
val available get() = synchronizer.saplingBalances.value?.available
val shieldedAddress =
runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) }
val transparentAddress =
runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) }
val birthdayHeight get() = synchronizer.latestBirthdayHeight
val networkName get() = synchronizer.network.networkName
val connectionInfo get() = service.connectionInfo.toString()
suspend fun transparentBalance(): WalletBalance {
synchronizer.refreshUtxos(transparentAddress, synchronizer.latestBirthdayHeight)
return synchronizer.getTransparentBalance(transparentAddress)
}
suspend fun sync(timeout: Long = -1): TestWallet {
val killSwitch = walletScope.launch {
if (timeout > 0) {
delay(timeout)
throw TimeoutException("Failed to sync wallet within ${timeout}ms")
}
}
if (!synchronizer.isStarted) {
twig("Starting sync")
synchronizer.start(walletScope)
} else {
twig("Awaiting next SYNCED status")
}
// block until synced
synchronizer.status.first { it == Synchronizer.Status.SYNCED }
killSwitch.cancel()
twig("Synced!")
return this
}
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet {
Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending() }
.collect {
twig("Updated transaction: $it")
}
Twig.clip("$alias sending")
return this
}
suspend fun rewindToHeight(height: Int): TestWallet {
synchronizer.rewindToNearestHeight(height, false)
return this
}
suspend fun shieldFunds(): TestWallet {
twig("checking $transparentAddress for transactions!")
synchronizer.refreshUtxos(transparentAddress, 935000).let { count ->
twig("FOUND $count new UTXOs")
}
synchronizer.getTransparentBalance(transparentAddress).let { walletBalance ->
twig("FOUND utxo balance of total: ${walletBalance.total} available: ${walletBalance.available}")
if (walletBalance.available.value > 0L) {
synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey)
.onCompletion { twig("done shielding funds") }
.catch { twig("Failed with $it") }
.collect()
}
}
return this
}
suspend fun join(timeout: Long? = null): TestWallet {
// block until stopped
twig("Staying alive until synchronizer is stopped!")
if (timeout != null) {
twig("Scheduling a stop in ${timeout}ms")
walletScope.launch {
delay(timeout)
synchronizer.stop()
}
}
synchronizer.status.first { it == Synchronizer.Status.STOPPED }
twig("Stopped!")
return this
}
companion object {
init {
Twig.enabled(true)
}
}
enum class Backups(val seedPhrase: String, val testnetBirthday: Int, val mainnetBirthday: Int) {
// TODO: get the proper birthday values for these wallets
DEFAULT("column rhythm acoustic gym cost fit keen maze fence seed mail medal shrimp tell relief clip cannon foster soldier shallow refuse lunar parrot banana", 1_355_928, 1_000_000),
SAMPLE_WALLET("input frown warm senior anxiety abuse yard prefer churn reject people glimpse govern glory crumble swallow verb laptop switch trophy inform friend permit purpose", 1_330_190, 1_000_000),
DEV_WALLET("still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread", 1_000_000, 991645),
ALICE("quantum whisper lion route fury lunar pelican image job client hundred sauce chimney barely life cliff spirit admit weekend message recipe trumpet impact kitten", 1_330_190, 1_000_000),
BOB("canvas wine sugar acquire garment spy tongue odor hole cage year habit bullet make label human unit option top calm neutral try vocal arena", 1_330_190, 1_000_000),
;
}
}

View File

@@ -0,0 +1,60 @@
package cash.z.ecc.android.sdk.util
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
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.type.ZcashNetwork
import org.junit.Ignore
import org.junit.Test
class TransactionCounterUtil {
private val network = ZcashNetwork.Mainnet
private val context = InstrumentationRegistry.getInstrumentation().context
private val service = LightWalletGrpcService(context, network)
init {
Twig.plant(TroubleshootingTwig())
}
@Test
@Ignore("This test is broken")
fun testBlockSize() {
val sizes = mutableMapOf<Int, Int>()
service.getBlockRange(900_000..910_000).forEach { b ->
twig("h: ${b.header.size()}")
val s = b.serializedSize
sizes[s] = (sizes[s] ?: 0) + 1
}
twig("sizes: ${sizes.toSortedMap()}")
}
@Test
@Ignore("This test is broken")
fun testCountTransactions() {
val txCounts = mutableMapOf<Int, Int>()
val outputCounts = mutableMapOf<Int, Int>()
var totalOutputs = 0
var totalTxs = 0
service.getBlockRange(900_000..950_000).forEach { b ->
b.header.size()
b.vtxList.map { it.outputsCount }.forEach { oCount ->
outputCounts[oCount] = (outputCounts[oCount] ?: 0) + oCount.coerceAtLeast(1)
totalOutputs += oCount
}
b.vtxCount.let { count ->
txCounts[count] = (txCounts[count] ?: 0) + count.coerceAtLeast(1)
totalTxs += count
}
}
twig("txs: $txCounts")
twig("outputs: $outputCounts")
twig("total: $totalTxs $totalOutputs")
}
}
/*
*/

View File

@@ -0,0 +1,36 @@
package cash.z.ecc.fixture
import cash.z.ecc.android.sdk.internal.KEY_EPOCH_SECONDS
import cash.z.ecc.android.sdk.internal.KEY_HASH
import cash.z.ecc.android.sdk.internal.KEY_HEIGHT
import cash.z.ecc.android.sdk.internal.KEY_TREE
import cash.z.ecc.android.sdk.internal.KEY_VERSION
import cash.z.ecc.android.sdk.internal.VERSION_1
import cash.z.ecc.android.sdk.type.WalletBirthday
import org.json.JSONObject
object WalletBirthdayFixture {
// These came from the mainnet 1500000.json file
const val HEIGHT = 1500000
const val HASH = "00000000019e5b25a95c7607e7789eb326fddd69736970ebbe1c7d00247ef902"
const val EPOCH_SECONDS = 1639913234L
@Suppress("MaxLineLength")
const val TREE = "01ce183032b16ed87fcc5052a42d908376526126346567773f55bc58a63e4480160013000001bae5112769a07772345dd402039f2949c457478fe9327363ff631ea9d78fb80d0177c0b6c21aa9664dc255336ed450914088108c38a9171c85875b4e53d31b3e140171add6f9129e124651ca894aa842a3c71b1738f3ee2b7ba829106524ef51e62101f9cebe2141ee9d0a3f3a3e28bce07fa6b6e1c7b42c01cc4fe611269e9d52da540001d0adff06de48569129bd2a211e3253716362da97270d3504d9c1b694689ebe3c0122aaaea90a7fa2773b8166937310f79a4278b25d759128adf3138d052da3725b0137fb2cbc176075a45db2a3c32d3f78e669ff2258fd974e99ec9fb314d7fd90180165aaee3332ea432d13a9398c4863b38b8a7a491877a5c46b0802dcd88f7e324301a9a262f8b92efc2e0e3e4bd1207486a79d62e87b4ab9cc41814d62a23c4e28040001e3c4ee998682df5c5e230d6968e947f83d0c03682f0cfc85f1e6ec8e8552c95a000155989fed7a8cc7a0d479498d6881ca3bafbe05c7095110f85c64442d6a06c25c0185cd8c141e620eda0ca0516f42240aedfabdf9189c8c6ac834b7bdebc171331d01ecceb776c043662617d62646ee60985521b61c0b860f3a9731e66ef74ed8fb320118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
fun new(
height: Int = HEIGHT,
hash: String = HASH,
time: Long = EPOCH_SECONDS,
tree: String = TREE
) = WalletBirthday(height = height, hash = hash, time = time, tree = tree)
}
fun WalletBirthday.toJson() = JSONObject().apply {
put(WalletBirthday.KEY_VERSION, WalletBirthday.VERSION_1)
put(WalletBirthday.KEY_HEIGHT, height)
put(WalletBirthday.KEY_HASH, hash)
put(WalletBirthday.KEY_EPOCH_SECONDS, time)
put(WalletBirthday.KEY_TREE, tree)
}.toString()

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="lightwalletd_allow_very_insecure_connections">false</bool>
</resources>

View File

@@ -0,0 +1,18 @@
kitchen renew wide common vague fold vacuum tilt amazing pear square gossip jewel month tree shock scan alpha just spot fluid toilet view dinner
urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist decorate rhythm phone style benefit portion bus truck top
wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame
labor elite banana cement royal tiger smile robust talk street bread bitter admit spy leg alcohol opinion mimic crane bid damp trigger wagon share
icon future member loan initial music bless cigar artist cross scorpion disease click else palm recall obscure horse wire energy frost route stone raven
way fruit group range army seven stem ridge panel duty deal like mango engage adult market drama large year love clay desert culture evoke
stairs bridge romance offer bronze organ soldier point unveil soup figure economy purity rapid eight error make goat poet when letter gold coil gate
execute thing home flat rare pitch plug poverty never design cute essay mosquito unhappy pen phone aerobic basket empower system extend concert leopard leopard
thought balcony raw renew sister define isolate bridge rigid critic extra enhance accuse skin either lock owner boat grid legal coral judge oyster olympic
pull curious short apology slot giraffe island caution cricket attract episode acoustic age fly crucial earth broccoli eternal eyebrow marriage lazy thank actor police
army boat guess direct network version mean rice brown sauce bronze health stable way proud gift primary reason company raw sorry virtual other ahead
humble educate desert govern quality cup illness spatial whale zoo novel hollow velvet erosion gadget glove great occur milk staff gravity word skate soul
horror scene device ahead before blossom surface staff shrug horse wood drill style garage north account twice easily slam require nose sentence catalog mango
bronze this era window wonder strike label grid keep paddle kiwi age input flock just eagle coil like toward burst mobile obtain giant idle
aisle dwarf bulb catch anxiety follow attack that habit exclude laptop spoon enough walnut picture reward pact license behind question save cover exotic drip
two length electric immune antique rotate junior spoon torch liberty eyebrow shoe army away horn anger oak chase grow ride enrich soft push orient
bike crunch vintage smoke okay screen side pattern thrive top timber payment flight garment lift heavy enable sting humble obscure reveal art kangaroo owner
treat stumble only reward else turtle across shop vocal dynamic goddess toss review polar enable plate process cabin injury rifle sword group agree slush