Initial commit
This commit is contained in:
10
sdk-lib/src/androidTest/AndroidManifest.xml
Normal file
10
sdk-lib/src/androidTest/AndroidManifest.xml
Normal 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>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"network": "main",
|
||||
"height": 1290000,
|
||||
"hash": "00000000014836c3cbc011276cbd3702a76a1fea7eb2c0c2c257321220376450",
|
||||
"time": 1624075741,
|
||||
"saplingTree": "01accf4fc3dc4233bbe757f94e0d4cd23b4aa2e6ac472601f4f53ca4dc86a8a05901fae977171a6103a0338990e073ffe50e29fc8bf0400dcd3378ebfe7a146ed1481300014f7b33dd5159ac66f2670b7db8925065e7154e0199ff7ee7559b276ba56ad1200173e9881f21357e54027a4275114f0f6ad4ca17143554182f63c77f3288a23a20011d65465ab942440e200d429ef892452b4b05c5b21e9a6e6d968a719c67b5e85b000000000000000150926c74975e2d8ff095defb75a4a6d9f17007e87a74230a65a3265d8f45032900012ffde6dccbef68b60cd7b4e7a8fe7989f5954fa4bacad01b247d16b9bfa5084000000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"network": "main",
|
||||
"height": 1290000,
|
||||
"hash": "00000000014836c3cbc011276cbd3702a76a1fea7eb2c0c2c257321220376450",
|
||||
"time": 1624075741,
|
||||
"saplingTree": "01accf4fc3dc4233bbe757f94e0d4cd23b4aa2e6ac472601f4f53ca4dc86a8a05901fae977171a6103a0338990e073ffe50e29fc8bf0400dcd3378ebfe7a146ed1481300014f7b33dd5159ac66f2670b7db8925065e7154e0199ff7ee7559b276ba56ad1200173e9881f21357e54027a4275114f0f6ad4ca17143554182f63c77f3288a23a20011d65465ab942440e200d429ef892452b4b05c5b21e9a6e6d968a719c67b5e85b000000000000000150926c74975e2d8ff095defb75a4a6d9f17007e87a74230a65a3265d8f45032900012ffde6dccbef68b60cd7b4e7a8fe7989f5954fa4bacad01b247d16b9bfa5084000000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"network": "main",
|
||||
"height": 1300000,
|
||||
"hash": "00000000027222bdbcf9c5f807f851f97312ac6e0dbbc2b93f2be21a69c59d44",
|
||||
"time": 1624830312,
|
||||
"saplingTree": "01f5a97e2679a2bb9103caf37b825f92fcd73fff836234844dfcf1815394522b2c01526587b9b9e8aeb0eb572d81fec1f5127b8278ba0f57e451bd6b796596940a2213000131c7ff90fafff6159b8fb6544a2bcbba6c102903158fce8f9a9d3c6654abb23300013555cb7f4f79badeaca9bf2dca5a8704f0929053d50e95c03002f9a4d5286c3a01ad3557e11c1607ec888dc84f5f8899c3c79fb1f50b613946452ec7dd5e53763c0001c4583f4482b949390dba355fc8fa63019c83acd644ddd633cb50211d236f870600000001088da0d78eefd0c222507927e403b972d0890d0c31e08b02268fbe39ac4a6e170001edf82d4e2b4893ea2028ca8c5149e50a4c358b856d73f2de2b9a22034fa78f22012ffde6dccbef68b60cd7b4e7a8fe7989f5954fa4bacad01b247d16b9bfa5084000000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
|
||||
}
|
||||
103
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/AssetTest.kt
Normal file
103
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/AssetTest.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
// }
|
||||
}
|
||||
@@ -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")
|
||||
// }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
|
||||
*/
|
||||
@@ -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()
|
||||
4
sdk-lib/src/androidTest/res/values/bools.xml
Normal file
4
sdk-lib/src/androidTest/res/values/bools.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="lightwalletd_allow_very_insecure_connections">false</bool>
|
||||
</resources>
|
||||
18
sdk-lib/src/androidTest/resources/utils/seeds.txt
Normal file
18
sdk-lib/src/androidTest/resources/utils/seeds.txt
Normal 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
|
||||
Reference in New Issue
Block a user