forked from hush/hush-android-wallet-sdk
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
317 lines
13 KiB
317 lines
13 KiB
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.model.BlockHeight
|
|
import cash.z.ecc.android.sdk.model.Darkside
|
|
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|
import 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.assertNotNull
|
|
import org.junit.Assert.assertTrue
|
|
|
|
class DarksideTestCoordinator(val wallet: TestWallet) {
|
|
constructor(
|
|
alias: String = "DarksideTestCoordinator",
|
|
seedPhrase: String = DEFAULT_SEED_PHRASE,
|
|
startHeight: BlockHeight = DEFAULT_START_HEIGHT,
|
|
network: ZcashNetwork = ZcashNetwork.Mainnet,
|
|
endpoint: LightWalletEndpoint = LightWalletEndpoint.Darkside
|
|
) : this(TestWallet(seedPhrase, alias, network, endpoint, startHeight = startHeight))
|
|
|
|
private val targetHeight = BlockHeight.new(wallet.network, 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: BlockHeight? = null) = 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 != null && (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 != null && (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: BlockHeight) {
|
|
runBlocking {
|
|
assertTrue((synchronizer as SdkSynchronizer).findBlockHashAsHex(height) != null)
|
|
assertTrue((synchronizer as SdkSynchronizer).findBlockHash(height)?.size ?: 0 > 0)
|
|
}
|
|
}
|
|
|
|
fun validateLatestHeight(height: BlockHeight) = 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: BlockHeight) = runBlocking<Unit> {
|
|
val info = synchronizer.processorInfo.first()
|
|
val lastDownloadedHeight = info.lastDownloadedHeight
|
|
assertNotNull(lastDownloadedHeight)
|
|
assertTrue(
|
|
"Expected to have at least downloaded $minHeight but the last downloaded block was" +
|
|
" $lastDownloadedHeight! Full details: $info",
|
|
lastDownloadedHeight!! >= minHeight
|
|
)
|
|
}
|
|
|
|
fun validateMinHeightScanned(minHeight: BlockHeight) = runBlocking<Unit> {
|
|
val info = synchronizer.processorInfo.first()
|
|
val lastScannedHeight = info.lastScannedHeight
|
|
assertNotNull(lastScannedHeight)
|
|
assertTrue(
|
|
"Expected to have at least scanned $minHeight but the last scanned block was" +
|
|
" $lastScannedHeight! Full details: $info",
|
|
lastScannedHeight!! >= minHeight
|
|
)
|
|
}
|
|
|
|
fun validateMaxHeightScanned(maxHeight: BlockHeight) = runBlocking<Unit> {
|
|
val lastDownloadedHeight = synchronizer.processorInfo.first().lastScannedHeight
|
|
assertNotNull(lastDownloadedHeight)
|
|
assertTrue(
|
|
"Did not expect to be synced beyond $maxHeight but we are synced to" +
|
|
" $lastDownloadedHeight",
|
|
lastDownloadedHeight!! <= maxHeight
|
|
)
|
|
}
|
|
|
|
fun validateBlockHash(height: BlockHeight, expectedHash: String) {
|
|
val hash = runBlocking { (synchronizer as SdkSynchronizer).findBlockHashAsHex(height) }
|
|
assertEquals(expectedHash, hash)
|
|
}
|
|
|
|
fun onReorg(callback: (errorHeight: BlockHeight, rewindHeight: BlockHeight) -> 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: BlockHeight? = null
|
|
|
|
/**
|
|
* 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: BlockHeight = DEFAULT_START_HEIGHT,
|
|
tipHeight: BlockHeight = startHeight + 100
|
|
): DarksideChainMaker = apply {
|
|
darkside
|
|
.reset(startHeight)
|
|
.stageBlocks(blocksUrl)
|
|
applyTipHeight(tipHeight)
|
|
}
|
|
|
|
fun stageTransaction(url: String, targetHeight: BlockHeight): DarksideChainMaker = apply {
|
|
darkside.stageTransactions(url, targetHeight)
|
|
}
|
|
|
|
fun stageTransactions(targetHeight: BlockHeight, vararg urls: String): DarksideChainMaker = apply {
|
|
urls.forEach {
|
|
darkside.stageTransactions(it, targetHeight)
|
|
}
|
|
}
|
|
|
|
fun stageEmptyBlocks(startHeight: BlockHeight, count: Int = 10): DarksideChainMaker = apply {
|
|
darkside.stageEmptyBlocks(startHeight, count)
|
|
}
|
|
|
|
fun stageEmptyBlock() = stageEmptyBlocks(lastTipHeight!! + 1, 1)
|
|
|
|
fun applyTipHeight(tipHeight: BlockHeight): 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: BlockHeight = lastTipHeight!! + 1) {
|
|
stageEmptyBlocks(lastTipHeight!! + 1, (targetHeight.value - lastTipHeight!!.value).toInt())
|
|
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 val DEFAULT_START_HEIGHT = BlockHeight.new(ZcashNetwork.Mainnet, 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"
|
|
}
|
|
}
|
|
|