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.
211 lines
8.3 KiB
211 lines
8.3 KiB
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.BlockHeight
|
|
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
|
import cash.z.ecc.android.sdk.model.Testnet
|
|
import cash.z.ecc.android.sdk.model.WalletBalance
|
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|
import cash.z.ecc.android.sdk.tool.DerivationTool
|
|
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 endpoint: LightWalletEndpoint = LightWalletEndpoint.Testnet,
|
|
startHeight: BlockHeight? = null
|
|
) {
|
|
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, endpoint, 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
|
|
|
|
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(null) }
|
|
.collect {
|
|
twig("Updated transaction: $it")
|
|
}
|
|
Twig.clip("$alias sending")
|
|
return this
|
|
}
|
|
|
|
suspend fun rewindToHeight(height: BlockHeight): TestWallet {
|
|
synchronizer.rewindToNearestHeight(height, false)
|
|
return this
|
|
}
|
|
|
|
suspend fun shieldFunds(): TestWallet {
|
|
twig("checking $transparentAddress for transactions!")
|
|
synchronizer.refreshUtxos(transparentAddress, BlockHeight.new(ZcashNetwork.Mainnet, 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: BlockHeight, val mainnetBirthday: BlockHeight) {
|
|
// 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",
|
|
BlockHeight.new(
|
|
ZcashNetwork.Testnet,
|
|
1_355_928
|
|
),
|
|
BlockHeight.new(ZcashNetwork.Mainnet, 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",
|
|
BlockHeight.new(
|
|
ZcashNetwork.Testnet,
|
|
1_330_190
|
|
),
|
|
BlockHeight.new(ZcashNetwork.Mainnet, 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",
|
|
BlockHeight.new(
|
|
ZcashNetwork.Testnet,
|
|
1_000_000
|
|
),
|
|
BlockHeight.new(ZcashNetwork.Mainnet, 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",
|
|
BlockHeight.new(
|
|
ZcashNetwork.Testnet,
|
|
1_330_190
|
|
),
|
|
BlockHeight.new(ZcashNetwork.Mainnet, 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",
|
|
BlockHeight.new(
|
|
ZcashNetwork.Testnet,
|
|
1_330_190
|
|
),
|
|
BlockHeight.new(ZcashNetwork.Mainnet, 1_000_000)
|
|
),
|
|
;
|
|
}
|
|
}
|
|
|