Merge pull request #108 from zcash/feature/demo-repair
Feature/demo repair
This commit is contained in:
commit
0abd55f528
32
Cargo.toml
32
Cargo.toml
|
@ -21,28 +21,36 @@ log-panics = "2.0.0"
|
|||
protobuf = { version = "2", optional = true }
|
||||
|
||||
[dependencies.ff]
|
||||
git = "https://github.com/str4d/librustzcash.git"
|
||||
rev = "f3f5338282eeda6d9f5bff69f6930a5473c95925"
|
||||
git = "https://github.com/gmale/librustzcash.git"
|
||||
branch = "feature/add-scan-limit"
|
||||
|
||||
[dependencies.pairing]
|
||||
git = "https://github.com/str4d/librustzcash.git"
|
||||
rev = "f3f5338282eeda6d9f5bff69f6930a5473c95925"
|
||||
git = "https://github.com/gmale/librustzcash.git"
|
||||
branch = "feature/add-scan-limit"
|
||||
|
||||
[dependencies.zcash_client_backend]
|
||||
git = "https://github.com/str4d/librustzcash.git"
|
||||
rev = "f3f5338282eeda6d9f5bff69f6930a5473c95925"
|
||||
git = "https://github.com/gmale/librustzcash.git"
|
||||
branch = "feature/add-scan-limit"
|
||||
|
||||
[dependencies.zcash_client_sqlite]
|
||||
git = "https://github.com/str4d/librustzcash.git"
|
||||
rev = "f3f5338282eeda6d9f5bff69f6930a5473c95925"
|
||||
git = "https://github.com/gmale/librustzcash.git"
|
||||
branch = "feature/add-scan-limit"
|
||||
|
||||
[dependencies.zcash_primitives]
|
||||
git = "https://github.com/str4d/librustzcash.git"
|
||||
rev = "f3f5338282eeda6d9f5bff69f6930a5473c95925"
|
||||
git = "https://github.com/gmale/librustzcash.git"
|
||||
branch = "feature/add-scan-limit"
|
||||
|
||||
[dependencies.zcash_proofs]
|
||||
git = "https://github.com/str4d/librustzcash.git"
|
||||
rev = "f3f5338282eeda6d9f5bff69f6930a5473c95925"
|
||||
git = "https://github.com/gmale/librustzcash.git"
|
||||
branch = "feature/add-scan-limit"
|
||||
|
||||
#[patch.'https://github.com/str4d/librustzcash.git']
|
||||
#ff = { path = '../../clones/librustzcash/ff' }
|
||||
#pairing = { path = '../../clones/librustzcash/pairing' }
|
||||
#zcash_client_backend = { path = '../../clones/librustzcash/zcash_client_backend' }
|
||||
#zcash_client_sqlite = { path = '../../clones/librustzcash/zcash_client_sqlite' }
|
||||
#zcash_primitives = { path = '../../clones/librustzcash/zcash_primitives' }
|
||||
#zcash_proofs = { path = '../../clones/librustzcash/zcash_proofs' }
|
||||
|
||||
[features]
|
||||
mainnet = ["zcash_client_sqlite/mainnet"]
|
||||
|
|
|
@ -15,7 +15,21 @@ android {
|
|||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
missingDimensionStrategy "network", "zcashtestnet"
|
||||
}
|
||||
flavorDimensions 'network'
|
||||
productFlavors {
|
||||
// would rather name them "testnet" and "mainnet" but product flavor names cannot start with the word "test"
|
||||
zcashtestnet {
|
||||
dimension 'network'
|
||||
applicationId 'cash.z.wallet.sdk.demoapp.testnet'
|
||||
matchingFallbacks = ['zcashtestnet', 'debug']
|
||||
}
|
||||
|
||||
zcashmainnet {
|
||||
dimension 'network'
|
||||
applicationId 'cash.z.wallet.sdk.demoapp.mainnet'
|
||||
matchingFallbacks = ['zcashmainnet', 'release']
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
@ -36,7 +50,12 @@ dependencies {
|
|||
// SDK
|
||||
implementation project(path: ':sdk')
|
||||
// implementation "cash.z.android.wallet:zcash-android-core:$sdk_version@aar"
|
||||
// implementation "cash.z.android.wallet:zcash-kotlin-bip39:$sdk_version@aar"
|
||||
|
||||
// sample mnemonic plugin
|
||||
implementation 'com.github.zcash:zcash-android-wallet-plugins:1.0.0'
|
||||
implementation 'com.madgag.spongycastle:core:1.58.0.0'
|
||||
implementation 'io.github.novacrypto:BIP39:2019.01.27'
|
||||
implementation 'io.github.novacrypto:securestring:2019.01.27'
|
||||
|
||||
// SDK: grpc
|
||||
implementation "io.grpc:grpc-okhttp:1.21.0"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package cash.z.wallet.sdk.demoapp
|
||||
|
||||
import android.app.Application
|
||||
import cash.z.wallet.sdk.demoapp.util.DemoConfig
|
||||
|
||||
class App : Application() {
|
||||
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
package cash.z.wallet.sdk.demoapp
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import cash.z.wallet.sdk.ext.TroubleshootingTwig
|
||||
import cash.z.wallet.sdk.ext.Twig
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
@ -32,6 +37,7 @@ abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
registerActionButtonListener()
|
||||
// just a quick way of enforcing the following for each demo:
|
||||
// - wait until the fragment is created, then run `initInBackground` on a background thread
|
||||
// - wait until init is finished
|
||||
|
@ -59,9 +65,58 @@ abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
|
|||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
unregisterActionButtonListener()
|
||||
onClear()
|
||||
}
|
||||
|
||||
private fun registerActionButtonListener() {
|
||||
(activity as? MainActivity)?.fabListener = this
|
||||
}
|
||||
|
||||
private fun unregisterActionButtonListener() {
|
||||
(activity as? MainActivity)?.apply {
|
||||
if (fabListener === this@BaseDemoFragment) fabListener = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to run whenever the fragment is paused. The intention is to clear out each demo
|
||||
* cleanly so that they are always a repeatable experience.
|
||||
*/
|
||||
open fun onClear() {}
|
||||
|
||||
/**
|
||||
* Callback that gets invoked on the visible fragment whenever the floating action button is
|
||||
* tapped. This provides a convenient placeholder for the developer to extend the
|
||||
* behavior for a demo, for instance by copying the address to the clipboard, whenever the FAB
|
||||
* is tapped on the address screen.
|
||||
*/
|
||||
open fun onActionButtonClicked() {
|
||||
// Show a message so that it's easy for developers to find how to replace this behavior for
|
||||
// each fragment. Simply override this [onActionButtonClicked] callback to add behavior to a
|
||||
// demo. In other words, this function probably doesn't need to change because desired
|
||||
// behavior should go in the child fragment, which overrides this.
|
||||
Snackbar.make(view!!, "Replace with your own action", Snackbar.LENGTH_LONG)
|
||||
.setAction("Action") { /* auto-close */ }.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to the given text to the clipboard.
|
||||
*/
|
||||
open fun copyToClipboard(text: String) {
|
||||
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)?.let { cm ->
|
||||
cm.setPrimaryClip(ClipData.newPlainText("DemoAppClip", text))
|
||||
}
|
||||
toast("Copied to clipboard!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to show a toast in the main activity.
|
||||
*/
|
||||
fun toast(message: String) {
|
||||
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate the ViewBinding. Unfortunately, the `inflate` function is not part of the ViewBinding
|
||||
* interface so the base class cannot take care of this behavior without some help.
|
||||
|
@ -69,5 +124,4 @@ abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
|
|||
abstract fun inflateBinding(layoutInflater: LayoutInflater): T
|
||||
abstract fun resetInBackground()
|
||||
abstract fun onResetComplete()
|
||||
abstract fun onClear()
|
||||
}
|
|
@ -2,6 +2,7 @@ package cash.z.wallet.sdk.demoapp
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
|
@ -13,11 +14,12 @@ import androidx.navigation.ui.setupWithNavController
|
|||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import cash.z.wallet.sdk.demoapp.util.DemoConfig
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||
var fabListener: BaseDemoFragment<out ViewBinding>? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -27,8 +29,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
val fab: FloatingActionButton = findViewById(R.id.fab)
|
||||
fab.setOnClickListener { view ->
|
||||
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
|
||||
.setAction("Action", null).show()
|
||||
onFabClicked(view)
|
||||
}
|
||||
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
|
||||
val navView: NavigationView = findViewById(R.id.nav_view)
|
||||
|
@ -46,6 +47,10 @@ class MainActivity : AppCompatActivity() {
|
|||
navView.setupWithNavController(navController)
|
||||
}
|
||||
|
||||
private fun onFabClicked(view: View) {
|
||||
fabListener?.onActionButtonClicked()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
menuInflater.inflate(R.menu.main, menu)
|
||||
|
|
|
@ -11,22 +11,21 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
|
|||
private var seed: ByteArray = App.instance.defaultConfig.seed
|
||||
private val initializer: Initializer = Initializer(App.instance)
|
||||
|
||||
private lateinit var address: String
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetAddressBinding
|
||||
= FragmentGetAddressBinding.inflate(layoutInflater)
|
||||
|
||||
override fun resetInBackground() {
|
||||
/**
|
||||
* Create and initialize the wallet. Initialization will return the private keys but for the
|
||||
* purposes of this demo we don't need them.
|
||||
*/
|
||||
initializer.new(seed)
|
||||
address = initializer.deriveAddress(seed)
|
||||
}
|
||||
|
||||
override fun onResetComplete() {
|
||||
binding.textInfo.text = initializer.rustBackend.getAddress()
|
||||
binding.textInfo.text = address
|
||||
}
|
||||
|
||||
override fun onClear() {
|
||||
initializer.clear()
|
||||
override fun onActionButtonClicked() {
|
||||
copyToClipboard(address)
|
||||
}
|
||||
|
||||
}
|
|
@ -10,6 +10,7 @@ import cash.z.wallet.sdk.service.LightWalletService
|
|||
|
||||
class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
|
||||
private val host = App.instance.defaultConfig.host
|
||||
private val port = App.instance.defaultConfig.port
|
||||
|
||||
private lateinit var lightwalletService: LightWalletService
|
||||
|
||||
|
@ -17,7 +18,7 @@ class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
|
|||
FragmentGetBlockBinding.inflate(layoutInflater)
|
||||
|
||||
override fun resetInBackground() {
|
||||
lightwalletService = LightWalletGrpcService(App.instance, host)
|
||||
lightwalletService = LightWalletGrpcService(App.instance, host, port)
|
||||
}
|
||||
|
||||
override fun onResetComplete() {
|
||||
|
|
|
@ -11,6 +11,7 @@ import cash.z.wallet.sdk.service.LightWalletService
|
|||
class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
|
||||
|
||||
private val host = App.instance.defaultConfig.host
|
||||
private val port = App.instance.defaultConfig.port
|
||||
|
||||
private lateinit var lightwalletService: LightWalletService
|
||||
|
||||
|
@ -18,7 +19,7 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
|
|||
FragmentGetBlockRangeBinding.inflate(layoutInflater)
|
||||
|
||||
override fun resetInBackground() {
|
||||
lightwalletService = LightWalletGrpcService(App.instance, host)
|
||||
lightwalletService = LightWalletGrpcService(App.instance, host, port)
|
||||
}
|
||||
|
||||
override fun onResetComplete() {
|
||||
|
|
|
@ -10,6 +10,7 @@ import cash.z.wallet.sdk.service.LightWalletService
|
|||
|
||||
class GetLatestHeightFragment : BaseDemoFragment<FragmentGetLatestHeightBinding>() {
|
||||
private val host = App.instance.defaultConfig.host
|
||||
private val port = App.instance.defaultConfig.port
|
||||
|
||||
private lateinit var lightwalletService: LightWalletService
|
||||
|
||||
|
@ -17,7 +18,7 @@ class GetLatestHeightFragment : BaseDemoFragment<FragmentGetLatestHeightBinding>
|
|||
FragmentGetLatestHeightBinding.inflate(layoutInflater)
|
||||
|
||||
override fun resetInBackground() {
|
||||
lightwalletService = LightWalletGrpcService(App.instance, host)
|
||||
lightwalletService = LightWalletGrpcService(App.instance, host, port)
|
||||
}
|
||||
|
||||
override fun onResetComplete() {
|
||||
|
@ -27,4 +28,9 @@ class GetLatestHeightFragment : BaseDemoFragment<FragmentGetLatestHeightBinding>
|
|||
override fun onClear() {
|
||||
lightwalletService.shutdown()
|
||||
}
|
||||
|
||||
override fun onActionButtonClicked() {
|
||||
toast("Refreshed!")
|
||||
onResetComplete()
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import cash.z.wallet.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding
|
|||
class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
|
||||
private var seed: ByteArray = App.instance.defaultConfig.seed
|
||||
private val initializer: Initializer = Initializer(App.instance)
|
||||
private val birthday = App.instance.defaultConfig.newWalletBirthday()
|
||||
private lateinit var spendingKeys: Array<String>
|
||||
private lateinit var viewingKeys: Array<String>
|
||||
|
||||
|
@ -22,10 +23,10 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
|
|||
* store these keys in its secure storage for retrieval, later. Private keys are only needed
|
||||
* for sending funds.
|
||||
*/
|
||||
spendingKeys = initializer.new(seed)
|
||||
spendingKeys = initializer.new(seed, birthday)
|
||||
|
||||
/*
|
||||
* Viewing keys can be derived from a seed or from spending keys.
|
||||
* Alternatively, viewing keys can also be derived directly from a seed or spending keys.
|
||||
*/
|
||||
viewingKeys = initializer.deriveViewingKeys(seed)
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.paging.PagedList
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import cash.z.wallet.sdk.Initializer
|
||||
import cash.z.wallet.sdk.Synchronizer
|
||||
import cash.z.wallet.sdk.block.CompactBlockProcessor
|
||||
import cash.z.wallet.sdk.demoapp.App
|
||||
import cash.z.wallet.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.wallet.sdk.demoapp.databinding.FragmentListTransactionsBinding
|
||||
|
@ -22,16 +23,21 @@ import kotlinx.coroutines.launch
|
|||
*/
|
||||
class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() {
|
||||
private val config = App.instance.defaultConfig
|
||||
private val initializer = Initializer(App.instance)
|
||||
private val initializer = Initializer(App.instance, host = config.host, port = config.port)
|
||||
private val birthday = config.loadBirthday()
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
private lateinit var adapter: TransactionAdapter<ConfirmedTransaction>
|
||||
private lateinit var address: String
|
||||
private var status: Synchronizer.Status? = null
|
||||
|
||||
private val isSynced get() = status == Synchronizer.Status.SYNCED
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListTransactionsBinding =
|
||||
FragmentListTransactionsBinding.inflate(layoutInflater)
|
||||
|
||||
override fun resetInBackground() {
|
||||
initializer.new(config.seed)
|
||||
synchronizer = Synchronizer(App.instance, config.host, initializer.rustBackend)
|
||||
initializer.new(config.seed, birthday)
|
||||
synchronizer = Synchronizer(App.instance, initializer)
|
||||
}
|
||||
|
||||
override fun onResetComplete() {
|
||||
|
@ -50,6 +56,7 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
|
||||
adapter = TransactionAdapter()
|
||||
lifecycleScope.launch {
|
||||
address = synchronizer.getAddress()
|
||||
synchronizer.receivedTransactions.collect { onTransactionsUpdated(it) }
|
||||
}
|
||||
binding.recyclerTransactions.adapter = adapter
|
||||
|
@ -63,20 +70,22 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
|
||||
private fun monitorStatus() {
|
||||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
}
|
||||
|
||||
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
|
||||
if (info.isScanning) binding.textInfo.text = "Scanning blocks...${info.scanProgress}%"
|
||||
}
|
||||
|
||||
private fun onProgress(i: Int) {
|
||||
val message = when (i) {
|
||||
100 -> "Scanning blocks..."
|
||||
else -> "Downloading blocks...$i%"
|
||||
}
|
||||
binding.textInfo.text = message
|
||||
if (i < 100) binding.textInfo.text = "Downloading blocks...$i%"
|
||||
}
|
||||
|
||||
private fun onStatus(status: Synchronizer.Status) {
|
||||
this.status = status
|
||||
binding.textStatus.text = "Status: $status"
|
||||
if (status == Synchronizer.Status.SYNCED) onSyncComplete()
|
||||
if (isSynced) onSyncComplete()
|
||||
}
|
||||
|
||||
private fun onSyncComplete() {
|
||||
|
@ -86,5 +95,24 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
private fun onTransactionsUpdated(transactions: PagedList<ConfirmedTransaction>) {
|
||||
twig("got a new paged list of transactions")
|
||||
adapter.submitList(transactions)
|
||||
|
||||
// show message when there are no transactions
|
||||
if (isSynced) {
|
||||
binding.textInfo.apply {
|
||||
if (transactions.isEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
text =
|
||||
"No transactions found. Try to either change the seed words in the" +
|
||||
" DemoConfig.kt file or send funds to this address (tap the FAB to copy it):\n\n $address"
|
||||
} else {
|
||||
visibility = View.INVISIBLE
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActionButtonClicked() {
|
||||
if (::address.isInitialized) copyToClipboard(address)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@ import cash.z.wallet.sdk.ext.*
|
|||
|
||||
class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
||||
private val config = App.instance.defaultConfig
|
||||
private val initializer = Initializer(App.instance)
|
||||
private val initializer = Initializer(App.instance, host = config.host, port = config.port)
|
||||
private val birthday = config.loadBirthday()
|
||||
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
private lateinit var keyManager: SampleStorageBridge
|
||||
|
@ -30,7 +31,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
// Observable properties (done without livedata or flows for simplicity)
|
||||
//
|
||||
|
||||
private var availableBalance = -1L
|
||||
private var balance = CompactBlockProcessor.WalletBalance()
|
||||
set(value) {
|
||||
field = value
|
||||
onUpdateSendButton()
|
||||
|
@ -56,9 +57,9 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
FragmentSendBinding.inflate(layoutInflater)
|
||||
|
||||
override fun resetInBackground() {
|
||||
val spendingKeys = initializer.new(config.seed)
|
||||
val spendingKeys = initializer.new(config.seed, birthday)
|
||||
keyManager = SampleStorageBridge().securelyStorePrivateKey(spendingKeys[0])
|
||||
synchronizer = Synchronizer(App.instance, config.host, initializer.rustBackend)
|
||||
synchronizer = Synchronizer(App.instance, initializer)
|
||||
}
|
||||
|
||||
// STARTING POINT
|
||||
|
@ -97,40 +98,47 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
private fun monitorChanges() {
|
||||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.balances.collectWith(lifecycleScope, ::onBalance)
|
||||
}
|
||||
|
||||
private fun onStatus(status: Synchronizer.Status) {
|
||||
binding.textStatus.text = "Status: $status"
|
||||
if (status == Synchronizer.Status.SYNCING) {
|
||||
isSyncing = true
|
||||
isSyncing = status != Synchronizer.Status.SYNCED
|
||||
if (status == Synchronizer.Status.SCANNING) {
|
||||
binding.textBalance.text = "Calculating balance..."
|
||||
} else {
|
||||
isSyncing = false
|
||||
if (!isSyncing) onBalance(balance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProgress(i: Int) {
|
||||
val message = when (i) {
|
||||
100 -> "Scanning blocks..."
|
||||
else -> "Downloading blocks...$i%"
|
||||
if (i < 100) {
|
||||
binding.textStatus.text = "Downloading blocks...$i%"
|
||||
binding.textBalance.visibility = View.INVISIBLE
|
||||
} else {
|
||||
binding.textBalance.visibility = View.VISIBLE
|
||||
}
|
||||
binding.textStatus.text = message
|
||||
binding.textBalance.text = ""
|
||||
}
|
||||
|
||||
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
|
||||
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
|
||||
}
|
||||
|
||||
private fun onBalance(balance: CompactBlockProcessor.WalletBalance) {
|
||||
availableBalance = balance.available
|
||||
binding.textBalance.text = """
|
||||
Available balance: ${balance.available.convertZatoshiToZecString()}
|
||||
Total balance: ${balance.total.convertZatoshiToZecString()}
|
||||
""".trimIndent()
|
||||
this.balance = balance
|
||||
if (!isSyncing) {
|
||||
binding.textBalance.text = """
|
||||
Available balance: ${balance.availableZatoshi.convertZatoshiToZecString(12)}
|
||||
Total balance: ${balance.totalZatoshi.convertZatoshiToZecString(12)}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSend(unused: View) {
|
||||
isSending = true
|
||||
val amount = amountInput.text.toString().toDouble().convertZecToZatoshi()
|
||||
val toAddress = addressInput.text.toString()
|
||||
val toAddress = addressInput.text.toString().trim()
|
||||
synchronizer.sendToAddress(
|
||||
keyManager.key,
|
||||
amount,
|
||||
|
@ -168,7 +176,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
text = "⌛ syncing"
|
||||
isEnabled = false
|
||||
}
|
||||
availableBalance <= 0 -> isEnabled = false
|
||||
balance.availableZatoshi <= 0 -> isEnabled = false
|
||||
else -> {
|
||||
text = "send"
|
||||
isEnabled = true
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package cash.z.wallet.sdk.demoapp.util
|
||||
|
||||
data class DemoConfig(
|
||||
val host: String = "34.68.177.238",//"192.168.1.134",//
|
||||
val port: Int = 9067,
|
||||
val birthdayHeight: Int = 620_000,//523_240,
|
||||
val network: ZcashNetwork = ZcashNetwork.TEST_NET,
|
||||
val seed: ByteArray = "testreferencealice".toByteArray(),
|
||||
val toAddress: String = "ztestsapling1fg82ar8y8whjfd52l0xcq0w3n7nn7cask2scp9rp27njeurr72ychvud57s9tu90fdqgwdt07lg",
|
||||
val sendAmount: Double = 0.0024
|
||||
)
|
||||
|
||||
enum class ZcashNetwork { MAIN_NET, TEST_NET }
|
|
@ -0,0 +1,82 @@
|
|||
package cash.z.wallet.sdk.demoapp.util
|
||||
|
||||
import cash.z.android.plugin.MnemonicPlugin
|
||||
import io.github.novacrypto.bip39.MnemonicGenerator
|
||||
import io.github.novacrypto.bip39.SeedCalculator
|
||||
import io.github.novacrypto.bip39.Words
|
||||
import io.github.novacrypto.bip39.wordlists.English
|
||||
import java.security.SecureRandom
|
||||
|
||||
class SimpleMnemonics : MnemonicPlugin {
|
||||
|
||||
override fun nextEntropy(): ByteArray {
|
||||
return ByteArray(Words.TWENTY_FOUR.byteLength()).apply {
|
||||
SecureRandom().nextBytes(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nextMnemonic(): CharArray {
|
||||
return nextMnemonic(nextEntropy())
|
||||
}
|
||||
|
||||
override fun nextMnemonic(entropy: ByteArray): CharArray {
|
||||
return StringBuilder().let { builder ->
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
builder.toString().toCharArray()
|
||||
}
|
||||
}
|
||||
|
||||
override fun nextMnemonicList(): List<CharArray> {
|
||||
return nextMnemonicList(nextEntropy())
|
||||
}
|
||||
|
||||
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> {
|
||||
return WordListBuilder().let { builder ->
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
builder.wordList
|
||||
}
|
||||
}
|
||||
|
||||
override fun toSeed(mnemonic: CharArray): ByteArray {
|
||||
return SeedCalculator().calculateSeed(String(mnemonic), "")
|
||||
}
|
||||
|
||||
override fun toWordList(mnemonic: CharArray): List<CharArray> {
|
||||
val wordList = mutableListOf<CharArray>()
|
||||
var cursor = 0
|
||||
repeat(mnemonic.size) { i ->
|
||||
val isSpace = mnemonic[i] == ' '
|
||||
if (isSpace || i == (mnemonic.size - 1)) {
|
||||
val wordSize = i - cursor + if (isSpace) 0 else 1
|
||||
wordList.add(CharArray(wordSize).apply {
|
||||
repeat(wordSize) {
|
||||
this[it] = mnemonic[cursor + it]
|
||||
}
|
||||
})
|
||||
cursor = i + 1
|
||||
}
|
||||
}
|
||||
return wordList
|
||||
}
|
||||
|
||||
class WordListBuilder {
|
||||
val wordList = mutableListOf<CharArray>()
|
||||
fun append(c: CharSequence) {
|
||||
if (c[0] != English.INSTANCE.space) addWord(c)
|
||||
}
|
||||
|
||||
private fun addWord(c: CharSequence) {
|
||||
c.length.let { size ->
|
||||
val word = CharArray(size)
|
||||
repeat(size) {
|
||||
word[it] = c[it]
|
||||
}
|
||||
wordList.add(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,9 +2,16 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_content_end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.88" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/text_layout_start_height"
|
||||
|
@ -15,7 +22,7 @@
|
|||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.2"/>
|
||||
app:layout_constraintVertical_bias="0.2" />
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
|
@ -39,27 +46,27 @@
|
|||
android:textSize="20sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="loading blocks..."
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_status"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/text_status" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_transactions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="32dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_info"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_status"
|
||||
app:layout_constraintBottom_toTopOf="@id/guideline_content_end"
|
||||
tools:itemCount="15"
|
||||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_transaction"
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:ems="8"
|
||||
android:hint="to address"
|
||||
android:inputType="number"
|
||||
android:textSize="20sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
<string name="app_name">Demo App</string>
|
||||
<string name="navigation_drawer_open">Open navigation drawer</string>
|
||||
<string name="navigation_drawer_close">Close navigation drawer</string>
|
||||
<string name="nav_header_title">Android SDK Demo</string>
|
||||
<string name="nav_header_subtitle">v1.0.0-alpha02</string>
|
||||
<string name="nav_header_desc">Navigation header</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package cash.z.wallet.sdk.demoapp
|
||||
|
||||
import cash.z.wallet.sdk.Initializer
|
||||
import cash.z.wallet.sdk.demoapp.util.SimpleMnemonics
|
||||
|
||||
data class DemoConfig(
|
||||
val host: String = "lightwalletd.z.cash",
|
||||
val port: Int = 9067,
|
||||
val birthdayHeight: Int = 735_000,
|
||||
val sendAmount: Double = 0.0018,
|
||||
|
||||
// corresponds to address: zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx
|
||||
val seedWords: String = "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame",
|
||||
|
||||
// corresponds to seed: urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist decorate rhythm phone style benefit portion bus truck top
|
||||
val toAddress: String = "zs1lcdmue7rewgvzh3jd09sfvwq3sumu6hkhpk53q94kcneuffjkdg9e3tyxrugkmpza5c3c5e6eqh"
|
||||
) {
|
||||
val seed: ByteArray get() = SimpleMnemonics().toSeed(seedWords.toCharArray())
|
||||
|
||||
fun newWalletBirthday() = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance)
|
||||
fun loadBirthday(height: Int = birthdayHeight) = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance, height)
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<resources>
|
||||
<string name="app_name">Demo App - Mainnet</string>
|
||||
<string name="nav_header_title">Android SDK Demo : MAINNET</string>
|
||||
</resources>
|
|
@ -0,0 +1,22 @@
|
|||
package cash.z.wallet.sdk.demoapp
|
||||
|
||||
import cash.z.wallet.sdk.Initializer
|
||||
import cash.z.wallet.sdk.demoapp.util.SimpleMnemonics
|
||||
|
||||
data class DemoConfig(
|
||||
val host: String = "lightwalletd.testnet.z.cash",
|
||||
val port: Int = 9067,
|
||||
val birthdayHeight: Int = 620_000,
|
||||
val sendAmount: Double = 0.0018,
|
||||
|
||||
// corresponds to address: ztestsapling1zhqvuq8zdwa8nsnde7074kcfsat0w25n08jzuvz5skzcs6h9raxu898l48xwr8fmkny3zqqrgd9
|
||||
val seedWords: String = "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame",
|
||||
|
||||
// corresponds to seed: urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist decorate rhythm phone style benefit portion bus truck top
|
||||
val toAddress: String = "ztestsapling1ddttvrm6ueug4vwlczs8daqjaul60aur4udnvcz9qdnjt9ekt2tsxheqvv3mn50wvhmzj4ge9rl"
|
||||
) {
|
||||
val seed: ByteArray get() = SimpleMnemonics().toSeed(seedWords.toCharArray())
|
||||
|
||||
fun newWalletBirthday() = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance)
|
||||
fun loadBirthday(height: Int = birthdayHeight) = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance, height)
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<resources>
|
||||
<string name="app_name">Demo App - Testnet</string>
|
||||
<string name="nav_header_title">Android SDK Demo : TESTNET</string>
|
||||
</resources>
|
|
@ -10,7 +10,7 @@ buildscript {
|
|||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.0-beta04'
|
||||
classpath 'com.android.tools.build:gradle:3.6.0-rc01'
|
||||
classpath"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
@ -20,12 +20,9 @@ buildscript {
|
|||
|
||||
allprojects {
|
||||
repositories {
|
||||
// mavenLocal()
|
||||
// flatDir {
|
||||
// dirs 'libs'
|
||||
// }
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#Mon Sep 30 13:00:06 MDT 2019
|
||||
#Mon Feb 24 12:41:21 EST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||
|
|
|
@ -252,7 +252,7 @@ class Initializer(
|
|||
*
|
||||
* @return the address that corresponds to the seed and account index.
|
||||
*/
|
||||
fun deriveAddress(seed: ByteArray, accountIndex: Int) =
|
||||
fun deriveAddress(seed: ByteArray, accountIndex: Int = 0) =
|
||||
requireRustBackend().deriveAddress(seed, accountIndex)
|
||||
|
||||
/**
|
||||
|
|
|
@ -402,10 +402,48 @@ class CompactBlockProcessor(
|
|||
val lastDownloadRange: IntRange = 0..-1, // empty range
|
||||
val lastScanRange: IntRange = 0..-1 // empty range
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns false when all values match their defaults.
|
||||
*/
|
||||
val hasData get() = networkBlockHeight != -1
|
||||
|| lastScannedHeight != -1
|
||||
|| lastDownloadedHeight != -1
|
||||
|| lastDownloadRange != 0..-1
|
||||
|| lastScanRange != 0..-1
|
||||
|
||||
/**
|
||||
* Returns true when there are more than zero blocks remaining to download.
|
||||
*/
|
||||
val isDownloading: Boolean get() = !lastDownloadRange.isEmpty()
|
||||
&& lastDownloadedHeight < lastDownloadRange.last
|
||||
|
||||
/**
|
||||
* Returns true when downloading has completed and there are more than zero blocks remaining
|
||||
* to be scanned.
|
||||
*/
|
||||
val isScanning: Boolean get() = !isDownloading
|
||||
&& !lastScanRange.isEmpty()
|
||||
&& lastScannedHeight < lastScanRange.last
|
||||
|
||||
/**
|
||||
* The amount of scan progress from 0 to 100.
|
||||
*/
|
||||
val scanProgress get() = when {
|
||||
lastScannedHeight <= -1 -> 0
|
||||
lastScanRange.isEmpty() -> 100
|
||||
lastScannedHeight >= lastScanRange.last -> 100
|
||||
else -> {
|
||||
// when lastScannedHeight == lastScanRange.first, we have scanned one block, thus the offsets
|
||||
val blocksScanned = (lastScannedHeight - lastScanRange.first + 1).coerceAtLeast(0)
|
||||
// we scan the range inclusively so 100..100 is one block to scan, thus the offset
|
||||
val numberOfBlocks = lastScanRange.last - lastScanRange.first + 1
|
||||
// take the percentage then convert and round
|
||||
((blocksScanned.toFloat() / numberOfBlocks) * 100.0f).let { percent ->
|
||||
percent.coerceAtMost(100.0f).roundToInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ class LightWalletGrpcService private constructor(
|
|||
appContext: Context,
|
||||
host: String,
|
||||
port: Int = DEFAULT_LIGHTWALLETD_PORT,
|
||||
usePlaintext: Boolean = !appContext.resources.getBoolean(R.bool.is_mainnet)
|
||||
usePlaintext: Boolean = appContext.resources.getBoolean(R.bool.lightwalletd_allow_very_insecure_connections)
|
||||
) : this(createDefaultChannel(appContext, host, port, usePlaintext))
|
||||
|
||||
/* LightWalletService implementation */
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="lightwalletd_allow_very_insecure_connections">true</bool>
|
||||
<bool name="lightwalletd_allow_very_insecure_connections">false</bool>
|
||||
</resources>
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"network": "testnet",
|
||||
"height": 280000,
|
||||
"hash": "000420e7fcc3a49d729479fb0b560dd7b8617b178a08e9e389620a9d1dd6361a",
|
||||
"time": 1535262293,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"network": "testnet",
|
||||
"height": 421720,
|
||||
"hash": "001ede53476a31a91da3313eddf4e41409fb7f4e003840700557b576024d09b4",
|
||||
"time": 1550762014,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"network": "testnet",
|
||||
"height": 425865,
|
||||
"hash": "0011c4de26004e564347b8af218ca16cd07b08c4159b1cc9c43afa6cb8807bed",
|
||||
"time": 1551215770,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"network": "testnet",
|
||||
"height": 518000,
|
||||
"hash": "000ba586d734c295f0bc034be229b1c96cb040f9d4929efdb5d2b187eeb238fb",
|
||||
"time": 1560645743,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"network": "testnet",
|
||||
"height": 523240,
|
||||
"hash": "00000c33da2196f0ed1bda71043f671fc69a0212e01f892653e212ab358f6b79",
|
||||
"time": 1561002603,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "testnet",
|
||||
"height": 530000,
|
||||
"hash": "001004e11f5b63e063ecb10711833e4d110ab167cd676144c11e979eb42179ce",
|
||||
"time": 1561488512,
|
||||
"tree": "010fc06d6576652186452691f69b55f315be91e07c7320065e4985164312604e700010018371952ba3c6f909c4f032fcc46aec898a9b468490c1225decf4f1d143717c180000000189f3389d4ff6319ec5f85e36be2391cbbbd800b3ffe615c57dde69eed21b323400000000013f9eebf69ce2789e1a05f9175ee82fcddf8b1efab5c0f7859a1b9efe1f42734f000145e3d4899fcd7f0f1236ae31eafb3f4b65ad6b11a17eae1729cec09bd3afa01a000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "testnet",
|
||||
"height": 540000,
|
||||
"hash": "0013e6d62a29fc081f10aa0cc4a03af305c9dc4dca317a4f7cd696c326c86ad4",
|
||||
"time": 1562523361,
|
||||
"tree": "01e446f3e79c822030dc4e1a13b2a3fdebd13f6cec365d1905f9e78245bac0d65a0186b3f5412e66792f9d44abd945b30d9bf7a56e9646a4c83dbfd363d60107911410013ba43319ed57963d602fc5723416045e8ab16fd942ba1386f8fdd74d0786104100017ad3a51ad25bf7ccef253e069dd4c2c4f1f8c8e385e51a7b72eee3e195fa6e49000001f102d5f41b6821ee0242953b07394c53416ee8360d7ffb5ce47c2d7f4d490c3a01b6cde3e9d07a442e79de9fc892b6ed18fcf83bf2a43e8d0ddc8ea3bd9fc9900f0000013f9eebf69ce2789e1a05f9175ee82fcddf8b1efab5c0f7859a1b9efe1f42734f000145e3d4899fcd7f0f1236ae31eafb3f4b65ad6b11a17eae1729cec09bd3afa01a000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "testnet",
|
||||
"height": 550000,
|
||||
"hash": "001bc53084578d3df818acef394453204790689af282b31f657f0da8b870d6fb",
|
||||
"time": 1563268291,
|
||||
"tree": "018008a9400be1be294647ae0505dd3d26e12701f58e958ee131c29c411ccd59530010019f1953b2e4615ce152fc70774d87424c01b887947bc076739039d4c5fe02a2700156e5a29f59819c780e1135312db8c1d9d5e8b79cbf36493e61bd4fa36981b83a000120bc90ef41127ebd83fa7671b2602dec62245c0722bd7f98c5521c0156d88c5600012369764ea6c7a8c633b70b574d448a17c6ffe9d8b80fa9da290345130a66af2f00018fd65606ffe7bc828fa2b2068b6f0c2be263b977ba583d987a56cdb8ee83933300013f9eebf69ce2789e1a05f9175ee82fcddf8b1efab5c0f7859a1b9efe1f42734f000145e3d4899fcd7f0f1236ae31eafb3f4b65ad6b11a17eae1729cec09bd3afa01a000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "testnet",
|
||||
"height": 560000,
|
||||
"hash": "0006b59014c7c8abfa0e7eaf5c759adde1ab2afa0aaba7160d86b745a99e6937",
|
||||
"time": 1564201759,
|
||||
"tree": "01484b1395a01be2beff0b528189aaa3adab099d926da39a50c5b5baffc9bfcf4001b2c4b97b06a53b61a5354de4f2cc793bec2b1e83cbd3ff652233776d1dc6e5181000000001fc142959f4fb4f53281b457a00a7239a1db6c2368fabc19ab546f7c49cb4ce2f01433fc9f38ad1f6b03f2b4467726ff8d6e8fb4f37f91d0bf360189ffbf86a1f640001c0486086941cf792a3b4e58e25667bd01cf1fd0c12c33735cc806fd2e2dcc946018fd65606ffe7bc828fa2b2068b6f0c2be263b977ba583d987a56cdb8ee83933300013f9eebf69ce2789e1a05f9175ee82fcddf8b1efab5c0f7859a1b9efe1f42734f000145e3d4899fcd7f0f1236ae31eafb3f4b65ad6b11a17eae1729cec09bd3afa01a000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "testnet",
|
||||
"height": 570000,
|
||||
"hash": "001db31c6aa784f883a413165ac558aa730bca423a352736b0b068d8988c8ad2",
|
||||
"time": 1565248604,
|
||||
"tree": "01f4422de604552bdac6948fc27edd0d3304da5406418c400df8778d992d234a63001000000134bced2c3e1770b9d403d67072bc8f5781de855f4950deda4f41b78305b1732801e43491dc39138114ea822504bbba18ba5395e6ae0f387dd1d9d9238bffe651590000000001fe587a9e45db504168d4cd71ba22f4013f9a4259280715f5c04b5809700fba42013f9eebf69ce2789e1a05f9175ee82fcddf8b1efab5c0f7859a1b9efe1f42734f000145e3d4899fcd7f0f1236ae31eafb3f4b65ad6b11a17eae1729cec09bd3afa01a000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "testnet",
|
||||
"height": 580000,
|
||||
"hash": "0002c477bac088303cb56f07a3c9f43ea7070d08205c4334aec8ca4ae6ff9002",
|
||||
"time": 1566344850,
|
||||
"tree": "0152358cb627d07252efcb0bb16f467fc531179132e754ca73372aa1614cbe5324001001061e91caaec882dc744e766e300296ec365b154ba0efae2e53f16825c4330c0b014661efd814f40a418879ee180167ef612de7fea3ef66ffc6e212be26e650ba52018bba4fe6f2ba22d41a2c79641a997e2e939322cc31c95c8071da78cc7eb8ef0d000116167b403c56651f18b0598de87867713170f35ef202b33f24aeb94d4b25193500000138cbc4cb015400c0436372d5583297ff9c1a3b2f7c99348afe44535cb3bdb33e000001c104705fac60a85596010e41260d07f3a64f38f37a112eaef41cd9d736edc5270145e3d4899fcd7f0f1236ae31eafb3f4b65ad6b11a17eae1729cec09bd3afa01a000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"height": 620000,
|
||||
"hash": "005f97953c8e1265d6b45f4435ffa32918e53e8f0025c286a4080c3eab167197",
|
||||
"time": 1569572035,
|
||||
"tree": "0170cf036ea1ea3c6e08432e18b6a372ca0b8b83671cc13ab0cf9e28c182f6c36f00100000013f3fc2c16ac4780f1c472ca65534ab08911f325a9edde5ea7f24364b47c9a95300017621b12e518cbbbdb7511ab423e0bddda412ed61ed3cff5be2140de65d6a0069010576153a5a2098812e7a028c37c3398e186f398c9b07bc199784ab97e5535c3e0000019a6ce2f0f7dbb2de493a315abf62d8ca96ccc701f116b6ddfae33870a2183d3c01c9d3564eff54ebc328eab2e4f1150c3637f4f47516f879a0cfebdf49fe7b1d5201c104705fac60a85596010e41260d07f3a64f38f37a112eaef41cd9d736edc5270145e3d4899fcd7f0f1236ae31eafb3f4b65ad6b11a17eae1729cec09bd3afa01a000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
|
||||
}
|
Loading…
Reference in New Issue