Initial commit

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

View File

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

View File

@@ -0,0 +1,88 @@
package cash.z.ecc.android.sdk.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.internal.twigTask
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import kotlinx.coroutines.runBlocking
// import org.junit.BeforeClass
// import org.junit.Test
//
// class MultiAccountIntegrationTest : ScopedTest() {
//
// /**
// * Test multiple viewing keys by doing the following:
// *
// * - sync "account A" with 100 test blocks containing:
// * (in zatoshi) four 100_000 notes and one 10_000 note
// * - import a viewing key for "account B"
// * - send a 10_000 zatoshi transaction from A to B
// * - include that tx in the next block and mine that block (on the darkside), then scan it
// * - verify that A's balance reflects a single 100_000 note being spent but pending confirmations
// * - advance the chain by 9 more blocks to reach 10 confirmations
// * - verify that the change from the spent note is reflected in A's balance
// * - check B's balance and verify that it received the full 10_000 (i.e. that A paid the mining fee)
// *
// * Although we sent funds to an address, the synchronizer has both spending keys so it is able
// * to track transactions for both addresses!
// */
// @Test
// fun testViewingKeyImport() = runBlocking {
// validatePreConditions()
//
// with(sithLord) {
// twigTask("importing viewing key") {
// synchronizer.importViewingKey(secondKey)
// }
//
// twigTask("Sending funds") {
// sithLord.createAndSubmitTx(10_000, secondAddress, "multi-account works!")
// chainMaker.applyPendingTransactions(663251)
// await(targetHeight = 663251)
// }
// // verify that the transaction block height was scanned
// validator.validateMinHeightScanned(663251)
//
// // balance before confirmations (the large 100_000 note gets selected)
// validator.validateBalance(310_000)
//
// // add remaining confirmations so that funds become spendable and await until they're scanned
// chainMaker.advanceBy(9)
// await(targetHeight = 663260)
//
// // balance after confirmations
// validator.validateBalance(390_000)
//
// // check the extra viewing key balance!!!
// // accountIndex 1 corresponds to the imported viewingKey for the address where we sent the funds!
// validator.validateBalance(available = 10_000, accountIndex = 1)
// }
// }
//
// /**
// * Verify that before the integration test begins, the wallet is synced up to the expected block
// * and contains the expected balance.
// */
// private fun validatePreConditions() {
// with(sithLord) {
// twigTask("validating preconditions") {
// validator.validateMinHeightScanned(663250)
// validator.validateMinBalance(410_000)
// }
// }
// }
//
//
// companion object {
// private val sithLord = DarksideTestCoordinator()
// private val secondAddress = "zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx"
// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l"
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord.enterTheDarkside()
// sithLord.chainMaker.makeSimpleChain()
// sithLord.startSync(classScope).await()
// }
// }
// }

View File

@@ -0,0 +1,75 @@
package cash.z.ecc.android.sdk.darkside
// import cash.z.ecc.android.sdk.SdkSynchronizer
// import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.internal.twig
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import kotlinx.coroutines.Job
// import kotlinx.coroutines.delay
// import kotlinx.coroutines.flow.launchIn
// import kotlinx.coroutines.flow.onEach
// import kotlinx.coroutines.runBlocking
// import org.junit.Assert.assertEquals
// import org.junit.BeforeClass
// import org.junit.Test
// class MultiAccountTest : ScopedTest() {
//
// @Test
// fun testTargetBlock_sanityCheck() {
// with(sithLord) {
// validator.validateMinHeightScanned(663250)
// validator.validateMinBalance(200000)
// }
// }
//
// @Test
// fun testTargetBlock_send() = runBlocking {
// with(sithLord) {
//
// twig("<importing viewing key><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>")
// synchronizer.importViewingKey(secondKey)
// twig("<DONE importing viewing key><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>")
//
// twig("IM GONNA SEND!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
// sithLord.sendAndWait(testScope, spendingKey, 10000, secondAddress, "multi-account works!")
// chainMaker.applySentTransactions()
// await(targetHeight = 663251)
//
// twig("done waiting for 663251!")
// validator.validateMinHeightScanned(663251)
//
// // balance before confirmations
// validator.validateBalance(310000)
//
// // add remaining confirmations
// chainMaker.advanceBy(9)
// await(targetHeight = 663260)
//
// // balance after confirmations
// validator.validateBalance(390000)
//
// // check the extra viewing key balance!!!
// val account1Balance = (synchronizer as SdkSynchronizer).processor.getBalanceInfo(1)
// assertEquals(10000, account1Balance.totalZatoshi)
// twig("done waiting for 663261!")
// }
// }
//
//
// companion object {
// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
// private val sithLord = DarksideTestCoordinator()
// private val secondAddress = "zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx"
// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l"
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord.enterTheDarkside()
// sithLord.chainMaker.simpleChain()
// sithLord.startSync(classScope).await()
// }
// }
// }

View File

@@ -0,0 +1,196 @@
package cash.z.ecc.android.sdk.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.internal.twig
// import cash.z.ecc.android.sdk.internal.twigTask
// import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import cash.z.ecc.android.sdk.util.SimpleMnemonics
// import cash.z.wallet.sdk.rpc.CompactFormats
// import cash.z.wallet.sdk.rpc.Service
// import io.grpc.*
// import kotlinx.coroutines.delay
// import kotlinx.coroutines.runBlocking
// import org.junit.Assert.assertEquals
// import org.junit.BeforeClass
// import org.junit.Ignore
// import org.junit.Test
// import java.util.concurrent.TimeUnit
// class MultiRecipientIntegrationTest : ScopedTest() {
//
// @Test
// @Ignore
// fun testMultiRecipients() = runBlocking {
// with(sithLord) {
// val m = SimpleMnemonics()
// randomPhrases.map {
// m.toSeed(it.toCharArray())
// }.forEach { seed ->
// twig("ZyZ4: I've got a seed $seed")
// initializer.apply {
// // delay(250)
// twig("VKZyZ: ${deriveViewingKeys(seed)[0]}")
// // delay(500)
// twig("SKZyZ: ${deriveSpendingKeys(seed)[0]}")
// // delay(500)
// twig("ADDRZyZ: ${deriveAddress(seed)}")
// // delay(250)
// }
// }
// }
// delay(500)
// }
//
// @Test
// fun loadVks() = runBlocking {
// with(sithLord) {
// viewingKeys.forEach {
// twigTask("importing viewing key") {
// synchronizer.importViewingKey(it)
// }
// }
// twigTask("Sending funds") {
// createAndSubmitTx(10_000, addresses[0], "multi-account works!")
// chainMaker.applyPendingTransactions(663251)
// await(targetHeight = 663251)
// }
// }
// }
//
// // private fun sendToMyHomies() {
// // twig("uno")
// // val rustPoc = LightWalletGrpcService(localChannel)
// // twig("dos")
// // val pong: Int = rustPoc.getLatestBlockHeight()
// // twig("tres")
// // assertEquals(800000, pong)
// // }
//
//
// private fun sendToMyHomies0() {
// val rustPoc = LocalWalletGrpcService(localChannel)
// val pong: Service.PingResponse = rustPoc.sendMoney(Service.PingResponse.newBuilder().setEntry(10).setEntry(11).build())
// assertEquals(pong.entry, 12)
// }
//
// object localChannel : ManagedChannel() {
// private var _isShutdown = false
// get() {
// twig("zyz: returning _isShutdown")
// return field
// }
// private var _isTerminated = false
// get() {
// twig("zyz: returning _isTerminated")
// return field
// }
//
// override fun <RequestT : Any?, ResponseT : Any?> newCall(
// methodDescriptor: MethodDescriptor<RequestT, ResponseT>?,
// callOptions: CallOptions?
// ): ClientCall<RequestT, ResponseT> {
// twig("zyz: newCall")
// return LocalCall()
// }
//
// override fun isTerminated() = _isTerminated
//
// override fun authority(): String {
// twig("zyz: authority")
// return "none"
// }
//
// override fun shutdown(): ManagedChannel {
// twig("zyz: shutdown")
// _isShutdown = true
// return this
// }
//
// override fun isShutdown() = _isShutdown
//
// override fun shutdownNow() = shutdown()
//
// override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean {
// twig("zyz: awaitTermination")
// _isTerminated = true
// return _isTerminated
// }
// }
//
// class LocalCall<RequestT, ResponseT> : ClientCall<RequestT, ResponseT>() {
// override fun sendMessage(message: RequestT) {
// twig("zyz: sendMessage: $message")
// }
//
// override fun halfClose() {
// twig("zyz: halfClose")
// }
//
// override fun start(responseListener: Listener<ResponseT>?, headers: Metadata?) {
// twig("zyz: start")
// responseListener?.onMessage(Service.BlockID.newBuilder().setHeight(800000).build() as? ResponseT)
// responseListener?.onClose(Status.OK, headers)
// }
//
// override fun cancel(message: String?, cause: Throwable?) {
// twig("zyz: cancel: $message caused by $cause")
// }
//
// override fun request(numMessages: Int) {
// twig("zyz: request $numMessages")
// }
// }
//
// private fun sendToMyHomies1() = runBlocking {
// with(sithLord) {
// twigTask("Sending funds") {
// // createAndSubmitTx(200_000, addresses[0], "multi-account works!")
// chainMaker.applyPendingTransactions(663251)
// await(targetHeight = 663251)
// }
// }
// }
//
// companion object {
// private val sithLord = DarksideTestCoordinator(, "MultiRecipientInRust")
//
// private val randomPhrases = listOf(
// "profit save black expose rude feature early rocket alter borrow finish october few duty flush kick spell bean burden enforce bitter theme silent uphold",
// "unit ice dial annual duty feature smoke expose hard joy globe just accuse inner fog cash neutral forum strategy crash subject hurdle lecture sand",
// "average talent frozen work brand output major soldier witness keen brown bind indicate burden furnace long crime joke inhale chronic ordinary renew boat flame",
// "echo viable panic unaware stay magnet cake museum yellow abandon mountain height lunch advance tongue market bamboo cushion okay morning minute icon obtain december",
// "renew enlist travel stand trust execute decade surge follow push student school focus woman ripple movie that bitter plug same index wife spread differ"
// )
//
// private val viewingKeys = listOf(
// "zxviews1qws7ryw7qqqqpqq77dmhl9tufzdsgy8hcjq8kxjtgkfwwgqn4a26ahmhmjqueptd2pmq3f73pm8uaa25aze5032qw4dppkx4l625xcjcm94d5e65fcq4j2uptnjuqpyu2rvud88dtjwseglgzfe5l4te2xw62yq4tv62d2f6kl4706c6dmfxg2cmsdlzlt9ykpvacaterq4alljr3efke7k46xcrg4pxc02ezj0txwqjjve23nqqp7t5n5qat4d8569krxgkcd852uqg2t2vn",
// "zxviews1qdtp7dwfqqqqpqq3zxegnzc6qtacjp4m6qhyz7typdw9h9smra3rn322dkhyfg8kktk66k7zaj9tt5j6e58enx89pwry4rxwmcuzqyxlsap965r5gxpt604chmjyuhder6xwu3tx0h608as5sgxapqdqa6v6hy6qzh9fft0ns3cj9f8zrhu0ukzf9gn2arr02kzdct0jh5ee3zjch3xscjv34pzkgpueuq0pyl706alssuchqu4jmjm22fcq3htlwxt3f3hdytne7mgscrz5m",
// "zxviews1qvfmgpzjqqqqpqqnpl2s9n774mrv72zsuw73km9x6ax2s26d0d0ua20nuxvkexa4lq5fsc6psl8csspyqrlwfeuele5crlwpyjufgkzyy6ffw8hc52hn04jzru6mntms8c2cm255gu200zx4pmz06k3s90jatwehazl465tf6uyj6whwarpcca9exzr7wzltelq5tusn3x3jchjyk6cj09xyctjzykp902w4x23zdsf46d3fn9rtkgm0rmek296c5nhuzf99a2x6umqr804k9",
// "zxviews1qv85jn3hqqqqpq9jam3g232ylvvhy8e5vdhp0x9zjppr49sw6awwrm3a3d8l9j9es2ed9h29r6ta5tzt53j2y0ex84lzns0thp7n9wzutjapq29chfewqz34q5g6545f8jf0e69jcg9eyv66s8pt3y5dwxg9nrezz8q9j9fwxryeleayay6m09zpt0dem8hkazlw5jk6gedrakp9z7wzq2ptf6aqkft6z02mtrnq4a5pguwp4m8xkh52wz0r3naeycnqllnvsn8ag5q73pqgd",
// "zxviews1qwhel8pxqqqqpqxjl3cqu2z8hu0tqdd5qchkrdtsjuce9egdqlpu7eff2rn3gknm0msw7ug6qp4ynppscvv6hfm2nkf42lhz8la5et3zsej84xafcn0xdd9ms452hfjp4tljshtffscsl68wgdv3j5nnelxsdcle5rnwkuz6lvvpqs7s2x0cnhemhnwzhx5ccakfgxfym0w8dxglq4h6pwukf2az6lcm38346qc5s9rgx6s988fr0kxnqg0c6g6zlxa2wpc7jh0gz7q4ysx0l"
// )
// private val spendingKeys = listOf(
// "secret-extended-key-main1qws7ryw7qqqqpqq77dmhl9tufzdsgy8hcjq8kxjtgkfwwgqn4a26ahmhmjqueptd2pt49qhm63lt8v93tlqzw7psmkvqqfm6xdnc2qwkflfcenqs7s4sj2yn0c75n982wjrf5k5h37vt3wxwr3pqnjk426lltctrms2uqmqgkl4706c6dmfxg2cmsdlzlt9ykpvacaterq4alljr3efke7k46xcrg4pxc02ezj0txwqjjve23nqqp7t5n5qat4d8569krxgkcd852uqxj5ljt",
// "secret-extended-key-main1qdtp7dwfqqqqpqq3zxegnzc6qtacjp4m6qhyz7typdw9h9smra3rn322dkhyfg8kk26p0fcjuklryw0ed6falf6c7dwqehleca0xf6m6tlnv5zdjx7lqs4xmseqjz0fvk273aczatxxjaqmy3kv8wtzcc6pf6qtrjy5g2mqgs3cj9f8zrhu0ukzf9gn2arr02kzdct0jh5ee3zjch3xscjv34pzkgpueuq0pyl706alssuchqu4jmjm22fcq3htlwxt3f3hdytne7mgacmaq6",
// "secret-extended-key-main1qvfmgpzjqqqqpqqnpl2s9n774mrv72zsuw73km9x6ax2s26d0d0ua20nuxvkexa4lzc4n8a3zfvyn2qns37fx00avdtjewghmxz5nc2ey738nrpu4pqqnwysmcls5yek94lf03d5jtsa25nmuln4xjvu6e4g0yrr6xesp9cr6uyj6whwarpcca9exzr7wzltelq5tusn3x3jchjyk6cj09xyctjzykp902w4x23zdsf46d3fn9rtkgm0rmek296c5nhuzf99a2x6umqvf4man",
// "secret-extended-key-main1qv85jn3hqqqqpq9jam3g232ylvvhy8e5vdhp0x9zjppr49sw6awwrm3a3d8l9j9estq9a548lguf0n9fsjs7c96uaymhysuzeek5eg8un0fk8umxszxstm0xfq77x68yjk4t4j7h2xqqjf8nmkx0va3cphnhxpvd0l5dhzgyxryeleayay6m09zpt0dem8hkazlw5jk6gedrakp9z7wzq2ptf6aqkft6z02mtrnq4a5pguwp4m8xkh52wz0r3naeycnqllnvsn8ag5qru36vk",
// "secret-extended-key-main1qwhel8pxqqqqpqxjl3cqu2z8hu0tqdd5qchkrdtsjuce9egdqlpu7eff2rn3gknm0mdwr9358t3dlcf47vakdwewxy64k7ds7y3k455rfch7s2x8mfesjsxptyfvc9heme3zj08wwdk4l9mwce92lvrl797wmmddt65ygwcqlvvpqs7s2x0cnhemhnwzhx5ccakfgxfym0w8dxglq4h6pwukf2az6lcm38346qc5s9rgx6s988fr0kxnqg0c6g6zlxa2wpc7jh0gz7qx7zl33"
// )
// private val addresses = listOf(
// "zs1d8lenyz7uznnna6ttmj6rk9l266989f78c3d79f0r6r28hn0gc9fzdktrdnngpcj8wr2cd4zcq2",
// "zs13x79khp5z0ydgnfue8p88fjnrjxtnz0gwxyef525gd77p72nqh7zr447n6klgr5yexzp64nc7hf",
// "zs1jgvqpsyzs90hlqz85qry3zv52keejgx0f4pnljes8h4zs96zcxldu9llc03dvhkp6ds67l4s0d5",
// "zs1lr428hhedq3yk8n2wr378e6ua3u3r4ma5a8dqmf3r64y96vww5vh6327jfudtyt7v3eqw22c2t6",
// "zs1hy7mdwl6y0hwxts6a5lca2xzlr0p8v5tkvvz7jfa4d04lx5uedg6ya8fmthywujacx0acvfn837"
// )
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord.enterTheDarkside()
// sithLord.chainMaker.makeSimpleChain()
// sithLord.startSync(classScope).await()
// }
// }
// }

View File

@@ -0,0 +1,96 @@
package cash.z.ecc.android.sdk.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import org.junit.Before
// import org.junit.BeforeClass
// import org.junit.Test
//
// class OutboundTransactionsTest : ScopedTest() {
//
// @Before
// fun beforeEachTest() {
// testCoordinator.clearUnminedTransactions()
// }
//
// @Test
// fun testSendIncrementsTransaction() {
// validator.validateTransactionCount(initialTxCount)
// testCoordinator.sendTransaction(txAmount).awaitSync()
// validator.validatTransactionCount(initialTxCount + 1)
// }
//
// @Test
// fun testSendReducesBalance() {
// validator.validateBalance(initialBalance)
// testCoordinator.sendTransaction(txAmount).awaitSync()
// validator.validateBalanceLessThan(initialBalance)
// }
//
// @Test
// fun testTransactionPending() {
// testCoordinator.sendTransaction(txAmount).awaitSync()
// validator.validateTransactionPending(testCoordinator.lastTransactionId)
// }
//
// @Test
// fun testTransactionConfirmations_1() {
// testCoordinator.sendTransaction(txAmount).generateNextBlock().awaitSync()
// validator.validateConfirmations(testCoordinator.lastTransactionId, 1)
// validator.validateBalanceLessThan(initialBalance - txAmount)
// }
//
// @Test
// fun testTransactionConfirmations_9() {
// testCoordinator.sendTransaction(txAmount).generateNextBlock().advanceBlocksBy(8).awaitSync()
// validator.validateConfirmations(testCoordinator.lastTransactionId, 9)
// validator.validateBalanceLessThan(initialBalance - txAmount)
// }
//
// @Test
// fun testTransactionConfirmations_10() {
// testCoordinator.sendTransaction(txAmount).generateNextBlock().advanceBlocksBy(9).awaitSync()
// validator.validateConfirmations(testCoordinator.lastTransactionId, 10)
// validator.validateBalance(initialBalance - txAmount)
// }
//
// @Test
// fun testTransactionExpiration() {
// validator.validateBalance(initialBalance)
//
// // pending initially
// testCoordinator.sendTransaction(txAmount).awaitSync()
// val id = testCoordinator.lastTransactionId
// validator.validateTransactionPending(id)
//
// // still pending after 9 blocks
// testCoordinator.advanceBlocksBy(9).awaitSync()
// validator.validateTransactionPending(id)
// validator.validateBalanceLessThan(initialBalance)
//
// // expired after 10 blocks
// testCoordinator.advanceBlocksBy(1).awaitSync()
// validator.validateTransactionExpired(id)
//
// validator.validateBalance(initialBalance)
// }
//
//
//
// companion object {
// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
// private const val initialBalance = 1.234
// private const val txAmount = 1.1
// private const val initialTxCount = 3
// private val testCoordinator = DarksideTestCoordinator()
// private val validator = testCoordinator.validator
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// testCoordinator
// .enterTheDarkside()
// .resetBlocks(blocksUrl)
// }
// }
// }

View File

@@ -0,0 +1,25 @@
package cash.z.ecc.android.sdk.darkside
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTest
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
/**
* Integration test to run in order to catch any regressions in transparent behavior.
*/
@RunWith(AndroidJUnit4::class)
class TransparentIntegrationTest : DarksideTest() {
@Before
fun setup() = runOnce {
sithLord.await()
}
@Test
@Ignore("This test is broken")
fun sanityTest() {
validator.validateTxCount(5)
}
}

View File

@@ -0,0 +1,102 @@
package cash.z.ecc.android.sdk.darkside.reorgs
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import cash.z.ecc.android.sdk.internal.twig
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class InboundTxTests : ScopedTest() {
@Test
fun testTargetBlock_downloaded() {
validator.validateMinHeightDownloaded(firstBlock)
}
@Test
fun testTargetBlock_scanned() {
validator.validateMinHeightScanned(targetTxBlock - 1)
}
@Test
fun testLatestHeight() {
validator.validateLatestHeight(targetTxBlock - 1)
}
@Test
fun testTxCountInitial() {
validator.validateTxCount(0)
}
@Test
fun testTxCountAfter() {
twig("ADDING TRANSACTIONS!!!")
// add 2 transactions to block 663188 and 'mine' that block
addTransactions(targetTxBlock, tx663174, tx663188)
sithLord.await(timeout = 30_000L, targetHeight = targetTxBlock)
validator.validateTxCount(2)
}
private fun addTransactions(targetHeight: Int, vararg txs: String) {
val overwriteBlockCount = 5
chainMaker
// .stageEmptyBlocks(targetHeight, overwriteBlockCount)
.stageTransactions(targetHeight, *txs)
.applyTipHeight(targetHeight)
}
companion object {
private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val tx663174 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt"
private const val tx663188 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt"
private const val txIndexReorg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-index-reorg/t1.txt"
private val txSend = 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"
)
private val txRecv = 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"
)
private const val firstBlock = 663150
private const val targetTxBlock = 663188
private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
private val chainMaker = sithLord.chainMaker
@BeforeClass
@JvmStatic
fun startAllTests() {
sithLord.enterTheDarkside()
chainMaker
.resetBlocks(blocksUrl, startHeight = firstBlock, tipHeight = targetTxBlock)
.stageEmptyBlocks(firstBlock + 1, 100)
.applyTipHeight(targetTxBlock - 1)
sithLord.synchronizer.start(classScope)
sithLord.await()
}
}
}

View File

@@ -0,0 +1,53 @@
package cash.z.ecc.android.sdk.darkside.reorgs // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import org.junit.Assert.assertFalse
// import org.junit.Assert.assertTrue
// import org.junit.BeforeClass
// import org.junit.Test
//
// class ReorgBasicTest : ScopedTest() {
//
// private var callbackTriggered = false
//
// @Test
// fun testReorgChangesBlockHash() {
// testCoordinator.resetBlocks(blocksUrl)
// validator.validateBlockHash(targetHeight, targetHash)
// testCoordinator.updateBlocks(reorgUrl)
// validator.validateBlockHash(targetHeight, reorgHash)
// }
//
// @Test
// fun testReorgTriggersCallback() {
// callbackTriggered = false
// testCoordinator.resetBlocks(blocksUrl)
// testCoordinator.synchronizer.registerReorgListener(reorgCallback)
// assertFalse(callbackTriggered)
//
// testCoordinator.updateBlocks(reorgUrl).awaitSync()
// assertTrue(callbackTriggered)
// testCoordinator.synchronizer.unregisterReorgListener()
// }
//
// fun reorgCallback() {
// callbackTriggered = true
// }
//
// companion object {
// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
// private const val reorgUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"
// private const val targetHeight = 663250
// private const val targetHash = "tbd"
// private const val reorgHash = "tbd"
// private val testCoordinator = DarksideTestCoordinator()
// private val validator = testCoordinator.validator
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// testCoordinator.enterTheDarkside()
// }
// }
// }

View File

@@ -0,0 +1,239 @@
package cash.z.ecc.android.sdk.darkside.reorgs // package cash.z.ecc.android.sdk.integration
//
// 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.test.ScopedTest
// import cash.z.ecc.android.sdk.ext.import
// import cash.z.ecc.android.sdk.internal.twig
// import cash.z.ecc.android.sdk.darkside.test.DarksideApi
// import io.grpc.StatusRuntimeException
// import kotlinx.coroutines.delay
// import kotlinx.coroutines.flow.filter
// import kotlinx.coroutines.flow.first
// import kotlinx.coroutines.flow.onEach
// import kotlinx.coroutines.runBlocking
// import org.junit.Assert.*
// import org.junit.Before
// import org.junit.BeforeClass
// import org.junit.Test
//
// class ReorgHandlingTest : ScopedTest() {
//
// @Before
// fun setup() {
// timeout(30_000L) {
// synchronizer.awaitSync()
// }
// }
//
// @Test
// fun testBeforeReorg_minHeight() = timeout(30_000L) {
// // validate that we are synced, at least to the birthday height
// synchronizer.validateMinSyncHeight(birthdayHeight)
// }
//
// @Test
// fun testBeforeReorg_maxHeight() = timeout(30_000L) {
// // validate that we are not synced beyond the target height
// synchronizer.validateMaxSyncHeight(targetHeight)
// }
//
// @Test
// fun testBeforeReorg_latestBlockHash() = timeout(30_000L) {
// val latestBlock = getBlock(targetHeight)
// assertEquals("foo", latestBlock.header.toStringUtf8())
// }
//
// @Test
// fun testAfterSmallReorg_callbackTriggered() = timeout(30_000L) {
// hadReorg = false
// triggerSmallReorg()
// assertTrue(hadReorg)
// }
//
// @Test
// fun testAfterSmallReorg_callbackTriggered() = timeout(30_000L) {
// hadReorg = false
// triggerSmallReorg()
// assertTrue(hadReorg)
// }
// // @Test
// // fun testSync_100Blocks()= timeout(10_000L) {
// // // validate that we are synced below the target height, at first
// // synchronizer.validateMaxSyncHeight(targetHeight - 1)
// // // then trigger and await more blocks
// // synchronizer.awaitHeight(targetHeight)
// // // validate that we are above the target height afterward
// // synchronizer.validateMinSyncHeight(targetHeight)
// // }
//
// private fun Synchronizer.awaitSync() = runBlocking<Unit> {
// twig("*** Waiting for sync ***")
// status.onEach {
// twig("got processor status $it")
// assertTrue("Error: Cannot complete test because the server is disconnected.", it != Synchronizer.Status.DISCONNECTED)
// delay(1000)
// }.filter { it == Synchronizer.Status.SYNCED }.first()
// twig("*** Done waiting for sync! ***")
// }
//
// private fun Synchronizer.awaitHeight(height: Int) = runBlocking<Unit> {
// twig("*** Waiting for block $height ***")
// // processorInfo.first { it.lastScannedHeight >= height }
// processorInfo.onEach {
// twig("got processor info $it")
// delay(1000)
// }.first { it.lastScannedHeight >= height }
// twig("*** Done waiting for block $height! ***")
// }
//
// private fun Synchronizer.validateMinSyncHeight(minHeight: Int) = runBlocking<Unit> {
// val info = processorInfo.first()
// val lastDownloadedHeight = info.lastDownloadedHeight
// assertTrue("Expected to be synced beyond $minHeight but the last downloaded block was" +
// " $lastDownloadedHeight details: $info", lastDownloadedHeight >= minHeight)
// }
//
// private fun Synchronizer.validateMaxSyncHeight(maxHeight: Int) = runBlocking<Unit> {
// val lastDownloadedHeight = processorInfo.first().lastScannedHeight
// assertTrue("Did not expect to be synced beyond $maxHeight but we are synced to" +
// " $lastDownloadedHeight", lastDownloadedHeight <= maxHeight)
// }
//
// private fun getBlock(height: Int) =
// lightwalletd.getBlockRange(height..height).first()
//
// private val lightwalletd
// get() = (synchronizer as SdkSynchronizer).processor.downloader.lightwalletService
//
// companion object {
// private const val port = 9067
// private const val birthdayHeight = 663150
// private const val targetHeight = 663200
// 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"
// private val context = InstrumentationRegistry.getInstrumentation().context
// private val initializer = Initializer(context, host, port, "ReorgHandlingTests")
// private lateinit var synchronizer: Synchronizer
// private lateinit var sithLord: DarksideApi
//
// @BeforeClass
// @JvmStatic
// fun startOnce() {
//
// sithLord = DarksideApi(context, host, port)
// enterTheDarkside()
//
// // don't start until after we enter the darkside (otherwise the we find no blocks to begin with and sleep for an interval)
// synchronizer.start(classScope)
// }
//
// private fun enterTheDarkside() = runBlocking<Unit> {
// // verify that we are on the darkside
// try {
// twig("entering the darkside")
// var info = synchronizer.getServerInfo()
// assertTrue(
// "Error: not on the darkside",
// info.chainName.contains("darkside")
// or info.vendor.toLowerCase().contains("darkside", true)
// )
// twig("initiating the darkside")
// sithLord.initiate(birthdayHeight + 10)
// info = synchronizer.getServerInfo()
// assertTrue(
// "Error: server not configured for the darkside. Expected initial height of" +
// " $birthdayHeight but found ${info.blockHeight}", birthdayHeight <= info.blockHeight)
// twig("darkside initiation complete!")
// } catch (error: StatusRuntimeException) {
// fail("Error while fetching server status. Testing cannot begin due to:" +
// " ${error.message}. Verify that the server is running")
// }
// }
// }
// /*
//
// beginning to process new blocks (with lower bound: 663050)...
// downloading blocks in range 663202..663202
// found 1 missing blocks, downloading in 1 batches of 100...
// downloaded 663202..663202 (batch 1 of 1) [663202..663202] | 10ms
// validating blocks in range 663202..663202 in db: /data/user/0/cash.z.ecc.android.sdk.test/databases/ReorgTest22_Cache.db
// offset = min(100, 10 * (1)) = 10
// lowerBound = max(663201 - 10, 663050) = 663191
// handling chain error at 663201 by rewinding to block 663191
// chain error detected at height: 663201. Rewinding to: 663191
// beginning to process new blocks (with lower bound: 663050)...
// downloading blocks in range 663192..663202
// found 11 missing blocks, downloading in 1 batches of 100...
// downloaded 663192..663202 (batch 1 of 1) [663192..663202] | 8ms
// validating blocks in range 663192..663202 in db: /data/user/0/cash.z.ecc.android.sdk.test/databases/ReorgTest22_Cache.db
// offset = min(100, 10 * (2)) = 20
// lowerBound = max(663191 - 20, 663050) = 663171
// handling chain error at 663191 by rewinding to block 663171
// chain error detected at height: 663191. Rewinding to: 663171
// beginning to process new blocks (with lower bound: 663050)...
// downloading blocks in range 663172..663202
// found 31 missing blocks, downloading in 1 batches of 100...
// downloaded 663172..663202 (batch 1 of 1) [663172..663202] | 15ms
// validating blocks in range 663172..663202 in db: /data/user/0/cash.z.ecc.android.sdk.test/databases/ReorgTest22_Cache.db
// scanning blocks for range 663172..663202 in batches
// batch scanned: 663202/663202
// batch scan complete!
// Successfully processed new blocks. Sleeping for 20000ms
//
// */
// //
// // @Test
// // fun testHeightChange() {
// // setTargetHeight(targetHeight)
// // synchronizer.validateSyncedTo(targetHeight)
// // }
// //
// // @Test
// // fun testSmallReorgSync() {
// // verifyReorgSync(smallReorgSize)
// // }
// //
// // @Test
// // fun testSmallReorgCallback() {
// // verifyReorgCallback(smallReorgSize)
// // }
// //
// // @Test
// // fun testLargeReorgSync() {
// // verifyReorgSync(largeReorgSize)
// // }
// //
// // @Test
// // fun testLargeReorgCallback() {
// // verifyReorgCallback(largeReorgSize)
// // }
// //
// //
// // //
// // // Helper Functions
// // //
// //
// // fun verifyReorgSync(reorgSize: Int) {
// // setTargetHeight(targetHeight)
// // synchronizer.validateSyncedTo(targetHeight)
// // getHash(targetHeight).let { initialHash ->
// // setReorgHeight(targetHeight - reorgSize)
// // synchronizer.validateSyncedTo(targetHeight)
// // assertNotEquals("Hash should change after a reorg", initialHash, getHash(targetHeight))
// // }
// // }
// //
// // fun verifyReorgCallback(reorgSize: Int) {
// // setTargetHeight(targetHeight)
// // synchronizer.validateSyncedTo(targetHeight)
// // getHash(targetHeight).let { initialHash ->
// // setReorgHeight(targetHeight - 10)
// // synchronizer.validateReorgCallback()
// // }
// // }
//
//
// }
//

View File

@@ -0,0 +1,46 @@
package cash.z.ecc.android.sdk.darkside.reorgs
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReorgSetupTest : ScopedTest() {
private val birthdayHeight = 663150
private val targetHeight = 663250
@Before
fun setup() {
sithLord.await()
}
@Test
fun testBeforeReorg_minHeight() = timeout(30_000L) {
// validate that we are synced, at least to the birthday height
validator.validateMinHeightDownloaded(birthdayHeight)
}
@Test
fun testBeforeReorg_maxHeight() = timeout(30_000L) {
// validate that we are not synced beyond the target height
validator.validateMaxHeightScanned(targetHeight)
}
companion object {
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
@BeforeClass
@JvmStatic
fun startOnce() {
sithLord.enterTheDarkside()
sithLord.synchronizer.start(classScope)
}
}
}

View File

@@ -0,0 +1,61 @@
package cash.z.ecc.android.sdk.darkside.reorgs
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import cash.z.ecc.android.sdk.internal.twig
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReorgSmallTest : ScopedTest() {
private val targetHeight = 663250
private val hashBeforeReorg = "09ec0d5de30d290bc5a2318fbf6a2427a81c7db4790ce0e341a96aeac77108b9"
private val hashAfterReorg = "tbd"
@Before
fun setup() {
sithLord.await()
}
@Test
fun testBeforeReorg_latestBlockHash() = timeout(30_000L) {
validator.validateBlockHash(targetHeight, hashBeforeReorg)
}
@Test
fun testAfterReorg_callbackTriggered() = timeout(30_000L) {
hadReorg = false
// sithLord.triggerSmallReorg()
sithLord.await()
twig("checking whether a reorg happened (spoiler: ${if (hadReorg) "yep" else "nope"})")
assertTrue(hadReorg)
}
@Test
fun testAfterReorg_latestBlockHash() = timeout(30_000L) {
validator.validateBlockHash(targetHeight, hashAfterReorg)
}
companion object {
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
private var hadReorg = false
@BeforeClass
@JvmStatic
fun startOnce() {
sithLord.enterTheDarkside()
validator.onReorg { _, _ ->
hadReorg = true
}
sithLord.synchronizer.start(classScope)
}
}
}

View File

@@ -0,0 +1,75 @@
package cash.z.ecc.android.sdk.darkside.reorgs
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import cash.z.ecc.android.sdk.darkside.test.SimpleMnemonics
import cash.z.ecc.android.sdk.ext.toHex
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SetupTest : ScopedTest() {
// @Test
// fun testFirstBlockExists() {
// validator.validateHasBlock(
// firstBlock
// )
// }
//
// @Test
// fun testLastBlockExists() {
// validator.validateHasBlock(
// lastBlock
// )
// }
//
// @Test
// fun testLastBlockHash() {
// validator.validateBlockHash(
// lastBlock,
// lastBlockHash
// )
// }
@Test
@Ignore("This test is broken")
fun tempTest() {
val 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"
val result = SimpleMnemonics().toSeed(phrase.toCharArray()).toHex()
assertEquals("abc", result)
}
@Test
@Ignore("This test is broken")
fun tempTest2() {
val s = SimpleMnemonics()
val ent = s.nextEntropy()
val phrase = s.nextMnemonic(ent)
assertEquals("a", "${ent.toHex()}|${String(phrase)}")
}
companion object {
private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val firstBlock = 663150
private const val lastBlock = 663200
private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord
// .enterTheDarkside()
// // TODO: fix this
// // .resetBlocks(blocksUrl, startHeight = firstBlock, tipHeight = lastBlock)
// .startSync(classScope)
// .await()
// }
}
}

View File

@@ -0,0 +1,35 @@
package cash.z.ecc.android.sdk.darkside.reproduce
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTest
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReproduceZ2TFailureTest : DarksideTest() {
@Before
fun setup() {
println("dBUG RUNNING")
}
@Test
@Ignore("This test is broken")
fun once() {
}
@Test
@Ignore("This test is broken")
fun twice() {
}
companion object {
@JvmStatic
@BeforeClass
fun beforeAll() {
println("dBUG BEFOERE IOT ALL")
}
}
}

View File

@@ -0,0 +1,177 @@
package cash.z.ecc.android.sdk.darkside.test
import android.content.Context
import cash.z.ecc.android.sdk.R
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 cash.z.wallet.sdk.rpc.Darkside
import cash.z.wallet.sdk.rpc.Darkside.DarksideTransactionsURL
import cash.z.wallet.sdk.rpc.DarksideStreamerGrpc
import cash.z.wallet.sdk.rpc.Service
import io.grpc.ManagedChannel
import io.grpc.stub.StreamObserver
import java.lang.RuntimeException
import java.util.concurrent.TimeUnit
import kotlin.random.Random
class DarksideApi(
private val channel: ManagedChannel,
private val singleRequestTimeoutSec: Long = 10L
) {
constructor(
appContext: Context,
host: String,
port: Int = ZcashNetwork.Mainnet.defaultPort,
usePlainText: Boolean = appContext.resources.getBoolean(
R.bool.lightwalletd_allow_very_insecure_connections
)
) : this(
LightWalletGrpcService.createDefaultChannel(
appContext,
host,
port,
usePlainText
)
)
//
// Service APIs
//
fun reset(
saplingActivationHeight: Int = 419200,
branchId: String = "e9ff75a6", // Canopy,
chainName: String = "darkside${ZcashNetwork.Mainnet.networkName}"
) = apply {
twig("resetting darksidewalletd with saplingActivation=$saplingActivationHeight branchId=$branchId chainName=$chainName")
Darkside.DarksideMetaState.newBuilder()
.setBranchID(branchId)
.setChainName(chainName)
.setSaplingActivation(saplingActivationHeight)
.build().let { request ->
createStub().reset(request)
}
}
fun stageBlocks(url: String) = apply {
twig("staging blocks url=$url")
createStub().stageBlocks(url.toUrl())
}
fun stageTransactions(url: String, targetHeight: Int) = apply {
twig("staging transaction at height=$targetHeight from url=$url")
createStub().stageTransactions(
DarksideTransactionsURL.newBuilder().setHeight(targetHeight).setUrl(url).build()
)
}
fun stageEmptyBlocks(startHeight: Int, count: Int = 10, nonce: Int = Random.nextInt()) = apply {
twig("staging $count empty blocks starting at $startHeight with nonce $nonce")
createStub().stageBlocksCreate(
Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight).setCount(count).setNonce(nonce).build()
)
}
fun stageTransactions(txs: Iterator<Service.RawTransaction>?, tipHeight: Int) {
if (txs == null) {
twig("no transactions to stage")
return
}
twig("staging transaction at height=$tipHeight")
val response = EmptyResponse()
createStreamingStub().stageTransactionsStream(response).apply {
txs.forEach {
twig("stageTransactions: onNext calling!!!")
onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.toLong()).build()) // apply the tipHeight because the passed in txs might not know their destination height (if they were created via SendTransaction)
twig("stageTransactions: onNext called")
}
twig("stageTransactions: onCompleted calling!!!")
onCompleted()
twig("stageTransactions: onCompleted called")
}
response.await()
}
fun applyBlocks(tipHeight: Int) {
twig("applying blocks up to tipHeight=$tipHeight")
createStub().applyStaged(tipHeight.toHeight())
}
fun getSentTransactions(): MutableIterator<Service.RawTransaction>? {
twig("grabbing sent transactions...")
return createStub().getIncomingTransactions(Service.Empty.newBuilder().build())
}
// fun setMetaState(
// branchId: String = "2bb40e60", // Blossom,
// chainName: String = "darkside",
// saplingActivationHeight: Int = 419200
// ): DarksideApi = apply {
// createStub().setMetaState(
// Darkside.DarksideMetaState.newBuilder()
// .setBranchID(branchId)
// .setChainName(chainName)
// .setSaplingActivation(saplingActivationHeight)
// .build()
// )
// }
// fun setLatestHeight(latestHeight: Int) = setState(latestHeight, reorgHeight)
//
// fun setReorgHeight(reorgHeight: Int)
// = setState(latestHeight.coerceAtLeast(reorgHeight), reorgHeight)
//
// fun setState(latestHeight: Int = -1, reorgHeight: Int = latestHeight): DarksideApi {
// this.latestHeight = latestHeight
// this.reorgHeight = reorgHeight
// // TODO: change this service to accept ints as heights, like everywhere else
// createStub().darksideSetState(
// Darkside.DarksideState.newBuilder()
// .setLatestHeight(latestHeight.toLong())
// .setReorgHeight(reorgHeight.toLong())
// .build()
// )
// return this
// }
private fun createStub(): DarksideStreamerGrpc.DarksideStreamerBlockingStub =
DarksideStreamerGrpc
.newBlockingStub(channel)
.withDeadlineAfter(singleRequestTimeoutSec, TimeUnit.SECONDS)
private fun createStreamingStub(): DarksideStreamerGrpc.DarksideStreamerStub =
DarksideStreamerGrpc
.newStub(channel)
.withDeadlineAfter(singleRequestTimeoutSec, TimeUnit.SECONDS)
private fun String.toUrl() = Darkside.DarksideBlocksURL.newBuilder().setUrl(this).build()
private fun Int.toHeight() = Darkside.DarksideHeight.newBuilder().setHeight(this).build()
class EmptyResponse : StreamObserver<Service.Empty> {
var completed = false
var error: Throwable? = null
override fun onNext(value: Service.Empty?) {
twig("<><><><><><><><> EMPTY RESPONSE: ONNEXT CALLED!!!!")
}
override fun onError(t: Throwable?) {
twig("<><><><><><><><> EMPTY RESPONSE: ONERROR CALLED!!!!")
error = t
completed = true
}
override fun onCompleted() {
twig("<><><><><><><><> EMPTY RESPONSE: ONCOMPLETED CALLED!!!")
completed = true
}
fun await() {
while (!completed) {
twig("awaiting server response...")
Thread.sleep(20L)
}
if (error != null) throw RuntimeException("Server responded with an error: $error caused by ${error?.cause}")
}
}
}

View File

@@ -0,0 +1,18 @@
package cash.z.ecc.android.sdk.darkside.test
open class DarksideTest(name: String = javaClass.simpleName) : ScopedTest() {
val sithLord = DarksideTestCoordinator()
val validator = sithLord.validator
fun runOnce(block: () -> Unit) {
if (!ranOnce) {
sithLord.enterTheDarkside()
sithLord.synchronizer.start(classScope)
block()
ranOnce = true
}
}
companion object {
private var ranOnce = false
}
}

View File

@@ -0,0 +1,311 @@
package cash.z.ecc.android.sdk.darkside.test
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.type.ZcashNetwork
import io.grpc.StatusRuntimeException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
class DarksideTestCoordinator(val wallet: TestWallet) {
constructor(
alias: String = "DarksideTestCoordinator",
seedPhrase: String = DEFAULT_SEED_PHRASE,
startHeight: Int = DEFAULT_START_HEIGHT,
host: String = COMPUTER_LOCALHOST,
network: ZcashNetwork = ZcashNetwork.Mainnet,
port: Int = network.defaultPort
) : this(TestWallet(seedPhrase, alias, network, host, startHeight = startHeight, port = port))
private val targetHeight = 663250
private val context = InstrumentationRegistry.getInstrumentation().context
// dependencies: private
private lateinit var darkside: DarksideApi
// dependencies: public
val validator = DarksideTestValidator()
val chainMaker = DarksideChainMaker()
// wallet delegates
val synchronizer get() = wallet.synchronizer
val send get() = wallet::send
//
// High-level APIs
//
/**
* Setup dependencies, including the synchronizer and the darkside API connection
*/
fun enterTheDarkside(): DarksideTestCoordinator = runBlocking {
// verify that we are on the darkside
try {
twig("entering the darkside")
initiate()
synchronizer.getServerInfo().apply {
assertTrue(
"Error: not on the darkside",
vendor.contains("dark", true)
or chainName.contains("dark", true)
)
}
twig("darkside initiation complete!")
} catch (error: StatusRuntimeException) {
Assert.fail(
"Error while fetching server status. Testing cannot begin due to:" +
" ${error.message} Caused by: ${error.cause} Verify that the server is running!"
)
}
this@DarksideTestCoordinator
}
/**
* Setup the synchronizer and darksidewalletd with their initial state
*/
fun initiate() {
twig("*************** INITIALIZING TEST COORDINATOR (ONLY ONCE) ***********************")
val channel = synchronizer.channel
darkside = DarksideApi(channel)
darkside.reset()
}
// fun triggerSmallReorg() {
// darkside.setBlocksUrl(smallReorg)
// }
//
// fun triggerLargeReorg() {
// darkside.setBlocksUrl(largeReorg)
// }
// redo this as a call to wallet but add delay time to wallet join() function
/**
* Waits for, at most, the given amount of time for the synchronizer to download and scan blocks
* and reach a 'SYNCED' status.
*/
fun await(timeout: Long = 60_000L, targetHeight: Int = -1) = runBlocking {
ScopedTest.timeoutWith(this, timeout) {
twig("*** Waiting up to ${timeout / 1_000}s for sync ***")
synchronizer.status.onEach {
twig("got processor status $it")
if (it == Synchronizer.Status.DISCONNECTED) {
twig("waiting a bit before giving up on connection...")
} else if (targetHeight != -1 && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
twig("awaiting new blocks from server...")
}
}.map {
// whenever we're waiting for a target height, for simplicity, if we're sleeping,
// and in between polls, then consider it that we're not synced
if (targetHeight != -1 && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
twig("switching status to DOWNLOADING because we're still waiting for height $targetHeight")
Synchronizer.Status.DOWNLOADING
} else {
it
}
}.filter { it == Synchronizer.Status.SYNCED }.first()
twig("*** Done waiting for sync! ***")
}
}
// /**
// * Send a transaction and wait until it has been fully created and successfully submitted, which
// * takes about 10 seconds.
// */
// suspend fun createAndSubmitTx(
// zatoshi: Long,
// toAddress: String,
// memo: String = "",
// fromAccountIndex: Int = 0
// ) = coroutineScope {
//
// wallet.send(toAddress, memo, zatoshi, fromAccountIndex)
// }
fun stall(delay: Long = 5000L) = runBlocking {
twig("*** Stalling for ${delay}ms ***")
delay(delay)
}
//
// Validation
//
inner class DarksideTestValidator {
fun validateHasBlock(height: Int) {
runBlocking {
assertTrue((synchronizer as SdkSynchronizer).findBlockHashAsHex(height) != null)
assertTrue((synchronizer as SdkSynchronizer).findBlockHash(height)?.size ?: 0 > 0)
}
}
fun validateLatestHeight(height: Int) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val networkBlockHeight = info.networkBlockHeight
assertTrue(
"Expected latestHeight of $height but the server last reported a height of" +
" $networkBlockHeight! Full details: $info",
networkBlockHeight == height
)
}
fun validateMinHeightDownloaded(minHeight: Int) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val lastDownloadedHeight = info.lastDownloadedHeight
assertTrue(
"Expected to have at least downloaded $minHeight but the last downloaded block was" +
" $lastDownloadedHeight! Full details: $info",
lastDownloadedHeight >= minHeight
)
}
fun validateMinHeightScanned(minHeight: Int) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val lastScannedHeight = info.lastScannedHeight
assertTrue(
"Expected to have at least scanned $minHeight but the last scanned block was" +
" $lastScannedHeight! Full details: $info",
lastScannedHeight >= minHeight
)
}
fun validateMaxHeightScanned(maxHeight: Int) = runBlocking<Unit> {
val lastDownloadedHeight = synchronizer.processorInfo.first().lastScannedHeight
assertTrue(
"Did not expect to be synced beyond $maxHeight but we are synced to" +
" $lastDownloadedHeight",
lastDownloadedHeight <= maxHeight
)
}
fun validateBlockHash(height: Int, expectedHash: String) {
val hash = runBlocking { (synchronizer as SdkSynchronizer).findBlockHashAsHex(height) }
assertEquals(expectedHash, hash)
}
fun onReorg(callback: (errorHeight: Int, rewindHeight: Int) -> Unit) {
synchronizer.onChainErrorHandler = callback
}
fun validateTxCount(count: Int) {
val txCount = runBlocking { (synchronizer as SdkSynchronizer).getTransactionCount() }
assertEquals("Expected $count transactions but found $txCount instead!", count, txCount)
}
fun validateMinBalance(available: Long = -1, total: Long = -1) {
val balance = synchronizer.saplingBalances.value
if (available > 0) {
assertTrue("invalid available balance. Expected a minimum of $available but found ${balance?.available}", available <= balance?.available?.value!!)
}
if (total > 0) {
assertTrue("invalid total balance. Expected a minimum of $total but found ${balance?.total}", total <= balance?.total?.value!!)
}
}
suspend fun validateBalance(available: Long = -1, total: Long = -1, accountIndex: Int = 0) {
val balance = (synchronizer as SdkSynchronizer).processor.getBalanceInfo(accountIndex)
if (available > 0) {
assertEquals("invalid available balance", available, balance.available)
}
if (total > 0) {
assertEquals("invalid total balance", total, balance.total)
}
}
}
//
// Chain Creations
//
inner class DarksideChainMaker {
var lastTipHeight = -1
/**
* Resets the darksidelightwalletd server, stages the blocks represented by the given URL, then
* applies those changes and waits for them to take effect.
*/
fun resetBlocks(
blocksUrl: String,
startHeight: Int = DEFAULT_START_HEIGHT,
tipHeight: Int = startHeight + 100
): DarksideChainMaker = apply {
darkside
.reset(startHeight)
.stageBlocks(blocksUrl)
applyTipHeight(tipHeight)
}
fun stageTransaction(url: String, targetHeight: Int): DarksideChainMaker = apply {
darkside.stageTransactions(url, targetHeight)
}
fun stageTransactions(targetHeight: Int, vararg urls: String): DarksideChainMaker = apply {
urls.forEach {
darkside.stageTransactions(it, targetHeight)
}
}
fun stageEmptyBlocks(startHeight: Int, count: Int = 10): DarksideChainMaker = apply {
darkside.stageEmptyBlocks(startHeight, count)
}
fun stageEmptyBlock() = stageEmptyBlocks(lastTipHeight + 1, 1)
fun applyTipHeight(tipHeight: Int): DarksideChainMaker = apply {
twig("applying tip height of $tipHeight")
darkside.applyBlocks(tipHeight)
lastTipHeight = tipHeight
}
/**
* Creates a chain with 100 blocks and a transaction in the middle.
*
* The chain starts at block 663150 and ends at block 663250
*/
fun makeSimpleChain() {
darkside
.reset(DEFAULT_START_HEIGHT)
.stageBlocks("https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-incoming/blocks.txt")
applyTipHeight(DEFAULT_START_HEIGHT + 100)
}
fun advanceBy(numEmptyBlocks: Int) {
val nextBlock = lastTipHeight + 1
twig("adding $numEmptyBlocks empty blocks to the chain starting at $nextBlock")
darkside.stageEmptyBlocks(nextBlock, numEmptyBlocks)
applyTipHeight(nextBlock + numEmptyBlocks)
}
fun applyPendingTransactions(targetHeight: Int = lastTipHeight + 1) {
stageEmptyBlocks(lastTipHeight + 1, targetHeight - lastTipHeight)
darkside.stageTransactions(darkside.getSentTransactions()?.iterator(), targetHeight)
applyTipHeight(targetHeight)
}
}
companion object {
/**
* This is a special localhost value on the Android emulator, which allows it to contact
* the localhost of the computer running the emulator.
*/
const val COMPUTER_LOCALHOST = "10.0.2.2"
// Block URLS
private const val beforeReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val smallReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"
private const val largeReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-large-reorg.txt"
private const val DEFAULT_START_HEIGHT = 663150
private const val DEFAULT_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"
}
}

View File

@@ -0,0 +1,57 @@
package cash.z.ecc.android.sdk.darkside.test
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import org.junit.Before
/**
* Subclass this to validate the environment for running Darkside tests.
*/
open class DarksideTestPrerequisites {
@Before
fun verifyEmulator() {
require(isProbablyEmulator(ApplicationProvider.getApplicationContext())) {
"Darkside tests are configured to only run on the Android Emulator. Please see https://github.com/zcash/zcash-android-wallet-sdk/blob/master/docs/tests/Darkside.md"
}
}
companion object {
private fun isProbablyEmulator(context: Context): Boolean {
if (isDebuggable(context)) {
// This is imperfect and could break in the future
if (null == Build.DEVICE ||
"generic" == Build.DEVICE || // $NON-NLS
("generic_x86" == Build.DEVICE) // $NON-NLS
) {
return true
}
}
return false
}
/**
* @return Whether the application running is debuggable. This is determined from the
* ApplicationInfo object (`BuildInfo` is useless for libraries.)
*/
private fun isDebuggable(context: Context): Boolean {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.getPackageInfo(
context.packageName,
PackageManager.PackageInfoFlags.of(0L)
)
} else {
@Suppress("Deprecation")
context.packageManager.getPackageInfo(context.packageName, 0)
}
// Normally shouldn't be null, but could be with a MockContext
return packageInfo.applicationInfo?.let {
0 != (it.flags and ApplicationInfo.FLAG_DEBUGGABLE)
} ?: false
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cash.z.ecc.android.sdk.darkside">
<application android:name="androidx.multidex.MultiDexApplication" />
</manifest>

View File

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